Migrated the two ad-hoc buttons to wrappers; everything else (display-
only reflective copy, the open-loops list, the Newport shutdown
template) stays verbatim since the page is intentionally low-grammar.
- Back-arrow button → LumotiaIconButton (icon=ArrowLeft, size=sm)
- "Close" button → LumotiaButton variant=primary
Bespoke: none on this page (no recording state, no transcript surface).
Per-page gate: npm run check (0/0/5704 files). Vitest / browser-mode /
e2e baselines stay green (no behaviour change).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Additive token grammar (no renames, no replacements):
- --color-caution (dark #e8be4a, light #a08a1f) becomes the canonical
name for tuned-amber notice surfaces in the new wrapper grammar
- --color-warning is kept as a CSS var() alias of --color-caution so
every existing text-warning / bg-warning call site stays valid
- --color-info (dark #7a9ec0, light #3d6a8a) is the soft blue-grey
signal for the new LumotiaNotice info variant
- --color-accent-environment (dark #8fae9a, light #4a7058) is an
optional sage/moss support token for empty-state illustrations
and environment-neutral status dots. NOT a brand swap — amber/
copper --color-accent stays primary
Mirrored in src/design-system/colors_and_type.css (the buildless
preview pages bypass Tailwind so the duplication is intentional).
KI-05 resolved in the same commit, per the plan:
- src/lib/types/app.ts: drop `theme` from SettingsState
- src/lib/stores/page.svelte.ts: drop `theme: "Dark"` from defaults
- src/routes/+layout.svelte: drop the migration $effect, add a
one-shot migrateLegacyTheme() on mount that copies any historical
lumotia_settings.theme into preferences.theme and strips the
legacy field. Idempotent — subsequent loads short-circuit
- src/routes/{float,viewer,preview}/+layout@.svelte: drop the same
$effect; secondary windows inherit theme via PREFERENCES_CHANGED_EVENT
- src/lib/pages/SettingsPage.svelte: both SegmentedButton bindings
(quick-settings row at :1272, Appearance group at :2606) now use
Svelte 5 function bindings ({ get, set }) backed by prefs.theme
and updatePreferences. No SegmentedButton API change
Phase 3 per-page gate green: npm run check (0/0/4129 files),
npm test (13/13), npm run test:e2e (16/16). No regressions in
the Phase 1 smoke baseline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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.
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.
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.
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.
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.
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>
- 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.
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.
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.
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.
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.
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]).
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.
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.
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>
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.
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>
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>
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>
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>
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>
HistoryPage previously serialised the full TranscriptDto — text, segments,
manual + LLM tags, audio path — into `localStorage["kon_viewer_item"]` so
the viewer window could pick it up on mount. On a multi-hour transcript
that's MB-scale of user voice content sitting in storage that any
same-origin script in any open Kon window can read.
Hand off only `{ id }` (and a timestamp on re-saves). The viewer fetches
the canonical row from SQLite via the existing `get_transcript` Tauri
command and hydrates via the now-exported `mapTranscriptRow`. Cross-window
sync via the `storage` event still works — the receiving window re-fetches
on event instead of trusting the payload.
- HistoryPage `openViewer` + `openEditor`: write `{ id }` only.
- viewer `onMount` + `handleStorageChange`: route through new
`loadFromHandoff` which calls `invoke("get_transcript", { id })`.
- viewer `saveItemToHistory`: re-stamp localStorage with `{ id, stamp }`
to retrigger the storage event in sibling windows without leaking
content.
- `mapTranscriptRow` exported from page.svelte.ts for the viewer's use.
Backward-compatible at the parse layer: the `{ id }` shape extracts cleanly
from a stale full-DTO payload (TranscriptEntry already carries `id` at top
level), so a session that survives the upgrade picks up the new path on
next handoff without manual cleanup.
https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb
The Phase 9 bulk-delete path passed UUID strings to deleteFromHistory(index),
which expected an integer; JS coerced the string to NaN and splice(NaN, 1)
collapsed to splice(0, 1), so bulk-delete silently removed the first N visible
rows instead of the selected ones, then fired delete_transcript against the
wrong IDs.
Clear-all called saveHistory(), which was a no-op stub left over from the
same incomplete-refactor pattern that produced the manualTags persistence bug
fixed in 7eb52d9. The in-memory array was emptied, but SQLite still held
every transcript, so they reappeared on next loadHistory().
- Add deleteFromHistoryById(id) next to the index-keyed deleteFromHistory.
- bulkDelete now calls deleteFromHistoryById.
- clearAll now awaits an explicit per-id delete loop and surfaces a toast on
partial failure.
- Remove the saveHistory() stub and its sole caller's import.
https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb
Sparkline: friendlier aria-label ("3 completed today. 14 total over
the last 7 days." rather than a numeric list), per-bar <title>
tooltips with absolute date + count, 30 ms stagger entrance via
scaleY animation. Badge: 180 ms opacity + translateY entrance on
mount; conditional render means each new badge re-fires the
animation. Both animations respect prefers-reduced-motion.
The earlier draft tabindex=0 on the SVG was correctly flagged by
svelte-check as noninteractive_tabindex; SVG role="img" + aria-label
is sufficient for SR navigation without putting it in the keyboard
tab order.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 8 carryover backlog: the showMomentumSparkline toggle was
sitting under Rituals and visually claimed by the Launch-at-login
border-t subgroup. New top-level Tasks section hosts it, ready to
absorb future task-page settings (energy default, WIP limit, etc.).
The deeper Phase 9 SettingsPage restructure (search box + 7-group
progressive disclosure via the new SettingsGroup component) is
deferred to a follow-up polish session: the existing 2309-line file
uses a hand-rolled accordion that needs careful unwinding, and is
not in this session's scope. SettingsGroup component remains
available for that future pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-row Tag button calls extract_content_tags_cmd, persists via
saveTranscriptMeta. Dashed-italic chips render the AI tags distinct
from manual; clicking a chip promotes it into manualTags (the
LLM tag disappears, the manual one stays). Top toolbar gains "Tag all
untagged" for batch tagging across the corpus, with progress text.
Existing addManualTag / removeManualTag handlers swap their no-op
saveHistory() calls for saveTranscriptMeta — picks up the latent
manualTags persistence bug as a side effect.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Slim leading checkbox on every row, tinted-row state when selected.
Bulk-action toolbar appears only when selection is non-empty: select
all (visible), clear, export selected (via exportTranscriptsToDir),
delete selected (single confirm). Esc clears selection. Cmd/Ctrl+A
selects all visible when focus is inside the list and not in a text
input. Stop-propagation on the checkbox keeps the click off the
row-expand toggle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the clipboard-only path with saveTranscriptAsMarkdown. User
picks the location via the OS save dialog; cancellation leaves no side
effect, no toast, no fallback. Toast on success names the file.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Default on. Controls only the sparkline; the "N today" badge is
unconditional. Copy kept in the zero-loss register: "Never counts
against you." Co-located with the Rituals section for Phase 8.
Can move to a dedicated section in Phase 9 polish.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Badge renders when today's count > 0. Sparkline renders when the
setting is enabled and any of the last 7 days has a completion.
Wrapped in a narrow aria-live region so increments announce without
re-reading the rest of the header.
Fix: converted todayCount from $derived module export to a getter
function (Svelte 5 derived_invalid_export constraint).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>