Commit Graph

182 Commits

Author SHA1 Message Date
1c4ac98504 agent: lumotia — Phase B.10 pin focusTimer expired-rehydrate startTick invariant
Phase B.10 audit of commit 5ba761a (focusTimer rehydrate startTick
invariant; Race-10). The commit landed a comment-only change at
src/lib/stores/focusTimer.svelte.ts lines 204-208:

  // startTick() is REQUIRED here: tick() is the only thing that
  // observes `now >= completionFlashUntil` and calls clear(). Without
  // it, the completion flash would stay visible until the user
  // interacts with the app. stopTick() runs via clear() once the
  // flash window elapses.

A future edit could silently drop the startTick() call (the commit
acknowledged this with the comment) and the regression would only
surface on a real session: the user reopens Lumotia after a closed
expired timer, sees the completion flash, and waits for it to clear.
It never does. They click somewhere → clear() fires from the click
handler. Visible bug, but no automated gate.

The 5ba761a commit could not add a test at the time it landed because
vitest wasn't wired in the workspace — vitest scaffold landed in
commit 206ac62 the next day as Phase A.5. Now that vitest exists
(jsdom environment, .svelte.ts rune transformer, fake timers via
vi.useFakeTimers — see vite.config.js test block), the invariant is
straightforwardly testable.

Fix: new src/lib/stores/focusTimer.test.ts. The single test
`auto-clears the completion flash after the 3s window via the tick loop`:

  1. Seeds localStorage with a timer started 60 s ago that lasted only
     30 s — already-expired by 30 s when rehydrate runs.
  2. Calls focusTimer.rehydrate(). Asserts the flash is now visible
     and the timer is reported active.
  3. vi.advanceTimersByTime(3_500) — pushes wall-clock past the
     completionFlashUntil mark, drives the setInterval ticks.
  4. Asserts showingCompletionFlash is false, active is false,
     remainingMs is 0, and localStorage has been wiped.

If a future edit removes the startTick() call on the already-expired
branch of rehydrate(), step (3) won't run any ticks, the flash won't
clear, and step (4) fires the regression assertion. The test pins the
invariant the comment alone could not.

Verification:
  * npm run test → 13/13 (was 12, +1 from this commit). Both test
    files pass: localStorageMigration.test.ts (unchanged, 12 tests)
    and the new focusTimer.test.ts (1 test).
  * npm run check → 0 errors / 0 warnings across 4015 files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 20:25:32 +01:00
206ac6219d agent: lumotia — Phase A.5 vitest scaffold + localStorageMigration unit tests
First frontend unit test framework on Lumotia. Pinned exact versions for
supply-chain hygiene (matches the rust-toolchain.toml discipline from the
27661c8 hygiene pass and the npm audit signatures pre-flight from e4d56b8):

  - vitest 4.1.6 (compatible with vite 6, supports vite 6/7/8)
  - jsdom 29.1.1

Installed with `npm install --save-dev --save-exact --ignore-scripts` per
the install discipline documented in the README — the --ignore-scripts
flag blocks the postinstall vector that npm worms (Shai-Hulud,
mini-Shai-Hulud) rely on.

vite.config.js:
  - Switched defineConfig import to vitest/config (superset of vite/config;
    production builds ignore the `test` key).
  - test.environment = "jsdom" so storage-shim tests drive real browser APIs.
  - test.include scoped to src/**/*.{test,spec}.{ts,js} — colocated with
    source, mirrors the Rust #[cfg(test)] sibling pattern.
  - test.exclude blocks src-tauri/ (owned by cargo test).
  - restoreMocks + clearMocks + unstubAllGlobals on so module-level state
    can't leak between tests.

src/lib/utils/localStorageMigration.test.ts — 12 tests:
  migrateLocalStorageKey:
    - copies value + removes old when only old exists
    - removes old + keeps new when both exist (lumotia is authoritative)
    - no-op when only new exists
    - no-op when neither exists
    - idempotent (second call after first migrates nothing)
    - preserves the value's exact bytes (no JSON round-trip)
    - preserves empty-string values (distinct from null)
    - survives DOMException / quota errors without re-raising
    - no-op when localStorage is undefined (SSR-safe)
  migrateLocalStorageKeys:
    - processes pairs in order
    - per-pair failure does not strand remaining pairs (resilience)
    - empty pairs list is a clean no-op

package.json:
  - "test": "vitest run" (one-shot, CI-friendly)
  - "test:watch": "vitest" (dev loop)

README: documents `npm run test` alongside `cargo test --workspace` and
`npm run check` in the Testing section.

