diff --git a/CLAUDE.md b/CLAUDE.md index 7bb0466..811f537 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,12 +24,17 @@ Remove BG / Generate / Tools, each owning one queue that may hold several convert groups at once. A selection never spans two convert groups, because one options panel can only describe one target set. See `src/shared/tabs.ts`. +**Signed-off redesign (2026-10-04):** `docs/mockups/terminal-v5/10-s2-vscode-grouped.html`, dark-only, +strict monochrome. Rules and feedback trail: `docs/design/redesign-direction.md`. + **The look is designed collaboratively with the owner. Make NO visual assumptions.** Before building or restyling any UI, present mockups (self-contained browser HTML, like the RCMM Show/Hide exploration), offer options, and iterate to explicit sign-off. This covers layout, components, palette, typography, motion, empty/loading states, and the file-preview -experience. The current renderer is a deliberately plain placeholder until that design work -happens. Engineering/plumbing (main process, tool modules, IPC, tests, packaging) moves fast +experience. The renderer implements the signed-off terminal design (rules in +`docs/design/redesign-direction.md`): dark only, strict monochrome, every colour is a token in +`src/renderer/src/theme/tokens.css`, and `test/monochrome-source.test.ts` fails on any other colour +literal. New screens still go through mockups first. Engineering/plumbing (main process, tool modules, IPC, tests, packaging) moves fast without design ceremony. ## Scope (v1, built in phases) @@ -57,7 +62,9 @@ src/ (planned) output.ts collision-safe output naming (ported from RCMM Get-UniqueOutPath) (planned) ipc.ts renderer <-> engine wiring preload/ contextBridge — the typed `window.filesmith` API - renderer/ React UI (placeholder until designed) + renderer/ React UI: shell/ (title bar, sidebar, status bar), queue/ (files table), + inspector/ (Options, Preview, Info), options/ (one settings file per verb), + views/ (Generate, Tools, Completed, Settings), ui/ (primitives), icons/, theme/ (tokens, CSS) shared/ types.ts — Job, ToolId, FileKind, Options, progress events tabs.ts — the VERB-first navigation model (rail tabs + Tools cards) resources/bin/ bundled CLI binaries (gitignored; fetched by scripts, packed by electron-builder) diff --git a/docs/design/redesign-direction.md b/docs/design/redesign-direction.md new file mode 100644 index 0000000..5fa67f3 --- /dev/null +++ b/docs/design/redesign-direction.md @@ -0,0 +1,34 @@ +# Redesign direction (owner feedback, 2026-10-04) + +**LOCKED IN (2026-10-04): `docs/mockups/terminal-v5/10-s2-vscode-grouped.html`** is the target design. +Build the renderer to match it. The rest of this file is the feedback trail that led there. + +Mockup rounds live in `docs/mockups/terminal*/` (each has an `index.html` gallery and `shots/`). + +- **Dark only.** Light mode is being removed. Black or near-black background. Themes come later. +- **Strict monochrome.** Greys only (r=g=b). Errors use a glyph and weight, never red. +- **Round 1** (`terminal/`): rejected. Too cluttered, too much information, too colourful. +- **Round 2** (`terminal-v2/`): better, but too minimal. Wants icons on tabs and controls. +- **Round 3** (`terminal-v3/`): "more the look", but a straight copy of Wind's settings page. Do our own thing. +- **Round 4** (`terminal-v4/`): IDE-inspired (VS Code settings editor, JetBrains settings/tool windows, Zed, + Xcode). Keep round 3's density, icons everywhere, simple and easy to navigate. +- **Pick from round 4:** `terminal-v4/10-explorer-details` (details table + right inspector). Wanted changes: + left sidebar collapsible, expanded shows icon + label; no "Drop files here" row (files are added from the + top). Variations in `terminal-v5/`. +- **Pick from v5:** `terminal-v5/05-e-flush-panels` (flush, square, mono). Controls must clearly read as + buttons: plain words with an underline or dotted link styling were not obvious enough. +- Running rows: no progress line under the row. Show progress compactly as `62%(4s)`, not "about 4 s left", + in the status column. +- Result column: the arrow sits on the size/result split. Done = actual result, running = grey `~estimate`, + queued/failed = empty. The batch estimate shows in the right panel before you press Convert. +- Post-build feedback (2026-10-04): Add files is grey (not white); no white left bars on selected rows or the + active sidebar item; the active item's count is a plain number, not an inverted box. Add files has no fill, + like the other toolbar buttons. No white fills anywhere: selected segments, chips, checkboxes and the primary + button use grey `#2a2a2a` with light text (the `--inv-*` tokens). The Tools tab is "PDF Tools", with no + divider line above it. +- Files toolbar is only Add files + count. Row actions live in a right-click menu (Open, Show in File + Explorer, Retry, Stop, Clear finished, Remove from list, Delete); destructive ones confirm first. While work + runs, the run button becomes a red Stop (`--stop-*`, the app's one hue). Preview/Info show nothing for a + multi-selection ("Select one file to preview"). +- Wind is a reference, not a template. Avoid its signatures: the floating progress+button pill, the icon-tile + header band, and big rounded cards of right-aligned setting rows. diff --git a/docs/mockups/terminal-v2/01-wind-plain.html b/docs/mockups/terminal-v2/01-wind-plain.html new file mode 100644 index 0000000..1dbc083 --- /dev/null +++ b/docs/mockups/terminal-v2/01-wind-plain.html @@ -0,0 +1,174 @@ + + + + + +Filesmith + + + +
+ + Filesmith +
+ + + +
+
+ +
+ + +
+

Convert

+ + + + + +
+ Format +
+ + + + +
+ Quality +
+
+ 80 +
+
+ + +
+
+ + diff --git a/docs/mockups/terminal-v2/02-wind-settings-card.html b/docs/mockups/terminal-v2/02-wind-settings-card.html new file mode 100644 index 0000000..4fd4009 --- /dev/null +++ b/docs/mockups/terminal-v2/02-wind-settings-card.html @@ -0,0 +1,249 @@ + + + + + + +Filesmith + + + +
+
+ + Filesmith +
+
+ + + +
+
+ +
+ + +
+
+

Convert

+ +
+
IMG_4821.HEICdone
+
screenshot.png62%
+
hero-banner.pngqueued
+
scan_0091.tiff✕ failed
+ +
+ +
+
+ Format +
+ +
+ + + + +
+
+
+
+ +
+ + 80 +
+
+
+ +
+
+
+
+ + + + diff --git a/docs/mockups/terminal-v2/03-wind-no-rail.html b/docs/mockups/terminal-v2/03-wind-no-rail.html new file mode 100644 index 0000000..b7b100b --- /dev/null +++ b/docs/mockups/terminal-v2/03-wind-no-rail.html @@ -0,0 +1,180 @@ + + + + + + +Filesmith, no rail + + + +
+ + Filesmith +
+ + + +
+
+ + + +
+

Convert

+ + +
+ drop files
+ +
+
+
+ + + + +
+
+ + 80 +
+
+
+ +
+
+ + diff --git a/docs/mockups/terminal-v2/04-wind-icon-rail.html b/docs/mockups/terminal-v2/04-wind-icon-rail.html new file mode 100644 index 0000000..c5e733e --- /dev/null +++ b/docs/mockups/terminal-v2/04-wind-icon-rail.html @@ -0,0 +1,153 @@ + + + + + +Filesmith, icon rail + + + +
+
+
Filesmith
+
+ + + +
+
+ +
+ + +
+

Convert

+ +
+
    +
  • IMG_4821.HEICdone
  • +
  • screenshot.png62%
  • +
  • hero-banner.pngqueued
  • +
  • scan_0091.tiff✕ failed
  • +
+ +
+
Format
+
+ + + + +
+ +
+
Quality 80
+
+ +
+
+ + +
+
+
+
+
+ + diff --git a/docs/mockups/terminal-v2/05-prompt-caret.html b/docs/mockups/terminal-v2/05-prompt-caret.html new file mode 100644 index 0000000..8f6fb25 --- /dev/null +++ b/docs/mockups/terminal-v2/05-prompt-caret.html @@ -0,0 +1,161 @@ + + + + + + +Filesmith Prompt + + + +
+ + Filesmith +
+ + + +
+
+ +
+ + +
+
+

convert

+ +
+
    +
  • IMG_4821.HEICdone
  • +
  • screenshot.png62%
  • +
  • hero-banner.pngqueued
  • +
  • scan_0091.tiff✕ failed
  • +
+ + +
+ + + + +
+ + +
+
+
+
+ + diff --git a/docs/mockups/terminal-v2/06-one-sentence.html b/docs/mockups/terminal-v2/06-one-sentence.html new file mode 100644 index 0000000..8ceb0a0 --- /dev/null +++ b/docs/mockups/terminal-v2/06-one-sentence.html @@ -0,0 +1,186 @@ + + + + + + +Filesmith One Sentence + + + +
+
+ + Filesmith +
+
+ + + +
+
+ +
+ + +
+ +

+ convert 4 files to at + + + + + + + +

+
+
+ + + + + diff --git a/docs/mockups/terminal-v2/07-verb-in-title.html b/docs/mockups/terminal-v2/07-verb-in-title.html new file mode 100644 index 0000000..80694eb --- /dev/null +++ b/docs/mockups/terminal-v2/07-verb-in-title.html @@ -0,0 +1,154 @@ + + + + + + +Filesmith + + + +
+
+ + Filesmith +
+
+ + + +
+
+ +
+
+ + + + + +
+
+ + + + +
+ +
+
+
+ + + diff --git a/docs/mockups/terminal-v2/08-verbs-at-bottom.html b/docs/mockups/terminal-v2/08-verbs-at-bottom.html new file mode 100644 index 0000000..99db75a --- /dev/null +++ b/docs/mockups/terminal-v2/08-verbs-at-bottom.html @@ -0,0 +1,125 @@ + + + + + +Filesmith, verbs at the bottom + + + +
+
+ + Filesmith +
+ + + +
+
+ +
+

Convert

+ + + + + +
+ + + + +
+ + +
+ + +
+ + diff --git a/docs/mockups/terminal-v2/09-one-batch-bar.html b/docs/mockups/terminal-v2/09-one-batch-bar.html new file mode 100644 index 0000000..8239441 --- /dev/null +++ b/docs/mockups/terminal-v2/09-one-batch-bar.html @@ -0,0 +1,128 @@ + + + + + + +Filesmith + + + +
+
+
+ + Filesmith +
+
+ + + +
+
+ +
+ + +
+
+

Convert

+ + + +
    +
  • IMG_4821.HEICdone
  • +
  • screenshot.pngrunning
  • +
  • hero-banner.pngqueued
  • +
  • scan_0091.tifffailed
  • +
+
+ +
+ + + + +
+ + +
+
+
+
+ + diff --git a/docs/mockups/terminal-v2/10-bracket-bar.html b/docs/mockups/terminal-v2/10-bracket-bar.html new file mode 100644 index 0000000..e33d456 --- /dev/null +++ b/docs/mockups/terminal-v2/10-bracket-bar.html @@ -0,0 +1,136 @@ + + + + + + +Filesmith Bracket Bar + + + +
+ + filesmith +
+ + + +
+
+ +
+ + +
+
+

convert

+ +
    +
  1. IMG_4821.HEICdone
  2. +
  3. screenshot.png[######····]
  4. +
  5. hero-banner.pngqueued
  6. +
  7. scan_0091.tifffailed
  8. +
+ +
+ + + + +
+ + +
+
+
+ + diff --git a/docs/mockups/terminal-v2/11-glyph-gutter.html b/docs/mockups/terminal-v2/11-glyph-gutter.html new file mode 100644 index 0000000..f7f2815 --- /dev/null +++ b/docs/mockups/terminal-v2/11-glyph-gutter.html @@ -0,0 +1,123 @@ + + + + + +Filesmith Glyph Column + + + +
+
+
+ + Filesmith +
+
+ + + +
+
+ +
+ + +
+

Convert

+ +
    +
  • IMG_4821.HEIC
  • +
  • + + screenshot.png
  • +
  • hero-banner.png
  • +
  • scan_0091.tiff
  • +
  • +
+ +
+ + + + +
+ + +
+
+
+ + diff --git a/docs/mockups/terminal-v2/12-heavy-rule.html b/docs/mockups/terminal-v2/12-heavy-rule.html new file mode 100644 index 0000000..cf885fc --- /dev/null +++ b/docs/mockups/terminal-v2/12-heavy-rule.html @@ -0,0 +1,108 @@ + + + + + + +Filesmith Heavy Rule + + + +
+
+ + Filesmith +
+ + + +
+
+ +
+

+ + + + + + + +

+ + + + +
+ + + + +
+ + +
+
+ + diff --git a/docs/mockups/terminal-v2/13-big-verb.html b/docs/mockups/terminal-v2/13-big-verb.html new file mode 100644 index 0000000..ebca730 --- /dev/null +++ b/docs/mockups/terminal-v2/13-big-verb.html @@ -0,0 +1,140 @@ + + + + + + +Filesmith Big Verb + + + +
+
+
+ + Filesmith +
+
+ + + +
+
+ +
+ + +
+

Convert

+ +
    +
  • IMG_4821.HEICdone
  • +
  • screenshot.png62%
  • +
  • hero-banner.pngqueued
  • +
  • scan_0091.tiff✕ failed
  • +
+ +
+
+ + + + +
+ +
+
+
+
+ + diff --git a/docs/mockups/terminal-v2/14-two-halves.html b/docs/mockups/terminal-v2/14-two-halves.html new file mode 100644 index 0000000..1c2c2e1 --- /dev/null +++ b/docs/mockups/terminal-v2/14-two-halves.html @@ -0,0 +1,114 @@ + + + + + +Filesmith + + + +
+ + Filesmith + +
+ + + +
+
+ + +
+ +
+
+ + + + +
+ +
+
+ + diff --git a/docs/mockups/terminal-v2/15-key-value-grid.html b/docs/mockups/terminal-v2/15-key-value-grid.html new file mode 100644 index 0000000..c2ea561 --- /dev/null +++ b/docs/mockups/terminal-v2/15-key-value-grid.html @@ -0,0 +1,159 @@ + + + + + +Filesmith + + + +
+ + Filesmith +
+ + + +
+
+ +
+ + +
+

Convert

+ +
+ IMG_4821.HEIC done + screenshot.png 62% + hero-banner.png queued + scan_0091.tiff failed + + + + format +
+ + + + +
+ quality +
+ +
+
+
+
+ + + diff --git a/docs/mockups/terminal-v2/16-drop-frame.html b/docs/mockups/terminal-v2/16-drop-frame.html new file mode 100644 index 0000000..e42d706 --- /dev/null +++ b/docs/mockups/terminal-v2/16-drop-frame.html @@ -0,0 +1,183 @@ + + + + + +Filesmith + + + +
+ + Filesmith +
+ + + +
+
+ +
+ + +
+

Convert

+
+
+
    +
  • IMG_4821.HEICdone
  • +
  • screenshot.png62%
  • +
  • hero-banner.pngqueued
  • +
  • scan_0091.tifffailed
  • +
+
drop files or 
+
+ +
+
+ + + + +
+ +
+
+
+
+ + diff --git a/docs/mockups/terminal-v2/17-inversion-only.html b/docs/mockups/terminal-v2/17-inversion-only.html new file mode 100644 index 0000000..a5b90f2 --- /dev/null +++ b/docs/mockups/terminal-v2/17-inversion-only.html @@ -0,0 +1,119 @@ + + + + + +Filesmith – Inversion + + + +
+
+ filesmith +
+ + + +
+
+ +
+ + +
+
+

convert

+ +
    +
  • IMG_4821.HEICdone
  • +
  • screenshot.png62%
  • +
  • hero-banner.pngqueued
  • +
  • scan_0091.tifffailed
  • +
+ +
+ + + + +
+ + +
+
+
+
+ + diff --git a/docs/mockups/terminal-v2/18-right-rail.html b/docs/mockups/terminal-v2/18-right-rail.html new file mode 100644 index 0000000..54e04ce --- /dev/null +++ b/docs/mockups/terminal-v2/18-right-rail.html @@ -0,0 +1,124 @@ + + + + + + +Filesmith Right Rail + + + +
+
+ + Filesmith +
+ + + +
+
+ +
+
+

Convert

+ +
    +
  • IMG_4821.HEICdone
  • +
  • screenshot.png62%
  • +
  • hero-banner.pngqueued
  • +
  • scan_0091.tifffailed
  • +
+ + +
+ + + + +
+ + +
+ + +
+
+ + diff --git a/docs/mockups/terminal-v2/19-one-number.html b/docs/mockups/terminal-v2/19-one-number.html new file mode 100644 index 0000000..affb3ce --- /dev/null +++ b/docs/mockups/terminal-v2/19-one-number.html @@ -0,0 +1,150 @@ + + + + + + +Filesmith + + + +
+
+
+ + Filesmith +
+
+ + + +
+
+ +
+ + +
+
    +
  • IMG_4821.HEIC
  • +
  • screenshot.png62%
  • +
  • hero-banner.png
  • +
  • scan_0091.tifffailed
  • +
+ +
+
+ + + + +
+ +
+
+
+
+ + diff --git a/docs/mockups/terminal-v2/20-empty-until-dropped.html b/docs/mockups/terminal-v2/20-empty-until-dropped.html new file mode 100644 index 0000000..256fdd1 --- /dev/null +++ b/docs/mockups/terminal-v2/20-empty-until-dropped.html @@ -0,0 +1,125 @@ + + + + + + +Filesmith + + + +
+ + Filesmith +
+ + + +
+
+ +
+ + +
+
+
    +
  1. IMG_4821.HEICdone
  2. +
  3. screenshot.png62%
  4. +
  5. hero-banner.pngqueued
  6. +
  7. scan_0091.tifffailed
  8. +
+ + + +
+
+ + + + +
+ +
+
+
+
+ + + + diff --git a/docs/mockups/terminal-v2/index.html b/docs/mockups/terminal-v2/index.html new file mode 100644 index 0000000..ffa5961 --- /dev/null +++ b/docs/mockups/terminal-v2/index.html @@ -0,0 +1,40 @@ +Terminal v2 mockups + +

Filesmith, terminal v2

20 monochrome directions, fewer things on screen. Click one to open it.

+
+Wind, plain
01Wind, plain

Wind's Settings shell with nothing extra added. A left rail, a title, four lines and one white pill. It looks like Wind's sibling app.

+Wind, settings card
02Wind, settings card

The queue and the options sit in Wind-style cards, so the convert page reads like a settings page.

+Wind, no rail
03Wind, no rail

Wind's tokens, but the rail is removed. The verbs become a quiet word row under the title bar, which frees the whole width for black space.

+Wind, icon rail
04Wind, icon rail

The closest to a shipping desktop app: a Wind rail with small 1.5px icons beside each verb, and the content kept very sparse.

+Prompt
05Prompt

The title is a prompt: '› convert_' with a slow blinking underscore. That one device carries the terminal feel.

+One sentence
06One sentence

The options read as one line: 'convert 4 files to webp at 80'. The underlined words are the controls.

+Verb in the title
07Verb in the title

No rail at all. The title 'Convert ⌄' is the navigation, and clicking it drops the 7 verbs.

+Verbs at the bottom
08Verbs at the bottom

The work sits at the top of the window and the 7 verbs rest along the bottom edge like a quiet dock line.

+One bar
09One bar

The rows show only a word each. Progress for the whole batch is one 1px white line under the queue.

+Bracket bar
10Bracket bar

Only the running file shows a text bar, '[######····]'. That bracketed bar is the only typographic device.

+Glyph column
11Glyph column

Status is a single glyph in a left gutter, '✓ ○ · ✕', so the right side of every row stays empty.

+One heavy rule
12One heavy rule

A single 2px white rule under the title is the only line on screen. Everything else floats.

+Big verb
13Big verb

The verb is the hero: 'Convert' set at 88px light in the top-left. The rest is small and quiet.

+Two halves
14Two halves

Files on the left half and the decision on the right. Each half has one job, separated by black space only.

+Key and value
15Key and value

Everything is a two-column mono list: names with statuses, and settings with values, all on one alignment line.

+Drop frame
16Drop frame

The queue lives inside a large dashed region. The drop target and the list are the same object.

+Inversion
17Inversion

Inversion is the only emphasis anywhere: the active verb, the chosen format and the failed word are small black-on-light blocks. No pills, no lines, no bold.

+Right rail
18Right rail

Content first: the work starts at the left edge and the verbs sit in a thin right-hand list.

+One number
19One number

Rows carry only names, dimmed when done. A single large '62%' beside the running row is the one live number on screen.

+Nothing until dropped
20Nothing until dropped

The resting screen is almost blank: the verb list and one centred line 'drop files'. The queue, format and button appear only once files arrive.

+
\ No newline at end of file diff --git a/docs/mockups/terminal-v2/shots/01-wind-plain.png b/docs/mockups/terminal-v2/shots/01-wind-plain.png new file mode 100644 index 0000000..259f861 Binary files /dev/null and b/docs/mockups/terminal-v2/shots/01-wind-plain.png differ diff --git a/docs/mockups/terminal-v2/shots/02-wind-settings-card.png b/docs/mockups/terminal-v2/shots/02-wind-settings-card.png new file mode 100644 index 0000000..23480d5 Binary files /dev/null and b/docs/mockups/terminal-v2/shots/02-wind-settings-card.png differ diff --git a/docs/mockups/terminal-v2/shots/03-wind-no-rail.png b/docs/mockups/terminal-v2/shots/03-wind-no-rail.png new file mode 100644 index 0000000..2bd04a9 Binary files /dev/null and b/docs/mockups/terminal-v2/shots/03-wind-no-rail.png differ diff --git a/docs/mockups/terminal-v2/shots/04-wind-icon-rail.png b/docs/mockups/terminal-v2/shots/04-wind-icon-rail.png new file mode 100644 index 0000000..bb391ed Binary files /dev/null and b/docs/mockups/terminal-v2/shots/04-wind-icon-rail.png differ diff --git a/docs/mockups/terminal-v2/shots/05-prompt-caret.png b/docs/mockups/terminal-v2/shots/05-prompt-caret.png new file mode 100644 index 0000000..f790014 Binary files /dev/null and b/docs/mockups/terminal-v2/shots/05-prompt-caret.png differ diff --git a/docs/mockups/terminal-v2/shots/06-one-sentence.png b/docs/mockups/terminal-v2/shots/06-one-sentence.png new file mode 100644 index 0000000..b9293ca Binary files /dev/null and b/docs/mockups/terminal-v2/shots/06-one-sentence.png differ diff --git a/docs/mockups/terminal-v2/shots/07-verb-in-title.png b/docs/mockups/terminal-v2/shots/07-verb-in-title.png new file mode 100644 index 0000000..f5d5821 Binary files /dev/null and b/docs/mockups/terminal-v2/shots/07-verb-in-title.png differ diff --git a/docs/mockups/terminal-v2/shots/08-verbs-at-bottom.png b/docs/mockups/terminal-v2/shots/08-verbs-at-bottom.png new file mode 100644 index 0000000..169ad09 Binary files /dev/null and b/docs/mockups/terminal-v2/shots/08-verbs-at-bottom.png differ diff --git a/docs/mockups/terminal-v2/shots/09-one-batch-bar.png b/docs/mockups/terminal-v2/shots/09-one-batch-bar.png new file mode 100644 index 0000000..f191ab7 Binary files /dev/null and b/docs/mockups/terminal-v2/shots/09-one-batch-bar.png differ diff --git a/docs/mockups/terminal-v2/shots/10-bracket-bar.png b/docs/mockups/terminal-v2/shots/10-bracket-bar.png new file mode 100644 index 0000000..e656b2f Binary files /dev/null and b/docs/mockups/terminal-v2/shots/10-bracket-bar.png differ diff --git a/docs/mockups/terminal-v2/shots/11-glyph-gutter.png b/docs/mockups/terminal-v2/shots/11-glyph-gutter.png new file mode 100644 index 0000000..0842e47 Binary files /dev/null and b/docs/mockups/terminal-v2/shots/11-glyph-gutter.png differ diff --git a/docs/mockups/terminal-v2/shots/12-heavy-rule.png b/docs/mockups/terminal-v2/shots/12-heavy-rule.png new file mode 100644 index 0000000..45ec198 Binary files /dev/null and b/docs/mockups/terminal-v2/shots/12-heavy-rule.png differ diff --git a/docs/mockups/terminal-v2/shots/13-big-verb.png b/docs/mockups/terminal-v2/shots/13-big-verb.png new file mode 100644 index 0000000..6687901 Binary files /dev/null and b/docs/mockups/terminal-v2/shots/13-big-verb.png differ diff --git a/docs/mockups/terminal-v2/shots/14-two-halves.png b/docs/mockups/terminal-v2/shots/14-two-halves.png new file mode 100644 index 0000000..17c8be1 Binary files /dev/null and b/docs/mockups/terminal-v2/shots/14-two-halves.png differ diff --git a/docs/mockups/terminal-v2/shots/15-key-value-grid.png b/docs/mockups/terminal-v2/shots/15-key-value-grid.png new file mode 100644 index 0000000..2c08f89 Binary files /dev/null and b/docs/mockups/terminal-v2/shots/15-key-value-grid.png differ diff --git a/docs/mockups/terminal-v2/shots/16-drop-frame.png b/docs/mockups/terminal-v2/shots/16-drop-frame.png new file mode 100644 index 0000000..e37febf Binary files /dev/null and b/docs/mockups/terminal-v2/shots/16-drop-frame.png differ diff --git a/docs/mockups/terminal-v2/shots/17-inversion-only.png b/docs/mockups/terminal-v2/shots/17-inversion-only.png new file mode 100644 index 0000000..508a88d Binary files /dev/null and b/docs/mockups/terminal-v2/shots/17-inversion-only.png differ diff --git a/docs/mockups/terminal-v2/shots/18-right-rail.png b/docs/mockups/terminal-v2/shots/18-right-rail.png new file mode 100644 index 0000000..09dda9c Binary files /dev/null and b/docs/mockups/terminal-v2/shots/18-right-rail.png differ diff --git a/docs/mockups/terminal-v2/shots/19-one-number.png b/docs/mockups/terminal-v2/shots/19-one-number.png new file mode 100644 index 0000000..d116322 Binary files /dev/null and b/docs/mockups/terminal-v2/shots/19-one-number.png differ diff --git a/docs/mockups/terminal-v2/shots/20-empty-until-dropped.png b/docs/mockups/terminal-v2/shots/20-empty-until-dropped.png new file mode 100644 index 0000000..786ef43 Binary files /dev/null and b/docs/mockups/terminal-v2/shots/20-empty-until-dropped.png differ diff --git a/docs/mockups/terminal-v2/shots/_index.png b/docs/mockups/terminal-v2/shots/_index.png new file mode 100644 index 0000000..aa9e65e Binary files /dev/null and b/docs/mockups/terminal-v2/shots/_index.png differ diff --git a/docs/mockups/terminal-v3/01-wind-classic.html b/docs/mockups/terminal-v3/01-wind-classic.html new file mode 100644 index 0000000..66490f3 --- /dev/null +++ b/docs/mockups/terminal-v3/01-wind-classic.html @@ -0,0 +1,305 @@ + + + + + + +Filesmith Wind Classic + + + +
+
+ +
Filesmith
+
+ + + +
+
+ +
+ + +
+
+
+

Convert

Change images to another format.

+
+ 6 files + +
+
+ +
+
+
Queue2 of 6 done
+
+
+
+
IMG_4821.HEIC3.2 MB
+
640 KB
+
+ + +
+
+
+
+
product-shot.jpg2.1 MB
+
486 KB
+
+ + +
+
+
+
+
screenshot-2026-10-03.png1.8 MB
+
62%
+
+ +
+
+
+
+
hero-banner@2x.png4.6 MB
+
queued
+
+ +
+
+
+
+
scan_0091.tiff22.4 MB
+
Unsupported compression
+
+ + +
+
+
+
+
logo-final.bmp1.1 MB
+
queued
+
+ +
+
+
+ +
+ +
+
Options
+
+
+
Format.webp
+
+ + + + + + +
+
+
+
Quality
+
+ + + +
+
+
+
Output
+
+ + +
+
+
+
Keep metadataCamera info and color profile
+ +
+
+ +
+
+ + +
+
+
+ + + diff --git a/docs/mockups/terminal-v3/02-wind-stacked.html b/docs/mockups/terminal-v3/02-wind-stacked.html new file mode 100644 index 0000000..6f85a92 --- /dev/null +++ b/docs/mockups/terminal-v3/02-wind-stacked.html @@ -0,0 +1,291 @@ + + + + + +Filesmith Wind Stacked + + + +
+
+ + Filesmith +
+ + + +
+
+ +
+ + +
+
+
+ +
+

Convert

+

Change images to another format.

+
+
+ 6 files + +
+
+
+ +
+
Queue1 failed
+
+
+ +
IMG_4821.HEIC3.2 MB
+
640 KB
+
+ + +
+
+
+ +
product-shot.jpg2.1 MB
+
486 KB
+
+ + +
+
+
+ +
screenshot-2026-10-03.png1.8 MB
+
62%
+
+ +
+
+
+ +
hero-banner@2x.png4.6 MB
+
queued
+
+ +
+
+
+ +
scan_0091.tiff22.4 MB
+
Unsupported compression
+
+ + +
+
+
+ +
logo-final.bmp1.1 MB
+
queued
+
+ +
+
+
+ +
Options.webp
+
+
+
FormatWebP, small and widely supported
+
+ + + + + + +
+
+
+
QualityBalanced, sharp at a fraction of the size
+
+ + + +
+
+
+
OutputWhere converted files are saved
+
+ + +
+
+
+
Keep metadataCamera data, dates and color profile
+ +
+
+
+ +
+
+ 2 of 6 done + +
+
+
+
+
+ + + diff --git a/docs/mockups/terminal-v3/03-wind-tiled-nav.html b/docs/mockups/terminal-v3/03-wind-tiled-nav.html new file mode 100644 index 0000000..61e8d80 --- /dev/null +++ b/docs/mockups/terminal-v3/03-wind-tiled-nav.html @@ -0,0 +1,326 @@ + + + + + +Filesmith, Wind Tiled Nav + + + + + +
+
+ + Filesmith + convert +
+ + + +
+
+ +
+ + +
+
+
+
+

Convert

+

Change images to another format.

+
+ 6 files + +
+ +
+
+

Queue

2 of 6 done
+
    +
  • + +
    IMG_4821.HEIC
    3.2 MB
    +
    640 KB
    +
    +
  • +
  • + +
    product-shot.jpg
    2.1 MB
    +
    486 KB
    +
    +
  • +
  • + +
    screenshot-2026-10-03.png
    1.8 MB
    +
    62%
    +
    +
  • +
  • + +
    hero-banner@2x.png
    4.6 MB
    +
    queued
    +
    +
  • +
  • + +
    scan_0091.tiff
    22.4 MB
    +
    Unsupported compression
    +
    +
  • +
  • + +
    logo-final.bmp
    1.1 MB
    +
    queued
    +
    +
  • +
+ +
+ + +
+ + +
+
+
+ + + diff --git a/docs/mockups/terminal-v3/04-wind-grouped-rail.html b/docs/mockups/terminal-v3/04-wind-grouped-rail.html new file mode 100644 index 0000000..4370f4c --- /dev/null +++ b/docs/mockups/terminal-v3/04-wind-grouped-rail.html @@ -0,0 +1,318 @@ + + + + + +Filesmith, Wind Grouped Rail + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + Filesmith +
+
+ + + +
+
+ +
+ + +
+
+
+
+

Convert

+

Change images to another format.

+
+ 6 files + +
+ +
+ +
+
Queue2 of 6 done
+
+
+
+
IMG_4821.HEIC
3.2 MB
+
640 KB
+
+
+
+
+
product-shot.jpg
2.1 MB
+
486 KB
+
+
+
+
+
screenshot-2026-10-03.png
1.8 MB
+
62%
+
+
+
+
hero-banner@2x.png
4.6 MB
+
queued
+
+
+
+
+
scan_0091.tiff
22.4 MB
+
Unsupported compression
+
+
+
+
logo-final.bmp
1.1 MB
+
queued
+
+
+
Drop more files here
+
+
+ + +
+
Format.webp
+
+
+ + +
+
+ +
+
Quality and output
+
+
Quality
+
+
+
Save to
+ + +
+ +
+
Metadata
+
+
Keep metadataCamera info and color profile
+ +
+ +
+ +
+
+ + +
+
+
+ + diff --git a/docs/mockups/terminal-v3/05-wind-command-header.html b/docs/mockups/terminal-v3/05-wind-command-header.html new file mode 100644 index 0000000..a443521 --- /dev/null +++ b/docs/mockups/terminal-v3/05-wind-command-header.html @@ -0,0 +1,311 @@ + + + + + +Filesmith – Wind Command Header + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + Filesmith + +
+ + + +
+
+ +
+ + +
+
+
+
+
+

Convert

+

Change images to another format.

+
+
+ 6files + + +
+
+
+ 2 of 6 done +
+ 44% +
+
+ +
+
+
Queue1 failed
+
+
+
+
IMG_4821.HEIC
+
3.2 MB→640 KB
+
+
+
+
+
product-shot.jpg
+
2.1 MB→486 KB
+
+
+
+
+
screenshot-2026-10-03.png1.8 MB
+
62%
+
+
+
+
+
hero-banner@2x.png4.6 MB
+
queued
+
+
+
+
+
scan_0091.tiff22.4 MB
+
Unsupported compression
+
+
+
+
+
logo-final.bmp1.1 MB
+
queued
+
+
+ +
+
+ + +
+
+
+ + +
+ + diff --git a/docs/mockups/terminal-v3/06-linear-inspector.html b/docs/mockups/terminal-v3/06-linear-inspector.html new file mode 100644 index 0000000..c580164 --- /dev/null +++ b/docs/mockups/terminal-v3/06-linear-inspector.html @@ -0,0 +1,352 @@ + + + + + + +Filesmith Linear Inspector + + + + + + + + +
+
+
+ + Filesmith +
+
+
+ + + +
+
+ +
+ + + + +
+
+
+
+

Convert

+

Change images to another format.

+
+
+ 6 files + +
+
+ +
NameSizeStatus
+ +
+
+ + IMG_4821.HEIC + 3.2 MB + 640 KB + +
+
+ + product-shot.jpg + 2.1 MB + 486 KB + +
+
+ + screenshot-2026-10-03.png + 1.8 MB + 62% + +
+
+ + hero-banner@2x.png + 4.6 MB + queued + +
+
+ + scan_0091.tiff + 22.4 MB + Unsupported compression + +
+
+ + logo-final.bmp + 1.1 MB + queued + +
+
+ + + +
+ Converting + 1 failed +
+
+ + + +
+
+ + + diff --git a/docs/mockups/terminal-v3/07-raycast-panel.html b/docs/mockups/terminal-v3/07-raycast-panel.html new file mode 100644 index 0000000..ebe461e --- /dev/null +++ b/docs/mockups/terminal-v3/07-raycast-panel.html @@ -0,0 +1,349 @@ + + + + + +Filesmith Raycast Panel + + + + + + + +
+
+
+ + Filesmith +
+
+ + + +
+
+ +
+
+ + +
+
+
+
+

Convert

+

Change images to another format.

+
+
+ 6 files + +
+
+ +
+
+
Queue1 failed
+
+
+
+
IMG_4821.HEIC
3.2 MB
+
+
3.2 MB → 640 KB
+
+
+
+
+
+
product-shot.jpg
2.1 MB
+
+
2.1 MB → 486 KB
+
+
+
+
+
+
screenshot-2026-10-03.png
1.8 MB
+
+
62%
+
+
+
+
+
+
hero-banner@2x.png
4.6 MB
+
+
queued
+
+
+
+
+
+
scan_0091.tiff
22.4 MB
+
+
Unsupported compression
+
+
+
+
+
logo-final.bmp
1.1 MB
+
+
queued
+
+
+
+
+ +
+ + +
+ + +
+
+
+
+ + + diff --git a/docs/mockups/terminal-v3/08-zed-activity-bar.html b/docs/mockups/terminal-v3/08-zed-activity-bar.html new file mode 100644 index 0000000..dd2469a --- /dev/null +++ b/docs/mockups/terminal-v3/08-zed-activity-bar.html @@ -0,0 +1,334 @@ + + + + + +Filesmith 08 Zed Activity Bar + + + + + + + + +
+
+ +
Filesmith
+
convert
+
+ + + +
+
+ +
+ + +
+
+
+

Convert

+
Change images to another format.
+
+ 6 files + +
+
+ +
+
+
Queue2 of 6 done
+
+
+
+
+
IMG_4821.HEIC
3.2 MB
+
640 KB +
+
+
+
+
product-shot.jpg
2.1 MB
+
486 KB +
+
+
+
+
screenshot-2026-10-03.png
1.8 MB
+
62% +
+
+
+
+
hero-banner@2x.png
4.6 MB
+
queued +
+
+
+
+
scan_0091.tiff
22.4 MB
+
Unsupported compression +
+
+
+
+
logo-final.bmp
1.1 MB
+
queued +
+
+
+ +
+ 35.2 MB total + +
+
+
+ + +
+
+
+ + +
+ + + diff --git a/docs/mockups/terminal-v3/09-top-icon-tabs.html b/docs/mockups/terminal-v3/09-top-icon-tabs.html new file mode 100644 index 0000000..c651a3b --- /dev/null +++ b/docs/mockups/terminal-v3/09-top-icon-tabs.html @@ -0,0 +1,288 @@ + + + + + +Filesmith Top Tabs + + + +
+ +
+ + Filesmith + + + + +
+ + + +
+
+
+

Convert

Change images to another format.

+ + 6 files + +
+ +
+ +
+
Queue2 of 6 done
+
+
+
+
+
IMG_4821.HEIC
3.2 MB
+
640 KB
+
+
+
+
+
product-shot.jpg
2.1 MB
+
486 KB
+
+
+
+
+
screenshot-2026-10-03.png
1.8 MB
+
62%
+
+
+
+
+
hero-banner@2x.png
4.6 MB
+
queued
+
+
+
+
+
scan_0091.tiff
22.4 MB
+
Unsupported compression
+
+
+
+
+
logo-final.bmp
1.1 MB
+
queued
+
+
+
+ +
+
+ + + +
+ + +
+
+ + + diff --git a/docs/mockups/terminal-v3/10-segmented-switcher.html b/docs/mockups/terminal-v3/10-segmented-switcher.html new file mode 100644 index 0000000..c61e961 --- /dev/null +++ b/docs/mockups/terminal-v3/10-segmented-switcher.html @@ -0,0 +1,342 @@ + + + + + +Filesmith, Segmented Switcher + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + Filesmith + + + + +
+ + + +
+
+
+
+
+

Convert

+

Change images to another format.

+
+ + 6 files + +
+
+
+ +
+
+
Queue2 of 6 done
+
+
+
+
IMG_4821.HEIC
3.2 MB
+
+ 3.2 MB →640 KB +
+
+
+
+
+
product-shot.jpg
2.1 MB
+
+ 2.1 MB →486 KB +
+
+
+
+
+
screenshot-2026-10-03.png
1.8 MB
+
+ 62% +
+
+
+
+
hero-banner@2x.png
4.6 MB
+
+ queued +
+
+
+
+
+
scan_0091.tiff
22.4 MB
+
+ Unsupported compression + +
+
+
+
+
logo-final.bmp
1.1 MB
+
+ queued +
+
+
+
Drop more files here
+
+
+ + +
+
+
+ + +
+ + diff --git a/docs/mockups/terminal-v3/11-bottom-dock.html b/docs/mockups/terminal-v3/11-bottom-dock.html new file mode 100644 index 0000000..6c3d778 --- /dev/null +++ b/docs/mockups/terminal-v3/11-bottom-dock.html @@ -0,0 +1,291 @@ + + + + + +Filesmith - Bottom Dock + + + +
+ +
+ +
Filesmith
+
convert
+
+ + + +
+
+ +
+
+
+
+

Convert

+

Change images to another format.

+
+
+ 6 files + +
+
+ +
+
+ + +
+
Queue2 of 6 done
+
+
+
+
+
IMG_4821.HEIC
3.2 MB
+
640 KB
+
+ + +
+
+
+
+
product-shot.jpg
2.1 MB
+
486 KB
+
+ + +
+
+
+
+
screenshot-2026-10-03.png
1.8 MB
+
62%
+
+ +
+
+
+
+
hero-banner@2x.png
4.6 MB
+
queued
+
+ +
+
+
+
+
scan_0091.tiff
22.4 MB
+
Unsupported compression
+
+ + +
+
+
+
+
logo-final.bmp
1.1 MB
+
queued
+
+ +
+
+
+ +
+
+ + + + +
+
+ + +
+ +
+ +
+ + diff --git a/docs/mockups/terminal-v3/12-thumb-grid.html b/docs/mockups/terminal-v3/12-thumb-grid.html new file mode 100644 index 0000000..7a5f4bb --- /dev/null +++ b/docs/mockups/terminal-v3/12-thumb-grid.html @@ -0,0 +1,316 @@ + + + + + +Filesmith, Thumbnail Grid + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
Filesmith
+
convert
+ + + +
+ +
+ + +
+
+
+

Convert

Change images to another format.

+ 6 files + +
+ +
+
+
Queue2 of 6 done
+
+
+
HEIC +
+
+
IMG_4821.HEIC
3.2 MB
+
Done640 KB
+
+ +
+
JPG +
+
+
product-shot.jpg
2.1 MB
+
Done486 KB
+
+ +
+
PNG
+
+
+
screenshot-2026-10-03.png
1.8 MB
+
62%
+
+ +
+
PNG
+
+
+
hero-banner@2x.png
4.6 MB
+
Queued
+
+ +
+
TIFF
+
+
+
scan_0091.tiff
22.4 MB
+
Unsupported compression
+
+ +
+
BMP
+
+
+
logo-final.bmp
1.1 MB
+
Queued
+
+
+ +
+ + +
+
+
+ + +
+ + diff --git a/docs/mockups/terminal-v3/13-quiet-table.html b/docs/mockups/terminal-v3/13-quiet-table.html new file mode 100644 index 0000000..b4e1e11 --- /dev/null +++ b/docs/mockups/terminal-v3/13-quiet-table.html @@ -0,0 +1,360 @@ + + + + + +Filesmith Quiet Table + + + + + + + + +
+
+ + Filesmith + + + + +
+ +
+ + +
+
+
+
+

Convert

+

Change images to another format.

+
+ + 6 files + +
+ +
+
+
Queue2 of 6 done
+
+
+ Name + Size + Result + Status + +
+ +
+
IMG_4821.HEIC
+ 3.2 MB + 640 KB + Done +
+
+ +
+
product-shot.jpg
+ 2.1 MB + 486 KB + Done +
+
+ +
+
screenshot-2026-10-03.png
+ 1.8 MB + – + 62% +
+
+ +
+
hero-banner@2x.png
+ 4.6 MB + – + Queued +
+
+ +
+
scan_0091.tiff
+ 22.4 MB + – + Unsupported compression +
+
+ +
+
logo-final.bmp
+ 1.1 MB + – + Queued +
+
+ + +
+
+ +
+
Options
+
+
+
Format.webp
+
+ + + + + + +
+
+
+
Quality
+
+ + + +
+
+
+
Output
+
+ + +
+
+
+
Keep metadata
Dates, camera and color profile
+ +
+
+ +
+
+
+
+ + +
+
+
+ + + diff --git a/docs/mockups/terminal-v3/14-prompt-title.html b/docs/mockups/terminal-v3/14-prompt-title.html new file mode 100644 index 0000000..c72844f --- /dev/null +++ b/docs/mockups/terminal-v3/14-prompt-title.html @@ -0,0 +1,315 @@ + + + + + +Filesmith Prompt Title + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + Filesmith + convert +
+ + + +
+
+ +
+ + +
+
+
+
+

>convert

+

Change images to another format.

+
+
+ 6 files + +
+
+
+ +
+
+
#queue2 of 6 done
+
+
+
+
IMG_4821.HEIC
3.2 MB
+
640 KB
+
+
+
+
+
product-shot.jpg
2.1 MB
+
486 KB
+
+
+
+
+
screenshot-2026-10-03.png
1.8 MB
+
62%
+
+
+
+
hero-banner@2x.png
4.6 MB
+
queued
+
+
+
+
+
scan_0091.tiff
22.4 MB
+
Unsupported compression
+
+
+
+
logo-final.bmp
1.1 MB
+
queued
+
+
+
+ +
+ +
+
#options
+
+
+
format.webp
+
+ + +
+
+
+
quality
+
+
+
+
output
+
+ + +
+
+
+
metadata
Keep EXIF, colour profile, dates
+ +
+
+ +
+
+ + +
+
+
+ + diff --git a/docs/mockups/terminal-v3/15-glyph-gutter.html b/docs/mockups/terminal-v3/15-glyph-gutter.html new file mode 100644 index 0000000..fa8bafd --- /dev/null +++ b/docs/mockups/terminal-v3/15-glyph-gutter.html @@ -0,0 +1,318 @@ + + + + + +Filesmith – Glyph Gutter + + + +
+ +
+ +
Filesmith
+
convert
+
+ + + +
+
+ +
+ + +
+
+
+
+

Convert

+

Change images to another format.

+
+
+ 6 files + +
+ +
+ +
+
Queue2 of 6 done
+
+ +
+
+
+
IMG_4821.HEIC
3.2 MB
+
done640 KB
+
+
+ +
+
+
+
product-shot.jpg
2.1 MB
+
done486 KB
+
+
+ +
+
+
+
screenshot-2026-10-03.png
1.8 MB
+
62%
+
+
+ +
+
+
+
hero-banner@2x.png
4.6 MB
+
queued
+
+
+ +
+
+
+
scan_0091.tiff
22.4 MB
+
Unsupported compression
+
+
+ +
+
+
+
logo-final.bmp
1.1 MB
+
queued
+
+
+ +
+
+
Drop more files here
+
+
+
+ + + +
+ + +
+
+
+ + + diff --git a/docs/mockups/terminal-v3/16-drop-frame.html b/docs/mockups/terminal-v3/16-drop-frame.html new file mode 100644 index 0000000..ce09a9e --- /dev/null +++ b/docs/mockups/terminal-v3/16-drop-frame.html @@ -0,0 +1,325 @@ + + + + + + +Filesmith Drop Frame + + + +
+ +
+ + Filesmith + convert +
+ + + +
+
+ +
+ + +
+
+
+
+

Convert

+

Change images to another format.

+
+
+ 6 files + +
+
+ +
+
+
Queue2 of 6 done
+
+ + +
+ +
+
+
IMG_4821.HEIC
3.2 MB
+
640 KB
+
+ + +
+
+ +
+
+
product-shot.jpg
2.1 MB
+
486 KB
+
+ + +
+
+ +
+
+
screenshot-2026-10-03.png
1.8 MB
+
62%
+
+ +
+
+ +
+
+
hero-banner@2x.png
4.6 MB
+
queued
+
+ +
+
+ +
+
+
scan_0091.tiff
22.4 MB
+
Unsupported compression
+
+ + +
+
+ +
+
+
logo-final.bmp
1.1 MB
+
queued
+
+ +
+
+ +
+ +
+
+ +
+
Options
+
+
+
Format.webp
+
+ + +
+
+
+
Quality
+
+
+
+
Output
+
+ + +
+
+
+
Keep metadata
Dates, camera and location
+ +
+
+ +
+
+
+
+ + +
+
+
+ + diff --git a/docs/mockups/terminal-v3/17-preview-column.html b/docs/mockups/terminal-v3/17-preview-column.html new file mode 100644 index 0000000..c82cf73 --- /dev/null +++ b/docs/mockups/terminal-v3/17-preview-column.html @@ -0,0 +1,341 @@ + + + + + +Filesmith – Preview Column + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
Filesmith
+
convert
+
+ + + +
+
+ +
+ + +
+
+
+
+

Convert

+

Change images to another format.

+
+ 6 files + +
+ +
+
+
Queue2 of 6 done
+
+
+
+
IMG_4821.HEIC3.2 MB
+
3.2 MB640 KB
+
+
+
+
+
product-shot.jpg2.1 MB
+
2.1 MB486 KB
+
+
+
+
+
screenshot-2026-10-03.png1.8 MB
+
62%
+
+
+
+
+
hero-banner@2x.png4.6 MB
+
queued
+
+
+
+
+
scan_0091.tiff22.4 MB
+
Unsupported compression
+
+
+
+
+
logo-final.bmp1.1 MB
+
queued
+
+
+ +
+
+ +
+
Preview.webp
+
+
+ + + + + + + + + + + hero-banner@2x.png + 2880 × 1200 +
+
+ 4.6 MB~920 KB + -80% +
+ +
+
Format
+
+ + +
+
+ +
+
Quality
+
+
+ +
+
Output
+
+
+ +
+
Keep metadata
+ +
+ + +
+
+
+ + +
+
+
+ + diff --git a/docs/mockups/terminal-v3/18-ruled-sheet.html b/docs/mockups/terminal-v3/18-ruled-sheet.html new file mode 100644 index 0000000..8248149 --- /dev/null +++ b/docs/mockups/terminal-v3/18-ruled-sheet.html @@ -0,0 +1,322 @@ + + + + + + +Filesmith Ruled Sheet + + + + + + + + +
+
+ +
Filesmith
+
convert
+ + + +
+ +
+ + +
+
+
+
+

Convert

+

Change images to another format.

+
+ 6 files + +
+ +
+
+
Queue2 of 6 done
+
+
+
+
IMG_4821.HEIC
3.2 MB
+
640 KB
+
+
+
+
+
product-shot.jpg
2.1 MB
+
486 KB
+
+
+
+
+
screenshot-2026-10-03.png
1.8 MB
+
62%
+
+
+
+
+
hero-banner@2x.png
4.6 MB
+
queued
+
+
+
+
+
scan_0091.tiff
22.4 MB
+
Unsupported compression
+
+
+
+
+
logo-final.bmp
1.1 MB
+
queued
+
+
+
+ +
+ + +
+
+
+ + +
+ + + diff --git a/docs/mockups/terminal-v3/19-verb-sentence.html b/docs/mockups/terminal-v3/19-verb-sentence.html new file mode 100644 index 0000000..71db577 --- /dev/null +++ b/docs/mockups/terminal-v3/19-verb-sentence.html @@ -0,0 +1,290 @@ + + + + + +Filesmith Verb Sentence + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + Filesmith + convert + + + +
+ +
+ + +
+
+
+
+

Convert

+

Change images to another format.

+
+ 6 files +
+ +
+
+
+ Convert 6 files to , , . +
+
+
+ + +
+
+ +
+
+

Queue

+ + 2 of 6 done + +
+ +
+
+ IMG_4821.HEIC + 3.2 MB + 640 KB webp + +
+
+
+ product-shot.jpg + 2.1 MB + 486 KB webp + +
+
+
+ screenshot-2026-10-03.png + 1.8 MB + 62% + +
+
+
+ hero-banner@2x.png + 4.6 MB + queued + +
+
+
+ scan_0091.tiff + 22.4 MB + Unsupported compression + +
+
+
+ logo-final.bmp + 1.1 MB + queued + +
+
Drop more files here
+
+ + +
+
+
+
+ + diff --git a/docs/mockups/terminal-v3/20-toolbar-strip.html b/docs/mockups/terminal-v3/20-toolbar-strip.html new file mode 100644 index 0000000..1a1895b --- /dev/null +++ b/docs/mockups/terminal-v3/20-toolbar-strip.html @@ -0,0 +1,295 @@ + + + + + +Filesmith Toolbar Strip + + + +
+ + +
+ + Filesmith + convert + + + +
+ +
+ + + + +
+
+
+
+

Convert

+

Change images to another format.

+
+ 6 files + +
+ +
+ + + + +

Queue

2 of 6 done
+
+
+
+
IMG_4821.HEIC
3.2 MB
+
640 KB
+
+ + +
+
+
+
+
product-shot.jpg
2.1 MB
+
486 KB
+
+ + +
+
+
+
+
screenshot-2026-10-03.png
1.8 MB
+
62%
+
+ +
+
+
+
+
hero-banner@2x.png
4.6 MB
+
queued
+
+ +
+
+
+
+
scan_0091.tiff
22.4 MB
+
Unsupported compression
+
+ + +
+
+
+
+
logo-final.bmp
1.1 MB
+
queued
+
+ +
+
+ +
+
+ + +
+
+
+ + diff --git a/docs/mockups/terminal-v3/index.html b/docs/mockups/terminal-v3/index.html new file mode 100644 index 0000000..53b28e4 --- /dev/null +++ b/docs/mockups/terminal-v3/index.html @@ -0,0 +1,44 @@ + + +Filesmith mockups, round 3 + +

Filesmith, terminal round 3

20 directions with icons and more substance. Click a card to open the full mockup.

+
+Wind Classic mockup
01Wind Classic

Filesmith as a sibling of Wind Settings: same rail, same header band, same cards. The queue sits in the middle and the options on the right, with nothing left over.

+Wind Stacked Rows mockup
02Wind Stacked Rows

Wind's single-column settings rhythm: a queue card, then an options card made of real Wind rows, and the floating Save pill that becomes Convert.

+Wind Tiled Nav mockup
03Wind Tiled Nav

Wind with a little more presence in the rail: every verb icon lives in its own small tile, so the nav reads as a set of tools, not a list of words.

+Wind Grouped Rail mockup
04Wind Grouped Rail

Wind Settings structure, with the rail grouped like Wind's bottom group: Image, AI and More sections make 7 verbs feel organised, not long.

+Wind Command Header mockup
05Wind Command Header

Wind's layout, but the header band does the heavy lifting: the title, the file count, Add files and the Convert button all live in one confident band, which leaves the body for the queue and a slim options card.

+Linear Inspector mockup
06Linear Inspector

Linear's calm list plus a single quiet inspector: the queue is a borderless list with hairlines, and the options live in a full-height right pane separated by one line.

+Raycast Panel mockup
07Raycast Panel

The whole app as one Raycast-like floating panel on black: verb tabs along the top, a list in the middle, and an action bar at the bottom.

+Zed Activity Bar mockup
08Zed Activity Bar

An editor-like activity bar: a narrow column of stacked icon-plus-label buttons, which frees width for a wide queue and a generous options column.

+Top Icon Tabs mockup
09Top Icon Tabs

No sidebar at all: the verbs become a row of icon-and-label tabs under the titlebar, so the content gets the full 1440px width.

+Segmented Switcher mockup
10Segmented Switcher

The verbs live in one segmented control, like a mode switch on a pro camera: a single object that says this app does seven things.

+Bottom Dock mockup
11Bottom Dock

The verbs sit in a quiet dock at the bottom, like a tool tray, so the top of the window belongs entirely to the job at hand.

+Thumbnail Grid mockup
12Thumbnail Grid

Files as visual cards, not lines: a 3x2 grid of thumbnails where each card shows its own status, which feels more like a media tool.

+Quiet Table mockup
13Quiet Table

The queue as a proper table with four light column headers, for people who like to scan by column: name, size, result, status.

+Prompt Title mockup
14Prompt Title

Round 2's prompt-caret idea with real substance: the page title reads like a command, but the screen around it is a complete, carded app.

+Glyph Gutter mockup
15Glyph Gutter

Status first: every queue row starts with a status glyph in its own gutter, so the column of checks, a ring and a triangle tells the story before you read a name.

+Drop Frame mockup
16Drop Frame

The queue lives inside a visible drop frame: the dashed boundary says 'drop here' all the time, so the empty-state idea becomes the structure.

+Preview Column mockup
17Preview Column

A media tool should show the picture: the right column opens with a greyscale preview of the selected file and its expected result, then the options, so you see what you are about to make.

+Ruled Sheet mockup
18Ruled Sheet

Typography-led with real weight: one 2px white rule under the header carries the structure, and the queue and options sit on a single ruled sheet with clear sections, not floating cards.

+Verb Sentence mockup
19Verb Sentence

The header is a sentence you can edit: 'Convert 6 files to WebP, Balanced, next to source', where each grey word is a select that sets that option, so the sentence is the options panel.

+Toolbar Strip mockup
20Toolbar Strip

All options in one toolbar strip above the queue, like an image editor's options bar, so the queue gets the full width and plenty of room.

+
diff --git a/docs/mockups/terminal-v3/shots/01-wind-classic.png b/docs/mockups/terminal-v3/shots/01-wind-classic.png new file mode 100644 index 0000000..636116f Binary files /dev/null and b/docs/mockups/terminal-v3/shots/01-wind-classic.png differ diff --git a/docs/mockups/terminal-v3/shots/02-wind-stacked.png b/docs/mockups/terminal-v3/shots/02-wind-stacked.png new file mode 100644 index 0000000..a60d605 Binary files /dev/null and b/docs/mockups/terminal-v3/shots/02-wind-stacked.png differ diff --git a/docs/mockups/terminal-v3/shots/03-wind-tiled-nav.png b/docs/mockups/terminal-v3/shots/03-wind-tiled-nav.png new file mode 100644 index 0000000..b032f5c Binary files /dev/null and b/docs/mockups/terminal-v3/shots/03-wind-tiled-nav.png differ diff --git a/docs/mockups/terminal-v3/shots/04-wind-grouped-rail.png b/docs/mockups/terminal-v3/shots/04-wind-grouped-rail.png new file mode 100644 index 0000000..e43a0e0 Binary files /dev/null and b/docs/mockups/terminal-v3/shots/04-wind-grouped-rail.png differ diff --git a/docs/mockups/terminal-v3/shots/05-wind-command-header.png b/docs/mockups/terminal-v3/shots/05-wind-command-header.png new file mode 100644 index 0000000..68f36e8 Binary files /dev/null and b/docs/mockups/terminal-v3/shots/05-wind-command-header.png differ diff --git a/docs/mockups/terminal-v3/shots/06-linear-inspector.png b/docs/mockups/terminal-v3/shots/06-linear-inspector.png new file mode 100644 index 0000000..f6ed282 Binary files /dev/null and b/docs/mockups/terminal-v3/shots/06-linear-inspector.png differ diff --git a/docs/mockups/terminal-v3/shots/07-raycast-panel.png b/docs/mockups/terminal-v3/shots/07-raycast-panel.png new file mode 100644 index 0000000..9e3a879 Binary files /dev/null and b/docs/mockups/terminal-v3/shots/07-raycast-panel.png differ diff --git a/docs/mockups/terminal-v3/shots/08-zed-activity-bar.png b/docs/mockups/terminal-v3/shots/08-zed-activity-bar.png new file mode 100644 index 0000000..7382790 Binary files /dev/null and b/docs/mockups/terminal-v3/shots/08-zed-activity-bar.png differ diff --git a/docs/mockups/terminal-v3/shots/09-top-icon-tabs.png b/docs/mockups/terminal-v3/shots/09-top-icon-tabs.png new file mode 100644 index 0000000..2fb254c Binary files /dev/null and b/docs/mockups/terminal-v3/shots/09-top-icon-tabs.png differ diff --git a/docs/mockups/terminal-v3/shots/10-segmented-switcher.png b/docs/mockups/terminal-v3/shots/10-segmented-switcher.png new file mode 100644 index 0000000..5c2cec6 Binary files /dev/null and b/docs/mockups/terminal-v3/shots/10-segmented-switcher.png differ diff --git a/docs/mockups/terminal-v3/shots/11-bottom-dock.png b/docs/mockups/terminal-v3/shots/11-bottom-dock.png new file mode 100644 index 0000000..dd84058 Binary files /dev/null and b/docs/mockups/terminal-v3/shots/11-bottom-dock.png differ diff --git a/docs/mockups/terminal-v3/shots/12-thumb-grid.png b/docs/mockups/terminal-v3/shots/12-thumb-grid.png new file mode 100644 index 0000000..b230b54 Binary files /dev/null and b/docs/mockups/terminal-v3/shots/12-thumb-grid.png differ diff --git a/docs/mockups/terminal-v3/shots/13-quiet-table.png b/docs/mockups/terminal-v3/shots/13-quiet-table.png new file mode 100644 index 0000000..43df70a Binary files /dev/null and b/docs/mockups/terminal-v3/shots/13-quiet-table.png differ diff --git a/docs/mockups/terminal-v3/shots/14-prompt-title.png b/docs/mockups/terminal-v3/shots/14-prompt-title.png new file mode 100644 index 0000000..f76933b Binary files /dev/null and b/docs/mockups/terminal-v3/shots/14-prompt-title.png differ diff --git a/docs/mockups/terminal-v3/shots/15-glyph-gutter.png b/docs/mockups/terminal-v3/shots/15-glyph-gutter.png new file mode 100644 index 0000000..bef99f6 Binary files /dev/null and b/docs/mockups/terminal-v3/shots/15-glyph-gutter.png differ diff --git a/docs/mockups/terminal-v3/shots/16-drop-frame.png b/docs/mockups/terminal-v3/shots/16-drop-frame.png new file mode 100644 index 0000000..675e13a Binary files /dev/null and b/docs/mockups/terminal-v3/shots/16-drop-frame.png differ diff --git a/docs/mockups/terminal-v3/shots/17-preview-column.png b/docs/mockups/terminal-v3/shots/17-preview-column.png new file mode 100644 index 0000000..811a502 Binary files /dev/null and b/docs/mockups/terminal-v3/shots/17-preview-column.png differ diff --git a/docs/mockups/terminal-v3/shots/18-ruled-sheet.png b/docs/mockups/terminal-v3/shots/18-ruled-sheet.png new file mode 100644 index 0000000..ff4cbc9 Binary files /dev/null and b/docs/mockups/terminal-v3/shots/18-ruled-sheet.png differ diff --git a/docs/mockups/terminal-v3/shots/19-verb-sentence.png b/docs/mockups/terminal-v3/shots/19-verb-sentence.png new file mode 100644 index 0000000..b1a0552 Binary files /dev/null and b/docs/mockups/terminal-v3/shots/19-verb-sentence.png differ diff --git a/docs/mockups/terminal-v3/shots/20-toolbar-strip.png b/docs/mockups/terminal-v3/shots/20-toolbar-strip.png new file mode 100644 index 0000000..e43e809 Binary files /dev/null and b/docs/mockups/terminal-v3/shots/20-toolbar-strip.png differ diff --git a/docs/mockups/terminal-v3/shots/_index.png b/docs/mockups/terminal-v3/shots/_index.png new file mode 100644 index 0000000..8e9f3d2 Binary files /dev/null and b/docs/mockups/terminal-v3/shots/_index.png differ diff --git a/docs/mockups/terminal-v4/01-activity-explorer.html b/docs/mockups/terminal-v4/01-activity-explorer.html new file mode 100644 index 0000000..718bda9 --- /dev/null +++ b/docs/mockups/terminal-v4/01-activity-explorer.html @@ -0,0 +1,437 @@ + + + + + +Filesmith Explorer + + + + + +
+ +
+
+ + + Filesmith +
+
+ + + +
+ +
+ + + + + + + +
+
+
+ screenshot-2026-10-03.png + +
+
+ + + + +
+
+ + +
+
+ +
+
PNGWebP +
+
+
Dimensions
2880 × 1800
+
Format
PNGWebP
+
Size
1.8 MB~410 KB
+
Progress
62%
+
+
+
+
+ + + +
+ + diff --git a/docs/mockups/terminal-v4/02-settings-editor.html b/docs/mockups/terminal-v4/02-settings-editor.html new file mode 100644 index 0000000..26ce804 --- /dev/null +++ b/docs/mockups/terminal-v4/02-settings-editor.html @@ -0,0 +1,440 @@ + + + + + +Filesmith, Convert + + + + + + + + +
+ +
+
Filesmith
+
Convert
+
+ + + +
+ +
+ + + + +
+
+ +
+ + +
+
+ +
+ +
+ +
+ + +
+
Format
+
+ +
Format: TargetModified
+
The file type every image in the queue is converted to.
+
+
+ +
+
+
+ +
Quality
+
+ +
Quality: LevelModified
+
Trade file size against detail. Balanced suits most photos.
+
+
+ +
+
+
+ +
Output
+
+ +
Output: Location
+
Where converted files are saved. Originals are never replaced.
+
+ + +
+
+ +
Metadata
+
+ +
Metadata: Keep
+ +
+
+
+
+ + + +
+ + + +
+ + diff --git a/docs/mockups/terminal-v4/03-twin-stripes.html b/docs/mockups/terminal-v4/03-twin-stripes.html new file mode 100644 index 0000000..c73f4a4 --- /dev/null +++ b/docs/mockups/terminal-v4/03-twin-stripes.html @@ -0,0 +1,426 @@ + + + + + +Filesmith, Twin Stripes + + + + + + + + + + + +
+
+ +
+
+ + + +
+
+ + + +
+
+ +
+ + + + +
+
+
Convert
+
+ + + + +
+ +
+ + +
+
+ +
+
+
+
Name
+
Size
+
Result
+
Status
+
+
+ +
+
+
IMG_4821.HEIC
+
3.2 MB
+
640 KB−80%
+
Done
+
+
+ +
+
+
product-shot.jpg
+
2.1 MB
+
486 KB−77%
+
Done
+
+
+ +
+
+
screenshot-2026-10-03.png
+
1.8 MB
+
62%
+
Converting
+
+
+ +
+
+
hero-banner@2x.png
+
4.6 MB
+
+
Queued
+
+
+ +
+
+
scan_0091.tiff
+
22.4 MB
+
Unsupported compression
+
Failed
+
+
+ +
+
+
logo-final.bmp
+
1.1 MB
+
+
Queued
+
+
+ +
Drop files here or
+
+ + +
+
+ Previewproduct-shot.jpg +
+ + +
+
+
+
+
+ +
+
+
Outputproduct-shot.webp
+
FormatJPGWebP
+
Size2.1 MB486 KB
+
Dimensions2400 × 1600
+
QualityBalanced, q 82
+
MetadataKept
+
Saved toNext to source
+
+
+
+
+ + + + + + +
+ + + + diff --git a/docs/mockups/terminal-v4/04-verb-tabs.html b/docs/mockups/terminal-v4/04-verb-tabs.html new file mode 100644 index 0000000..b4fe8fc --- /dev/null +++ b/docs/mockups/terminal-v4/04-verb-tabs.html @@ -0,0 +1,440 @@ + + + + + +Filesmith – Verb Tabs + + + + + + + +
+ +
+
+ + + +
+
+ + + + + +
+
+
+ + + + + + + +
+ +
+
+ + Name + Type + Size + Status + +
+ +
+ + IMG_4821.HEIC + HEIC + 3.2 MB640 KB + Done + +
+
+ + product-shot.jpg + JPG + 2.1 MB486 KB + Done + +
+
+ + screenshot-2026-10-03.png + PNG + 1.8 MB + 62% + +
+
+ + hero-banner@2x.png + PNG + 4.6 MB + Queued + +
+
+ + scan_0091.tiff + TIFF + 22.4 MB + Unsupported compression + +
+
+ + logo-final.bmp + BMP + 1.1 MB + Queued + +
+ +
Drop files here or
+
+ + +
+
+ Details + IMG_4821.HEIC + + + +
+
+
+
+
+ + + + + +
+
Before, HEIC3.2 MB
+
+
+
+ + + + +
+
After, WebP640 KB
+
+
+
+
FormatHEICWebP
+
Dimensions4032 × 3024
+
Size3.2 MB640 KB
+
Saved80%
+
MetadataKept
+
OutputIMG_4821.webp
+
+
+
+
+ + + +
+ + + + + + diff --git a/docs/mockups/terminal-v4/05-project-widget.html b/docs/mockups/terminal-v4/05-project-widget.html new file mode 100644 index 0000000..c92b135 --- /dev/null +++ b/docs/mockups/terminal-v4/05-project-widget.html @@ -0,0 +1,494 @@ + + + + + +Filesmith + + + + + + + + +
+ +
+ + + + +
+ + +
+ + + +
+
+ + + +
+ +
+
+ + + + + + + +
+ +
+
+ Name + SizeResultStatus +
+ +
+ + IMG_4821.HEIC + 3.2 MB + + 640 KB + done + +
+
+ + product-shot.jpg + 2.1 MB + + 486 KB + done + +
+
+ + screenshot-2026-10-03.png + 1.8 MB + + 62% + +
+
+ + hero-banner@2x.png + 4.6 MB + + queued + +
+
+ + scan_0091.tiff + 22.4 MB + + Unsupported compression + +
+
+ + logo-final.bmp + 1.1 MB + + queued + +
+
Drop files here, or
+
+ + +
+
+ + IMG_4821.HEIC + + + +
+
+
+
+ +
+ +
+ + + HEIC + WebP +
+
OriginalConverted
+
+
+
File
+
Dimensions4032 × 3024
+
FormatHEIC → WebP
+
Size3.2 MB → 640 KB
+
Saved80%
+
Output
+
NameIMG_4821.webp
+
FolderNext to source
+
MetadataKept
+
+
+
+
+ + + +
+ + + +
+ + + + diff --git a/docs/mockups/terminal-v4/06-preferences-tree.html b/docs/mockups/terminal-v4/06-preferences-tree.html new file mode 100644 index 0000000..a9d54d7 --- /dev/null +++ b/docs/mockups/terminal-v4/06-preferences-tree.html @@ -0,0 +1,361 @@ + + + + + +Filesmith Preferences Tree + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
Filesmith
+ +
+ + + +
+
+ +
+ + + +
+ +
+
+ ConvertFormat + For 6 images + +
+
+
+
Target format
+
Save as +
+ +
+ Small files, opens everywhere +
+
+
+
Quality
+
Level +
+ Looks the same as the source at about a quarter of the size +
+
+
+
Output
+
Save to +
+ Next to source + Choose folder +
+ +
+
+
+
Metadata
+
+ Keep metadataCamera data, dates and color profile +
+
+
+
+ + +
+
+
+ Queue6 + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + +
NameDimensionsSizeStatus
IMG_4821.HEIC4032 × 30243.2 MB640 KBDone
product-shot.jpg2400 × 24002.1 MB486 KBDone
screenshot-2026-10-03.png2880 × 18001.8 MB62%
hero-banner@2x.png3200 × 12004.6 MBQueued
scan_0091.tiff5100 × 700022.4 MBUnsupported compression
logo-final.bmp1024 × 10241.1 MBQueued
+
Drop files here or browse
+
+
+
+ + +
+ + diff --git a/docs/mockups/terminal-v4/07-zed-hairline.html b/docs/mockups/terminal-v4/07-zed-hairline.html new file mode 100644 index 0000000..6674212 --- /dev/null +++ b/docs/mockups/terminal-v4/07-zed-hairline.html @@ -0,0 +1,411 @@ + + + + + +Filesmith, Hairline + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
Filesmith/Convert
+
+
+ + + +
+
+ +
+ + +
+
+
Convert 6 files
+
+ + + + + +
+
+
NameSizeResult
+ +
+ + IMG_4821.HEIC + 3.2 MB + 640 KB + +
+
+
+
+ + + + + +
HEIC3.2 MB
+
+ + + + + +
WebP640 KB
+
+
+
FormatHEIC WebP
+
Size3.2 MB640 KB−80%
+
Dimensions4032 × 3024
+
Saved toIMG_4821.webp
+
+
+ +
+ + product-shot.jpg + 2.1 MB + 486 KB + +
+
+ + screenshot-2026-10-03.png + 1.8 MB + 62% + +
+
+ + hero-banner@2x.png + 4.6 MB + queued + +
+
+ + scan_0091.tiff + 22.4 MB + Unsupported compression + +
+
+ + logo-final.bmp + 1.1 MB + queued + +
+
+ + Drop files or add files +
+ +
+ +
+ + +
+ + +
+ + diff --git a/docs/mockups/terminal-v4/08-fleet-grid.html b/docs/mockups/terminal-v4/08-fleet-grid.html new file mode 100644 index 0000000..1170b08 --- /dev/null +++ b/docs/mockups/terminal-v4/08-fleet-grid.html @@ -0,0 +1,360 @@ + + + + + +Filesmith, Fleet Grid + + + + + + + + +
+
Filesmith
+ +
+ + + +
+
+ + + +
+
+ +
+

Options

+ + .webp +
+ +
+
Format
+
+
+ +
+
+ +
Quality
+
+
+ +
+
+ +
Output
+
+ + +
+ +
Keep metadata
+
+ +
+
+ +
+ 6 images → WebP, balanced, next to source + + +
+ +
+
+

Queue

+ 6 + + + +
+ +
+
+ + Name + Type + Size + Status + +
+
+
+
+ IMG_4821.HEIC + HEIC + 3.2 MB640 KB + done + +
+
+
+ product-shot.jpg + JPG + 2.1 MB486 KB + done + +
+
+
+ screenshot-2026-10-03.png + PNG + 1.8 MB + 62% + +
+
+
+ hero-banner@2x.png + PNG + 4.6 MB + queued + +
+
+
+ scan_0091.tiff + TIFF + 22.4 MB + Unsupported compression + +
+
+
+ logo-final.bmp + BMP + 1.1 MB + queued + +
+
+
Drop files here, or browse
+
+
+ +
+
+ + + + diff --git a/docs/mockups/terminal-v4/09-walkthrough-steps.html b/docs/mockups/terminal-v4/09-walkthrough-steps.html new file mode 100644 index 0000000..6c17acf --- /dev/null +++ b/docs/mockups/terminal-v4/09-walkthrough-steps.html @@ -0,0 +1,534 @@ + + + + + +Filesmith, Walkthrough + + + + + + + +
+
+ + Filesmith +
+
Convert
+
+ + + +
+
+ +
+ + + + + +
+
Convert +
+ +
+
+
+

Convert images

+
4 of 4 ready
+
+ +
    +
  1. + +
    +

    Drop files on the queue, or pick them here.

    + +
    +
  2. +
  3. + +
    +

    Every file in the queue becomes this format.

    +
    + + + + + + +
    +
    Transparency yesTypical size -70%
    +
    + +
    +
    +
  4. +
  5. + +
    +

    Trade file size against detail.

    +
    + +
    +
    +
    +
  6. +
  7. + +
    +

    Where converted files are saved.

    + + + +
    +
  8. +
+ +
+ +
+
Batch
+
+
Files
6 images
+
Converts to
WebP, balanced
+
Input
35.2 MB
+
Estimated
7.9 MB  -78%
+
+
+
+ +
+
+ + +
+
+
+

Queue

6 +
+ + + + +
+
+ +
+
+
Name
Size
Status
+
+
+
+ IMG_4821.HEIC + 3.2 MB640 KB + done + +
+
+ product-shot.jpg + 2.1 MB486 KB + done + +
+
+ screenshot-2026-10-03.png + 1.8 MB + 62% + +
+
+ hero-banner@2x.png + 4.6 MB + queued + +
+
+ scan_0091.tiff + 22.4 MB + Unsupported compression + +
+
+ logo-final.bmp + 1.1 MB + queued + +
+
Drop files here or
+
+
+
+ + +
+
+ +
+ + +
+
+
+
+
+
+ + + + + + + + +
+
HEICSource3.2 MB
+
+
+
+ + + + + + + +
+
WebPResult640 KB
+
+
+
+
+
File
+
+
Dimensions
4032 × 3024
+
Format
HEIC  →  WebP
+
Size
3.2 MB → 640 KB
+
Saved
80%
+
+
+
+
Output
+
+
Name
IMG_4821.webp
+
Location
Next to source
+
Metadata
Kept
+
Took
1.4 s
+
+
+
+
+
+
+
+ + + + + + + diff --git a/docs/mockups/terminal-v4/10-explorer-details.html b/docs/mockups/terminal-v4/10-explorer-details.html new file mode 100644 index 0000000..b2bcea5 --- /dev/null +++ b/docs/mockups/terminal-v4/10-explorer-details.html @@ -0,0 +1,493 @@ + + + + + + +Filesmith Details + + + + + + + + + + +
+
Filesmith
+ + +
+ + + +
+
+ +
+ + + + +
+
+ + + + +
+ 6 files, 1 selected + +
+
+ +
+ +
+
Name
+
Kind
+
Size
+
Result
+
Status
+
+ +
+
IMG_4821.HEIC
+
HEIC
+
3.2 MB
+
640 KB−80%
+
Done
+
+ +
+
product-shot.jpg
+
JPG
+
2.1 MB
+
486 KB−77%
+
Done
+
+ +
+
screenshot-2026-10-03.png
+
PNG
+
1.8 MB
+
62%
+
Converting
+
+ +
+
hero-banner@2x.png
+
PNG
+
4.6 MB
+
+
Queued
+
+ +
+
scan_0091.tiff
+
TIFF
+
22.4 MB
+
+
Unsupported compression
+
+ +
+
logo-final.bmp
+
BMP
+
1.1 MB
+
+
Queued
+
+ +
Drop files here, or
+
+
+
6 files
+
+
35.2 MB
+
5.3 MB1.1 MBso far
+
2 of 6 done
+
+
+ +
+ + + + +
+ + + + + + + diff --git a/docs/mockups/terminal-v4/11-nova-prefpane.html b/docs/mockups/terminal-v4/11-nova-prefpane.html new file mode 100644 index 0000000..c18f1d1 --- /dev/null +++ b/docs/mockups/terminal-v4/11-nova-prefpane.html @@ -0,0 +1,371 @@ + + + + + +Filesmith Pref Pane + + + + + + + + +
+
Filesmith
+
Convert
+
+ + + +
+
+ + + + + +
+
+
+ Format: +
+
+ +
+
Small files that open in every browser.
+
+
+
+ Quality: +
+
+
Looks the same at about a fifth of the size.
+
+
+ +
+ +
+ Save to: +
+ + +
+
+
+ Metadata: +
+ +
Camera data, dates and colour profile.
+
+
+ +
+ +
+ + +
+
+
+ + +
+
+ Queue6 +
+ + +
+ +
+
+ + Name + Type + Size + + Status + +
+ +
+ + IMG_4821.HEIC + HEIC + 3.2 MB640 KB + + Done + +
+
+ + product-shot.jpg + JPG + 2.1 MB486 KB + + Done + +
+
+ + screenshot-2026-10-03.png + PNG + 1.8 MB + + 62% + +
+
+ + hero-banner@2x.png + PNG + 4.6 MB + + Queued + +
+
+ + scan_0091.tiff + TIFF + 22.4 MB + + Unsupported compression + +
+
+ + logo-final.bmp + BMP + 1.1 MB + + Queued + +
+
Drop more files here or browse
+
+ + + + + + diff --git a/docs/mockups/terminal-v4/12-options-sidebar.html b/docs/mockups/terminal-v4/12-options-sidebar.html new file mode 100644 index 0000000..779978f --- /dev/null +++ b/docs/mockups/terminal-v4/12-options-sidebar.html @@ -0,0 +1,439 @@ + + + + + + +Filesmith Options Sidebar + + + + + + + + + +
+
Filesmith
+
Convert  /  6 files
+
+ + + +
+
+ +
+ + + + + + + +
+
+
Queue6 files
+
+ + + +
+
+ +
+
+ + Name + Type + Size + Status + +
+ +
+
+
IMG_4821.HEIC
HEIC
+
3.2 MB640 KB
+
Done
+ +
+ +
+
+
product-shot.jpg
JPG
+
2.1 MB486 KB
+
Done
+ +
+ +
+
+
screenshot-2026-10-03.png
PNG
+
1.8 MB
+
62%
+ +
+ +
+
+
hero-banner@2x.png
PNG
+
4.6 MB
+
Queued
+ +
+ +
+
+
scan_0091.tiff
TIFF
+
22.4 MB
+
Unsupported compression
+ +
+ +
+
+
logo-final.bmp
BMP
+
1.1 MB
+
Queued
+ +
+ +
Drop files here, or
+
+ +
+
+ + +
+ +
+
+
+
+
+
+
+
+
+
+
+
File
screenshot-2026-10-03.png
+
Dimensions
2880 × 1800
+
Size
1.8 MB estimated ~ 310 KB
+
Converting to
WebP, balanced
+
Saves to
Next to source
+
Progress
62%
+
+
+
+
+
+ + + + diff --git a/docs/mockups/terminal-v4/13-run-configurations.html b/docs/mockups/terminal-v4/13-run-configurations.html new file mode 100644 index 0000000..07251d6 --- /dev/null +++ b/docs/mockups/terminal-v4/13-run-configurations.html @@ -0,0 +1,388 @@ + + + + + +Filesmith, Run Configurations + + + + + + + + +
+ +
+
Filesmith
+ + + +
+ + + +
+
+ + + +
+
+ +
+ + + +
+ +
+ + +
+
WebP · Balancedused for the next run + default
+ +
+
WebP · Balanced
+
+ +
Format +
+ +
+ +
Quality +
+ +
Sharp at a fraction of the size
+ +
Output +
+
+ +
Metadata +
+ Camera data, dates and colour profile
+ +
Output name
+
Example + IMG_4821.HEIC IMG_4821.webp
+
+ + +
+ + +
+ Queue 6 + + + + + + +
+ + +
+
Name SizeStatus
+ +
IMG_4821.HEIC + 3.2 MB640 KB + Done +
+ +
product-shot.jpg + 2.1 MB486 KB + Done +
+ +
screenshot-2026-10-03.png + 1.8 MB + 62% +
+ +
hero-banner@2x.png + 4.6 MB + Queued +
+ +
scan_0091.tiff + 22.4 MB + Unsupported compression +
+ +
logo-final.bmp + 1.1 MB + Queued +
+ +
Drop files here or add files
+
+
+
+ + + +
+ + diff --git a/docs/mockups/terminal-v4/14-command-center.html b/docs/mockups/terminal-v4/14-command-center.html new file mode 100644 index 0000000..c2d629b --- /dev/null +++ b/docs/mockups/terminal-v4/14-command-center.html @@ -0,0 +1,383 @@ + + + + + +Filesmith Command Center + + + + + + + + +
+ +
+
+ +
+ + + +
+
+ +
+ + +
+ +
+ +
+ + + + + +
+
+ +
+ + +
+ + Name + Dimensions + Status + Size + +
+
+
+ + IMG_4821.HEIC + 4032 × 3024 + Done + 3.2 MB640 KB + +
+
+ + product-shot.jpg + 2400 × 1600 + Done + 2.1 MB486 KB + +
+
+ + screenshot-2026-10-03.png + 2880 × 1800 + 62% + 1.8 MB + +
+
+ + hero-banner@2x.png + 3840 × 1280 + Queued + 4.6 MB + +
+
+ + scan_0091.tiff + 5100 × 6600 + Unsupported compression + 22.4 MB + +
+
+ + logo-final.bmp + 1024 × 1024 + Queued + 1.1 MB + +
+
+
+ + Drop files anywhere, or +
+ + +
+ + + + + + +
+ +
+
+
+ + + +
+ + diff --git a/docs/mockups/terminal-v4/15-split-editor.html b/docs/mockups/terminal-v4/15-split-editor.html new file mode 100644 index 0000000..1459029 --- /dev/null +++ b/docs/mockups/terminal-v4/15-split-editor.html @@ -0,0 +1,522 @@ + + + + + +Filesmith + + + + + + + + +
+ +
+
Filesmith
+
+ +
+ + +
+ + + +
+
+ +
+ + + + + + + +
+ +
+
+
Queue6
+
Completed142
+
+
+ +
+ + + +
+ 6 files, 35.2 MB +
+
+ + Name + Size + Result + +
+
    +
  • +
    + IMG_4821.HEIC + 3.2 MB + 640 KB + +
  • +
  • +
    + product-shot.jpg + 2.1 MB + 486 KB + +
  • +
  • +
    + screenshot-2026-10-03.png + 1.8 MB + 62% + +
  • +
  • +
    + hero-banner@2x.png + 4.6 MB + queued + +
  • +
  • +
    + scan_0091.tiff + 22.4 MB + Unsupported compression + +
  • +
  • +
    + logo-final.bmp + 1.1 MB + queued + +
  • +
+
Drop files here or
+
+
Saved so far 4.17 MBAverage 78% smaller
+
+ +
+ + +
+
+
product-shot.jpg
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+
+
BeforeJPG 2.1 MB
+
+
+
+
AfterWebP 486 KB
+
+
+
+
Properties
+ + + + + + + + + + +
PropertyBeforeAfter
Dimensions2400 × 16002400 × 1600
FormatJPGWebP
Size2.1 MB486 KB
Qualityq 92Balanced, q 80
MetadataEXIF, ICCKept
Saved77%
+
+
+
+ + + +
+ + diff --git a/docs/mockups/terminal-v4/16-state-tree.html b/docs/mockups/terminal-v4/16-state-tree.html new file mode 100644 index 0000000..e4c3213 --- /dev/null +++ b/docs/mockups/terminal-v4/16-state-tree.html @@ -0,0 +1,460 @@ + + + + + +Filesmith – State Tree + + + + + +
+ +
+
+ +
+ + + +
+
+ + + + + + + + +
+
+ +
+ + +
+
+ +
+
+
+ +
+
+ Convert to +
+ + + + + + +
+
+
+
+ +
+ +
+
+ Preset +
+ + + +
+
+
+
+ +
+ +
+
+ Save to +
+ + +
+
+
Originals are never overwritten.
+
+
+ +
+ +
+
+ Source data + +
+
Camera data, dates and colour profile.
+
+
+ +
+ +
+
+ +
+
+
+
+ + + +
+ + diff --git a/docs/mockups/terminal-v4/17-sublime-quiet.html b/docs/mockups/terminal-v4/17-sublime-quiet.html new file mode 100644 index 0000000..a84c1da --- /dev/null +++ b/docs/mockups/terminal-v4/17-sublime-quiet.html @@ -0,0 +1,324 @@ + + + + + +Filesmith - Quiet + + + + + + + + +
+ +
+
+ +
+
+ + + + + + +
+
+ + + + +
+
+
+ + Name + Size + Status + +
+ +
+ + + IMG_4821.HEIC + 3.2 MB640 KB + + +
+ +
+ + + product-shot.jpg + 2.1 MB486 KB + + +
+ +
+ + + screenshot-2026-10-03.png + 1.8 MB + 62% + +
+ +
+ + + hero-banner@2x.png + 4.6 MB + + +
+ +
+ + + scan_0091.tiff + 22.4 MB + Unsupported compression + +
+ +
+ + + logo-final.bmp + 1.1 MB + + +
+ + +
+ + +
+ + +
+ + + + + product-shot.jpg2.1 MB → 486 KB + +
+ + +
+ + diff --git a/docs/mockups/terminal-v4/18-labelled-activity.html b/docs/mockups/terminal-v4/18-labelled-activity.html new file mode 100644 index 0000000..a8a47d3 --- /dev/null +++ b/docs/mockups/terminal-v4/18-labelled-activity.html @@ -0,0 +1,366 @@ + + + + + + +Filesmith Labelled Bar + + + + + + + + +
+
+
Filesmith
+ +
+ + + +
+
+ +
+ + + + +
+
+

Queue

6 files +
+ + + + +
+
+ +
+ + Name + Size + Status + +
+
+ + IMG_4821.HEIC + 3.2 MB640 KB + Done + +
+
+ + product-shot.jpg + 2.1 MB486 KB + Done + +
+
+ + screenshot-2026-10-03.png + 1.8 MB + 62% + +
+
+ + hero-banner@2x.png + 4.6 MB + Queued + +
+
+ + scan_0091.tiff + 22.4 MB + Unsupported compression + +
+
+ + logo-final.bmp + 1.1 MB + Queued + +
+ +
Drop more files here
+
+ + + +
+ + +
+ + diff --git a/docs/mockups/terminal-v4/19-properties-window.html b/docs/mockups/terminal-v4/19-properties-window.html new file mode 100644 index 0000000..4210585 --- /dev/null +++ b/docs/mockups/terminal-v4/19-properties-window.html @@ -0,0 +1,363 @@ + + + + + +Filesmith Properties Window + + + + + + + +
+
+ +
+ + + +
+
+ + + + + +
+
+
Convert queue (6)
+ +
+ +
+ + + + +
+ +
+ Name TypeSizeStatus +
+ +
+ + IMG_4821.HEICHEIC + 3.2 MB640 KB + Done +
+
+ + product-shot.jpgJPG + 2.1 MB486 KB + Done +
+
+ + screenshot-2026-10-03.pngPNG + 1.8 MB + 62% +
+
+ + hero-banner@2x.pngPNG + 4.6 MB + Queued +
+
+ + scan_0091.tiffTIFF + 22.4 MB + Unsupported compression +
+
+ + logo-final.bmpBMP + 1.1 MB + Queued +
+ +
Drop files here or
+ +
+ +
+
+ + + + + + + + + + diff --git a/docs/mockups/terminal-v4/20-navigator-selector.html b/docs/mockups/terminal-v4/20-navigator-selector.html new file mode 100644 index 0000000..162237d --- /dev/null +++ b/docs/mockups/terminal-v4/20-navigator-selector.html @@ -0,0 +1,477 @@ + + + + + +Filesmith + + + + + + + +
+
Filesmith
+
+ + +
+ + + +
+ +
+ + + + + +
+
+ + +
+ + + + + +
+ + +
+ +
+
+
+
+
+
+
+
+
+
+ JPG 2.1 MB + WebP 486 KB +
+
+ +
+ product-shot.jpg +
+ 1600 × 1200 +
+ +
+
+ + + +
+ + + + + + diff --git a/docs/mockups/terminal-v4/index.html b/docs/mockups/terminal-v4/index.html new file mode 100644 index 0000000..d865cf8 --- /dev/null +++ b/docs/mockups/terminal-v4/index.html @@ -0,0 +1,146 @@ + + +Filesmith IDE mockups + +

Filesmith / terminal-v4 / IDE-inspired layouts

+

20 directions that borrow from how IDEs lay out simple, easy to navigate settings pages and menus. Click a card to open the mockup.

+
+ + Explorer mockup +
01Explorer
+

VS Code activity bar (2px edge bar active, badge), Explorer sidebar view with 22px collapsible section headers, empty editor as preview, status bar

+

The VS Code Explorer turned into a file tool. A narrow activity bar holds the verbs, and a 300px sidebar stacks collapsible QUEUE, OPTIONS and OUTPUT sections. The editor area previews the selected file. It is calm and familiar, and nothing floats.

+
+ + Settings Editor mockup +
02Settings Editor
+

VS Code Settings editor (search + category tree + stacked rows, modified bar), VS Code secondary side bar, activity bar

+

Convert options read like VS Code's Settings UI: a search field, a category tree and stacked setting rows with a one-line description each. The queue lives in a secondary sidebar on the right. It is the literal 'IDE settings page' feel the owner asked for.

+
+ + Twin Stripes mockup +
03Twin Stripes
+

JetBrains New UI stripes (filled square active, separators), run widget in the title bar, tool windows (Options docked right), compact toolbar

+

JetBrains New UI framing: a tool window stripe on each edge, the queue as the main table, and a run widget in the title bar that holds the primary action, the way IntelliJ holds Run.

+
+ + Verb Tabs mockup +
04Verb Tabs
+

VS Code editor tabs (active top border, merges into content), breadcrumbs bar, JetBrains-style property inspector rows

+

The verbs are editor tabs across the top, each with an icon, and Convert is the active tab merging into the content. A breadcrumb names the target. A plain property inspector holds the options. There is no rail at all.

+
+ + Project Widget mockup +
05Project Widget
+

Zed / JetBrains title bar project picker, Zed flat list panes, inspector pane with disclosure groups

+

Zed and JetBrains style: no rail. The verb is picked from a 'Convert ▾' widget in the title bar, so the whole window belongs to a flat queue list and an inspector. The most uncluttered take.

+
+ + Preferences Tree mockup +
06Preferences Tree
+

JetBrains Settings dialog (tree with search, breadcrumb header, titled-separator groups), horizontal split

+

The JetBrains Settings dialog as a whole app. A 220px tree lists the verbs, and each verb expands to its option pages (Convert › Format, Quality, Output). A breadcrumb header and titled separators replace cards, and the queue sits in a lower split.

+
+ + Hairline mockup +
07Hairline
+

Zed (thin dock, crisp mono, hairline panes, slim pane headers), VS Code status bar

+

Zed minimalism with enough substance: a 40px icon column, one hairline split between queue and options, mono for data. Quiet, but every verb, row and button carries an icon.

+
+ + Fleet Grid mockup +
08Fleet Grid
+

JetBrains Fleet top toolbar and centred workspace, property grid / key-value table, compact toolbar

+

JetBrains Fleet: a top toolbar instead of a rail, a centred workspace, and options as a crisp key-value property grid. Modern IDE-lite with air, still compact.

+
+ + Walkthrough mockup +
09Walkthrough
+

VS Code Welcome / Walkthrough step list (expanding step, check glyphs), activity bar, status bar

+

Borrowed from VS Code's Get Started walkthroughs: the left pane is a short numbered checklist (Add files, Format, Quality, Output), the active step expands inline to its control, finished steps show a check and their value. Setup becomes a guided menu, the queue fills the rest.

+
+ + Details + Inspector mockup +
10Details + Inspector
+

JetBrains / Explorer list with sortable column headers, Xcode inspector segmented icon tabs, property grid for Info

+

Explorer details view meets the Xcode inspector. The queue is a sortable column table, and the right inspector switches between Options, Preview and Info with icon segments. Power-user, but tidy.

+
+ + Pref Pane mockup +
11Pref Pane
+

Nova / macOS preference-pane icon toolbar (square, monochrome), two-column right-aligned label forms, status bar

+

Panic Nova and classic preference panes: a top toolbar of square verb icons with small labels, and a two-column right-aligned form beside the queue. Reads like a well-made desktop app, not a web page.

+
+ + Options Sidebar mockup +
12Options Sidebar
+

VS Code activity bar, a sidebar view titled like settings with stacked setting rows, editor area as details list, editor header actions

+

An activity bar plus a sidebar that IS the options: flush setting rows stacked in a 320px view, separated by hairlines, no cards, no row icons. The editor area is a large, airy details list of the queue.

+
+ + Run Configurations mockup +
13Run Configurations
+

JetBrains Run/Debug Configurations dialog (config list + form), JetBrains stripe, compact toolbar

+

JetBrains Run/Debug Configurations as the options model: a short list of named targets on the left, the selected one's form on the right, and the queue below. Picking a setup feels like picking a run config.

+
+ + Command Center mockup +
14Command Center
+

VS Code Command Center in the title bar, quick pick (closed trigger), breadcrumbs, status bar

+

The VS Code Command Center drives everything: a flat title-bar field searches verbs, files and settings. The verb and format show as a breadcrumb of dropdown triggers. Minimal chrome for keyboard users.

+
+ + Split Editor mockup +
15Split Editor
+

VS Code split editor groups with their own tabs, property grid, sidebar collapsible sections

+

Two editor groups side by side: the queue on the left, and a per-file before/after view on the right with a property grid. Options sit in a collapsible sidebar section. Best layout for checking results.

+
+ + State Tree mockup +
16State Tree
+

VS Code tree view with indent guides and focus outline, Problems view grouping, inspector disclosure groups

+

The queue is a tree whose top nodes are states (Failed, Running, Queued, Done) with counts, and indent guides lead to the files. Problems surface first, the way an IDE's Problems view does. Options sit as a centred form.

+
+ + Quiet mockup +
17Quiet
+

Sublime Text quiet chrome, tab top-line, single-line inline bar (Find panel), status bar

+

Sublime-quiet: almost no chrome, verb tabs with icon and label in the title bar, and every option squeezed into one inline bar above the queue, like Sublime's Find panel. The window is almost all files.

+
+ + Labelled Bar mockup +
18Labelled Bar
+

Labelled tool-window bar (Android Studio / Fleet), JetBrains property rows, status bar

+

An activity bar with a small label under every icon, like IDE tool-window names, so it is easy to navigate without tooltips, next to a compact property panel. Friendliest for non-developers.

+
+ + Properties Window mockup +
19Properties Window
+

Visual Studio Properties window (categorised grid, description pane), tool window title bars with tab wells, document tab toolbar

+

Classic Visual Studio docking: the queue is a document tab, and a Properties tool window holds the options as a categorised property grid with a description box at the bottom that explains the focused setting. Familiar, structured, explanation only on demand.

+
+ + Navigator mockup +
20Navigator
+

Xcode navigator selector bar and jump bar, JetBrains titled separators, VS Code status bar

+

Xcode's navigator selector bar: the verbs are a row of icons across the top of the left pane, which holds the queue below them. A jump bar names the target, and an inspector holds the options. A single-pane-per-job layout that is not any one IDE.

+
+
+ diff --git a/docs/mockups/terminal-v4/shots/01-activity-explorer.png b/docs/mockups/terminal-v4/shots/01-activity-explorer.png new file mode 100644 index 0000000..90539d5 Binary files /dev/null and b/docs/mockups/terminal-v4/shots/01-activity-explorer.png differ diff --git a/docs/mockups/terminal-v4/shots/02-settings-editor.png b/docs/mockups/terminal-v4/shots/02-settings-editor.png new file mode 100644 index 0000000..8a2ddb9 Binary files /dev/null and b/docs/mockups/terminal-v4/shots/02-settings-editor.png differ diff --git a/docs/mockups/terminal-v4/shots/03-twin-stripes.png b/docs/mockups/terminal-v4/shots/03-twin-stripes.png new file mode 100644 index 0000000..0d34b73 Binary files /dev/null and b/docs/mockups/terminal-v4/shots/03-twin-stripes.png differ diff --git a/docs/mockups/terminal-v4/shots/04-verb-tabs.png b/docs/mockups/terminal-v4/shots/04-verb-tabs.png new file mode 100644 index 0000000..0b23700 Binary files /dev/null and b/docs/mockups/terminal-v4/shots/04-verb-tabs.png differ diff --git a/docs/mockups/terminal-v4/shots/05-project-widget.png b/docs/mockups/terminal-v4/shots/05-project-widget.png new file mode 100644 index 0000000..61e419e Binary files /dev/null and b/docs/mockups/terminal-v4/shots/05-project-widget.png differ diff --git a/docs/mockups/terminal-v4/shots/06-preferences-tree.png b/docs/mockups/terminal-v4/shots/06-preferences-tree.png new file mode 100644 index 0000000..458dbc3 Binary files /dev/null and b/docs/mockups/terminal-v4/shots/06-preferences-tree.png differ diff --git a/docs/mockups/terminal-v4/shots/07-zed-hairline.png b/docs/mockups/terminal-v4/shots/07-zed-hairline.png new file mode 100644 index 0000000..13b7d09 Binary files /dev/null and b/docs/mockups/terminal-v4/shots/07-zed-hairline.png differ diff --git a/docs/mockups/terminal-v4/shots/08-fleet-grid.png b/docs/mockups/terminal-v4/shots/08-fleet-grid.png new file mode 100644 index 0000000..1d67aee Binary files /dev/null and b/docs/mockups/terminal-v4/shots/08-fleet-grid.png differ diff --git a/docs/mockups/terminal-v4/shots/09-walkthrough-steps.png b/docs/mockups/terminal-v4/shots/09-walkthrough-steps.png new file mode 100644 index 0000000..30138ee Binary files /dev/null and b/docs/mockups/terminal-v4/shots/09-walkthrough-steps.png differ diff --git a/docs/mockups/terminal-v4/shots/10-explorer-details.png b/docs/mockups/terminal-v4/shots/10-explorer-details.png new file mode 100644 index 0000000..9669e6e Binary files /dev/null and b/docs/mockups/terminal-v4/shots/10-explorer-details.png differ diff --git a/docs/mockups/terminal-v4/shots/11-nova-prefpane.png b/docs/mockups/terminal-v4/shots/11-nova-prefpane.png new file mode 100644 index 0000000..623d798 Binary files /dev/null and b/docs/mockups/terminal-v4/shots/11-nova-prefpane.png differ diff --git a/docs/mockups/terminal-v4/shots/12-options-sidebar.png b/docs/mockups/terminal-v4/shots/12-options-sidebar.png new file mode 100644 index 0000000..cf02fe7 Binary files /dev/null and b/docs/mockups/terminal-v4/shots/12-options-sidebar.png differ diff --git a/docs/mockups/terminal-v4/shots/13-run-configurations.png b/docs/mockups/terminal-v4/shots/13-run-configurations.png new file mode 100644 index 0000000..a69d743 Binary files /dev/null and b/docs/mockups/terminal-v4/shots/13-run-configurations.png differ diff --git a/docs/mockups/terminal-v4/shots/14-command-center.png b/docs/mockups/terminal-v4/shots/14-command-center.png new file mode 100644 index 0000000..6d61c58 Binary files /dev/null and b/docs/mockups/terminal-v4/shots/14-command-center.png differ diff --git a/docs/mockups/terminal-v4/shots/15-split-editor.png b/docs/mockups/terminal-v4/shots/15-split-editor.png new file mode 100644 index 0000000..5b629e4 Binary files /dev/null and b/docs/mockups/terminal-v4/shots/15-split-editor.png differ diff --git a/docs/mockups/terminal-v4/shots/16-state-tree.png b/docs/mockups/terminal-v4/shots/16-state-tree.png new file mode 100644 index 0000000..538d00e Binary files /dev/null and b/docs/mockups/terminal-v4/shots/16-state-tree.png differ diff --git a/docs/mockups/terminal-v4/shots/17-sublime-quiet.png b/docs/mockups/terminal-v4/shots/17-sublime-quiet.png new file mode 100644 index 0000000..a1b30c4 Binary files /dev/null and b/docs/mockups/terminal-v4/shots/17-sublime-quiet.png differ diff --git a/docs/mockups/terminal-v4/shots/18-labelled-activity.png b/docs/mockups/terminal-v4/shots/18-labelled-activity.png new file mode 100644 index 0000000..626d605 Binary files /dev/null and b/docs/mockups/terminal-v4/shots/18-labelled-activity.png differ diff --git a/docs/mockups/terminal-v4/shots/19-properties-window.png b/docs/mockups/terminal-v4/shots/19-properties-window.png new file mode 100644 index 0000000..6829593 Binary files /dev/null and b/docs/mockups/terminal-v4/shots/19-properties-window.png differ diff --git a/docs/mockups/terminal-v4/shots/20-navigator-selector.png b/docs/mockups/terminal-v4/shots/20-navigator-selector.png new file mode 100644 index 0000000..e506d52 Binary files /dev/null and b/docs/mockups/terminal-v4/shots/20-navigator-selector.png differ diff --git a/docs/mockups/terminal-v4/shots/_index.png b/docs/mockups/terminal-v4/shots/_index.png new file mode 100644 index 0000000..677e39e Binary files /dev/null and b/docs/mockups/terminal-v4/shots/_index.png differ diff --git a/docs/mockups/terminal-v5/01-a-refined.html b/docs/mockups/terminal-v5/01-a-refined.html new file mode 100644 index 0000000..efa5a8c --- /dev/null +++ b/docs/mockups/terminal-v5/01-a-refined.html @@ -0,0 +1,518 @@ + + + + + + +Filesmith Refined + + + + + + + + + + +
+
Filesmith
+ + +
+ + + +
+
+ +
+ + + + +
+
+ + + + + + +
+ 6 files, 1 selected + +
+
+ +
+
+
Name
+
Kind
+
Size
+
Result
+
Status
+
+ +
+
IMG_4821.HEIC
+
HEIC
+
3.2 MB
+
640 KB−80%
+
Done
+
+ +
+
product-shot.jpg
+
JPG
+
2.1 MB
+
486 KB−77%
+
Done
+
+ +
+
screenshot-2026-10-03.png
+
PNG
+
1.8 MB
+
62%
+
Converting
+
+ +
+
hero-banner@2x.png
+
PNG
+
4.6 MB
+
+
Queued
+
+ +
+
scan_0091.tiff
+
TIFF
+
22.4 MB
+
+
Unsupported compression
+
+ +
+
logo-final.bmp
+
BMP
+
1.1 MB
+
+
Queued
+
+ +
+
+
6 files
+
+
35.2 MB
+
5.3 MB1.1 MBso far
+
2 of 6 done
+
+
+
+ + + +
+ + + + + + diff --git a/docs/mockups/terminal-v5/02-b-sectioned-inspector.html b/docs/mockups/terminal-v5/02-b-sectioned-inspector.html new file mode 100644 index 0000000..c5c13af --- /dev/null +++ b/docs/mockups/terminal-v5/02-b-sectioned-inspector.html @@ -0,0 +1,455 @@ + + + + + + +Filesmith Sectioned + + + + + + + + + +
+
Filesmith
+ + +
+ + + +
+
+ +
+ + + +
+
+ + + + + +
+ 6 files, 1 selected + +
+
+ +
+
+
Name
+
Kind
+
Size
+
Result
+
Status
+
+ +
+
IMG_4821.HEIC
+
HEIC
+
3.2 MB
+
640 KB−80%
+
Done
+
+ +
+
product-shot.jpg
+
JPG
+
2.1 MB
+
486 KB−77%
+
Done
+
+ +
+
screenshot-2026-10-03.png
+
PNG
+
1.8 MB
+
62%
+
Converting
+
+ +
+
hero-banner@2x.png
+
PNG
+
4.6 MB
+
+
Queued
+
+ +
+
scan_0091.tiff
+
TIFF
+
22.4 MB
+
+
Unsupported compression
+
+ +
+
logo-final.bmp
+
BMP
+
1.1 MB
+
+
Queued
+
+ +
+
+
6 files
+
+
35.2 MB
+
5.3 MB1.1 MBso far
+
2 of 6 done
+
+
+
+ + + +
+ + + + + + diff --git a/docs/mockups/terminal-v5/03-c-stacked-settings.html b/docs/mockups/terminal-v5/03-c-stacked-settings.html new file mode 100644 index 0000000..b2f7694 --- /dev/null +++ b/docs/mockups/terminal-v5/03-c-stacked-settings.html @@ -0,0 +1,520 @@ + + + + + + +Filesmith Stacked Settings + + + + + + + + + + +
+
Filesmith
+ + +
+ + + +
+
+ +
+ + + + +
+
+ + + + + +
+ 6 files, 1 selected + + +
+
+ +
+
+ +
+ + IMG_4821.HEICDone +
HEIC3.2 MB640 KB−80%
+ +
+ +
+ + product-shot.jpgDone +
JPG2.1 MB486 KB−77%
+ +
+ +
+ + screenshot-2026-10-03.pngConverting +
PNG1.8 MB62%
+ +
+ +
+ + hero-banner@2x.pngQueued +
PNG4.6 MB
+ +
+ +
+ + scan_0091.tiffUnsupported compression +
TIFF22.4 MBRe-save the scan as LZW or uncompressed TIFF, then retry
+ +
+ +
+ + logo-final.bmpQueued +
BMP1.1 MB
+ +
+ +
+
+
+ 6 files + 35.2 MB + 5.3 MB1.1 MBso far + 2 of 6 done +
+
+
+ + + +
+ + + + + + diff --git a/docs/mockups/terminal-v5/04-d-preview-first.html b/docs/mockups/terminal-v5/04-d-preview-first.html new file mode 100644 index 0000000..2f1669f --- /dev/null +++ b/docs/mockups/terminal-v5/04-d-preview-first.html @@ -0,0 +1,511 @@ + + + + + + +Filesmith Preview First + + + + + + + + + + +
+
Filesmith
+ + +
+ + + +
+
+ +
+ + + +
+
+ + + + +
+ 6 files, 1 selected + +
+
+ +
+
+
Name
+
Kind
+
Size
+
Result
+
+
+ +
+ + +
+
screenshot-2026-10-03.png
+
PNG
+
1.8 MB
+
62%Converting to WebP
+
+
+ + + +
+
hero-banner@2x.png
+
PNG
+
4.6 MB
+
Next up, to WebP
+
+
+
+
logo-final.bmp
+
BMP
+
1.1 MB
+
2nd in line, to WebP
+
+
+ + + +
+
IMG_4821.HEIC
+
HEIC
+
3.2 MB
+
640 KB−80%IMG_4821.webp
+
+
+
+
product-shot.jpg
+
JPG
+
2.1 MB
+
486 KB−77%product-shot.webp
+
+
+ + +
Failed1
+
+
scan_0091.tiff
+
TIFF
+
22.4 MB
+
Unsupported compressionLZW, 16-bit planar
+
+
+
+ +
+
6 files
+
+
35.2 MB
+
5.3 MB1.1 MBso far2 of 6 done
+
+
+
+
+ + + +
+ + + + + + diff --git a/docs/mockups/terminal-v5/05-e-flush-panels.html b/docs/mockups/terminal-v5/05-e-flush-panels.html new file mode 100644 index 0000000..0fab48e --- /dev/null +++ b/docs/mockups/terminal-v5/05-e-flush-panels.html @@ -0,0 +1,511 @@ + + + + + + +Filesmith Flush + + + + + + + + + +
+
Filesmith
+ + +
+ + + +
+
+ +
+ + + +
+
+ + + + +
+ 6 files, 1 selected + +
+
+ +
+
+
+
name
+
kind
+
size
+
result
+
status
+
+ +
+
+
IMG_4821.HEIC
+
HEIC
+
3.2 MB
+
640 KB-80%
+
Done
+
+ +
+
+
product-shot.jpg
+
JPG
+
2.1 MB
+
486 KB-77%
+
Done
+
+ +
+
+
screenshot-2026-10-03.png
+
PNG
+
1.8 MB
+
~410 KB-77%
+
62%(4s)
+
+ +
+
+
hero-banner@2x.png
+
PNG
+
4.6 MB
+
+
Queued
+
+ +
+
+
scan_0091.tiff
+
TIFF
+
22.4 MB
+
+
Unsupported compression
+
+ +
+
+
logo-final.bmp
+
BMP
+
1.1 MB
+
+
Queued
+
+ +
+
+
+
6 files
+
+
35.2 MB
+
5.3 MB1.1 MBso far
+
2 of 6 done
+
+
+
+ + +
+ + + + + + diff --git a/docs/mockups/terminal-v5/05-e1-boxed.html b/docs/mockups/terminal-v5/05-e1-boxed.html new file mode 100644 index 0000000..1deba40 --- /dev/null +++ b/docs/mockups/terminal-v5/05-e1-boxed.html @@ -0,0 +1,528 @@ + + + + + + +Filesmith Flush Boxed + + + + + + + + + +
+
Filesmith
+ + +
+ + + +
+
+ +
+ + + +
+
+ + + + +
+ 6 files, 1 selected + +
+
+ +
+
+
+
name
+
kind
+
size
+
result
+
status
+
+ +
+
+
IMG_4821.HEIC
+
HEIC
+
3.2 MB
+
640 KB-80%
+
Done
+
+ +
+
+
product-shot.jpg
+
JPG
+
2.1 MB
+
486 KB-77%
+
Done
+
+ +
+
+
screenshot-2026-10-03.png
+
PNG
+
1.8 MB
+
~410 KB-77%
+
62%(4s)
+
+ +
+
+
hero-banner@2x.png
+
PNG
+
4.6 MB
+
+
Queued
+
+ +
+
+
scan_0091.tiff
+
TIFF
+
22.4 MB
+
+
Unsupported compression
+
+ +
+
+
logo-final.bmp
+
BMP
+
1.1 MB
+
+
Queued
+
+ +
+
+
+
6 files
+
+
35.2 MB
+
5.3 MB1.1 MBso far
+
2 of 6 done
+
+
+
+ + +
+ + + + + + diff --git a/docs/mockups/terminal-v5/05-e2-raised.html b/docs/mockups/terminal-v5/05-e2-raised.html new file mode 100644 index 0000000..80e1a8d --- /dev/null +++ b/docs/mockups/terminal-v5/05-e2-raised.html @@ -0,0 +1,529 @@ + + + + + + +Filesmith Flush + + + + + + + + + +
+
Filesmith
+ + +
+ + + +
+
+ +
+ + + +
+
+ + + + +
+ 6 files, 1 selected + +
+
+ +
+
+
+
name
+
kind
+
size
+
result
+
status
+
+ +
+
+
IMG_4821.HEIC
+
HEIC
+
3.2 MB
+
640 KB-80%
+
Done
+
+ +
+
+
product-shot.jpg
+
JPG
+
2.1 MB
+
486 KB-77%
+
Done
+
+ +
+
+
screenshot-2026-10-03.png
+
PNG
+
1.8 MB
+
~410 KB-77%
+
62%(4s)
+
+ +
+
+
hero-banner@2x.png
+
PNG
+
4.6 MB
+
+
Queued
+
+ +
+
+
scan_0091.tiff
+
TIFF
+
22.4 MB
+
+
Unsupported compression
+
+ +
+
+
logo-final.bmp
+
BMP
+
1.1 MB
+
+
Queued
+
+ +
+
+
+
6 files
+
+
35.2 MB
+
5.3 MB1.1 MBso far
+
2 of 6 done
+
+
+
+ + +
+ + + + + + diff --git a/docs/mockups/terminal-v5/05-e3-bracketed.html b/docs/mockups/terminal-v5/05-e3-bracketed.html new file mode 100644 index 0000000..3cbd420 --- /dev/null +++ b/docs/mockups/terminal-v5/05-e3-bracketed.html @@ -0,0 +1,535 @@ + + + + + + +Filesmith Bracketed + + + + + + + + + +
+
Filesmith
+ + +
+ + + +
+
+ +
+ + + +
+
+ + + + +
+ 6 files, 1 selected + +
+
+ +
+
+
+
name
+
kind
+
size
+
result
+
status
+
+ +
+
+
IMG_4821.HEIC
+
HEIC
+
3.2 MB
+
640 KB-80%
+
Done
+
+ +
+
+
product-shot.jpg
+
JPG
+
2.1 MB
+
486 KB-77%
+
Done
+
+ +
+
+
screenshot-2026-10-03.png
+
PNG
+
1.8 MB
+
~410 KB-77%
+
62%(4s)
+
+ +
+
+
hero-banner@2x.png
+
PNG
+
4.6 MB
+
+
Queued
+
+ +
+
+
scan_0091.tiff
+
TIFF
+
22.4 MB
+
+
Unsupported compression
+
+ +
+
+
logo-final.bmp
+
BMP
+
1.1 MB
+
+
Queued
+
+ +
+
+
+
6 files
+
+
35.2 MB
+
5.3 MB1.1 MBso far
+
2 of 6 done
+
+
+
+ + +
+ + + + + + diff --git a/docs/mockups/terminal-v5/06-t1-table-boxed.html b/docs/mockups/terminal-v5/06-t1-table-boxed.html new file mode 100644 index 0000000..cea70b8 --- /dev/null +++ b/docs/mockups/terminal-v5/06-t1-table-boxed.html @@ -0,0 +1,572 @@ + + + + + + +Filesmith Table Boxed + + + + + + + + + +
+
Filesmith
+ + +
+ + + +
+
+ +
+ + + +
+
+ + + + +
+ 6 files, 1 selected + +
+
+ +
+
+
+
name
+
kind
+
size
+
result
+
status
+
+ +
+
+
IMG_4821.HEIC
+
HEIC
+
3.2 MB
+
640 KB-80%
+
Done
+
+ +
+
+
product-shot.jpg
+
JPG
+
2.1 MB
+
486 KB-77%
+
Done
+
+ +
+
+
screenshot-2026-10-03.png
+
PNG
+
1.8 MB
+
~410 KB-77%
+
62%(4s)
+
+ +
+
+
hero-banner@2x.png
+
PNG
+
4.6 MB
+
+
Queued
+
+ +
+
+
scan_0091.tiff
+
TIFF
+
22.4 MB
+
+
Unsupported compression
+
+ +
+
+
logo-final.bmp
+
BMP
+
1.1 MB
+
+
Queued
+
+ +
+
+
+
6 files
+
+
35.2 MB
+
5.3 MB1.1 MBso far
+
2 of 6 done
+
+
+
+ + +
+ + + + + + diff --git a/docs/mockups/terminal-v5/07-t2-table-filled.html b/docs/mockups/terminal-v5/07-t2-table-filled.html new file mode 100644 index 0000000..5c2dfeb --- /dev/null +++ b/docs/mockups/terminal-v5/07-t2-table-filled.html @@ -0,0 +1,546 @@ + + + + + + +Filesmith Table Filled + + + + + + + + + +
+
Filesmith
+ + +
+ + + +
+
+ +
+ + + +
+
+ + + + +
+ 6 files, 1 selected + +
+
+ +
+
+
+
name
+
kind
+
size
+
result
+
status
+
+ +
+
+
IMG_4821.HEIC
+
HEIC
+
3.2 MB
+
640 KB-80%
+
Done
+
+ +
+
+
product-shot.jpg
+
JPG
+
2.1 MB
+
486 KB-77%
+
Done
+
+ +
+
+
screenshot-2026-10-03.png
+
PNG
+
1.8 MB
+
~410 KB-77%
+
62%(4s)
+
+ +
+
+
hero-banner@2x.png
+
PNG
+
4.6 MB
+
+
Queued
+
+ +
+
+
scan_0091.tiff
+
TIFF
+
22.4 MB
+
+
Unsupported compression
+
+ +
+
+
logo-final.bmp
+
BMP
+
1.1 MB
+
+
Queued
+
+ +
+
+
+
6 files
+
+
35.2 MB
+
5.3 MB1.1 MBso far
+
2 of 6 done
+
+
+
+ + +
+ + + + + + diff --git a/docs/mockups/terminal-v5/08-t3-table-zebra.html b/docs/mockups/terminal-v5/08-t3-table-zebra.html new file mode 100644 index 0000000..e1fd90a --- /dev/null +++ b/docs/mockups/terminal-v5/08-t3-table-zebra.html @@ -0,0 +1,631 @@ + + + + + + +Filesmith Zebra Grid + + + + + + + + + +
+
Filesmith
+ + +
+ + + +
+
+ +
+ + + +
+
+ + + + +
+ 6 files, 1 selected + +
+
+ +
+
+
+
name
+
kind
+
size
+
result
+
status
+
+ +
+
+
IMG_4821.HEIC
+
HEIC
+
3.2 MB
+
640 KB-80%
+
Done
+
+ +
+
+
product-shot.jpg
+
JPG
+
2.1 MB
+
486 KB-77%
+
Done
+
+ +
+
+
screenshot-2026-10-03.png
+
PNG
+
1.8 MB
+
~410 KB-77%
+
62%(4s)
+
+ +
+
+
hero-banner@2x.png
+
PNG
+
4.6 MB
+
+
Queued
+
+ +
+
+
scan_0091.tiff
+
TIFF
+
22.4 MB
+
+
Unsupported compression
+
+ +
+
+
logo-final.bmp
+
BMP
+
1.1 MB
+
+
Queued
+
+ +
+
+
+
6 files
+
+
35.2 MB
+
5.3 MB1.1 MBso far
+
2 of 6 done
+
+
+
+ + +
+ + + + + + diff --git a/docs/mockups/terminal-v5/09-s1-vscode-classic.html b/docs/mockups/terminal-v5/09-s1-vscode-classic.html new file mode 100644 index 0000000..072d706 --- /dev/null +++ b/docs/mockups/terminal-v5/09-s1-vscode-classic.html @@ -0,0 +1,576 @@ + + + + + + +Filesmith VS Code Classic + + + + + + + + + +
+
Filesmith
+ + +
+ + + +
+
+ +
+ + + +
+
+ + + + +
+ 6 files, 1 selected + +
+
+ +
+
+
+
name
+
kind
+
size
+
result
+
status
+
+ +
+
+
IMG_4821.HEIC
+
HEIC
+
3.2 MB
+
640 KB-80%
+
Done
+
+ +
+
+
product-shot.jpg
+
JPG
+
2.1 MB
+
486 KB-77%
+
Done
+
+ +
+
+
screenshot-2026-10-03.png
+
PNG
+
1.8 MB
+
~410 KB-77%
+
62%(4s)
+
+ +
+
+
hero-banner@2x.png
+
PNG
+
4.6 MB
+
+
Queued
+
+ +
+
+
scan_0091.tiff
+
TIFF
+
22.4 MB
+
+
Unsupported compression
+
+ +
+
+
logo-final.bmp
+
BMP
+
1.1 MB
+
+
Queued
+
+ +
+
+
+
6 files
+
+
35.2 MB
+
5.3 MB1.1 MBso far
+
2 of 6 done
+
+
+
+ + +
+ + + + + + diff --git a/docs/mockups/terminal-v5/10-s2-vscode-grouped.html b/docs/mockups/terminal-v5/10-s2-vscode-grouped.html new file mode 100644 index 0000000..c87e5c7 --- /dev/null +++ b/docs/mockups/terminal-v5/10-s2-vscode-grouped.html @@ -0,0 +1,602 @@ + + + + + + +Filesmith Settings Grouped + + + + + + + + + +
+
Filesmith
+ + +
+ + + +
+
+ +
+ + + +
+
+ + + + +
+ 6 files, 1 selected + +
+
+ +
+
+
+
name
+
kind
+
size
+
result
+
status
+
+ +
+
+
IMG_4821.HEIC
+
HEIC
+
3.2 MB
+
640 KB-80%
+
Done
+
+ +
+
+
product-shot.jpg
+
JPG
+
2.1 MB
+
486 KB-77%
+
Done
+
+ +
+
+
screenshot-2026-10-03.png
+
PNG
+
1.8 MB
+
~410 KB-77%
+
62%(4s)
+
+ +
+
+
hero-banner@2x.png
+
PNG
+
4.6 MB
+
+
Queued
+
+ +
+
+
scan_0091.tiff
+
TIFF
+
22.4 MB
+
+
Unsupported compression
+
+ +
+
+
logo-final.bmp
+
BMP
+
1.1 MB
+
+
Queued
+
+ +
+
+
+
6 files
+
+
35.2 MB
+
5.3 MB1.1 MBso far
+
2 of 6 done
+
+
+
+ + +
+ + + + + + diff --git a/docs/mockups/terminal-v5/11-s3-vscode-compact.html b/docs/mockups/terminal-v5/11-s3-vscode-compact.html new file mode 100644 index 0000000..d2cc2be --- /dev/null +++ b/docs/mockups/terminal-v5/11-s3-vscode-compact.html @@ -0,0 +1,588 @@ + + + + + + +Filesmith VS Code Compact + + + + + + + + + +
+
Filesmith
+ + +
+ + + +
+
+ +
+ + + +
+
+ + + + +
+ 6 files, 1 selected + +
+
+ +
+
+
+
name
+
kind
+
size
+
result
+
status
+
+ +
+
+
IMG_4821.HEIC
+
HEIC
+
3.2 MB
+
640 KB-80%
+
Done
+
+ +
+
+
product-shot.jpg
+
JPG
+
2.1 MB
+
486 KB-77%
+
Done
+
+ +
+
+
screenshot-2026-10-03.png
+
PNG
+
1.8 MB
+
~410 KB-77%
+
62%(4s)
+
+ +
+
+
hero-banner@2x.png
+
PNG
+
4.6 MB
+
+
Queued
+
+ +
+
+
scan_0091.tiff
+
TIFF
+
22.4 MB
+
+
Unsupported compression
+
+ +
+
+
logo-final.bmp
+
BMP
+
1.1 MB
+
+
Queued
+
+ +
+
+
+
6 files
+
+
35.2 MB
+
5.3 MB1.1 MBso far
+
2 of 6 done
+
+
+
+ + +
+ + + + + + diff --git a/docs/mockups/terminal-v5/index.html b/docs/mockups/terminal-v5/index.html new file mode 100644 index 0000000..bb9b4cc --- /dev/null +++ b/docs/mockups/terminal-v5/index.html @@ -0,0 +1,72 @@ + + +Filesmith Explorer Variants + +

Filesmith / terminal-v5 / explorer-details variants

+

5 takes on terminal-v4 10-explorer-details. Every one has a collapsible left sidebar that shows labels and icons, and none has the files-added line at the top. They differ in the right-side panel and the main view. Each row shows the sidebar expanded and collapsed. Click to open the mockup.

+
+
+ +

Closest to the original: the explorer-details layout with a collapsible labelled sidebar and no files-added line at the top.

+ +
+
+ +

Right panel without top tabs: one scrolling inspector with JetBrains-style titled separators (FORMAT, QUALITY, OUTPUT, NAMING), label-left/control-right rows and a small summary of the selected file.

+ +
+
+ +

Right panel like the VS Code settings editor: each setting stacked (bold title, grey description, full-width control), generous rhythm, collapsible groups.

+ +
+
+ +

Right panel opens with a compact before/after preview of the selected file, with the options below it.

+ +
+
+ +

Flatter and more brutalist: no rounded corners, panels split by 1px lines only, segmented controls without fills (selected = underline or inverted text).

+ +
+
+ diff --git a/docs/mockups/terminal-v5/shots/01-a-refined-collapsed.png b/docs/mockups/terminal-v5/shots/01-a-refined-collapsed.png new file mode 100644 index 0000000..7015710 Binary files /dev/null and b/docs/mockups/terminal-v5/shots/01-a-refined-collapsed.png differ diff --git a/docs/mockups/terminal-v5/shots/01-a-refined.png b/docs/mockups/terminal-v5/shots/01-a-refined.png new file mode 100644 index 0000000..7016974 Binary files /dev/null and b/docs/mockups/terminal-v5/shots/01-a-refined.png differ diff --git a/docs/mockups/terminal-v5/shots/02-b-sectioned-inspector-collapsed.png b/docs/mockups/terminal-v5/shots/02-b-sectioned-inspector-collapsed.png new file mode 100644 index 0000000..3afadc0 Binary files /dev/null and b/docs/mockups/terminal-v5/shots/02-b-sectioned-inspector-collapsed.png differ diff --git a/docs/mockups/terminal-v5/shots/02-b-sectioned-inspector.png b/docs/mockups/terminal-v5/shots/02-b-sectioned-inspector.png new file mode 100644 index 0000000..ae2dc2d Binary files /dev/null and b/docs/mockups/terminal-v5/shots/02-b-sectioned-inspector.png differ diff --git a/docs/mockups/terminal-v5/shots/03-c-stacked-settings-collapsed.png b/docs/mockups/terminal-v5/shots/03-c-stacked-settings-collapsed.png new file mode 100644 index 0000000..ae7c74b Binary files /dev/null and b/docs/mockups/terminal-v5/shots/03-c-stacked-settings-collapsed.png differ diff --git a/docs/mockups/terminal-v5/shots/03-c-stacked-settings.png b/docs/mockups/terminal-v5/shots/03-c-stacked-settings.png new file mode 100644 index 0000000..4fc5c07 Binary files /dev/null and b/docs/mockups/terminal-v5/shots/03-c-stacked-settings.png differ diff --git a/docs/mockups/terminal-v5/shots/04-d-preview-first-collapsed.png b/docs/mockups/terminal-v5/shots/04-d-preview-first-collapsed.png new file mode 100644 index 0000000..a6149d9 Binary files /dev/null and b/docs/mockups/terminal-v5/shots/04-d-preview-first-collapsed.png differ diff --git a/docs/mockups/terminal-v5/shots/04-d-preview-first.png b/docs/mockups/terminal-v5/shots/04-d-preview-first.png new file mode 100644 index 0000000..083a154 Binary files /dev/null and b/docs/mockups/terminal-v5/shots/04-d-preview-first.png differ diff --git a/docs/mockups/terminal-v5/shots/05-e-flush-panels-collapsed.png b/docs/mockups/terminal-v5/shots/05-e-flush-panels-collapsed.png new file mode 100644 index 0000000..f3e4eea Binary files /dev/null and b/docs/mockups/terminal-v5/shots/05-e-flush-panels-collapsed.png differ diff --git a/docs/mockups/terminal-v5/shots/05-e-flush-panels.png b/docs/mockups/terminal-v5/shots/05-e-flush-panels.png new file mode 100644 index 0000000..cbbc564 Binary files /dev/null and b/docs/mockups/terminal-v5/shots/05-e-flush-panels.png differ diff --git a/docs/mockups/terminal-v5/shots/05-e1-boxed.png b/docs/mockups/terminal-v5/shots/05-e1-boxed.png new file mode 100644 index 0000000..dd8f575 Binary files /dev/null and b/docs/mockups/terminal-v5/shots/05-e1-boxed.png differ diff --git a/docs/mockups/terminal-v5/shots/05-e2-raised.png b/docs/mockups/terminal-v5/shots/05-e2-raised.png new file mode 100644 index 0000000..ea070f3 Binary files /dev/null and b/docs/mockups/terminal-v5/shots/05-e2-raised.png differ diff --git a/docs/mockups/terminal-v5/shots/05-e3-bracketed.png b/docs/mockups/terminal-v5/shots/05-e3-bracketed.png new file mode 100644 index 0000000..80124dc Binary files /dev/null and b/docs/mockups/terminal-v5/shots/05-e3-bracketed.png differ diff --git a/docs/mockups/terminal-v5/shots/06-t1-table-boxed.png b/docs/mockups/terminal-v5/shots/06-t1-table-boxed.png new file mode 100644 index 0000000..6f50041 Binary files /dev/null and b/docs/mockups/terminal-v5/shots/06-t1-table-boxed.png differ diff --git a/docs/mockups/terminal-v5/shots/07-t2-table-filled.png b/docs/mockups/terminal-v5/shots/07-t2-table-filled.png new file mode 100644 index 0000000..13d200b Binary files /dev/null and b/docs/mockups/terminal-v5/shots/07-t2-table-filled.png differ diff --git a/docs/mockups/terminal-v5/shots/08-t3-table-zebra.png b/docs/mockups/terminal-v5/shots/08-t3-table-zebra.png new file mode 100644 index 0000000..ec50223 Binary files /dev/null and b/docs/mockups/terminal-v5/shots/08-t3-table-zebra.png differ diff --git a/docs/mockups/terminal-v5/shots/09-s1-vscode-classic.png b/docs/mockups/terminal-v5/shots/09-s1-vscode-classic.png new file mode 100644 index 0000000..c69538a Binary files /dev/null and b/docs/mockups/terminal-v5/shots/09-s1-vscode-classic.png differ diff --git a/docs/mockups/terminal-v5/shots/10-s2-vscode-grouped.png b/docs/mockups/terminal-v5/shots/10-s2-vscode-grouped.png new file mode 100644 index 0000000..ff50f70 Binary files /dev/null and b/docs/mockups/terminal-v5/shots/10-s2-vscode-grouped.png differ diff --git a/docs/mockups/terminal-v5/shots/11-s3-vscode-compact.png b/docs/mockups/terminal-v5/shots/11-s3-vscode-compact.png new file mode 100644 index 0000000..5da33dc Binary files /dev/null and b/docs/mockups/terminal-v5/shots/11-s3-vscode-compact.png differ diff --git a/docs/mockups/terminal-v5/shots/_index.png b/docs/mockups/terminal-v5/shots/_index.png new file mode 100644 index 0000000..27d48dc Binary files /dev/null and b/docs/mockups/terminal-v5/shots/_index.png differ diff --git a/docs/mockups/terminal-v5/shots/impl-10-s2.png b/docs/mockups/terminal-v5/shots/impl-10-s2.png new file mode 100644 index 0000000..34c1d9b Binary files /dev/null and b/docs/mockups/terminal-v5/shots/impl-10-s2.png differ diff --git a/docs/mockups/terminal-v5/shots/impl-collapsed.png b/docs/mockups/terminal-v5/shots/impl-collapsed.png new file mode 100644 index 0000000..1f5e735 Binary files /dev/null and b/docs/mockups/terminal-v5/shots/impl-collapsed.png differ diff --git a/docs/mockups/terminal-v5/shots/impl-completed.png b/docs/mockups/terminal-v5/shots/impl-completed.png new file mode 100644 index 0000000..5cec0d2 Binary files /dev/null and b/docs/mockups/terminal-v5/shots/impl-completed.png differ diff --git a/docs/mockups/terminal-v5/shots/impl-empty.png b/docs/mockups/terminal-v5/shots/impl-empty.png new file mode 100644 index 0000000..2f36ef7 Binary files /dev/null and b/docs/mockups/terminal-v5/shots/impl-empty.png differ diff --git a/docs/mockups/terminal-v5/shots/impl-generate.png b/docs/mockups/terminal-v5/shots/impl-generate.png new file mode 100644 index 0000000..4a89abc Binary files /dev/null and b/docs/mockups/terminal-v5/shots/impl-generate.png differ diff --git a/docs/mockups/terminal-v5/shots/impl-info.png b/docs/mockups/terminal-v5/shots/impl-info.png new file mode 100644 index 0000000..15619f2 Binary files /dev/null and b/docs/mockups/terminal-v5/shots/impl-info.png differ diff --git a/docs/mockups/terminal-v5/shots/impl-menu.png b/docs/mockups/terminal-v5/shots/impl-menu.png new file mode 100644 index 0000000..87743bc Binary files /dev/null and b/docs/mockups/terminal-v5/shots/impl-menu.png differ diff --git a/docs/mockups/terminal-v5/shots/impl-preview.png b/docs/mockups/terminal-v5/shots/impl-preview.png new file mode 100644 index 0000000..8fc6514 Binary files /dev/null and b/docs/mockups/terminal-v5/shots/impl-preview.png differ diff --git a/docs/mockups/terminal-v5/shots/impl-settings.png b/docs/mockups/terminal-v5/shots/impl-settings.png new file mode 100644 index 0000000..479cc1e Binary files /dev/null and b/docs/mockups/terminal-v5/shots/impl-settings.png differ diff --git a/docs/mockups/terminal-v5/shots/impl-tools.png b/docs/mockups/terminal-v5/shots/impl-tools.png new file mode 100644 index 0000000..198948f Binary files /dev/null and b/docs/mockups/terminal-v5/shots/impl-tools.png differ diff --git a/docs/mockups/terminal/01-wind-native.html b/docs/mockups/terminal/01-wind-native.html new file mode 100644 index 0000000..64fb829 --- /dev/null +++ b/docs/mockups/terminal/01-wind-native.html @@ -0,0 +1,440 @@ + + + + + + +Filesmith · Wind Native + + + +
+ + +
+
+ + Filesmith +
+ convert + + + + +
+ +
+ + + + +
+
+
+
+

Convert

+

Change format

+
+
8 files4 groups
+
+ +
+ +
+
+ + Drop files here or browse + +
+ + +
+
Images4 imagesselected +
+ +
+
+
IMG_4821.HEIC
HEIC → WebP · 3.2 MB
+
3.2 MB → 640 KB  -80%
+
+
+
+ +
+
+
screenshot-2026-10-03 141522.png
PNG → WebP · 1.8 MB
+
62%  ETA 4 s
+
+
+
+
+ +
+
+
hero-banner@2x.png
PNG → WebP · 4.6 MB
+
Queued
+
+
+
+ +
+
+
scan_receipt_0091.tiff
TIFF → WebP · 22.4 MB
+
Unsupported TIFF compression (JPEG-in-TIFF, old-style)
+
+
+
+
+ + +
+
Video1 videoDifferent group, click to switch
+
+
MOV
+
screen-recording-standup.mov
MOV → MP4 · 812 MB
+
18.4%  ETA 3 min 12 s
+
+
+
+
+
+ + +
+
Documents2 documents
+
+
PDF
+
Q3-board-deck.pdf
PDF → DOCX · 14.1 MB
+
Queued
+
+
+
+
+
TXT
+
release-notes-0.3.txt
TXT → PDF · 6 KB
+
6 KB → 48 KB
+
+
+ +
+
+ + +
+
Archives1 archive
+
+
CBZ
+
One-Piece-v104.cbz
CBZ → CB7 · 186 MB
+
Queued
+
+
+
+
+
+ + + +
+
+
+ + + +
+ + diff --git a/docs/mockups/terminal/02-wind-dense-inspector.html b/docs/mockups/terminal/02-wind-dense-inspector.html new file mode 100644 index 0000000..e7aad97 --- /dev/null +++ b/docs/mockups/terminal/02-wind-dense-inspector.html @@ -0,0 +1,483 @@ + + + + + +Filesmith · Dense Inspector + + + + +
+ + +
+
+ + Filesmith +
+ convert / images + + + + +
+ +
+ + + + + +
+
+

Convert

+ Change format + 8 files + + + +
+ +
+ + add filesCtrl Oor drop anywhere in this window + + images · video · audio · pdf · docs · archives +
+ +
+
+ + + + + + + + +
+ + +
+ + Images4 images · active group + 31.9 MB · → WebP +
+ +
+ + HEIC + IMG_4821.HEIC + 3.2 MB + HEIC→WebP + 3.2M → 640K -80% + done + +
+ +
+ + PNG + screenshot-2026-10-03 141522.png + 1.8 MB + PNG→WebP + running · eta 4s + 62% + +
+ +
+ + PNG + hero-banner@2x.png + 4.6 MB + PNG→WebP + queued · #1 + – + +
+ +
+ + TIFF + scan_receipt_0091.tiff
failed  Unsupported TIFF compression (JPEG-in-TIFF, old-style). Re-save as standard TIFF or PNG and retry.
+ 22.4 MB + TIFF→WebP + failed · Unsupported TIFF compression (JPEG-in-TIFF, old-style) + err + +
+ + +
+ + Video1 video · select to switch group + 812 MB · → MP4 +
+
+ + MOV + screen-recording-standup.mov + 812 MB + MOV→MP4 + running · eta 3m 12s + 18.4% + +
+ + +
+ + Documents2 documents + 14.1 MB · mixed +
+
+ + PDF + Q3-board-deck.pdf + 14.1 MB + PDF→DOCX + queued + – + +
+
+ + TXT + release-notes-0.3.txt + 6 KB + TXT→PDF + 6K → 48K +700% + done + + +
+ + +
+ + Archives1 archive + 186 MB · → CB7 +
+
+ + CBZ + One-Piece-v104.cbz + 186 MB + CBZ→CB7 + queued + – + +
+ +
drop more files anywhere · they join the matching group
+
+ 8 rows · 4 groups · 1.03 GB in + ↑↓ moveSpace selectDel removeCtrl Enter convert +
+
+
+ + + +
+ + + +
+ + diff --git a/docs/mockups/terminal/03-tmux-panes.html b/docs/mockups/terminal/03-tmux-panes.html new file mode 100644 index 0000000..7bdce06 --- /dev/null +++ b/docs/mockups/terminal/03-tmux-panes.html @@ -0,0 +1,561 @@ + + + + + + +Filesmith, Tmux Panes + + + + + + +
+ + +
+ + Filesmith + session filesmith : window 0:convert + + Alt0-6 switch verb   Tab next pane +
+ + + +
+
+ +
+ + + + +
+ +
+ ─ queue [8] ─ +
+

Convert

+ Change format + 8 files +
+ + +
+ +
──images(4) · selected
+ +
+ ✓heic + IMG_4821.HEIC3.2M + heic→webp + 3.2M → 640K -80% + +
+
+ ▸png + screenshot-2026-10-03 141522.png1.8M + png→webp + [██████▌····] 62% eta 4s + +
+ +
+ ·png + hero-banner@2x.png4.6M + png→webp + queued + +
+ +
+ ✗tiff + scan_receipt_0091.tiff22.4M + tiff→webp + failed + +
+
└ Unsupported TIFF compression (JPEG-in-TIFF, old-style)
+ +
──video(1)· other group, not in selection
+ +
+ ▸mov + screen-recording-standup.mov812M + mov→mp4 + [██·········] 18.4% eta 3m12s + +
+ +
──documents(2)
+ +
+ ·pdf + Q3-board-deck.pdf14.1M + pdf→docx + queued + +
+ +
+ ✓txt + release-notes-0.3.txt6K + txt→pdf + 6K → 48K + + +
+ +
──archives(1)
+ +
+ ·cbz + One-Piece-v104.cbz186M + cbz→cb7 + queued + +
+
+ +
+ 2 of 8 done · 2 running · 1 failed + [████▌······]41% + + gpu RTX 5090 · Real-ESRGAN ready + +
+
+ + +
+ ─ log [2 running] ─ +

magick "screenshot-2026-10-03 141522.png" -quality 80 -define webp:method=4 "screenshot-2026-10-03 141522.webp"

+

encoding 2880x1800 rgba, pass 2 of 3, 62%

+

ffmpeg -i screen-recording-standup.mov -c:v libx264 -crf 23 -preset medium -c:a aac screen-recording-standup.mp4

+

frame= 4412 fps=118 q=23.0 size= 38912kB time=00:02:27.06 bitrate=2167.8kbits/s speed=3.93x

+

scan_receipt_0091.tiff: magick exited 1, Unsupported TIFF compression (JPEG-in-TIFF, old-style)

+

+
+
+ + + +
+ + + +
+ + diff --git a/docs/mockups/terminal/04-command-first.html b/docs/mockups/terminal/04-command-first.html new file mode 100644 index 0000000..c7b5449 --- /dev/null +++ b/docs/mockups/terminal/04-command-first.html @@ -0,0 +1,435 @@ + + + + + + +Filesmith · Command First + + + + + + +
+ + +
+
+ + Filesmith +
+ +
+ + + +
+
+ + +
+ +
+ + + + + + +
+ Ctrl K commands + + + +
+
to:webptarget format
+
+ + + + + + + + + + +
+
png, tiff off: already a source format← → moveEnter pickEsc close
+
+
+ + +
+ convert 4 images → webp · balanced · next to source + + never overwrites · collisions save as name (converted).webp, then (2) +
+ + +
+
+

Convert

Change format + + 8 files + +
+
+
images· 4selectedtokens apply to this group
+
+ + HEIC + IMG_4821.HEIC + 3.2 MB + HEIC → WebP + done · 3.2 MB → 640 KB (-80%) + +
+
+ + + PNG + screenshot-2026-10-03 141522.png + 1.8 MB + PNG → WebP + 62% · eta 4 s + +
+
+ + PNG + hero-banner@2x.png + 4.6 MB + PNG → WebP + queued + +
+
+ + TIFF + scan_receipt_0091.tiff + 22.4 MB + TIFF → WebP + failed · Unsupported TIFF compression (JPEG-in-TIFF, old-style) + +
+ +
+
video· 1select to retarget on:
+
+ + + MOV + screen-recording-standup.mov + 812 MB + MOV → MP4 + 18.4% · eta 3 min 12 s + +
+ +
+
documents· 2
+
+ + PDF + Q3-board-deck.pdf + 14.1 MB + PDF → DOCX + queued + +
+
+ + TXT + release-notes-0.3.txt + 6 KB + TXT → PDF + done · 6 KB → 48 KB + +
+ +
+
archives· 1
+
+ + CBZ + One-Piece-v104.cbz + 186 MB + CBZ → CB7 + queued + +
+
+
+ + + +
+ + + diff --git a/docs/mockups/terminal/05-tui-function-keys.html b/docs/mockups/terminal/05-tui-function-keys.html new file mode 100644 index 0000000..685e070 --- /dev/null +++ b/docs/mockups/terminal/05-tui-function-keys.html @@ -0,0 +1,399 @@ + + + + + +Filesmith · TUI Function Keys + + + + + + + +
+ + +
+
Filesmith
+
convertqueue 8 · 2 running
+
v0.3.0
+ + + +
+ +
+
+

F1 Convert

+ Change format + + 8 files + sort group + next group Tab +
+ +
+ +
+
╡ Queue 8 files · 4 groups ╞
+
╡ add · stop · del ╞
+ + + +
+
ST
NAME
SIZE
ROUTE
PROGRESS
RESULT
+
+ +
├─ images (4) ─ active group, 4 selected ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
+ +
+
*
OK
IMG_4821.HEIC
3.2 MB
HEIC → WebP
+
[|||||||||||||||| 100%]
+
3.2 MB → 640 KB -80%
+
+
+
+
*
RUN
screenshot-2026-10-03 141522.png
1.8 MB
PNG → WebP
+
[|||||||||| 62%]
+
eta 4 s
+
+
+
+
*
WAIT
hero-banner@2x.png
4.6 MB
PNG → WebP
+
[ 0%]
+
queued, 1st
+
+
+
+
*
ERR
scan_receipt_0091.tiff
22.4 MB
TIFF → WebP
+
[|||| 24%]
+
not converted
+
+
+
+
└ Unsupported TIFF compression (JPEG-in-TIFF, old-style). Re-save as standard TIFF, then retry with r.
+
+ +
├─ video (1) ─ other group, not selectable while images is active ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
+
+
RUN
screen-recording-standup.mov
812 MB
MOV → MP4
+
[||| 18.4%]
+
eta 3 min 12 s
+
+
+ +
├─ documents (2) ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
+
+
WAIT
Q3-board-deck.pdf
14.1 MB
PDF → DOCX
+
[ 0%]
+
queued
+
+
+
+
OK
release-notes-0.3.txt
6 KB
TXT → PDF
+
[|||||||||||||||| 100%]
+
6 KB → 48 KB
+
+
+ +
└─ archives (1) ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
+
+
WAIT
One-Piece-v104.cbz
186 MB
CBZ → CB7
+
[ 0%]
+
queued
+
+
+ + + + +
+
╞═ cursor ═ hero-banner@2x.png
+
+ fromD:\Design\exports\hero-banner@2x.png + imagePNG · 2880 × 1200 · RGBA · 4.6 MB + writesD:\Design\exports\hero-banner@2x.webp + statequeued, starts when screenshot-2026-10-03 141522.png finishes +
+
+ +
╡ ↑↓ move Space mark Tab group F10 menu ╞
+
+ + + +
+ + +
+ Total + [||||||||||||||||||||||||| 41%] + 2/8 done + 2 run + 1 fail + 4 wait + + +
+
+ ffmpeg 7.1 + ImageMagick 7.1.1 + mutool 1.24 + 7-Zip 24.08 + Rar.exe not found, CBR writing off + + RTX 5090 · Real-ESRGAN ready +
+
+ + + +
+ + diff --git a/docs/mockups/terminal/06-repl-log.html b/docs/mockups/terminal/06-repl-log.html new file mode 100644 index 0000000..0b7edb7 --- /dev/null +++ b/docs/mockups/terminal/06-repl-log.html @@ -0,0 +1,377 @@ + + + + + + +Filesmith REPL Log + + + + + + +
+ + +
+
+ + Filesmith +
+
session 14:01:58 · convert · 8 files
+
+ + + +
+
+ + + + +
+ +
+
+
14:01:58#filesmith 0.3.0 · convert: change format · drop files anywhere in this window
+
14:02:11>dropped 8 files, sorted into 4 groups by kind
+
+ + +
+
# images · 4 selected · options below
+ +
+
14:02:11+IMG_4821.HEIC3.2 MBheic → webp +
+
= 640 KB -80% from 3.2 MB · 0.8 s
+
+ +
+
14:02:11+screenshot-2026-10-03 141522.png1.8 MBpng → webp +
+
... 62%eta 4 s
+
+ +
+
14:02:11+hero-banner@2x.png4.6 MBpng → webp +
+
· queued, next after screenshot-2026-10-03 141522.png
+ +
+ +
+
14:02:12!scan_receipt_0091.tiff22.4 MBtiff → webp +
+
! Unsupported TIFF compression (JPEG-in-TIFF, old-style)
+
+
+ +
+ +
+
# video · 1 not in selection, click this line to switch options
+
+
14:02:11+screen-recording-standup.mov812 MBmov → mp4 +
+
... 18.4%eta 3 min 12 s
+
+
+ +
+
+
# documents · 2
+
+
14:02:11+Q3-board-deck.pdf14.1 MBpdf → docx +
+
· queued
+
+
+
14:02:11+release-notes-0.3.txt6 KBtxt → pdf +
+
= 48 KB from 6 KB · 0.3 s
+
+
+ +
+
+
# archives · 1
+
+
14:02:11+One-Piece-v104.cbz186 MBcbz → cb7 +
+
· queued
+
+
+
+
14:06:40+drop more files anywhere, or
+
+
+ + +
+
+

images · 4 selected

Each choice rewrites the command below.

+
--to +
+ + + + + + + + + + png and tiff are off, some sources already are +
+
+
--quality +
+ + + +
+
+
--out +
+ + + last used D:\Photos\2026-10\ + Never overwrites. Collisions save as name (converted).webp, then (2). +
+
+
+
+ + +
+
+
+ $convert     +
+ + +
+
+
+ + + +
+ + + + diff --git a/docs/mockups/terminal/07-swiss-rules.html b/docs/mockups/terminal/07-swiss-rules.html new file mode 100644 index 0000000..d4803db --- /dev/null +++ b/docs/mockups/terminal/07-swiss-rules.html @@ -0,0 +1,463 @@ + + + + + + +Filesmith Swiss Rules + + + + + + +
+ +
+
+
+ Filesmith +
+
+
RTX 5090 · Real-ESRGAN ready
+
+ + + +
+
+ + + +
+
+

Convert

+

Change format. Pick one group, choose a target, run it.

+
+
+
8
+
Files4 groups
+
+
+
41%
+
Total2 of 8 done2 running · 1 failed
+
+
+ +
+
+ + +
+

Images

4 imagesSelected
+ +
+ 01
HEIC
+
IMG_4821.HEIC3.2 MB
+
HEIC/WEBP
+
−80%3.2 MB → 640 KB
+
+ +
+
+ +
+ 02
PNG
+
screenshot-2026-10-03 141522.png 1.8 MB
+
PNG/WEBP
+
62%ETA 4 s
+
+ + +
+
+ +
+ 03
PNG
+
hero-banner@2x.png4.6 MB
+
PNG/WEBP
+
Queued
+
+ + +
+
+ +
+ 04
TIFF
+
scan_receipt_0091.tiff22.4 MB
+
TIFF/WEBP
+
FailedRetry after fix
+
+ +
+
Unsupported TIFF compression (JPEG-in-TIFF, old-style)
+
+
+ +
+

Video

1 videoNot in selection, one group at a time
+
+ 05
MOV
+
screen-recording-standup.mov 812 MB
+
MOV/MP4
+
18.4%ETA 3 min 12 s
+
+ + +
+
+
+ +
+

Documents

2 documents
+
+ 06
PDF
+
Q3-board-deck.pdf14.1 MB
+
PDF/DOCX
+
Queued
+
+ + +
+
+
+ 07
TXT
+
release-notes-0.3.txt6 KB
+
TXT/PDF
+
Done6 KB → 48 KB
+
+ +
+
+
+ +
+

Archives

1 archive
+
+ 08
CBZ
+
One-Piece-v104.cbz186 MB
+
CBZ/CB7
+
Queued
+
+ + +
+
+
+
+ + +
+ + +
+ + diff --git a/docs/mockups/terminal/08-geist-breadcrumb.html b/docs/mockups/terminal/08-geist-breadcrumb.html new file mode 100644 index 0000000..4468543 --- /dev/null +++ b/docs/mockups/terminal/08-geist-breadcrumb.html @@ -0,0 +1,456 @@ + + + + + + +Filesmith · Geist Breadcrumb + + + + + + + + +
+ +
+
Filesmith
+
+
Ctrl K jump to anything
+ + + +
+ +
+ + + + +
+
+
+ / + / + + 8 files +
+
+
+

Convert

+

Change format

+
+
+ + +
+
+
+ +
+ + + +
IMAGES · 4Selected group · options on the right
+
+ + +
IMG_4821.HEIC
3.2 MB
+
HEIC→WEBP
+
3.2 MB → 640 KB-80%
+
+
+
+ + +
screenshot-2026-10-03 141522.png
1.8 MB
+
PNG→WEBP
+
62%ETA 4 s
+
+
+
+ + +
hero-banner@2x.png
4.6 MB
+
PNG→WEBP
+
Queued
+
+
+
+ + +
scan_receipt_0091.tiff
22.4 MBUnsupported TIFF compression (JPEG-in-TIFF, old-style)
+
TIFF→WEBP
+
Failed
+
+
+ + +
VIDEO · 1Separate group, select to edit
+
+ + MOV +
screen-recording-standup.mov
812 MB
+
MOV→MP4
+
18.4%ETA 3 min 12 s
+
+
+ + +
DOCUMENTS · 2
+
+ + PDF +
Q3-board-deck.pdf
14.1 MB
+
PDF→DOCX
+
Queued
+
+
+
+ + TXT +
release-notes-0.3.txt
6 KB
+
TXT→PDF
+
6 KB → 48 KB
+
+
+ + +
ARCHIVES · 1
+
+ + CBZ +
One-Piece-v104.cbz
186 MB
+
CBZ→CB7
+
Queued
+
+ + +
+
+
+ + + +
+ + + +
+ + diff --git a/docs/mockups/terminal/09-explorer-tree.html b/docs/mockups/terminal/09-explorer-tree.html new file mode 100644 index 0000000..2cc3cec --- /dev/null +++ b/docs/mockups/terminal/09-explorer-tree.html @@ -0,0 +1,535 @@ + + + + + +Filesmith, Explorer Tree + + + + + + +
+ + +
+
+
+ Filesmith +
+ + +
+ + + +
+
+ +
+ + + + +
+
+
+
+ + + + + +
+
+ Convert + Change format + images + 8 files in Convert · 4 images here +
+ +
+
1# Images · 4 selected
+
2# sources: heic ×1, png ×2, tiff ×1 · 32.0 MB total
+
3
+
4target:
+
5format:
+
6
+
7# png, tiff off: a selected file is already that format
+
8quality:
+
9
+
10
+
11output:
+
12folder: #
+
13# e.g. D:\Photos\2026-10\IMG_4821.webp
+
14naming: never-overwrite
+
15# collisions save as name (converted).webp, then name (converted) (2).webp
+
16
+
17inputs:
+
18- IMG_4821.HEIC3.2 MBheic → webpdone 3.2 MB → 640 KB -80%
+
19- screenshot-2026-10-03 141522.png1.8 MBpng → webprunning 62% eta 4 s
+
20- hero-banner@2x.png4.6 MBpng → webpqueued
+
21- scan_receipt_0091.tiff22.4 MBtiff → webpfailed
+
22# Unsupported TIFF compression (JPEG-in-TIFF, old-style)
+
+ +
+ heic, png, tiff → webp · balanced · next to source + + +
+
+ + + +
+ + +
+
+ + + + + + +
+
+
14:22:03imagesIMG_4821.HEIC → IMG_4821.webpdone · 3.2 MB → 640 KB (-80%) · magick
+
14:22:04documentsrelease-notes-0.3.txt → .pdfdone · 6 KB → 48 KB · mutool
+
14:22:06imagesscan_receipt_0091.tifffailed · Unsupported TIFF compression (JPEG-in-TIFF, old-style)
+
14:22:07videoscreen-recording-standup.mov → .mp4running 18.4% · 812 MB · eta 3 min 12 s · ffmpeg
+
14:22:09imagesscreenshot-2026-10-03 141522.png → .webprunning 62% · eta 4 s · magick
+
14:22:09queuedhero-banner@2x.png, Q3-board-deck.pdf, One-Piece-v104.cbzpng → webp, pdf → docx, cbz → cb7 · 204.7 MB
+
+
+
+
+ + + +
+ + diff --git a/docs/mockups/terminal/10-powerline-status.html b/docs/mockups/terminal/10-powerline-status.html new file mode 100644 index 0000000..6b8dd30 --- /dev/null +++ b/docs/mockups/terminal/10-powerline-status.html @@ -0,0 +1,389 @@ + + + + + + +Filesmith Powerline + + + + + + +
+ + +
+
+ + Filesmith +
+
+ + + +
+ + +
+
+
+

Convert

+ Change format + 8 files +
+ + + +
+
+

Images

4 images · 4 selected + Esc close options +
+ +
+
+ fFormat +
+ + + + + + + + + + png, tiff: some sources already are +
+
+
+ qQuality +
+ + + +
+
+
+ oOutput +
+ + + last used D:\Photos\2026-10\ +
+
+
+ Never overwrites. Collisions save as name (converted).webp, then (2). + Ctrl Enter convert 4 images +
+
+ +
+ heic +
IMG_4821.HEIC3.2 MB · heic → webp
+
3.2 MB → 640 KB -80%
+
+
+
+ png +
screenshot-2026-10-03 141522.png1.8 MB · png → webp
+
62% · 4 s
+
+
+
+ png +
hero-banner@2x.png4.6 MB · png → webp
+
queued
+
+ +
+
+ tiff +
scan_receipt_0091.tiff22.4 MB · tiff → webp
+
failed
+
+
E: Unsupported TIFF compression (JPEG-in-TIFF, old-style)
+
+
+ +
+

Video

1 videoother group, select with Tab
+
+ mov +
screen-recording-standup.mov812 MB · mov → mp4
+
18.4% · 3 min 12 s
+
+
+
+ +
+

Documents

2 documents
+
+ pdf +
Q3-board-deck.pdf14.1 MB · pdf → docx
+
queued
+
+
+
+ txt +
release-notes-0.3.txt6 KB · txt → pdf
+
6 KB → 48 KB +700%
+
+
+
+ +
+

Archives

1 archive
+
+ cbz +
One-Piece-v104.cbz186 MB · cbz → cb7
+
queued
+
+
+
+
+
+ + + + + + +
+ + diff --git a/docs/mockups/terminal/11-bottom-dock.html b/docs/mockups/terminal/11-bottom-dock.html new file mode 100644 index 0000000..ba6748f --- /dev/null +++ b/docs/mockups/terminal/11-bottom-dock.html @@ -0,0 +1,448 @@ + + + + + + +Filesmith Bottom Dock + + + + + + +
+ + +
+
+ + Filesmith +
+
/convert
+
+ 1–7 switch mode   Ctrl K commands +
+ + + +
+
+ + +
+
+
+

Convert

+

Change format

+
8 files
+
+ + + +
+
Images4 images4 selected · options active
+ +
+
+
IMG_4821.HEIC
HEIC → WebP3.2 MB
+
-80%
3.2 MB → 640 KB
+
+
+ +
+
+
screenshot-2026-10-03 141522.png
PNG → WebP1.8 MB
+
62%
ETA 4 s
+
+
+
+ +
+
+
hero-banner@2x.png
PNG → WebP4.6 MB
+
queued
#3 in line
+
+
+ +
+
+
scan_receipt_0091.tiff
TIFF → WebP22.4 MBUnsupported TIFF compression (JPEG-in-TIFF, old-style)
+
failed
retry R
+
+
+ +
Video1 videoShift+click to switch group
+
+
MOV
+
screen-recording-standup.mov
MOV → MP4812 MB
+
18.4%
ETA 3 min 12 s
+
+
+
+ +
Documents2 documents
+
+
PDF
+
Q3-board-deck.pdf
PDF → DOCX14.1 MB
+
queued
#4 in line
+
+
+
+
TXT
+
release-notes-0.3.txt
TXT → PDF6 KB
+
done
6 KB → 48 KB
+
+ +
+ +
Archives1 archive
+
+
CBZ
+
One-Piece-v104.cbz
CBZ → CB7186 MB
+
queued
#5 in line
+
+
+
+
+ + + +
+ + + + + + +
+ + diff --git a/docs/mockups/terminal/12-verb-tint.html b/docs/mockups/terminal/12-verb-tint.html new file mode 100644 index 0000000..4dfed91 --- /dev/null +++ b/docs/mockups/terminal/12-verb-tint.html @@ -0,0 +1,432 @@ + + + + + + +Filesmith Verb Tint + + + + + + + + +
+ + +
+
+ + Filesmith +
+
+ ~/queue · 8 files + + + +
+ + + + + +
+
Images4 selected
+ +
+ to +
+ + + + + + + + + +
+
+ +
+ quality +
+ + + +
+
+ +
+ out + +
+ + + +
+ + +
+
+ + or drop anywhere in the window + + Never overwrites. Collisions save as name (converted).webp, then (2). +
+ + + +
+ +
Images4 images4 of 4 selected
+ +
+ heic + IMG_4821.HEIC + HEIC→WebP + 3.2 MB + done + 3.2 MB → 640 KB -80% + +
+ +
+ png + screenshot-2026-10-03 141522.png + PNG→WebP + 1.8 MB + 62% + eta 4 s + +
+ +
+ png + hero-banner@2x.png + PNG→WebP + 4.6 MB + queued + + +
+ +
+ tiff + scan_receipt_0091.tiff + TIFF→WebP + 22.4 MB + failed + Unsupported TIFF compression (JPEG-in-TIFF, old-style) + +
+ + +
Video1 videoselect group to edit options
+
+ mov + screen-recording-standup.mov + MOV→MP4 + 812 MB + 18.4% + eta 3 min 12 s + +
+ + +
Documents2 documents
+
+ pdf + Q3-board-deck.pdf + PDF→DOCX + 14.1 MB + queued + + +
+
+ txt + release-notes-0.3.txt + TXT→PDF + 6 KB + done + 6 KB → 48 KB + + +
+ + +
Archives1 archive
+
+ cbz + One-Piece-v104.cbz + CBZ→CB7 + 186 MB + queued + + +
+
+ + +
+
logengine output, newest first
+
+
14:22:11magickscreenshot-2026-10-03 141522.png → .webp q=82 62%
+
14:22:09magickerror scan_receipt_0091.tiff: Unsupported TIFF compression (JPEG-in-TIFF, old-style)
+
14:22:07magickok IMG_4821.HEIC → IMG_4821.webp 3.2 MB → 640 KB 1.4 s
+
14:21:40ffmpegscreen-recording-standup.mov → .mp4 libx264 crf 23 frame=3311 fps=58 18.4%
+
14:20:55mutoolok release-notes-0.3.txt → release-notes-0.3.pdf 6 KB → 48 KB 0.3 s
+
+
+
+ + + +
+ + diff --git a/docs/mockups/terminal/13-one-bit.html b/docs/mockups/terminal/13-one-bit.html new file mode 100644 index 0000000..79a29fe --- /dev/null +++ b/docs/mockups/terminal/13-one-bit.html @@ -0,0 +1,468 @@ + + + + + + +Filesmith One Bit + + + + + + +
+ +
+
Filesmith
+
convert/queue/images
+
+
CommandCtrl K
+
+ + + +
+
+ +
+ + +
+
+

Convert

+ Change format +
+
+
files8
+
groups4
+
in1.04 GB
+
+
+ +
+
+
+ Files + 8 files in 4 groups + + + +
+ + + +
+ +
+ + Images4 images + Selected +
+
+ HEIC + IMG_4821.HEIC3.2 MB + HEIC→WEBP + OK640 KB-80% + +
+
+ PNG + screenshot-2026-10-03 141522.png1.8 MB + PNG→WEBP + 62%4 s + +
+
+ PNG + hero-banner@2x.png4.6 MB + PNG→WEBP + Queued + +
+
+ TIFF + scan_receipt_0091.tiff22.4 MB + TIFF→WEBP + Errfailed + + Unsupported TIFF compression (JPEG-in-TIFF, old-style) +
+ + +
+ + Video1 video + other group, select to switch +
+
+ MOV + screen-recording-standup.mov812 MB + MOV→MP4 + 18.4%3 min 12 s + +
+ +
+ + Documents2 documents +
+
+ PDF + Q3-board-deck.pdf14.1 MB + PDF→DOCX + Queued + +
+
+ TXT + release-notes-0.3.txt6 KB + TXT→PDF + OK6 KB → 48 KB + +
+ +
+ + Archives1 archive +
+
+ CBZ + One-Piece-v104.cbz186 MB + CBZ→CB7 + Queued + +
+
+ +
+
Loglast 6 events
+
+
14:21:58doneIMG_4821.HEIC → IMG_4821.webp, 640 KB
+
14:22:03donerelease-notes-0.3.txt → release-notes-0.3.pdf, 48 KB
+
14:22:05errorscan_receipt_0091.tiff: Unsupported TIFF compression (JPEG-in-TIFF, old-style)
+
14:22:06startscreenshot-2026-10-03 141522.png with ImageMagick 7.1.1
+
14:22:06startscreen-recording-standup.mov with ffmpeg 7.1 (libx264, crf 23)
+
14:22:11waithero-banner@2x.png, Q3-board-deck.pdf, One-Piece-v104.cbz
+
>
+
+
+
+ + +
+
+
+ + +
+ + diff --git a/docs/mockups/terminal/14-serif-contrast.html b/docs/mockups/terminal/14-serif-contrast.html new file mode 100644 index 0000000..8302e1c --- /dev/null +++ b/docs/mockups/terminal/14-serif-contrast.html @@ -0,0 +1,416 @@ + + + + + + +Filesmith Serif Contrast + + + + + + +
+ + +
+
Filesmith
+
convert / images, video, documents, archives
+
+ + + +
+
+ +
+ + + + + +
+
+
+

Convert

+

Change format, 8 files in the queue

+
+ +
+ +
+ + +
+

Images

4 images
+
+
+
+
IMG_4821.HEIC
HEIC → WebP  ·  3.2 MB
+
Done3.2 MB → 640 KB
+
-80%
+
+
+
+
+
screenshot-2026-10-03 141522.png
PNG → WebP  ·  1.8 MB
+
Running 62%About 4 s left
+
+
+
+
+
+
+
hero-banner@2x.png
PNG → WebP  ·  4.6 MB
+
QueuedNext in line
+
+
+
+
+
+
scan_receipt_0091.tiff
TIFF → WebP  ·  22.4 MB
Unsupported TIFF compression (JPEG-in-TIFF, old-style)
+
FailedRetry or remove
+
+
+
+
+
+ + +
+

Video

1 video
+
+
+
MOV
+
screen-recording-standup.mov
MOV → MP4  ·  812 MB
+
Running 18.4%About 3 min 12 s left
+
+
+
+
+
+
+ + +
+

Documents

2 documents
+
+
+
PDF
+
Q3-board-deck.pdf
PDF → DOCX  ·  14.1 MB
+
QueuedAfter images
+
+
+
+
+
TXT
+
release-notes-0.3.txt
TXT → PDF  ·  6 KB
+
Done6 KB → 48 KB
+
+700%
+
+
+
+
+ + +
+

Archives

1 archive
+
+
+
CBZ
+
One-Piece-v104.cbz
CBZ → CB7  ·  186 MB
+
QueuedAfter documents
+
+
+
+
+
+ +
+
+ + + +
+ + + +
+ + diff --git a/docs/mockups/terminal/15-spreadsheet-grid.html b/docs/mockups/terminal/15-spreadsheet-grid.html new file mode 100644 index 0000000..61ba51a --- /dev/null +++ b/docs/mockups/terminal/15-spreadsheet-grid.html @@ -0,0 +1,506 @@ + + + + + + +Filesmith Spreadsheet Grid + + + + + + +
+ + +
+
+ + Filesmith +
+

Convert

Change format8 files
+
+
+ + + +
+
+ + +
+ +
fx
+
+ =CONVERT(A1:A4, "webp", "balanced", "next-to-source") +
+
evaluates to 4 jobs · 32.0 MB in · est. 6.4 MB out
+
+ +
+ +
+
+
+
A
+
B name
+
C size
+
D route
+
E status
+
F progress
+
G result
+
H
+
+ + +
+
+
Images4 imagesactive groupHEIC, PNG, TIFF
+
+ +
+
+
1
+
HEIC
+
IMG_4821.HEIC
+
3.2 MB
+
HEIC → WebP
+
done
+
100%
+
3.2 MB → 640 KB −80%
+
+
+
+
2
+
PNG
+
screenshot-2026-10-03 141522.png
+
1.8 MB
+
PNG → WebP
+
running
+
62%
+
ETA 4 s
+
+
+
+
3
+
PNG
+
hero-banner@2x.png
+
4.6 MB
+
PNG → WebP
+
queued
+
0%
+
waiting
+
+
+
+
4
+
TIFF
+
scan_receipt_0091.tiff
+
22.4 MB
+
TIFF → WebP
+
#ERR
+
–
+
+
+
+ +
+ + +
+
+
Video1 videoOther convert group, click its header to switch
+
+
+
5
+
MOV
+
screen-recording-standup.mov
+
812 MB
+
MOV → MP4
+
running
+
18.4%
+
ETA 3 min 12 s
+
+
+ + +
+
+
Documents2 documents
+
+
+
6
+
PDF
+
Q3-board-deck.pdf
+
14.1 MB
+
PDF → DOCX
+
queued
+
0%
+
waiting
+
+
+
+
7
+
TXT
+
release-notes-0.3.txt
+
6 KB
+
TXT → PDF
+
done
+
100%
+
6 KB → 48 KB
+
+
+ + +
+
+
Archives1 archiveCBR output needs Rar.exe
+
+
+
8
+
CBZ
+
One-Piece-v104.cbz
+
186 MB
+
CBZ → CB7
+
queued
+
0%
+
waiting
+
+
+ + +
+
9
+ +
+ +
+ + +
+

E4 · #ERR

+

Unsupported TIFF compression (JPEG-in-TIFF, old-style). Re-save the scan as a standard TIFF or PNG, then retry.

+
+
+ + + +
+ + + + + + +
+ + + diff --git a/docs/mockups/terminal/16-warp-blocks.html b/docs/mockups/terminal/16-warp-blocks.html new file mode 100644 index 0000000..60d9844 --- /dev/null +++ b/docs/mockups/terminal/16-warp-blocks.html @@ -0,0 +1,391 @@ + + + + + + +Filesmith Warp Blocks + + + + + + +
+ + +
+
Filesmith
+ ~/convert + + + + +
+ +
+ + + + +
+
+

Convert

+ Change format + + 8 files · 4 blocks + + +
+ +
+
+ + +
+ +
+
✓HEICIMG_4821.HEICHEIC → WebP3.2 MB + 3.2 MB → 640 KB -80% +
+
▸PNGscreenshot-2026-10-03 141522.pngPNG → WebP1.8 MB + 62%eta 4 s +
+
·PNGhero-banner@2x.pngPNG → WebP4.6 MB + queued +
+
✕TIFFscan_receipt_0091.tiffTIFF → WebP22.4 MB + failed + +
! Unsupported TIFF compression (JPEG-in-TIFF, old-style) · re-save as standard TIFF and retry
+
+
1/4 done · 1 running · 1 failed62% · eta 4 s
+
+ + +
+ +
+
▸MOVscreen-recording-standup.movMOV → MP4812 MB + 18.4%eta 3 min 12 s +
+
+
0/1 done · 1 running18% · eta 3 min 12 sclick header to select block
+
+ + +
+ +
+
·PDFQ3-board-deck.pdfPDF → DOCX14.1 MB + queued +
+
✓TXTrelease-notes-0.3.txtTXT → PDF6 KB + 6 KB → 48 KB +
+
+
1/2 done50%
+
+ + +
+ +
+
·CBZOne-Piece-v104.cbzCBZ → CB7186 MB + queued +
+
+
0/1 done · waiting
+
+
+ + + +
+ + +
+ +add filesdrop anywhere, or browseCtrl O +
+ + + +
+
+
+ + diff --git a/docs/mockups/terminal/17-linear-triptych.html b/docs/mockups/terminal/17-linear-triptych.html new file mode 100644 index 0000000..0432f74 --- /dev/null +++ b/docs/mockups/terminal/17-linear-triptych.html @@ -0,0 +1,485 @@ + + + + + + +Filesmith · Linear Preview + + + + + + + + +
+ +
+
+ + Filesmith +
+
/Convert/IMG_4821.HEIC
+
+ + + + +
+ +
+ + + + +
+
+

Convert 8 files

+

Change format

+
+ + +
+ + +
+ +
IMG_4821.HEIC
+
HEIC → WebP3.2 MB → 640 KB
+
-80%
+
+ +
+ +
screenshot-2026-10-03 141522.png
+
PNG → WebP1.8 MBETA 4 s
+
62%
+
+ +
+ +
hero-banner@2x.png
+
PNG → WebP4.6 MBqueued
+
+
+ +
+ +
scan_receipt_0091.tiff
+
22.4 MBUnsupported TIFF compression (JPEG-in-TIFF, old-style)
+
+
+ + +
+ +
screen-recording-standup.mov
+
MOV → MP4812 MBETA 3m 12s
+
18.4%
+
+ + +
+ +
Q3-board-deck.pdf
+
PDF → DOCX14.1 MBqueued
+
+
+
+ +
release-notes-0.3.txt
+
TXT → PDF6 KB → 48 KB
+
done
+
+ + +
+ +
One-Piece-v104.cbz
+
CBZ → CB7186 MBqueued
+
+
+
+ +
+ JK move + X select + Space preview + +
+
+ + +
+
+ + IMG_4821.HEIC + HEIC → WebP +
+ + + +
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
Before HEIC 3.2 MB
+
After WebP 640 KB
+
4032 × 3024 · 12.2 MP
+
-80% · saved 2.56 MB
+
+
+ +
+ queued14:02:11 + 0.4 s + running14:02:11.4 + 3.6 s + done14:02:15 + + 4.0 s total +
+ +
+
+

Images · 4 selected

+ HEIC, PNG ×2, TIFF +
+ options apply to this group only +
+ +
+
Target format
+
+ + + + + + + + + +
+
QualityWebP q82
+
+ +
+
+ +
+
Output
+
+
Next to source D:\Photos\2026-10\
+
+ +

Never overwrites. Collisions save as name (converted).webp, then (2).

+
+ +
+ 1 done, 1 running, 1 queued, 1 failed · ≈ 1.4 MB out +
+ + +
+
+
+
+ + + +
+ + diff --git a/docs/mockups/terminal/18-pipeline-board.html b/docs/mockups/terminal/18-pipeline-board.html new file mode 100644 index 0000000..a99ab50 --- /dev/null +++ b/docs/mockups/terminal/18-pipeline-board.html @@ -0,0 +1,459 @@ + + + + + + +Filesmith Pipeline Board + + + + + + +
+ + +
+
+ + Filesmith +
+
convert/board
+
+ + + +
+
+ +
+ + + + +
+
+
+

Convert

+

Change format

+
+
+ 8 files · 4 groups +
+ + +
+
+ +
+
Queued3
+
Running2
+
Done2
+
Failed1
+
+
+ +
or drop anywhere on the board · files sort into lanes by kind
+
+ +
+ +
+ +
+
+
queued1
+
+
PNGPNG → WebP4.6 MB +
+ hero-banner@2x.png +
queuednext up
+
+
+
+
running1
+
+
PNGPNG → WebP1.8 MB +
+ screenshot-2026-10-03 141522.png +
+
62%ETA 4 s
+
+
+
+
done1
+
+
HEICHEIC → WebP3.2 MB
+ IMG_4821.HEIC +
3.2 MB → 640 KB-80%
+
+
+
+
failed1
+
+
TIFFTIFF → WebP22.4 MB
+ scan_receipt_0091.tiff +
Unsupported TIFF compression (JPEG-in-TIFF, old-style)
+
+
+
+
+ + +
+ +
+
queued0
+
running1
MOVscreen-recording-standup.mov812 MB18.4% · 3:12
+
done0
+
failed0
+
+
+ +
+ +
+
queued1
PDFQ3-board-deck.pdf14.1 MBPDF → DOCX
+
running0
+
done1
TXTrelease-notes-0.3.txt6 KB → 48 KBTXT → PDF
+
failed0
+
+
+ +
+ +
+
queued1
CBZOne-Piece-v104.cbz186 MBCBZ → CB7
+
running0
+
done0
+
failed0
+
+
+
+ +
+
Engine log
+
14:21:58magickIMG_4821.HEIC start heic → webp -quality 82
+
14:22:07magickIMG_4821.HEIC → IMG_4821.webp 640 KB 0.9 s
+
14:22:08mutoolrelease-notes-0.3.txt → release-notes-0.3.pdf 48 KB 0.2 s
+
14:22:09magickscan_receipt_0091.tiff: Unsupported TIFF compression (JPEG-in-TIFF, old-style)
+
14:22:11ffmpegscreen-recording-standup.mov frame=3312 fps=118 time=00:00:55.20 speed=3.9x
+
+
+ + + +
+ + + +
+ + diff --git a/docs/mockups/terminal/19-amber-manpage.html b/docs/mockups/terminal/19-amber-manpage.html new file mode 100644 index 0000000..978e132 --- /dev/null +++ b/docs/mockups/terminal/19-amber-manpage.html @@ -0,0 +1,469 @@ + + + + + + +Filesmith Amber Manpage + + + + + + +
+ +
+
+ + Filesmith man convert +
+
+
+ + + +
+
+ + + +
+ + +
+
+

NAME

+

convert - change file format

+
+ +
+

SYNOPSIS

+
+
+ $ filesmith convert -t webp -q balanced -o next-to-source IMG_4821.HEIC screenshot-2026-10-03… +2 + +
+
+ + 8 files in queue, 4 images selected +
+
+
+ +
+

OPTIONS for Images · 4 selected

+
+
+
-t, --to FORMAT
+
+
+ + + + + + + + + +
+ png, tiff off: a selected source is already that format. +
+
+ +
+
-q, --quality LEVEL
+
+
+ + + +
+
+
+ +
+
-o, --out DIR
+
+ Next to source (default)   +
+
+ +
+
--no-clobber always on
+
Never overwrites. Collisions save as name (converted).webp, then (2).
+
+
+
+ +
+

FILES

+ +
+

Images (4) selected · active group

+
+ HEIC +
IMG_4821.HEIC
+
heic → webp 3.2M → 640K -80% done
+
+ + +
+
+
+ PNG +
screenshot-2026-10-03 141522.png
+
png → webp 1.8M 62% eta 4s
+
+ + +
+
+
+ PNG +
hero-banner@2x.png
+
png → webp 4.6M queued
+
+ + +
+
+
+ TIFF +
scan_receipt_0091.tiff
+
tiff → webp 22.4M ERR unsupported tiff compression (JPEG-in-TIFF, old-style)
+
+ + +
+
+
+ +
+

Video (1) other group, not in selection

+
+ MOV +
screen-recording-standup.mov
+
mov → mp4 812M 18.4% eta 3m 12s
+
+ + +
+
+
+ +
+

Documents (2)

+
+ PDF +
Q3-board-deck.pdf
+
pdf → docx 14.1M queued
+
+ + +
+
+
+ TXT +
release-notes-0.3.txt
+
txt → pdf 6K → 48K done
+
+ +
+
+
+ +
+

Archives (1)

+
+ CBZ +
One-Piece-v104.cbz
+
cbz → cb7 186M queued
+
+ + +
+
+
+

Options describe one group at a time. Select a row in another group to switch.

+
+
+ + +
+ + +
+ + diff --git a/docs/mockups/terminal/20-hud-metrics.html b/docs/mockups/terminal/20-hud-metrics.html new file mode 100644 index 0000000..c638383 --- /dev/null +++ b/docs/mockups/terminal/20-hud-metrics.html @@ -0,0 +1,450 @@ + + + + + +Filesmith HUD Metrics + + + + + + +
+ + +
+
+
+ Filesmith +
+
convert · 2 running · 38 MB/s
+
+ + + +
+
+ +
+ + + +
+ +
+

Convert

+ Change format + 8 files + + +
+ + +
+
+
Progressoverall
+
41%
+
+
0ETA 3 min 14 s100
+
+
+
Done
+
2/ 8
+
+
2 run1 fail3 queued
+
+
+
Throughputlast 60 s · peak 51
+
38MB/s
+ +
+
+
GPUAI tools
+
RTX 5090
+
+ Real-ESRGANready + rembgready + VRAM1.2 / 32 GB · idle +
+
+
+ + +
+
+
filesizeroutestatusspeed · progress
+ +
Images4 imagesselected1 done · 1 running · 1 failed
+
+
+
+ IMG_4821.HEIC3.2 MB + HEIC → WebPdone + 3.2 MB → 640 KB-80% + +
+
+
+ screenshot-2026-10-03 141522.png1.8 MB + PNG → WebPrunning + ⣀⣤⣶⣿⣷⣶⣾⣿⣶⣴62%4 s + + +
+
+
+ hero-banner@2x.png4.6 MB + PNG → WebPqueued + next + +
+
+
TIF
+ scan_receipt_0091.tiff22.4 MB + TIFF → WebPfailed + Unsupported TIFF compression (JPEG-in-TIFF, old-style) + +
+
+ +
Video1 videorunning
+
+
+
MOV
+ screen-recording-standup.mov812 MB + MOV → MP4running + ⣶⣶⣤⣤⣶⣶⣶⣤⣴⣶18.4%3 min 12 s + + +
+
+ +
Documents2 documents1 done
+
+
+
PDF
+ Q3-board-deck.pdf14.1 MB + PDF → DOCXqueued + waiting + +
+
+
TXT
+ release-notes-0.3.txt6 KB + TXT → PDFdone + 6 KB → 48 KB + +
+
+ +
Archives1 archivequeued
+
+
+
CBZ
+ One-Piece-v104.cbz186 MB + CBZ → CB7queued + waiting + + +
+
+
end of queueR retry failedSpace pause allTab next group
+
+ + + +
+
+
+ + + +
+ + diff --git a/docs/mockups/terminal/index.html b/docs/mockups/terminal/index.html new file mode 100644 index 0000000..d84336f --- /dev/null +++ b/docs/mockups/terminal/index.html @@ -0,0 +1,118 @@ + + + + + +Filesmith Terminal Mockups + + + +
+

filesmith / terminal redesign / 20 mockups

+

dark only. click a card to open the mockup.

+
+
+ +
Wind Native mockup
+
01Wind Native

The safe bet: Filesmith rebuilt on Wind's exact settings DNA, so the two apps feel like siblings.

+
+ +
Wind Dense Inspector mockup
+
02Wind Dense Inspector

Wind's tokens tightened into a pro-tool table with a fixed right inspector, for people who want to see 40 rows at once.

+
+ +
Tmux Panes mockup
+
03Tmux Panes

The window is a tmux session: three hard-split panes and a green status line, with real GUI controls inside each pane.

+
+ +
Command First mockup
+
04Command First

No sidebar at all: a big command bar owns the top of the window and every verb is one keystroke away, Raycast for files.

+
+ +
TUI Function Keys mockup
+
05TUI Function Keys

An htop or Midnight Commander homage drawn with box characters, with a F-key bar along the bottom for every verb.

+
+ +
REPL Log mockup
+
06REPL Log

The app is a scrolling session log with a prompt at the bottom; every drop, option and result is a line you can read back.

+
+ +
Swiss Rules mockup
+
07Swiss Rules

Brutalist Swiss: heavy white rules, a condensed display face and verbs as top tabs, black and white only.

+
+ +
Geist Breadcrumb mockup
+
08Geist Breadcrumb

Vercel-grade black and white precision: icon rail, breadcrumb header, a flat borderless queue and card-stacked options, very little ornament.

+
+ +
Explorer Tree mockup
+
09Explorer Tree

A VS Code-style explorer where verbs are folders, convert groups are subfolders and the options panel reads like a config file.

+
+ +
Powerline Status mockup
+
10Powerline Status

Vim modal energy without the box lines: an airy, borderless workspace where a bold powerline bar at the bottom carries all global state.

+
+ +
Bottom Dock mockup
+
11Bottom Dock

Verbs live in a bottom mode dock like a DAW or a camera app, leaving the full height for the queue and options.

+
+ +
Verb Tint mockup
+
12Verb Tint

No sidebar and no side panel: verbs as quiet top tabs, options as one toolbar strip, and each verb owns a whisper of colour on hairlines and progress.

+
+ +
One Bit mockup
+
13One Bit

A 1-bit, pixel-precise system: pixel-font headings, dithered progress and nothing but pure white on pure black.

+
+ +
Serif Contrast mockup
+
14Serif Contrast

An editorial calm take: a sharp serif title against mono data, spacious rows, almost a printed job sheet in the dark.

+
+ +
Spreadsheet Grid mockup
+
15Spreadsheet Grid

The queue is a spreadsheet: column letters, row numbers, cell selection and a formula bar that shows the job as a function.

+
+ +
Warp Blocks mockup
+
16Warp Blocks

Each convert group is a command block like Warp terminal: a header command, live output rows, and a block you select as a unit.

+
+ +
Linear Preview mockup
+
17Linear Preview

Preview-first, Linear-style: a narrow keyboard-driven list on the left and a dominant inspector that shows the focused file big, with the group's options beneath.

+
+ +
Pipeline Board mockup
+
18Pipeline Board

Status columns instead of a list: queued, running, done and failed lanes with groups as swimlanes, readable at a glance.

+
+ +
Amber Manpage mockup
+
19Amber Manpage

Each verb is a man page: one centred document with NAME, SYNOPSIS, OPTIONS and FILES sections, warm paper-white text with amber only on headings and flags.

+
+ +
HUD Metrics mockup
+
20HUD Metrics

A status-first HUD: the top third is live metrics with braille sparklines, the queue sits beneath as a quiet list.

+
+
+ + diff --git a/docs/mockups/terminal/shots/01-wind-native.png b/docs/mockups/terminal/shots/01-wind-native.png new file mode 100644 index 0000000..9f7187e Binary files /dev/null and b/docs/mockups/terminal/shots/01-wind-native.png differ diff --git a/docs/mockups/terminal/shots/02-wind-dense-inspector.png b/docs/mockups/terminal/shots/02-wind-dense-inspector.png new file mode 100644 index 0000000..8648467 Binary files /dev/null and b/docs/mockups/terminal/shots/02-wind-dense-inspector.png differ diff --git a/docs/mockups/terminal/shots/03-tmux-panes.png b/docs/mockups/terminal/shots/03-tmux-panes.png new file mode 100644 index 0000000..ce5ede0 Binary files /dev/null and b/docs/mockups/terminal/shots/03-tmux-panes.png differ diff --git a/docs/mockups/terminal/shots/04-command-first.png b/docs/mockups/terminal/shots/04-command-first.png new file mode 100644 index 0000000..8184524 Binary files /dev/null and b/docs/mockups/terminal/shots/04-command-first.png differ diff --git a/docs/mockups/terminal/shots/05-tui-function-keys.png b/docs/mockups/terminal/shots/05-tui-function-keys.png new file mode 100644 index 0000000..77d7090 Binary files /dev/null and b/docs/mockups/terminal/shots/05-tui-function-keys.png differ diff --git a/docs/mockups/terminal/shots/06-repl-log.png b/docs/mockups/terminal/shots/06-repl-log.png new file mode 100644 index 0000000..d868e30 Binary files /dev/null and b/docs/mockups/terminal/shots/06-repl-log.png differ diff --git a/docs/mockups/terminal/shots/07-swiss-rules.png b/docs/mockups/terminal/shots/07-swiss-rules.png new file mode 100644 index 0000000..d1e4a8d Binary files /dev/null and b/docs/mockups/terminal/shots/07-swiss-rules.png differ diff --git a/docs/mockups/terminal/shots/08-geist-breadcrumb.png b/docs/mockups/terminal/shots/08-geist-breadcrumb.png new file mode 100644 index 0000000..e514087 Binary files /dev/null and b/docs/mockups/terminal/shots/08-geist-breadcrumb.png differ diff --git a/docs/mockups/terminal/shots/09-explorer-tree.png b/docs/mockups/terminal/shots/09-explorer-tree.png new file mode 100644 index 0000000..deb5a1c Binary files /dev/null and b/docs/mockups/terminal/shots/09-explorer-tree.png differ diff --git a/docs/mockups/terminal/shots/10-powerline-status.png b/docs/mockups/terminal/shots/10-powerline-status.png new file mode 100644 index 0000000..f0ce2b6 Binary files /dev/null and b/docs/mockups/terminal/shots/10-powerline-status.png differ diff --git a/docs/mockups/terminal/shots/11-bottom-dock.png b/docs/mockups/terminal/shots/11-bottom-dock.png new file mode 100644 index 0000000..3a1cc82 Binary files /dev/null and b/docs/mockups/terminal/shots/11-bottom-dock.png differ diff --git a/docs/mockups/terminal/shots/12-verb-tint.png b/docs/mockups/terminal/shots/12-verb-tint.png new file mode 100644 index 0000000..3b193e9 Binary files /dev/null and b/docs/mockups/terminal/shots/12-verb-tint.png differ diff --git a/docs/mockups/terminal/shots/13-one-bit.png b/docs/mockups/terminal/shots/13-one-bit.png new file mode 100644 index 0000000..da0fabc Binary files /dev/null and b/docs/mockups/terminal/shots/13-one-bit.png differ diff --git a/docs/mockups/terminal/shots/14-serif-contrast.png b/docs/mockups/terminal/shots/14-serif-contrast.png new file mode 100644 index 0000000..da3feb1 Binary files /dev/null and b/docs/mockups/terminal/shots/14-serif-contrast.png differ diff --git a/docs/mockups/terminal/shots/15-spreadsheet-grid.png b/docs/mockups/terminal/shots/15-spreadsheet-grid.png new file mode 100644 index 0000000..544eac0 Binary files /dev/null and b/docs/mockups/terminal/shots/15-spreadsheet-grid.png differ diff --git a/docs/mockups/terminal/shots/16-warp-blocks.png b/docs/mockups/terminal/shots/16-warp-blocks.png new file mode 100644 index 0000000..89ba96e Binary files /dev/null and b/docs/mockups/terminal/shots/16-warp-blocks.png differ diff --git a/docs/mockups/terminal/shots/17-linear-triptych.png b/docs/mockups/terminal/shots/17-linear-triptych.png new file mode 100644 index 0000000..b71e1d6 Binary files /dev/null and b/docs/mockups/terminal/shots/17-linear-triptych.png differ diff --git a/docs/mockups/terminal/shots/18-pipeline-board.png b/docs/mockups/terminal/shots/18-pipeline-board.png new file mode 100644 index 0000000..4f3922a Binary files /dev/null and b/docs/mockups/terminal/shots/18-pipeline-board.png differ diff --git a/docs/mockups/terminal/shots/19-amber-manpage.png b/docs/mockups/terminal/shots/19-amber-manpage.png new file mode 100644 index 0000000..52d614b Binary files /dev/null and b/docs/mockups/terminal/shots/19-amber-manpage.png differ diff --git a/docs/mockups/terminal/shots/20-hud-metrics.png b/docs/mockups/terminal/shots/20-hud-metrics.png new file mode 100644 index 0000000..76263e0 Binary files /dev/null and b/docs/mockups/terminal/shots/20-hud-metrics.png differ diff --git a/docs/mockups/terminal/shots/_index.png b/docs/mockups/terminal/shots/_index.png new file mode 100644 index 0000000..513387d Binary files /dev/null and b/docs/mockups/terminal/shots/_index.png differ diff --git a/docs/superpowers/plans/2026-10-04-terminal-redesign.md b/docs/superpowers/plans/2026-10-04-terminal-redesign.md new file mode 100644 index 0000000..d6f331d --- /dev/null +++ b/docs/superpowers/plans/2026-10-04-terminal-redesign.md @@ -0,0 +1,9155 @@ +# Terminal Redesign (S2 "VS Code grouped") Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Rebuild the Filesmith renderer to match the signed-off mockup `docs/mockups/terminal-v5/10-s2-vscode-grouped.html`: a dark, strictly monochrome, square, flush-panel workbench (title bar with breadcrumb, collapsible sidebar, files table, Options / Preview / Info inspector, status bar), with every existing behaviour kept. + +**Architecture:** Pure view logic (row model, totals, sorting, select-all, size estimates, breadcrumbs, status summary, keyboard maps) lands first in `.ts` files with Vitest tests. Then a token layer (`theme/*.css`, CSS custom properties mirrored into Tailwind v4 `@theme`) and a 16px icon registry, then `components/ui/` primitives, and finally the shell, queue table, inspector, options split and remaining views are rebuilt on top. Main-process changes are limited to the spec's M1 to M5 plus one additive preload method (`pickFolder`). + +**Tech Stack:** Electron 43, React 19, TypeScript strict, Vite via electron-vite 5, Tailwind CSS v4, Vitest 3, Playwright `_electron`, `@fontsource/ibm-plex-sans` and `@fontsource/ibm-plex-mono`. + +**Spec:** `docs/superpowers/specs/2026-10-04-terminal-redesign-design.md` (design rules and feedback trail: `docs/design/redesign-direction.md`; target: `docs/mockups/terminal-v5/10-s2-vscode-grouped.html`, screenshot `docs/mockups/terminal-v5/shots/10-s2-vscode-grouped.png`). + +## Global Constraints + +- **Dark only.** No `dark:` variants, no theme toggle, no `prefers-color-scheme` branch. Every colour is a CSS custom property defined once in `src/renderer/src/theme/tokens.css`. +- **Strict monochrome.** Every colour has r=g=b. Errors, warnings and danger actions use a glyph, weight 600 and inverted fill, never red, green or amber. This includes the window close button, the former per-verb rail colours and `GROUP_COLOR`. The one exception is the Remove BG custom-colour swatch, which shows user data. +- **Token values, verbatim:** `--bg-0 #0a0a0a`, `--bg-1 #0f0f0f`, `--hover #1a1a1a`, `--selected #202020`, `--field #141414`, `--track #2a2a2a`, `--line #262626`, `--line-strong #3a3a3a`, `--fg1 #ededed`, `--fg2 #b4b4b4`, `--fg3 #8c8c8c`, `--fg-disabled #5c5c5c`, `--inv-bg #ededed`, `--inv-fg #0a0a0a`, `--inv-hover #ffffff`, `--inv-active #bdbdbd`, `--focus #ededed`. +- **Square and flush.** `border-radius: 0` globally, 1px borders only, no shadows, no cards except the estimate box. 2px `--fg1` bars mark active items. +- **Type:** `--sans: "IBM Plex Sans", system-ui, sans-serif`, `--mono: "IBM Plex Mono", Consolas, monospace`, weights 400/500/600, bundled through `@fontsource` (no Google Fonts; CSP stays as is). Body `13px/1.4 var(--sans)`. Numbers use `tabular-nums`. +- **Fixed heights:** 32 title bar / sidebar head / toolbar / tab bar / table row / info row; 28 table head / select / input / segment / in-row button; 36 sidebar item; 30 totals; 48 primary footer; 24 status bar. +- **No drop row, no progress bar under a row.** Progress reads `62%(4s)` in the status column. The whole window accepts drops. +- **Controls read as buttons** (boxed, outlined or inverted). No underlined or dotted words as controls. +- **Avoid Wind's signatures:** no floating progress-plus-button pill, no icon-tile header band, no big rounded cards of right-aligned setting rows. +- **No em-dashes anywhere**: code, comments, copy, commit messages, PR text. Use commas, en-dashes or rephrase. +- **Renderer stays browser-only.** No `fs` or `child_process` in `src/renderer`. The preload API is additive only (`pickFolder`); no existing `window.filesmith.*` method changes signature. +- **Never overwrite** a source or an existing output. Output paths still come from `reserveOutPath` / `reserveFileInDir` / `uniqueOutDir`. +- **State semantics are frozen.** Existing reducer actions, `queueKey`, `optionsKey` and the v2 session schema keep their meaning. New actions are additive. +- **React Fast Refresh rule:** `.tsx` files export components only. Pure helpers live in sibling `.ts` files so Vitest can import them. +- **Tests:** unit tests in `test/*.test.ts` (renderer modules imported by relative path `../src/renderer/src/...`, shared through `@shared/...`); e2e in `e2e/*.spec.ts`. +- **Owner checks O1 to O10** in spec section 5 are built as proposed there; they are approved together with this plan (O10 is the run scope, see Task 12). O7 resolution: ship OUTPUT > Location (M3); show FILES > If file exists as a fixed, disabled select reading `add (2)`; defer Naming and Keep metadata. +- **Delivery:** one issue, one branch `feat/-terminal-redesign`, one PR. Never merge without the owner's explicit "merge" for this PR. Version bump to `0.5.0` inside the PR. +- **Deleting files:** `trash ""` for anything outside this session's scratchpad (global rule); `git rm` for tracked files being removed by the change. + +## Review Focus + +- **Output folder removed between choosing it and pressing Run:** the job must fail with "Output folder not found" and must never fall back to writing somewhere else. Pinned in Task 3. +- **Zero-byte source or folder output:** the result cell must not show `NaN%` or `-100%`; a folder output shows `folder` and no percentage, a zero-byte source shows no percentage. Pinned in Task 5. +- **Re-running a row and restoring an old session:** a source re-queued in place must drop its previous `outputSize` / `outputPath`, and a `done` source restored from a pre-0.5 session (no `outputPath` on the source) must render `Done` with an empty result cell, not a crash or a stale value. Pinned in Tasks 4 and 5. +- **Header checkbox in a mixed queue:** with a selection in group B it selects all of B and nothing else; with no selection it acts on the first group in display order; it never produces a cross-group selection. Pinned in Task 6. +- **Estimator with nothing to go on:** unknown formats, audio without a bitrate, and zero-byte sources must yield `null` (empty cell, card hidden), never `0` or `NaN`; a batch with some non-estimable rows sums only the estimable ones and says how many. Pinned in Task 7. + +## File Structure + +``` +src/main/ + index.ts M1 window size, nativeTheme, bg; M4 FILESMITH_USER_DATA + windowSize.ts NEW initialWindowSize(workArea) (pure) + ipc.ts + 'files:pick-folder' + output.ts reserveOutPath(..., outDir?) + resolveOutDir() + jobQueue.ts ctx.outDir from options.outDir + tools/tool.ts ToolContext.outDir + tools/estimate.ts onPct(pct, etaSec) + tools/registry.ts pass ctx.outDir to every reserve/unique call; forward eta +src/preload/index.ts + pickFolder() +src/shared/ + icons.ts NEW ICON_NAMES, IconName + sizeEstimate.ts NEW estimateOutputBytes, medianRatio, estimateBatch + tabs.ts icon: IconName, SETTINGS_TAB, colour fields removed (Task 16) +src/renderer/src/ + index.css imports theme/*, Tailwind @theme mirror + main.tsx font imports + state.ts source->result link, select order, selectIds, hideFinished + theme/tokens.css NEW the only colour literals + theme/base.css NEW reset, focus, scrollbars, app grid, motion + theme/workbench.css NEW ported mockup component CSS + theme/controls.css NEW new controls: chips, range, popup, progress, disabled + theme/views.css NEW group rows, empty states, tools list, generate grid, menu, dialog, wipe + components/icons/ NEW Icon.tsx, shapes.ts + components/ui/ NEW Button, Checkbox, Segmented, Select, TextField, Setting, RangeField, + ChipGrid, ProgressBar, Tabs, EstimateCard, OutputSizeList, + roving.ts, selectNav.ts + components/shell/ NEW TitleBar, Breadcrumb, Sidebar, StatusBar, crumbs.ts, sidebarState.ts, + useSidebar.ts, railPrefs.ts, statusModel.ts, shortcuts.ts + components/queue/ NEW QueueToolbar, QueueTable, QueueRow, StatusCell, ResultCell, TotalsRow, + EmptyState, rowModel.ts, tableSort.ts, selectAll.ts, tableKeys.ts + components/inspector/ NEW Inspector, PreviewPane, InfoPane, Wipe, infoModel.ts + components/options/ NEW OptionsPane + one file per verb + generate/ + upscale/ + components/views/ NEW GenerateView, ToolsView, CompletedView, SettingsView + components/ DEL TopBar, TabRail, OperationTitle, DropZone, Queue, OptionsPanel, + ToolsGrid, PromptBox, CompletedView (old), Icon (old) +test/ new *.test.ts per pure module +e2e/ selector updates, ui.spec.ts, visual.spec.ts +``` + +The app will look half-converted between Tasks 8 and 16. That is expected on the branch; each task still passes `npm test`, `npm run typecheck` and `npm run lint`. + +--- + +### Task 0: Issue, branch and design record + +**Files:** + +- Add (currently untracked): `docs/design/redesign-direction.md`, `docs/mockups/terminal/`, `docs/mockups/terminal-v2/`, `docs/mockups/terminal-v3/`, `docs/mockups/terminal-v4/`, `docs/mockups/terminal-v5/`, `docs/superpowers/specs/2026-10-04-terminal-redesign-design.md`, `docs/superpowers/plans/2026-10-04-terminal-redesign.md` + +**Interfaces:** + +- Consumes: nothing. +- Produces: branch `feat/-terminal-redesign` where `` is the new issue number. Every later commit lands here. + +- [ ] **Step 1: Create the issue** + +```bash +gh issue create --title "Terminal redesign (S2 VS Code grouped)" --body "Rebuild the renderer to match docs/mockups/terminal-v5/10-s2-vscode-grouped.html. Spec: docs/superpowers/specs/2026-10-04-terminal-redesign-design.md. Plan: docs/superpowers/plans/2026-10-04-terminal-redesign.md." +``` + +Note the issue number `` it prints. + +- [ ] **Step 2: Branch from an up-to-date main** + +```bash +git switch main && git pull --ff-only +git switch -c feat/-terminal-redesign +``` + +- [ ] **Step 3: Commit the design record** + +`CLAUDE.md` has an uncommitted owner edit; leave it unstaged here. Task 18 edits `CLAUDE.md` and commits both together. + +```bash +git add docs/design/redesign-direction.md docs/mockups/terminal docs/mockups/terminal-v2 docs/mockups/terminal-v3 docs/mockups/terminal-v4 docs/mockups/terminal-v5 docs/superpowers/specs/2026-10-04-terminal-redesign-design.md docs/superpowers/plans/2026-10-04-terminal-redesign.md +git commit -m "docs(design): terminal redesign direction, mockups, spec and plan + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 1: Window defaults, dark native theme, test userData override, folder picker (M1, M4) + +**Files:** + +- Create: `src/main/windowSize.ts` +- Modify: `src/main/index.ts:1` (imports), `src/main/index.ts:134-148` (`createWindow`), top-level before `requestSingleInstanceLock` +- Modify: `src/main/ipc.ts` (after the `'files:pick'` handler at `:161-167`) +- Modify: `src/preload/index.ts:21-23` (next to `pickFiles` / `pickImage`) +- Test: `test/window-size.test.ts` + +**Interfaces:** + +- Consumes: nothing. +- Produces: `initialWindowSize(work: { width: number; height: number }): { width: number; height: number }`; `MIN_WINDOW = { width: 1100, height: 640 }`; `window.filesmith.pickFolder(): Promise`; env `FILESMITH_USER_DATA` (absolute folder) redirects `userData` when set. + +- [ ] **Step 1: Write the failing test** + +```ts +// test/window-size.test.ts +import { describe, expect, it } from 'vitest' +import { initialWindowSize, MIN_WINDOW } from '../src/main/windowSize' + +describe('initialWindowSize', () => { + it('uses 1440x900 when the work area is larger', () => { + expect(initialWindowSize({ width: 2560, height: 1400 })).toEqual({ width: 1440, height: 900 }) + }) + + it('shrinks to the work area on a small display', () => { + expect(initialWindowSize({ width: 1366, height: 728 })).toEqual({ width: 1366, height: 728 }) + }) + + it('never goes below the minimum, even on a tiny display', () => { + expect(initialWindowSize({ width: 1024, height: 600 })).toEqual(MIN_WINDOW) + }) +}) +``` + +- [ ] **Step 2: Run it and watch it fail** + +Run: `npx vitest run test/window-size.test.ts` +Expected: FAIL, cannot resolve `../src/main/windowSize`. + +- [ ] **Step 3: Implement** + +```ts +// src/main/windowSize.ts +// The redesign's table needs about 1440px to give the name column room +// (spec section 3.7). Clamp to the display so a small laptop still fits. + +export const DEFAULT_WINDOW = { width: 1440, height: 900 } +export const MIN_WINDOW = { width: 1100, height: 640 } + +export function initialWindowSize(work: { width: number; height: number }): { + width: number + height: number +} { + return { + width: Math.max(MIN_WINDOW.width, Math.min(DEFAULT_WINDOW.width, work.width)), + height: Math.max(MIN_WINDOW.height, Math.min(DEFAULT_WINDOW.height, work.height)) + } +} +``` + +- [ ] **Step 4: Run the test** + +Run: `npx vitest run test/window-size.test.ts` +Expected: PASS (3 tests). + +- [ ] **Step 5: Wire it into `createWindow`, set the dark native theme, add the userData override** + +In `src/main/index.ts` change the electron import to: + +```ts +import { app, nativeTheme, protocol, screen, shell, BrowserWindow } from 'electron' +import { initialWindowSize, MIN_WINDOW } from './windowSize' +``` + +Directly after the imports (before any `app.requestSingleInstanceLock()` call and before `app.whenReady`), add: + +```ts +// Test hook (spec M4): e2e points userData at a temp folder to seed a session. +// Read only when set, so a normal launch is unaffected. +const userDataOverride = process.env['FILESMITH_USER_DATA'] +if (userDataOverride) app.setPath('userData', userDataOverride) +``` + +Replace the `new BrowserWindow({...})` options head in `createWindow()` with: + +```ts +// Dark only: native scrollbars, dialogs and the pre-paint fill follow the app. +nativeTheme.themeSource = 'dark' +const size = initialWindowSize(screen.getPrimaryDisplay().workAreaSize) +const mainWindow = new BrowserWindow({ + width: size.width, + height: size.height, + minWidth: MIN_WINDOW.width, + minHeight: MIN_WINDOW.height, + show: false, + // Frameless: the renderer draws the 32px title bar and window controls. + frame: false, + backgroundColor: '#0a0a0a', + webPreferences: { + preload: join(__dirname, '../preload/index.js'), + sandbox: false + } +}) +``` + +- [ ] **Step 6: Add the folder picker (OUTPUT > Location needs it)** + +In `src/main/ipc.ts`, after the `'files:pick'` handler: + +```ts +// Output folder for OUTPUT > Location. Additive: no existing channel changes. +ipcMain.handle('files:pick-folder', async (e) => { + const r = await openDialog(e, { + title: 'Choose output folder', + properties: ['openDirectory', 'createDirectory'] + }) + return r.canceled || !r.filePaths.length ? null : r.filePaths[0] +}) +``` + +In `src/preload/index.ts`, after `pickImage`: + +```ts + /** Folder picker for the output location; resolves to the path or null. */ + pickFolder: (): Promise => ipcRenderer.invoke('files:pick-folder'), +``` + +- [ ] **Step 7: Verify** + +Run: `npm run typecheck && npm run lint && npm test` +Expected: all green. + +Run: `npm run dev`, check the window opens at 1440x900 (or the work area) with a near-black fill before the renderer paints, then close it. + +- [ ] **Step 8: Commit** + +```bash +git add src/main/windowSize.ts src/main/index.ts src/main/ipc.ts src/preload/index.ts test/window-size.test.ts +git commit -m "feat(main): dark window defaults, 1440x900 clamp, userData test hook, folder picker + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 2: Remaining-seconds estimate for ticker-driven jobs (M2) + +**Files:** + +- Modify: `src/main/tools/estimate.ts:19-47` +- Modify: `src/main/tools/registry.ts:118`, `:442`, `:595`, `:724-730`, `:990`, `:1307-1309` +- Test: `test/estimate.test.ts` (extend) + +**Interfaces:** + +- Consumes: `ToolContext.onProgress(percent, message?, etaSec?)` from `src/main/tools/tool.ts:7` (unchanged). +- Produces: `estimateProgress(expectedSec, onPct: (pct: number, etaSec: number | null) => void, opts)`. `etaSec` is `ceil(expectedSec - elapsed)` while positive, otherwise `null` (the row then shows `62%` without a bracket). + +- [ ] **Step 1: Write the failing test** (append to `test/estimate.test.ts`) + +```ts +describe('estimateProgress remaining seconds', () => { + it('counts down from the expected duration and goes null once overdue', () => { + vi.useFakeTimers() + const etas: (number | null)[] = [] + const t = estimateProgress(2, (_p, eta) => etas.push(eta)) + expect(etas[0]).toBe(2) // 2 - 0.2 = 1.8, rounded up + vi.advanceTimersByTime(1000) // elapsed 1.2s + expect(etas[etas.length - 1]).toBe(1) + vi.advanceTimersByTime(2000) // elapsed 3.2s, past the estimate + expect(etas[etas.length - 1]).toBeNull() + t.stop() + vi.useRealTimers() + }) +}) +``` + +- [ ] **Step 2: Run it and watch it fail** + +Run: `npx vitest run test/estimate.test.ts` +Expected: FAIL, `etas[0]` is `undefined`. + +- [ ] **Step 3: Implement** (in `estimate.ts`) + +Change the callback type and the tick: + +```ts +export function estimateProgress( + expectedSec: number, + onPct: (pct: number, etaSec: number | null) => void, + opts: { ceiling?: number; startPct?: number } = {} +): EstimateTicker { +``` + +and inside `tick`: + +```ts +const pct = startPct + (ceiling - startPct) * (1 - Math.exp(-elapsed / tau)) +// The same expected duration that shapes the curve doubles as an ETA, so +// image, PDF and rembg rows can show 62%(4s) like ffmpeg rows do. +const left = expectedSec - elapsed +onPct(Math.min(ceiling, pct), left > 0 ? Math.ceil(left) : null) +``` + +- [ ] **Step 4: Forward the ETA at every caller in `registry.ts`** + +Replace each `(p) => ctx.onProgress(p)` passed to `estimateProgress` with `(p, eta) => ctx.onProgress(p, undefined, eta)` (lines 118, 442, 595, 990). For the PiD ticker at `:724-730`: + +```ts +est = estimateProgress( + expectedSec, + (p, eta) => { + lastPct = p + ctx.onProgress(p, undefined, eta) + }, + { startPct: lastPct } +) +``` + +For the archive from-pdf ticker at `:1307-1309`, which caps the estimate at 90%: + +```ts +const est = estimateProgress(estimateSecForBytes(file.size, 0.15), (p, eta) => + ctx.onProgress(Math.min(p, 90), undefined, eta) +) +``` + +Check with `grep -n "estimateProgress(" src/main/tools/registry.ts` that all six call sites now take `(p, eta)`. + +`message` stays `undefined`, and the reducer already keeps the last message on a percent-only update (`state.ts:647`). A `null` ETA reaches the renderer as `undefined` (`jobQueue.ts:99`); Task 4 makes the reducer drop the previous ETA in that case, so an overdue row shows `62%` and not a frozen `(1s)`. + +- [ ] **Step 5: Verify** + +Run: `npx vitest run test/estimate.test.ts && npm run typecheck && npm run lint` +Expected: PASS, typecheck and lint clean. + +- [ ] **Step 6: Commit** + +```bash +git add src/main/tools/estimate.ts src/main/tools/registry.ts test/estimate.test.ts +git commit -m "feat(engine): report remaining seconds from the estimated-progress ticker + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 3: Optional output folder (M3, OUTPUT > Location) + +**Files:** + +- Modify: `src/main/output.ts` +- Modify: `src/main/tools/tool.ts:4-8` +- Modify: `src/main/jobQueue.ts:89-100` +- Modify: `src/main/tools/registry.ts` (every `reserveOutPath(file.path, ...)` and `uniqueOutDir(dirname(file.path), ...)`) +- Test: `test/output.test.ts` (extend) + +**Interfaces:** + +- Consumes: `JobOptions.outDir?: string` (new optional key; empty string or absent means next to the source). +- Produces: `reserveOutPath(sourcePath: string, ext: string, tag: string, outDir?: string): string`; `resolveOutDir(value: unknown): string | undefined` (throws `Error('Output folder not found: ')` when set but missing); `ToolContext.outDir?: string`. + +Every tool helper already has `ctx` in scope (they all call `ctx.onProgress`), so threading the folder through the context reaches the Comfy, ncnn and PiD helpers that never see `options`. + +- [ ] **Step 1: Write the failing tests** (append to `test/output.test.ts`; reuse that file's temp-dir setup and imports, adding `resolveOutDir` to the `../src/main/output` import and `mkdtempSync`, `mkdirSync`, `writeFileSync`, `existsSync` from `fs`, `join` from `path`, `tmpdir` from `os` if not already imported) + +```ts +describe('output folder', () => { + it('reserves the output in the chosen folder, not next to the source', () => { + const root = mkdtempSync(join(tmpdir(), 'fs-outdir-')) + const srcDir = join(root, 'src') + const outDir = join(root, 'out') + mkdirSync(srcDir) + mkdirSync(outDir) + const src = join(srcDir, 'a.png') + writeFileSync(src, 'x') + const out = reserveOutPath(src, '.webp', 'converted', outDir) + expect(out).toBe(join(outDir, 'a.webp')) + expect(existsSync(join(srcDir, 'a.webp'))).toBe(false) + }) + + it('keeps collision safety inside the chosen folder', () => { + const root = mkdtempSync(join(tmpdir(), 'fs-outdir-')) + writeFileSync(join(root, 'a.webp'), 'existing') + const out = reserveOutPath(join(root, 'elsewhere', 'a.png'), '.webp', 'converted', root) + expect(out).toBe(join(root, 'a (converted).webp')) + }) + + it('treats an empty or missing option as next to the source', () => { + expect(resolveOutDir(undefined)).toBeUndefined() + expect(resolveOutDir('')).toBeUndefined() + }) + + it('refuses a folder that no longer exists instead of writing elsewhere', () => { + const gone = join(tmpdir(), `fs-gone-${Date.now()}`) + expect(() => resolveOutDir(gone)).toThrow(/Output folder not found/) + }) +}) +``` + +- [ ] **Step 2: Run them and watch them fail** + +Run: `npx vitest run test/output.test.ts` +Expected: FAIL, `resolveOutDir` is not exported and the first test writes next to the source. + +- [ ] **Step 3: Implement in `output.ts`** + +```ts +/** Atomically-reserved output with a new extension: next to the source, or in + * `outDir` when the user chose an output folder. Collision rules are identical. */ +export function reserveOutPath( + sourcePath: string, + ext: string, + tag: string, + outDir?: string +): string { + const dir = outDir ?? dirname(sourcePath) + const name = basename(sourcePath, extname(sourcePath)) + return reserveFileInDir(dir, name, ext, tag) +} + +/** The job's output folder from `options.outDir`, or undefined for "next to + * source". A folder that has gone missing is an error: silently writing next to + * the source would put files where the user did not ask for them. */ +export function resolveOutDir(value: unknown): string | undefined { + if (typeof value !== 'string' || value.trim() === '') return undefined + if (!existsSync(value)) throw new Error(`Output folder not found: ${value}`) + return value +} +``` + +- [ ] **Step 4: Thread it through the context** + +`src/main/tools/tool.ts`, inside `ToolContext`: + +```ts + /** Folder chosen under OUTPUT > Location; undefined means next to the source. */ + outDir?: string +``` + +`src/main/jobQueue.ts`, in `execute()` (add `resolveOutDir` to an `import { resolveOutDir } from './output'`): + +```ts + const file = fileInfoFromPath(req.input) + const outDir = resolveOutDir(req.options.outDir) + const output = await tool.run(file, req.options, { + signal: ctrl.signal, + outDir, + onProgress: (percent, message, etaSec) => +``` + +`resolveOutDir` throwing lands in the existing `catch`, which emits `failed` with the message. + +- [ ] **Step 5: Pass `ctx.outDir` at every reserve site in `registry.ts`** + +Run these two mechanical replacements, then read every changed line: + +```bash +sed -i -E "s/reserveOutPath\(file\.path, ([^)]*)\)/reserveOutPath(file.path, \1, ctx.outDir)/" src/main/tools/registry.ts +sed -i -E "s/uniqueOutDir\(dirname\(file\.path\),/uniqueOutDir(ctx.outDir ?? dirname(file.path),/" src/main/tools/registry.ts +grep -n "reserveOutPath\|uniqueOutDir" src/main/tools/registry.ts +``` + +Expected: every call ends in `ctx.outDir)` or starts with `ctx.outDir ??`. The multi-line `uniqueOutDir(` at `:432-433` and `:1164-1165` puts `dirname(file.path),` on its own line; edit those two by hand to `ctx.outDir ?? dirname(file.path),`. `src/main/generate/index.ts:120` is left alone (Generate has no Location setting). + +- [ ] **Step 6: Verify** + +Run: `npm test && npm run typecheck && npm run lint` +Expected: all green, including the existing `registry*.test.ts` and `percent-paths.test.ts`. + +- [ ] **Step 7: Commit** + +```bash +git add src/main/output.ts src/main/tools/tool.ts src/main/jobQueue.ts src/main/tools/registry.ts test/output.test.ts +git commit -m "feat(engine): optional output folder with unchanged collision safety + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 4: State additions: source-to-result link, ordered range, explicit selection, clear finished, Settings tab id + +**Files:** + +- Modify: `src/renderer/src/state.ts` (`Action` union `:167-179`, `selectInQueue` `:419-455`, `markQueued` `:578-596`, `jobEvent` done branch `:604-637`) +- Modify: `src/shared/tabs.ts` (`TabId` `:17-18`, `COMPLETED_TAB` `:100-108`, `tabById` `:223-226`) +- Test: `test/queues.test.ts`, `test/verb-state.test.ts`, `test/tabs.test.ts` (extend) + +**Interfaces:** + +- Consumes: existing `reducer`, `initialState`, `QueueItem`. +- Produces: + - Action `{ type: 'select'; id: string; mode: SelectMode; order?: string[] }`: `order` is the visible id order for range selection. + - Action `{ type: 'selectIds'; ids: string[] }`: replaces the current workspace selection; anchor becomes `ids[0] ?? null`. + - Action `{ type: 'hideFinished' }`: sets `hiddenInput` on `done` and `canceled` input rows of the current workspace. + - On a `done` job event the source row gains `outputPath` and `outputSize`; `markQueued` clears both, and `etaSec`. + - A `running` event that carries a percent but no `etaSec` clears the stored `etaSec` (it used to keep the previous value forever, which would freeze `62%(1s)` once a ticker runs past its estimate). + - `TabId` gains `'settings'`; `SETTINGS_TAB: Tab`; `tabById('settings')` returns it. + +- [ ] **Step 1: Write the failing tests** + +Append to `test/queues.test.ts`: + +```ts +describe('source to result link', () => { + it('writes the output path and size onto the source when its job finishes', () => { + let s = reducer(start, { type: 'addItems', files: [img('a.png')], key: CONVERT }) + const id = s.queues[CONVERT]!.items[0].id + s = reducer(s, { + type: 'jobEvent', + event: { id, status: 'done', outputPath: 'C:/x/a.webp', outputSize: 4 } + }) + const src = s.queues[CONVERT]!.items.find((i) => i.id === id)! + expect(src.outputPath).toBe('C:/x/a.webp') + expect(src.outputSize).toBe(4) + // The separate result item, which Completed relies on, is still appended. + expect(s.queues[CONVERT]!.items.filter((i) => i.isResult)).toHaveLength(1) + }) + + it('clears the previous result when the row is queued again in place', () => { + let s = reducer(start, { type: 'addItems', files: [img('a.png')], key: CONVERT }) + const id = s.queues[CONVERT]!.items[0].id + s = reducer(s, { + type: 'jobEvent', + event: { id, status: 'done', outputPath: 'C:/x/a.webp', outputSize: 4 } + }) + s = reducer(s, { type: 'markQueued', ids: [id] }) + const src = s.queues[CONVERT]!.items.find((i) => i.id === id)! + expect(src.outputPath).toBeUndefined() + expect(src.outputSize).toBeUndefined() + }) +}) + +describe('compact ETA', () => { + it('drops a stale ETA on a percent-only update and on re-queue', () => { + let s = reducer(start, { type: 'addItems', files: [img('a.png')], key: CONVERT }) + const id = s.queues[CONVERT]!.items[0].id + const eta = (): number | undefined => s.queues[CONVERT]!.items.find((i) => i.id === id)!.etaSec + s = reducer(s, { type: 'jobEvent', event: { id, status: 'running', percent: 40, etaSec: 4 } }) + expect(eta()).toBe(4) + s = reducer(s, { type: 'jobEvent', event: { id, status: 'running', message: 'Still going' } }) + expect(eta()).toBe(4) // a message-only update keeps it + s = reducer(s, { type: 'jobEvent', event: { id, status: 'running', percent: 60 } }) + expect(eta()).toBeUndefined() + s = reducer(s, { type: 'jobEvent', event: { id, status: 'running', percent: 70, etaSec: 2 } }) + s = reducer(s, { type: 'markQueued', ids: [id] }) + expect(eta()).toBeUndefined() + }) +}) + +describe('clear finished', () => { + it('hides done and canceled inputs but keeps their results for Completed', () => { + let s = reducer(start, { + type: 'addItems', + files: [img('a.png'), img('b.png'), img('c.png')], + key: CONVERT + }) + const [a, b] = s.queues[CONVERT]!.items.map((i) => i.id) + s = reducer(s, { + type: 'jobEvent', + event: { id: a, status: 'done', outputPath: 'C:/x/a.webp' } + }) + s = reducer(s, { type: 'jobEvent', event: { id: b, status: 'canceled' } }) + s = reducer(s, { type: 'hideFinished' }) + const q = s.queues[CONVERT]! + expect(q.items.filter((i) => !i.isResult && !i.hiddenInput).map((i) => i.file.name)).toEqual([ + 'c.png' + ]) + expect(q.items.some((i) => i.isResult && i.outputPath === 'C:/x/a.webp')).toBe(true) + }) +}) +``` + +Append to `test/verb-state.test.ts` (reuse its `img` helper if present, otherwise copy the `img` helper from `test/queues.test.ts`): + +```ts +describe('selection in visible order', () => { + it('ranges over the order the table shows, not insertion order', () => { + let s = reducer(initialState, { + type: 'addItems', + files: [img('a.png'), img('b.png'), img('c.png')], + key: 'convert' + }) + const [a, b, c] = s.queues.convert!.items.map((i) => i.id) + s = reducer(s, { type: 'select', id: c, mode: 'single' }) + // Table sorted c, a, b: shift-click a selects c and a only. + s = reducer(s, { type: 'select', id: a, mode: 'range', order: [c, a, b] }) + expect(s.queues.convert!.selected.sort()).toEqual([a, c].sort()) + }) + + it('selectIds replaces the selection and drops ids outside the queue', () => { + let s = reducer(initialState, { + type: 'addItems', + files: [img('a.png'), img('b.png')], + key: 'convert' + }) + const [a, b] = s.queues.convert!.items.map((i) => i.id) + s = reducer(s, { type: 'selectIds', ids: [b, a, 'nope'] }) + expect(s.queues.convert!.selected).toEqual([b, a]) + expect(s.queues.convert!.anchor).toBe(b) + }) +}) +``` + +Append to `test/tabs.test.ts`: + +```ts +import { SETTINGS_TAB } from '@shared/tabs' + +describe('settings tab', () => { + it('is reachable by id but is not one of the seven verbs', () => { + expect(tabById('settings')).toBe(SETTINGS_TAB) + expect(TABS.some((t) => t.id === 'settings')).toBe(false) + }) +}) +``` + +(Merge the import into the file's existing `@shared/tabs` import.) + +- [ ] **Step 2: Run them and watch them fail** + +Run: `npx vitest run test/queues.test.ts test/verb-state.test.ts test/tabs.test.ts` +Expected: FAIL (source has no `outputPath`; unknown action types; `SETTINGS_TAB` missing). + +- [ ] **Step 3: Implement in `state.ts`** + +Action union additions: + +```ts + | { type: 'select'; id: string; mode: SelectMode; order?: string[] } + | { type: 'selectIds'; ids: string[] } + | { type: 'hideFinished' } +``` + +(replace the existing `select` member with the one above). + +`selectInQueue` gains an `order` parameter: + +```ts +function selectInQueue(q: QueueState, id: string, mode: SelectMode, visible?: string[]): QueueState { + const known = new Set(q.items.map((i) => i.id)) + // Range follows what the user SEES: the table may be sorted. + const order = visible ? visible.filter((v) => known.has(v)) : q.items.map((i) => i.id) +``` + +and the reducer case becomes `return mapQueue(state, (q) => selectInQueue(q, action.id, action.mode, action.order))`. + +New reducer cases (before `default`): + +```ts + case 'selectIds': + return mapQueue(state, (q) => { + const known = new Set(q.items.filter(inInput).map((i) => i.id)) + const ids = action.ids.filter((id) => known.has(id)) + return { ...q, selected: ids, anchor: ids[0] ?? null } + }) + case 'hideFinished': + return mapQueue(state, (q) => { + const items = q.items.map((i) => + !i.isResult && (i.status === 'done' || i.status === 'canceled') + ? { ...i, hiddenInput: true } + : i + ) + const kept = items.filter((i) => inInput(i) || inOutput(i)) + const selectable = new Set(kept.filter(inInput).map((i) => i.id)) + return { + items: kept, + selected: q.selected.filter((s) => selectable.has(s)), + anchor: q.anchor && selectable.has(q.anchor) ? q.anchor : null + } + }) +``` + +`markQueued`: next to `outputPath: undefined,` add `outputSize: undefined,` and `etaSec: undefined,`. + +Non-done `jobEvent` branch (`state.ts:640-652`): replace `etaSec: e.etaSec ?? i.etaSec,` with + +```ts + // A fresh percent without an ETA means the tool no longer has one + // (ticker overdue, ffmpeg before its first time stamp): drop the old one. + etaSec: e.percent != null ? e.etaSec : i.etaSec, +``` + +`jobEvent` done branch: in the source update object add the link: + +```ts + ? { + ...i, + status: 'done' as ItemStatus, + percent: 100, + message: undefined, + error: undefined, + // The table shows source -> result on one row (spec 6.1). + outputPath: e.outputPath, + outputSize: e.outputSize + } +``` + +Check that `mapQueue` acts on the current workspace queue (it does for `select` and `clearSelection` today); `hideFinished` and `selectIds` use it the same way. + +- [ ] **Step 4: Implement in `tabs.ts`** + +```ts +export type TabId = + | 'convert' + | 'compress' + | 'resize' + | 'upscale' + | 'removebg' + | 'generate' + | 'tools' + | 'completed' + | 'settings' +``` + +After `COMPLETED_TAB`: + +```ts +/** Sidebar layout and tool status. Pinned to the bottom group, not a verb. */ +export const SETTINGS_TAB: Tab = { + id: 'settings', + label: 'Settings', + desc: 'Sidebar and tools', + color: '#6e6e6e', + icon: 'settings', + kinds: [], + tool: 'convert' +} +``` + +(`color` is removed from every tab in Task 16; it stays here so the type still checks.) + +```ts +export function tabById(id: TabId): Tab { + if (id === 'completed') return COMPLETED_TAB + if (id === 'settings') return SETTINGS_TAB + return TABS.find((t) => t.id === id) ?? TABS[0] +} +``` + +- [ ] **Step 5: Run the tests** + +Run: `npm test && npm run typecheck` +Expected: PASS. If `App.tsx` fails typecheck on an exhaustive `switch (state.tab)`, add a `case 'settings':` that falls through to the Completed branch for now; Task 15 gives it its own view. + +- [ ] **Step 6: Commit** + +```bash +git add src/renderer/src/state.ts src/shared/tabs.ts test/queues.test.ts test/verb-state.test.ts test/tabs.test.ts +git commit -m "feat(state): link sources to results, ordered range, selectIds, clear finished, settings tab + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 5: Row model (result, status, row action, totals, compact progress) + +**Files:** + +- Create: `src/renderer/src/components/queue/rowModel.ts` +- Test: `test/row-model.test.ts` + +**Interfaces:** + +- Consumes: `QueueItem`, `inInput`, `ItemStatus` from `state.ts`; `formatBytes` from `@shared/compress`. +- Produces (all exported from `rowModel.ts`): + - `formatEtaCompact(sec: number | null | undefined): string` gives `'(4s)'`, `'(2m)'`, `'(1h 5m)'` or `''`. + - `progressParts(pct: number, etaSec?: number | null): { pct: string; eta: string }` and `formatProgress(pct, etaSec?) => pct + eta` (`formatProgress(62, 4) === '62%(4s)'`). + - `kindLabel(ext: string): string` gives `'HEIC'`. + - `pctChange(src: number, out: number): number | null` and `formatPct(n: number): string` (`-80%`, `+12%`, `0%`). + - `shortError(message: string | undefined): string`. + - `type RowActionKind = 'reveal' | 'remove' | 'cancel' | 'retry'` + - `interface RowView { kind: string; size: string; result: ResultView | null; status: StatusView; action: RowActionView }` with + `ResultView { text: string; pct: string | null; estimate: boolean; grew: boolean }`, + `StatusView { kind: ItemStatus; text: string; pct?: string; eta?: string; title?: string }`, + `RowActionView { kind: RowActionKind; icon: 'folder' | 'close' | 'retry'; label: string; ghost: boolean }`. + - `rowView(item: QueueItem, estimateBytes?: number | null): RowView` + - `interface Totals { files: number; bytes: number; doneSrc: number; doneOut: number; done: number; failed: number; inFlight: number }` and `queueTotals(items: QueueItem[]): Totals` (input rows only). + - `doneSamples(items: QueueItem[], group: string, options: JobOptions): { source: number; output: number }[]` (done input rows of `group` whose `runOptions` equal `options`). + +- [ ] **Step 1: Write the failing test** + +```ts +// test/row-model.test.ts +import { describe, expect, it } from 'vitest' +import { + doneSamples, + formatEtaCompact, + formatPct, + formatProgress, + kindLabel, + pctChange, + queueTotals, + rowView, + shortError +} from '../src/renderer/src/components/queue/rowModel' +import type { QueueItem } from '../src/renderer/src/state' + +const item = (over: Partial = {}): QueueItem => ({ + id: over.id ?? 'a', + file: { path: 'C:/x/a.heic', name: 'a.heic', ext: '.heic', kind: 'image', size: 3_200_000 }, + thumb: null, + status: 'ready', + percent: 0, + ...over +}) + +describe('compact progress', () => { + it('formats the mockup string', () => { + expect(formatProgress(62, 4)).toBe('62%(4s)') + }) + it('drops the bracket without an ETA', () => { + expect(formatProgress(62.4, undefined)).toBe('62%') + expect(formatProgress(62.4, null)).toBe('62%') + }) + it('keeps one decimal below 10%', () => { + expect(formatProgress(5.5, 120)).toBe('5.5%(2m)') + expect(formatProgress(0, 3)).toBe('0%(3s)') + }) + it('formats minutes and hours', () => { + expect(formatEtaCompact(59)).toBe('(59s)') + expect(formatEtaCompact(90)).toBe('(2m)') + expect(formatEtaCompact(3900)).toBe('(1h 5m)') + expect(formatEtaCompact(-1)).toBe('') + expect(formatEtaCompact(Number.NaN)).toBe('') + }) +}) + +describe('labels', () => { + it('derives the kind from the extension', () => { + expect(kindLabel('.heic')).toBe('HEIC') + expect(kindLabel('')).toBe('') + }) + it('signs the percentage with a plain hyphen', () => { + expect(pctChange(1000, 200)).toBe(-80) + expect(formatPct(-80)).toBe('-80%') + expect(formatPct(12)).toBe('+12%') + expect(formatPct(0)).toBe('0%') + }) + it('has no percentage for a zero-byte source', () => { + expect(pctChange(0, 10)).toBeNull() + }) +}) + +describe('shortError', () => { + it('maps known failures to a short label', () => { + expect(shortError('magick: Unsupported compression method (5)')).toBe('Unsupported compression') + expect(shortError('RAR output needs WinRAR (Rar.exe) installed')).toBe('WinRAR not found') + expect(shortError('The archive is password protected')).toBe('Password-protected') + }) + it('falls back to the first clause, capped at 32 characters', () => { + expect(shortError('Disk full. Free some space and retry')).toBe('Disk full') + const long = shortError('Something very long happened while encoding the frames') + expect(long).toHaveLength(32) + expect(long.endsWith('…')).toBe(true) + }) + it('says Failed when there is no message', () => { + expect(shortError(undefined)).toBe('Failed') + }) +}) + +describe('rowView', () => { + it('done: actual result, percentage, reveal action as ghost', () => { + const v = rowView(item({ status: 'done', outputPath: 'C:/x/a.webp', outputSize: 640_000 })) + expect(v.status).toMatchObject({ kind: 'done', text: 'Done' }) + expect(v.result).toEqual({ text: '625 KB', pct: '-80%', estimate: false, grew: false }) + expect(v.action).toEqual({ + kind: 'reveal', + icon: 'folder', + label: 'Show in folder', + ghost: true + }) + }) + it('done with a folder output: no size, no percentage', () => { + const v = rowView(item({ status: 'done', outputPath: 'C:/x/a (pages)' })) + expect(v.result).toEqual({ text: 'folder', pct: null, estimate: false, grew: false }) + }) + it('done from an old session without a link: empty result, no crash', () => { + const v = rowView(item({ status: 'done' })) + expect(v.result).toBeNull() + expect(v.status.text).toBe('Done') + }) + it('growth is flagged so the cell can use weight instead of amber', () => { + const v = rowView(item({ status: 'done', outputPath: 'C:/x/a.png', outputSize: 6_400_000 })) + expect(v.result).toMatchObject({ pct: '+100%', grew: true }) + }) + it('running: progress, compact ETA, grey estimate, cancel ghost', () => { + const v = rowView( + item({ status: 'running', percent: 62, hasProgress: true, etaSec: 4 }), + 410_000 + ) + expect(v.status).toMatchObject({ kind: 'running', pct: '62%', eta: '(4s)' }) + expect(v.result).toMatchObject({ text: '~400 KB', estimate: true }) + expect(v.action).toMatchObject({ kind: 'cancel', icon: 'close', ghost: true }) + }) + it('running without real progress shows an ellipsis and no estimate cell', () => { + const v = rowView(item({ status: 'running', hasProgress: false }), null) + expect(v.status.pct).toBe('…') + expect(v.result).toBeNull() + }) + it('queued: no result, remove ghost', () => { + const v = rowView(item({ status: 'queued' }), 999) + expect(v.result).toBeNull() + expect(v.status.text).toBe('Queued') + expect(v.action).toMatchObject({ kind: 'remove', ghost: true }) + }) + it('failed: short label, full tooltip, retry always visible', () => { + const v = rowView( + item({ status: 'failed', error: 'Unsupported compression in TIFF. Try PNG.' }) + ) + expect(v.status).toMatchObject({ + text: 'Unsupported compression', + title: 'Unsupported compression in TIFF. Try PNG.' + }) + expect(v.result).toBeNull() + expect(v.action).toEqual({ kind: 'retry', icon: 'retry', label: 'Retry', ghost: false }) + }) + it('ready: blank status, remove ghost', () => { + const v = rowView(item()) + expect(v.status.text).toBe('') + expect(v.action.kind).toBe('remove') + }) + it('canceled: Canceled, retry ghost', () => { + const v = rowView(item({ status: 'canceled' })) + expect(v.status.text).toBe('Canceled') + expect(v.action).toMatchObject({ kind: 'retry', ghost: true }) + }) + it('kind and size columns', () => { + const v = rowView(item()) + expect(v.kind).toBe('HEIC') + expect(v.size).toBe('3.1 MB') + }) +}) + +describe('queueTotals', () => { + it('sums inputs, counts statuses, and only adds done rows with a file output', () => { + const t = queueTotals([ + item({ id: '1', status: 'done', outputPath: 'o1', outputSize: 1_000_000 }), + item({ id: '2', status: 'done', outputPath: 'C:/x/folder' }), + item({ id: '3', status: 'running' }), + item({ id: '4', status: 'queued' }), + item({ id: '5', status: 'failed' }), + item({ id: 'r', isResult: true, status: 'done', outputPath: 'o1', outputSize: 1_000_000 }), + item({ id: 'h', hiddenInput: true }) + ]) + expect(t).toEqual({ + files: 5, + bytes: 16_000_000, + doneSrc: 3_200_000, + doneOut: 1_000_000, + done: 2, + failed: 1, + inFlight: 2 + }) + }) +}) + +describe('doneSamples', () => { + it('uses done rows of the same group run with the same options', () => { + const opts = { format: '.webp', quality: 'balanced' } + const s = doneSamples( + [ + item({ id: '1', status: 'done', outputSize: 800_000, runOptions: opts }), + item({ + id: '2', + status: 'done', + outputSize: 100, + runOptions: { ...opts, quality: 'best' } + }), + item({ id: '3', status: 'running', runOptions: opts }) + ], + 'image', + { quality: 'balanced', format: '.webp' } + ) + expect(s).toEqual([{ source: 3_200_000, output: 800_000 }]) + }) +}) +``` + +- [ ] **Step 2: Run it and watch it fail** + +Run: `npx vitest run test/row-model.test.ts` +Expected: FAIL, module not found. + +- [ ] **Step 3: Implement** + +```ts +// src/renderer/src/components/queue/rowModel.ts +import type { JobOptions } from '@shared/types' +import { formatBytes } from '@shared/compress' +import { groupOf, inInput, type ItemStatus, type QueueItem } from '../../state' + +// Pure view model for one table row (spec 4.3) and the totals row (spec 4.4). +// Kept apart from the .tsx files so React Fast Refresh and Vitest both work. + +export function formatEtaCompact(sec: number | null | undefined): string { + if (sec == null || !Number.isFinite(sec) || sec < 0) return '' + const s = Math.round(sec) + if (s < 60) return `(${Math.max(1, s)}s)` + const m = Math.round(sec / 60) + if (m < 60) return `(${m}m)` + return `(${Math.floor(m / 60)}h ${m % 60}m)` +} + +export function progressParts(pct: number, etaSec?: number | null): { pct: string; eta: string } { + const n = pct > 0 && pct < 10 ? pct.toFixed(1) : String(Math.round(pct)) + return { pct: `${n}%`, eta: formatEtaCompact(etaSec) } +} + +export function formatProgress(pct: number, etaSec?: number | null): string { + const p = progressParts(pct, etaSec) + return p.pct + p.eta +} + +export function kindLabel(ext: string): string { + return ext.replace(/^\./, '').toUpperCase() +} + +export function pctChange(src: number, out: number): number | null { + if (!(src > 0) || !Number.isFinite(out)) return null + return Math.round(((out - src) / src) * 100) +} + +export function formatPct(n: number): string { + return n > 0 ? `+${n}%` : `${n}%` +} + +const KNOWN_ERRORS: [RegExp, string][] = [ + [/unsupported compression/i, 'Unsupported compression'], + [/winrar|rar\.exe/i, 'WinRAR not found'], + [/password/i, 'Password-protected'], + [/no (en|de)code delegate|missing encoder/i, 'Missing encoder'], + [/not installed|could not be found|enoent|tool missing/i, 'Tool missing'] +] +const MAX_ERROR = 32 + +export function shortError(message: string | undefined): string { + const m = (message ?? '').trim() + if (!m) return 'Failed' + for (const [re, label] of KNOWN_ERRORS) if (re.test(m)) return label + const first = m.split(/[.:;\n]|,\s/)[0].trim() || m + return first.length > MAX_ERROR ? first.slice(0, MAX_ERROR - 1).trimEnd() + '…' : first +} + +export type RowActionKind = 'reveal' | 'remove' | 'cancel' | 'retry' +export interface ResultView { + text: string + pct: string | null + estimate: boolean + grew: boolean +} +export interface StatusView { + kind: ItemStatus + text: string + pct?: string + eta?: string + title?: string +} +export interface RowActionView { + kind: RowActionKind + icon: 'folder' | 'close' | 'retry' + label: string + ghost: boolean +} +export interface RowView { + kind: string + size: string + result: ResultView | null + status: StatusView + action: RowActionView +} + +function resultFor(item: QueueItem, estimateBytes?: number | null): ResultView | null { + const src = item.file.size + if (item.status === 'done') { + if (item.outputSize != null) { + const pct = pctChange(src, item.outputSize) + return { + text: formatBytes(item.outputSize), + pct: pct == null ? null : formatPct(pct), + estimate: false, + grew: pct != null && pct > 0 + } + } + return item.outputPath ? { text: 'folder', pct: null, estimate: false, grew: false } : null + } + if (item.status === 'running' && estimateBytes != null && estimateBytes > 0) { + const pct = pctChange(src, estimateBytes) + return { + text: `~${formatBytes(estimateBytes)}`, + pct: pct == null ? null : formatPct(pct), + estimate: true, + grew: pct != null && pct > 0 + } + } + return null +} + +function statusFor(item: QueueItem): StatusView { + switch (item.status) { + case 'done': + return { kind: 'done', text: 'Done' } + case 'queued': + return { kind: 'queued', text: 'Queued' } + case 'running': { + if (!item.hasProgress) return { kind: 'running', text: '', pct: '…', eta: '' } + const p = progressParts(item.percent, item.etaSec) + return { kind: 'running', text: '', pct: p.pct, eta: p.eta } + } + case 'failed': + return { kind: 'failed', text: shortError(item.error), title: item.error ?? 'Failed' } + case 'canceled': + return { kind: 'canceled', text: 'Canceled' } + default: + return { kind: 'ready', text: '' } + } +} + +function actionFor(status: ItemStatus): RowActionView { + switch (status) { + case 'done': + return { kind: 'reveal', icon: 'folder', label: 'Show in folder', ghost: true } + case 'running': + return { kind: 'cancel', icon: 'close', label: 'Cancel', ghost: true } + case 'failed': + return { kind: 'retry', icon: 'retry', label: 'Retry', ghost: false } + case 'canceled': + return { kind: 'retry', icon: 'retry', label: 'Retry', ghost: true } + default: + return { kind: 'remove', icon: 'close', label: 'Remove', ghost: true } + } +} + +export function rowView(item: QueueItem, estimateBytes?: number | null): RowView { + return { + kind: kindLabel(item.file.ext), + size: formatBytes(item.file.size), + result: resultFor(item, estimateBytes), + status: statusFor(item), + action: actionFor(item.status) + } +} + +export interface Totals { + files: number + bytes: number + doneSrc: number + doneOut: number + done: number + failed: number + inFlight: number +} + +export function queueTotals(items: QueueItem[]): Totals { + const t: Totals = { files: 0, bytes: 0, doneSrc: 0, doneOut: 0, done: 0, failed: 0, inFlight: 0 } + for (const i of items) { + if (!inInput(i)) continue + t.files += 1 + t.bytes += i.file.size + if (i.status === 'done') { + t.done += 1 + if (i.outputSize != null) { + t.doneSrc += i.file.size + t.doneOut += i.outputSize + } + } else if (i.status === 'failed') t.failed += 1 + else if (i.status === 'queued' || i.status === 'running') t.inFlight += 1 + } + return t +} + +function sameOptions(a: JobOptions | undefined, b: JobOptions): boolean { + if (!a) return false + const keys = new Set([...Object.keys(a), ...Object.keys(b)]) + for (const k of keys) if (JSON.stringify(a[k]) !== JSON.stringify(b[k])) return false + return true +} + +export function doneSamples( + items: QueueItem[], + group: string, + options: JobOptions +): { source: number; output: number }[] { + return items + .filter( + (i) => + inInput(i) && + i.status === 'done' && + i.outputSize != null && + groupOf(i.file) === group && + sameOptions(i.runOptions, options) + ) + .map((i) => ({ source: i.file.size, output: i.outputSize as number })) +} +``` + +Note: `formatBytes(640_000)` is `625 KB` and `formatBytes(410_000)` is `400 KB` (1024-based, as the app has always shown); the tests assert those exact strings. + +- [ ] **Step 4: Run the test** + +Run: `npx vitest run test/row-model.test.ts` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add src/renderer/src/components/queue/rowModel.ts test/row-model.test.ts +git commit -m "feat(queue): pure row model for result, status, actions and totals + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 6: Table sorting, select-all and table keyboard map + +**Files:** + +- Create: `src/renderer/src/components/queue/tableSort.ts`, `src/renderer/src/components/queue/selectAll.ts`, `src/renderer/src/components/queue/tableKeys.ts` +- Modify: `src/renderer/src/components/queueGroups.ts:9` (export `GROUP_ORDER`, add `groupLabel`) +- Test: `test/table-sort.test.ts`, `test/select-all.test.ts`, `test/table-keys.test.ts` + +**Interfaces:** + +- Consumes: `QueueItem`, `groupOf`, `inInput` from `state.ts`; `GROUP_ORDER` from `queueGroups.ts`. +- Produces: + - `type SortKey = 'name' | 'kind' | 'size' | 'result' | 'status'`; `interface SortState { key: SortKey; dir: 'asc' | 'desc' }`. + - `nextSort(cur: SortState | null, key: SortKey): SortState | null` (asc, desc, off). + - `sortItems(items: QueueItem[], sort: SortState | null): QueueItem[]` (stable, never mutates). + - `groupedRows(items: QueueItem[], sort: SortState | null): { group: string; label: string; items: QueueItem[] }[]` (groups in `GROUP_ORDER`, each sorted). + - `visibleOrder(groups): string[]`. + - `type CheckState = 'none' | 'mixed' | 'all'`; `activeGroupFor(items, selected): string | null`; `headerCheck(items, selected): CheckState`; `toggleAllIds(items, selected): string[]`. + - `export const GROUP_ORDER: string[]`; `groupLabel(group: string): string` from `queueGroups.ts`. + - `type TableKey = 'up' | 'down' | 'extendUp' | 'extendDown' | 'toggle' | 'selectAll' | 'remove' | 'open' | 'menu'`; `tableKey(e: KeyLike): TableKey | null` where `KeyLike = { key: string; ctrlKey: boolean; metaKey: boolean; shiftKey: boolean; altKey: boolean }`. + +- [ ] **Step 1: Write the failing tests** + +```ts +// test/table-sort.test.ts +import { describe, expect, it } from 'vitest' +import { + groupedRows, + nextSort, + sortItems, + visibleOrder +} from '../src/renderer/src/components/queue/tableSort' +import type { QueueItem } from '../src/renderer/src/state' + +const it_ = ( + id: string, + name: string, + ext: string, + size: number, + over: Partial = {} +): QueueItem => ({ + id, + file: { path: `C:/x/${name}`, name, ext, kind: ext === '.mp4' ? 'video' : 'image', size }, + thumb: null, + status: 'ready', + percent: 0, + ...over +}) + +describe('nextSort', () => { + it('cycles ascending, descending, off', () => { + const a = nextSort(null, 'size') + expect(a).toEqual({ key: 'size', dir: 'asc' }) + const b = nextSort(a, 'size') + expect(b).toEqual({ key: 'size', dir: 'desc' }) + expect(nextSort(b, 'size')).toBeNull() + }) + it('starts ascending when switching column', () => { + expect(nextSort({ key: 'size', dir: 'desc' }, 'name')).toEqual({ key: 'name', dir: 'asc' }) + }) +}) + +describe('sortItems', () => { + const rows = [ + it_('1', 'b.png', '.png', 30), + it_('2', 'a.png', '.png', 10), + it_('3', 'c.jpg', '.jpg', 20) + ] + it('keeps insertion order when off and never mutates', () => { + const copy = [...rows] + expect(sortItems(rows, null).map((r) => r.id)).toEqual(['1', '2', '3']) + sortItems(rows, { key: 'size', dir: 'asc' }) + expect(rows).toEqual(copy) + }) + it('sorts by name, size and kind', () => { + expect(sortItems(rows, { key: 'name', dir: 'asc' }).map((r) => r.id)).toEqual(['2', '1', '3']) + expect(sortItems(rows, { key: 'size', dir: 'desc' }).map((r) => r.id)).toEqual(['1', '3', '2']) + expect(sortItems(rows, { key: 'kind', dir: 'asc' }).map((r) => r.id)).toEqual(['3', '1', '2']) + }) + it('sorts by result size with empty results last', () => { + const r = [ + it_('1', 'a.png', '.png', 10), + it_('2', 'b.png', '.png', 10, { status: 'done', outputSize: 5 }), + it_('3', 'c.png', '.png', 10, { status: 'done', outputSize: 2 }) + ] + expect(sortItems(r, { key: 'result', dir: 'asc' }).map((x) => x.id)).toEqual(['3', '2', '1']) + expect(sortItems(r, { key: 'result', dir: 'desc' }).map((x) => x.id)).toEqual(['2', '3', '1']) + }) + it('sorts by status with failures first', () => { + const r = [ + it_('1', 'a.png', '.png', 1, { status: 'done' }), + it_('2', 'b.png', '.png', 1, { status: 'failed' }), + it_('3', 'c.png', '.png', 1, { status: 'running' }) + ] + expect(sortItems(r, { key: 'status', dir: 'asc' }).map((x) => x.id)).toEqual(['2', '3', '1']) + }) +}) + +describe('groupedRows', () => { + it('sorts within each group and keeps groups in display order', () => { + const g = groupedRows( + [it_('v', 'z.mp4', '.mp4', 99), it_('2', 'b.png', '.png', 2), it_('1', 'a.png', '.png', 1)], + { key: 'name', dir: 'asc' } + ) + expect(g.map((x) => x.group)).toEqual(['image', 'video']) + expect(visibleOrder(g)).toEqual(['1', '2', 'v']) + }) + it('skips hidden inputs and results', () => { + const g = groupedRows( + [ + it_('1', 'a.png', '.png', 1, { hiddenInput: true }), + it_('2', 'b.png', '.png', 1, { isResult: true }) + ], + null + ) + expect(g).toEqual([]) + }) +}) +``` + +```ts +// test/select-all.test.ts +import { describe, expect, it } from 'vitest' +import { + activeGroupFor, + headerCheck, + toggleAllIds +} from '../src/renderer/src/components/queue/selectAll' +import type { QueueItem } from '../src/renderer/src/state' + +const row = (id: string, ext: string): QueueItem => ({ + id, + file: { + path: `C:/x/${id}${ext}`, + name: id + ext, + ext, + kind: ext === '.mp4' ? 'video' : 'image', + size: 1 + }, + thumb: null, + status: 'ready', + percent: 0 +}) +const items = [row('v1', '.mp4'), row('i1', '.png'), row('i2', '.png'), row('v2', '.mp4')] + +describe('active group', () => { + it('is the group of the selection', () => { + expect(activeGroupFor(items, ['v2'])).toBe('video') + }) + it('falls back to the first group in display order, not insertion order', () => { + expect(activeGroupFor(items, [])).toBe('image') + }) + it('is null for an empty queue', () => { + expect(activeGroupFor([], [])).toBeNull() + }) +}) + +describe('header checkbox', () => { + it('reads none, mixed and all for the active group only', () => { + expect(headerCheck(items, [])).toBe('none') + expect(headerCheck(items, ['i1'])).toBe('mixed') + expect(headerCheck(items, ['i1', 'i2'])).toBe('all') + }) + it('selects every row of the selected group and nothing else', () => { + expect(toggleAllIds(items, ['v1'])).toEqual(['v1', 'v2']) + }) + it('selects the first display group when nothing is selected', () => { + expect(toggleAllIds(items, [])).toEqual(['i1', 'i2']) + }) + it('clears when the whole group is already selected', () => { + expect(toggleAllIds(items, ['i1', 'i2'])).toEqual([]) + }) +}) +``` + +```ts +// test/table-keys.test.ts +import { describe, expect, it } from 'vitest' +import { tableKey } from '../src/renderer/src/components/queue/tableKeys' + +const k = ( + key: string, + mods: Partial<{ ctrlKey: boolean; metaKey: boolean; shiftKey: boolean; altKey: boolean }> = {} +) => ({ + key, + ctrlKey: false, + metaKey: false, + shiftKey: false, + altKey: false, + ...mods +}) + +describe('tableKey', () => { + it('maps the spec 4.1 keys', () => { + expect(tableKey(k('ArrowUp'))).toBe('up') + expect(tableKey(k('ArrowDown'))).toBe('down') + expect(tableKey(k('ArrowUp', { shiftKey: true }))).toBe('extendUp') + expect(tableKey(k('ArrowDown', { shiftKey: true }))).toBe('extendDown') + expect(tableKey(k(' '))).toBe('toggle') + expect(tableKey(k('a', { ctrlKey: true }))).toBe('selectAll') + expect(tableKey(k('A', { metaKey: true }))).toBe('selectAll') + expect(tableKey(k('Delete'))).toBe('remove') + expect(tableKey(k('Enter'))).toBe('open') + expect(tableKey(k('ContextMenu'))).toBe('menu') + expect(tableKey(k('F10', { shiftKey: true }))).toBe('menu') + }) + it('ignores everything else, including Ctrl+Enter (that is Run)', () => { + expect(tableKey(k('Enter', { ctrlKey: true }))).toBeNull() + expect(tableKey(k('a'))).toBeNull() + expect(tableKey(k('ArrowUp', { altKey: true }))).toBeNull() + }) +}) +``` + +- [ ] **Step 2: Run them and watch them fail** + +Run: `npx vitest run test/table-sort.test.ts test/select-all.test.ts test/table-keys.test.ts` +Expected: FAIL, modules not found. + +- [ ] **Step 3: Export the group order and label** + +In `queueGroups.ts` change `const GROUP_ORDER` to `export const GROUP_ORDER` and add: + +```ts +/** "Images" / "Video" / "Documents"; "Other" for an unknown group. */ +export function groupLabel(group: string): string { + return GROUP_LABEL[group] ?? 'Other' +} + +/** Index of a group in display order; unknown groups sort last. */ +export function groupRank(group: string): number { + const i = GROUP_ORDER.indexOf(group) + return i < 0 ? GROUP_ORDER.length : i +} +``` + +- [ ] **Step 4: Implement the three modules** + +```ts +// src/renderer/src/components/queue/tableSort.ts +import { groupOf, inInput, type ItemStatus, type QueueItem } from '../../state' +import { groupLabel, groupRank } from '../queueGroups' + +export type SortKey = 'name' | 'kind' | 'size' | 'result' | 'status' +export interface SortState { + key: SortKey + dir: 'asc' | 'desc' +} + +/** Header click cycle: ascending, descending, back to insertion order. */ +export function nextSort(cur: SortState | null, key: SortKey): SortState | null { + if (!cur || cur.key !== key) return { key, dir: 'asc' } + return cur.dir === 'asc' ? { key, dir: 'desc' } : null +} + +const STATUS_RANK: Record = { + failed: 0, + running: 1, + queued: 2, + ready: 3, + canceled: 4, + done: 5 +} + +function compare(a: QueueItem, b: QueueItem, key: SortKey): number { + switch (key) { + case 'name': + return a.file.name.localeCompare(b.file.name, undefined, { + numeric: true, + sensitivity: 'base' + }) + case 'kind': + return a.file.ext.localeCompare(b.file.ext) + case 'size': + return a.file.size - b.file.size + case 'status': + return STATUS_RANK[a.status] - STATUS_RANK[b.status] + case 'result': + return (a.outputSize ?? 0) - (b.outputSize ?? 0) + } +} + +export function sortItems(items: QueueItem[], sort: SortState | null): QueueItem[] { + if (!sort) return items.slice() + const sign = sort.dir === 'asc' ? 1 : -1 + return items + .map((item, index) => ({ item, index })) + .sort((x, y) => { + if (sort.key === 'result') { + // Rows without a result always go last, whichever the direction. + const ex = x.item.outputSize == null + const ey = y.item.outputSize == null + if (ex !== ey) return ex ? 1 : -1 + } + return sign * compare(x.item, y.item, sort.key) || x.index - y.index + }) + .map((x) => x.item) +} + +export interface RowGroup { + group: string + label: string + items: QueueItem[] +} + +/** Input rows bucketed by convert group in display order, each bucket sorted. */ +export function groupedRows(items: QueueItem[], sort: SortState | null): RowGroup[] { + const by = new Map() + for (const i of items) { + if (!inInput(i)) continue + const g = groupOf(i.file) + const list = by.get(g) + if (list) list.push(i) + else by.set(g, [i]) + } + return [...by.keys()] + .sort((a, b) => groupRank(a) - groupRank(b)) + .map((g) => ({ group: g, label: groupLabel(g), items: sortItems(by.get(g)!, sort) })) +} + +export function visibleOrder(groups: RowGroup[]): string[] { + return groups.flatMap((g) => g.items.map((i) => i.id)) +} +``` + +```ts +// src/renderer/src/components/queue/selectAll.ts +import { groupOf, inInput, type QueueItem } from '../../state' +import { groupRank } from '../queueGroups' + +export type CheckState = 'none' | 'mixed' | 'all' + +/** The group the header checkbox speaks for: the selection's group, or the + * first group in display order when nothing is selected. */ +export function activeGroupFor(items: QueueItem[], selected: string[]): string | null { + const rows = items.filter(inInput) + const first = rows.find((i) => i.id === selected[0]) + if (first) return groupOf(first.file) + const groups = [...new Set(rows.map((i) => groupOf(i.file)))] + if (!groups.length) return null + return groups.sort((a, b) => groupRank(a) - groupRank(b))[0] +} + +function groupIds(items: QueueItem[], group: string | null): string[] { + return items.filter((i) => inInput(i) && groupOf(i.file) === group).map((i) => i.id) +} + +export function headerCheck(items: QueueItem[], selected: string[]): CheckState { + const ids = groupIds(items, activeGroupFor(items, selected)) + const n = ids.filter((id) => selected.includes(id)).length + if (n === 0) return 'none' + return n === ids.length ? 'all' : 'mixed' +} + +/** What a header click selects: the whole active group, or nothing when the + * group is already fully selected. Never crosses groups. */ +export function toggleAllIds(items: QueueItem[], selected: string[]): string[] { + const ids = groupIds(items, activeGroupFor(items, selected)) + return headerCheck(items, selected) === 'all' ? [] : ids +} +``` + +```ts +// src/renderer/src/components/queue/tableKeys.ts +export type TableKey = + 'up' | 'down' | 'extendUp' | 'extendDown' | 'toggle' | 'selectAll' | 'remove' | 'open' | 'menu' + +export interface KeyLike { + key: string + ctrlKey: boolean + metaKey: boolean + shiftKey: boolean + altKey: boolean +} + +/** Keyboard map for the files table (spec 4.1). Ctrl+Enter is left for Run. */ +export function tableKey(e: KeyLike): TableKey | null { + if (e.altKey) return null + const mod = e.ctrlKey || e.metaKey + if (mod) return e.key.toLowerCase() === 'a' && !e.shiftKey ? 'selectAll' : null + if (e.key === 'ArrowUp') return e.shiftKey ? 'extendUp' : 'up' + if (e.key === 'ArrowDown') return e.shiftKey ? 'extendDown' : 'down' + if (e.key === ' ') return 'toggle' + if (e.key === 'Delete') return 'remove' + if (e.key === 'Enter') return 'open' + if (e.key === 'ContextMenu' || (e.key === 'F10' && e.shiftKey)) return 'menu' + return null +} +``` + +- [ ] **Step 5: Run the tests** + +Run: `npx vitest run test/table-sort.test.ts test/select-all.test.ts test/table-keys.test.ts test/queue-groups.test.ts` +Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add src/renderer/src/components/queue/tableSort.ts src/renderer/src/components/queue/selectAll.ts src/renderer/src/components/queue/tableKeys.ts src/renderer/src/components/queueGroups.ts test/table-sort.test.ts test/select-all.test.ts test/table-keys.test.ts +git commit -m "feat(queue): sort cycle, group-scoped select-all and table key map + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 7: Size estimator (per file and batch) + +**Files:** + +- Create: `src/shared/sizeEstimate.ts` +- Test: `test/size-estimate.test.ts` + +**Interfaces:** + +- Consumes: `FileInfo`, `JobOptions`, `ToolId` from `@shared/types`; `estimatedPngBytes` from `@shared/compress`. +- Produces: + - `interface Sample { source: number; output: number }` + - `medianRatio(samples: Sample[]): number | null` + - `ratioFor(tool: ToolId, file: FileInfo, options: JobOptions): number | null` + - `interface EstimateCtx { samples?: Sample[]; pixelRatio?: number | null; outPixels?: number | null }` + - `estimateOutputBytes(file: FileInfo, tool: ToolId, options: JobOptions, ctx?: EstimateCtx): number | null` + - `estimateBatch(rows: { size: number; estimate: number | null }[]): { from: number; to: number; files: number } | null` + +Order of evidence (spec 6.2): batch samples first, then pixels (resize, upscale), then the ratio table, else `null`. + +- [ ] **Step 1: Write the failing test** + +```ts +// test/size-estimate.test.ts +import { describe, expect, it } from 'vitest' +import { estimateBatch, estimateOutputBytes, medianRatio, ratioFor } from '@shared/sizeEstimate' +import type { FileInfo } from '@shared/types' + +const png: FileInfo = { + path: 'C:/a.png', + name: 'a.png', + ext: '.png', + kind: 'image', + size: 1_000_000 +} +const mp4: FileInfo = { + path: 'C:/a.mp4', + name: 'a.mp4', + ext: '.mp4', + kind: 'video', + size: 10_000_000 +} +const wav: FileInfo = { + path: 'C:/a.wav', + name: 'a.wav', + ext: '.wav', + kind: 'audio', + size: 5_000_000 +} +const pdf: FileInfo = { path: 'C:/a.pdf', name: 'a.pdf', ext: '.pdf', kind: 'pdf', size: 2_000_000 } + +describe('medianRatio', () => { + it('takes the median output/source ratio', () => { + expect( + medianRatio([ + { source: 10, output: 1 }, + { source: 10, output: 3 }, + { source: 10, output: 9 } + ]) + ).toBeCloseTo(0.3) + expect( + medianRatio([ + { source: 10, output: 2 }, + { source: 10, output: 4 } + ]) + ).toBeCloseTo(0.3) + }) + it('ignores zero-byte sources and returns null with nothing usable', () => { + expect(medianRatio([])).toBeNull() + expect(medianRatio([{ source: 0, output: 5 }])).toBeNull() + }) +}) + +describe('ratio table', () => { + it('knows image convert targets by quality preset', () => { + expect(ratioFor('convert', png, { format: '.webp', quality: 'balanced' })).toBeCloseTo(0.22) + expect(ratioFor('convert', png, { format: '.avif', quality: 'smaller' })).toBeCloseTo(0.1) + }) + it('has no ratio for lossless or odd targets', () => { + expect(ratioFor('convert', png, { format: '.bmp' })).toBeNull() + expect(ratioFor('convert', mp4, { format: '.mkv' })).toBeNull() + }) + it('scales video compress by codec, quality and scale', () => { + const full = ratioFor('compress', mp4, { videoCodec: 'h264', quality: 80, scale: 100 })! + const half = ratioFor('compress', mp4, { videoCodec: 'h264', quality: 80, scale: 50 })! + expect(full).toBeCloseTo(0.5) + expect(half).toBeCloseTo(0.125) + }) + it('has no ratio for audio without a known source bitrate', () => { + expect(ratioFor('compress', wav, { audioCodec: 'mp3', audioBitrate: 192 })).toBeNull() + }) + it('maps PDF levels', () => { + expect(ratioFor('compress', pdf, { pdfLevel: 'smallest' })).toBeCloseTo(0.3) + }) + it('never estimates tools whose output is not comparable', () => { + for (const t of ['pdf', 'archive', 'generate', 'removebg'] as const) + expect(ratioFor(t, png, {})).toBeNull() + }) +}) + +describe('estimateOutputBytes', () => { + it('prefers finished rows of the same batch over the table', () => { + const v = estimateOutputBytes( + png, + 'convert', + { format: '.webp', quality: 'balanced' }, + { + samples: [{ source: 100, output: 50 }] + } + ) + expect(v).toBe(500_000) + }) + it('uses pixels for resize and upscale', () => { + expect( + estimateOutputBytes(png, 'resize', { mode: 'percent', percent: 50 }, { pixelRatio: 0.25 }) + ).toBe(250_000) + expect(estimateOutputBytes(png, 'upscale', { upscaleFactor: 2 }, { outPixels: 1000 })).toBe( + 1200 + ) + }) + it('returns null, never 0 or NaN, when there is nothing to go on', () => { + expect(estimateOutputBytes(png, 'resize', {}, {})).toBeNull() + expect(estimateOutputBytes({ ...png, size: 0 }, 'convert', { format: '.webp' })).toBeNull() + expect(estimateOutputBytes(wav, 'compress', { audioCodec: 'mp3' })).toBeNull() + }) +}) + +describe('estimateBatch', () => { + it('sums only rows that have an estimate and counts them', () => { + expect( + estimateBatch([ + { size: 100, estimate: 20 }, + { size: 50, estimate: null }, + { size: 300, estimate: 60 } + ]) + ).toEqual({ from: 400, to: 80, files: 2 }) + }) + it('is null when no row can be estimated', () => { + expect(estimateBatch([{ size: 100, estimate: null }])).toBeNull() + expect(estimateBatch([])).toBeNull() + }) +}) +``` + +- [ ] **Step 2: Run it and watch it fail** + +Run: `npx vitest run test/size-estimate.test.ts` +Expected: FAIL, module not found. + +- [ ] **Step 3: Implement** + +```ts +// src/shared/sizeEstimate.ts +import type { FileInfo, JobOptions, ToolId } from './types' +import { estimatedPngBytes } from './compress' + +// Output-size estimates for the result column and the batch card (spec 6.2). +// Always shown with "~". Conservative by design: a wrong guess is worse than an +// empty cell, so anything unpredictable returns null. + +export interface Sample { + source: number + output: number +} + +export function medianRatio(samples: Sample[]): number | null { + const r = samples + .filter((s) => s.source > 0 && s.output >= 0) + .map((s) => s.output / s.source) + .sort((a, b) => a - b) + if (!r.length) return null + const mid = Math.floor(r.length / 2) + return r.length % 2 ? r[mid] : (r[mid - 1] + r[mid]) / 2 +} + +type Preset = 'smaller' | 'balanced' | 'best' +const IMAGE_CONVERT: Record> = { + '.webp': { smaller: 0.15, balanced: 0.22, best: 0.35 }, + '.avif': { smaller: 0.1, balanced: 0.16, best: 0.25 }, + '.jxl': { smaller: 0.15, balanced: 0.22, best: 0.35 }, + '.jpg': { smaller: 0.25, balanced: 0.35, best: 0.5 }, + '.jpeg': { smaller: 0.25, balanced: 0.35, best: 0.5 } +} +const VIDEO_CODEC: Record = { h264: 0.5, h265: 0.35, av1: 0.3 } +const PDF_LEVEL: Record = { lossless: 0.9, high: 0.7, balanced: 0.5, smallest: 0.3 } + +const num = (v: unknown, d: number): number => (typeof v === 'number' && Number.isFinite(v) ? v : d) +const clamp01 = (n: number): number => Math.max(0.01, Math.min(1, n)) + +export function ratioFor(tool: ToolId, file: FileInfo, options: JobOptions): number | null { + if (tool === 'convert') { + if (file.kind !== 'image') return null + const row = IMAGE_CONVERT[String(options.format ?? '')] + if (!row) return null + const q = String(options.quality ?? 'balanced') as Preset + return row[q] ?? row.balanced + } + if (tool === 'compress') { + const q = num(options.quality, 80) + if (file.kind === 'image') { + const fmt = String(options.imageFormat ?? 'keep') + if (fmt === 'webp') return clamp01(0.08 + (q / 100) * 0.3) + if (fmt === 'avif') return clamp01(0.05 + (q / 100) * 0.2) + return clamp01((q / 100) * 0.6) + } + if (file.kind === 'video') { + const base = VIDEO_CODEC[String(options.videoCodec ?? 'h264')] + if (base == null) return null + const s = num(options.scale, 100) / 100 + return clamp01(base * (q / 80) * s * s) + } + if (file.kind === 'pdf') return PDF_LEVEL[String(options.pdfLevel ?? 'balanced')] ?? null + return null // audio: needs the source bitrate, which is not probed + } + return null +} + +export interface EstimateCtx { + samples?: Sample[] + /** Output pixels / source pixels (resize), from the dimension probe. */ + pixelRatio?: number | null + /** Output pixel count (upscale writes PNG). */ + outPixels?: number | null +} + +export function estimateOutputBytes( + file: FileInfo, + tool: ToolId, + options: JobOptions, + ctx: EstimateCtx = {} +): number | null { + if (!(file.size > 0)) return null + const fromBatch = medianRatio(ctx.samples ?? []) + if (fromBatch != null) return Math.round(file.size * fromBatch) + if (tool === 'resize') + return ctx.pixelRatio != null && ctx.pixelRatio > 0 + ? Math.round(file.size * ctx.pixelRatio) + : null + if (tool === 'upscale') + return ctx.outPixels != null && ctx.outPixels > 0 + ? Math.round(estimatedPngBytes(ctx.outPixels, 1)) + : null + const r = ratioFor(tool, file, options) + return r == null ? null : Math.round(file.size * r) +} + +export function estimateBatch( + rows: { size: number; estimate: number | null }[] +): { from: number; to: number; files: number } | null { + let from = 0 + let to = 0 + let files = 0 + for (const r of rows) { + if (r.estimate == null) continue + from += r.size + to += r.estimate + files += 1 + } + return files ? { from, to, files } : null +} +``` + +`estimatedPngBytes(w, h)` is `w * h * 1.2` (`src/shared/compress.ts:118`); passing `(outPixels, 1)` reuses it unchanged. + +- [ ] **Step 4: Run the test** + +Run: `npx vitest run test/size-estimate.test.ts` +Expected: PASS. + +- [ ] **Step 5: Commit** + +```bash +git add src/shared/sizeEstimate.ts test/size-estimate.test.ts +git commit -m "feat(shared): conservative output-size estimator with batch extrapolation + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 8: Design tokens, bundled fonts, base and workbench CSS (light theme removed) + +**Files:** + +- Create: `src/renderer/src/theme/tokens.css`, `src/renderer/src/theme/base.css`, `src/renderer/src/theme/workbench.css` +- Modify: `src/renderer/src/index.css` (rewrite), `src/renderer/src/main.tsx:1-5` (font imports), `package.json` (dependencies) +- Test: `test/theme-tokens.test.ts` + +**Interfaces:** + +- Consumes: nothing. +- Produces: the CSS custom properties listed in Global Constraints plus `--sans`, `--mono`, `--backdrop`, `--sbw: 208px`, `--sbw-c: 48px`, `--cols: 40px 1fr 64px 88px 184px 232px`; Tailwind utilities `bg-bg-0 bg-bg-1 bg-hover bg-selected bg-field bg-track border-line border-line-strong text-fg1 text-fg2 text-fg3 text-fg-disabled bg-inv-bg text-inv-fg font-sans font-mono`; the class vocabulary every later task uses: `app wb no-insp titlebar brand tsep crumb winctl side side-head lbl toggle nav nav-sep spacer bottom item lab n center toolbar tbtn add ibtn tb-right count qtable qbody cols thead th tr td ck num sel box mixed on name thumb meta size res arr split est pct grew rc st done run q canceled fail prog eta row-act ghost fill totals insp tabs tab tl ibody ihead vs vs-gh vs-set vs-t vs-d vs-sel half vs-in vs-row sbtn seg vs-chk lt vs-est eh ev from dgrid ifoot primary statusbar sitem mini sright warn wipe line tag l r mono sr-only scroll-thin`. + +Mockup class names that collide with Tailwind utilities are renamed: `.table` becomes `.qtable`, `.grid` becomes `.dgrid`. Breadcrumb links become ` + ) +} + +export function IconButton({ + icon, + label, + ...rest +}: B & { icon: IconName; label: string }): JSX.Element { + return ( + + ) +} + +export function SmallButton({ + icon, + children, + ...rest +}: B & { icon?: IconName; children: ReactNode }): JSX.Element { + return ( + + ) +} + +export function RowAction({ + icon, + label, + ghost, + ...rest +}: B & { icon: IconName; label: string; ghost?: boolean }): JSX.Element { + return ( + + ) +} + +export function PrimaryButton({ + icon = 'play', + children, + ...rest +}: B & { icon?: IconName; children: ReactNode }): JSX.Element { + return ( + + ) +} +``` + +```tsx +// src/renderer/src/components/ui/Checkbox.tsx +import type { JSX } from 'react' +import { Icon } from '../icons/Icon' + +export function Checkbox({ + checked, + onChange, + label, + size = 'sm', + focusable = false +}: { + checked: boolean | 'mixed' + onChange: () => void + label: string + size?: 'sm' | 'md' + /** Table rows take focus themselves, so row boxes are tabindex -1 (spec 4.1). */ + focusable?: boolean +}): JSX.Element { + const on = checked === true + return ( + { + e.stopPropagation() + e.preventDefault() + onChange() + }} + onKeyDown={(e) => { + if (e.key === ' ' || e.key === 'Enter') { + e.preventDefault() + e.stopPropagation() + onChange() + } + }} + > + {on && } + + ) +} +``` + +```tsx +// src/renderer/src/components/ui/Segmented.tsx +import { useRef, type JSX } from 'react' +import { rovingIndex } from './roving' + +export function Segmented({ + value, + options, + onChange, + label +}: { + value: T + options: { value: T; label: string }[] + onChange: (v: T) => void + label: string +}): JSX.Element { + const refs = useRef<(HTMLButtonElement | null)[]>([]) + const idx = options.findIndex((o) => o.value === value) + return ( +
+ {options.map((o, i) => ( + + ))} +
+ ) +} +``` + +```tsx +// src/renderer/src/components/ui/Select.tsx +import { useEffect, useId, useRef, useState, type JSX, type KeyboardEvent } from 'react' +import { Icon } from '../icons/Icon' +import { nextEnabled, type SelectOption } from './selectNav' + +// A custom listbox (spec 2.7): Chromium draws a native +} + +/** Commit-on-blur/Enter number input with optional clamping (replaces DimInput). + * The draft exists only while editing, so no effect has to sync it. */ +export function NumberField({ + value, + onCommit, + label, + placeholder, + clamp +}: { + value: number | '' + onCommit: (v: number | '') => void + label: string + placeholder?: string + clamp?: (n: number) => number +}): JSX.Element { + const [draft, setDraft] = useState(null) + function commit(): void { + if (draft == null) return + const t = draft.trim() + if (t === '') onCommit('') + else { + const n = Number(t) + if (Number.isFinite(n)) onCommit(clamp ? clamp(n) : n) + } + setDraft(null) + } + return ( + setDraft(e.target.value)} + onBlur={commit} + onKeyDown={(e) => { + if (e.key === 'Enter') commit() + if (e.key === 'Escape') setDraft(null) + }} + /> + ) +} +``` + +```tsx +// src/renderer/src/components/ui/Setting.tsx +import type { JSX, ReactNode } from 'react' +import { Icon } from '../icons/Icon' +import { Checkbox } from './Checkbox' + +export function SettingGroup({ + title, + collapsible, + open = true, + onToggle, + children +}: { + title: string + collapsible?: boolean + open?: boolean + onToggle?: () => void + children: ReactNode +}): JSX.Element { + return ( +
+

+ {collapsible ? ( + + ) : ( + title + )} +

+ {open && children} +
+ ) +} + +export function Setting({ + title, + desc, + warn, + children +}: { + title: string + desc?: ReactNode + /** Prefix the description with the warning glyph (monochrome warning). */ + warn?: boolean + children?: ReactNode +}): JSX.Element { + return ( +
+
{title}
+ {desc != null && ( +
+ {warn && } + {desc} +
+ )} + {children} +
+ ) +} + +export function CheckSetting({ + checked, + onChange, + label, + sub +}: { + checked: boolean + onChange: (v: boolean) => void + label: string + sub?: string +}): JSX.Element { + return ( +
onChange(!checked)}> + onChange(!checked)} + label={label} + size="md" + focusable + /> + + {label} + {sub && {sub}} + +
+ ) +} +``` + +```tsx +// src/renderer/src/components/ui/RangeField.tsx +import type { CSSProperties, JSX } from 'react' + +export function RangeField({ + label, + value, + min, + max, + step = 1, + onChange, + format = String, + ends +}: { + label: string + value: number + min: number + max: number + step?: number + onChange: (v: number) => void + format?: (v: number) => string + ends?: [string, string] +}): JSX.Element { + const fill = max > min ? ((value - min) / (max - min)) * 100 : 0 + return ( +
+
+ onChange(Number(e.target.value))} + style={{ '--fill': `${fill}%` } as CSSProperties} + /> + {format(value)} +
+ {ends && ( +
+ {ends[0]} + {ends[1]} +
+ )} +
+ ) +} +``` + +```tsx +// src/renderer/src/components/ui/ChipGrid.tsx +import { useRef, type JSX } from 'react' +import { rovingIndex } from './roving' + +export interface Chip { + value: T + label: string + disabled?: boolean + title?: string +} + +export function ChipGrid({ + value, + chips, + onChange, + label, + cols = 4 +}: { + value: T | null + chips: Chip[] + onChange: (v: T) => void + label: string + cols?: number +}): JSX.Element { + const refs = useRef<(HTMLButtonElement | null)[]>([]) + const idx = chips.findIndex((c) => c.value === value && !c.disabled) + const focusIdx = + idx >= 0 + ? idx + : Math.max( + 0, + chips.findIndex((c) => !c.disabled) + ) + return ( +
+ {chips.map((c, i) => ( + + ))} +
+ ) +} +``` + +```tsx +// src/renderer/src/components/ui/ProgressBar.tsx +import type { JSX } from 'react' + +/** 2px track with an fg1 fill; a null value is an indeterminate sweep (static + * under reduced motion). Replaces the three copies in the tool setup cards. */ +export function ProgressBar({ + value, + label, + wide +}: { + value?: number | null + label: string + wide?: boolean +}): JSX.Element { + const ind = value == null + return ( +
+ +
+ ) +} +``` + +```tsx +// src/renderer/src/components/ui/Tabs.tsx +import { useRef, type JSX } from 'react' +import type { IconName } from '@shared/icons' +import { Icon } from '../icons/Icon' +import { rovingIndex } from './roving' + +export interface TabDef { + id: T + label: string + icon: IconName +} + +export function Tabs({ + tabs, + value, + onChange, + label, + idPrefix +}: { + tabs: TabDef[] + value: T + onChange: (v: T) => void + label: string + idPrefix: string +}): JSX.Element { + const refs = useRef<(HTMLButtonElement | null)[]>([]) + return ( +
+ {tabs.map((t, i) => ( + + ))} +
+ ) +} +``` + +```tsx +// src/renderer/src/components/ui/EstimateCard.tsx +import type { JSX } from 'react' +import { formatBytes } from '@shared/compress' +import { Icon } from '../icons/Icon' +import { formatPct, pctChange } from '../queue/rowModel' + +export function EstimateCard({ + from, + to, + files +}: { + from: number + to: number + files: number +}): JSX.Element { + const pct = pctChange(from, to) + const noun = `${files} file${files === 1 ? '' : 's'}` + const label = `Estimated output for ${noun}: ${formatBytes(from)} to about ${formatBytes(to)}${ + pct != null ? `, ${formatPct(pct)}` : '' + }` + return ( +
+
+ estimate + {noun} +
+
+ {formatBytes(from)} + + ~{formatBytes(to)} + {pct != null && {formatPct(pct)}} +
+
+ ) +} +``` + +```tsx +// src/renderer/src/components/ui/OutputSizeList.tsx +import type { JSX } from 'react' + +export type SizeRow = { name: string; from: string; to: string } + +/** Input to output resolution list under the video scale, resize and upscale + * settings (replaces three identical copies). */ +export function OutputSizeList({ rows }: { rows: SizeRow[] }): JSX.Element | null { + if (!rows.length) return null + return ( +
    + {rows.map((r, i) => ( +
  • + {r.name} + + {r.from} to {r.to} + +
  • + ))} +
+ ) +} +``` + +- [ ] **Step 6: Write `theme/controls.css`** + +```css +/* Controls the mockup does not draw, in its language (spec 2.5 and 2.7). */ + +/* disabled: fg-disabled text, line border, no hover change */ +.sbtn:disabled, +.vs-sel:disabled, +.ibtn:disabled, +.row-act:disabled, +.chip[aria-disabled='true'] { + color: var(--fg-disabled); + border-color: var(--line); + background: transparent; +} +.sbtn:disabled svg, +.vs-sel:disabled svg { + color: var(--fg-disabled); +} +.primary:disabled { + background: var(--selected); + color: var(--fg-disabled); +} + +/* select popup */ +.vs-selwrap { + position: relative; + width: 100%; +} +.vs-selwrap.half { + width: 168px; +} +.vs-row .vs-selwrap { + flex: 1; + width: auto; +} +.vs-sel .ph { + color: var(--fg3); +} +.vs-pop { + position: absolute; + z-index: 30; + left: 0; + right: 0; + top: 100%; + max-height: 280px; + overflow-y: auto; + margin: 0; + padding: 0; + list-style: none; + background: var(--bg-0); + border: 1px solid var(--line-strong); + border-top: 0; +} +.vs-pop li[role='option'] { + height: 28px; + display: flex; + align-items: center; + gap: 8px; + padding: 0 8px; + font: 13px var(--mono); + color: var(--fg2); + cursor: pointer; +} +.vs-pop li .val { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.vs-pop li.act { + background: var(--hover); + color: var(--fg1); +} +.vs-pop li.cur { + color: var(--fg1); + font-weight: 600; +} +.vs-pop li.dis { + color: var(--fg-disabled); + cursor: default; +} +.vs-pop-gh { + padding: 10px 8px 4px; + font: 600 11px/1 var(--mono); + letter-spacing: 0.09em; + color: var(--fg3); +} + +/* group header that collapses (Generate > ADVANCED) */ +.vs-ghb { + display: inline-flex; + align-items: center; + gap: 6px; + font: inherit; + letter-spacing: inherit; + color: inherit; +} +.vs-ghb:hover { + color: var(--fg1); +} +.vs-d.warn { + display: flex; + gap: 6px; + align-items: flex-start; + color: var(--fg2); +} +.vs-d.warn svg { + margin-top: 2px; +} + +/* chip grid: square outlined cells, the selected one inverted */ +.chips { + display: grid; + border-top: 1px solid var(--line-strong); + border-left: 1px solid var(--line-strong); +} +.chip { + height: 28px; + display: grid; + place-items: center; + font: 13px/1 var(--mono); + color: var(--fg2); + border-right: 1px solid var(--line-strong); + border-bottom: 1px solid var(--line-strong); + transition: + background-color 0.1s, + color 0.1s; +} +.chip:hover:not([aria-disabled='true']) { + background: var(--hover); + color: var(--fg1); +} +.chip:active:not([aria-disabled='true']) { + background: var(--track); +} +.chip.on { + background: var(--inv-bg); + color: var(--inv-fg); + font-weight: 600; +} +.chip.on:hover { + background: var(--inv-hover); +} +.chip:focus-visible { + outline-offset: -3px; + position: relative; + z-index: 1; +} + +/* range: 2px track, fg1 fill, square 10px thumb */ +.rng-row { + display: flex; + align-items: center; + gap: 10px; +} +.rng output { + min-width: 52px; + text-align: right; + font-size: 13px; + color: var(--fg1); +} +.rng input[type='range'] { + flex: 1; + height: 16px; + margin: 0; + background: transparent; + appearance: none; + cursor: pointer; +} +.rng input[type='range']::-webkit-slider-runnable-track { + height: 2px; + background: linear-gradient(to right, var(--fg1) var(--fill), var(--track) var(--fill)); +} +.rng input[type='range']::-webkit-slider-thumb { + appearance: none; + width: 10px; + height: 10px; + margin-top: -4px; + background: var(--fg1); + border: 0; +} +.rng input[type='range']:hover::-webkit-slider-thumb { + background: var(--inv-hover); +} +.rng input[type='range']:focus-visible { + outline-offset: 2px; +} +.rng-ends { + display: flex; + justify-content: space-between; + font-size: 11px; + color: var(--fg3); + margin-top: 2px; +} + +/* progress: determinate fill or an indeterminate sweep */ +.mini.wide { + width: 100%; +} +.mini.ind i { + width: 30%; + animation: fs-sweep 1.2s linear infinite; +} +@keyframes fs-sweep { + from { + left: -30%; + } + to { + left: 100%; + } +} +@media (prefers-reduced-motion: reduce) { + .mini.ind i { + animation: none; + left: 0; + } + .chip { + transition: none; + } +} + +/* output size list */ +.sizes { + margin: 4px 0 0; + padding: 0; + list-style: none; + font-size: 12px; + color: var(--fg2); +} +.sizes li { + display: flex; + justify-content: space-between; + gap: 12px; + padding: 2px 0; +} +.sizes .nm { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--fg3); +} +``` + +Add `@import './theme/controls.css';` after the `workbench.css` import in `index.css`. + +- [ ] **Step 7: Verify** + +Run: `npm test && npm run typecheck && npm run lint` +Expected: all green (the components are not mounted yet; Tasks 11 to 15 mount them). + +- [ ] **Step 8: Commit** + +```bash +git add src/renderer/src/components/ui src/renderer/src/theme/controls.css src/renderer/src/index.css test/ui-nav.test.ts +git commit -m "feat(ui): monochrome primitives (buttons, select, segmented, chips, range, tabs, settings) + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 11: App shell: title bar and breadcrumb, collapsible sidebar, status bar, global shortcuts + +**Files:** + +- Create in `src/renderer/src/components/shell/`: `crumbs.ts`, `sidebarState.ts`, `useSidebar.ts`, `railPrefs.ts`, `useRailPrefs.ts`, `statusModel.ts`, `shortcuts.ts`, `TitleBar.tsx`, `Breadcrumb.tsx`, `Sidebar.tsx`, `StatusBar.tsx` +- Modify: `src/renderer/src/components/queue/selectAll.ts` (add `oneGroupIds`) +- Modify: `src/renderer/src/App.tsx` (root layout `:852-1022`, `run()` `:661-729`, new state and effects) +- Delete: `src/renderer/src/components/TopBar.tsx`, `src/renderer/src/components/TabRail.tsx`, `src/renderer/src/assets/fmark.png` (only if `grep -rn fmark src` shows no other user) +- Test: `test/shell-model.test.ts`, `test/select-all.test.ts` (extend) + +**Interfaces:** + +- Consumes: `Icon`, `ProgressBar` (Tasks 9, 10); `TABS`, `COMPLETED_TAB`, `SETTINGS_TAB`, `tabById`, `type Tab`, `type TabId`, `type ToolCard` from `@shared/tabs`; `groupLabel` (Task 6); `inInput`, `groupOf` from `state.ts`; `activeGroupFor` (Task 6). +- Produces: + - `type CrumbAction = 'tools' | 'clearSelection'`; `interface Crumb { label: string; action?: CrumbAction; ariaLabel?: string }`; `crumbsFor(tab: TabId, card: ToolCard | null, group: string | null): Crumb[]` + - `type SidebarPref = 'expanded' | 'collapsed'`; `NARROW_PX = 1280`; `isCollapsed(pref, narrow, override: boolean | null): boolean`; `afterToggle(pref, narrow, override): { pref: SidebarPref; override: boolean | null }`; hook `useSidebar(): { collapsed: boolean; toggle: () => void }` + - `normalizeOrder(saved: unknown, all: TabId[]): TabId[]`; `moveItem(list: T[], from: number, to: number): T[]`; `sidebarVerbs(order: TabId[], hidden: TabId[]): TabId[]` (excludes `tools` and hidden); hook `useRailPrefs(): { order: TabId[]; hidden: TabId[]; setOrder(o: TabId[]): void; toggleHidden(id: TabId): void }` + - `interface StatusSummary { running: { label: string; pct: number } | null; message: string | null; done: string | null; failed: number }`; `verbGerund(label: string): string`; `statusSummary(items: QueueItem[], batch: string[] | null, verb: string, message?: string | null): StatusSummary` + - `type Shortcut = 'toggleSidebar' | 'addFiles' | 'run'`; `shortcutFor(e: KeyLike): Shortcut | null` (`KeyLike` from `queue/tableKeys.ts`) + - `oneGroupIds(items: QueueItem[], ids: string[]): string[]` (keeps only ids in the display-first group among them) + - Components `TitleBar({ crumbs, onCrumb })`, `Sidebar({ tab, verbs, showTools, counts, completedCount, collapsed, onToggle, onSelect })`, `StatusBar({ summary, onFailedClick })` + - The sidebar is ` + ) +} +``` + +```tsx +// src/renderer/src/components/shell/TitleBar.tsx +import type { JSX } from 'react' +import { Icon } from '../icons/Icon' +import { Breadcrumb } from './Breadcrumb' +import type { Crumb, CrumbAction } from './crumbs' + +export function TitleBar({ + crumbs, + onCrumb +}: { + crumbs: Crumb[] + onCrumb: (a: CrumbAction) => void +}): JSX.Element { + return ( +
+
+ + Filesmith +
+
+ + {/* Window controls: 10x10 glyphs at stroke 1; close is not red (spec 2.5). */} +
+ + + +
+
+ ) +} +``` + +```tsx +// src/renderer/src/components/shell/Sidebar.tsx +import type { JSX } from 'react' +import { COMPLETED_TAB, SETTINGS_TAB, tabById, type Tab, type TabId } from '@shared/tabs' +import { Icon } from '../icons/Icon' + +export function Sidebar({ + tab, + verbs, + showTools, + counts, + completedCount, + collapsed, + onToggle, + onSelect +}: { + tab: TabId + verbs: TabId[] + showTools: boolean + counts: Record + completedCount: number + collapsed: boolean + onToggle: () => void + onSelect: (t: TabId) => void +}): JSX.Element { + const item = (t: Tab, n?: number): JSX.Element => ( + + ) + return ( + + ) +} +``` + +```tsx +// src/renderer/src/components/shell/StatusBar.tsx +import type { JSX } from 'react' +import { Icon } from '../icons/Icon' +import { ProgressBar } from '../ui/ProgressBar' +import type { StatusSummary } from './statusModel' + +export function StatusBar({ + summary, + onFailedClick +}: { + summary: StatusSummary + onFailedClick: () => void +}): JSX.Element { + return ( +
+ {summary.running && ( +
+ + {summary.running.label} + +
+ )} + {summary.message && ( +
+ + {summary.message} + +
+ )} + {summary.done &&
{summary.done}
} +
+ {summary.failed > 0 && ( + + )} +
+
+ ) +} +``` + +- [ ] **Step 6: Wire the shell into `App.tsx`** + +Imports: remove `TopBar` and `TabRail`; add + +```ts +import { TitleBar } from './components/shell/TitleBar' +import { Sidebar } from './components/shell/Sidebar' +import { StatusBar } from './components/shell/StatusBar' +import { crumbsFor } from './components/shell/crumbs' +import { useSidebar } from './components/shell/useSidebar' +import { useRailPrefs } from './components/shell/useRailPrefs' +import { sidebarVerbs } from './components/shell/railPrefs' +import { statusSummary } from './components/shell/statusModel' +import { shortcutFor } from './components/shell/shortcuts' +import { oneGroupIds } from './components/queue/selectAll' +``` + +New state next to the other `useState`s: + +```ts +const sidebar = useSidebar() +const rail = useRailPrefs() +// Ids of the last run per workspace, for "Converting 3 of 6" (spec 6.4). +const [batches, setBatches] = useState>({}) +``` + +In `run()`: after the merge branch's `dispatch({ type: 'markQueued', ids: [anchorId], ... })` add `setBatches((b) => ({ ...b, [qKey]: [anchorId] }))`; after the per-file `dispatch({ type: 'markQueued', ids: targets.map((t) => t.id), ... })` add `setBatches((b) => ({ ...b, [qKey]: targets.map((t) => t.id) }))`. + +Global shortcuts effect (after the job-event effect). `run` and `browse` are re-created each render, so read them through the existing `latest` ref pattern: add a ref `const actions = useRef({ run, browse, runCount, toggle: sidebar.toggle })` and assign `actions.current = { run, browse, runCount, toggle: sidebar.toggle }` in the render body after `runCount` is computed, then: + +```ts +useEffect(() => { + const onKey = (e: KeyboardEvent): void => { + const s = shortcutFor(e) + if (!s) return + e.preventDefault() + const a = actions.current + if (s === 'toggleSidebar') a.toggle() + else if (s === 'addFiles') void a.browse() + else if (s === 'run' && a.runCount > 0) void a.run() + } + window.addEventListener('keydown', onKey) + return () => window.removeEventListener('keydown', onKey) +}, []) +``` + +Derived values before `return`: + +```ts +const crumbs = crumbsFor(state.tab, card ?? null, activeGroup) +const verbLabel = card ? card.label : tab.label +const summary = statusSummary( + onToolsGrid || onCompleted || state.tab === 'settings' ? [] : cur.items, + batches[qKey] ?? null, + verbLabel, + genRun.running && genRun.message && !genRun.message.startsWith('Generating') + ? genRun.message + : null +) +const showInspector = !onToolsGrid && !onCompleted && state.tab !== 'settings' +``` + +Replace the outer layout. The root becomes: + +```tsx +
+ + a === 'tools' + ? dispatch({ type: 'setActiveTool', tool: null }) + : dispatch({ type: 'clearSelection' }) + } + /> +
+ dispatch({ type: 'setTab', tab: t })} + /> + {/* existing
...
stays for now; change its tag to +
and drop its Tailwind classes */} + {/* existing OptionsPanel stays for now, rendered when showInspector */} +
+ + dispatch({ + type: 'selectIds', + ids: oneGroupIds( + cur.items, + cur.items.filter((i) => i.status === 'failed').map((i) => i.id) + ) + }) + } + /> + + +
+``` + +Move the `onDragOver` / `onDragLeave` / `onDrop` handlers from the old `
` onto the `.app` root (spec 4.9: the whole window accepts drops). Keep the existing window-level `dragover`/`drop` `preventDefault` effect. + +- [ ] **Step 7: Delete the replaced components** + +```bash +git rm src/renderer/src/components/TopBar.tsx src/renderer/src/components/TabRail.tsx +grep -rn "fmark" src || git rm src/renderer/src/assets/fmark.png +``` + +(The second line removes the old logo only when nothing else references it; `build/icon.ico` and the installer icon are untouched, spec section 8.) + +- [ ] **Step 8: Verify** + +Run: `npm test && npm run typecheck && npm run lint` +Expected: all green. + +Run: `npm run dev` and check by hand: title bar shows the anvil, `Filesmith`, the breadcrumb and three square window buttons (close turns light grey on hover, never red; minimize, maximize and close work); the sidebar lists six verbs, a separator, Tools, then Completed and Settings at the bottom; the toggle and Ctrl+B collapse it to 48px with labels faded, the state survives a restart; narrowing the window under 1280px collapses it without changing the stored state; dropping a file anywhere in the window adds it. + +- [ ] **Step 9: Commit** + +```bash +git add -A src/renderer/src/components/shell src/renderer/src/components/queue/selectAll.ts src/renderer/src/App.tsx test/shell-model.test.ts test/select-all.test.ts +git commit -m "feat(shell): title bar with breadcrumb, collapsible sidebar, status bar, global shortcuts + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 12: Files table, toolbar, row actions, keyboard and drops + +**Files:** + +- Create in `src/renderer/src/components/queue/`: `QueueToolbar.tsx`, `QueueTable.tsx`, `QueueRow.tsx`, `ResultCell.tsx`, `StatusCell.tsx`, `TotalsRow.tsx`, `EmptyState.tsx` +- Create: `src/renderer/src/theme/views.css`; Modify: `src/renderer/src/index.css` (add `@import './theme/views.css';` last) +- Modify: `src/renderer/src/App.tsx` (centre content for queue workspaces, `onItemClick` `:339-347`, new `retry`, `onRowAction`, toolbar handlers, per-row estimates, run scope) +- Delete: `src/renderer/src/components/Queue.tsx`, `src/renderer/src/components/DropZone.tsx` + +**Interfaces:** + +- Consumes: `rowView`, `queueTotals`, `doneSamples`, `RowActionKind`, `Totals` (Task 5); `groupedRows`, `visibleOrder`, `nextSort`, `SortKey`, `SortState`, `RowGroup` (Task 6); `headerCheck`, `toggleAllIds`, `activeGroupFor` (Task 6); `tableKey`, `TableKey` (Task 6); `estimateOutputBytes` (Task 7); `Checkbox`, `RowAction`, `AddFilesButton`, `IconButton`, `SmallButton` (Task 10); reducer actions `select` (with `order`), `selectIds`, `hideFinished` (Task 4). +- Produces: + - `QueueTable({ groups, totals, selected, activeGroup, sort, check, estimates, onSort, onToggleAll, onSelectAll, onRowClick, onToggleRow, onExtend, onOpen, onMenu, onAction, onRemove, onSelectGroup, onAdd })`; the table root is `role="grid"` with `aria-label="Files"`. + - `QueueToolbar({ files, selected, dropping, canRemove, canRetry, canClear, canStop, onAdd, onRemove, onRetry, onClear, onStop })` + - `EmptyState({ icon, title, line, action? })` (reused by Completed in Task 15) + - App functions `retry(ids: string[])`, `onRowAction(id, kind)`. + - Run scope (spec 4.1): with nothing selected, Run and the options act on every input row of the first group in display order; the inspector says `all N files`. + +- [ ] **Step 1: Write the table components** + +```tsx +// src/renderer/src/components/queue/ResultCell.tsx +import type { JSX } from 'react' +import { Icon } from '../icons/Icon' +import type { ResultView } from './rowModel' + +/** The arrow sits centred on the size|result column boundary, on a patch of the + * row background, and only when the cell has content (spec 4.3). */ +export function ResultCell({ result }: { result: ResultView | null }): JSX.Element { + return ( +
+ {result && ( + + + {result.estimate ? {result.text} : {result.text}} + {result.pct && ( + + {result.pct} + + )} + + )} +
+ ) +} +``` + +```tsx +// src/renderer/src/components/queue/StatusCell.tsx +import type { JSX } from 'react' +import { Icon } from '../icons/Icon' +import { RowAction } from '../ui/Button' +import type { RowActionKind, RowActionView, StatusView } from './rowModel' + +export function StatusCell({ + status, + action, + onAction +}: { + status: StatusView + action: RowActionView + onAction: (k: RowActionKind) => void +}): JSX.Element { + return ( +
+ {status.kind === 'failed' ? ( + + + {status.text} + + ) : status.kind === 'running' ? ( + + + + {status.pct} + {status.eta} + + + ) : status.kind === 'done' ? ( + + + Done + + ) : status.kind === 'queued' ? ( + + + Queued + + ) : status.kind === 'canceled' ? ( + + + Canceled + + ) : null} + { + e.stopPropagation() + onAction(action.kind) + }} + onDoubleClick={(e) => e.stopPropagation()} + /> +
+ ) +} +``` + +```tsx +// src/renderer/src/components/queue/QueueRow.tsx +import type { JSX, MouseEvent } from 'react' +import type { QueueItem } from '../../state' +import { Checkbox } from '../ui/Checkbox' +import { ResultCell } from './ResultCell' +import { StatusCell } from './StatusCell' +import type { RowActionKind, RowView } from './rowModel' + +export function QueueRow({ + item, + view, + selected, + dim, + focusable, + onClick, + onToggle, + onOpen, + onMenu, + onAction, + onKeyDown +}: { + item: QueueItem + view: RowView + selected: boolean + /** A row of a different convert group than the selection (spec 2.6). */ + dim: boolean + focusable: boolean + onClick: (e: MouseEvent) => void + onToggle: () => void + onOpen: () => void + onMenu: (x: number, y: number) => void + onAction: (k: RowActionKind) => void + onKeyDown: (e: React.KeyboardEvent) => void +}): JSX.Element { + return ( +
{ + e.preventDefault() + onMenu(e.clientX, e.clientY) + }} + onKeyDown={onKeyDown} + > +
+ +
+
+ {item.thumb ? ( + + ) : ( + + )} + + {item.file.name} + + {/* Below 1180px the kind column folds into the name cell (spec 3.7). */} + +
+
+ {view.kind} +
+
+ {view.size} +
+ + +
+ ) +} +``` + +```tsx +// src/renderer/src/components/queue/TotalsRow.tsx +import type { JSX } from 'react' +import { formatBytes } from '@shared/compress' +import { Icon } from '../icons/Icon' +import type { Totals } from './rowModel' + +export function TotalsRow({ totals: t }: { totals: Totals }): JSX.Element { + return ( +
+
+
+ {t.files} file{t.files === 1 ? '' : 's'} +
+
+
+ {formatBytes(t.bytes)} +
+
+ {t.doneSrc > 0 && ( + <> + {formatBytes(t.doneSrc)} + + {formatBytes(t.doneOut)} + so far + + )} +
+
+ {t.files > 0 && ( + + {t.done} of {t.files} done + + )} +
+
+ ) +} +``` + +```tsx +// src/renderer/src/components/queue/EmptyState.tsx +import type { JSX } from 'react' +import type { IconName } from '@shared/icons' +import { Icon } from '../icons/Icon' +import { SmallButton } from '../ui/Button' + +/** Centred empty block (spec 4.12): no drop zone, an outlined button instead. */ +export function EmptyState({ + icon, + title, + line, + action +}: { + icon: IconName + title: string + line: string + action?: { label: string; onClick: () => void } +}): JSX.Element { + return ( +
+ +
{title}
+
{line}
+ {action && ( + + {action.label} + + )} +
+ ) +} +``` + +```tsx +// src/renderer/src/components/queue/QueueToolbar.tsx +import type { JSX } from 'react' +import { AddFilesButton, IconButton } from '../ui/Button' + +export function QueueToolbar({ + files, + selected, + dropping, + canRemove, + canRetry, + canClear, + canStop, + onAdd, + onRemove, + onRetry, + onClear, + onStop +}: { + files: number + selected: number + dropping: boolean + canRemove: boolean + canRetry: boolean + canClear: boolean + canStop: boolean + onAdd: () => void + onRemove: () => void + onRetry: () => void + onClear: () => void + onStop: () => void +}): JSX.Element { + const count = dropping + ? 'Drop to add' + : `${files} file${files === 1 ? '' : 's'}${selected ? `, ${selected} selected` : ''}` + return ( +
+ + + + +
+ + {count} + + +
+
+ ) +} +``` + +```tsx +// src/renderer/src/components/queue/QueueTable.tsx +import { Fragment, useRef, useState, type JSX, type KeyboardEvent, type MouseEvent } from 'react' +import { Checkbox } from '../ui/Checkbox' +import { Icon } from '../icons/Icon' +import { groupOf } from '../../state' +import { EmptyState } from './EmptyState' +import { QueueRow } from './QueueRow' +import { TotalsRow } from './TotalsRow' +import { rowView, type RowActionKind, type Totals } from './rowModel' +import type { CheckState } from './selectAll' +import { tableKey } from './tableKeys' +import { visibleOrder, type RowGroup, type SortKey, type SortState } from './tableSort' + +const HEADS: { key: SortKey; label: string; num?: boolean }[] = [ + { key: 'name', label: 'name' }, + { key: 'kind', label: 'kind' }, + { key: 'size', label: 'size', num: true }, + { key: 'result', label: 'result' }, + { key: 'status', label: 'status' } +] + +export function QueueTable({ + groups, + totals, + selected, + activeGroup, + sort, + check, + estimates, + onSort, + onToggleAll, + onSelectAll, + onRowClick, + onToggleRow, + onExtend, + onOpen, + onMenu, + onAction, + onRemove, + onSelectGroup, + onAdd +}: { + groups: RowGroup[] + totals: Totals + selected: string[] + activeGroup: string | null + sort: SortState | null + check: CheckState + estimates: Record + onSort: (k: SortKey) => void + onToggleAll: () => void + onSelectAll: () => void + onRowClick: (id: string, e: MouseEvent) => void + onToggleRow: (id: string) => void + /** Shift+Arrow: extend the range to this id. */ + onExtend: (id: string) => void + onOpen: (id: string) => void + onMenu: (id: string, x: number, y: number) => void + onAction: (id: string, k: RowActionKind) => void + onRemove: (id: string) => void + onSelectGroup: (group: string) => void + onAdd: () => void +}): JSX.Element { + const body = useRef(null) + const order = visibleOrder(groups) + const [focusId, setFocusId] = useState(null) + const tabStop = focusId && order.includes(focusId) ? focusId : (selected[0] ?? order[0] ?? null) + + function focusRow(id: string | undefined): void { + if (!id) return + setFocusId(id) + body.current?.querySelector(`[data-id="${CSS.escape(id)}"]`)?.focus() + } + + function onRowKey(id: string, e: KeyboardEvent): void { + if (e.target !== e.currentTarget) return + const k = tableKey(e) + if (!k) return + e.preventDefault() + const i = order.indexOf(id) + if (k === 'up' || k === 'down') focusRow(order[k === 'up' ? i - 1 : i + 1]) + else if (k === 'extendUp' || k === 'extendDown') { + const next = order[k === 'extendUp' ? i - 1 : i + 1] + if (next) { + focusRow(next) + onExtend(next) + } + } else if (k === 'toggle') onToggleRow(id) + else if (k === 'selectAll') onSelectAll() + else if (k === 'remove') onRemove(id) + else if (k === 'open') onOpen(id) + else { + const r = e.currentTarget.getBoundingClientRect() + onMenu(id, r.left + 40, r.bottom) + } + } + + return ( +
+
+
+ +
+ {HEADS.map((h) => { + const on = sort?.key === h.key + return ( +
onSort(h.key)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + onSort(h.key) + } + }} + > + {h.label} + +
+ ) + })} +
+
+ {groups.length === 0 ? ( + + ) : ( + groups.map((g) => ( + + {groups.length > 1 && ( +
+ +
+ )} + {g.items.map((item) => ( + { + setFocusId(item.id) + onRowClick(item.id, e) + }} + onToggle={() => onToggleRow(item.id)} + onOpen={() => onOpen(item.id)} + onMenu={(x, y) => onMenu(item.id, x, y)} + onAction={(k) => onAction(item.id, k)} + onKeyDown={(e) => onRowKey(item.id, e)} + /> + ))} +
+ )) + )} +
+ +
+ ) +} +``` + +- [ ] **Step 2: Write `theme/views.css` (queue part)** + +```css +/* Views and states the mockup does not draw (spec 2.6, 3.3, 4.9, 4.12). */ + +/* cross-group rows: fg3 text, thumbnail at 40% (monochrome replacement for opacity-40) */ +.tr.dim .name, +.tr.dim .meta, +.tr.dim .size { + color: var(--fg3); +} +.tr.dim .thumb { + opacity: 0.4; +} + +/* mixed-queue group row: SettingGroup style, selects the whole group */ +.grp-row { + height: 28px; + border-bottom: 1px solid var(--line); +} +.grp { + width: 100%; + height: 100%; + display: flex; + align-items: center; + gap: 10px; + padding: 0 10px 0 16px; + font: 600 11px/1 var(--mono); + letter-spacing: 0.09em; + color: var(--fg3); +} +.grp::after { + content: ''; + flex: 1; + height: 1px; + background: var(--line); +} +.grp .n { + font-weight: 500; + letter-spacing: 0; +} +.grp:hover { + color: var(--fg1); + background: var(--hover); +} + +/* narrow window (spec 3.7, O5): below 1180px the kind column folds into the + name cell as a trailing fg3 tag. Hidden cells take no grid slot, so the + head, rows and totals all drop their third cell and use five columns. */ +.ktag { + display: none; +} +@media (max-width: 1179px) { + .cols { + --cols: 40px 1fr 88px 184px 232px; + } + .cols > :nth-child(3) { + display: none; + } + .ktag { + display: inline; + margin-left: auto; + } +} + +/* totals arrow is inline, not the split arrow */ +.totals .tarr, +.totals .pct { + color: var(--fg3); +} + +/* drop target: a 1px fg1 inset outline around the centre panel */ +.center.dropping::after { + content: ''; + position: absolute; + inset: 0; + outline: 1px solid var(--fg1); + outline-offset: -1px; + pointer-events: none; +} + +/* empty states */ +.empty { + height: 100%; + min-height: 220px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + color: var(--fg2); +} +.empty .et { + font-weight: 600; + font-size: 14px; + color: var(--fg1); +} +.empty .el { + font-size: 12px; + color: var(--fg3); + margin-bottom: 6px; +} +.empty .sbtn { + align-self: center; +} +``` + +Add `@import './theme/views.css';` as the last import in `index.css`. + +- [ ] **Step 3: Wire the table into `App.tsx`** + +Imports: remove `DropZone` and `Queues`; add + +```ts +import { QueueTable } from './components/queue/QueueTable' +import { QueueToolbar } from './components/queue/QueueToolbar' +import { doneSamples, queueTotals, type RowActionKind } from './components/queue/rowModel' +import { groupedRows, nextSort, visibleOrder, type SortState } from './components/queue/tableSort' +import { activeGroupFor, headerCheck, toggleAllIds } from './components/queue/selectAll' +import { estimateOutputBytes } from '@shared/sizeEstimate' +import { resizedSize } from '@shared/resize' +import type { JobOptions, ToolId } from '@shared/types' +``` + +State: `const [sorts, setSorts] = useState>({})`. + +**Run scope** (spec 4.1). Replace the selection-derived block (`:265-276`) so that with nothing selected the panel and Run act on the first display group: + +```ts +const selectedItems = cur.items.filter((i) => cur.selected.includes(i.id)) +// With nothing selected, options and Run speak for the first group in the +// queue (spec 4.1), not an empty panel. +const firstGroup = activeGroupFor(cur.items, []) +const scopeItems = selectedItems.length + ? selectedItems + : cur.items.filter((i) => inInput(i) && firstGroup != null && groupOf(i.file) === firstGroup) +const selEff = scopeItems.map(effectiveFile) +const activeKind: FileKind | null = selEff.length ? selEff[0].kind : null +// Dimming and the breadcrumb follow the SELECTION only. +const activeGroup: string | null = selectedItems.length + ? groupOf(effectiveFile(selectedItems[0])) + : null +const scopeGroup: string | null = selEff.length ? groupOf(selEff[0]) : null +const fallbackKind: FileKind = tab.kinds[0] ?? card?.kinds[0] ?? 'image' +const optGroup = scopeGroup ?? convertGroup(fallbackKind, '') +``` + +and in the `runList` filter (`:298`) change `selectedItems.filter(` to `scopeItems.filter(`. Leave the rest of `runList`'s predicate unchanged. Everywhere the OptionsPanel props used `activeGroup` to decide whether to show options, pass `scopeGroup` instead (until Task 13 replaces the panel). + +**Row clicks follow the visible order:** + +```ts +const sort = sorts[qKey] ?? null +const groups = groupedRows(cur.items, sort) +const order = visibleOrder(groups) + +function onItemClick(id: string, e: MouseEvent): void { + const mode: SelectMode = e.shiftKey ? 'range' : e.ctrlKey || e.metaKey ? 'toggle' : 'single' + dispatch({ type: 'select', id, mode, order }) +} +``` + +**Retry and row actions:** + +```ts +function toolFor(it: QueueItem, opts: JobOptions): ToolId { + return engineFor( + state.tab, + groupOf(it.file), + card, + { kind: it.file.kind, ext: it.file.ext }, + typeof opts.format === 'string' ? opts.format : undefined + ).tool +} + +/** Same path as run() for one in-place item, with the options it last ran + * with (spec 4.3). A merge row retries with the same input list, because + * run() now stores `mergeInputs` in its `runOptions` (see below). */ +function retry(ids: string[]): void { + const started: string[] = [] + for (const id of ids) { + const it = cur.items.find((i) => i.id === id) + if (!it || (it.status !== 'failed' && it.status !== 'canceled')) continue + const opts = it.runOptions ?? curOptions + dispatch({ type: 'markQueued', ids: [id], options: opts }) + void window.filesmith.runJob({ + id, + tool: toolFor(it, opts), + input: it.file.path, + options: opts + }) + started.push(id) + } + if (started.length) setBatches((b) => ({ ...b, [qKey]: started })) +} + +function onRowAction(id: string, kind: RowActionKind): void { + const it = cur.items.find((i) => i.id === id) + if (!it) return + if (kind === 'reveal') { + if (it.outputPath) window.filesmith.reveal(it.outputPath) + } else if (kind === 'cancel') cancelJob(id) + else if (kind === 'remove') dismiss([id], 'input') + else retry([id]) +} +``` + +In `run()`'s merge branch (`App.tsx:697`) change `dispatch({ type: 'markQueued', ids: [anchorId], options: opts })` to `dispatch({ type: 'markQueued', ids: [anchorId], options: { ...opts, mergeInputs: paths } })`. Without it a retried merge row would reach the engine with no input list and fail again with "Select at least two PDFs to merge" (`registry.ts:322-323`). + +**Per-row estimates** for running rows: + +```ts +function estimateFor(it: QueueItem): number | null { + const opts = it.runOptions ?? curOptions + const t = toolFor(it, opts) + const d = vDims[it.file.path] + let pixelRatio: number | null = null + let outPixels: number | null = null + if (t === 'resize') { + if (String(opts.mode ?? 'percent') === 'percent') + pixelRatio = (Number(opts.percent ?? 50) / 100) ** 2 + else if (d) { + const o = resizedSize( + d.width, + d.height, + numOrNull(opts.width), + numOrNull(opts.height), + opts.fit === 'stretch' ? 'stretch' : 'contain' + ) + if (o) pixelRatio = (o.w * o.h) / (d.width * d.height) + } + } else if (t === 'upscale' && d) { + const f = Number(opts.upscaleFactor ?? 4) + outPixels = d.width * f * d.height * f + } + return estimateOutputBytes(it.file, t, opts, { + samples: doneSamples(cur.items, groupOf(it.file), opts), + pixelRatio, + outPixels + }) +} +const estimates: Record = {} +for (const i of cur.items) + if (inInput(i) && i.status === 'running') estimates[i.id] = estimateFor(i) +``` + +(`numOrNull` already exists at `:803`; move it above `estimateFor` if it is declared later.) + +**Toolbar flags and handlers:** + +```ts +const inputs = cur.items.filter(inInput) +const groupForBulk = activeGroupFor(cur.items, cur.selected) +const failedInGroup = inputs.filter( + (i) => i.status === 'failed' && groupOf(i.file) === groupForBulk +) +const inFlight = inputs.filter((i) => i.status === 'queued' || i.status === 'running') +``` + +Replace the old `DropZone` + `Queues` branch of the centre with: + +```tsx +<> + 0} + canRetry={failedInGroup.length > 0} + canClear={inputs.some((i) => i.status === 'done' || i.status === 'canceled')} + canStop={inFlight.length > 0} + onAdd={() => void browse()} + onRemove={() => dismiss(cur.selected, 'input')} + onRetry={() => retry(failedInGroup.map((i) => i.id))} + onClear={() => dispatch({ type: 'hideFinished' })} + onStop={() => inFlight.forEach((i) => cancelJob(i.id))} + /> + setSorts((s) => ({ ...s, [qKey]: nextSort(sort, k) }))} + onToggleAll={() => dispatch({ type: 'selectIds', ids: toggleAllIds(cur.items, cur.selected) })} + onSelectAll={() => { + const ids = toggleAllIds(cur.items, cur.selected) + if (ids.length) dispatch({ type: 'selectIds', ids }) + }} + onRowClick={onItemClick} + onToggleRow={(id) => dispatch({ type: 'select', id, mode: 'toggle', order })} + onExtend={(id) => dispatch({ type: 'select', id, mode: 'range', order })} + onOpen={(id) => { + const it = cur.items.find((i) => i.id === id) + if (it) openExternally('input', it) + }} + onMenu={(id, x, y) => { + const it = cur.items.find((i) => i.id === id) + if (it) openMenu('input', it, x, y) + }} + onAction={onRowAction} + onRemove={(id) => dismiss(cur.selected.includes(id) ? cur.selected : [id], 'input')} + onSelectGroup={(g) => + dispatch({ + type: 'selectIds', + ids: inputs.filter((i) => groupOf(i.file) === g).map((i) => i.id) + }) + } + onAdd={() => void browse()} + /> + +``` + +Set the centre element's class to `` `center${dragging ? ' dropping' : ''}` ``. The `.center` grid has rows `32px 1fr`; the toolbar and table are its two children. `dragging` is still set by the root's `onDragOver` and cleared by `onDragLeave` / `onDrop`; skip setting it while on Generate, the Tools grid, Completed or Settings (unchanged guard). + +Note on Ctrl+A: when the whole group is already selected `toggleAllIds` returns `[]`, so `onSelectAll` leaves the selection as is instead of clearing it. + +- [ ] **Step 4: Delete the replaced components** + +```bash +git rm src/renderer/src/components/Queue.tsx src/renderer/src/components/DropZone.tsx +``` + +- [ ] **Step 5: Verify** + +Run: `npm test && npm run typecheck && npm run lint` +Expected: all green (`queue-groups.test.ts` still passes; `groupItemsByGroup` stays exported for its test until Task 16 decides its fate). + +Run: `npm run dev` and check by hand with a mix of 6 images: column widths match the mockup (40 / name / 64 / 88 / 184 / 232), narrowing the window under 1180px drops the kind column and shows the kind as a grey tag at the end of the name cell, header labels are lowercase, the head and totals stay fixed while rows scroll, the result arrow sits exactly on the size/result border and its patch matches hover and selected backgrounds; a running row shows `NN%(Ns)` with no bar under it; a failed row shows the inverted pill and an always-visible Retry that re-runs it; done rows show `Show in folder` on hover and it reveals the OUTPUT; queued rows' Remove cancels and removes; clicking `size` sorts ascending, again descending (chevron flips), a third time restores the order; header checkbox selects the active group only; Up/Down move focus, Space toggles, Shift+Arrow extends, Ctrl+A selects the group, Delete removes, Enter opens, Shift+F10 opens the menu; an empty queue shows "No files yet" with an Add files button and keeps head and totals; dragging files over the window outlines the centre and the count reads `Drop to add`. Add a video to the image queue: group rows `IMAGES 6` / `VIDEO 1` appear, image rows dim when the video is selected, clicking a group row selects that group. + +- [ ] **Step 6: Commit** + +```bash +git add -A src/renderer/src/components/queue src/renderer/src/theme/views.css src/renderer/src/index.css src/renderer/src/App.tsx +git commit -m "feat(queue): details table with result arrow, compact progress, row actions, sort and keyboard + +Co-Authored-By: Claude Opus 5.5 " +``` + +--- + +### Task 13: Inspector and the options split (grouped settings, estimate card, output location) + +**Files:** + +- Create: `src/renderer/src/components/inspector/Inspector.tsx` +- Create in `src/renderer/src/components/options/`: `types.ts`, `OptionsPane.tsx`, `ConvertSettings.tsx`, `PageRenderSettings.tsx`, `CompressSettings.tsx`, `ResizeSettings.tsx`, `UpscaleSettings.tsx`, `RemoveBgSettings.tsx`, `PdfSettings.tsx`, `ArchiveSettings.tsx`, `OutputSettings.tsx`, `upscale/PidSetup.tsx`, `upscale/ComfySetup.tsx`, `generate/GenerateSettings.tsx`, `generate/ModelSelect.tsx`, `generate/CompanionDownload.tsx`, `generate/LocateComfy.tsx`, `generate/AddModel.tsx`, `generate/AdvancedSettings.tsx`, `generate/restore.ts` +- Move (git mv): `components/useArchiveStatus.ts`, `useComfyModels.ts`, `useGenerateStatus.ts`, `usePidStatus.ts` into `components/options/hooks/` +- Modify: `src/renderer/src/App.tsx` (replace ``, memoize `onSet`, batch estimate, inspector state) +- Delete: `src/renderer/src/components/OptionsPanel.tsx`, `PidUpscale.tsx`, `ComfyImport.tsx`, `OperationTitle.tsx` +- Test: `test/generate-restore.test.ts` + +**Interfaces:** + +- Consumes: every primitive from Task 10; `Icon`; `estimateBatch` (Task 7); `groupNoun` from `queueGroups.ts`; `window.filesmith.pickFolder` (Task 1); existing hooks and IPC calls. +- Produces: + - `type SetOption = (k: string, v: string | number | boolean) => void` (`options/types.ts`) + - `OptionsPane({ tab, tool, options, kind, srcExts, sourceExt, runCount, videoOutputs, resizeOutputs, upscaleOutputs, estimate, set })` where `estimate: { from: number; to: number; files: number } | null` and the three output lists are `SizeRow[]` + - `type InspTab = 'options' | 'preview' | 'info'` (exported from `inspector/Inspector.tsx` as a type) and `Inspector({ tab, onTab, title, sub, runLabel, runDisabled, onRun, children })`. Its title is an `

`; the Run button carries `data-testid="run"`. + - `isRestoreName(label: string): boolean` (`generate/restore.ts`, moved verbatim from `OptionsPanel.tsx:1116`) + +Every existing control is kept (spec 4.2); headings group them; the old label becomes the setting title and the old hint or `HelpTip` text becomes the description. Behavioural fixes riding along: `ArchiveSettings` from-pdf reuses `PageRenderSettings`, so the quality slider writes `pageQuality` (the engine reads `options.pageQuality`, `registry.ts:1301`); `onSet` is memoized; `useArchiveStatus` is called once; the stale "three toggles" comment is dropped. + +- [ ] **Step 1: Move the restoration-name helper out and test it** + +Copy `isRestoreName` from `OptionsPanel.tsx:1116` (the function and its regex, unchanged) into `options/generate/restore.ts` as `export function isRestoreName(label: string): boolean`. Then: + +```ts +// test/generate-restore.test.ts +import { describe, expect, it } from 'vitest' +import { isRestoreName } from '../src/renderer/src/components/options/generate/restore' + +describe('isRestoreName', () => { + it('flags restoration checkpoints so Generate does not auto-pick them', () => { + expect(isRestoreName('SUPIR-v0Q')).toBe(true) + }) + it('leaves ordinary text-to-image models alone', () => { + expect(isRestoreName('sd_xl_base_1.0')).toBe(false) + }) +}) +``` + +Run: `npx vitest run test/generate-restore.test.ts` +Expected: PASS. (If the copied regex does not match `SUPIR-v0Q`, the copy is wrong: re-copy it from `OptionsPanel.tsx:1116`.) + +- [ ] **Step 2: Move the status hooks** + +```bash +mkdir -p src/renderer/src/components/options/hooks +git mv src/renderer/src/components/useArchiveStatus.ts src/renderer/src/components/useComfyModels.ts src/renderer/src/components/useGenerateStatus.ts src/renderer/src/components/usePidStatus.ts src/renderer/src/components/options/hooks/ +``` + +Fix their relative imports (`../../../` depth changes) until `npm run typecheck` passes for those four files. + +- [ ] **Step 3: Shared type and the simple settings files** + +```ts +// src/renderer/src/components/options/types.ts +export type SetOption = (k: string, v: string | number | boolean) => void +``` + +```tsx +// src/renderer/src/components/options/PageRenderSettings.tsx +import type { JSX } from 'react' +import type { JobOptions } from '@shared/types' +import { RangeField } from '../ui/RangeField' +import { Segmented } from '../ui/Segmented' +import { Setting, SettingGroup } from '../ui/Setting' +import type { SetOption } from './types' + +/** Page rendering for every route that turns a PDF into images. Writes + * `pageQuality`, never `quality`, so it cannot collide with convert's preset. */ +export function PageRenderSettings({ + options, + set +}: { + options: JobOptions + set: SetOption +}): JSX.Element { + const pageFormat = String(options.pageFormat ?? 'jpg') + return ( + + + set('dpi', v)} + format={(v) => `${v} dpi`} + /> + + + set('pageFormat', v)} + /> + + {pageFormat === 'jpg' && ( + + set('pageQuality', v)} + ends={['smaller file', 'higher quality']} + /> + + )} + + ) +} +``` + +```tsx +// src/renderer/src/components/options/ConvertSettings.tsx +import type { JSX } from 'react' +import type { FileKind, JobOptions } from '@shared/types' +import { familyFormats, isSameFormat, sharedTargets } from '@shared/convert' +import { needsRar } from '@shared/archive' +import { Select } from '../ui/Select' +import type { SelectOption } from '../ui/selectNav' +import { Segmented } from '../ui/Segmented' +import { Setting, SettingGroup } from '../ui/Setting' +import { OutputSettings } from './OutputSettings' +import { PageRenderSettings } from './PageRenderSettings' +import type { SetOption } from './types' + +export function ConvertSettings({ + options, + kind, + sourceExt, + srcExts, + verb, + hasRar, + set +}: { + options: JobOptions + kind: FileKind + sourceExt: string | null + srcExts: string[] + /** The archive verb this target resolves to, if any: repack / to-pdf / from-pdf. */ + verb?: string + hasRar: boolean + set: SetOption +}): JSX.Element { + // Targets valid for EVERY selected source, so a mixed pdf+docx selection never + // offers CBZ (which only the pdf could do). + const formats = srcExts.length + ? sharedTargets(kind, srcExts) + : familyFormats(kind, sourceExt ?? '') + const choices: SelectOption[] = formats.map((f) => { + const isSource = srcExts.some((e) => isSameFormat(f.ext, e)) + const noRar = needsRar(f.ext) && !hasRar + return { + value: f.ext, + label: f.label.toLowerCase(), + disabled: isSource || noRar, + reason: noRar ? 'WinRAR not found' : isSource ? 'already this format' : undefined + } + }) + return ( + <> + + + { + if (v === 'source') set('outDir', '') + }} + /> + void choose()}> + Choose folder + +

+ + + + + set('pdfLevel', v)} + /> + + + set('pdfGray', v)} + label="Convert to greyscale" + sub="Smaller, but every page loses its colour" + /> + + + ) + if (kind === 'video') { + const scale = Number(options.scale ?? 100) + return ( + + + set('audioCodec', v)} + /> + + + ({ value: b, label: `${b}k` }))} + onChange={(v) => set('audioBitrate', v)} + /> + + + ) + return ( + + + set('bgFill', v)} /> + + {fill === 'custom' && ( + + {/* The swatch shows user data, the one non-grey value on screen (spec 4.2). */} + + + )} + {fill === 'image' && ( + {bgImage.split(/[\\/]/).pop()} : undefined} + > + + void window.filesmith.pickImage().then((p) => { + if (p) set('bgImagePath', p) + }) + } + > + Choose image + + + )} + + ) +} +``` + +```tsx +// src/renderer/src/components/options/PdfSettings.tsx +import type { JSX } from 'react' +import type { JobOptions } from '@shared/types' +import { RangeField } from '../ui/RangeField' +import { Setting, SettingGroup } from '../ui/Setting' +import { TextField } from '../ui/TextField' +import type { SetOption } from './types' + +/** Each Tools card is one op, so there is no op picker (spec 4.2). */ +export function PdfSettings({ + options, + runCount, + set +}: { + options: JobOptions + runCount: number + set: SetOption +}): JSX.Element | null { + const op = String(options.op ?? 'extract-text') + if (op === 'pages-to-images') + return ( + + + set('dpi', v)} + format={(v) => `${v} dpi`} + /> + + + ) + if (op === 'split-range') + return ( + + + Ranges and single pages, for example 1-3,5,8-10. + + } + > + set('range', e.target.value)} + /> + + + ) + if (op === 'merge' && runCount < 2) + return ( + + + + ) + return null +} +``` + +```tsx +// src/renderer/src/components/options/ArchiveSettings.tsx +import type { JSX } from 'react' +import type { JobOptions } from '@shared/types' +import { ARCHIVE_FORMATS, COMIC_FORMATS, needsRar } from '@shared/archive' +import { ChipGrid } from '../ui/ChipGrid' +import { Segmented } from '../ui/Segmented' +import { Setting, SettingGroup } from '../ui/Setting' +import { PageRenderSettings } from './PageRenderSettings' +import type { SetOption } from './types' + +/** Archive Tools cards (only reachable when such a card exists). from-pdf reuses + * PageRenderSettings, which fixes the old bug where this panel wrote `quality` + * while the engine reads `pageQuality`. */ +export function ArchiveSettings({ + options, + srcExts, + hasRar, + set +}: { + options: JobOptions + srcExts: string[] + hasRar: boolean + set: SetOption +}): JSX.Element { + const op = String(options.op ?? 'repack') + const targets = (list: typeof ARCHIVE_FORMATS): JSX.Element => ( + + { + // .zip and .cbz are the same container but not the same file: compare by extension. + const isSource = srcExts.includes(f.ext) + const noRar = needsRar(f.ext) && !hasRar + return { + value: f.ext, + label: f.label.toLowerCase(), + disabled: isSource || noRar, + title: noRar + ? 'WinRAR not found' + : isSource + ? 'Files are already this format' + : undefined + } + })} + onChange={(v) => set('format', v)} + /> + + ) + if (op === 'extract') + return ( + + + + ) + if (op === 'to-pdf') + return ( + + + + ) + if (op === 'from-pdf') + return ( + <> + {targets(COMIC_FORMATS)} + + + ) + return ( + + {targets(ARCHIVE_FORMATS)} + + set('store', v === 'store')} + /> + + + ) +} +``` + +The two info-only descriptions (`extract`, `to-pdf`) and the `.cbz` default target are copied word for word from the old `ArchiveOptions` (`OptionsPanel.tsx:1006-1033`). + +- [ ] **Step 4: Upscale settings and its setup blocks** + +`options/upscale/PidSetup.tsx`: move `PidInstallCard` from `components/PidUpscale.tsx` and `PidRemoveButton` from `OptionsPanel.tsx`, keeping every state, effect and IPC call (`pidInstalling`, `pidInstall`, `onPidProgress`, `pidRemove`, the two-step remove confirmation). Restyle as `Setting` blocks per spec 5.5, using this mapping: + +| Old markup | New | +| ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | +| card container with title + paragraph | `` (licence notice and VRAM text become the description) | +| primary download button | `` | +| remove / uninstall button | `` | +| progress bar (rounded track + accent fill) | `` followed by `{pct}%` | +| any `text-[#...]`, `bg-[#...]`, `rounded-*`, `shadow-*` class | removed | + +`options/upscale/ComfySetup.tsx`: the same treatment for `ComfyImportCard` and its local `ProgressBar` from `components/ComfyImport.tsx` (three states: engine not built, scanning, ready; the unusable-files list renders as a `
    `). Delete the local `ProgressBar` in favour of `ui/ProgressBar`. + +```tsx +// src/renderer/src/components/options/UpscaleSettings.tsx +import { useEffect, useMemo, useState, type JSX } from 'react' +import type { JobOptions } from '@shared/types' +import { + UPSCALE_COMFY, + UPSCALE_FACTORS, + UPSCALE_GPU_MODES, + UPSCALE_MODELS, + type Choice, + type UpscaleModel +} from '@shared/compress' +import { SmallButton } from '../ui/Button' +import { ChipGrid } from '../ui/ChipGrid' +import { OutputSizeList, type SizeRow } from '../ui/OutputSizeList' +import { Segmented } from '../ui/Segmented' +import { Select } from '../ui/Select' +import { Setting, SettingGroup } from '../ui/Setting' +import { useComfyModels } from './hooks/useComfyModels' +import { usePidStatus } from './hooks/usePidStatus' +import { ComfyImportCard } from './upscale/ComfySetup' +import { PidInstallCard, PidRemoveButton } from './upscale/PidSetup' +import type { SetOption } from './types' + +export function UpscaleSettings({ + options, + outputs, + set +}: { + options: JobOptions + outputs: SizeRow[] + set: SetOption +}): JSX.Element { + const factor = Number(options.upscaleFactor ?? 4) + const comfy = useComfyModels() + const { status: pid, refresh: refreshPid } = usePidStatus() + const rawModel = String(options.upscaleModel ?? 'photo') + const hasNvidia = Boolean(comfy.status?.nvidia || pid?.nvidia) + const [ncnn, setNcnn] = useState<{ value: string; label: string; user: boolean }[] | null>(null) + useEffect(() => { + let alive = true + void window.filesmith.upscaleModels().then((m) => alive && setNcnn(m)) + return () => { + alive = false + } + }, []) + const comfyModels = comfy.status?.models + const comfyChoices = useMemo( + () => + (comfyModels ?? []).map((m) => ({ + value: `comfy:${m.path}`, + label: `${m.name}, ${m.scale}×${m.badge === 'experimental' ? ', experimental' : ''}` + })), + [comfyModels] + ) + const ncnnChoices: Choice[] = ncnn?.length + ? ncnn.map((m) => ({ + value: m.value as UpscaleModel, + label: m.user ? `${m.label}, added by you` : m.label + })) + : UPSCALE_MODELS + const isPid = rawModel === 'pid' + const isComfyPath = rawModel.startsWith('comfy:') + const inAi = isPid || isComfyPath || rawModel === 'comfy' + const category = inAi + ? 'comfy' + : ncnnChoices.some((c) => c.value === rawModel) + ? rawModel + : (ncnnChoices[0]?.value ?? 'photo') + const categoryChoices = [...ncnnChoices, ...(hasNvidia ? [UPSCALE_COMFY] : [])] + const showPid = hasNvidia && Boolean(pid?.installed || comfy.status?.pidReusable) + const subChoices = [ + ...comfyChoices, + ...(showPid ? [{ value: 'pid', label: 'PiD (diffusion), 4×' }] : []) + ] + const subDefault = comfyChoices[0]?.value ?? (showPid ? 'pid' : 'comfy') + const subValue = isPid ? 'pid' : isComfyPath ? rawModel : subDefault + const pidNeedsInstall = isPid && pid != null && !pid.installed + const vramMb = pid?.nvidia?.vramMb ?? null + const lowVram = isPid && vramMb != null && vramMb < 12_000 + const pickCategory = (v: string): void => set('upscaleModel', v === 'comfy' ? subDefault : v) + useEffect(() => { + if (pid == null || comfy.status == null) return + if (inAi && !hasNvidia) set('upscaleModel', 'photo') + else if (isComfyPath && !comfyChoices.some((c) => c.value === rawModel)) + set('upscaleModel', subDefault) + else if (isPid && !showPid) set('upscaleModel', subDefault) + }, [ + pid, + comfy.status, + hasNvidia, + inAi, + isComfyPath, + isPid, + showPid, + rawModel, + comfyChoices, + subDefault, + set + ]) + const gpuReason = !hasNvidia ? (pid?.cudaReason ?? comfy.status?.cudaReason) : undefined + + return ( + <> + + + ({ value: f, label: `${f}×` }))} + onChange={(v) => set('upscaleFactor', v)} + /> + + + set('upscaleModel', v)} + /> + )} + {!inAi && ( + void window.filesmith.upscaleOpenModelsFolder()} + > + Add your own model + + )} + + {category === 'comfy' && pidNeedsInstall && } + {category === 'comfy' && isPid && pid?.installed && ( + + )} + {category === 'comfy' && comfy.status && ( + + )} + + {(!isPid || lowVram) && ( + + {!isPid && ( + + ({ + value: o.value, + label: o.label.toLowerCase() + }))} + onChange={(v) => set('gpuMode', v)} + /> + + )} + {lowVram && ( + + )} + + )} + {outputs.length > 0 && ( + + + + + + )} + + ) +} +``` + +The separators inside model labels change from `·` to `, ` only for display; stored values (`comfy:`, `pid`, `photo`, ...) are unchanged. + +- [ ] **Step 5: Generate settings** + +Create `generate/ModelSelect.tsx` exporting `ModelSelect({ models, value, onChange })`: a `Select` whose options are the models with `group` = the architecture group heading the old `ModelPicker` used for its ``s (`OptionsPanel.tsx:1123-1167`), no `disabled` flag, and `reason: 'needs download'` for non-runnable models (replacing the old dash-prefixed suffix). Non-runnable models must stay selectable, exactly as the old picker documents (`OptionsPanel.tsx:1119-1121`): picking one is what reveals `CompanionDownload` or the Try anyway button below, so disabling them would make both unreachable. Move `CompanionDownload` (`:1172-1241`), `LocateComfy` (`:1252-1276`) and `AddModel` (`:1284-1356`) into their own files under `generate/` with every effect, state and IPC call unchanged, restyled with the same mapping table as Step 4 (buttons become `SmallButton`; `AddModel`'s three actions use icons `addfile`, `folder`, `folder`; the missing-file list is `
      `; the download bar is ``). + +`generate/AdvancedSettings.tsx` exports `AdvancedSettings({ options, info, set })` and holds Steps, CFG, Guidance and Seed from `OptionsPanel.tsx:1631-1725` with their ranges unchanged (`steps`: min 8 for sdxl and flux1 else 1, max 50; `cfg`: 1-15 step 0.5, sdxl only; `guidance`: 1-10 step 0.5 when `info.hasGuidance`; `seed`: `NumberField` plus a `CheckSetting` "Random seed" that writes `-1`). Each `HelpTip` text becomes that setting's `desc`. It renders inside `` with local `useState(false)` for `open`. + +`generate/GenerateSettings.tsx`: + +```tsx +import { useEffect, useMemo, useRef, type JSX } from 'react' +import type { JobOptions } from '@shared/types' +import { GEN_MAX_COUNT, GEN_SIZES, GEN_STYLES, clampDim } from '@shared/generate' +import { archInfoFor, type GenArch } from '@shared/genArch' +import { SmallButton } from '../../ui/Button' +import { ChipGrid } from '../../ui/ChipGrid' +import { RangeField } from '../../ui/RangeField' +import { Select } from '../../ui/Select' +import { Setting, SettingGroup } from '../../ui/Setting' +import { NumberField, TextField } from '../../ui/TextField' +import { useGenerateStatus } from '../hooks/useGenerateStatus' +import type { SetOption } from '../types' +import { AddModel } from './AddModel' +import { AdvancedSettings } from './AdvancedSettings' +import { CompanionDownload } from './CompanionDownload' +import { LocateComfy } from './LocateComfy' +import { ModelSelect } from './ModelSelect' +import { isRestoreName } from './restore' + +export function GenerateSettings({ + options, + set +}: { + options: JobOptions + set: SetOption +}): JSX.Element { + const { status, refresh } = useGenerateStatus() + const model = String(options.model ?? '') + const w = Number(options.width ?? 1024) + const h = Number(options.height ?? 1024) + const isPreset = GEN_SIZES.some((s) => s.width === w && s.height === h) + const sizeValue = + String(options.sizeMode ?? '') === 'custom' || !isPreset ? 'custom' : `${w}x${h}` + const models = useMemo(() => status?.models ?? [], [status]) + const selected = models.find((m) => m.name === model) + const tryAnyway = Boolean(options.tryAnyway) + const dimCaps = status?.dimCaps?.[selected?.arch ?? 'sdxl'] + const arch: GenArch = selected?.arch ?? 'sdxl' + const info = archInfoFor(arch, status?.archInfo) + useEffect(() => { + if (!models.length || selected) return + const good = + models.find((m) => m.runnable && !isRestoreName(m.label)) ?? + models.find((m) => m.runnable) ?? + models[0] + if (good && good.name !== model) set('model', good.name) + }, [model, models, selected, set]) + const prevArch = useRef(null) + useEffect(() => { + if (!status) return + if (prevArch.current === null) { + prevArch.current = arch + return + } + if (prevArch.current === arch) return + prevArch.current = arch + set('tryAnyway', 0) + set('steps', info.steps) + set('cfg', info.cfg) + if (info.hasGuidance) set('guidance', info.guidance) + }, [arch, info, set, status]) + + const sizeChoices = [ + ...GEN_SIZES.map((s) => ({ value: `${s.width}x${s.height}`, label: s.label })), + { value: 'custom', label: 'custom' } + ] + return ( + <> + {status && !status.available && } + + + {models.length > 0 && ( + set('model', v)} /> + )} + {selected && !selected.runnable && !selected.missing?.length && selected.tryAnyway && ( + set('tryAnyway', tryAnyway ? 0 : 1)} + > + {tryAnyway ? 'Will try anyway, click to cancel' : 'Try anyway'} + + )} + + {selected && !selected.runnable && selected.missing?.length ? ( + + ) : null} + + + + {info.cfg !== 1 && ( + + set('negative', e.target.value)} + /> + + )} + + ({ value: s.value, label: s.label.toLowerCase() }))} + onChange={(v) => set('style', v)} + /> + + + + + set('count', v)} + /> + + +