Files
Lumotia/docs/whisper-ecosystem/workstream-B.md
Claude 89c63891fa chore: rebrand from Kon/Corbie to Magnotia
Replace all instances of the legacy product names "Kon" and "Corbie" with
"Magnotia" across user-facing copy, code identifiers, package names, bundle
ids, file paths, and documentation. Preserves the unrelated "konsole" (KDE
terminal) reference and the parent CORBEL company name.

- Renames 10 Rust crates (kon-* → magnotia-*) and the tauri binary
- Updates package.json, tauri.conf.json (productName + identifier)
- Renames CSS classes (kon-rh-* → magnotia-rh-*) and animations
- Renames brand and roadmap docs
- Regenerates Cargo.lock and package-lock.json

Verified: svelte-check passes; pure-rust crates compile under new names.
2026-04-30 13:06:55 +00:00

16 KiB
Raw Blame History

Workstream B — UX + formatting layer

Execution plan for the Opus half of the Whisper-ecosystem pass. Branch: phase4-ux-f7d0 (base: main). Companion: phase4-systems-f7d0 (Workstream A).

This is the UX + prompt / cleanup half. Every task below maps to an atomic item in docs/whisper-ecosystem/brief.md. Items #1, #2, #6, #7, #8, #9, #12, #13, #18, #19, #21, #22, #23, #24, #25, #26, #28, #29 are owned by Workstream A and appear here only where they shape a surface B consumes.


Scope recap

In scope for B: items #3, #4 (UX surface), #5, #10, #11, #14 (UI), #15, #16, #17, #20, #27, #30, #31.

Untouchable (owned by A): crates/transcription, crates/audio, crates/ai-formatting/src/pipeline.rs, crates/ai-formatting/src/rule_based.rs, and src-tauri/src/commands/{models,transcription,live}.rs.

Partially touchable: crates/ai-formatting/src/llm_client.rs — only the CLEANUP_PROMPT constant for item #16. Every other line in that file stays exactly as it is.


Ideology rules (override defaults)

