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>
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.
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>
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.
- 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.
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.
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 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>
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>
Two small Phase 1 follow-ups for the Android target:
1. tauri.conf.json: add `bundle.android.minSdkVersion: 24`. Android 7.0
is the floor — gives us Vulkan availability (for the eventual GPU
feature flag), AAudio for cpal, and is what Pixel-class hardware
tests against. Keeps the global `identifier` on `uk.co.corbel.kon`
for now; the Corbie rebrand sweep will land `corbel.technology.corbie`
as a single coherent commit.
2. src/lib/utils/runtime.ts: add `isAndroid()` and `isMobile()` helpers
alongside the existing `hasTauriRuntime()`. Both use UA sniffing —
sufficient for feature-gating UI, never for security decisions.
These are how the Svelte side will hide:
- hotkey config (no global hotkey API on Android)
- paste-mode picker (auto-paste maps to a copy-only flow)
- meeting auto-capture toggle (process list unavailable)
- multi-window buttons (open-viewer, open-float, etc.)
- system-tray-related affordances
Tauri 2 doesn't expose a synchronous platform-detection helper that
works during initial render, so UA sniffing is the pragmatic choice.
https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb
The Phase 9 bulk export utility had a single success toast that was emitted
even when zero of N writes succeeded — "Exported 0 of 5 transcripts to
folder/" reading like the user just deliberately exported nothing.
Branch on the result count:
- 0 of N: error toast pointing at the console for write failures.
- N of N: success toast.
- M of N: warn toast — partial export, with the same console pointer.
Single-file save (`saveTranscriptAsMarkdown`) was already correct:
explicit success on save, error on failure, silent on user-cancelled —
left untouched.
https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb
Error toasts are sticky (duration: 0) so a misbehaving command that fires
errors in a loop — a backend that flaps, a polling effect over a broken
endpoint — accumulates toast items in the in-memory store indefinitely.
The audit found no other unbounded $state arrays in the frontend stores
(history caps at 500, recentNudges prunes by time, tasks/taskLists
replace-on-load), but `items.push(toast)` had no upper bound.
Add MAX_TOASTS = 50 with FIFO eviction. Doesn't change behaviour for
realistic toast volumes; only kicks in if something is genuinely wrong.
https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb
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>
Reusable progressive-disclosure wrapper around the native <details>
element. Animated chevron, hover + focus-visible affordances, and
prefers-reduced-motion guard. Designed to host the six collapsed
groups in the Phase 9 SettingsPage restructure.
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>
TranscriptEntry gains llmTags: string[]; TranscriptRow gains the
storage-shape llmTags: string. ContentTags type added. mapTranscriptRow
hydrates llmTags from the comma-joined column. saveTranscriptMeta
forwards llmTags through to update_transcript_meta_cmd, mirroring the
existing manualTags handling.
buildFrontmatter unions auto + manual + llm tags so exported markdown
surfaces every source in one tags: list.
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>
Centralises the save-dialog plus write-file plumbing. suggestedFilename
slugs the title into "<slug>-<YYYY-MM-DD>.md". saveTranscriptAsMarkdown
opens the system save dialog and writes via write_text_file_cmd; on
cancel returns null with no toast or fallback. exportTranscriptsToDir
writes one .md per item to a chosen folder, in-batch collision suffix
" (2)" etc. Documents the deliberate non-check of pre-existing files
in the chosen directory.
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>
Phase 8 completionStats store listens on these events to refresh the
daily count. Keeps the badge + sparkline accurate after un-tick / delete
paths, which don't currently fire kon:task-completed.
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>
Tiny inline SVG. Seven bars, zero-days render as 1 px baseline stubs.
fill=currentColor so the parent's text colour (tertiary ink on the
Tasks header) drives it. Self-hides if all 7 days are zero.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Owns the last-7-days completion series. Refreshes on task-completed /
step-completed / task-uncompleted / task-deleted window events and on
window focus (for day rollover). Derives todayCount from the newest
entry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the Phase 8 frontend types. New setting defaults to true (sparkline
on for everyone on upgrade) and persists via the existing settings
envelope; no migration needed because missing keys spread over the
defaults object on load.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Small if-then automation layer. Rules persist in SQLite; the runner
lives on the frontend and binds to the Phase 6 event bus so the rule
pipeline reuses the same delivery primitives (timer events, TTS,
Tasks navigation).
Storage:
- Migration v12 adds implementation_rules (id, enabled, trigger_kind,
trigger_value, actions_json, last_fired_key, created_at,
updated_at) with enabled+trigger_kind index for the runner's hot
path.
- CRUD helpers: insert / list / get / set-enabled / mark-fired /
delete, plus a round-trip test.
Commands (all main-window-guarded via ensure_main_window):
- list_implementation_rules
- create_implementation_rule — validates HH:MM, checks the target
task exists at save time for surface-task actions, caps
speak-line at 240 chars, pins v1 timers to 5 minutes.
- set_implementation_rule_enabled
- mark_implementation_rule_fired — main-thread idempotency shim so
the runner can atomically claim a fire.
- delete_implementation_rule
Runner (implementationIntentions.svelte.ts):
- Subscribes to kon:task-completed and kon:morning-triage-finished
(MorningTriageModal now emits on all three exit paths — empty,
skipped, picked — so skip counts as finishing).
- 30 s poll for time-of-day rules, plus an immediate check on startup
so a rule whose time has already passed today catches up once.
- Idempotency via last_fired_key composed as YYYY-MM-DD@HH:MM for
time rules; new time rules whose HH:MM has already passed today
are pre-seeded so they don't fire retroactively on save.
- Rules are paused when Nudges "Mute for now" is on — a hard mute
stops all rule delivery in addition to OS notifications.
- Stale-task safety: if a surface-task action's target has been
deleted, the runner opens Tasks and warns clearly rather than
pretending to surface something that's gone.
Editor (ImplementationRulesEditor.svelte):
- Lives in Settings under a new "If-then rules" accordion section.
- `If` picker: time of day (with time input), a task completes,
morning triage finishes.
- `Then` composer: optional surface (inbox / today / all tasks /
specific task), optional 5-min timer, optional speak-aloud line.
- Saved rules list with enable toggle + delete.
Rules table integration for Phase 10b rename sweep: add
implementation_rules to the kon.db → corbie.db migration shim when
that phase lands.
Gates: fmt, clippy -D warnings, cargo test 265/0, svelte-check
0/0, npm build green. Pre-existing Vite chunk warning on sounds.ts
is unrelated to Phase 7.