Acts on the polish-pass brief returned for 5f. Touches the quietware
branch only; v0.2 fallback (and compactPreviewText, used by its
row-height measurer) is left intact.
Date groups liveGroups derived walks `filtered` and breaks at
local-calendar-day boundaries. dateGroupLabel
formats Today / Yesterday / D MMM / D MMM YYYY.
Section headers are 11px mono uppercase, separated
by the same divide-y the rows use.
Row hierarchy New quietRowPreview returns a real transcript
snippet (item.text → item.preview → empty). The
title repeat in 5f was compactPreviewText returning
the title under the title; that helper stays for
v0.2's height math, the quietware row uses the new
one. Falls back to "No preview available" in-template.
Row meta-line HH:MM · duration · Dictation|File. Date moved to
the group header — repeating it per row wasted
hierarchy.
Row actions Open stays visible (tertiary), other actions moved
behind a quiet LumotiaIconButton(MoreHorizontal)
trigger that opens LumotiaMenu with Copy /
Export markdown / Move to Trash. Trigger is
always present (not hover-only) so keyboard
focus works.
Search alignment Dropped max-w-2xl. Search now spans the same
content gutter as the rows so the right edge
aligns with where the actions column begins —
intentional, not arbitrary.
Trash rows Same row anatomy. Restore visible. No overflow
menu — perm-delete-from-trash UI is deferred per
the existing comment (TRANSCRIPT_TRASH_RETENTION_DAYS
handles hard-removal). Meta-line shows
"Deleted … · Auto-purges after 30 days".
Date-grouping formula (per CLAUDE.md rule):
same local-calendar day as now → "Today"
previous local-calendar day → "Yesterday"
same local-calendar year → "D MMM"
older → "D MMM YYYY"
Day boundaries are strict local midnight, not 24-hour windows. Items
without savedAt fall into an "undated" bucket.
npm run check: 0 errors.
Calm local archive instead of SaaS table. Same gated-layout pattern
as Phases 5c/5d. v0.2 surface unchanged; quietware path renders
HistoryPage via LumotiaPageSkeleton.
Zones.
Header "History" title + "Search and reopen saved
transcripts." subtext + Live/Trash tab toggle
with live counts in mono.
Primary surface search input + scrollable list of items, or
empty-state typography when nothing saved.
Mono footer N saved · N in trash · Local only.
Action dock omitted — actions live per-row (Open / Copy /
Delete) so the dock doesn't add noise when the user is browsing
a list of items each with their own context.
State + handlers preserved.
history (store), trashItems, viewMode, searchQuery, filtered
(derived), trashError, trashLoading, restoringId,
compactPreviewText, copyItem, removeItem, openViewer,
restoreFromTrash, formatTimestamp.
Bulk actions, audio playback, tag chips, ClearAll modal and
Phase 9 multi-select toolbar all stay accessible via the v0.2
fallback path when quietware is off. The quietware view focuses
on the core archive flow.
Per-item row.
Mic icon if source=dictation, FileText icon if source=file.
Title (or first 80 chars of preview if untitled).
Two-line preview clamp.
Mono timestamp via formatTimestamp.
Open / Copy / Delete actions in tertiary buttons on the right.
Empty states.
Live mode, no items:
FileText icon + Young Serif italic "Nothing saved yet." +
Work Sans support.
Live mode, search returns nothing:
same icon + "Nothing matched." + "Try different keywords or
clear the search."
Trash mode, no items:
Clock icon + "Trash is empty." + 30-day retention copy.
Search input.
Lives at the top of the primary surface. Standard search input
with focus ring picking up --button-primary-bg.
Capture script.
scripts/capture-quietware-screenshots.mjs now navigates to the
History page via the sidebar nav button after Files, and
screenshots both quietware modes.
Verified.
- npm run check: 0 errors, 0 warnings across 5707 files.
- Captures confirm both empty and populated states render
correctly (populated state captured with temporary mock data
in the history store, reverted before this commit).
Phase 5f is complete. Phase 5e (Tasks) is the last layout migration
before Phase 6 / 7.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Targeted migration on a 1 225-LOC page. The FTS5 search input stays a
plain <input> (LumotiaCombobox needs an options list; free-text search
doesn't fit the API cleanly enough to justify a rewrite for v0.2).
Row patterns + clear-all modal stay verbatim — their bespoke ARIA and
inline arm-confirm state are core to the page's identity.
- Card import → LumotiaCard (4 use sites bulk-swapped)
- EmptyState import → LumotiaEmptyState (4 use sites)
- LumotiaButton import added for selective use in follow-up sweeps
Bespoke surfaces left verbatim: VirtualSegmentList, audio player,
clear-all type-the-word modal, tag-chip filter bar.
Per-page gate: npm run check (0/0/5704 files).
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>
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.
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.
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.
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
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>
Wholesale JS -> TS migration of the frontend — stores, utils, actions,
and all Svelte component scripts adopt type annotations. Compile-time
surfaces (app.d.ts, lib/types/) added for shared DTO types.
Build plumbing:
- package.json: dev:frontend script that runs svelte-kit sync first
- tauri.conf.json: beforeDevCommand points at dev:frontend
- run.sh: dropped the sed-hack that temporarily blanked beforeDevCommand;
now relies on npm run dev:frontend to avoid double-Vite
- jsconfig.json: allowImportingTsExtensions
Preserves all Group 1 behaviour:
- page.svelte.ts keeps loadHistory / loadTasks Tauri-first, no
localStorage; saveTranscriptMeta + mapTranscriptRow + mapTaskRow
intact; update_task_cmd and update_transcript_meta_cmd invocations
carry the correct payload shape.
- Toasts, preferences stores typed without behaviour change.
- Viewer still routes segment edits through saveTranscriptMeta; the
Task 1.5 TODO markers are gone.
taskExtractor.ts is functionally improved during the migration:
- multi-task matches in the same sentence
- list-style shopping-verb expansion (get bread, milk, and cheese)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Second dogfood sprint. Headline fix: Linux now uses native KWin/Mutter
decorations instead of fragile frameless `startResizeDragging`, which
collapsed diagonal corner resize to a single axis and made drag feel
laggy. macOS / Windows keep custom chrome via `useCustomChrome` gate.
Other changes:
- Cross-window preferences sync via `kon:preferences-changed` Tauri
event — theme and font changes propagate live to float/viewer.
- Hotkey recorder rewritten to use capture-phase document listener
gated by $effect; button focus was unreliable in webkit2gtk.
- History page redesigned for cognitive-load hygiene: title-first
compact row, inline title input, Edit popout opening /viewer in
edit mode, clipboard export as .md with YAML frontmatter, manual
tag chips + + Add tag input, header tag filter (cap 7), global
Starred filter, `tag:xyz` search syntax.
- `deriveAutoTags` kept as empty hook for post-Task-7 LLM topic tags;
research found all previous auto-tag chips redundant with row UI.
- Viewer window adds edit mode with debounced-save textarea; native
title renamed to "Kon - Transcription Editor".
- Window minimums updated per GNOME HIG + WCAG reflow research:
main 960x600, float 360x480, editor 560x520.
- Microphone picker filters raw ALSA strings (hw:, plughw:, front:,
sysdefault:, null) and dedupes by CARD=X. New `description` field
on DeviceInfo reads /proc/asound/cards so Blue Yeti shows as "Blue
Microphones" instead of the short "Microphones" card name.
- GPU reporting fixed: get_runtime_capabilities now returns
accelerators=[cpu,vulkan] and whisper.supports_gpu=true, matching
the transcribe-rs whisper-vulkan feature linked unconditionally.
- ResizeHandles kept for macOS/Windows frameless: 12px edges, 20px
corners via CSS vars, pointerdown + setPointerCapture, corners
above edges in z-order, rendered as sibling (not child) of the
animated layout root so `position: fixed` is viewport-relative.
- Dueling drag-region handlers removed — `data-tauri-drag-region` and
manual `startDragging()` were stacked on the same elements; kept
the manual handler which has the button/input early-return logic.
See HANDOVER.md for the full session log and deferred items.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
addToHistory, renameHistoryEntry, deleteFromHistory in
src/lib/stores/page.svelte.js now dual-write to:
- the in-memory `history` array (UI snappiness, unchanged)
- localStorage (offline / browser-preview fallback, unchanged)
- SQLite via the new Tauri commands from 1cce567 (the canonical store)
The SQLite write is best-effort: failure does not lose the in-memory
copy. Console-warns for diagnostic visibility. Browser-preview path
correctly skips the Tauri call.
HistoryPage rename flow (renameItem) now calls renameHistoryEntry, which
goes through update_transcript. Closes the long-standing TODO from
architecture-review.md §13: rename was UI-only, never persisted, lost
on reload.
On rename failure a warn-toast surfaces "your change is visible now but
did not save" so the user is not surprised on next launch.
NOT WIRED YET (deferred):
- HistoryPage.searchQuery still filters in-memory rather than calling
search_transcripts. Fine for small histories; FTS5 infrastructure is
in place to upgrade when needed.
- Reading initial history from SQLite on session boot. localStorage
remains the cold-start source for now; SQLite catches up via dual-
write. A backfill / one-time sync command can land later.
- Replace card-per-entry layout with single-line rows (title truncated, duration, source icon, date, chevron)
- Add expand/collapse per row via expandedId $state (id-based, one at a time)
- Expanded detail shows full transcript, audio player (when active), and action buttons (Rename, Copy, Open viewer, Delete)
- All existing functionality preserved: search, playback, rename, delete, openViewer, clearAll
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Security fixes from code audit:
- CSP re-enabled in tauri.conf.json with strict directives
(was null — critical vulnerability)
- XSS fix in viewer highlightText(): HTML entities escaped before
inserting <mark> tags via {@html}
- Removed 3 unwrap() calls in rule_based.rs British English conversion
— replaced with safe let-else guards
- Removed unwrap() on main window lookup in lib.rs setup — now uses
if-let for graceful handling
- Wrapped JSON.parse in DictationPage transcription-result listener
with try/catch
Rebrand cleanup:
- Renamed all localStorage keys from ramble_* to kon_* across
7 files (stores, viewer, float, history)
12 tests passing, clippy clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- New copy_to_clipboard Tauri command using arboard 3.6
- Replaced all 5 navigator.clipboard.writeText() calls across
DictationPage, HistoryPage, FilesPage, and viewer with invoke()
- Native clipboard access independent of WebView permissions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>