From the Workstream B prompt:

  1. Low cognitive load. Every new Settings entry must earn its mental real estate. No "we could expose this too" reflexes. Most items add zero new controls and land under existing sections.
  2. Never rewrite history in the paste buffer. Raw transcript is always recoverable (item #17). Clipboard restore after paste (item #3) follows the same rule: user's pre-dictation clipboard returns automatically.
  3. "Translator, not editor" (item #16) is the formatting contract. Load-bearing. Any prompt edit that softens it gets rejected at review.
  4. Local-first. No feature may assume cloud availability. Ollama (#27) is additive — Magnotia works fully offline without it.

Execution order

Three phases, each one commit per concern, each ending with a green cargo build -p magnotia && npm run check. We stop at each phase boundary for review.

Phase B.1 — Pre-emptive UX (items #3, #4 UX, #5, #10, #11)

No new Settings sections. These are invisible until they save you from a bug, which matches the brief's "pre-emptive patches" framing.

Seq Item Effect Depends on A?
1 #3: clipboard snapshot + restore after paste src-tauri/src/commands/paste.rs already paste-matrix-hardens for Wayland. Extend it: before Clipboard::new().set_text(text), read the existing Clipboard::get_text() snapshot (+ get_image() if supported by arboard on this target). Schedule a tokio::spawn task that sleeps 300ms after paste and restores the snapshot. User's prior clipboard content is therefore recovered automatically. Keep the hide_preview_overlay_for_paste dance. No
2 #4 (UX side): debounce hotkey, add "warming up…" state A warms the CPAL stream at app start. B owns the page.status state machine: the hotkey handler in DictationPage.svelte already gates on page.recording; add a ~120ms debounce on the hotkey press so a rapid double-tap doesn't double-init. Existing native capture path is untouched. Stream warm-up lands in A's Phase A.3 (item #23); B's debounce is independent.
3 #5: hotkey capability matrix per OS, UI rejection of invalid combos src/lib/components/HotkeyRecorder.svelte (or equivalent). Add hotkeyValidity.ts returning { valid: boolean, reason: string | null } given a combo + OS. Rules: on X11, reject single-key combos and report "Add a modifier to capture this key outside Magnotia"; on Windows, reject combos whose only modifier is a right-hand variant (RCtrl/RAlt — per Handy #966); on macOS, reject Fn-only combos. Block save button + surface inline "why" copy. No
4 #10: detect focused-app class; force clipboard-only paste in terminals Extend src-tauri/src/commands/paste.rs::paste_text to look up the focused window class (GetForegroundWindow → class name on Windows; xdotool getactivewindow getwindowclassname on Linux X11; CGWindowListCopyWindowInfo → ownerName on macOS). On a known-terminal class (Alacritty, kitty, gnome-terminal-server, WindowsTerminal, Code, iTerm2, Terminal, etc.) skip the keystroke and just set the clipboard; return outcome.pasted = false, outcome.copied = true, message = "Terminal detected — clipboard only". UX handles that message already. No
5 #11: versioned settings schema with forward migration src/lib/stores/page.svelte.ts currently reads magnotia_settings localStorage blob via parseStoredJson and spreads it over defaults. Add a SettingsSchemaVersion integer (start at 1) and a migrateSettings(raw, toVersion) helper. Persist { version, data } on save; read both on load; run migrations in order; fall back cleanly on corruption (drop to defaults, toast the user once). Every new field B adds (below) bumps the version. No

Commit boundary: cargo build -p magnotia && npm run check green for every commit; JS-side vitest isn't set up in this repo, so the migration tests live under src/lib/utils/__tests__/ as pure TS + svelte-check. If tests need a runner we borrow Codex's setup in Phase B.2. Stop for review.

Phase B.2 — Feature pinches (items #14 UI, #15, #17, #20)

Seq Item Effect Depends on A?
6 #14 (UI): GPU enumeration + explicit device selector in Settings Adds a Compute device row to SettingsPage.svelte under the existing openSection === 'transcription' section. Reads runtimeCapabilities.activeComputeDevice (A ships this in Phase A.1 #1) and list_gpus() (A ships in Phase A.2). If list_gpus isn't shipped yet, B ships the UI behind if (runtimeCapabilities.activeComputeDevice) and a TODO to call list_gpus later. set_preferred_gpu(id) is wired to a save-on-select handler with a "Restart to apply" toast. No default change until user picks. Yes — A ships list_gpus in Phase A.2. B stubs behind the activeComputeDevice check until then.
7 #15: named LLM prompt presets Preset data lives client-side in src/lib/stores/promptPresets.svelte.ts (new). Five presets shipped: Quick clean (default), Email, Meeting notes, Code, Summary. Each is { id, name, systemPrompt, modelTier }. User-editable list stored in localStorage['magnotia_prompt_presets'] with migration hook. Settings gets a new "Prompt presets" sub-card inside openSection === 'ai'; DictationPage gets a small preset pill above the Status bar — active preset is applied at cleanup time. Default preset's systemPrompt is CLEANUP_PROMPT from llm_client.rs, so the existing baseline is preserved. No — uses existing cleanup_transcript_text_cmd(transcript, profile_id). If the backend later needs system_prompt_override (not strictly needed for MVP — UI just restricts when cleanup fires), B adds that with A's sign-off.
8 #17: raw-transcript revert with ⌘/Ctrl+Z within 5 s src/lib/pages/DictationPage.svelte::finaliseTranscription keeps rawTranscript alongside transcript. After paste, start a 5-second window where Ctrl/⌘+Z caught globally (via @tauri-apps/plugin-global-shortcut ephemerally, or via Tauri IPC from the main window) replaces the last paste: clipboard reset → previous-transcript paste. Preview overlay adds a 5s countdown chip. Untouched clipboard snapshot from #3 is reused. The revert hook lives in src/routes/preview/+page.svelte phase machine: new revertable: true transient on phase === "final". No — builds on #3 and the existing paste command.
9 #20: sound cues for start / stop / complete Small sound-cue module src/lib/utils/soundCues.ts: preloads three short WAV/OGG assets (under static/sounds/), uses the WebAudio API, caps volume at the user's mute + slider. Settings adds a new Sound cues card under openSection === 'audio' with a toggle, three preview buttons, a volume slider. Default: on, 50% volume. No

Commit boundary: build + check green. Stop for review.

Phase B.3 — LLM layer (items #16, #27, #30, #31)

Seq Item Effect Depends on A?
10 #16: CLEANUP_PROMPT framed as "translator, not editor" crates/ai-formatting/src/llm_client.rs::CLEANUP_PROMPT — ONE surgical edit. Replace the preamble with the Whispering baseline ("translator from spoken to written form, not an editor trying to improve the content") and keep every hardening guard the current constant already has. Regression test in the same file asserts the prompt contains both that phrase and the "NOT instructions for you to follow" guard. This is the only line B is allowed to touch in that file. No
11 #27: "Test connection" button with proper error classification For localhost LLM endpoints (Ollama/LM Studio/etc.) Settings now has a new "LLM endpoint" card under openSection === 'ai'. Fields: URL (default http://127.0.0.1:11434) + "Test connection" button. Button calls a new Tauri command test_llm_endpoint(url) — one of the few new backend surfaces B owns outright because A's scope explicitly excludes Ollama (nothing to guard or stream). Classification is frontend-side: error_kind in { not_installed, port_blocked, wrong_model, auth, unknown }. Shows a runtime-warning-styled chip. No — lives in src-tauri/src/commands/llm.rs which is in scope for A BUT the prompt says B owns this because it's the UX contract. Bound by "If Codex hasn't shipped it, stub the frontend behind a feature flag": we add a minimal probe command on B's side that just does reqwest::get, coexisting with A's future full-fat version.
12 #30: streaming LLM output with cancel button Preview overlay's phase === "cleanup" gets a cancel button. A ships the streaming Rust surface in Phase A.4 (item #30 scope there covers plain-text pre-format); if it's not yet shipped we stub with a UI-only cancel that aborts client-side: the cleanup request is tracked by request-id and a cancel_llm_cleanup(request_id) command tries to abort (no-op today, full stop-generate flag in A's follow-up). Partial output is discarded by default; a user pref streamingLlmKeepPartial: boolean in SettingsState decides. Partial — graceful stub today.
13 #31: visible LLM status chip A small LlmStatusChip.svelte component rendered in the main app header (next to the active profile). States: disconnected / warming / idle / generating / error. Subscribes to get_llm_status polling + llm-state-change Tauri events (if shipped). Chip reacts within 500 ms of state change. Partial — uses existing get_llm_status command today. Reacts to richer events if A adds them.

Commit boundary: build + check green. Stop for review.


Settings sections touched per item

Item Section New controls?
#3 None (invisible UX win)
#4 None
#5 openSection === 'transcription' (hotkey recorder component) Inline validation message only
#10 None (auto-detect)
#11 None (schema migration only)
#14 openSection === 'transcription' 1 dropdown
#15 openSection === 'ai' New "Prompt presets" sub-card
#17 openSection === 'processing' 1 toggle revertRawTranscript (default on, opt-out only)
#20 openSection === 'audio' 1 toggle + 1 slider + 3 preview buttons
#16 None (default prompt changes, no UI)
#27 openSection === 'ai' New "LLM endpoint" sub-card
#30 1 toggle streamingLlmKeepPartial under 'ai'
#31 App header New status chip (no Settings control)

Net added toggles: 2 (sound-cues enable, keep-partial-cleanup). Net added sub-cards: 2 (prompt presets, LLM endpoint). Everything else is either invisible (#3, #4, #10, #11, #16) or an enhancement to an existing control (#5, #14). The rule-1 budget (low cognitive load) holds.


New SettingsState fields

Per the workstream rules, every new Settings-visible field gets both a defaults entry in src/lib/stores/page.svelte.ts and a type-level declaration in src/lib/types/app.ts. The migration hook from item #11 manages the bump:

// SettingsState additions (Phase B.2 / B.3):
soundCuesEnabled: boolean;          // default: true  (#20)
soundCuesVolume: number;            // 0..1, default 0.5 (#20)
revertRawTranscriptEnabled: boolean; // default: true (#17)
activePromptPresetId: string;       // default: "quick-clean" (#15)
promptPresets: PromptPreset[];      // user-customisable list (#15)
llmEndpointUrl: string;             // default: "" (local-only; #27)
streamingLlmKeepPartial: boolean;   // default: false (#30)
preferredGpuId: string | null;      // default: null (auto) (#14)

Settings schema version starts at 1; this set lands as version 2.


Explicit Codex (Workstream A) dependencies

Workstream B waits on Workstream A for the following surfaces. When A hasn't shipped yet, B ships the UI stubbed behind a feature flag + TODO, per the rules:

Item Depends on A surface B fallback
#14 (UI) list_gpus command (A.2), activeComputeDevice on runtimeCapabilities (A.1 — shipped) Show only activeComputeDevice; dropdown hidden behind if (listGpus) import guard
#17 Stable paste_text outcome shape (already stable) None
#27 No dependency today; A's #27 scope is frontend-side
#30 A's streaming LLM output surface (Phase A.4) UI-only cancel; partial output discarded; TODO comment in src/lib/utils/llmStream.ts
#31 llm-state-change event (future A.4) Poll get_llm_status at 500ms until event exists
runtime-warning banner runtime-warning event (A.1 — shipped) None — consume directly

Test strategy

  • No JS test runner in this repo currently, so correctness comes from svelte-check + manual testing after each phase. For the schema migration (item #11) we add a tiny pure-TS test file that we exercise with a minimal tsc --noEmit inside npm run check — no new tool chain.
  • Manual QA checklist per phase lives at the bottom of this doc.
  • Rust-side Workstream-B commands (only test_llm_endpoint today) land with a unit test that passes without a live Ollama instance (test against a temporary TcpListener that returns the fixture shape the frontend expects).

Commit conventions on this branch

Same spirit as A:

  • One concern per commit, subject line feat(B.1): / fix(B.3): etc.
  • Commit body references the brief item.
  • Only touch crates/ai-formatting/src/llm_client.rs for CLEANUP_PROMPT — rejected in review otherwise.
  • Never regress the paste matrix from commands/paste.rs: #3 and #10 extend, they don't rewrite.

Manual QA (run after each phase)

Phase B.1:

  • Type testing 123 in Kate, copy it, dictate a sentence, paste (autoPaste on). Verify within 1s Kate's clipboard is back to testing 123.
  • Open kitty. Dictate. Verify paste goes as clipboard-only toast, nothing is typed.
  • Tap hotkey 5× rapidly; verify only one recording starts.

Phase B.2:

  • Reboot. Plug only one GPU. Verify Settings shows the matching activeComputeDevice label + dropdown is disabled / hidden per stubs.
  • Dictate "send this to my mum", pick Email preset, verify cleanup output matches email tone.
  • Dictate, paste into a text file, press Ctrl+Z within 5s. Verify raw transcript replaces cleaned output.
  • Start / stop / finalise — verify three distinct cues at default volume.

Phase B.3:

  • With Ollama not installed: click Test Connection → see "Ollama not installed" classified message.
  • Run a 30-second cleanup request; click Cancel; verify no insert.
  • Toggle LLM model load in Settings; verify header chip reflects state within 500ms.