Commit Graph

298 Commits

Author SHA1 Message Date
697642fa4d ci(check): add svelte-check and workspace lib tests
svelte-check catches type/template errors Vite's build skips; cargo
test --workspace --lib runs our pure-unit suites (prompt contract,
hallucination filter, preset parsing) without GPU or runtime deps.
Test step is Linux-only so the Windows/macOS legs stay focused on
platform compile coverage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 17:32:18 +01:00
ce2b4fdac6 feat(ai B.1 #15 + A.1 #28): cleanup presets and sequential-GPU guard
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
Two new Settings → AI knobs that compose cleanly with what already
shipped (aiTier, LLM model, translator prompt framing).

**B.1 #15 — Named cleanup presets.** LlmPromptPreset enum
(Default / Email / Notes / Code) appends a short context hint onto
the CLEANUP_PROMPT just before generation. Presets shape tone and
structure ("email paragraph", "bulleted meeting notes", "preserve
technical terms") without licensing the content-editing the
translator-not-editor framing forbids. cleanup_transcript_text_cmd
now takes `preset: Option<String>` which runs through the new
LlmPromptPreset::parse (normalises aliases like "meeting-notes",
collapses unknown values to Default).

**A.1 #28 — Sequential-GPU guard.** New LocalEngine::unload drops
the backend + model_id so a subsequent load actually reclaims VRAM.
load_llm_model, load_model, and load_parakeet_model Tauri commands
grow an optional `concurrent: bool` argument. When concurrent is
Some(false), loading LLM first unloads whisper+parakeet, and vice
versa — prevents VRAM OOM on tight-VRAM setups. Default is the
previous parallel behaviour so nothing changes for multi-GB cards.
Transcribe-in-progress paths (transcribe_pcm, transcribe_file, live)
pass None, so mid-dictation model loads don't accidentally tear
down the LLM.

Settings UI (AI section):
- Cleanup preset segmented button + descriptive copy for each option.
- GPU concurrency segmented button with explicit trade-off text
  ("faster transitions vs fits in tight VRAM").

Frontend wiring:
- settings.llmPromptPreset flows from DictationPage's
  cleanupTranscriptIfEnabled into the Tauri command.
- settings.aiGpuConcurrency flows from both DictationPage (auto-load
  on record) and SettingsPage (manual load/unload buttons) as
  `concurrent: "parallel" === true` to the load commands.

Tests: three new preset cases in crates/ai-formatting/src/llm_client.rs
(parse aliases, suffix non-empty for non-default, default suffix
empty). All 139 existing lib tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 17:12:48 +01:00
a57da0feb5 feat(llm B.1 #27): Test LLM button with classified error diagnostics
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
The brief's pain point is opaque load failures: llama-cpp-2's errors
bubble up as raw C++ strings ("cudaMalloc failed: out of memory",
"invalid gguf magic"). A user seeing that has no path to recovery.

New backend command test_llm_model runs a staged diagnostic:
  1. Model not downloaded → `not-downloaded` + download hint.
  2. File size ≤90% of expected → `incomplete` (stalled download)
     + re-download hint. Matters because llama-cpp-2 can segfault
     on truncated GGUF rather than returning cleanly.
  3. Requested model already loaded → `ready`, no side effects.
  4. Otherwise attempt a real load. On failure, classify_llm_load_error
     maps the raw string to one of:
       - load-failed-vram         (OOM / cudaMalloc / allocation)
       - load-failed-corrupt      (GGUF magic / unsupported format)
       - load-failed-permission   (permission denied / access denied)
       - load-failed-other        (catch-all)
     Each category has a prewritten actionable hint pointing at the
     specific Settings surface (tier picker, re-download, file perms).

classify_llm_load_error is pure-string and unit-tested — 8 cases
covering the main categories plus edge cases (OOM alias, Windows
"Access is denied", unknown errors). Ordered narrow-to-broad so
overlap doesn't misclassify.

Settings UI gets a "Test" button in the AI section's action row,
visible whenever the model is downloaded (both downloaded-idle and
loaded states). Shows inline hint below the status line when the
test surfaces one. Refreshes both local and global LLM status after
the test since a successful test implicitly loads the model.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 17:04:11 +01:00
70b97c5273 feat(ui B.1 #20): sound cues on start / stop / complete via Web Audio
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
Hands-off feedback for recording lifecycle: a short C5 pitch at
record start, a falling G4 at record stop, and a staggered C5–E5–G5
major third when the finalise flow completes. Synthesised at runtime
via the Web Audio API (OscillatorNode + linear-ramped GainNode
envelope) rather than shipping WAV assets — keeps the binary size
flat and lets us tweak timbre without touching bundled files.

Off by default. Settings → Output exposes the toggle with a volume
slider (0–100%, default 15%) and a "Test" button that plays the
completion cue so the user can confirm loudness without recording.

Hooked at three call sites in DictationPage:
- playStartCue after page.recording = true in startRecording
- playStopCue at the top of stopRecording
- playCompleteCue just before `saved = true` at the end of
  finaliseTranscription's transcript-present branch

All three no-op when settings.soundCues is false. The Web Audio
context is lazily constructed on first cue (most browsers suspend
it until a user gesture — Tauri's webview inherits that). If the
AudioContext can't be built we silently drop the cue rather than
throwing.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 16:48:44 +01:00
1dd09e14ca feat(ai-formatting A.1 #26): detect prompt-loop repetition cascades
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
ufal/whisper_streaming #161 documents the classic Whisper streaming
failure: on ambiguous audio the model falls into a prompt loop,
cascading a single token for 10+ words ("I I I I I I I I I I I…").
The chunk-boundary duplicate detector in live.rs doesn't catch
this — the repeat is within a single chunk, and the text is
technically novel so FTS is happy to keep it.

Fold the detection into is_hallucination as a third pass (after
HALLUCINATION_MARKERS substring-match and HALLUCINATION_TRAIL_PHRASES
exact-match). has_consecutive_repetition walks the token stream
(whitespace-split, lowercased) and returns true when any run of
≥REPETITION_RUN_THRESHOLD (4) identical tokens is found.

Threshold chosen deliberately: three consecutive matches appear in
normal speech ("no no no, that's wrong"), four almost never does.
Tests pin both sides — "I I I I I" detected, "no no no" allowed,
alternating patterns ("I am I am I am I am") allowed regardless of
length.

Phrase-level repetition ("thank you thank you thank you thank you")
is a documented companion failure mode but needs a sliding n-gram
matcher — deferred with a code comment flagging it.

No caller changes: post_process_segments already drops
is_hallucination hits when anti_hallucination is enabled.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 16:44:22 +01:00
f525004d05 feat(ai-formatting A.1 #22): expand hallucination blocklist for subtitle-training leakage
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
Whisper was trained on subtitle corpora, so silence and room tone
trigger caption-style artefacts that the previous three-marker
blocklist ("[blank_audio]", "[music]", "[silence]") didn't catch:
"Thanks for watching!", "Please subscribe.", "ご視聴ありがとうござ
いました", "♪♪♪", etc. Documented in WhisperLive #185 / #246 and
ufal/whisper_streaming #121 as the top streaming-transcript-quality
issue after chunk-boundary repeats.

HALLUCINATION_MARKERS widens from 3 to 16 entries: all common
bracketed non-speech tags (applause / laughter / inaudible /
background noise / sounds), parens variants, and musical notation
(♪ / ♫). Still contains-match so the marker triggers even when
Whisper wraps it in other noise.

HALLUCINATION_TRAIL_PHRASES (renamed from AUTO_THANKS_PHRASES) jumps
from 4 to ~30 entries: YouTube sign-offs, subtitle-credit leakage,
and the two most common non-English variants (Japanese "thanks for
watching" + MBC Korean news sign-off). Stays exact-match so
legitimate dialogue containing "thanks" or "subscribe" mid-sentence
never gets dropped — a new regression test pins that invariant.

The <15-char length gate on trail phrases is removed; some of the
new entries (e.g. "please subscribe to our channel.") are longer.
Exact-match against a known list is safety enough.

No caller changes: post_process_segments already drops segments for
which is_hallucination returns true when anti_hallucination is on.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 16:41:55 +01:00
ad311d278f feat(sidebar B.1 #31): visible LLM status chip with live state
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
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>
2026-04-21 16:40:02 +01:00
ae4c1e3c6d feat(preview B.1 #17): raw-transcript revert button in preview overlay
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
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>
2026-04-21 16:29:43 +01:00
e10f435eb1 feat(transcription A.1 #23): silent warm-up inference after Whisper model load
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
prewarm_default_model loaded the model and returned. That moves the
model into RAM, but whisper.cpp still allocates its context window +
fills GPU shader caches on the first inference call — producing the
~4–5 s cold-start latency documented in ufal/whisper_streaming #96
and #135 that feels like "Kon dropped my first sentence."

Extend the pre-warm task: after engine.load, feed one second of
silence (16000 zero samples at 16 kHz) through transcribe_sync with
default options. Silence returns empty segments; the *work* is the
context allocation, which now happens at app boot rather than on the
user's first hotkey press.

Net: the user's first real dictation should complete within ~1.5× the
steady-state RTF they'll see on subsequent runs, satisfying the A.1
#23 acceptance criterion. No new public API; all inside the existing
spawn_blocking.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 16:25:09 +01:00
42ba18a274 feat(ai-formatting B.1 #16): reframe CLEANUP_PROMPT as translator, not editor
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
The previous prompt led with "You are a transcript cleanup assistant"
and listed cleanup rules. That framing quietly licenses the LLM to
treat cleanup as content editing — rephrasing for clarity, summarising
long sentences, "improving" phrasing. That's precisely the failure
mode OpenWhispr / Scriberr / Whispering users complain about ("the
LLM changed my meaning").

New framing lifts Whispering's published baseline: "translator from
spoken to written form — not an editor trying to improve the content."
Adds an explicit rule: do NOT improve, summarise, expand, or rephrase;
faithful written-form translation only, never content editing.

Both load-bearing concerns are now regression-tested — the existing
prompt-injection hardening assertions stay, and a new test pins the
translator framing + explicit no-editing rule against drift during
future refactors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 16:23:06 +01:00
3790fa0c91 docs(readme): add project README with architectural profile
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
Previously the repo had no root README. Pitch + architecture + crate-level
breakdown + routes + runtime stack + roadmap all lived in dated HANDOVER
docs + docs/brief/ + memory. This pulls the canonical overview into the
single place a new contributor / investor / cursory browser will actually
land first.

Sections: design principles, what Kon does today (shipped capabilities),
architecture (3-layer: Rust workspace → Tauri commands → Svelte UI),
crate-by-crate responsibilities, Tauri command surface, frontend shape,
runtime stack with pinned versions, platform support matrix, build + dev
setup, testing, pointers into docs/, roadmap, contributing notes, licence
placeholder, contact.

All claims are grounded in current code state (136 tests, 9 crates, 18
Tauri command modules, Tauri 2.10.3, whisper-rs 0.16, llama-cpp-2
0.1.144, sqlx 0.8.6). No marketing puff; preserves the ideology-firm
stance from memory.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 16:11:16 +01:00
dependabot[bot]
f8c9769e04 chore(deps-dev): bump picomatch
Bumps the npm_and_yarn group with 1 update in the / directory: [picomatch](https://github.com/micromatch/picomatch).


Updates `picomatch` from 4.0.3 to 4.0.4
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/4.0.3...4.0.4)

---
updated-dependencies:
- dependency-name: picomatch
  dependency-version: 4.0.4
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-21 15:10:24 +00:00
dependabot[bot]
becbf69c35 chore(deps-dev): bump vite in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the / directory: [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite).


Updates `vite` from 6.4.1 to 6.4.2
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v6.4.2/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v6.4.2/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 6.4.2
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-21 15:10:23 +00:00
dependabot[bot]
b8b953dfa8 chore(deps-dev): bump @sveltejs/kit
Bumps the npm_and_yarn group with 1 update in the / directory: [@sveltejs/kit](https://github.com/sveltejs/kit/tree/HEAD/packages/kit).


Updates `@sveltejs/kit` from 2.55.0 to 2.57.1
- [Release notes](https://github.com/sveltejs/kit/releases)
- [Changelog](https://github.com/sveltejs/kit/blob/main/packages/kit/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/kit/commits/@sveltejs/kit@2.57.1/packages/kit)

---
updated-dependencies:
- dependency-name: "@sveltejs/kit"
  dependency-version: 2.57.1
  dependency-type: direct:development
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-21 15:10:15 +00:00
jake
e75f676fc1 feat(docs): add brief and brand reference docs to phase-2 branch
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-21 16:03:49 +01:00
8ba5641451 feat: add design system handoff to feat/design-system branch
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>
2026-04-21 16:03:49 +01:00
Cursor Agent
0338495a57 fix(vocab): dedupe bulk import case-insensitively within the paste
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
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>
2026-04-21 16:00:03 +01:00
Cursor Agent
74062f8381 fix(capabilities): cover transcription-preview window in default ACL
The preview window was missing from the default capability's window
list. Its frontend calls core:window:allow-hide (getCurrentWindow().hide()
on dismiss / auto-hide) plus invoke('copy_to_clipboard') and a handful
of event listeners — none of those would have been permitted at runtime
because the capability never matched this window's label.

Add 'transcription-preview' alongside the other secondary windows so
the preview actually has access to the permissions it already relies on.
Regenerate gen/schemas/capabilities.json to match.

Co-authored-by: jars <jakejars@users.noreply.github.com>
2026-04-21 16:00:03 +01:00
Cursor Agent
ded8811ca9 feat(B.1 #10): detect focused terminal and switch to clipboard-only paste
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
Extends commands/paste.rs::paste_text with a pre-keystroke check:
if GetForegroundWindow (Windows) / xdotool getactivewindow (Linux
X11) / 'tell System Events ...' (macOS) reports a focused app class
matching KNOWN_TERMINAL_CLASSES, skip the synthesised Ctrl+V and
return an outcome with copied=true, pasted=false, and a user-facing
message ('Terminal detected (kitty) — use Ctrl+Shift+V to insert').

Matches Handy #692: Kitty/Alacritty/Windows Terminal/Codex CLI all
double-insert the transcript when a PTY sees both a synthesised
Ctrl+V and the terminal's own paste hotkey. The terminal list is
ordered most-specific-first so 'windowsterminal' wins over the
generic 'terminal' needle.

Adds classify_terminal() as a pure helper + seven unit tests. The
platform probe (detect_focused_window_class) isolates the fragile
shell-out so the classification rule is test-covered without
needing a real desktop. Wayland doesn't expose a reliable
focused-window API to unprivileged clients, so it conservatively
returns None and the normal paste path runs — consistent with
Kon's Wayland-Pipewire lane.

Prior-clipboard restore (#3) is intentionally skipped when terminal
mode takes over: the user's path to insert the transcript is their
own paste, which needs the transcript on the clipboard.

Co-authored-by: jars <jakejars@users.noreply.github.com>
2026-04-21 15:59:01 +01:00
Cursor Agent
df58d98adc feat(B.1 #5): per-OS hotkey capability matrix with inline rejection copy
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>
2026-04-21 15:59:01 +01:00
Cursor Agent
8e5e034df1 feat(B.1 #4 UX): debounce hotkey press events by 120ms
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>
2026-04-21 15:59:01 +01:00
Cursor Agent
f3a0673eaa feat(B.1 #3): snapshot prior clipboard and restore 300 ms after paste
Extends commands/paste.rs so that when auto-paste fires we:
  1. capture the user's existing clipboard text BEFORE overwriting
     it with the transcript (via arboard::Clipboard::get_text — a
     non-text clipboard returns None and the restore step is
     skipped, which keeps images / files safe),
  2. after the paste keystroke lands, sleep 300 ms,
  3. restore the snapshot only if the clipboard still holds the
     transcript we wrote — respects any Cmd+C the user did in the
     300 ms window.

Moves the decision to a pure should_restore(current, transcript)
helper with four unit tests. The existing paste-matrix regression
tests are unchanged.

Matches Handy #921 (workstream-B brief item #3). Coexists with the
Wayland preview-hide dance already in this file, and doesn't run
when the paste keystroke fails (transcript stays on the clipboard
for the user's own paste).

Co-authored-by: jars <jakejars@users.noreply.github.com>
2026-04-21 15:59:01 +01:00
Cursor Agent
cc3bffa72c feat(B.1 #11): versioned settings schema with forward migration
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>
2026-04-21 15:59:01 +01:00
Cursor Agent
db9e119c1b docs(phase4-ux): execution plan for Workstream B
Sequences the 13 B-scope items from docs/whisper-ecosystem/brief.md
into three phases (pre-emptive UX, feature pinches, LLM layer) with
stop-for-review boundaries between each.

Enumerates the Settings sections touched per item (net: +2 toggles,
+2 sub-cards, nothing invisible becomes visible), the new
SettingsState fields with defaults, the schema migration bump
(version 1 -> 2), and the explicit Workstream A dependencies +
stubbed fallbacks for each (#14 list_gpus, #30 streaming cleanup,
#31 llm-state-change event).

Co-authored-by: jars <jakejars@users.noreply.github.com>
2026-04-21 15:59:01 +01:00
Cursor Agent
2f763e124b fix(ci): set LIBCLANG_PATH on Linux; install Vulkan via brew on macOS
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
Two follow-ups to the previous CI deps commit:

1. Linux: libclang-dev installs libclang.so under
   /usr/lib/llvm-*/lib and /usr/lib/x86_64-linux-gnu, but
   bindgen-0.72.1 does not probe the llvm-versioned directory by
   default. Resolve the newest /usr/lib/llvm-*/lib candidate
   dynamically and export LIBCLANG_PATH so bindgen finds it without
   any clang-sys guesswork. Also install glslang-tools + spirv-tools
   so find_package(Vulkan COMPONENTS glslc) succeeds against
   ggml-vulkan's cmake step.

2. macOS: llama-cpp-sys-2 sets GGML_VULKAN=ON unconditionally when
   the vulkan feature is enabled; cmake's find_package(Vulkan) then
   fails on a vanilla macOS-latest runner because there is no
   Vulkan SDK shipped by default. The LunarG macOS SDK ships as a
   non-scriptable interactive installer, so we compose MoltenVK +
   Vulkan loader + headers + shaderc via Homebrew formulae instead.
   Export VULKAN_SDK=$(brew --prefix) and CMAKE_PREFIX_PATH=same so
   both the build.rs branch and cmake's FindVulkan resolve headers /
   libs / glslc from /opt/homebrew.

Applied symmetrically to check.yml and build.yml. Windows config is
unchanged.

Co-authored-by: jars <jakejars@users.noreply.github.com>
2026-04-21 15:54:15 +01:00
Cursor Agent
5c36bdec28 fix(ci A.1 #12): drop tauri.windows.conf.json — cargo check fails on unknown _comment field + missing resources
Two issues with the previous #12 approach, both caught by CI:

1. tauri-build rejects the '_comment' json field as unknown when
   parsing tauri.windows.conf.json:

     unknown field `_comment`, expected one of `$schema`,
     `product-name`, `productName`, ...

   The schema is strict, so the doc-comment has to live elsewhere.

2. tauri-build's bundle.resources list is resolved at build-script
   (cargo check) time, not at 'tauri build' time. With the DLLs
   intentionally gitignored for licensing reasons (see the dir's
   README), every cargo check run on Windows would fail.

Fix: delete tauri.windows.conf.json entirely. The intent of #12 —
'runtime falls back to CPU when Vulkan is absent' — is already
live in src-tauri/src/commands/models.rs::detect_active_compute_device,
unchanged.

Rewrite resources/windows/README.md to document a cargo tauri build
--resource ... invocation for the release engineer. That's the only
invocation that needs the DLLs present; everyone else (including
CI's cargo check) doesn't go near them.

This matches how Kon already handles CI/release split elsewhere
(macOS code-sign certs, Windows code-sign certs, etc. all stay out
of tauri.conf.json for the same reason).

Co-authored-by: jars <jakejars@users.noreply.github.com>
2026-04-21 15:54:15 +01:00
Cursor Agent
db8f1bf19d fix(ci): install libclang + Vulkan SDK on all platforms
Both whisper-rs-sys and llama-cpp-sys-2 run bindgen at build time,
which requires libclang.so/dylib/dll resolvable from the loader.
None of the three GitHub runners ship this out of the box today:
  - ubuntu-22.04: bindgen-0.72.1 panics with 'couldn't find any
    valid shared libraries matching libclang.so*'
  - macos-latest: same panic against libclang.dylib
  - windows-latest: choco already installed llvm here, but libclang
    was on an unset LIBCLANG_PATH, so bindgen still couldn't find it

And llama-cpp-sys-2's vulkan feature (wired on by whisper-rs' vulkan
feature → whisper-rs-sys + its own shared ggml build) hard-panics on
Windows when VULKAN_SDK is unset, and needs libvulkan.so linkable on
Linux.

Changes, applied symmetrically to check.yml and build.yml:
  - Linux: add libclang-dev, clang, libvulkan-dev to apt-get install.
  - macOS: brew install llvm, set LIBCLANG_PATH to brew --prefix
    llvm /lib so bindgen can load libclang.dylib.
  - Windows: choco install vulkan-sdk, set VULKAN_SDK to the
    newest-version directory under C:\VulkanSDK (resolved
    dynamically so a minor-version bump doesn't hardcode-break
    anything), set LIBCLANG_PATH to the llvm bin dir.

Unblocks the per-push cargo check job on main, phase4-systems-f7d0,
and phase4-ux-f7d0; also unblocks the release build. The Windows
Vulkan SDK install is the new long pole (~2 min on a cold runner)
but is needed unconditionally while the vulkan feature is on in
crates/transcription/Cargo.toml.

Co-authored-by: jars <jakejars@users.noreply.github.com>
2026-04-21 15:54:15 +01:00
Cursor Agent
c163a9a07b fix(ci): declare @chenglou/pretext dependency in package.json
src/lib/utils/textMeasure.ts imports @chenglou/pretext but the
package was only present in the local node_modules — npm reported it
as 'extraneous'. npm ci in CI refuses to install anything that isn't
declared, so vite build fell over at the Rollup resolve step:

    [vite]: Rollup failed to resolve import '@chenglou/pretext' from
    'src/lib/utils/textMeasure.ts'.

Pins to the version that was already installed locally (0.0.5) and
regenerates package-lock.json. npm run build now completes cleanly
through the SvelteKit / Vite / adapter-static pipeline.

Co-authored-by: jars <jakejars@users.noreply.github.com>
2026-04-21 15:54:15 +01:00
Cursor Agent
b6bd265176 feat(A.1 #12): Windows installer bundles Vulkan loader + libssl, graceful CPU fallback
Adds src-tauri/tauri.windows.conf.json with a bundle.resources list
for vulkan-1.dll, libssl-3-x64.dll, and libcrypto-3-x64.dll shipped
side-by-side with kon.exe, plus a src-tauri/resources/windows/README.md
explaining how release engineers populate the directory (licensing
constraints keep it manual rather than scripted).

The runtime fallback is already live from commit A.1 #1: if the
Vulkan loader is missing after launch, emit_runtime_warnings() fires
a runtime-warning event (kind: vulkan-loader-missing) and
get_runtime_capabilities() reports activeComputeDevice=cpu with a
reason. The app starts and transcribes on CPU — degraded but never
broken — so the acceptance criterion 'launches cleanly on a VM with
no GPU' holds by construction.

Matches Whispering #840/#829 and Buzz #1459 pain patterns.

Co-authored-by: jars <jakejars@users.noreply.github.com>
2026-04-21 15:54:15 +01:00
Cursor Agent
06e50281cb feat(A.1 #9): PowerAssertion guard around live session + LLM generation
Adds src-tauri/src/commands/power.rs exposing a PowerAssertion RAII
guard that macOS uses to pin NSProcessInfo.beginActivityWithOptions
around long-running work. Wired into:
  - run_live_session (entire live-dictation lifetime)
  - cleanup_transcript_text_cmd's spawn_blocking body (LLM run)

Non-macOS targets get a no-op guard so callers don't have to #cfg
the call sites. The actual Objective-C bridge to NSProcessInfo is
stubbed (begin_activity returns Err so the guard logs a warning
instead of silently pretending); the stub doesn't regress recording
or LLM behaviour on macOS — it just means App Nap is not yet
suppressed, which matches today's behaviour. Full objc2 integration
is a follow-up that can introduce objc2 cleanly in its own commit.

Matches Whispering #549/#559 pain-pattern; acceptance text ("10
minute background recording completes unattended") is satisfied
once the bridge is finished, and nothing regresses today.

Co-authored-by: jars <jakejars@users.noreply.github.com>
2026-04-21 15:54:15 +01:00
Cursor Agent
9266bf5463 feat(A.1 #8): harden transcription model downloads with sha + resume tests
Ports the kon-llm model_manager resume pattern the rest of the way
into kon-transcription::model_manager:
  - download() now validates an existing complete file against its
    sha256 before skipping; a hash mismatch removes the file and
    re-fetches, instead of serving a corrupt file to whisper.cpp.
  - download_file() now distinguishes 206 Partial Content, 200 OK
    (resume silently ignored by server), and other statuses, rather
    than treating any non-206 as 'just use it as a fresh start'.
    200-on-Range is handled by discarding the partial and starting
    over cleanly.
  - New tests: download_file_resumes_from_partial_and_verifies_sha
    (TcpListener fixture, same shape as kon-llm's), and
    download_file_fails_on_sha_mismatch_and_cleans_part_file.
  - sha256_of_file helper + unit test for the existing-file guard.

Dev-deps: tempfile + tokio(net/io-util/macros). Total workspace
lib-test count: 116 → 123.

Co-authored-by: jars <jakejars@users.noreply.github.com>
2026-04-21 15:54:15 +01:00
Cursor Agent
fafa0fc878 feat(A.1 #1): surface active compute device + runtime-warning banner
Extends get_runtime_capabilities() with three new fields:
  - activeComputeDevice: {kind, label, reason} — "GPU (Vulkan)" on
    the happy path, "CPU (fallback)" with a reason when the Vulkan
    loader is absent at runtime. libloading::Library::new probes
    libvulkan.so.1 / vulkan-1.dll / libMoltenVK.dylib per target OS.
  - cpuFeatures: { avx2, avx512f, fma, sse4_2, neon, hasGgmlBaseline }
    sourced from the new probe_cpu_features() helper. hasGgmlBaseline
    is the one flag the Settings banner actually reads.
  - parallelModeAvailable: placeholder false until Phase A.4 lands
    the real GPU VRAM probe + GpuGuard semaphore permit logic.

Adds emit_runtime_warnings(&AppHandle) called once at setup() after
prewarm_default_model. Emits a runtime-warning event with kind
"avx2-missing" or "vulkan-loader-missing" so Workstream B can
render a dismissible Settings banner without polling capabilities.

Contract matches docs/whisper-ecosystem/workstream-A.md §Item #1 for
Workstream B to consume without waiting for Phase A.2 to land the
real whisper_print_system_info bridge.

Co-authored-by: jars <jakejars@users.noreply.github.com>
2026-04-21 15:54:15 +01:00
Cursor Agent
770516460d feat(A.1 #7): runtime CPU feature detection (AVX2/FMA/AVX-512/SSE4.2/NEON)
Extends hardware::CpuInfo with a CpuFeatures struct populated via
std::is_x86_feature_detected! on x86_64 and an architectural
assumption for aarch64 (NEON). Adds has_ggml_baseline() so callers
can cheaply ask 'will whisper.cpp / llama.cpp ship a fast path on
this CPU?' without knowing the arch-specific rule.

The point of #7 is giving the runtime a way to surface a clear
"non-AVX2 fallback" warning before the user hits a wall of silent
slowness. The banner itself ships in the next commit as part of
get_runtime_capabilities (item #1). Tests cover the baseline helper
on both x86 and non-x86 targets.

Co-authored-by: jars <jakejars@users.noreply.github.com>
2026-04-21 15:54:15 +01:00
Cursor Agent
1bb39699f5 feat(A.1 #6): fail Windows build when tokenizers enters the dep graph
Adds a build.rs guard that parses Cargo.lock and panics on Windows
if the tokenizers crate ever appears in the workspace dependency
tree, mirroring the MSVC C-runtime conflict that broke Whispering
v7.11.0 when they linked whisper-rs-sys + tokenizers in the same
binary.

On non-Windows hosts the guard downgrades to a cargo:warning so
cross-compilation or CI from Linux surfaces the issue before a
Windows build attempt actually panics.

No tokenizers crate is in the tree today; the guard is preemptive.
If we ever legitimately need HF tokenizers on Windows, the escape
hatch is an out-of-process sidecar (separate CRT).

Co-authored-by: jars <jakejars@users.noreply.github.com>
2026-04-21 15:54:15 +01:00
Cursor Agent
be0684193f feat(A.1 #2): widen CSP connect-src for localhost LLM endpoints
Adds http://127.0.0.1:* and ws://127.0.0.1:* to the connect-src
allowlist so future BYO-cloud LLM integrations (Ollama on 11434,
llama.cpp server on 8080, LM Studio on 1234, etc.) can fetch(...)
without tripping the CSP.

Kon's bundled LLM (llama-cpp-2) is in-process and does not need HTTP,
but the localhost surface is the standard external LLM transport and
pre-approving it here lets Workstream B wire Ollama's Test Connection
(item #27) without re-spinning the CSP. No tauri-plugin-http is in
use today — when that lands the scope allowlist goes in capabilities.

Matches the pain pattern from Vibe #438 (Tauri scope blocking
127.0.0.1 LLM endpoints).

Co-authored-by: jars <jakejars@users.noreply.github.com>
2026-04-21 15:54:15 +01:00
Cursor Agent
35cfdfddf1 docs(phase4-systems): execution plan for Workstream A
Sequences the 18 A-scope items from docs/whisper-ecosystem/brief.md
into four phases (pre-emptive patches, engine abstraction, streaming
correctness, LLM guard) with stop-for-review boundaries between each.

Lists the new command + event contracts (#1 activeComputeDevice,
#14 list_gpus/set_preferred_gpu, #24 tentative segment flag, #28
parallel-mode toggle) that Workstream B will consume.

Co-authored-by: jars <jakejars@users.noreply.github.com>
2026-04-21 15:54:15 +01:00
da74a84009 ci: point Swatinem/rust-cache at the real workspace root
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
Both workflows had `workspaces: src-tauri -> target`, which tells
rust-cache the workspace lives at src-tauri/ and its target dir is
src-tauri/target. Neither is true: the workspace is defined at the repo
root (Cargo.toml:1–3 — members = ["src-tauri", "crates/*"]), so cargo
walks up and puts artifacts at ./target, not ./src-tauri/target.

Result: the cache action was saving an empty (or wrong) directory on
every run. Every CI run on every OS effectively started from a cold
build, which is the actual reason the Windows job appeared to compile
sqlx from scratch every push — it was compiling sqlx from scratch every
push.

Point the cache at the real workspace root. While here, drop the
`working-directory: src-tauri` on the cargo check step so the command
runs from the workspace root too; cargo finds the same workspace either
way, but running from root is consistent with the cache's view.

Expected impact: Windows check job drops from ~15–25 min cold-every-time
to ~2–3 min on warm runs, matching Linux/macOS behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 15:42:12 +01:00
26b41389b2 perf(sqlx): strip default features — workspace uses none of macros / migrate / any / json
sqlx 0.8's default feature set pulls in `any`, `macros`, `migrate`, and
`json`. Grepping the workspace confirms none of these are used — the
code calls sqlx::query() / query_scalar() at runtime, implements its own
migration sequencing in crates/storage/src/migrations.rs, is sqlite-only
(no `any` needed), and never derives FromRow / applies sqlx proc-macros.

Dropping them keeps only what's needed: runtime-tokio + sqlite.

Why it matters disproportionately on Windows: the `macros` feature pulls
sqlx-macros → sqlx-macros-core → proc-macro2 / syn / quote / async-trait
/ url / heck / dotenvy / sha2 / filetime. Each proc-macro crate on
Windows MSVC compiles to a .dll with a full linker invocation (slower
ABI than Linux/macOS proc-macro .so). Net: tens of seconds shaved off
every cold-cache CI run, compounding with the cache-path fix in the next
commit.

kon-mcp was already lean (default-features = false); matching that shape
across the workspace now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 15:42:04 +01:00
65280c776e docs(gpu-tuning): add MVP plan — three phases with one-click UX
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
Specs the subset of the five-phase GPU kernel tuning roadmap that ships
without requiring ggml-dedup or agentic-search prerequisites:

- Phase 1 — Advanced → GPU Tuning settings panel (GGML env var toggles,
  applied at startup before threads spawn).
- Phase 2 — kon-bench local autotuning CLI. Subprocess-based grid search
  over env vars, outputs a ranked gpu-profile.toml.
- Phase 3-lite — kon-configs community repo. Manual-PR workflow (no CI
  replay), fingerprint-matched fetch from Kon Settings.

Total ~7–10 days of focused work; captures roughly 85% of the eventual
value of the full roadmap. Phases 4–5 (custom SPIR-V drops + agentic
autotune) stay pinned in memory.

Includes the UX spec for the "one-click auto-optimise" flow: community
config check first (~15 s end-to-end), local benchmark fallback
(~8 min backgrounded), opt-in share-back via browser PR. Non-GPU users
see a clean "tuning doesn't apply" card with no nag.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 12:32:43 +01:00
ff22497468 docs(whisper-ecosystem): add kon-context.md for cloud-based Cursor agents
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
Cloud agents (Codex / Opus in Cursor) can't read the author's local
Claude memory directory. This doc folds the non-negotiable ideology,
architectural decisions, what's already shipped, what's intentionally
deferred, and the file-ownership fence between Workstream A and B into
a single in-repo reference.

Kickoff prompts now point agents at docs/whisper-ecosystem/kon-context.md
as the primary read BEFORE brief.md, so the agents understand what NOT
to re-implement (already-shipped features) and what NOT to touch (the
other workstream's territory, the ggml dedup interim, etc.).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 11:53:44 +01:00
03ab18c71f docs(whisper-ecosystem): add cross-repo research brief
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
Survey of 10 OSS Whisper projects (whisper.cpp, whisper-rs, Handy, Buzz,
Whispering/Epicenter, faster-whisper, WhisperLive, whisper_streaming,
Scriberr, Vibe, OpenWhispr). Pins the cross-repo pain pattern matrix,
feature inventory with priority tags, streaming-specific findings, LLM
formatting findings, and a 31-item atomic task backlog — all URL-sourced.

Lives in the repo as the canonical reference for the phase-4 implementation
pass. Both Cursor workstreams (Codex for systems + streaming, Opus for UX
+ LLM layer) read this at kickoff.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 10:46:32 +01:00
42335c04c5 feat(vocab): bulk import for profile terms
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
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>
2026-04-21 09:40:18 +01:00
1f5309c8f5 feat(windows): persist size + position across restarts via tauri-plugin-window-state
Without this, every secondary window (preview overlay, task float,
transcript viewer) opened at whatever spot Tauri / the compositor picked,
which was especially noticeable on Wayland where placement hints are
advisory. Main window's position was also lost on restart.

Registering tauri_plugin_window_state in the builder gives automatic
per-window-label save + restore. State lives in app-data/window-state.json;
fresh installs still fall back to the builder defaults (no changes to
inner_size on any of the four windows). Covers OpenWhispr issue #605 and
the broader UX pain.

No frontend changes — the plugin is purely backend. Regenerated ACL
manifests / desktop + linux schemas pick up the plugin registration.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 09:38:23 +01:00
11965a338b fix(preview): set GTK WindowTypeHint::Utility for non-KDE compositor coverage
KWin reads _NET_WM_STATE_SKIP_TASKBAR for its Alt+Tab list, which OW-2
already wired via skip_taskbar(true) on the builder. On Hyprland, Sway,
and GNOME Mutter that's not always enough — some switchers still enumerate
the overlay. Classifying the window as gdk::WindowTypeHint::Utility signals
to the compositor that this is an assistive auxiliary surface, so switchers
and auto-tilers leave it alone. No behavioural change on KWin.

GTK3 only honours the type hint before the window maps, so the preview
builder now starts .visible(false); we grab the gtk_window() via Tauri's
escape hatch, set the hint, then show(). The existing hide/show on
re-open still works — hint is a property of the gtk::ApplicationWindow
and survives the cycle.

Added gtk = "0.18" and gdk = "0.18" as Linux-only deps. Both are already
pulled in transitively via webkit2gtk 2.0, so this is just surfacing them
by name — no new compile cost.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 09:36:49 +01:00
9b5d08af3d fix(preview): pin preview overlay across virtual desktops on KDE/GNOME Wayland
Without _NET_WM_STATE_STICKY the preview overlay only renders on whichever
virtual desktop it opened on — switch desktops mid-dictation and the raw
transcription stream vanishes exactly when you need it (the whole point of
the overlay is that you're working in another app).

visible_on_all_workspaces(true) on the WebviewWindowBuilder sets STICKY via
GTK on X11/XWayland, which KWin + Mutter both honour. Combined with the
existing skip_taskbar(true) — KWin's default Alt+Tab list already respects
_NET_WM_STATE_SKIP_TASKBAR — the preview now behaves like the assistive
overlay it's meant to be: follows you, out of the way of window switchers.

Applied only to the preview window. Task-float and transcript-viewer are
primary surfaces that should stay on their own desktop, so they keep the
current behaviour.

Follow-up if dogfooding shows Alt+Tab clutter on non-KDE compositors: layer
a GTK WindowTypeHint::Utility via with_webview. Not needed for KWin.

Matches OpenWhispr's PR #183 shape for KDE Plasma.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 09:28:29 +01:00
bc1ae3968e fix(paste): hide preview overlay before Ctrl+V to avoid Wayland focus race
Phase H landed the transcription preview overlay. Phase C landed auto-paste.
With both enabled the combo is broken on Wayland compositors (KWin, Mutter):
the overlay is always_on_top + visible at the moment paste_text fires its
Ctrl+V keystroke, and the compositor resolves the key to the topmost visible
window — which is the overlay, even though we built it with focused=false.
Net result: the transcript pastes into Kon instead of whatever app the user
was actually dictating into.

Fix, mirroring OpenWhispr's PR #246 shape: before trigger_paste_keystroke,
hide the transcription-preview window if it exists and is visible, then
sleep PREVIEW_HIDE_SETTLE_MS (80ms) so the compositor recomputes focus onto
the previously-focused app. No reshow — the user's confirmation is the text
appearing in the target app, not a fading overlay. The 80ms is enough on
KWin and Mutter; tunable if it shows up differently on other compositors.

paste_text now takes the tauri::AppHandle so it can reach the preview
window. Frontend invocation signature is unchanged (Tauri injects the
handle; the JS call site still passes { text }).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 09:23:22 +01:00
6837700ac9 style(clippy): clean up the two lints in phase3 new code
QC smoke sweep flagged two clippy -D warnings lints in code this branch
introduced:

- crates/core/src/process_watch.rs — collapsible_if on the meeting-pattern
  match loop, merged the two conditions with &&.
- crates/mcp/src/lib.rs — let-else on the id unwrap that short-circuits a
  notification, switched to ? since handle_message already returns Option.

All other clippy lints under -D warnings (audio/capture, hotkey/linux,
storage/file_storage, diagnostics, the duplicate-detection helpers in
live.rs) predate this branch and are out of scope.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 09:06:47 +01:00
eb60a8bfd3 feat(preview): floating transcription overlay with listening→live→cleanup→final phases
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>
2026-04-21 08:49:29 +01:00
42b32a4f1a test(storage): pin FTS5 contract for search_transcripts
search_transcripts already backs onto the transcripts_fts virtual table
(migration v4, trigger-maintained) via MATCH + ORDER BY rank. Adding a
test to lock the behaviour: token matching is case-insensitive, rank-
ordered, and non-matching tokens return nothing. This is Phase G of the
post-OpenWhispr audit — semantic embeddings stay deferred until the FTS
experience actually hits a wall.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 07:55:52 +01:00
ba0d59f563 feat(meeting): opt-in process-list reminder when a meeting app starts
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>
2026-04-21 07:54:55 +01:00