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.
Review feedback: src-tauri/src/commands/models.rs was still naming
load_whisper unconditionally, so a --no-default-features workspace
build (kon-transcription without the whisper feature) would have
compiled the transcription crate cleanly but failed in the kon crate
as soon as it tried to resolve the load_whisper symbol.
Adds a matching [features] section to src-tauri/Cargo.toml:
- default = ["whisper"]
- whisper = ["kon-transcription/whisper"]
and declares the kon-transcription dep with default-features = false so
the feature actually propagates rather than being forced-on by the
child crate's default. Cfg-gates the load_whisper import in models.rs
and adds a companion match arm that returns a runtime error when the
feature is off, keeping the API shape intact.
Verified with both cargo build -p kon and cargo build -p kon
--no-default-features.
Review feedback: the original guard substring-searched the whole file
after the first "csp" token, which (a) false-passes if any unrelated
JSON value elsewhere in the config happens to contain a localhost URL
and (b) false-fails if the CSP is ever re-serialised with escaped
forward slashes.
Switches to serde_json + a /app/security/csp pointer lookup, then
splits the CSP on ';', finds the connect-src directive, tokenises its
allow-list on whitespace, and requires an exact match for both
http://127.0.0.1:* and ws://127.0.0.1:*. The error now also includes
the current connect-src value so a developer who breaks it can see
exactly what needs restoring.
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>
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>
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>
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>
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
WebKitGTK denies getUserMedia by default with no permission prompt.
Connect to the permission-request signal and auto-allow, plus enable
media-stream and media-capabilities in WebKit settings.
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>
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>
- 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>