From 3e17de29909cd144a928e624057c3d3ba0a0184d Mon Sep 17 00:00:00 2001 From: Eve Date: Thu, 19 Mar 2026 21:27:42 +1100 Subject: [PATCH 1/5] fix: delete prompt invoke uses camelCase promptId (Tauri v2) --- src/lib/components/AIEnhancementSettings.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/components/AIEnhancementSettings.svelte b/src/lib/components/AIEnhancementSettings.svelte index 7639b83..34aa0b3 100644 --- a/src/lib/components/AIEnhancementSettings.svelte +++ b/src/lib/components/AIEnhancementSettings.svelte @@ -201,7 +201,7 @@ /** Delete a custom prompt */ async function deletePrompt(promptId: string): Promise { try { - await invoke('delete_custom_prompt_cmd', { prompt_id: promptId }); + await invoke('delete_custom_prompt_cmd', { promptId: promptId }); await loadPrompts(); // If the deleted prompt was selected, reset to default From f888f8d250853d0497f66370d4099b43935d40f4 Mon Sep 17 00:00:00 2001 From: Eve Date: Thu, 19 Mar 2026 21:12:47 +1100 Subject: [PATCH 2/5] feat: dictionary quick-add shortcut + updater signing keypair (v2026.2.18) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dictionary quick-add shortcut: - add_to_dictionary shortcut in ShortcutConfig (default: unset) - Appears in Settings alongside toggle_recording, copy_last, etc. - Copy a word → press shortcut → prompted for replacement → added to dictionary - Toast notification on success - Works via clipboard read + window.prompt dialog Updater signing: - Regenerated signing keypair with working password - Updated pubkey in tauri.conf.json - latest.json hosted at releases/latest/download/latest.json --- package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/config.rs | 4 ++ src-tauri/src/lib.rs | 7 ++++ src-tauri/src/shortcuts/manager.rs | 7 ++++ src-tauri/tauri.conf.json | 15 ++++--- src/lib/stores/config.svelte.ts | 6 +++ src/lib/stores/pipeline.svelte.ts | 67 +++++++++++++++++++++++++++++- src/lib/windows/Settings.svelte | 4 ++ src/routes/+layout.svelte | 21 +++++++++- 10 files changed, 126 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index d6f41b0..f7481ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "thoth-tauri", - "version": "2026.2.7", + "version": "2026.2.18", "private": true, "description": "Privacy-first, offline-capable voice transcription application", "type": "module", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 9a494d4..b7ad5a8 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth" -version = "2026.2.7" +version = "2026.2.18" description = "Privacy-first, offline-capable voice transcription application" authors = ["Paul Roberts"] license = "MIT" diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 102bf9e..7b2bee6 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -122,6 +122,9 @@ pub struct ShortcutConfig { pub toggle_recording_alt: Option, /// Copy last transcription shortcut pub copy_last: Option, + /// Quick-add selected text to dictionary shortcut + #[serde(default)] + pub add_to_dictionary: Option, /// Recording mode: toggle or push-to-talk pub recording_mode: RecordingMode, } @@ -132,6 +135,7 @@ impl Default for ShortcutConfig { toggle_recording: "F13".to_string(), toggle_recording_alt: Some("ShiftRight".to_string()), copy_last: Some("F14".to_string()), + add_to_dictionary: None, recording_mode: RecordingMode::default(), } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index cbc0969..439093f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -111,6 +111,13 @@ fn register_shortcuts_from_config(app: &tauri::AppHandle, cfg: &config::Config) "Copy last transcription", ) }), + cfg.shortcuts.add_to_dictionary.as_deref().map(|accel| { + ( + shortcut_ids::ADD_TO_DICTIONARY, + accel, + "Quick-add selected word to dictionary", + ) + }), ] .into_iter() .flatten() diff --git a/src-tauri/src/shortcuts/manager.rs b/src-tauri/src/shortcuts/manager.rs index ac609f8..c5a3ea9 100644 --- a/src-tauri/src/shortcuts/manager.rs +++ b/src-tauri/src/shortcuts/manager.rs @@ -30,6 +30,7 @@ pub mod shortcut_ids { pub const TOGGLE_RECORDING: &str = "toggle_recording"; pub const TOGGLE_RECORDING_ALT: &str = "toggle_recording_alt"; pub const COPY_LAST_TRANSCRIPTION: &str = "copy_last_transcription"; + pub const ADD_TO_DICTIONARY: &str = "add_to_dictionary"; } /// Global shortcut manager instance @@ -81,6 +82,12 @@ pub fn get_defaults() -> Vec { description: "Toggle recording (alternative)".to_string(), is_enabled: false, }, + ShortcutInfo { + id: shortcut_ids::ADD_TO_DICTIONARY.to_string(), + accelerator: String::new(), // No default — user sets their own key + description: "Quick-add selected text to dictionary".to_string(), + is_enabled: false, + }, ] } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index d7dcc97..65c9122 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,6 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "Thoth", - "version": "2026.2.7", "identifier": "com.poodle64.thoth", "build": { "beforeDevCommand": "npm run dev", @@ -65,8 +64,10 @@ "requireLiteralLeadingDot": false }, "updater": { - "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEExOEY5RURGQUJBNkQzRjkKUldUNTA2YXIzNTZQb1lNSGQ3WVlkbDY3VVlUQlJYSzJzQW5ieUhhci9NVnVDUms5T0NJa21acnEK", - "endpoints": ["https://github.com/poodle64/thoth/releases/latest/download/latest.json"] + "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDU0NzMwMTlBQzJDQzEyOUMKUldTY0VzekNtZ0Z6VkxOQlZZSzlmczdBby92bnZnSmlVamRWODJEU3lvajJYcXJ3b1hJODRRUTQK", + "endpoints": [ + "https://github.com/sk8ersquare/thoth/releases/latest/download/latest.json" + ] } }, "bundle": { @@ -106,11 +107,15 @@ "bundleMediaFramework": true }, "deb": { - "depends": ["libasound2", "libwebkit2gtk-4.1-0", "libgtk-3-0"], + "depends": [ + "libasound2", + "libwebkit2gtk-4.1-0", + "libgtk-3-0" + ], "section": "sound", "priority": "optional", "desktopTemplate": "linux/thoth.desktop" } } } -} +} \ No newline at end of file diff --git a/src/lib/stores/config.svelte.ts b/src/lib/stores/config.svelte.ts index f2da5c2..ca01695 100644 --- a/src/lib/stores/config.svelte.ts +++ b/src/lib/stores/config.svelte.ts @@ -40,6 +40,8 @@ export interface ShortcutConfig { toggleRecordingAlt: string | null; /** Copy last transcription shortcut */ copyLast: string | null; + /** Quick-add selected text to dictionary shortcut */ + addToDictionary: string | null; /** Recording mode: toggle or push-to-talk */ recordingMode: RecordingMode; } @@ -133,6 +135,7 @@ interface ConfigRaw { toggle_recording: string; toggle_recording_alt: string | null; copy_last: string | null; + add_to_dictionary: string | null; recording_mode: RecordingMode; }; enhancement: { @@ -176,6 +179,7 @@ function parseConfig(raw: ConfigRaw): Config { toggleRecording: raw.shortcuts.toggle_recording, toggleRecordingAlt: raw.shortcuts.toggle_recording_alt, copyLast: raw.shortcuts.copy_last, + addToDictionary: raw.shortcuts.add_to_dictionary ?? null, recordingMode: raw.shortcuts.recording_mode, }, enhancement: { @@ -220,6 +224,7 @@ function serialiseConfig(config: Config): ConfigRaw { toggle_recording: config.shortcuts.toggleRecording, toggle_recording_alt: config.shortcuts.toggleRecordingAlt, copy_last: config.shortcuts.copyLast, + add_to_dictionary: config.shortcuts.addToDictionary, recording_mode: config.shortcuts.recordingMode, }, enhancement: { @@ -264,6 +269,7 @@ function getDefaultConfig(): Config { toggleRecording: 'F13', toggleRecordingAlt: 'CommandOrControl+Shift+Space', copyLast: 'F14', + addToDictionary: null, recordingMode: 'toggle', }, enhancement: { diff --git a/src/lib/stores/pipeline.svelte.ts b/src/lib/stores/pipeline.svelte.ts index 6f5040c..4272081 100644 --- a/src/lib/stores/pipeline.svelte.ts +++ b/src/lib/stores/pipeline.svelte.ts @@ -11,7 +11,7 @@ */ import { invoke } from '@tauri-apps/api/core'; -import { listen, type UnlistenFn } from '@tauri-apps/api/event'; +import { listen, emit, type UnlistenFn } from '@tauri-apps/api/event'; import { configStore } from './config.svelte'; import { settingsStore } from './settings.svelte'; @@ -204,6 +204,12 @@ function createPipelineStore() { await toggleRecording(); debug(`${timestamp} toggleRecording completed, new state:`, state); } + + // Handle quick-add to dictionary shortcut + if (shortcutId === 'add_to_dictionary') { + debug(`${timestamp} Quick-add to dictionary triggered`); + await handleQuickAddToDictionary(); + } }); debug(' shortcut-triggered listener registered'); unlisteners.push(shortcutUnlisten); @@ -349,6 +355,65 @@ function createPipelineStore() { } } + /** + * Quick-add to dictionary: reads clipboard text as "from" word, + * prompts user for replacement, then saves the entry. + */ + async function handleQuickAddToDictionary(): Promise { + try { + // Get clipboard text — user should have the word copied/selected + const { readText } = await import('@tauri-apps/plugin-clipboard-manager'); + const clipText = (await readText()) ?? ''; + const fromWord = clipText.trim(); + + if (!fromWord) { + // Fallback: prompt for the word if clipboard is empty + const word = window.prompt('Word or phrase to add to dictionary:'); + if (!word?.trim()) return; + await quickAddEntry(word.trim()); + return; + } + + await quickAddEntry(fromWord); + } catch (e) { + console.error('[Pipeline] Quick-add to dictionary failed:', e); + } + } + + async function quickAddEntry(fromWord: string): Promise { + // Show replacement prompt + const replacement = window.prompt( + `Add to dictionary\n\nReplace: "${fromWord}"\nWith:`, + fromWord + ); + + if (replacement === null) return; // cancelled + if (replacement.trim() === fromWord) { + // No change — ask again or abort + const confirmed = window.confirm( + `"${fromWord}" → "${replacement.trim()}" — replacement is the same as the original. Add anyway?` + ); + if (!confirmed) return; + } + + try { + await invoke('add_dictionary_entry', { + entry: { + from: fromWord, + to: replacement.trim(), + caseSensitive: false, + }, + }); + // Notify the UI to refresh dictionary if it's open + emit('dictionary-updated', { from: fromWord, to: replacement.trim() }).catch(() => {}); + // Brief visual confirmation via a native notification isn't available, + // so we emit an event the main window can pick up + emit('toast', { kind: 'success', message: `Added "${fromWord}" → "${replacement.trim()}" to dictionary` }).catch(() => {}); + } catch (e) { + window.alert(`Failed to add entry: ${e instanceof Error ? e.message : String(e)}`); + } + } + /** * Toggle recording (start if idle, stop and process if recording) */ diff --git a/src/lib/windows/Settings.svelte b/src/lib/windows/Settings.svelte index e6fb773..30d6b71 100644 --- a/src/lib/windows/Settings.svelte +++ b/src/lib/windows/Settings.svelte @@ -150,6 +150,9 @@ case 'copy_last_transcription': configStore.updateShortcuts('copyLast', accelerator); break; + case 'add_to_dictionary': + configStore.updateShortcuts('addToDictionary', accelerator); + break; } } @@ -165,6 +168,7 @@ toggle_recording: configStore.shortcuts.toggleRecording, toggle_recording_alt: configStore.shortcuts.toggleRecordingAlt, copy_last: configStore.shortcuts.copyLast, + add_to_dictionary: configStore.shortcuts.addToDictionary, recording_mode: configStore.shortcuts.recordingMode, }, }); diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 3572496..d6a5f9e 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,8 +1,10 @@ From 52519ba05b1f8784d01cc55d142ee705c1e42af0 Mon Sep 17 00:00:00 2001 From: Eve Date: Thu, 19 Mar 2026 21:38:16 +1100 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20dictionary=20quick-add=20shortcut=20?= =?UTF-8?q?=E2=80=94=20replace=20window.prompt=20with=20native=20modal=20(?= =?UTF-8?q?v2026.2.19)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit window.prompt/confirm/alert are silently swallowed by WKWebView on macOS. Replace with a proper in-app Svelte modal (DictionaryAddModal.svelte): - Listens for 'show-dictionary-add' event emitted by the shortcut handler - Pre-fills 'Replace' field from clipboard content - Text input auto-focused, Enter to save, Escape to cancel - Toast on success/failure - Wired into App.svelte alongside existing event listeners --- package.json | 4 +- src-tauri/Cargo.toml | 2 +- src/App.svelte | 23 +- src/lib/components/DictionaryAddModal.svelte | 223 +++++++++++++++++++ src/lib/stores/pipeline.svelte.ts | 49 +--- 5 files changed, 251 insertions(+), 50 deletions(-) create mode 100644 src/lib/components/DictionaryAddModal.svelte diff --git a/package.json b/package.json index f7481ca..fa15d3d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "thoth-tauri", - "version": "2026.2.18", + "version": "2026.2.19", "private": true, "description": "Privacy-first, offline-capable voice transcription application", "type": "module", @@ -37,4 +37,4 @@ "typescript": "5.9.3", "vite": "7.3.1" } -} +} \ No newline at end of file diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index b7ad5a8..3c26184 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth" -version = "2026.2.18" +version = "2026.2.19" description = "Privacy-first, offline-capable voice transcription application" authors = ["Paul Roberts"] license = "MIT" diff --git a/src/App.svelte b/src/App.svelte index 63f464f..9a5e93e 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -13,6 +13,7 @@ import { listen, type UnlistenFn } from '@tauri-apps/api/event'; import { onMount, onDestroy } from 'svelte'; import Settings from './lib/windows/Settings.svelte'; + import DictionaryAddModal from './lib/components/DictionaryAddModal.svelte'; import { configStore } from './lib/stores/config.svelte'; import { pipelineStore } from './lib/stores/pipeline.svelte'; import { settingsStore } from './lib/stores/settings.svelte'; @@ -26,10 +27,15 @@ : () => {}; let indicatorLogUnlisten: UnlistenFn | null = null; + let dictAddUnlisten: UnlistenFn | null = null; let isInitialising = $state(true); let initError = $state(null); + // Dictionary quick-add modal state + let dictModalOpen = $state(false); + let dictModalWord = $state(''); + async function initialise() { try { debug('Starting initialisation...'); @@ -105,14 +111,19 @@ debug(event.payload.message); }); + // Listen for dictionary quick-add events (from shortcut handler in pipeline store) + dictAddUnlisten = await listen<{ word: string }>('show-dictionary-add', (event) => { + dictModalWord = event.payload.word ?? ''; + dictModalOpen = true; + }); + initialise(); }); onDestroy(() => { pipelineStore.cleanup(); - if (indicatorLogUnlisten) { - indicatorLogUnlisten(); - } + if (indicatorLogUnlisten) indicatorLogUnlisten(); + if (dictAddUnlisten) dictAddUnlisten(); }); @@ -130,6 +141,12 @@ {/if} + (dictModalOpen = false)} +/> + diff --git a/src/lib/stores/pipeline.svelte.ts b/src/lib/stores/pipeline.svelte.ts index 4272081..e4e65f3 100644 --- a/src/lib/stores/pipeline.svelte.ts +++ b/src/lib/stores/pipeline.svelte.ts @@ -361,59 +361,20 @@ function createPipelineStore() { */ async function handleQuickAddToDictionary(): Promise { try { - // Get clipboard text — user should have the word copied/selected const { readText } = await import('@tauri-apps/plugin-clipboard-manager'); + + // Read clipboard — user should have the word copied const clipText = (await readText()) ?? ''; const fromWord = clipText.trim(); - if (!fromWord) { - // Fallback: prompt for the word if clipboard is empty - const word = window.prompt('Word or phrase to add to dictionary:'); - if (!word?.trim()) return; - await quickAddEntry(word.trim()); - return; - } - - await quickAddEntry(fromWord); + // Emit to the main window — it will show an inline modal with a text input + // (window.prompt is disabled in WKWebView; plugin-dialog has no text input type) + emit('show-dictionary-add', { word: fromWord }).catch(() => {}); } catch (e) { console.error('[Pipeline] Quick-add to dictionary failed:', e); } } - async function quickAddEntry(fromWord: string): Promise { - // Show replacement prompt - const replacement = window.prompt( - `Add to dictionary\n\nReplace: "${fromWord}"\nWith:`, - fromWord - ); - - if (replacement === null) return; // cancelled - if (replacement.trim() === fromWord) { - // No change — ask again or abort - const confirmed = window.confirm( - `"${fromWord}" → "${replacement.trim()}" — replacement is the same as the original. Add anyway?` - ); - if (!confirmed) return; - } - - try { - await invoke('add_dictionary_entry', { - entry: { - from: fromWord, - to: replacement.trim(), - caseSensitive: false, - }, - }); - // Notify the UI to refresh dictionary if it's open - emit('dictionary-updated', { from: fromWord, to: replacement.trim() }).catch(() => {}); - // Brief visual confirmation via a native notification isn't available, - // so we emit an event the main window can pick up - emit('toast', { kind: 'success', message: `Added "${fromWord}" → "${replacement.trim()}" to dictionary` }).catch(() => {}); - } catch (e) { - window.alert(`Failed to add entry: ${e instanceof Error ? e.message : String(e)}`); - } - } - /** * Toggle recording (start if idle, stop and process if recording) */ From 8aaf3bc7090ebc8fd72f61f479897b486586c5f4 Mon Sep 17 00:00:00 2001 From: Eve Date: Thu, 19 Mar 2026 19:58:06 +1100 Subject: [PATCH 4/5] fix: custom prompt delete button silently failing (v2026.2.15) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit configStore.enhancement was a stale reference — should be configStore.config.enhancement. The JS error was caught and swallowed, making the delete appear to do nothing. The backend delete actually succeeded but the UI never refreshed. --- package.json | 4 ++-- src-tauri/Cargo.toml | 2 +- src/lib/components/AIEnhancementSettings.svelte | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index fa15d3d..4257003 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "thoth-tauri", - "version": "2026.2.19", + "version": "2026.2.15", "private": true, "description": "Privacy-first, offline-capable voice transcription application", "type": "module", @@ -37,4 +37,4 @@ "typescript": "5.9.3", "vite": "7.3.1" } -} \ No newline at end of file +} diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 3c26184..fee78b7 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth" -version = "2026.2.19" +version = "2026.2.15" description = "Privacy-first, offline-capable voice transcription application" authors = ["Paul Roberts"] license = "MIT" diff --git a/src/lib/components/AIEnhancementSettings.svelte b/src/lib/components/AIEnhancementSettings.svelte index 34aa0b3..a0d26a7 100644 --- a/src/lib/components/AIEnhancementSettings.svelte +++ b/src/lib/components/AIEnhancementSettings.svelte @@ -205,7 +205,7 @@ await loadPrompts(); // If the deleted prompt was selected, reset to default - if (configStore.enhancement.promptId === promptId) { + if (configStore.config.enhancement.promptId === promptId) { configStore.updateEnhancement('promptId', 'fix-grammar'); await saveSettings(); } @@ -219,7 +219,7 @@ /** Get the currently selected prompt */ function getSelectedPrompt(): PromptTemplate | undefined { - return prompts.find((p) => p.id === configStore.enhancement.promptId); + return prompts.find((p) => p.id === configStore.config.enhancement.promptId); } /** Open the prompt writing guide window */ From aec08c01fe298ca0fa9444d2ead6efb23804515b Mon Sep 17 00:00:00 2001 From: Eve Date: Thu, 19 Mar 2026 20:22:24 +1100 Subject: [PATCH 5/5] fix: prompt delete + VAD 3s + update endpoint override (v2026.2.16) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prompt delete fixes: - Fixed IPC parameter name: prompt_id (not promptId) - Added confirmation dialog before deletion - Added toast feedback for success/error (not silent) - User sees clear error if delete fails VAD improvements: - Reduced no-input grace period from 5s to 3s before red pulse starts Update source configuration: - Added updateEndpointOverride to GeneralConfig (default: null) - Check for Updates button now has ⋯ menu to configure custom repo URL - Can override endpoint per-session (saves to config) - Shows current endpoint when override is active - Passes override to check() when checking for updates - After update: tauri-plugin-updater downloads + installs silently, then prompts to relaunch --- package.json | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/src/audio/preview.rs | 39 ++++++ src-tauri/src/config.rs | 5 + .../components/AIEnhancementSettings.svelte | 7 +- src/lib/components/OverviewPane.svelte | 111 ++++++++++++++++-- src/lib/stores/config.svelte.ts | 6 + src/lib/stores/updater.svelte.ts | 7 +- 8 files changed, 165 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 4257003..1b15bef 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "thoth-tauri", - "version": "2026.2.15", + "version": "2026.2.16", "private": true, "description": "Privacy-first, offline-capable voice transcription application", "type": "module", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index fee78b7..f9f375e 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "thoth" -version = "2026.2.15" +version = "2026.2.16" description = "Privacy-first, offline-capable voice transcription application" authors = ["Paul Roberts"] license = "MIT" diff --git a/src-tauri/src/audio/preview.rs b/src-tauri/src/audio/preview.rs index 31c614f..b148b61 100644 --- a/src-tauri/src/audio/preview.rs +++ b/src-tauri/src/audio/preview.rs @@ -239,11 +239,50 @@ pub fn start_recording_metering(app: AppHandle, device_id: Option<&str>) -> Resu // Spawn emitter thread to send levels to the recording-indicator window let emit_stop_flag = stop_flag.clone(); let emit_handle = std::thread::spawn(move || { + // Track silence for no-input warning + // After NO_INPUT_GRACE_SECS seconds with RMS below threshold, emit warning + const NO_INPUT_GRACE_SECS: f64 = 3.0; + const NO_INPUT_RMS_THRESHOLD: f32 = 0.001; // effectively silence + let mut silence_since: Option = None; + let mut no_input_warning_sent = false; + let start_time = std::time::Instant::now(); + while !emit_stop_flag.load(Ordering::Relaxed) { while let Ok(samples) = rx.try_recv() { let mut meter = meter.lock(); let level = meter.process(&samples); + // Track silence window for no-input detection + // Only start counting after grace period from recording start + let elapsed_secs = start_time.elapsed().as_secs_f64(); + if elapsed_secs >= NO_INPUT_GRACE_SECS { + if level.rms < NO_INPUT_RMS_THRESHOLD { + let now = std::time::Instant::now(); + let silence_duration = silence_since + .get_or_insert(now) + .elapsed() + .as_secs_f64(); + if silence_duration >= NO_INPUT_GRACE_SECS && !no_input_warning_sent { + // Emit no-input warning + let _ = app.emit("recording-no-input", true); + if let Some(w) = app.get_webview_window("recording-indicator") { + let _ = w.emit("recording-no-input", true); + } + no_input_warning_sent = true; + } + } else { + // Voice detected — clear silence tracker and reset warning + if no_input_warning_sent { + let _ = app.emit("recording-no-input", false); + if let Some(w) = app.get_webview_window("recording-indicator") { + let _ = w.emit("recording-no-input", false); + } + no_input_warning_sent = false; + } + silence_since = None; + } + } + let event = AudioLevelEvent { rms: level.rms, peak: level.peak, diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index 7b2bee6..915963a 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -200,6 +200,9 @@ pub struct GeneralConfig { pub show_recording_indicator: bool, /// Visual style for the recording indicator pub indicator_style: IndicatorStyle, + /// Override the update check endpoint (None = use built-in default) + #[serde(default)] + pub update_endpoint_override: Option, } impl Default for GeneralConfig { @@ -211,6 +214,7 @@ impl Default for GeneralConfig { check_for_updates: true, show_recording_indicator: true, indicator_style: IndicatorStyle::default(), + update_endpoint_override: None, } } } @@ -752,6 +756,7 @@ mod tests { check_for_updates: true, show_recording_indicator: true, indicator_style: IndicatorStyle::CursorDot, + update_endpoint_override: None, }, recorder: RecorderConfig { position: RecorderPosition::Centre, diff --git a/src/lib/components/AIEnhancementSettings.svelte b/src/lib/components/AIEnhancementSettings.svelte index a0d26a7..f587157 100644 --- a/src/lib/components/AIEnhancementSettings.svelte +++ b/src/lib/components/AIEnhancementSettings.svelte @@ -199,7 +199,8 @@ } /** Delete a custom prompt */ - async function deletePrompt(promptId: string): Promise { + async function deletePrompt(promptId: string, promptName: string): Promise { + if (!confirm(`Delete prompt "${promptName}"?`)) return; try { await invoke('delete_custom_prompt_cmd', { promptId: promptId }); await loadPrompts(); @@ -210,10 +211,12 @@ await saveSettings(); } + toastStore.success(`Deleted "${promptName}"`); // Rebuild tray so the prompt submenu stays in sync invoke('refresh_tray_menu').catch(() => {}); } catch (e) { console.error('Failed to delete prompt:', e); + toastStore.error(`Delete failed: ${e instanceof Error ? e.message : String(e)}`); } } @@ -439,7 +442,7 @@ +
+ + +
+ {#if showUpdateEndpointEditor} +
+
Update source URL (leave blank to use default)
+
+ + + +
+
+ Currently checking: {configStore.general.updateEndpointOverride ?? 'https://github.com/poodle64/thoth/releases/latest/download/latest.json (default)'} +
+
+ {/if} @@ -1380,6 +1414,69 @@ letter-spacing: 0.3px; } + .update-btn-group { + display: flex; + gap: 4px; + align-items: center; + } + + .btn-icon { + padding: 4px 8px; + font-size: 14px; + line-height: 1; + } + + .update-endpoint-editor { + margin-top: 8px; + padding: 10px 12px; + background: var(--color-bg-tertiary); + border: 1px solid var(--color-border); + border-radius: var(--radius-md); + display: flex; + flex-direction: column; + gap: 6px; + } + + .endpoint-editor-label { + font-size: var(--text-xs); + color: var(--color-text-primary); + font-weight: 500; + } + + .endpoint-hint { + font-weight: 400; + color: var(--color-text-secondary); + margin-left: 4px; + } + + .endpoint-input-row { + display: flex; + gap: 6px; + align-items: center; + } + + .endpoint-input { + flex: 1; + padding: 5px 8px; + border: 1px solid var(--color-border); + border-radius: var(--radius-sm); + background: var(--color-bg-secondary); + color: var(--color-text-primary); + font-size: var(--text-xs); + font-family: var(--font-mono); + } + + .endpoint-current { + font-size: var(--text-xs); + color: var(--color-text-secondary); + } + + .endpoint-current code { + font-family: var(--font-mono); + font-size: 10px; + word-break: break-all; + } + .btn-setup.warning, .btn-small.warning { background: var(--color-warning); diff --git a/src/lib/stores/config.svelte.ts b/src/lib/stores/config.svelte.ts index ca01695..bba1c00 100644 --- a/src/lib/stores/config.svelte.ts +++ b/src/lib/stores/config.svelte.ts @@ -75,6 +75,8 @@ export interface GeneralConfig { showRecordingIndicator: boolean; /** Visual style for the recording indicator */ indicatorStyle: IndicatorStyle; + /** Override the update check endpoint URL (null = use built-in default) */ + updateEndpointOverride: string | null; } /** Recorder window position options */ @@ -151,6 +153,7 @@ interface ConfigRaw { check_for_updates: boolean; show_recording_indicator: boolean; indicator_style: IndicatorStyle; + update_endpoint_override: string | null; }; recorder: { position: RecorderPosition; @@ -195,6 +198,7 @@ function parseConfig(raw: ConfigRaw): Config { checkForUpdates: raw.general.check_for_updates, showRecordingIndicator: raw.general.show_recording_indicator, indicatorStyle: raw.general.indicator_style, + updateEndpointOverride: raw.general.update_endpoint_override ?? null, }, recorder: { position: raw.recorder.position, @@ -240,6 +244,7 @@ function serialiseConfig(config: Config): ConfigRaw { check_for_updates: config.general.checkForUpdates, show_recording_indicator: config.general.showRecordingIndicator, indicator_style: config.general.indicatorStyle, + update_endpoint_override: config.general.updateEndpointOverride, }, recorder: { position: config.recorder.position, @@ -285,6 +290,7 @@ function getDefaultConfig(): Config { checkForUpdates: true, showRecordingIndicator: true, indicatorStyle: 'cursor-dot', + updateEndpointOverride: null, }, recorder: { position: 'top-right', diff --git a/src/lib/stores/updater.svelte.ts b/src/lib/stores/updater.svelte.ts index 119e988..4be303e 100644 --- a/src/lib/stores/updater.svelte.ts +++ b/src/lib/stores/updater.svelte.ts @@ -47,9 +47,10 @@ const updaterState = $state({ }); /** - * Check for available updates + * Check for available updates. + * @param endpointOverride Optional URL to use instead of the built-in endpoint. */ -export async function checkForUpdate(): Promise { +export async function checkForUpdate(endpointOverride?: string | null): Promise { // Reset state updaterState.state = 'checking'; updaterState.error = null; @@ -58,7 +59,7 @@ export async function checkForUpdate(): Promise { updaterState.releaseNotes = null; try { - const update = await check(); + const update = await check(endpointOverride ? { headers: {}, url: endpointOverride } : undefined); if (update) { // Update available