Affordances on MicroSteps rows (thumbs/edit/speaker/just-start) and
transcript-viewer segments (star, copy, delete) used opacity-0 →
opacity-100 on group-hover, which (a) hid them from keyboard users
until focus and (b) violated the "always-visible affordance" line in
the Kon design audit.
Replace with opacity-30 baseline + group-hover:opacity-100 +
focus-visible:opacity-100 (MicroSteps) / focus-within:opacity-100
(viewer, since the actions sit inside an interactive segment row).
Result: affordances are calm but visible at rest, lift on hover or
keyboard focus, never invisible.
HistoryPage was checked — its row checkbox at line 757 is already
opacity-50 → opacity-100 on hover, the pattern this PR is moving
toward, so no change needed there.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bundles a session of dogfood UX feedback plus the two Cursor Bugbot
findings on the auto-titles branch.
Onboarding (FirstRunPage):
- Welcome leads with "Set up automatically"; system breakdown and the
full model list move behind a "Choose manually" disclosure
- Morning / evening / autostart modal copy trimmed to one short
sentence each; CTAs shortened
- "Corbie" autostart string reverted to "Kon"
- Already-downloaded models are clickable in the picker so the
Settings → About → Replay onboarding flow doesn't re-download
- Autostart "No thanks" now does isEnabled() → disable() to actually
remove the OS login item when replaying after a previous "Yes"
Tasks page:
- Bucket nav (All / Inbox / Today / Soon / Later) now a horizontal
pill row; was stacking because nav was block-level
- List sidebar sized to content via self-start max-h-full instead of
stretching to viewport when sparse
- Energy chip surfaces at opacity-60 when unset (was opacity-0,
hidden until hover) so the affordance is discoverable
- "Brain-Dead" energy label → "Zero" everywhere user-facing; enum
stays brain_dead to avoid a destructive DB migration
LLM status chip (llmStatus.svelte.ts + Dictation/Settings):
- Chip no longer auto-warms when the engine isn't loaded; it's hidden
unless ready / generating / loading / error
- refreshLlmStatus takes { force: true } so post-load reconcile clears
stale "warming"; ambient refreshes still preserve in-flight state
- markError exported; failed loads surface "AI error" with detail
rather than silently going to off
- check_llm_model is the source of truth (replaces the bool-only
get_llm_status path in the store)
Float popout window:
- Native decorations off — was stacking two titlebars + two close X's
on KWin, one of which silently failed
- ResizeHandles mounted outside the animate-float-enter wrapper so
fixed-position handles anchor to the viewport, not the transformed
root; secondary-windows capability gains
core:window:allow-start-resize-dragging for tasks-float
- GTK Utility WindowTypeHint applied pre-map (mirroring the preview
window) so KWin Wayland honours always-on-top reliably
- visible_on_all_workspaces(true) so the pinned tasks list follows
workspace switches
- togglePin does hide()+show()+focus() on re-pin to nudge the
compositor into re-evaluating window state
- Pop-out / Edit / Open viewer buttons hidden on Android via
isAndroid() — the multi-window Tauri commands stub out there
Build / Bugbot:
- src-tauri Cargo.toml: whisper feature now chains whisper-vulkan, so
the dev runner's --no-default-features --features whisper
invocation actually pulls Vulkan acceleration instead of silently
falling back to CPU-only
- jsconfig.json's inherited "types": ["node"] fixed by adding
@types/node; corresponding @ts-expect-error in vite.config.js
removed now that process is a known global
Verification: svelte-check + cargo check pass clean. Manual
device-side validation still pending for float resize and replay
autostart "No thanks" — those are the only remaining confidence items.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the 15-second meeting-detection setInterval was started in
onMount unconditionally (when Tauri runtime was available). When
`settings.meetingAutoCapture` was disabled the callback still fired every
15 s, just to early-return — burning a wakeup that did no useful work
and confusing "is this firing? did the toggle take effect?" debugging.
Move the timer into a `$effect` whose only tracked dependency is
`settings.meetingAutoCapture`. Toggling off now clears the interval; toggling
on creates a fresh one. Reads of `meetingAutoCaptureApps` and `globalHotkey`
happen inside the interval callback (post-setup) so they don't trigger the
effect to tear down on every keystroke in the apps editor.
The `meetingCapturePoller` variable and its `onDestroy` cleanup are gone —
the effect's own cleanup return takes care of it on unmount.
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
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.
Frontend-owned nudge bus that consumes in-app signals Corbie already
produces, applies suppression, and fans out to OS notification + an
optional TTS read-aloud. OS-wide keyboard/window activity detection
stays deferred per the revised roadmap — the plan before rewrite
would have been brittle on Wayland, permission-heavy on macOS, and
low-quality everywhere.
Triggers (v1, all in-app signals):
- inactivity_with_active_timer — timer running, window blurred ≥ 90 s,
at least 60 s into the timer.
- pending_morning_triage — ritual enabled, past 10:00 local, last
shown ≠ today. Polls every 5 min while focused.
- micro_step_idle — micro-step decomposition created, no child step
or parent task completed within 15 min.
Suppression:
- Respects nudgesEnabled + nudgesMuted.
- No nudge while the app has focus (document.hasFocus).
- Hard cap 3 per rolling hour.
- Permission requested via @tauri-apps/plugin-notification on first
delivery; denial is silently respected.
Rust side:
- tauri-plugin-notification registered + ACL entries on the main-
window capability only (secondary windows can't fire nudges).
- commands::nudges::deliver_nudge — thin wrapper, security-guarded
via ensure_main_window, delegates to the plugin. No DB writes —
the roadmap's nudges-audit table is deferred until a concrete need
emerges.
Frontend glue:
- nudgeBus.svelte.ts — subscribes to window events, applies
suppression, calls deliver_nudge (+ tts_speak when speakAloud is
on).
- kon:task-completed now dispatched on complete_task_cmd success.
- kon:microstep-generated + kon:step-completed dispatched from
MicroSteps so the idle trigger can clear itself on any engagement.
- kon:focus-timer-cancelled added to focusTimer so the bus can reset
its inactivity state on cancel, not only on natural completion.
- nudgeBus started from +layout.svelte onMount, stopped on destroy.
Settings:
- New "Nudges" section with three toggles: Enable nudges, Mute for
now (separate so a hard mute doesn't lose preferences),
Speak nudges aloud (reuses Phase 4 TTS).
- All default OFF. No first-run prompt — nudges are a Settings-found
feature rather than a walkthrough step.
Out of scope per the revised Phase 6 spec: OS-wide keyboard/window
hooks, biometric signals, custom trigger editor (Phase 7), notification
sound (platform variance too high for Layer-1 — revisit in Phase 9
polish with a bundled .wav).
Gates: fmt clean, clippy -D warnings clean, cargo test 262/0 (4
existing + 262 current), npm run check 0/0, npm run build green.
Three opt-in rituals, all default OFF. Research-anchored (Barkley's
point-of-performance, Sweller cognitive-load theory, Newport shutdown
ritual, Gollwitzer implementation intentions, Thaler/Sunstein nudge
with informed consent for the ADHD audience).
Morning triage: modal gated on ritualsMorning toggle, configurable
trigger time (default 08:00 to respect ADHD sleep inertia rather than
the spec's 06:00), "pick up to three for today" with a gentle swap
message on the fourth attempt. Skip sets last-shown-today so it never
re-prompts the same calendar day. last-shown persists via kon_storage.
Evening wind-down: dedicated page, user-triggered only (tray menu +
Settings button). Mechanical closure + physical reset + intentional
cue — the whole Newport template. Open loops are read-only reflection;
Tasks page owns transactions. Copy is additive throughout: "you
finished X today", never "you didn't finish Y".
Autostart: tauri-plugin-autostart registered (LaunchAgent on macOS,
.desktop on Linux, registry Run on Windows). No bespoke Rust commands
— frontend calls the plugin's invoke-handlers directly. Toggle in
Settings is one-way (click → OS call → state update) to avoid the UI
lying during the round-trip. First-run presents a forced-choice prompt
for all three options, with "skip all" escape hatches per step.
Copy audit against RSD literature: no "overdue", "failed", or
day-to-day comparison framing anywhere in ritual surfaces.
Post-v0.1 ideas captured in the roadmap: calendar integration
(read-only ICS as interim, cloud sync parked) and right-click-to-task
(in-app simple, system-wide a separate phase).
Platform-dispatched TTS (spd-say + espeak-ng fallback on Linux, say on
macOS, PowerShell System.Speech on Windows) with a shared SpeakerButton
component. Tap to speak, tap again to stop; only one button speaks at
a time so two surfaces don't talk over each other. Text always travels
via argv (or a PowerShell here-string delivered through -EncodedCommand
on Windows) so user content never enters a shell string.
Mount points: DictationPage transcript footer, transcript viewer header,
per-step in MicroSteps. Settings gains a "Read aloud" accordion with
voice picker (lazy-loaded from the OS synth), rate slider 0.5-2.0x,
and a British-English test utterance.
Rust tests cover rate mapping, NaN handling, and Windows here-string
terminator safety. No pause/resume, no SSML, no cloud voices — that
stays out of scope per the Layer-1 roadmap.
Jake's feedback on Phase 1: make the timer pinnable / always-on-top,
combined with the existing Now-list pop-out. Two changes:
1. Mount <FocusTimer /> in src/routes/float/+layout@.svelte so the
running countdown stays visible in the always-on-top float window
alongside the WIP task list. No content change to the float page
itself — the timer is a global overlay.
2. Add a pop-out icon to the main-window focus timer that opens the
existing /float route via window.open. One click → timer + Now
list pinned on top without touching main window focus. Hidden
inside the float window itself (detected via URL) so you cannot
recursively pop out.
Result matches the Todo float-out UX the user already knows:
click ExternalLink, you get a small always-on-top window with
tasks + a live countdown ring in the top-right.
Closes the Core MVP gap in docs/brief/feature-set.md ("visual time
representation") and wires the dangling kon:start-timer emit that
MicroSteps.svelte has been firing into the void since the stub was
written. Implements phase 1 of the 2026-04-23 feature-complete
roadmap.
New:
- src/lib/stores/focusTimer.svelte.ts — singleton timer store with
localStorage persistence so a timer started in Dictation survives
page nav, window close, and reopen. Gentle WebAudio chime at
completion (no bundled asset). 250 ms tick. Completion flash for
3 s before auto-clear.
- src/lib/components/FocusTimer.svelte — floating top-right overlay
with SVG progress ring (shrinking colour: accent -> warning in
the final 15% -> success on completion). Cancel + "+1 min" on
hover. Renders nothing when idle.
Wired in +layout.svelte next to ToastViewport.
Two triggers now in the app:
- MicroSteps row 2-min button (pre-existing emit, previously no
listener)
- WipTaskList row 5-min button (new; the brief's "just-start"
from the Now column)
Respects prefers-reduced-motion via the existing
[data-reduce-motion] attribute.
Out of scope, carried to later phases: rhythmic voice anchoring
(Phase 6), custom-duration picker, multi-timer UI, native OS
notification (deferred to Phase 6 with the full nudge pipeline).
The LLM runtime has been quiet since it shipped in Phase 3 — users
had no surface-level signal that cleanup was loaded, warming, or
actively generating. Settings has verbose status text internally,
but a dictation-flow user never opens Settings during a run.
New: a shared $state store drives a small chip in the sidebar that
reflects the true LLM state in ≤500 ms of any transition (brief
item #31 acceptance). Five states:
off → hidden (user has aiTier === "off")
warming → model download or first load in flight; amber pulse
ready → loaded + idle; green dot
generating → cleanup_transcript_text_cmd or
extract_tasks_from_transcript_cmd in flight; accent
pulse with Sparkles icon
error → last operation failed; red dot with AlertTriangle
The store exposes three calls: refreshLlmStatus(aiTier) (polls the
backend), markGenerating(detail) / markGenerationDone(success).
DictationPage wraps its cleanup + extract calls in mark-generating
pairs. SettingsPage's LLM load / unload / delete / download paths
also refresh the global store so Settings-initiated transitions
surface in the sidebar immediately. The chip collapses to a
dot-only compact form when the sidebar is collapsed.
No backend changes — everything wires onto the existing
`get_llm_status` Tauri command.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Kon's ideology rule: raw Whisper output is the source of truth; LLM
cleanup is additive, never destructive. The preview overlay already
tracks both rawText and finalText across the listening → live →
cleanup → final phases — but until now the user had no one-click path
from final to raw if cleanup changed their meaning.
Frontend: a context-aware "Use raw" / "Copy raw" button appears in
the preview overlay's final phase, only when rawText and finalText
actually differ (Raw format mode or LLM-off leaves the button hidden).
Two behaviours depending on how the transcript reached the target:
- settings.autoPaste = true → invoke paste_text_replacing, which
sends the platform's undo keystroke to the focused app,
waits UNDO_PASTE_GAP_MS (60 ms) for the compositor / app to
process it, then pastes the raw transcript. The preview hides
itself beforehand so the keystroke doesn't race focus
(existing Wayland-hardening path).
- settings.autoPaste = false → nothing was pasted in the first
place, so just overwrite the clipboard with raw. User's own
paste yields raw.
Backend: new paste_text_replacing Tauri command plus a mirror of the
paste-backend matrix for undo (wtype -M ctrl z / xdotool key ctrl+z /
ydotool keycodes 29:1 44:1 44:0 29:0 / osascript cmd+z / SendKeys '^z').
Reuses the pick_linux_backend_order Wayland-vs-X11 preference.
Registered in the Tauri command handler.
Acceptance per the brief: "after paste, Ctrl+Z within 5 s replaces
LLM output with raw transcript" — satisfied via the 4 s auto-hide
window on the preview's final phase. The click extends auto-hide so
the user actually sees the confirmation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.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>
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>
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>
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>
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>
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.
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
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>
The invisible z-40 overlay was still eating pointer events across the
main content. Removed the backdrop — sidebar now floats without blocking
anything. Close with Escape key.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The full-screen backdrop (fixed inset-0 z-40) was eating all pointer
events, making the app unusable when the task sidebar was open. Replace
with a backdrop that only covers the content area (not titlebar) and
sits beside the sidebar rather than wrapping it.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add kon-hotkey crate with evdev-based global hotkey capture that works on
Wayland (and X11). Patterns from whisper-overlay: per-device async listeners,
inotify hotplug with udev permission retry, watch channel for live config
updates. Frontend detects Wayland at startup and selects evdev or
tauri-plugin-global-shortcut automatically.
Model downloads now support HTTP Range resume for interrupted downloads and
optional SHA256 integrity verification (incremental, no second pass).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add sidebarCollapsed to settings defaults (persists via localStorage on saveSettings)
- Sidebar collapses to 48px icons-only view with smooth 200ms transition
- Toggle button (chevron) at top of nav; [ keyboard shortcut in layout (input-safe)
- Auto-collapse on window resize below 900px (checked on mount and on resize event)
- Titlebar left spacer tracks sidebar width reactively (48px / 210px)
- All labels, profile selector, status text hidden in collapsed mode; nav items show tooltips via title attribute
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Backdrop tint div has direct onclick to close sidebar
- Sidebar panel uses stopPropagation so clicks inside don't close
- Outer container uses target check for self-click dismiss
- Svelte 5 compatible (no |self modifier)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
TaskSidebar:
- Changed from flex layout member to fixed overlay with backdrop
- Click backdrop or press Escape to close
- Content no longer shrinks when sidebar opens — buttons stay accessible
- Positioned at z-50 over main content, top offset for titlebar
Settings — Parakeet model management:
- Added Parakeet model state: download, check, load status
- New downloadParakeet() and loadParakeet() functions
- Listens for parakeet-download-progress events
- Parakeet section shows download/load/status controls
Settings — conditional engine sections:
- Whisper model selector only shows when engine === "whisper"
- Parakeet model controls only show when engine === "parakeet"
- Cleaner UI — only active engine's options visible
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Float window fixes:
- Root layout now detects /float and /viewer URLs and hides Sidebar,
Titlebar, TaskSidebar for secondary windows. Belt-and-braces fix
since +layout@.svelte should handle this but may not in SPA mode.
- Float window title: "Ramble - To-do" → "Kon - To-do"
Transcription doubling guard:
- Added chunk_id deduplication set in DictationPage — if a chunk_id
has already been processed, skip the duplicate result
- Set cleared on each new recording session
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 probe_system command: returns RAM, CPU, OS info
- New rank_models command: scores models against detected hardware
- FirstRunPage.svelte: hardware summary, ranked model list with
"Recommended" badge on top pick, download progress bar, skip option
- First-run detection in layout: checks list_models + list_parakeet_models,
shows wizard if both empty
- Sidebar hidden during first-run for clean wizard experience
- clippy clean
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Spinner: reversed sinhala-spin from counter-clockwise to clockwise
- Sidebar nav lock: removed {#key page.current} block that forced full
destroy/recreate of pages on every navigation. DictationPage's AudioWorklet
cleanup in onDestroy was blocking the UI during the {#key} destruction cycle.
Now uses {#if} blocks only — smoother page transitions.
- Rebranded sidebar: Ramble → Kon, v0.2 → v1.0
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>