Adds colors_and_type.css token system, fonts (Lexend, Instrument Serif Italic,
JetBrains Mono, Atkinson Hyperlegible Next, OpenDyslexic), SVG assets (wordmark,
waveform mark, grain), HTML preview spec cards, UI kit, and SKILL.md reference
under src/design-system/. Foundation for applying the new Kon visual identity.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously the bulk import ran new Set(...) on raw trimmed strings
before lowercasing, so 'ACME' and 'acme' both survived the first
dedupe pass. Neither existed in the store, so both got added —
defeating the commit message's claim that pasting the same block
twice with different casing is a no-op.
Collapse case variants at the initial dedupe step using a lowercase
seen-set, keeping the first occurrence's casing as written.
Co-authored-by: jars <jakejars@users.noreply.github.com>
Adds src/lib/utils/hotkeyValidity.ts with validateHotkey(combo, os)
and wires it into HotkeyRecorder.svelte. Rules:
- X11/Wayland/Linux: reject single-key combos unless the trigger
is F13..F24 (the conventional global-shortcut escape hatch).
- Windows: reject combos whose only modifier is a right-hand
variant (RCtrl/RAlt) — matches Handy #966's failure mode where
RegisterHotKey silently ignores them.
- macOS: reject Fn-only combos and bare-key combos (non-function
keys).
- unknown OS: pass through — better to ship a flawed combo than
reject one we can't validate.
When validation fails, the recorder leaves the previous known-good
hotkey intact and surfaces an inline warning-coloured sentence
explaining why, with actionable copy ("add a modifier", "use the
left-hand equivalent", etc.). The save button disappears because
settings.globalHotkey is never written to — no state drift.
Matches Handy #917 / #1019 / #966 / #956, brief item #5.
Co-authored-by: jars <jakejars@users.noreply.github.com>
A rapid double-tap of the global hotkey, evdev autorepeat, or a
sticky-key compositor quirk (KDE's 'slow keys') can all deliver
the same press twice within ~100ms. Without a guard, the recording
toggles into and out of the same frame and the capture is lost.
Gates the evdev 'kon:hotkey-pressed' forwarder in +layout.svelte
behind a 120ms debounce (Date.now()-based; no timers, so no tail
latency for a legitimate single press). The debounce is intentionally
shorter than a deliberate double-press cadence but longer than any
autorepeat interval we've seen in the wild.
The audio-stream-warming half of brief item #4 (Handy #1143) lives
in Workstream A's Phase A.3 warm-up WAV; this covers the UX side.
Co-authored-by: jars <jakejars@users.noreply.github.com>
Adds src/lib/utils/settingsMigrations.ts exposing
loadSettingsWithMigration() / saveSettingsWithVersion() around a
{version, data} envelope in localStorage["kon_settings"]. The
migration chain is indexed by destination version so adding a v2
is one MIGRATIONS[2] = (prev) => next entry away from working.
Legacy bare-object settings blobs are treated as v0 and folded into
v1 identically to before — no user-facing reset — but an unreadable
blob now surfaces a single Settings reset toast instead of silently
dropping data.
Covers Handy #602 ('settings reset on update') and unblocks the
other B.2/B.3 SettingsState additions listed in
docs/whisper-ecosystem/workstream-B.md: every subsequent field
lands behind a MIGRATIONS step, so older Kon builds stay readable.
Co-authored-by: jars <jakejars@users.noreply.github.com>
Settings → Vocabulary gets a "Bulk add from a list…" disclosure under the
single-term row. Expanding reveals a textarea; paste newline- or
comma-separated terms, hit Import, and the page loops addTerm for each
entry the active profile doesn't already have.
Dedupes case-insensitively against the existing term list so pasting the
same block twice is a no-op. Skipped + failed counts surface via toast;
persistent errors (any failing term) also land in vocabularyError so the
inline panel explains what went wrong.
Covers OpenWhispr issue #460 — one-at-a-time entry becomes friction past
roughly ten terms. No backend changes; addTerm is already in profilesStore.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ported the best bits of OpenWhispr's TranscriptionPreviewOverlay into Kon's
window conventions. Off by default — toggle in Settings → Output → "Floating
preview when Kon is unfocused". Opens only when the main window isn't
focused at the start of a recording, so it never adds noise when the user
can already see the transcript in the main surface.
Phase state machine (src/routes/preview/+page.svelte):
- listening — pulsing dot, no text yet
- live — animated bars + streaming raw Whisper output
- cleanup — accent bars while the LLM cleanup pass runs
- final — checkmark + formatted text + 4s auto-hide
Data plumbing: raw segment text is captured before post_process_segments in
live.rs (new raw_text field on LiveResultMessage) and in transcription.rs
(new raw_text in the transcription-result payload). DictationPage forwards
raw_text to the overlay via Tauri global events — preview-listening on
start, preview-append per chunk, preview-cleanup before the LLM pass,
preview-final with the formatted text, preview-hide when a run produced no
transcript (empty recording / cancel).
Window is always_on_top, skip_taskbar, focused=false so it never steals
focus from whatever the user is dictating into. open_preview_window shows
an existing hidden preview or builds it fresh; close_preview_window hides
without destroying so the next open is instant.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Default off. When on, the layout polls detect_meeting_processes every 15s
with the user's app-name patterns. On a fresh match (edge-triggered — no
re-toast until the app goes away and comes back) we fire a reminder toast
that tells the user which meeting app appeared and their global hotkey. We
never start recording on this signal; the ideology rule says the user
decides. The signal is a single channel: process list match only — no mic
activity heuristic, no calendar.
Backend adds kon_core::process_watch::{list_running_process_names,
match_meeting_patterns} over sysinfo, exposed to the frontend as the
detect_meeting_processes Tauri command.
Settings ships two new fields — meetingAutoCapture (bool) and
meetingAutoCaptureApps (string[]) — with a comma-separated input in the
Output section. Default app list is ["zoom", "teams"], user-editable.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
initI18n (src/lib/i18n/index.ts) registers three locales and picks the
initial one in order: kon_locale in localStorage > navigator.language short
code > en. +layout.svelte calls it once at app boot; guarded so per-window
re-init is a no-op.
Locale files are deliberately sparse — this is a scaffolding pass so strings
can be migrated incrementally. The Settings → Appearance → Language picker
plus its own description is the first real consumer; everything else
continues to render as hardcoded text until extracted.
Also: split the @chenglou/pretext ambient shim into src/lib/shims.d.ts. The
declaration previously lived in app.d.ts alongside a top-level `export {}`,
which made app.d.ts a module — scoping `declare module` to its own imports
and breaking resolution from src/lib/utils/textMeasure.ts. The fresh
.svelte-kit sync triggered by installing svelte-i18n surfaced it. Ambient
shim files must stay script-scoped (no top-level imports/exports).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an opt-in "auto-paste into focused window" toggle. When enabled, the
dictation pipeline sets the clipboard and then sends a Ctrl+V / Cmd+V
keystroke to whatever window currently has focus — the common case after a
global-hotkey dictation, since Kon's own window never stole focus.
Backend (src-tauri/src/commands/paste.rs) probes for a platform paste tool
and falls back cleanly:
- Linux Wayland: wtype > ydotool > xdotool
- Linux X11: xdotool > ydotool > wtype
- macOS: osascript System Events keystroke
- Windows: PowerShell WScript.Shell SendKeys
detect_paste_backends is a pure probe used by Settings to describe the
available backend next to the toggle (or nudge the user to install one).
paste_text always copies first, so auto-paste failure degrades to the
existing clipboard-only behaviour and surfaces a warn toast.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Parakeet-TDT scores 85 on any GPU-equipped English-capable system (Instant
speed + Great accuracy + GPU boost + headroom) vs ~75 for the best distilled
Whisper. A new test in recommendation.rs locks this in so future scoring
tweaks don't silently regress it.
FirstRunPage previously stored settings.modelSize by title-casing a lowercased
alias — which worked for Tiny/Base/Small/Medium but produced
"Whisper-distil-small-en" for the new distil ids. Swap to an id→label map
and pass the raw model id through to download_model/load_model; the backend
already accepts full ids via the whisper_model_id fallback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two new registry entries (crates/core/src/model_registry.rs):
- whisper-distil-small-en — 336 MB, ~6× faster than whisper-small-en
- whisper-distil-large-v3 — 1.55 GB, near large-v3 accuracy at medium size
Both are whisper.cpp-compatible GGML binaries hosted on HF by the
distil-whisper org; no runtime change, just wider model choice. English-only
by design (matches upstream Distil-Whisper).
The Settings model picker widens to six options — Tiny, Base, Small,
Distil-S, Medium, Distil-L — ordered roughly by accuracy. Download/load
commands now take the resolved model id (whisper-distil-*) instead of the
lowercased label, so the frontend owns the label↔id mapping.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
kon-llm now owns a real LlamaBackend + LlamaModel, with three Qwen3 tiers
(1.7B Q4, 4B-Instruct-2507 Q4, 14B Q5) selectable per hardware. Downloads
are resumable with SHA-256 verification and stored under ~/.kon/models/llm.
Engine exposes three high-level surfaces — all greedy/temp-0, GBNF-constrained
where output shape matters:
- cleanup_text (prompt-injection-hardened system prompt; profile terms
appended as "preserve these spellings" suffix)
- decompose_task (3–7 micro-steps, constrained JSON array)
- extract_tasks (optional-array; empty when no explicit commitments)
post_process_segments now takes an Option<&LlmEngine> and, when loaded and
format_mode != Raw, joins segments → cleanup → replaces segments with the
cleaned text (first segment span). Rule-based path still runs first; LLM
errors log and keep rule-based output.
Tauri commands: recommend_llm_tier, check_llm_model, download_llm_model,
load_llm_model, unload_llm_model, delete_llm_model, get_llm_status,
cleanup_transcript_text_cmd, extract_tasks_from_transcript_cmd,
decompose_and_store (LLM-backed subtasks).
Settings: AI tier toggle (off / cleanup / tasks), model picker with
downloaded/loaded status, download progress events via
kon:llm-download-progress.
Dictation: ensureLlmModelLoaded on mount, cleanupTranscriptIfEnabled after
stop when tier != off and format_mode != Raw, LLM task extraction when
tier=tasks (regex fallback on failure).
Interim: both llama-cpp-sys-2 and whisper-rs-sys statically link their own
ggml, so src-tauri/build.rs emits -Wl,--allow-multiple-definition on Linux.
Replace with a system-ggml shared-lib setup as a follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Major quality pass on top of Phase 2. Five substantive changes plus
cross-cutting touches across audio, hotkey, transcription, and Tauri
command layers.
Transcription quality
- Long-audio chunking in commands/transcription.rs: Parakeet and large
file transcription now chunk-and-recompose with overlap trimming, so
the live-path chunking advantage extends to file-based workflows.
- Stateful live speech gate in commands/live.rs on top of the earlier
duplicate-boundary filtering — distinguishes start-of-speech from
mid-speech and holds state across chunks.
Auto-learning corrections
- New crates/ai-formatting/src/correction_learning.rs: extracts user
text corrections from viewer edits and proposes additions to the
active profile's vocabulary.
- src-tauri/src/commands/profiles.rs bridge for frontend-driven
confirmation of learned terms.
- src/routes/viewer/+page.svelte hooks the learning path into the
segment-edit flow so corrections feed profile_terms without a
separate 'train this profile' UX.
Transcript profile provenance
- Migration v8 (crates/storage/src/migrations.rs) adds profile_id to
transcripts, defaulting to DEFAULT_PROFILE_ID so existing rows stay
valid.
- crates/storage/src/database.rs: TranscriptRow + CRUD carry profile_id.
- src-tauri/src/commands/transcripts.rs: add_transcript accepts and
persists profile_id.
- DictationPage.svelte + FilesPage.svelte send activeProfileId on
capture so learned corrections are attributed to the right profile.
Cleanup prompt contract
- crates/ai-formatting/src/llm_client.rs hardened: the CLEANUP_PROMPT
now specifies concrete do/do-not rules, ready for a real model-backed
cleanup pass. The llm_client is still a stub — kon-llm remains unwired
— but the prompt shape is final.
Cross-cutting polish
- Minor touches in audio (capture/decode/resample), hotkey (lib/linux/stub),
core, transcription (concurrency/model_manager/local_engine/whisper_rs),
and the rest of src-tauri/src/commands/*: error-path tightening, log
clarity, TS-migration follow-ups (@ts-nocheck additions for incremental
typing).
Verified locally: npm run check, cargo test -p kon-ai-formatting,
cargo test -p kon-storage, cargo test -p kon --lib commands::live::tests,
cargo check — all green.
Scope boundary: kon-llm crate is still a stub; task extraction remains
rule-based. Bundled local-LLM runtime is the next clean step and is not
in this commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both the status-line hint next to the record button and the empty-state
message now read settings.globalHotkey reactively, so 'Press record or
Super+E' (or whatever the user has bound) stays in sync with the actual
shortcut.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
webkit2gtk under KDE Wayland does not set e.metaKey on subsequent events
after the compositor intercepts Super — but the Super keydown itself is
still delivered with e.key === "Super". Track pressed modifiers from
raw keydown/keyup and OR with the webkit booleans so combos like Super+E
(which OpenWhispr already registers successfully via evdev) can now be
captured in Kon's recorder.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three correctness fixes to the hotkey recorder based on dogfood logs:
1. Modifier set now includes Super / OS / Hyper (webkit2gtk on Linux
fires e.key === "Super" for the Windows key — previously that key
got captured as the final trigger, producing invalid 'Ctrl+Shift+Super'
strings the evdev parser rejected).
2. resolveTriggerKey() uses e.code (physical, shift-independent key)
to resolve shifted punctuation back to the unshifted name the evdev
parser understands: '+' -> '=', '|' -> '\\', etc. Letters and digits
also use e.code (KeyA -> A, Digit1 -> 1) to avoid layout quirks.
3. Numpad keys intentionally not mapped to main-keyboard equivalents —
they are distinct evdev codes. Leaves the parser to reject them so
the user gets a toast instead of a silently-wrong binding.
Registration failures now surface as a toast and revert
settings.globalHotkey to the last successfully-registered value (if
any), so the UI cannot lie about what is actually bound.
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>
@tailwindcss/vite:generate:serve threw 'Invalid declaration:
getCurrentWindow' on ResizeHandles.svelte?svelte&type=style&lang.css
even after comment sanitisation. The style block is plain CSS with no
Tailwind directives, but the per-component virtual CSS module route
was hitting a parse bug in the Tailwind v4 + Svelte 5 combination.
Workaround: move the CSS into app.css (class names are already
component-specific, no scoping loss) and drop the component-local
<style> block. This sidesteps the virtual-module route entirely.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to 92ac7ea. Two apostrophes remained (KWin's and don't)
that kept tripping Tailwind v4 JIT the same way. Removed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to d1d344b. Tailwind's scanner still saw backtick-quoted
fragments like `decorations: false` and `position: fixed` in the
script comments as CSS property declarations, tripping on the next
JS identifier (getCurrentWindow). Removing all backticks from the
comment block sidesteps the entire scanner path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tailwind v4 JIT scanner mis-tokenised the comment 'Tauri`s `data-tauri-drag-region`'
as an unterminated string literal, breaking dev-server HMR. Comment rewritten
to avoid apostrophes near backticks. No behaviour change.
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>
Replacing the preferences object (spread reassignment) caused all consumers'
prefs references to go stale — the loop guard read the old value forever.
Svelte 5 shared state pattern requires a stable const object with property
mutations, not reassignment. Also removes duplicate theme sync $effect from
SettingsPage (already handled in +layout.svelte).
textMeasure.js, virtualList.js, and accessibilityTypography.js were
added in the 2026/04/04 session alongside VirtualSegmentList.svelte but
never committed. All four are imported from DictationPage, HistoryPage,
SettingsPage, and the viewer — a fresh clone had unresolvable imports.
textMeasure.js: pretext-backed text measurement with LRU cache.
measureTextHeight, measurePreWrap, clampTextLines, invalidateCache.
virtualList.js: binary-search visible range for variable-height virtual
lists. buildCumulativeOffsets + findVisibleRange.
accessibilityTypography.js: pretext font/line-height helpers that read
the user's accessibility preference store (Lexend / Atkinson /
OpenDyslexic mapping).
VirtualSegmentList.svelte: virtualised transcript segment renderer used
by the viewer page; depends on all three utils above.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Front-to-back QC pass turned up two independent missing-module
showstoppers (workspace did not compile; frontend did not load) plus a
handful of HANDOVER-claimed features that were wired but dead. Fixes:
P0 — gets the app booting again:
- Add the never-committed src/lib/utils/runtime.js (hasTauriRuntime
detection); 5 imports were resolving to nothing.
- Add the never-committed crates/audio/src/streaming_resample.rs
(rubato-backed StreamingResampler with new/push_samples/flush);
declared in lib.rs and used 3x by live.rs but had no impl.
- Drop the duplicate hasTauriRuntime import in routes/+layout.svelte.
- Allow the transcript-viewer window to use the default capability
(was missing from capabilities/default.json:windows, so the viewer
window could open but not invoke any Tauri command).
P1 — features documented as working but actually dead:
- Pump MicrophoneCapture::take_error_rx() into LiveStatusMessage::
Warning each loop iteration in commands/live.rs. The HANDOVER
promised cpal stream errors would surface as toasts; the channel
was created and never read.
- Replace .expect() on the WebKit media-permission setup with a
logged warning. Failure no longer aborts the whole process.
- Toast on save_preferences failure (preferences.svelte.js had a
silent console.error — now warns once per failure run via the
existing toasts store).
P2 — correctness/robustness:
- add_dictionary_entry: switch INSERT OR IGNORE to ON CONFLICT
DO UPDATE ... RETURNING id so duplicate terms get the real row id
instead of a stale auto-increment.
- search_transcripts: qualify ORDER BY fts.rank.
- InsertTranscriptParams + TranscriptRow: bump sample_rate /
audio_channels from i32 to i64 to match the Tauri DTO and avoid
silent truncation at the boundary.
- Drop the unused tauri-plugin-mcp dependency.
- Promote sqlx in src-tauri/Cargo.toml from linux-only to
unconditional (lib.rs names sqlx::SqlitePool unconditionally —
macOS/Windows builds were latently broken).
- hotkey/linux.rs: stop panicking the hotplug task on inotify
failure; degrade to "no hotplug" with a stderr warning.
- layout.svelte: store the global error/unhandledrejection handler
refs and remove them in onDestroy so HMR/window teardown doesn't
leak listeners.
Verified: cargo check -p kon-core -p kon-storage -p kon-cloud-providers
passes. cargo check on src-tauri/kon-audio/kon-hotkey requires alsa +
gtk system libs not present in this sandbox; their changes are
syntactically and type-checked against the rest of the workspace.
svelte-check requires npm install which is not available here.
https://claude.ai/code/session_018ozAs4UcRC8jbJbddqJtEw
CROSS-PLATFORM AUDIT:
- Linux x86_64 (Fedora 43, KDE Wayland): HIGH confidence (the dev target)
- Linux other distros / X11: MEDIUM (tested patterns, untested distros)
- Windows 10/11: LOW — theoretically supported via Tauri + CPAL + whisper.cpp
+ tauri-plugin-global-shortcut (the custom evdev hotkey is no-op on
Windows); has had zero hands-on testing
- macOS Apple Silicon / Intel: LOW — same; macOS Info.plist needs
NSMicrophoneUsageDescription for the bundled app, and the path bug
fixed in this commit
REAL BUG FIXED — macOS app_data_dir was Unix-style:
crates/storage/src/file_storage.rs::app_data_dir() previously fell into
the "Unix" branch on macOS and wrote to ~/.kon/, which violates Apple
guidelines and confuses install/uninstall tooling. Now correctly:
- Windows: %LOCALAPPDATA%/kon (unchanged)
- macOS: ~/Library/Application Support/Kon/
- Linux: $XDG_DATA_HOME/kon or ~/.local/share/kon (XDG Base Directory),
with legacy ~/.kon/ fallback so existing installs keep working
- Other Unix: ~/.kon/ (unchanged)
OS DETECTION LAYER:
- New get_os_info Tauri command in commands/diagnostics.rs returns:
{os, arch, family, usesCmd, isWayland, customHotkeyBackend,
primaryModifierLabel}
- New src/lib/utils/osInfo.js helper:
- async loadOsInfo() warms a cache, called once at root layout mount
- then isMac() / isWindows() / isLinux() / modKeyLabel() / isWayland()
are synchronous
- browser-preview fallback reads navigator.platform
- Lets components localise hotkey labels (Cmd vs Ctrl), file-picker
copy ("Open Finder" vs "Open Explorer"), etc, without recomputing in
every place.
cargo check -p kon-storage clean. Updated HANDOVER-2026-04-17.md with
a per-platform confidence table.
THE BRIEF:
For the friends beta we need verbal feedback AND technical feedback —
some bugs the user cannot describe but a stack trace can. Built it in
two layers, with Layer 3 deferred until there is real volume.
PRIVACY POSTURE (matches Kon's local-first positioning):
- All capture is to disk only. Nothing is transmitted.
- The manual report bundler shows the user exactly what would be
shared and lets them choose to copy / save it.
- No remote endpoint, no Sentry, no opt-out telemetry.
LAYER 1 — Always-on local capture:
- Rust panic hook (commands/diagnostics.rs::install_panic_hook) writes
each panic to ~/.kon/crashes/<unix-ts>-<short-id>.crash. Captures
thread name, OS/arch, RUST_BACKTRACE state, and the panic info.
Installed before tauri::Builder so it catches setup-time panics too.
- Frontend window.onerror + window.unhandledrejection in
src/routes/+layout.svelte forward to the new log_frontend_error
Tauri command, which inserts into the existing error_log SQLite
table. Best-effort: errors in the error handler itself are swallowed
so logging can never crash the app.
- Existing error_log table (migration v1) is now actually used —
closes the TODO from crates/storage/src/database.rs:409.
LAYER 2 — Manual diagnostic report:
- Settings → About → Diagnostics section: Generate report → Preview →
Copy / Save.
- Report is plain markdown so it pastes cleanly into email, Discord,
GitHub issues. Sections: app version + OS, sanitised settings JSON,
recent error_log rows, last 5 crash dumps with previews, log file
tail (8KB).
- Preview is shown in a <details> with the full text the user can
inspect before deciding to share.
- Save writes to ~/.kon/diagnostic-reports/kon-diagnostic-<ts>.md.
NEW FILES:
- src-tauri/src/commands/diagnostics.rs (panic hook + 5 Tauri commands)
CHANGES:
- crates/storage/src/file_storage.rs: crashes_dir() + logs_dir() helpers
- crates/storage/src/database.rs: ErrorLogRow + list_recent_errors()
- crates/storage/src/lib.rs: re-exports
- src-tauri/src/commands/mod.rs: + diagnostics module
- src-tauri/src/lib.rs: install_panic_hook() before Builder; 5 new
Tauri commands registered (log_frontend_error,
list_recent_errors_command, list_crash_files,
generate_diagnostic_report, save_diagnostic_report)
- src/routes/+layout.svelte: installGlobalErrorCapture() at mount
- src/lib/pages/SettingsPage.svelte: diagnostic state + buttons +
preview block at the end of the About section
cargo check -p kon-storage clean. Settings.svelte if/each balanced.
LAYER 3 (deferred):
- Optional opt-in remote reporting (self-hosted Sentry on Tartarus or
similar). Not for friends beta. Open up after volume justifies it.
VOCABULARY PANEL (Day 5):
src/lib/pages/SettingsPage.svelte gains a new collapsible "Vocabulary"
section between Audio and Transcription. Wires the dictionary commands
shipped in 1cce567:
- list_dictionary_command on mount + onfocus refresh
- add_dictionary_entry_command (term + optional note)
- delete_dictionary_entry_command
- Inline error feedback via vocabularyError
- Empty-state copy + per-entry remove button with aria-label
The dictionary terms are intended to be injected into the LLM cleanup
prompt so the model preserves user-specific vocabulary (medication
names, place names, jargon, names of people in the user's support
network — particularly important for the ND audience). The LLM client
itself is currently a stub (crates/ai-formatting/src/llm_client.rs);
when it lands, it can import list_dictionary from kon_storage and
inject terms into the prompt suffix.
WAYLAND SELF-RELAUNCH (Day 6):
src-tauri/src/lib.rs gains ensure_x11_on_wayland() which runs before
tauri::Builder. On Linux Wayland sessions (XDG_SESSION_TYPE=wayland) it
sets GDK_BACKEND=x11, WINIT_UNIX_BACKEND=x11, and
WEBKIT_DISABLE_DMABUF_RENDERER=1 — the env vars HANDOVER.md documents
the user manually prefixing.
Drops the "users have to remember the env-var prefix" friction.
Idempotent: existing values are respected. Inspired by Open-Whispr's
Chromium --ozone-platform=x11 self-relaunch.
Compile-checked: cargo check -p kon-storage clean. Settings.svelte
$state / #if balanced (28 state, 34/34 if/endif).
NEXT: Phase 6 — dogfooding readiness doc with Jake test instructions.
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.
THE PROBLEM (Codex review + architecture-review.md §1, §14):
Multiple critical paths discard errors silently — `let _ = insert_transcript(...)`,
`.catch(() => {})`, inline `error = ...` state that not every page surfaces. ND
users in particular need explicit feedback when something fails; silent
failure is worse than no feature.
SHIPPED:
src/lib/stores/toasts.svelte.js (new):
- Minimal in-house toast store (~80 lines, no svelte-french-toast dep)
- Severity → palette: info/success → moss, warn → signal, error → ember
- Auto-dismiss durations per severity (success 3s, info 4s, warn 6s,
error sticky)
- `invokeWithToast(invoke, command, args, errorTitle?)` helper wraps a
Tauri invoke and toasts on failure while still throwing for callers
that need to handle the error themselves
src/lib/components/ToastViewport.svelte (new):
- Reads from the toasts store
- Bottom-right stack, max-width clamp on small screens
- aria-live=polite + role=alert on error toasts (screen-reader friendly)
- Slide-in animation honours `html.reduce-motion` for the existing
preferences toggle
- Severity left-border in brand colours via existing CSS variables
src/routes/+layout.svelte:
- Mounts <ToastViewport /> once at the app root so toasts work from any
page or window
src/lib/pages/DictationPage.svelte:
- First error-toast wiring: failures starting recording now show a
sticky toast in addition to the inline `error` panel
- Replaces a previously easy-to-miss failure-mode
NEXT (Day 4 batch):
- Wire toasts into FilesPage, HistoryPage, SettingsPage, ModelDownloader
(all currently swallow errors)
- Add `add_transcript`, `list_transcripts`, `update_transcript` (closes the
long-standing TODO Codex flagged), `delete_transcript` Tauri commands
wrapping the existing storage layer
- Switch addToHistory to dual-write (localStorage + SQLite) so the
canonical store catches up with the actual transcript flow
- FTS5 transcripts_fts virtual table + search command
- Wrap multi-row writes (decompose_and_store) in DB transactions
Day 1 follow-up to 96980c7. The device-picker UI in Settings now
actually takes effect: settings.microphoneDevice flows from the Svelte
store, through the Tauri invoke, into MicrophoneCapture::start_with_device
on the Rust side.
Touched paths (back-to-front):
- src-tauri/src/commands/audio.rs:start_native_capture — new optional
`device_name: Option<String>` parameter; routes to start_with_device
when set, falls back to auto-select start() when None or empty.
- src-tauri/src/commands/live.rs:StartLiveTranscriptionConfig — new
optional `microphone_device: Option<String>` field with same
semantics (rename_all = "camelCase" maps it to microphoneDevice on
the wire).
- src-tauri/src/commands/live.rs:run_live_session — picks
start_with_device when an explicit name is provided.
- src/lib/pages/DictationPage.svelte — passes
microphoneDevice: settings.microphoneDevice || null in the invoke.
Behaviour:
- "Auto" in the picker (empty string) -> backend auto-selects, skipping
monitor sources and validating by RMS energy.
- Specific device -> backend opens that device by exact name; if it has
been disconnected the user gets a clear error pointing them back at
Settings.
cargo check -p kon-audio passes clean. Tauri-crate cargo check requires
cmake (pre-existing infra dependency for whisper-rs-sys); install via
`sudo dnf install cmake clang-devel`.
Day 1 of the upgrade plan (output/reports/kon-upgrade-plan-2026-04-17.md
in the CORBEL workspace). Fixes the HANDOVER.md blocker: native live
transcription was capturing silence because PulseAudio/PipeWire monitor
sources (speaker loopback) were winning the device-selection race —
they deliver zero-valued bytes that satisfied the original
"any device that produces data within 350ms" check.
WHAT CHANGED:
crates/audio/src/capture.rs (rewrite):
- New `DeviceInfo` struct (serde-derived) for the Settings device picker
- New `MicrophoneCapture::list_devices()` enumerates inputs with metadata
- New `MicrophoneCapture::start_with_device(name)` for explicit selection
- Refactored `start()` with monitor-source filtering by name pattern
(.monitor suffix, "Monitor of " prefix, "loopback" substring) and
RMS-energy validation in a 350ms window
- Two-pass selection: real inputs first, monitor sources only as
last-resort fallback with explicit warning log
- Drop counter (Arc<AtomicU64>) tracks chunks dropped by `try_send`
failure under load — Codex review caught this as a silent-failure risk
- `dropped_chunks()` accessor for the live session
- Verbose tracing at every step for diagnosis
- Unit test for monitor-name detection
- `cargo check -p kon-audio` passes clean
crates/audio/src/lib.rs:
- Re-export `DeviceInfo`
crates/audio/Cargo.toml:
- Add serde dependency (for DeviceInfo derives)
src-tauri/src/commands/audio.rs:
- New `list_audio_devices` Tauri command (returns Vec<DeviceInfo>)
src-tauri/src/lib.rs:
- Register `list_audio_devices` in invoke_handler
src/lib/pages/SettingsPage.svelte:
- New "Audio" section with microphone picker dropdown
- Auto-populates on mount via `list_audio_devices`
- Refresh button + clear messaging about monitor sources
- Likely-monitor entries marked disabled in the dropdown
- Auto mode is the default (empty string in settings.microphoneDevice)
src/lib/stores/page.svelte.js:
- New `microphoneDevice` field in defaults (empty = auto-select)
NEXT STEPS (per the upgrade plan):
- Wire `microphoneDevice` from settings into `MicrophoneCapture::start_with_device`
in the live and standalone capture paths (currently both still call
the auto-selecting `start()`)
- Test on real hardware (Wayland + multiple input devices)
- Codex sanity-check of this diff is running in parallel; addendum to
follow if anything substantive comes back
Refs: /home/jake/Documents/CORBEL-Projects/kon/HANDOVER.md
output/reports/kon-upgrade-plan-2026-04-17.md (CORBEL workspace)
prevTaskIds was declared as $state, causing the $effect that writes to
it to re-trigger itself infinitely (effect_update_depth_exceeded). The
variable is just a comparison reference — doesn't need to be reactive.
Changed to a plain let.
Also removed debug event loggers and reverted grain z-index.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>