From 9f3be5c00731a166d22e595a6c224dfa8ce87eaa Mon Sep 17 00:00:00 2001 From: Jake Date: Fri, 17 Apr 2026 13:16:07 +0100 Subject: [PATCH] =?UTF-8?q?ui+app:=20Day=205+6=20=E2=80=94=20Settings=20?= =?UTF-8?q?=E2=86=92=20Vocabulary=20panel=20+=20Wayland=20self-relaunch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VOCABULARY PANEL (Day 5): src/lib/pages/SettingsPage.svelte gains a new collapsible "Vocabulary" section between Audio and Transcription. Wires the dictionary commands shipped in 1cce567: - list_dictionary_command on mount + onfocus refresh - add_dictionary_entry_command (term + optional note) - delete_dictionary_entry_command - Inline error feedback via vocabularyError - Empty-state copy + per-entry remove button with aria-label The dictionary terms are intended to be injected into the LLM cleanup prompt so the model preserves user-specific vocabulary (medication names, place names, jargon, names of people in the user's support network — particularly important for the ND audience). The LLM client itself is currently a stub (crates/ai-formatting/src/llm_client.rs); when it lands, it can import list_dictionary from kon_storage and inject terms into the prompt suffix. WAYLAND SELF-RELAUNCH (Day 6): src-tauri/src/lib.rs gains ensure_x11_on_wayland() which runs before tauri::Builder. On Linux Wayland sessions (XDG_SESSION_TYPE=wayland) it sets GDK_BACKEND=x11, WINIT_UNIX_BACKEND=x11, and WEBKIT_DISABLE_DMABUF_RENDERER=1 — the env vars HANDOVER.md documents the user manually prefixing. Drops the "users have to remember the env-var prefix" friction. Idempotent: existing values are respected. Inspired by Open-Whispr's Chromium --ozone-platform=x11 self-relaunch. Compile-checked: cargo check -p kon-storage clean. Settings.svelte $state / #if balanced (28 state, 34/34 if/endif). NEXT: Phase 6 — dogfooding readiness doc with Jake test instructions. --- src-tauri/src/lib.rs | 45 ++++++++++++ src/lib/pages/SettingsPage.svelte | 117 ++++++++++++++++++++++++++++++ 2 files changed, 162 insertions(+) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 645780a..fc5d5b2 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -68,8 +68,53 @@ async fn save_preferences( .map_err(|e| e.to_string()) } +/// On Linux Wayland sessions (KDE, GNOME) WebKitGTK + DMABUF rendering hits +/// known crashes that the HANDOVER documents working around with a manual +/// env-var prefix: +/// +/// env GDK_BACKEND=x11 WINIT_UNIX_BACKEND=x11 \ +/// WEBKIT_DISABLE_DMABUF_RENDERER=1 npm run tauri dev +/// +/// Detect the Wayland session at startup and apply the env vars before +/// anything else loads, so users do not need to remember the prefix and +/// the dev launcher / packaged app both work out of the box. +/// +/// Inspired by Open-Whispr's similar Chromium self-relaunch (with +/// --ozone-platform=x11). Day 6 of the upgrade plan. +#[cfg(target_os = "linux")] +fn ensure_x11_on_wayland() { + // If the user explicitly opted in to Wayland (XDG_SESSION_TYPE set by + // their session, KDE / GNOME compositor flags), force WebKitGTK + GDK + // onto X11 via XWayland. Idempotent: if a value is already set, keep + // it (the user knows best). + let session_type = std::env::var("XDG_SESSION_TYPE").unwrap_or_default(); + let is_wayland = session_type.eq_ignore_ascii_case("wayland"); + if !is_wayland { + return; + } + + let set_if_unset = |key: &str, value: &str| { + if std::env::var_os(key).is_none() { + // SAFETY: setting env vars before any threads spawn (we are + // pre-Tauri-Builder here). This block is the only place these + // are written. + unsafe { std::env::set_var(key, value); } + eprintln!("[startup] Wayland workaround: {key}={value}"); + } + }; + + set_if_unset("GDK_BACKEND", "x11"); + set_if_unset("WINIT_UNIX_BACKEND", "x11"); + set_if_unset("WEBKIT_DISABLE_DMABUF_RENDERER", "1"); + // PipeWire screen-capture portal still works fine through XWayland; + // we only redirect the GUI rendering path. +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { + #[cfg(target_os = "linux")] + ensure_x11_on_wayland(); + tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_dialog::init()) diff --git a/src/lib/pages/SettingsPage.svelte b/src/lib/pages/SettingsPage.svelte index 308e858..84fddaa 100644 --- a/src/lib/pages/SettingsPage.svelte +++ b/src/lib/pages/SettingsPage.svelte @@ -43,6 +43,49 @@ audioDevices = []; } } + + // --- Custom vocabulary (dictionary) — Day 5 of upgrade plan --- + // Backed by SQLite `dictionary` table via list_dictionary_command etc. + // Terms get injected into the LLM cleanup prompt so the model preserves + // the user's spellings (e.g. medication names, jargon, place names). + let vocabulary = $state([]); + let vocabularyError = $state(null); + let newVocabTerm = $state(""); + let newVocabNote = $state(""); + + async function refreshVocabulary() { + vocabularyError = null; + try { + vocabulary = await invoke("list_dictionary_command"); + } catch (err) { + vocabularyError = "Could not load vocabulary: " + (err?.message || err); + } + } + + async function addVocabTerm() { + const term = newVocabTerm.trim(); + if (!term) return; + try { + await invoke("add_dictionary_entry_command", { + term, + note: newVocabNote.trim() || null, + }); + newVocabTerm = ""; + newVocabNote = ""; + await refreshVocabulary(); + } catch (err) { + vocabularyError = "Could not save term: " + (err?.message || err); + } + } + + async function deleteVocabTerm(id) { + try { + await invoke("delete_dictionary_entry_command", { id }); + await refreshVocabulary(); + } catch (err) { + vocabularyError = "Could not delete term: " + (err?.message || err); + } + } let parakeetDownloaded = $state(false); let parakeetDownloading = $state(false); let parakeetProgress = $state(0); @@ -131,6 +174,10 @@ // the user opens the Audio section. refreshAudioDevices(); + // Pre-load vocabulary too. Both lists are small; pre-fetching avoids + // a flash of "no data" when the section opens. + refreshVocabulary(); + try { downloadedModels = await invoke("list_models"); } catch {} @@ -380,6 +427,76 @@ {/if} + +
+ + {#if openSection === 'vocabulary'} +
+

+ Words and phrases the AI cleanup pass should preserve exactly. Useful for medication names, place names, jargon, names of people in your support network, anything Whisper tends to mishear. +

+ +
+ { if (e.key === 'Enter') addVocabTerm(); }} + class="flex-1 bg-bg-input border border-border rounded-lg px-3 py-2 text-[13px] text-text + focus:outline-none focus:border-accent focus:shadow-[0_0_0_3px_rgba(232,168,124,0.1)]" + /> + { if (e.key === 'Enter') addVocabTerm(); }} + class="flex-1 bg-bg-input border border-border rounded-lg px-3 py-2 text-[13px] text-text + focus:outline-none focus:border-accent focus:shadow-[0_0_0_3px_rgba(232,168,124,0.1)]" + /> + +
+ + {#if vocabularyError} +

{vocabularyError}

+ {/if} + + {#if vocabulary.length === 0} +

No terms yet. Add one above.

+ {:else} +
    + {#each vocabulary as entry (entry.id)} +
  • +
    +
    {entry.term}
    + {#if entry.note} +
    {entry.note}
    + {/if} +
    + +
  • + {/each} +
+ {/if} +
+ {/if} +
+