Verification:
- npm run test: 12/12 pass
- npm run check: 0 errors, 0 warnings (the new .ts test type-checks clean
  against jsconfig.json's strict typescript settings)
2026-05-14 07:29:57 +01:00
87e6248774 agent: code-atomiser-fix — trash view + restore button in HistoryPage (Rev-2 UI)
Adds a Live | Trash toggle to the HistoryPage header. The Trash tab
lists soft-deleted transcripts via the new list_trashed_transcripts
Tauri command and offers per-row Restore via restore_transcript.

Permanently-delete-from-trash is intentionally deferred — the 30-day
startup purge (TRANSCRIPT_TRASH_RETENTION_DAYS in src-tauri/src/lib.rs)
handles hard-removal until that UI lands. The retention policy is
surfaced in the panel header: Trashed items are kept for 30 days,
then permanently deleted on next app start.

Design notes:
  - View toggle is a tablist of two pill buttons (Live | Trash); the
    aria-selected attribute reflects the active mode.
  - Switching to Trash triggers an effect that loads the list once.
    Switching back to Live discards the trash data so stale rows
    don't reappear on toggle.
  - Live-mode header controls (Starred, Tag all untagged, Clear All)
    are hidden in Trash mode and replaced with a Refresh button.
  - Restore drops the row from the in-memory trash list rather than
    splicing into history; the live store reloads from SQLite on its
    own initialisation path, so we avoid drifting the in-memory shape
    from the canonical source.
  - The created timestamp is shown rather than the deletion
    timestamp; deleted_at is not currently in the TranscriptDto and
    expanding the DTO is out of scope for this fix.
  - Audio files may already have been removed by delete_transcript's
    best-effort filesystem cleanup, so restored text + metadata may
    surface without playable audio (documented in the storage-layer
    restore_transcript contract).

TODO(test): no Svelte component test framework wired in the repo
(vitest not installed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 18:04:39 +01:00
50d0715488 agent: code-atomiser-fix — type-the-word DELETE modal for clearAll (Rev-2 UI)
The prior clearAll UX was a 4-second inline arm-confirm: one click on
Clear All morphed into Confirm/Cancel pills; a second click within
the window wiped every transcript. With the soft-delete backend
(commit 15b74db) now under it, the data-loss class is partially
closed — items land in Trash, not /dev/null — but an absent-minded
double-tap still soft-deletes the entire history at once.

Fix: replace the inline arm-confirm with a modal that requires the
user to type the word DELETE (case-sensitive, exact) before the
Confirm button activates. The single-transcript and bulk-selection
delete flows keep their lighter arm-confirm pattern; only the
all-at-once nuke is gated by the modal.

Modal details:
  - autofocuses the input on open
  - Enter submits when the word matches
  - Escape closes; click-outside closes (when not in flight)
  - Confirm button disabled until input == DELETE
  - Clearing... spinner state while the SQLite soft-delete loop runs
  - retention policy (kept for 30 days) surfaced in the body copy
  - dialog role, aria-labelledby, aria-describedby, labelled input,
    tabindex=-1 on the card; backdrop carries role=presentation to
    keep the click-outside-to-close affordance out of the AT tree

clearAllArmed / armClearAll / disarmClearAll and their announcement
fragment are removed; bulkDeleteArmed (selection-subset) keeps the
arm-confirm pattern unchanged.

TODO(test): no Svelte component test framework wired in the repo
(vitest not installed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 18:01:10 +01:00
a2b47db193 agent: code-atomiser-fix — restrict write_text_file_cmd to app data + download dirs (Trust-1)
The Tauri command `write_text_file_cmd` took an arbitrary `path: String`
and flowed it straight into `tokio::fs::write`, with no main-window
guard, no canonicalisation, and no scope check. A compromised webview
could write anywhere the process had write access — overwriting shell
init files, dropping a runner into `~/.config/autostart`, etc. The
in-file comment "the dialog already constrains the user's choice"
described an intended invariant the IPC surface never enforced.

This change:

- adds `ensure_main_window(&window)?` so only the main webview can
  invoke the command;
- canonicalises the requested path's parent (rejecting nonexistent
  parents and resolving symlinks) before joining the filename;
- asserts the canonical target sits inside an allowlisted base
  (app data, app local data, downloads, documents, desktop), so a
  `"../../etc/passwd"` payload — even one obtained by symlink trickery
  in the chosen save dir — is refused with a clear error.

Six unit tests cover: outside-allowlist rejection, path-traversal
rejection, nested inside-allowlist acceptance, plain inside-allowlist
acceptance, nonexistent-parent rejection, and the pure
`is_inside_any_base` prefix check.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 17:53:17 +01:00
5ba761a4b8 agent: code-atomiser-fix — focusTimer rehydrate startTick invariant (Race-10)
Lock the invariant: the already-expired branch of rehydrate() must call
startTick() so the tick loop observes now >= completionFlashUntil and
auto-clears the flash. The call was already present; this commit adds
the inline comment so future edits cannot silently drop it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:12:10 +01:00
6aa6a434fb agent: code-atomiser-fix — FirstRunPage unlisten on all exits (Race-9)
Move both Tauri listen() calls inside the try block with let-declared
unlisten handles. A throw on the second listen() previously leaked the
first subscription and the finally block could itself throw on an
undefined unlistenParakeet, masking the original error. Finally now
guards each handle before invoking it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 15:12:06 +01:00
ab5f6ab995 agent: code-atomiser-fix — repair sed-scar provenance from 26c7307
The Magnotia->Lumotia rebrand commit 26c7307 ran a too-greedy
s/magnotia/lumotia/g across comments that already had the correct
legacy/target distinction. The commit author caught one case in
src-tauri/src/lib.rs ("magnotia era" -> "lumotia era" restored) but
missed four others, plus a duplicated word in a Cargo description
from an earlier two-name sed.

Fixed sites:
  * crates/storage/src/database.rs        — migrate_legacy_setting_keys docstring
  * src/lib/utils/localStorageMigration.ts — file-level docstring
  * src/lib/utils/settingsMigrations.ts    — historical-key comment
  * crates/cloud-providers/Cargo.toml      — description duplication
  * src-tauri/src/lib.rs                   — data-dir migration log + doc

None of the underlying code paths were wrong; the lies were
docstring-only. Risk: future maintainer reading any of these
comments at incident time could invert the migration direction or
conclude no migration ran.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 14:27:24 +01:00
f093d18a5e agent: lumotia-rebrand — final residuals (HANDOVER + arch-map readme + ui-kit React fn)
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
Phase 15.1 final-grep residuals:
- HANDOVER.md "Rebrand note" + Phase 10b row updated to reflect that
  the cascade completed 2026/05/13 (15 phases, both repos, QC-gated).
- HANDOVER.md two surviving sed artefacts: "Lumotia -> Lumotia" line
  restored to "Magnotia -> Lumotia" historical context;
  MAGNOTIA_LLM_TEST_MODEL test gate -> LUMOTIA_LLM_TEST_MODEL.
- src/design-system/ui_kits/index.html: MagnotiaApp React function ->
  LumotiaApp (sed boundary missed the no-separator boundary).
- docs/architecture-map/README.md: MAGNOTIA_LLM_TEST_MODEL doc note.

Preserved (audit trail):
- docs/handovers/ — historical handover docs.
- docs/superpowers/plans/2026-05-12-engine-slop-residuals.md and
  -area-a-storage-errors-survey.md — describe the slop-pass work
  using the names current at the time.
- build/index.html, package-lock.json — regenerate on next build/install.

cargo test --workspace: 339 pass / 0 fail. npm run check: 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 13:19:34 +01:00
26c7307607 agent: lumotia-rebrand — docs, scripts, root config, residuals
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
Phase 9 of the rebrand cascade. Sweep covers everything the Phase 8
frontend pass deliberately skipped: docs/, root markdown, scripts,
Cargo.toml descriptions, code comments that survived earlier
word-boundary sed, plus a handful of identifiers caught on the final
verify pass.

transcription-app changes:
- README.md, HANDOVER.md, KNOWN-ISSUES.md, run.sh — magnotia/Magnotia
  -> lumotia/Lumotia.
- docs/ — sweep across all subdirs except docs/handovers/ (preserved
  as immutable audit trail). Includes architecture-map references
  to magnotia_core::*, magnotia_storage::*, etc. now pointing at
  lumotia_*; dev-setup.md tracing output examples (lumotia_startup
  target); brief/ + superpowers/ + issues/ + whisper-ecosystem/ +
  audit/.
- Cargo.toml descriptions on 9 crates (core, audio, cloud-providers,
  hotkey, llm, mcp, plus referenced others).
- crates/core/src/{error,hardware,recommendation,paths}.rs +
  crates/audio/src/wav.rs + crates/llm/src/model_manager.rs +
  crates/cloud-providers/src/keystore.rs + crates/mcp/src/lib.rs —
  doc comments and a model-manager user-agent string.
- Caught on final pass: BroadcastChannel("magnotia_task_sync") -> ...
  ("lumotia_task_sync"); magnotia_locale i18n localStorage key
  renamed + migration shim added; CSS keyframe names
  magnotiaPulse / magnotiaBar / magnotiaFade renamed in the design-
  system kit; magnotia_viewer_item / magnotia_viewer_mode handoff
  keys renamed in HistoryPage + viewer/+page.svelte; src/assets/
  wordmark.svg text.
- src-tauri/src/lib.rs comment cleanup ("magnotia era" was sed'd
  to "lumotia era" earlier — restored).

Preserved (intentional):
- crates/core/src/paths.rs — keeps "magnotia" / "Magnotia" / ".magnotia"
  legacy detection strings in legacy_and_target_paths() so the
  migration shim can still find user data from the magnotia era.
- src/lib/stores/{page,focusTimer}.svelte.ts + src/lib/i18n/index.ts
  — migration call sites reference the legacy magnotia keys
  deliberately.
- docs/handovers/ — historical audit trail.

cargo build --workspace passes. npm run check: 0 errors / 0 warnings
(3958 files). cargo test --workspace: 339 pass / 0 fail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:38:03 +01:00
681a9b26dc agent: lumotia-rebrand — frontend strings (svelte + i18n + design-system)
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
Phase 8 of the rebrand cascade. Every rendered string is now Lumotia;
no Magnotia surface visible in the UI.

Sweep replaced \bmagnotia\b -> lumotia and \bMagnotia\b -> Lumotia
across all .svelte / .ts / .js / .css / .html / .json (excluding
package-lock.json which regenerates, target/, build/, node_modules/).

Surfaces touched:
- src/app.css — design-token comment header and .magnotia-rh-* CSS
  resize-handle class selectors (also the consuming elements in
  components/ResizeHandles.svelte and src/routes/*/+layout.svelte).
- src/lib/i18n/locales/{en,de,es}.json — brand name in translations.
- src/lib/i18n/index.ts — header comment.
- src/lib/Sidebar.svelte and most pages under src/lib/pages/ +
  src/lib/components/ — title bars, document titles, default
  filenames (lumotia-YYYY-MM-DD.* etc), toast strings, error
  messages, dialog headers.
- src/routes/+layout.svelte, +page.svelte, viewer/, float/, preview/.
- src/app.html page <title>.
- src/lib/utils/settingsMigrations.ts — fallback toast copy.
- src/design-system/{colors_and_type.css,SKILL.md,README.md,
  ui_kits/{Sidebar.jsx,index.html}} — design-tokens, doc strings,
  preview wordmark in the kit.
- package.json — name + description.

NOT touched (deferred / immutable):
- package-lock.json — regenerates on next npm install.
- The two migration-call sites in stores reference the legacy magnotia
  keys deliberately; restored after the sweep clobbered them.
- docs/, README.md, HANDOVER.md — Phase 9 scope.

npm run check: 0 errors / 0 warnings (3958 files).
cargo test --workspace: 339 pass / 0 fail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:29:37 +01:00
16081095e0 agent: lumotia-rebrand — localStorage keys + event channels migration
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
Phase 7 of the rebrand cascade. Persisted UI state + inter-window event
channels migrated from magnotia to lumotia naming, with one-shot
localStorage key migration so dogfooded UI state survives the rename.

src/lib/utils/localStorageMigration.ts (new):
- migrateLocalStorageKey(old, new): idempotent + crash-safe shim.
  - If new key exists, removes old (lumotia value is authoritative).
  - If only old exists, copies value to new key, removes old.
  - If neither, no-op.
- migrateLocalStorageKeys(pairs): batch wrapper.

src/lib/stores/page.svelte.ts:
- 4 key constants renamed to lumotia_settings / lumotia_profiles /
  lumotia_task_lists / lumotia_templates.
- BroadcastChannel name renamed to lumotia_task_lists.
- migrateLocalStorageKeys() called at module load before any read.

src/lib/stores/focusTimer.svelte.ts:
- STORAGE_KEY renamed to lumotia.focusTimer.v1.
- migrateLocalStorageKey() called at module load.

Event channels (magnotia: -> lumotia:) renamed across frontend + Rust:
- magnotia:toggle-recording (src/routes/+layout.svelte)
- magnotia:hotkey-pressed / -released (src-tauri/src/commands/hotkey.rs +
  consumers)
- magnotia:open-wind-down (src-tauri/src/tray.rs + consumer)
- magnotia:llm-download-progress (src-tauri/src/commands/llm.rs)
- magnotia:preferences-changed (src/lib/stores/preferences.svelte.ts +
  consumers)
- magnotia:start-timer (nudgeBus + dispatch sites)
- magnotia:focus-timer-{complete,cancelled} (focusTimer + nudgeBus)
- magnotia:microstep-generated (nudgeBus + dispatch sites)
- magnotia:step-completed (nudgeBus + dispatch sites)
- magnotia:task-{completed,uncompleted,deleted} (page.svelte.ts +
  nudgeBus + consumers)

Storage-event filters in src/routes/{float,viewer,preview}/+layout@.svelte
updated to filter on lumotia_settings.

User-facing toast strings still say "Magnotia" — deferred to Phase 8
(frontend strings).

npm run check: 0 errors / 0 warnings (3958 files).
cargo test --workspace: 339 pass / 0 fail.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:10:50 +01:00
bc2db91520 agent: lumotia-rebrand — fix QC blockers for phase 0
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
Phase 0 QC found two real blockers in the baseline commits:

1. crates/llm/src/lib.rs:215 — early-break guard was inverted. The
   guard fired when grammar.is_some() (where grammar already enforces
   shape, making the check redundant) and was disabled for
   grammar.is_none() (the content-tags path that actually needs it).
   Flipped to grammar.is_none() so the JSON-envelope short-circuit
   fires for free-form JSON generation as the commit message
   implied.

2. src/lib/pages/FilesPage.svelte — handleExport() success path had
   no toast, asymmetric with DictationPage which does. Added the
   toasts import and a toasts.success() call after the native save
   so both export surfaces give consistent feedback.

cargo test -p magnotia-llm --lib: 21 pass. npm run check: 0 errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 08:42:22 +01:00
2ca01e7c9d feat(export): prefer native save dialog over browser blob fallback
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
DictationPage + FilesPage handleExport() now use Tauri save() +
write_text_file_cmd when in the desktop runtime; browser blob path
remains as fallback when hasTauriRuntime() is false. saveMarkdown.ts
surfaces dialog errors via toasts. Adds txt to the extension map.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 08:26:19 +01:00
jars
fdf27db0a1 perf+fix: DMABUF default on Linux, popout ACL fixes, plugin version sync, JFK bench fixture
Bundled work from a low-end laptop (Ryzen 5 4650U / Vega 6 / Linux Mint
22.2 / X11) profiling pass.

perf: WEBKIT_DISABLE_DMABUF_RENDERER=1 default on all Linux
  Previously only set on Wayland sessions. Empirically it's a
  significant idle-cost win on integrated GPUs in either session type:
  env-var matrix (release binary, 75s settle, 10s jiffies CPU sample)
  showed magnotia idle CPU 12.30% → 2.80% of one core and idle GPU
  17% → 10% on this hardware. Users can opt back in by exporting
  WEBKIT_DISABLE_DMABUF_RENDERER=0. The Wayland-only XWayland
  fallback (GDK_BACKEND=x11, WINIT_UNIX_BACKEND=x11) is unchanged.

fix: secondary-windows ACL — allow set-always-on-top
  The float window's pin toggle calls setAlwaysOnTop() but the
  secondary-windows capability didn't permit it, so the popout was
  stuck always-on-top regardless of the pin state. Adds the
  core:window:allow-set-always-on-top permission. Narrow scope.

fix: guard registerGlobalHotkey against non-main webviews
  Cross-window settings sync via localStorage can re-fire the
  $effect(() => settings.globalHotkey) callback inside popout webviews
  where the main layout's registerGlobalHotkey is reachable. Adds an
  early-return when the current window label is not "main", so the
  popout doesn't trigger an ACL-denied register/unregister and the
  user no longer sees a spurious "Hotkey not registered" toast when
  popouts are open. Keeps the global-shortcut perm scoped to main.

build: pin @tauri-apps/api 2.10.1 + @tauri-apps/plugin-dialog 2.7.1
  Match the Rust crate versions tauri-cli's version-mismatch check
  enforces during release builds. Without this, `npm run tauri build`
  exits 0 silently while emitting an Error and never producing
  binaries.

test: add crates/transcription/tests/jfk_bench.rs
  Reproducible RTF regression fixture. Env-gated on
  MAGNOTIA_WHISPER_TEST_MODEL + MAGNOTIA_WHISPER_TEST_AUDIO so it
  never runs in CI without setup. Loads the JFK WAV inline (no hound
  dep), times model load + cold + warm transcribe, prints SUMMARY.
  Baselines on this hardware:
    --release --features whisper:               cold RTF 0.054, warm 0.050, RSS 125 MB
    --release --features whisper,whisper-vulkan: cold RTF 0.029, warm 0.028, RSS 125 MB
  Vulkan on RADV/Vega 6 nearly halves transcription latency for
  Whisper Tiny — useful baseline for Phase 10 hardware-recommendation
  scoring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 09:09:03 +01:00
2dfd7167fd fix(a11y): Toggle aria-invalid briefly when async onChange rejects
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
When an async onChange handler rejects, the toggle snapped back to its
previous value silently — screen reader users had no signal that the
action failed, only that the toggle suddenly returned to its original
state. Now aria-invalid="true" exposes the failure on the role=switch
button for 1.5s after rejection, so assistive tech announces the error.
Self-clears via setTimeout; no API change for callers.
2026-05-07 11:31:47 +01:00
aecb191705 refactor(theme): tokenise hard-coded amber shadows and reconcile duplicate @font-face declarations
Hard-coded rgba(214,132,80,X) shadows were tied to the dark-theme accent
and stayed off-hue in light mode (where the accent is a darker #a3683a).
Six tokens added to app.css @theme and mirrored in the light-theme block
plus design-system/colors_and_type.css:

  --shadow-accent-sm     0 0 8px   0.25 alpha  Toggle on-state
  --shadow-accent-md     0 4px 16px 0.3       ModelDownloader card
  --shadow-accent-lg     0 4px 20px 0.3       DictationPage record button
  --shadow-accent-glow   0 0 8px   0.4        progress-bar glow
  --shadow-accent-pill   0 1px 4px 0.3        SegmentedButton pill
  --shadow-accent-raised 0 2px 8px 0.2        FilesPage browse tile

Migrated seven sites: Toggle, SegmentedButton, ModelDownloader (x2),
FilesPage (x2), DictationPage record button, SettingsPage locale pill.
Focus-ring shadows (0_0_0_3px_rgba(...,0.1)) left alone — handled in
parallel via the --accent-shadow-focus token.

@font-face: app.css and colors_and_type.css both declare the same five
fonts. Duplication is unavoidable because preview/*.html loads the
design-system file directly without Vite, so it cannot share the
runtime declarations. font-weight ranges and font-style reconciled to
match exactly (Atkinson 200-800 not 400-700, JetBrains gains font-style:
normal); src URLs intentionally differ (root-absolute vs relative).
Comments added to both copies explaining the contract.
2026-05-07 11:31:14 +01:00
f4c0635549 fix(a11y): guard sidebar-toggle keypress against custom widgets and scope transcript font-size effect
- Extend isInputFocused to also bail when document.activeElement sits
  inside a custom widget with role combobox/listbox/radio/switch/menuitem/tab
  so single-letter shortcuts like '[' do not collapse the sidebar while
  focused on SegmentedButton, ZonePicker, etc.
- Move the transcript font-size CSS variable write from documentElement
  to document.body. Consumers inherit body styles, and scoping the write
  there avoids invalidating root-level styles every time fontSize changes.
2026-05-07 11:27:42 +01:00
736896c2b8 fix(a11y): announce HistoryPage inline-confirm armed state and tag-chip pressed state 2026-05-07 11:24:53 +01:00
9b17425bc9 fix(a11y): name FilesPage drop zone region and denest Card 2026-05-07 11:23:55 +01:00
d3554951e0 fix(a11y): privacy badge role/label and stronger search focus ring in light theme 2026-05-07 11:23:23 +01:00
47b005ad70 fix(a11y): bump record button to 48x48 and announce Recording stopped to screen readers
Record button bumped from 40x40 to 48x48 (WCAG 2.5.5 AA requires 44x44; 48 chosen as forgivable oversize for the most-touched control). Inner stop-square and record-circle bumped 14x14 to 16x16, Loader2 16 to 18, to keep the visual ratio.

Recording-status sr-only live region now announces both transitions: "Recording started" on start and "Recording stopped" on stop, with the stop message clearing after 1200ms so the live region does not hold stale text. Transition tracked via a plain let prevRecording (not $state) since it is bookkeeping and must not retrigger the effect. aria-atomic="true" added so the whole region re-reads on update.
2026-05-07 11:20:35 +01:00
ca3a55320b fix(a11y): bump all text below 12px to 12px floor and promote tertiary-on-body to secondary
Brand guidelines (docs/brand/magnotia-brand-guidelines.md §4) say "Never go
below 12px for any text" and "tertiary text must be ≥18px bold or ≥24px
regular". Audit found 246 sub-12px className sites and 166 cases of
text-text-tertiary paired with body sizes.

Bumped text-[9/10/11px] → text-[12px] across 26 .svelte files in src/lib
and src/routes. Promoted text-text-tertiary → text-text-secondary at body
sizes (12-14px) where context allowed; left placeholder pseudo-states,
ternary-conditional inactive states, and line-through "done" states alone
(8 residual pairings, all decorative).

Affects neurodivergent users on 1366×768 budget hardware most directly.
svelte-check: 0 errors, 0 warnings.
2026-05-07 11:18:10 +01:00
6da15d1ac8 feat(ui): wire deferred Card tone and elevation variants
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
2026-05-07 10:53:30 +01:00
9ba7babcb0 feat(settings): add quick-settings row for theme, font size, and hotkey 2026-05-07 10:52:51 +01:00
5778696140 feat(dictation): surface "Talk now, think later" empty state and hide post-transcript actions
Reduces first-run cognitive load. Save / Copy / Clear / Export now appear only
once the user has a transcript or is recording, sliding in via animate-fade-in
(brand --duration-ui + ease-out-quart). Template and Extract Tasks remain
always-visible because they shape transcription rather than acting on completed
content.

Empty state now leads with the catchphrase as a brand statement
(font-display italic 28px) with the hotkey hint demoted to body-font tertiary.
EmptyState gains an optional headline prop; callers without it render unchanged.
2026-05-07 10:49:37 +01:00
d2f64a231d feat(theme): bump accent chroma, push sensory zones apart, richer status tokens
Phase 10b colour push: theme reads as Magnotia, not "subdued generic dark".
Same lightness band as Phase 10a (AA preserved), more chroma across accent,
status tokens, and zone surfaces.

Dark accent: #c98555 -> #d68450 (subtle/hover/glow recomputed).
Light accent: #a06a3e -> #a3683a.
Dark status: success #7ec89a -> #5fc28a, danger #e87171 -> #e85f5f,
warning #e8c86e -> #e8be4a.
Light status: success #2f7549 -> #1f7344, danger #a83838 -> #b32626,
warning #b89a3e -> #a08a1f.

Sensory zones pushed further apart so each zone reads as its own
environment: cave cools toward blue-grey, energy warms toward red-orange,
reset cools toward green. AA on body text verified for every Card surface
in both themes.

Stale accent rgba (201,133,85) replaced with new (214,132,80) across nine
files. --shadow-accent in colors_and_type.css mirrored.

No secondary tint token added: Magnotia's identity is amber-as-the-only-
meaningful-colour. Cheaper to add a second meaningful colour later if a
real need appears than to dilute the discipline now.

Default surfaces, text tokens, and borders untouched per scope.
2026-05-07 10:46:35 +01:00
ec09f8ffdb feat(settings): finalise polish — autostart Toggle dedupe, AI Default/Advanced split, vocab consistency, model ETA, privacy badge, group icons
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
Six changes, one coherent polish pass on SettingsPage:

1. Autostart toggle dedupe. The 27-line custom button that mirrored
   Toggle's markup to dodge a bind:checked race is gone. Toggle's new
   async onChange + snap-back covers the OS round-trip; setLaunchAtLogin
   re-throws on failure so Toggle reverts checked. autostartSyncing
   state retired.

2. AI Assistant Default/Advanced split. Cleanup preset and GPU
   concurrency moved into a nested SettingsGroup title="Advanced",
   closed by default. The Assistant page now leads with four
   first-decision controls (tier, model picker, status, prewarm) and
   parks the tuning knobs behind a disclosure. Search keywords cascade
   from the Advanced group up to AI Assistant up to AI & Processing.

3. Vocabulary disclosure consistency. The hand-rolled Profiles and
   Templates sub-panels (with their own ChevronRight buttons +
   showProfiles/showTemplates state) are now nested SettingsGroups.
   The count badges live in the description slot ("3 profiles ·
   custom vocabulary..."). showProfiles, showTemplates, and the
   ChevronRight import retired.

4. Model-download ETA in Settings. Both whisper and LLM download
   chips now show "{percent}% · {bytes} / {total} · {duration} left",
   reusing formatDuration from utils/time.ts. New formatBytes +
   etaSecondsFromPercent helpers in SettingsPage; bytes pulled from
   the existing emit payload (DownloadProgress snake_case for whisper,
   done/total for the LLM event). FirstRunPage already does the
   timestamp-based ETA; we mirror its shape.

5. Privacy badge in sticky header. Single chip "100% local · no
   telemetry · no cloud" added next to the Settings heading using
   bg-accent-subtle + text-text-tertiary. The longer About list
   stays put as the deep-dive.

6. SettingsGroup icons on the seven top-level groups. Mic, BookOpen,
   Type, Sparkles, SquareCheck, Clipboard, Sliders — paired with the
   existing literal text titles per the icon-with-label brand rule.
   Inner nested groups stay icon-free.

Verification: npm run check returns 0 errors, 0 warnings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 10:19:26 +01:00
aa1ba48e07 feat(card): add tone (subtle, danger) and elevation (raised) variant props
Both props default to current behaviour, so all existing call sites render
unchanged. Subtle uses bg-bg-elevated for quieter empty-state contexts;
danger uses bg-danger/5 + border-danger/30 for calm caution panels (not
red-alert loud, matching brand). Raised layers shadow-md on top of the
existing low-contrast shadow for floating or detached surfaces.
2026-05-07 10:09:14 +01:00
3a27031eba feat(settings-group): add optional icon prop and tighten title/description rhythm
Title bumped to font-semibold so it scans first; description gets tracking-wide
plus mt-1 for cleaner rhythm. Chevron alignment shifted from mt-0.5 to mt-1 to
match the new spacing. Optional `icon` prop renders a Lucide component between
the chevron and the title block at 16x16, text-tertiary, no colour change on
open. Layout is preserved exactly when no icon is passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 10:07:39 +01:00
aebf6cd149 feat(toggle): add disabled, loading, and async onChange props
Extends the shared Toggle with three optional props while keeping the
existing bind:checked path untouched for current call sites.

- disabled: dims the control, sets aria-disabled, and early-returns on click.
- loading: forces a Loader2 spinner inside the thumb, applies cursor-wait
  and aria-busy, and blocks interaction. Cursor-wait wins over disabled's
  not-allowed when both are set, matching loading-precedence guidance.
- onChange(next): when supplied, replaces the immediate flip. Promise
  returns drive an internal pending flag so the toggle renders loading
  while the handler resolves; on resolve checked flips, on reject the
  toggle snaps back. Synchronous handlers flip immediately after the call.

Brand motion preserved: 150ms duration, cubic-bezier(0.2, 0.8, 0.2, 1),
no transition-all.
2026-05-07 10:06:06 +01:00
5e3a9f9f42 fix(ui): promote modal overlay to token, lift modal above grain, dedupe FilesPage drop-zone
- Add --color-overlay-dim token to app.css @theme (rgba(15,14,12,0.7) dark, rgba(26,24,22,0.55) light); mirror as --overlay-dim in design-system/colors_and_type.css.
- MorningTriageModal: replace inlined rgba scrim with var(--color-overlay-dim), drop backdrop-blur-sm (brand opposes glassmorphism by default), bump container to z-[60] so it sits above the .grain z-50 overlay.
- FilesPage: collapse two near-identical drop-zone blocks into one bordered region with conditional inner content; preserves isDragOver hover behaviour and aria region.
2026-05-07 10:04:30 +01:00
13af425dfa fix(hotkey): surface Esc-to-cancel hint and replace recording-keyframe reuse with success flash
Two scoped fixes to HotkeyRecorder:

- Append "Esc to cancel." to the recording prompt so the existing Escape
  handler is discoverable. srStatus already mentions Escape; no harmonisation
  needed.
- Replace animate-pulse-warm on the captured state with a static
  bg-success/10 + border-success/40 flash. pulse-warm is the
  recording-active keyframe; reusing it for the saved state muddled the
  visual vocabulary. The global * transition rule already animates
  background-color and border-color over --duration-ui (150ms) with the
  brand cubic-bezier ease, so no new keyframe is needed and prefers-reduced-motion
  is honoured automatically. Also swapped transition-all for transition-colors
  to comply with the no-transition-all brand-motion rule.
2026-05-07 10:02:08 +01:00
a4cca58289 fix(history): replace native confirm() with inline two-click confirmation
Native window.confirm() is OS-styled and breaks the warm-amber UI
tone with an abrupt brutalist modal. The brand voice is "calm,
informative, solution-first; never blame the user", and confirm() is
none of those.

Replaced both destructive paths (Clear All, Delete N selected) with
a two-click inline pattern: first click arms the trigger and morphs
it into "Delete X? [Confirm] [Cancel]" inline pills; auto-cancels
after 4s so a user who walked away doesn't return to a primed delete
button. No new modal, no new component, no new dependency.

Both timers are cleared in onDestroy so unmounting the page mid-arm
doesn't leak.
2026-05-07 09:21:04 +01:00
e857a814ad fix(ui): remove side-stripe borders in favour of subtle bg tints
Side-stripe borders (border-left: 2px or 3px in an accent colour, with
the rest of the element having no border) are the textbook AI-coded-IDE
"selected-state" pattern. The brand vocabulary is colour-tint and
chevron, not stripe-and-fill.

Replaced eight sites:
- VirtualSegmentList active/match/default segment rows: bg-accent/10,
  bg-warning/10, hover:bg-hover (drop the stripes; bg-warning bumped
  /5 → /10 so it's visible without the stripe doing the work).
- viewer/+page.svelte: same pattern as VirtualSegmentList.
- TasksPage profile-list tabs: bg-accent/10 + font-medium for active,
  hover:bg-hover for inactive. rounded-md added so the tint follows
  the row outline.
- ToastViewport: replaced border-left + variant colour with full 1px
  border + bg tint + border-color tint (color-mix() with the matching
  semantic token at 35% / 10%). Also removed the orphan --moss /
  --signal / --ember tokens — they were not defined in app.css and
  fell back to hex literals.
- preview/+page.svelte: dropped the phase-coloured left stripe and
  the borderColorClass derivation; the header already has a pulsing
  dot / animated bars / spinner for each phase, so the stripe was
  redundant.
2026-05-07 09:18:54 +01:00
360e5457dc fix(copy): replace em-dashes in user-facing strings with commas/periods
Per the project's no-dashes feedback rule (see CORBEL-Main memory
feedback_no_dashes.md), em-dashes in user-visible copy are the single
biggest "AI wrote this" surface tell. Forty-two occurrences across ten
files: ten in SettingsPage's group descriptions and option labels
(audio device picker, vocabulary help, prompt placeholders, model
descriptions), four in FirstRunPage's setup steps, three in
DictationPage / FilesPage / Energy chip tooltips, plus the privacy
bullets ("100% offline, no Python required, no cloud, no accounts,
no telemetry"), the rejection toast, MicroSteps thumb labels, and the
shutdown ritual prompts.

Replacement rule: subordinate-clause em-dashes followed by lowercase
become commas; sentence-break em-dashes followed by capitals become
full stops. Code comments left intact.

One non-em-dash regression caught while reviewing: TasksPage's energy
filter "no selection" placeholder showed "—" as a literal label; the
script changed it to a comma which read as a UI bug. Replaced with
"Any" instead.
2026-05-07 09:16:37 +01:00
637ed89eac feat(settings): ship search filter that opens matching groups
The settingsSearch state was declared in Phase 9c with a comment
describing the intended behaviour but no <input> bound to it. With 21
SettingsGroup instances and a 2,261-line page, finding any specific
control required scrolling and opening every group. For the
neurodivergent audience this is exactly the cognitive-load failure
the brand commits to avoiding.

Added a sticky search input at the top of the page (paired with the
"Settings" title in a single sticky header that bg-bg-matches the
page so groups don't bleed under it). Each SettingsGroup now passes
through searchMatches against its title, description, and a curated
keyword string. Parent groups inherit their children's keywords so a
search for "GPU" expands AI & Processing and the AI Assistant inner
group together.

Updated SettingsGroup to push prop changes to the DOM via $effect:
the native <details> element's open attribute is mutated by user
clicks outside Svelte's reactive graph, so a parent prop change
needs to force-sync. User-driven toggles where the prop value didn't
change keep their local state because $effect only re-runs on diff.

Verified live: baseline shows 3 groups open (Transcription, Terms &
profiles, Capture & export), "GPU" opens AI & Processing + AI
Assistant only (other 19 closed), "ritual" opens Tasks & Rituals +
Rituals only, X-button clear restores baseline.
2026-05-07 09:13:38 +01:00
7a21d0c7fc fix(ui): sync stale shadow rgba from pre-Phase-10a amber to current accent
Eighteen sites across SegmentedButton, Toggle, HotkeyRecorder,
ModelDownloader, FilesPage, and SettingsPage hardcoded the old amber
rgba(232,168,124,...) for focus rings, glows, and progress bars. After
the Phase 10a a11y darkening of --accent to #c98555, the inline shadows
no longer matched the accent fill they were paired with, so every
focused input rendered a glow at a noticeably different hue from its
border.

Replaced all of them with the current accent rgba(201,133,85,...).

Toggle component also had two motion violations: an active:scale-95
press-shrink and a cubic-bezier(0.16,1,0.3,1) (ease-out-expo) thumb
transition. Both replaced with the brand's --ease curve
(0.2,0.8,0.2,1) and the scale dropped, in line with the record-button
fix in the previous commit.

Removed redundant inline transition-duration overrides on FilesPage
Browse button (the global * rule already sets --duration-ui) and on
ModelDownloader's primary CTA (also dropped its active:scale-[0.97]).
2026-05-07 09:07:14 +01:00
a545c7a12d fix(dictation): drop bouncy press motion and update record-button glow rgba
Record button had active:scale-[0.93] transition-all, which contradicts
the brand motion guideline ("never bounce, slow, calm, deliberate"). The
record button is the most-touched control in the app; a 7% press-shrink
on it sets the wrong tone for everything else.

Removed the scale transform, removed the inline transition-duration
override (the global * rule in app.css already animates the named
properties at --duration-ui). Also updated the inline shadow rgba from
(232,168,124,0.3) to (201,133,85,0.3) so the glow tracks the new
a11y-darkened accent fill instead of the pre-Phase-10a amber.
2026-05-07 09:05:23 +01:00
f274611d23 fix(app): scope user-select:none to chrome elements only
Body had user-select:none globally so users could not copy error
strings, model paths, profile names, transcripts, status messages,
or any text into search or another tool. The brand essence is
"clarity without friction" and this was friction.

Body now defaults to user-select:text (the standard for text content),
and the chrome elements that should never be drag-selected (button,
summary, role=button|switch|tab|menuitem, data-no-select) opt out.

Verified live: body=text, button=none, paragraph=text (inherited).
2026-05-07 09:04:10 +01:00
fdeda6c642 fix(design-system): sync colors_and_type.css to app.css authoritative tokens
The runtime app reads tokens from app.css @theme; this file ships values
to the buildless preview pages under design-system/preview/*.html.
Several values had drifted from the Phase 10a a11y darkening done in
app.css, so the previews showed brighter accents and lighter tertiary
text than what users actually see.

Aligned: dark --text-tertiary, dark --accent (+ hover/subtle/glow/
border-focus/shadow-accent), light --text-tertiary, light --accent
(+ hover/subtle/glow), light --success, light --danger.

Header banner now states the file's role explicitly so future edits
don't recreate the drift.
2026-05-07 09:03:11 +01:00
2f85ec5806 fix(settings): rename text-error/border-error to text-danger so error states actually render
The Tailwind v4 @theme block defines --color-danger as the semantic error
token, and 15+ files across the codebase use text-danger consistently.
SettingsPage was the lone outlier with five text-error / border-error
sites (audio devices, profile delete, vocabulary, term delete, diagnostic
report). Without a --color-error token, those utilities resolved to the
default text colour, so error messages rendered in warm cream instead of
red. Verified live via Playwright probe before and after.
2026-05-07 09:01:41 +01:00
0f105f0e15 chore(llm): update callers for renamed model variants
Picks up the registry rename in the front-end and Tauri command layer:

- src/lib/types/app.ts: LlmModelIdStr now lists the four new ids
  (qwen3_5_2b / qwen3_5_4b / qwen3_5_9b / qwen3_6_27b).
- src/lib/pages/SettingsPage.svelte: LLM_MODELS table rebuilt with
  four tiers (Minimal / Standard / High / Maximum), matching subtitles
  and download-size copy. selectedLlmModelId fallback, hardware-warning
  thresholds, tier-availability check, and ensureRecommendedLlmTier
  fallback all retargeted at the new ids. The Maximum tier surfaces a
  64 GB / 24 GB warning so users with mid-range hardware see honest
  expectations.
- src-tauri/src/commands/llm.rs and commands/tasks.rs: doc-comment
  examples refreshed (Qwen3 4B → Qwen3.5 4B, Qwen3's tokenizer →
  Qwen's tokenizer — the BPE family is shared).
- src/lib/stores/llmStatus.svelte.ts: chip-detail example updated.

cargo build --workspace clean. cargo test --workspace clean.
npx svelte-check reports one pre-existing error in vite.config.js
(unused @ts-expect-error directive, dates back to the original
scaffold commit 9926a42); not introduced here, out of scope to fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 09:59:03 +01:00
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
749403697a fix(ui): SettingsPage launch-at-login toggle — replace bounce-easing with ease-out-quart
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
Final impeccable detect cleanup. The launch-at-login toggle thumb in the
Tasks & Rituals section was using cubic-bezier(0.34, 1.56, 0.64, 1) — the
same overshoot bounce that was replaced in Toggle.svelte by commit
6469663. Match the convention here so all toggle animations use the
same exponential ease.

Single-line CSS change. Visual smoke not exercised because the dev
server is winding down between subagents; npm run build confirms the
file still compiles clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:52:08 +01:00
eb3d2f90ab refactor(settings): regroup SettingsPage into 7 progressive-disclosure groups
Phase 9c follow-up. The 2309-line hand-rolled accordion is replaced with
seven SettingsGroup-wrapped top-level groups, each containing the
relevant existing sub-sections as nested SettingsGroups:

1. Audio — microphone (input device).
2. Vocabulary — terms & profiles, profiles & templates (legacy manager
   moved in here so all profile / vocabulary state lives together).
3. Transcription — engine, format mode, model management, compute device,
   language. Defaults to open to preserve the prior accordion's landing.
4. AI & Processing — post-processing toggles, AI Assistant tier and
   model management, if-then implementation rules. AI Assistant and
   if-then rules moved out of their original positions to sit alongside
   the deterministic post-processing toggles.
5. Tasks & Rituals — rituals (incl. launch-at-login, kept bundled with
   the existing Rituals UI block to avoid splitting that block), tasks
   page (sparkline), nudges.
6. Output & Capture — read aloud (TTS, lazy-loaded on first open via the
   new SettingsGroup `onopen` hook), capture & export.
7. Appearance & System — global hotkey, appearance (theme/zone/font/
   locale), accessibility, about (engine status + diagnostics).

Deviations from Codex's 7-group spec:
- Launch-at-login stays inside the Rituals sub-group (was bundled there
  in the existing markup; relocating would require splitting the
  Rituals UI block, which is out of scope for this pass).
- Profiles & Templates legacy manager pulled into the Vocabulary group
  rather than appearance/system.

Implementation notes:
- SettingsGroup gains an optional `onopen` callback prop, fired once on
  the first closed→open transition. Used by Read aloud to lazy-load TTS
  voices and by AI & Processing to refresh LLM status. Replaces the
  former toggleAiSection / toggleReadAloudSection imperative handlers.
- The centralised openSection state is removed; each <details> manages
  its own disclosure.
- Tokens are inherited from app.css; the prior global token darkening
  (commit 2da0a5b) covers all section labels via the existing utility
  classes — no per-component label-class swaps were added.

Search box deferred to a follow-up commit. Forcing `open=true` on
SettingsGroup based on a filter input would require either a controlled
`open` prop or a {#key} re-mount strategy, both of which add risk to
this restructure pass.

Tauri-bridged commands cannot be exercised in browser-only `npm run
dev`; structural verification done via npm build, npm check, and a
live dev server fetch of /settings. Real persistence smoke needs a
tauri dev session.

Verification:
- cargo fmt --check: pre-existing whitespace diffs in src-tauri/src/
  live.rs and src-tauri/src/lib.rs only (untouched by this commit).
- cargo clippy --all-targets -- -D warnings: clean.
- cargo test: 283 tests pass.
- npm run check: 1 pre-existing error in vite.config.js:5; 0 new.
- npm run build: clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:50:28 +01:00
6469663c25 fix(ui): impeccable detect — perf + taste touch-ups across four components
Mechanical fixes from `npx impeccable detect`:

- Sidebar.svelte: replace `transition: width, min-width` on the aside
  with a wrapping CSS-grid container animating `grid-template-columns`.
  Avoids per-frame layout cost from animating `width` directly.
- MorningTriageModal.svelte: swap pure `bg-black/50` overlay for the
  brand deep-neutral `rgba(26, 24, 22, 0.5)` (#1a1816 @ 50%). TODO left
  in source to promote this to a `--color-overlay` token in app.css.
- Toggle.svelte: drop bouncy `cubic-bezier(0.34, 1.56, 0.64, 1)`
  (1.56 overshoot) for ease-out-quart `cubic-bezier(0.16, 1, 0.3, 1)`.
  Still snappy, no overshoot — better fit for a toggle.
- TasksPage.svelte: same grid-template-columns refactor as Sidebar
  for the list-sidebar `transition: width` declaration.

Verification:
- npm run check: 1 pre-existing error (vite.config.js:5), 1 unrelated
  warning in SettingsGroup (out of scope, owned by parallel subagent).
- npm run build: clean.
- cargo clippy --all-targets -- -D warnings: clean (with LIBCLANG_PATH).
- cargo test --workspace: 283 passed, 0 failed.
- cargo fmt --check: pre-existing diffs in main.rs / lib.rs (no Rust
  files were touched in this commit).

Manual smoke deferred — `npm run dev` is in use by the SettingsPage
subagent, so the build-clean signal is the proxy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:48:14 +01:00
22d554e85b fix(a11y): T4 / T7 / H3 — energy chip + filter pill + per-row checkbox
Phase 10a a11y audit (2026-04-29):

T4: Energy radio chip text in dark mode read at 3.48:1 (text-text-tertiary
  on chip bg). Bumped non-selected chips to text-text-secondary so the
  hierarchy still reads but the resting state clears AA.

T7: Selected energy radio (and the match-my-energy toggle next to it)
  used bg-accent/15 — visually indistinguishable from surrounding bg in
  dark theme. Bumped to bg-accent/25 and added a 1px accent/30 border on
  the selected state so the selected pill is unambiguous in both themes.

H3: Per-row bulk-select checkbox in HistoryPage rested at opacity-50,
  which drops the unchecked outline below 3:1 over bg-bg-card. Bumped
  base opacity to 70%; hover/selected states unchanged.

Note on T6 (energy radiogroup arrow keys): verified the keyboard handler
in TasksPage. It is attached to the wrapper element with role=radiogroup,
and arrow-key events on the focused radio child bubble up to the wrapper
because the focused radio sits inside it. The handler reads
settings.currentEnergy (not the focused element) to pick the next index,
then focuses the new radio explicitly via querySelector. ArrowRight /
ArrowLeft / ArrowUp / ArrowDown / Home / End all wired correctly. No
change needed — pattern is sound.

Resolves: T4, T7, H3. T6 verified working as-is.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:04:51 +01:00
fa734c869f fix(a11y): P5 / G4 — add focus trap to MorningTriageModal
Phase 10a a11y audit (2026-04-29) flagged the morning triage modal as
having role="dialog" and aria-modal="true" but no focus trap, so Tab
leaks out to the sidebar/page beneath. Escape-to-close was already
wired and is preserved.

Behaviour now matches the W3C dialog pattern:

- On open, capture the invoking element (document.activeElement) and
  move focus to the first focusable inside the dialog.
- Tab cycles forward; Shift+Tab cycles backward. Wrap-around between
  first and last focusable.
- On close, restore focus to the captured invoker.
- The dialog container itself gets tabindex=-1 so it can hold focus
  if no children are focusable (e.g. during the loading state).

The focusable selector excludes aria-hidden elements and elements with
no offsetParent (display:none / visibility:hidden), so dynamic content
between loading -> tasks -> action buttons stays in sync.

Resolves: G4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:04:32 +01:00
d089fdb37f fix(a11y): P3 — drop focus:outline-none on inputs; restore global :focus-visible
Phase 10a a11y audit (2026-04-29) flagged seven inputs across the app
that strip the global 2px :focus-visible outline (defined in app.css:251)
without providing a comparable replacement. Net effect: keyboard users
see at most a 1px border-colour shift on focus, sometimes nothing.

The fix removes the focus:outline-none override so the global rule
applies. Affected inputs:

- FilesPage: file-transcript textarea (F2).
- TasksPage: search input, quick-add input, inline list-edit, new-list
  input (T1, T2, T3, plus the new-list rename input).
- HistoryPage: top search, inline title rename, tag-add (H1).
- ImplementationRulesEditor: trigger/surface/task selects + speak-line
  input (S7).
- TaskSidebar, WipTaskList: quick-add inputs (S8).

The 1px focus:border-accent on inputs that had it is retained as a
secondary cue; the global 2px ring is now the primary indicator and
matches button focus across the app.

Resolves: F2, T1, T2, T3, H1, S7, S8.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 12:04:23 +01:00