From 1d71e8e361ca79ebccbaa7b862ceb21d6ce7eee5 Mon Sep 17 00:00:00 2001 From: Jake Date: Wed, 13 May 2026 08:25:55 +0100 Subject: [PATCH 01/82] refactor(llm): remove GBNF grammar, switch to JSON-envelope extractor extract_content_tags now generates with grammar=None and parses the response via a manual brace-counting JSON envelope extractor that handles Qwen ... prefixes and trailing stop tokens. Five new unit tests. Bumps llama-cpp-2 to 0.1.146. Explicit features=[] on tauri dependency (no-op). Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/llm/Cargo.toml | 2 +- crates/llm/src/lib.rs | 119 ++++++++++++++++++++++++++++++++++++++---- src-tauri/Cargo.toml | 2 +- 3 files changed, 111 insertions(+), 12 deletions(-) diff --git a/crates/llm/Cargo.toml b/crates/llm/Cargo.toml index b3215bf..4895077 100644 --- a/crates/llm/Cargo.toml +++ b/crates/llm/Cargo.toml @@ -19,7 +19,7 @@ openmp = ["llama-cpp-2/openmp"] magnotia-core = { path = "../core" } encoding_rs = "0.8" futures-util = "0.3" -llama-cpp-2 = { version = "0.1.144", default-features = false } +llama-cpp-2 = { version = "0.1.146", default-features = false } reqwest = { version = "0.12", default-features = false, features = ["rustls-tls", "stream"] } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/crates/llm/src/lib.rs b/crates/llm/src/lib.rs index 1453ab9..65c92ad 100644 --- a/crates/llm/src/lib.rs +++ b/crates/llm/src/lib.rs @@ -212,6 +212,10 @@ impl LlmEngine { generated.push_str(&piece); sampler.accept(next); + if config.grammar.is_some() && json_envelope_complete(&generated) { + break; + } + if let Some(stop_index) = first_stop_index(&generated, &config.stop_sequences) { generated.truncate(stop_index); break; @@ -296,13 +300,12 @@ impl LlmEngine { } /// Phase 9 content-tag extraction. Emits a single (topic, intent) - /// pair under the `CONTENT_TAGS_GRAMMAR` GBNF. Truncates to the - /// trailing 2000 chars of the transcript so the prompt budget - /// stays well under any model's context window. Determinism is - /// enforced by temperature 0.0 and the closed-set intent grammar - /// rule; on the rare case the model emits a parse-able-but-out-of- - /// set intent, we re-validate with `is_valid_intent` and bubble - /// `InvalidJson` so the frontend toasts a clear error. + /// pair as JSON. Truncates to the trailing 2000 chars of the + /// transcript so the prompt budget stays well under any model's + /// context window. Determinism is enforced by temperature 0.0; + /// the parsed intent is re-validated with `is_valid_intent` and + /// invalid JSON bubbles as `InvalidJson` so the frontend toasts a + /// clear error. pub fn extract_content_tags( &self, transcript: &str, @@ -338,12 +341,11 @@ impl LlmEngine { max_tokens: 96, temperature: 0.0, stop_sequences: vec!["<|im_end|>".to_string(), "<|im_end_of_text|>".to_string()], - grammar: Some(grammars::CONTENT_TAGS_GRAMMAR.to_string()), + grammar: None, }, )?; - let tags: prompts::ContentTags = serde_json::from_str(raw.trim()) - .map_err(|e| EngineError::InvalidJson(format!("{e}: raw={raw:?}")))?; + let tags: prompts::ContentTags = parse_json_payload(&raw)?; if !prompts::is_valid_intent(&tags.intent) { return Err(EngineError::InvalidJson(format!( "intent out of closed set: {}", @@ -459,6 +461,62 @@ fn first_stop_index(text: &str, stop_sequences: &[String]) -> Option { .min() } +fn json_envelope_complete(text: &str) -> bool { + extract_json_envelope(text) == Some(text.trim()) +} + +fn extract_json_envelope(text: &str) -> Option<&str> { + let start = text + .char_indices() + .find_map(|(idx, ch)| (ch == '{' || ch == '[').then_some(idx))?; + let mut chars = text[start..].char_indices(); + let (_, first) = chars.next()?; + + let mut stack = vec![match first { + '{' => '}', + '[' => ']', + _ => unreachable!(), + }]; + let mut in_string = false; + let mut escaped = false; + + while let Some((offset, ch)) = chars.next() { + if in_string { + if escaped { + escaped = false; + } else if ch == '\\' { + escaped = true; + } else if ch == '"' { + in_string = false; + } + continue; + } + + match ch { + '"' => in_string = true, + '{' => stack.push('}'), + '[' => stack.push(']'), + '}' | ']' => { + if stack.pop() != Some(ch) { + return None; + } + if stack.is_empty() { + let end = start + offset + ch.len_utf8(); + return Some(&text[start..end]); + } + } + _ => {} + } + } + + None +} + +fn parse_json_payload Deserialize<'de>>(raw: &str) -> Result { + let payload = extract_json_envelope(raw).unwrap_or(raw.trim()); + serde_json::from_str(payload).map_err(|e| EngineError::InvalidJson(format!("{e}: raw={raw:?}"))) +} + fn render_chat_prompt( model: &LlamaModel, messages: &[(&str, &str)], @@ -548,6 +606,47 @@ mod tests { assert_eq!(index, Some(5)); } + #[test] + fn json_envelope_complete_detects_finished_object() { + assert!(json_envelope_complete( + r#"{"topic":"meeting","intent":"planning"}"# + )); + } + + #[test] + fn json_envelope_complete_detects_finished_array() { + assert!(json_envelope_complete(r#"["Call plumber","Buy milk"]"#)); + } + + #[test] + fn json_envelope_complete_ignores_braces_inside_strings() { + assert!(!json_envelope_complete(r#"{"topic":"literal } brace""#)); + } + + #[test] + fn json_envelope_complete_rejects_prefixes_and_trailing_text() { + assert!(!json_envelope_complete(r#"{"topic":"meeting""#)); + assert!(!json_envelope_complete(r#"{"topic":"meeting"} extra"#)); + } + + #[test] + fn extract_json_envelope_skips_qwen_thinking_prefix() { + let raw = + "\n\n\n\n{\"topic\":\"grant-application\",\"intent\":\"planning\"}"; + assert_eq!( + extract_json_envelope(raw), + Some("{\"topic\":\"grant-application\",\"intent\":\"planning\"}"), + ); + } + + #[test] + fn extract_json_envelope_handles_arrays_and_trailing_stop_text() { + assert_eq!( + extract_json_envelope("prefix [\"Call plumber\",\"Buy milk\"]<|im_end|>"), + Some("[\"Call plumber\",\"Buy milk\"]"), + ); + } + #[test] fn prompt_preflight_rejects_oversized_prompt_tokens() { let err = preflight_context_window(7_105, 1_024).unwrap_err(); diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 95075d9..ce58e81 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -41,7 +41,7 @@ magnotia-llm = { path = "../crates/llm" } # autostart plugins are desktop-only — gated below under # cfg(not(target_os = "android")). The dialog, opener, and notification # plugins all support Android natively so live in the unconditional list. -tauri = { version = "2" } +tauri = { version = "2", features = [] } tauri-plugin-opener = "2" tauri-plugin-dialog = "2" # Phase 6 nudges: OS-native notifications via the frontend-owned nudge From 2ca01e7c9dd11addbc7db97e9606585f6d920f7b Mon Sep 17 00:00:00 2001 From: Jake Date: Wed, 13 May 2026 08:26:19 +0100 Subject: [PATCH 02/82] feat(export): prefer native save dialog over browser blob fallback DictationPage + FilesPage handleExport() now use Tauri save() + write_text_file_cmd when in the desktop runtime; browser blob path remains as fallback when hasTauriRuntime() is false. saveMarkdown.ts surfaces dialog errors via toasts. Adds txt to the extension map. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/pages/DictationPage.svelte | 27 ++++++++++++++++-- src/lib/pages/FilesPage.svelte | 27 ++++++++++++++++-- src/lib/utils/saveMarkdown.ts | 45 ++++++++++++++++++++++-------- 3 files changed, 81 insertions(+), 18 deletions(-) diff --git a/src/lib/pages/DictationPage.svelte b/src/lib/pages/DictationPage.svelte index 6b0b146..53d4d82 100644 --- a/src/lib/pages/DictationPage.svelte +++ b/src/lib/pages/DictationPage.svelte @@ -637,17 +637,38 @@ activeTemplate = ""; } - function handleExport(format) { + async function handleExport(format) { if (!transcript) return; showExportMenu = false; const content = exportTranscript(transcript, segments, format); - const extMap = { vtt: "vtt", srt: "srt", md: "md", csv: "csv", html: "html" }; + const extMap = { txt: "txt", vtt: "vtt", srt: "srt", md: "md", csv: "csv", html: "html" }; const ext = extMap[format] || "txt"; + const defaultPath = `magnotia-${new Date().toISOString().slice(0, 10)}.${ext}`; + + if (hasTauriRuntime()) { + try { + const { save } = await import("@tauri-apps/plugin-dialog"); + const path = await save({ + title: `Export ${format.toUpperCase()} transcript`, + defaultPath, + filters: [{ name: format.toUpperCase(), extensions: [ext] }], + }); + if (!path) return; + await invoke("write_text_file_cmd", { path, contents: content }); + toasts.success(`Exported ${format.toUpperCase()} transcript`); + return; + } catch (err) { + console.error("dictation export failed", err); + error = `Export failed: ${typeof err === "string" ? err : err.message || err}`; + return; + } + } + const blob = new Blob([content], { type: "text/plain" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; - a.download = `magnotia-${new Date().toISOString().slice(0, 10)}.${ext}`; + a.download = defaultPath; a.click(); URL.revokeObjectURL(url); } diff --git a/src/lib/pages/FilesPage.svelte b/src/lib/pages/FilesPage.svelte index 80914de..9bc0e07 100644 --- a/src/lib/pages/FilesPage.svelte +++ b/src/lib/pages/FilesPage.svelte @@ -8,6 +8,7 @@ import Card from "$lib/components/Card.svelte"; import EmptyState from "$lib/components/EmptyState.svelte"; import { exportTranscript } from "$lib/utils/export.js"; + import { hasTauriRuntime } from "$lib/utils/runtime"; import { Upload } from 'lucide-svelte'; let fileTranscript = $state(""); @@ -132,17 +133,37 @@ } } - function handleExport(format) { + async function handleExport(format) { if (!fileTranscript) return; showExportMenu = false; const content = exportTranscript(fileTranscript, segments, format); - const extMap = { vtt: "vtt", srt: "srt", md: "md", csv: "csv", html: "html" }; + const extMap = { txt: "txt", vtt: "vtt", srt: "srt", md: "md", csv: "csv", html: "html" }; const ext = extMap[format] || "txt"; + const defaultPath = `magnotia-file-${new Date().toISOString().slice(0, 10)}.${ext}`; + + if (hasTauriRuntime()) { + try { + const { save } = await import("@tauri-apps/plugin-dialog"); + const path = await save({ + title: `Export ${format.toUpperCase()} transcript`, + defaultPath, + filters: [{ name: format.toUpperCase(), extensions: [ext] }], + }); + if (!path) return; + await invoke("write_text_file_cmd", { path, contents: content }); + return; + } catch (err) { + console.error("file transcript export failed", err); + error = `Export failed: ${typeof err === "string" ? err : err.message || err}`; + return; + } + } + const blob = new Blob([content], { type: "text/plain" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; - a.download = `magnotia-file-${new Date().toISOString().slice(0, 10)}.${ext}`; + a.download = defaultPath; a.click(); URL.revokeObjectURL(url); } diff --git a/src/lib/utils/saveMarkdown.ts b/src/lib/utils/saveMarkdown.ts index bd88540..54ed09b 100644 --- a/src/lib/utils/saveMarkdown.ts +++ b/src/lib/utils/saveMarkdown.ts @@ -33,14 +33,24 @@ export function suggestedFilename(item: TranscriptEntry): string { export async function saveTranscriptAsMarkdown( item: TranscriptEntry, ): Promise { - if (!hasTauriRuntime()) return null; + if (!hasTauriRuntime()) { + toasts.error("Export unavailable", "Native save dialogs are only available in the desktop app."); + return null; + } const md = buildMarkdown(item); const defaultPath = suggestedFilename(item); - const path = await save({ - title: "Save transcript as Markdown", - defaultPath, - filters: [{ name: "Markdown", extensions: ["md"] }], - }); + let path: string | null = null; + try { + path = await save({ + title: "Save transcript as Markdown", + defaultPath, + filters: [{ name: "Markdown", extensions: ["md"] }], + }); + } catch (err) { + console.error("saveTranscriptAsMarkdown dialog failed", err); + toasts.error("Couldn't open save dialog", String(err)); + return null; + } if (!path) return null; try { await invoke("write_text_file_cmd", { path, contents: md }); @@ -64,13 +74,24 @@ export async function saveTranscriptAsMarkdown( export async function exportTranscriptsToDir( items: TranscriptEntry[], ): Promise { - if (!hasTauriRuntime() || items.length === 0) return 0; + if (items.length === 0) return 0; + if (!hasTauriRuntime()) { + toasts.error("Export unavailable", "Native folder dialogs are only available in the desktop app."); + return 0; + } - const dir = await open({ - directory: true, - multiple: false, - title: "Choose export folder", - }); + let dir: string | string[] | null = null; + try { + dir = await open({ + directory: true, + multiple: false, + title: "Choose export folder", + }); + } catch (err) { + console.error("exportTranscriptsToDir dialog failed", err); + toasts.error("Couldn't open export folder dialog", String(err)); + return 0; + } if (!dir || typeof dir !== "string") return 0; const used = new Set(); From bc2db9152070a9ee29a595812d068dad076ea966 Mon Sep 17 00:00:00 2001 From: Jake Date: Wed, 13 May 2026 08:42:22 +0100 Subject: [PATCH 03/82] =?UTF-8?q?agent:=20lumotia-rebrand=20=E2=80=94=20fi?= =?UTF-8?q?x=20QC=20blockers=20for=20phase=200?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 0 QC found two real blockers in the baseline commits: 1. crates/llm/src/lib.rs:215 — early-break guard was inverted. The guard fired when grammar.is_some() (where grammar already enforces shape, making the check redundant) and was disabled for grammar.is_none() (the content-tags path that actually needs it). Flipped to grammar.is_none() so the JSON-envelope short-circuit fires for free-form JSON generation as the commit message implied. 2. src/lib/pages/FilesPage.svelte — handleExport() success path had no toast, asymmetric with DictationPage which does. Added the toasts import and a toasts.success() call after the native save so both export surfaces give consistent feedback. cargo test -p magnotia-llm --lib: 21 pass. npm run check: 0 errors. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/llm/src/lib.rs | 2 +- src/lib/pages/FilesPage.svelte | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/llm/src/lib.rs b/crates/llm/src/lib.rs index 65c92ad..067fc7b 100644 --- a/crates/llm/src/lib.rs +++ b/crates/llm/src/lib.rs @@ -212,7 +212,7 @@ impl LlmEngine { generated.push_str(&piece); sampler.accept(next); - if config.grammar.is_some() && json_envelope_complete(&generated) { + if config.grammar.is_none() && json_envelope_complete(&generated) { break; } diff --git a/src/lib/pages/FilesPage.svelte b/src/lib/pages/FilesPage.svelte index 9bc0e07..8d704af 100644 --- a/src/lib/pages/FilesPage.svelte +++ b/src/lib/pages/FilesPage.svelte @@ -9,6 +9,7 @@ import EmptyState from "$lib/components/EmptyState.svelte"; import { exportTranscript } from "$lib/utils/export.js"; import { hasTauriRuntime } from "$lib/utils/runtime"; + import { toasts } from "$lib/stores/toasts.svelte.js"; import { Upload } from 'lucide-svelte'; let fileTranscript = $state(""); @@ -151,6 +152,7 @@ }); if (!path) return; await invoke("write_text_file_cmd", { path, contents: content }); + toasts.success(`Exported ${format.toUpperCase()} transcript`); return; } catch (err) { console.error("file transcript export failed", err); From 089349d966cd79a34c6d6e53e75b7d073e46c5f1 Mon Sep 17 00:00:00 2001 From: Jake Date: Wed, 13 May 2026 08:48:09 +0100 Subject: [PATCH 04/82] =?UTF-8?q?agent:=20lumotia-rebrand=20=E2=80=94=20ru?= =?UTF-8?q?st=20workspace=20crates=20magnotia-*=20->=20lumotia-*?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2 of the rebrand cascade. Renames all 9 workspace crates from magnotia-* to lumotia-* plus the src-tauri binary crate name: - magnotia-ai-formatting -> lumotia-ai-formatting - magnotia-audio -> lumotia-audio - magnotia-cloud-providers -> lumotia-cloud-providers - magnotia-core -> lumotia-core - magnotia-hotkey -> lumotia-hotkey - magnotia-llm -> lumotia-llm - magnotia-mcp -> lumotia-mcp - magnotia-storage -> lumotia-storage - magnotia-transcription -> lumotia-transcription - magnotia -> lumotia (src-tauri binary) - magnotia_lib -> lumotia_lib (src-tauri lib target) Crate directories (crates/audio/ etc.) stay as-is; only the Cargo.toml [package] name field changes plus all consumer module imports (magnotia_core -> lumotia_core, etc.). Remaining magnotia_* references at this point are intentional and scoped to later phases: tracing targets (Phase 4), DB setting keys magnotia_preferences/magnotia_history (Phase 5). cargo build --workspace passes. cargo test --workspace: 330 pass, 0 fail. Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.lock | 326 +++++++++--------- crates/ai-formatting/Cargo.toml | 6 +- crates/ai-formatting/src/llm_client.rs | 4 +- crates/ai-formatting/src/pipeline.rs | 6 +- crates/ai-formatting/src/to_plain_text.rs | 2 +- crates/audio/Cargo.toml | 4 +- crates/audio/src/capture.rs | 22 +- crates/audio/src/concurrency.rs | 6 +- crates/audio/src/decode.rs | 4 +- crates/audio/src/resample.rs | 6 +- crates/audio/src/streaming_resample.rs | 4 +- crates/audio/src/vad.rs | 2 +- crates/audio/src/wav.rs | 4 +- crates/cloud-providers/Cargo.toml | 4 +- crates/cloud-providers/src/provider.rs | 6 +- crates/core/Cargo.toml | 2 +- crates/core/examples/tuning_log_demo.rs | 4 +- crates/core/src/error.rs | 4 +- crates/core/src/tuning.rs | 2 +- crates/hotkey/Cargo.toml | 4 +- crates/llm/Cargo.toml | 6 +- crates/llm/src/lib.rs | 2 +- crates/llm/src/model_manager.rs | 2 +- crates/llm/tests/content_tags_smoke.rs | 2 +- crates/llm/tests/smoke.rs | 6 +- crates/mcp/Cargo.toml | 6 +- crates/mcp/src/lib.rs | 10 +- crates/mcp/src/main.rs | 10 +- crates/storage/Cargo.toml | 6 +- crates/storage/src/error.rs | 4 +- crates/storage/src/file_storage.rs | 10 +- crates/transcription/Cargo.toml | 12 +- crates/transcription/build.rs | 2 +- crates/transcription/src/concurrency.rs | 4 +- crates/transcription/src/lib.rs | 6 +- crates/transcription/src/local_engine.rs | 6 +- crates/transcription/src/model_manager.rs | 26 +- crates/transcription/src/orchestrator.rs | 10 +- crates/transcription/src/registry.rs | 8 +- crates/transcription/src/transcriber.rs | 4 +- .../transcription/src/whisper_rs_backend.rs | 10 +- crates/transcription/tests/thread_sweep.rs | 4 +- src-tauri/Cargo.toml | 26 +- src-tauri/src/commands/audio.rs | 2 +- src-tauri/src/commands/diagnostics.rs | 6 +- src-tauri/src/commands/feedback.rs | 2 +- src-tauri/src/commands/hardware.rs | 6 +- src-tauri/src/commands/hotkey.rs | 4 +- src-tauri/src/commands/intentions.rs | 2 +- src-tauri/src/commands/live.rs | 26 +- src-tauri/src/commands/llm.rs | 12 +- src-tauri/src/commands/meeting.rs | 2 +- src-tauri/src/commands/models.rs | 14 +- src-tauri/src/commands/profiles.rs | 8 +- src-tauri/src/commands/rituals.rs | 4 +- src-tauri/src/commands/tasks.rs | 8 +- src-tauri/src/commands/transcription.rs | 26 +- src-tauri/src/commands/transcripts.rs | 6 +- src-tauri/src/lib.rs | 10 +- src-tauri/src/main.rs | 2 +- 60 files changed, 372 insertions(+), 372 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 62c007a..791bd2f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2752,6 +2752,169 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "lumotia" +version = "0.1.0" +dependencies = [ + "arboard", + "base64 0.22.1", + "gdk", + "gtk", + "lumotia-ai-formatting", + "lumotia-audio", + "lumotia-cloud-providers", + "lumotia-core", + "lumotia-hotkey", + "lumotia-llm", + "lumotia-storage", + "lumotia-transcription", + "objc2", + "objc2-foundation", + "serde", + "serde_json", + "sqlx", + "tauri", + "tauri-build", + "tauri-plugin-autostart", + "tauri-plugin-dialog", + "tauri-plugin-global-shortcut", + "tauri-plugin-notification", + "tauri-plugin-opener", + "tauri-plugin-window-state", + "tempfile", + "tokio", + "tracing", + "tracing-subscriber", + "uuid", + "webkit2gtk", +] + +[[package]] +name = "lumotia-ai-formatting" +version = "0.1.0" +dependencies = [ + "lumotia-core", + "lumotia-llm", + "regex-lite", + "tracing", +] + +[[package]] +name = "lumotia-audio" +version = "0.1.0" +dependencies = [ + "cpal", + "hound", + "lumotia-core", + "regex", + "rubato", + "serde", + "symphonia", + "tokio", + "tracing", +] + +[[package]] +name = "lumotia-cloud-providers" +version = "0.1.0" +dependencies = [ + "async-trait", + "lumotia-core", + "serde", +] + +[[package]] +name = "lumotia-core" +version = "0.1.0" +dependencies = [ + "async-trait", + "libloading 0.8.9", + "num_cpus", + "serde", + "serde_json", + "sysinfo", + "tempfile", + "thiserror 2.0.18", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "lumotia-hotkey" +version = "0.1.0" +dependencies = [ + "evdev", + "lumotia-core", + "nix 0.29.0", + "notify", + "serde", + "tokio", + "tracing", +] + +[[package]] +name = "lumotia-llm" +version = "0.1.0" +dependencies = [ + "encoding_rs", + "futures-util", + "llama-cpp-2", + "lumotia-core", + "reqwest 0.12.28", + "serde", + "serde_json", + "sha2", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tracing", +] + +[[package]] +name = "lumotia-mcp" +version = "0.1.0" +dependencies = [ + "anyhow", + "lumotia-storage", + "serde", + "serde_json", + "sqlx", + "tempfile", + "tokio", +] + +[[package]] +name = "lumotia-storage" +version = "0.1.0" +dependencies = [ + "log", + "lumotia-core", + "serde", + "sqlx", + "thiserror 1.0.69", + "tokio", + "uuid", +] + +[[package]] +name = "lumotia-transcription" +version = "0.1.0" +dependencies = [ + "async-trait", + "futures-util", + "lumotia-cloud-providers", + "lumotia-core", + "num_cpus", + "reqwest 0.12.28", + "sha2", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tracing", + "transcribe-rs", + "whisper-rs", +] + [[package]] name = "lzma-rust2" version = "0.15.7" @@ -2785,169 +2948,6 @@ dependencies = [ "libc", ] -[[package]] -name = "magnotia" -version = "0.1.0" -dependencies = [ - "arboard", - "base64 0.22.1", - "gdk", - "gtk", - "magnotia-ai-formatting", - "magnotia-audio", - "magnotia-cloud-providers", - "magnotia-core", - "magnotia-hotkey", - "magnotia-llm", - "magnotia-storage", - "magnotia-transcription", - "objc2", - "objc2-foundation", - "serde", - "serde_json", - "sqlx", - "tauri", - "tauri-build", - "tauri-plugin-autostart", - "tauri-plugin-dialog", - "tauri-plugin-global-shortcut", - "tauri-plugin-notification", - "tauri-plugin-opener", - "tauri-plugin-window-state", - "tempfile", - "tokio", - "tracing", - "tracing-subscriber", - "uuid", - "webkit2gtk", -] - -[[package]] -name = "magnotia-ai-formatting" -version = "0.1.0" -dependencies = [ - "magnotia-core", - "magnotia-llm", - "regex-lite", - "tracing", -] - -[[package]] -name = "magnotia-audio" -version = "0.1.0" -dependencies = [ - "cpal", - "hound", - "magnotia-core", - "regex", - "rubato", - "serde", - "symphonia", - "tokio", - "tracing", -] - -[[package]] -name = "magnotia-cloud-providers" -version = "0.1.0" -dependencies = [ - "async-trait", - "magnotia-core", - "serde", -] - -[[package]] -name = "magnotia-core" -version = "0.1.0" -dependencies = [ - "async-trait", - "libloading 0.8.9", - "num_cpus", - "serde", - "serde_json", - "sysinfo", - "tempfile", - "thiserror 2.0.18", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "magnotia-hotkey" -version = "0.1.0" -dependencies = [ - "evdev", - "magnotia-core", - "nix 0.29.0", - "notify", - "serde", - "tokio", - "tracing", -] - -[[package]] -name = "magnotia-llm" -version = "0.1.0" -dependencies = [ - "encoding_rs", - "futures-util", - "llama-cpp-2", - "magnotia-core", - "reqwest 0.12.28", - "serde", - "serde_json", - "sha2", - "tempfile", - "thiserror 2.0.18", - "tokio", - "tracing", -] - -[[package]] -name = "magnotia-mcp" -version = "0.1.0" -dependencies = [ - "anyhow", - "magnotia-storage", - "serde", - "serde_json", - "sqlx", - "tempfile", - "tokio", -] - -[[package]] -name = "magnotia-storage" -version = "0.1.0" -dependencies = [ - "log", - "magnotia-core", - "serde", - "sqlx", - "thiserror 1.0.69", - "tokio", - "uuid", -] - -[[package]] -name = "magnotia-transcription" -version = "0.1.0" -dependencies = [ - "async-trait", - "futures-util", - "magnotia-cloud-providers", - "magnotia-core", - "num_cpus", - "reqwest 0.12.28", - "sha2", - "tempfile", - "thiserror 2.0.18", - "tokio", - "tracing", - "transcribe-rs", - "whisper-rs", -] - [[package]] name = "markup5ever" version = "0.14.1" diff --git a/crates/ai-formatting/Cargo.toml b/crates/ai-formatting/Cargo.toml index 0d0a1a3..c5ddd1a 100644 --- a/crates/ai-formatting/Cargo.toml +++ b/crates/ai-formatting/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "magnotia-ai-formatting" +name = "lumotia-ai-formatting" version = "0.1.0" edition = "2021" description = "Text post-processing pipeline: filler removal, British English conversion, formatting for Magnotia" [dependencies] -magnotia-core = { path = "../core" } -magnotia-llm = { path = "../llm" } +lumotia-core = { path = "../core" } +lumotia-llm = { path = "../llm" } regex-lite = "0.1" tracing = "0.1" diff --git a/crates/ai-formatting/src/llm_client.rs b/crates/ai-formatting/src/llm_client.rs index b123b7b..01e85ff 100644 --- a/crates/ai-formatting/src/llm_client.rs +++ b/crates/ai-formatting/src/llm_client.rs @@ -3,7 +3,7 @@ //! The llm_client is not yet wired to a running model. This module defines //! the prompt contract so that wiring it produces correct, hardened output. -use magnotia_llm::{EngineError, LlmEngine}; +use lumotia_llm::{EngineError, LlmEngine}; /// System prompt sent before every cleanup call. /// @@ -161,7 +161,7 @@ pub fn cleanup_text( #[cfg(test)] mod tests { use super::*; - use magnotia_llm::EngineError; + use lumotia_llm::EngineError; #[test] fn empty_terms_returns_empty_string() { diff --git a/crates/ai-formatting/src/pipeline.rs b/crates/ai-formatting/src/pipeline.rs index 93aab42..a6a0f15 100644 --- a/crates/ai-formatting/src/pipeline.rs +++ b/crates/ai-formatting/src/pipeline.rs @@ -1,6 +1,6 @@ -use magnotia_core::constants::SMART_PARAGRAPH_GAP_SECS; -use magnotia_core::types::Segment; -use magnotia_llm::LlmEngine; +use lumotia_core::constants::SMART_PARAGRAPH_GAP_SECS; +use lumotia_core::types::Segment; +use lumotia_llm::LlmEngine; use crate::{llm_client, rule_based, to_plain_text::to_plain_text}; diff --git a/crates/ai-formatting/src/to_plain_text.rs b/crates/ai-formatting/src/to_plain_text.rs index e94f495..24a938f 100644 --- a/crates/ai-formatting/src/to_plain_text.rs +++ b/crates/ai-formatting/src/to_plain_text.rs @@ -13,7 +13,7 @@ //! is the whitespace pass and empty-segment filter, plus a single //! public function the pipeline can depend on. -use magnotia_core::types::Segment; +use lumotia_core::types::Segment; /// Join transcription segments into a single plain-text string /// suitable for feeding to an LLM cleanup prompt. diff --git a/crates/audio/Cargo.toml b/crates/audio/Cargo.toml index dc0366d..ca971e6 100644 --- a/crates/audio/Cargo.toml +++ b/crates/audio/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "magnotia-audio" +name = "lumotia-audio" version = "0.1.0" edition = "2021" description = "Audio capture (cpal), VAD, resampling (rubato), file decoding (symphonia), WAV I/O (hound) for Magnotia" [dependencies] -magnotia-core = { path = "../core" } +lumotia-core = { path = "../core" } # Microphone capture cpal = "0.17" diff --git a/crates/audio/src/capture.rs b/crates/audio/src/capture.rs index b545368..e8093c2 100644 --- a/crates/audio/src/capture.rs +++ b/crates/audio/src/capture.rs @@ -8,7 +8,7 @@ use regex::Regex; use serde::{Deserialize, Serialize}; use std::sync::OnceLock; -use magnotia_core::error::{MagnotiaError, Result}; +use lumotia_core::error::{MagnotiaError, Result}; const AUDIO_CHANNEL_CAPACITY: usize = 32; @@ -149,7 +149,7 @@ impl MicrophoneCapture { for device in devices { let name = device_display_name(&device).unwrap_or_default(); if name == device_name { - tracing::info!(target: "magnotia_audio", "start_with_device: opening explicit device '{name}'"); + tracing::info!(target: "lumotia_audio", "start_with_device: opening explicit device '{name}'"); return open_and_validate(device, &name, /* require_audio = */ true); } } @@ -196,7 +196,7 @@ impl MicrophoneCapture { }); tracing::info!( - target: "magnotia_audio", + target: "lumotia_audio", device_count = all_devices.len(), default = %default_name, "enumerated input devices" @@ -211,7 +211,7 @@ impl MicrophoneCapture { match open_and_validate(device.clone(), &name, true) { Ok(result) => return Ok(result), Err(e) => { - tracing::warn!(target: "magnotia_audio", device = %name, error = %e, "candidate device rejected"); + tracing::warn!(target: "lumotia_audio", device = %name, error = %e, "candidate device rejected"); } } } @@ -219,7 +219,7 @@ impl MicrophoneCapture { // Second pass: accept anything that delivers bytes (monitor sources // included). Better to capture from a monitor than fail entirely. tracing::warn!( - target: "magnotia_audio", + target: "lumotia_audio", "no non-monitor mic produced audio; falling back to monitor/loopback sources" ); for device in &all_devices { @@ -227,7 +227,7 @@ impl MicrophoneCapture { match open_and_validate(device.clone(), &name, false) { Ok(result) => { tracing::warn!( - target: "magnotia_audio", + target: "lumotia_audio", device = %name, "capturing from likely monitor source; recordings may be silent or contain system audio" ); @@ -367,7 +367,7 @@ fn open_and_validate( let format = config.sample_format(); tracing::info!( - target: "magnotia_audio", + target: "lumotia_audio", device = %name, sample_rate, channels, @@ -467,7 +467,7 @@ fn open_and_validate( let rms = (sum_sq / total_samples as f64).sqrt() as f32; tracing::info!( - target: "magnotia_audio", + target: "lumotia_audio", device = %name, samples = total_samples, rms, @@ -499,7 +499,7 @@ fn open_and_validate( } } - tracing::info!(target: "magnotia_audio", device = %name, "selected microphone"); + tracing::info!(target: "lumotia_audio", device = %name, "selected microphone"); Ok(( MicrophoneCapture { stream: Some(stream), @@ -547,7 +547,7 @@ where move |err| { // Surface stream errors to the live session via err_tx so the // frontend can show a toast. - tracing::error!(target: "magnotia_audio", error = %err, "capture stream error"); + tracing::error!(target: "lumotia_audio", error = %err, "capture stream error"); if err_tx .try_send(CaptureRuntimeError { device_name: err_device_name.clone(), @@ -560,7 +560,7 @@ where // even if the frontend never received the typed event. let prior = dropped_errors.fetch_add(1, Ordering::Relaxed); tracing::warn!( - target: "magnotia_audio", + target: "lumotia_audio", device = %err_device_name, dropped_error = prior + 1, "capture error channel full; dropping runtime error" diff --git a/crates/audio/src/concurrency.rs b/crates/audio/src/concurrency.rs index e0e68fb..53b136a 100644 --- a/crates/audio/src/concurrency.rs +++ b/crates/audio/src/concurrency.rs @@ -1,7 +1,7 @@ use std::path::Path; -use magnotia_core::error::Result; -use magnotia_core::types::AudioSamples; +use lumotia_core::error::Result; +use lumotia_core::types::AudioSamples; use crate::decode::decode_audio_file; use crate::resample::resample_to_16khz; @@ -16,6 +16,6 @@ pub async fn decode_and_resample(path: &Path) -> Result { }) .await .map_err(|e| { - magnotia_core::error::MagnotiaError::AudioDecodeFailed(format!("Task join error: {e}")) + lumotia_core::error::MagnotiaError::AudioDecodeFailed(format!("Task join error: {e}")) })? } diff --git a/crates/audio/src/decode.rs b/crates/audio/src/decode.rs index d4953c9..2b8f8b1 100644 --- a/crates/audio/src/decode.rs +++ b/crates/audio/src/decode.rs @@ -9,8 +9,8 @@ use symphonia::core::io::MediaSourceStream; use symphonia::core::meta::MetadataOptions; use symphonia::core::probe::Hint; -use magnotia_core::error::{MagnotiaError, Result}; -use magnotia_core::types::AudioSamples; +use lumotia_core::error::{MagnotiaError, Result}; +use lumotia_core::types::AudioSamples; /// Decode an audio file to mono f32 PCM samples. /// Supports all formats symphonia handles: mp3, aac, flac, wav, ogg, etc. diff --git a/crates/audio/src/resample.rs b/crates/audio/src/resample.rs index 93301f8..f0e2c24 100644 --- a/crates/audio/src/resample.rs +++ b/crates/audio/src/resample.rs @@ -2,9 +2,9 @@ use rubato::{ Resampler, SincFixedIn, SincInterpolationParameters, SincInterpolationType, WindowFunction, }; -use magnotia_core::constants::WHISPER_SAMPLE_RATE; -use magnotia_core::error::{MagnotiaError, Result}; -use magnotia_core::types::AudioSamples; +use lumotia_core::constants::WHISPER_SAMPLE_RATE; +use lumotia_core::error::{MagnotiaError, Result}; +use lumotia_core::types::AudioSamples; /// Resample audio to 16kHz mono using sinc interpolation (rubato). /// Returns a new AudioSamples at the target sample rate. diff --git a/crates/audio/src/streaming_resample.rs b/crates/audio/src/streaming_resample.rs index 17984cc..3f263f3 100644 --- a/crates/audio/src/streaming_resample.rs +++ b/crates/audio/src/streaming_resample.rs @@ -27,8 +27,8 @@ use rubato::{ Resampler, SincFixedIn, SincInterpolationParameters, SincInterpolationType, WindowFunction, }; -use magnotia_core::constants::WHISPER_SAMPLE_RATE; -use magnotia_core::error::{MagnotiaError, Result}; +use lumotia_core::constants::WHISPER_SAMPLE_RATE; +use lumotia_core::error::{MagnotiaError, Result}; /// Number of input samples the rubato resampler consumes per `process()` /// call. Matches the chunk size used in `resample::resample_to_16khz`. diff --git a/crates/audio/src/vad.rs b/crates/audio/src/vad.rs index 700ef15..3033b3e 100644 --- a/crates/audio/src/vad.rs +++ b/crates/audio/src/vad.rs @@ -7,7 +7,7 @@ // For now, all audio is treated as speech. This matches v0.2 behaviour // (no VAD) and doesn't affect core functionality. -use magnotia_core::constants::VAD_SPEECH_THRESHOLD; +use lumotia_core::constants::VAD_SPEECH_THRESHOLD; /// Stub speech detector. Treats all audio as speech. #[derive(Default)] diff --git a/crates/audio/src/wav.rs b/crates/audio/src/wav.rs index fd53851..8a53075 100644 --- a/crates/audio/src/wav.rs +++ b/crates/audio/src/wav.rs @@ -1,8 +1,8 @@ use std::io::BufWriter; use std::path::Path; -use magnotia_core::error::{MagnotiaError, Result}; -use magnotia_core::types::AudioSamples; +use lumotia_core::error::{MagnotiaError, Result}; +use lumotia_core::types::AudioSamples; /// Append-friendly WAV writer for long-running captures. /// diff --git a/crates/cloud-providers/Cargo.toml b/crates/cloud-providers/Cargo.toml index b8ecc4f..248a7a4 100644 --- a/crates/cloud-providers/Cargo.toml +++ b/crates/cloud-providers/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "magnotia-cloud-providers" +name = "lumotia-cloud-providers" version = "0.1.0" edition = "2021" description = "Provider trait and BYOK cloud STT scaffolding for Magnotia (Wyrdnote pending rebrand)" [dependencies] -magnotia-core = { path = "../core" } +lumotia-core = { path = "../core" } # Async-native trait. async_trait converts async fn into Pin> at the trait surface so TranscriptionProvider stays diff --git a/crates/cloud-providers/src/provider.rs b/crates/cloud-providers/src/provider.rs index 5c85b3e..184b28b 100644 --- a/crates/cloud-providers/src/provider.rs +++ b/crates/cloud-providers/src/provider.rs @@ -16,8 +16,8 @@ use std::fmt; use async_trait::async_trait; -use magnotia_core::error::Result; -use magnotia_core::types::{AudioSamples, ModelId, Transcript, TranscriptionOptions}; +use lumotia_core::error::Result; +use lumotia_core::types::{AudioSamples, ModelId, Transcript, TranscriptionOptions}; use serde::{Deserialize, Serialize}; /// Stable, lower-kebab-case identifier for a provider. Used in user @@ -70,7 +70,7 @@ pub enum CostClass { } /// Capabilities a provider advertises to the orchestrator and the UI. -/// Superset of `magnotia_transcription::TranscriberCapabilities` for +/// Superset of `lumotia_transcription::TranscriberCapabilities` for /// local providers, with extra fields cloud providers populate. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub struct ProviderCapabilities { diff --git a/crates/core/Cargo.toml b/crates/core/Cargo.toml index d432aa1..dc65e6a 100644 --- a/crates/core/Cargo.toml +++ b/crates/core/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "magnotia-core" +name = "lumotia-core" version = "0.1.0" edition = "2021" description = "Core types, constants, traits, hardware detection, and model registry for Magnotia" diff --git a/crates/core/examples/tuning_log_demo.rs b/crates/core/examples/tuning_log_demo.rs index ac35be7..0f57463 100644 --- a/crates/core/examples/tuning_log_demo.rs +++ b/crates/core/examples/tuning_log_demo.rs @@ -9,11 +9,11 @@ //! exactly one INFO line; subsequent calls with the same tuple are //! silenced by the per-process log-once cache. -use magnotia_core::tuning::{inference_thread_count, Workload}; +use lumotia_core::tuning::{inference_thread_count, Workload}; fn main() { tracing_subscriber::fmt() - .with_env_filter("magnotia_core=info") + .with_env_filter("lumotia_core=info") .with_target(true) .with_writer(std::io::stderr) .init(); diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index 0a318ab..2f0abbd 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -34,7 +34,7 @@ pub enum MagnotiaError { #[error("file not found: '{}'", .0.display())] FileNotFound(PathBuf), - /// Structured storage failure flowed up from `magnotia_storage::Error` via + /// Structured storage failure flowed up from `lumotia_storage::Error` via /// its `From` impl. Display reads through to `detail` so the operation + /// source context produced by the storage crate isn't double-prefixed. #[error("{detail}")] @@ -58,7 +58,7 @@ pub enum MagnotiaError { /// Coarse discriminator for `MagnotiaError::Storage`. The storage crate maps /// its full typed error onto one of these kinds at the boundary. Backend code /// that wants finer-grained branching pattern-matches on -/// `magnotia_storage::Error` directly. +/// `lumotia_storage::Error` directly. #[derive(Debug, Clone, Copy, Serialize)] #[serde(rename_all = "snake_case")] pub enum StorageKind { diff --git a/crates/core/src/tuning.rs b/crates/core/src/tuning.rs index 45c0e36..e0d560c 100644 --- a/crates/core/src/tuning.rs +++ b/crates/core/src/tuning.rs @@ -89,7 +89,7 @@ pub fn inference_thread_count(workload: Workload, gpu_offloaded: bool) -> usize if let Ok(mut seen) = log_seen().lock() { if seen.insert(key) { tracing::info!( - target: "magnotia_core::tuning", + target: "lumotia_core::tuning", threads = final_value, workload = ?workload, gpu_offloaded, diff --git a/crates/hotkey/Cargo.toml b/crates/hotkey/Cargo.toml index ae58f4c..3487ab5 100644 --- a/crates/hotkey/Cargo.toml +++ b/crates/hotkey/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "magnotia-hotkey" +name = "lumotia-hotkey" version = "0.1.0" edition = "2021" description = "Wayland-compatible global hotkey listener for Magnotia — evdev backend with device hotplug" [dependencies] -magnotia-core = { path = "../core" } +lumotia-core = { path = "../core" } tokio = { version = "1", features = ["rt", "sync", "macros", "time"] } serde = { version = "1", features = ["derive"] } tracing = "0.1" diff --git a/crates/llm/Cargo.toml b/crates/llm/Cargo.toml index 4895077..7b9a869 100644 --- a/crates/llm/Cargo.toml +++ b/crates/llm/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "magnotia-llm" +name = "lumotia-llm" version = "0.1.0" edition = "2021" description = "Local LLM engine for Magnotia (Qwen3.5 / Qwen3.6 via llama-cpp-2): transcript cleanup, task extraction, micro-step decomposition" @@ -7,7 +7,7 @@ description = "Local LLM engine for Magnotia (Qwen3.5 / Qwen3.6 via llama-cpp-2) [features] # Default desktop build keeps the existing openmp + vulkan acceleration. # Mobile / CPU-only targets can drop one or both via: -# cargo build -p magnotia-llm --no-default-features +# cargo build -p lumotia-llm --no-default-features # These are independent so an Android Vulkan build can opt into vulkan # without openmp (the NDK ships OpenMP libs but the toolchain configuration # is fragile across NDK versions). @@ -16,7 +16,7 @@ gpu-vulkan = ["llama-cpp-2/vulkan"] openmp = ["llama-cpp-2/openmp"] [dependencies] -magnotia-core = { path = "../core" } +lumotia-core = { path = "../core" } encoding_rs = "0.8" futures-util = "0.3" llama-cpp-2 = { version = "0.1.146", default-features = false } diff --git a/crates/llm/src/lib.rs b/crates/llm/src/lib.rs index 067fc7b..df6456d 100644 --- a/crates/llm/src/lib.rs +++ b/crates/llm/src/lib.rs @@ -9,7 +9,7 @@ use llama_cpp_2::llama_batch::LlamaBatch; use llama_cpp_2::model::params::LlamaModelParams; use llama_cpp_2::model::{AddBos, LlamaChatMessage, LlamaChatTemplate, LlamaModel}; use llama_cpp_2::sampling::LlamaSampler; -use magnotia_core::tuning::{inference_thread_count, Workload}; +use lumotia_core::tuning::{inference_thread_count, Workload}; use serde::{Deserialize, Serialize}; pub mod grammars; diff --git a/crates/llm/src/model_manager.rs b/crates/llm/src/model_manager.rs index b250d2c..d6ed932 100644 --- a/crates/llm/src/model_manager.rs +++ b/crates/llm/src/model_manager.rs @@ -240,7 +240,7 @@ pub fn recommend_tier(total_ram_bytes: u64, total_vram_bytes: Option) -> Ll } pub fn model_dir() -> PathBuf { - magnotia_core::paths::app_paths().llm_models_dir() + lumotia_core::paths::app_paths().llm_models_dir() } pub fn model_path(id: LlmModelId) -> PathBuf { diff --git a/crates/llm/tests/content_tags_smoke.rs b/crates/llm/tests/content_tags_smoke.rs index 3f90cb5..39b99dd 100644 --- a/crates/llm/tests/content_tags_smoke.rs +++ b/crates/llm/tests/content_tags_smoke.rs @@ -10,7 +10,7 @@ use std::env; use std::path::PathBuf; -use magnotia_llm::{is_valid_intent, LlmEngine, LlmModelId}; +use lumotia_llm::{is_valid_intent, LlmEngine, LlmModelId}; #[test] fn extract_content_tags_returns_valid_pair() { diff --git a/crates/llm/tests/smoke.rs b/crates/llm/tests/smoke.rs index 3e01a07..8c4a1be 100644 --- a/crates/llm/tests/smoke.rs +++ b/crates/llm/tests/smoke.rs @@ -11,8 +11,8 @@ use std::env; use std::path::PathBuf; -use magnotia_llm::LlmEngine; -use magnotia_llm::LlmModelId; +use lumotia_llm::LlmEngine; +use lumotia_llm::LlmModelId; #[test] fn llama_cpp_2_smoke_generates_and_wraps() { @@ -32,7 +32,7 @@ fn llama_cpp_2_smoke_generates_and_wraps() { let completion = engine .generate( "Write exactly one short greeting.", - &magnotia_llm::GenerationConfig { + &lumotia_llm::GenerationConfig { max_tokens: 32, temperature: 0.0, stop_sequences: vec!["\n".to_string()], diff --git a/crates/mcp/Cargo.toml b/crates/mcp/Cargo.toml index 9d8f036..863b624 100644 --- a/crates/mcp/Cargo.toml +++ b/crates/mcp/Cargo.toml @@ -1,18 +1,18 @@ [package] -name = "magnotia-mcp" +name = "lumotia-mcp" version = "0.1.0" edition = "2021" description = "Read-only MCP stdio server exposing Magnotia transcripts and tasks to external agents" [[bin]] -name = "magnotia-mcp" +name = "lumotia-mcp" path = "src/main.rs" [lib] path = "src/lib.rs" [dependencies] -magnotia-storage = { path = "../storage" } +lumotia-storage = { path = "../storage" } sqlx = { version = "0.8", default-features = false, features = ["runtime-tokio", "sqlite"] } serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/crates/mcp/src/lib.rs b/crates/mcp/src/lib.rs index f022ad8..7b28a83 100644 --- a/crates/mcp/src/lib.rs +++ b/crates/mcp/src/lib.rs @@ -206,7 +206,7 @@ async fn list_transcripts_tool(pool: &SqlitePool, args: Value) -> Result Result Result Result Result { - let rows = magnotia_storage::list_tasks(pool) + let rows = lumotia_storage::list_tasks(pool) .await .map_err(|e| error(-32603, format!("DB error: {e}")))?; @@ -460,7 +460,7 @@ mod tests { }); let pool = sqlx::SqlitePool::connect("sqlite::memory:").await.unwrap(); - magnotia_storage::migrations::run_migrations(&pool) + lumotia_storage::migrations::run_migrations(&pool) .await .unwrap(); let response = handle_message(&pool, request).await.expect("has response"); diff --git a/crates/mcp/src/main.rs b/crates/mcp/src/main.rs index 3439ba4..b6dd3d4 100644 --- a/crates/mcp/src/main.rs +++ b/crates/mcp/src/main.rs @@ -1,12 +1,12 @@ //! Stdio entry point for magnotia-mcp. Reads newline-delimited JSON-RPC messages -//! from stdin, dispatches via `magnotia_mcp::handle_message`, writes responses to +//! from stdin, dispatches via `lumotia_mcp::handle_message`, writes responses to //! stdout. Logs land on stderr so they don't collide with the JSON-RPC stream. use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; #[tokio::main(flavor = "current_thread")] async fn main() -> anyhow::Result<()> { - let db_path = magnotia_storage::database_path(); + let db_path = lumotia_storage::database_path(); eprintln!( "[magnotia-mcp] opening Magnotia database at {} (read-only)", db_path.display() @@ -15,7 +15,7 @@ async fn main() -> anyhow::Result<()> { // to the user's database, regardless of which tools the dispatcher // exposes. Migrations are deliberately skipped — this binary never owns // the schema; the main app is the single migration writer. - let pool = magnotia_storage::init_readonly(&db_path).await?; + let pool = lumotia_storage::init_readonly(&db_path).await?; eprintln!("[magnotia-mcp] ready, waiting for JSON-RPC on stdin"); let mut lines = BufReader::new(tokio::io::stdin()).lines(); @@ -28,7 +28,7 @@ async fn main() -> anyhow::Result<()> { } let response = match serde_json::from_str::(trimmed) { - Ok(raw) => match magnotia_mcp::handle_message(&pool, raw).await { + Ok(raw) => match lumotia_mcp::handle_message(&pool, raw).await { Some(response) => response, None => continue, // notification — no reply }, @@ -39,7 +39,7 @@ async fn main() -> anyhow::Result<()> { // clients saw silence instead of a structured error // (2026-04-22 review MAJOR). eprintln!("[magnotia-mcp] parse error: {err}"); - magnotia_mcp::parse_error_response(&err.to_string()) + lumotia_mcp::parse_error_response(&err.to_string()) } }; diff --git a/crates/storage/Cargo.toml b/crates/storage/Cargo.toml index 990b730..4c03ca9 100644 --- a/crates/storage/Cargo.toml +++ b/crates/storage/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "magnotia-storage" +name = "lumotia-storage" version = "0.1.0" edition = "2021" description = "SQLite persistence, BM25 search, and file storage for Magnotia" [dependencies] -magnotia-core = { path = "../core" } +lumotia-core = { path = "../core" } # SQLite with compile-time checked queries # default-features = false strips sqlx's `any`, `macros`, `migrate`, `json` — @@ -24,7 +24,7 @@ serde = { version = "1", features = ["derive"] } # Logging log = "0.4" -# Structured error derivation for magnotia_storage::Error. +# Structured error derivation for lumotia_storage::Error. thiserror = "1" # UUIDs for profile + profile_terms ids (v7 random). diff --git a/crates/storage/src/error.rs b/crates/storage/src/error.rs index f566ee6..e4117ff 100644 --- a/crates/storage/src/error.rs +++ b/crates/storage/src/error.rs @@ -1,7 +1,7 @@ //! Storage-local typed error. //! //! Backend code that wants to branch on storage failure modes pattern-matches -//! on [`Error`] directly. The crate boundary into [`magnotia_core::error::MagnotiaError`] +//! on [`Error`] directly. The crate boundary into [`lumotia_core::error::MagnotiaError`] //! flattens this into a [`MagnotiaError::Storage`] variant carrying a serde-friendly //! `kind`, an `operation` label, and the Display output as `detail` — so the //! frontend (which still receives stringified errors from Tauri commands today) @@ -14,7 +14,7 @@ use std::borrow::Cow; use std::path::PathBuf; -use magnotia_core::error::{MagnotiaError, StorageKind}; +use lumotia_core::error::{MagnotiaError, StorageKind}; /// Kinds of database-open operation that can fail before the pool is ready. #[derive(Debug, Clone, Copy)] diff --git a/crates/storage/src/file_storage.rs b/crates/storage/src/file_storage.rs index 3238ac9..53c1265 100644 --- a/crates/storage/src/file_storage.rs +++ b/crates/storage/src/file_storage.rs @@ -1,28 +1,28 @@ use std::path::PathBuf; pub fn app_data_dir() -> PathBuf { - magnotia_core::paths::app_paths().app_data_dir() + lumotia_core::paths::app_paths().app_data_dir() } /// Path to the SQLite database file. pub fn database_path() -> PathBuf { - magnotia_core::paths::app_paths().database_path() + lumotia_core::paths::app_paths().database_path() } /// Directory for saved audio recordings. pub fn recordings_dir() -> PathBuf { - magnotia_core::paths::app_paths().recordings_dir() + lumotia_core::paths::app_paths().recordings_dir() } /// Directory for crash dumps written by the Rust panic hook. /// Each crash is a single text file: `-.crash`. /// Used by the diagnostic-report bundler in Settings → About. pub fn crashes_dir() -> PathBuf { - magnotia_core::paths::app_paths().crashes_dir() + lumotia_core::paths::app_paths().crashes_dir() } /// Directory for the rolling Rust log file (magnotia.log + rotated magnotia.log.1, etc). /// Subscribers configured in src-tauri/src/lib.rs at startup. pub fn logs_dir() -> PathBuf { - magnotia_core::paths::app_paths().logs_dir() + lumotia_core::paths::app_paths().logs_dir() } diff --git a/crates/transcription/Cargo.toml b/crates/transcription/Cargo.toml index 46ad464..fd73017 100644 --- a/crates/transcription/Cargo.toml +++ b/crates/transcription/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "magnotia-transcription" +name = "lumotia-transcription" version = "0.1.0" edition = "2021" description = "Speech-to-text engine wrappers, model management, and inference concurrency for Magnotia" @@ -15,18 +15,18 @@ build = "build.rs" # `whisper-vulkan` is a separate feature so a non-Vulkan target (Android # without GPU drivers, a CPU-only Windows build) can pull in whisper-rs # but skip the Vulkan backend. Build CPU-only with: -# cargo build -p magnotia-transcription --no-default-features --features whisper +# cargo build -p lumotia-transcription --no-default-features --features whisper default = ["whisper", "whisper-vulkan"] whisper = ["dep:whisper-rs"] whisper-vulkan = ["whisper-rs?/vulkan"] [dependencies] -magnotia-core = { path = "../core" } +lumotia-core = { path = "../core" } # TranscriptionProvider async trait + EngineProfile + ProviderId. The # trait lives in cloud-providers so an OEM licensee can implement it # without depending on transcription internals. -magnotia-cloud-providers = { path = "../cloud-providers" } +lumotia-cloud-providers = { path = "../cloud-providers" } # Async-trait for the LocalProviderAdapter impl. async-trait = "0.1" @@ -57,12 +57,12 @@ thiserror = "2" tracing = "0.1" [dev-dependencies] -# TcpListener fixture for the download resume tests (mirrors magnotia-llm). +# TcpListener fixture for the download resume tests (mirrors lumotia-llm). # `macros` and `rt-multi-thread` enable #[tokio::test] and the multi-threaded # scheduler used by the orchestrator tests. tokio = { version = "1", features = ["rt", "rt-multi-thread", "sync", "net", "io-util", "macros"] } tempfile = "3" # Test-only — used by tests/thread_sweep.rs to label physical vs logical # core counts in the scaling table. Production code uses the -# `magnotia_core::constants::inference_thread_count` helper instead. +# `lumotia_core::constants::inference_thread_count` helper instead. num_cpus = "1" diff --git a/crates/transcription/build.rs b/crates/transcription/build.rs index e821f25..67ce607 100644 --- a/crates/transcription/build.rs +++ b/crates/transcription/build.rs @@ -11,7 +11,7 @@ //! workspace ever pulls `tokenizers` into the dependency graph on a //! Windows target. If we ever legitimately need it we can reintroduce //! it via a sidecar (isolated process, separate CRT) rather than -//! linking it into `magnotia_lib`. +//! linking it into `lumotia_lib`. //! //! The check is advisory on non-Windows targets — it still prints a //! cargo:warning if `tokenizers` appears, so the Windows failure isn't diff --git a/crates/transcription/src/concurrency.rs b/crates/transcription/src/concurrency.rs index 0c4c1d2..c693ba2 100644 --- a/crates/transcription/src/concurrency.rs +++ b/crates/transcription/src/concurrency.rs @@ -1,7 +1,7 @@ use std::sync::Arc; -use magnotia_core::error::{MagnotiaError, Result}; -use magnotia_core::types::{AudioSamples, TranscriptionOptions}; +use lumotia_core::error::{MagnotiaError, Result}; +use lumotia_core::types::{AudioSamples, TranscriptionOptions}; use crate::local_engine::{LocalEngine, TimedTranscript}; diff --git a/crates/transcription/src/lib.rs b/crates/transcription/src/lib.rs index d4f809a..cbbf416 100644 --- a/crates/transcription/src/lib.rs +++ b/crates/transcription/src/lib.rs @@ -23,11 +23,11 @@ pub use transcribe_rs::SpeechModel; pub use transcriber::{Transcriber, TranscriberCapabilities}; // Re-export the trait surface so downstream crates depend only on -// `magnotia_transcription` to access both local engines and the -// provider trait without a separate `magnotia_cloud_providers` +// `lumotia_transcription` to access both local engines and the +// provider trait without a separate `lumotia_cloud_providers` // dependency. This is the seam an OEM licensee uses when they swap // providers. -pub use magnotia_cloud_providers::{ +pub use lumotia_cloud_providers::{ CostClass, EngineProfile, NetworkRequirement, ProviderCapabilities, ProviderId, ProviderKind, ProviderTranscript, TranscriptionProvider, }; diff --git a/crates/transcription/src/local_engine.rs b/crates/transcription/src/local_engine.rs index 561ef84..b67e04e 100644 --- a/crates/transcription/src/local_engine.rs +++ b/crates/transcription/src/local_engine.rs @@ -4,8 +4,8 @@ use std::time::Instant; use transcribe_rs::{SpeechModel, TranscribeOptions, TranscriptionResult}; -use magnotia_core::error::{MagnotiaError, Result}; -use magnotia_core::types::{ +use lumotia_core::error::{MagnotiaError, Result}; +use lumotia_core::types::{ AudioSamples, EngineName, ModelId, Segment, Transcript, TranscriptionOptions, }; @@ -28,7 +28,7 @@ pub struct SpeechModelAdapter(pub Box); impl Transcriber for SpeechModelAdapter { fn capabilities(&self) -> TranscriberCapabilities { TranscriberCapabilities { - sample_rate: magnotia_core::constants::WHISPER_SAMPLE_RATE, + sample_rate: lumotia_core::constants::WHISPER_SAMPLE_RATE, channels: 1, supports_initial_prompt: false, } diff --git a/crates/transcription/src/model_manager.rs b/crates/transcription/src/model_manager.rs index aaafcb2..344edc2 100644 --- a/crates/transcription/src/model_manager.rs +++ b/crates/transcription/src/model_manager.rs @@ -2,9 +2,9 @@ use std::collections::HashSet; use std::path::{Path, PathBuf}; use std::sync::{LazyLock, Mutex}; -use magnotia_core::error::{MagnotiaError, Result}; -use magnotia_core::model_registry::{find_model, ModelFile}; -use magnotia_core::types::{DownloadProgress, ModelId}; +use lumotia_core::error::{MagnotiaError, Result}; +use lumotia_core::model_registry::{find_model, ModelFile}; +use lumotia_core::types::{DownloadProgress, ModelId}; static ACTIVE_DOWNLOADS: LazyLock>> = LazyLock::new(|| Mutex::new(HashSet::new())); @@ -40,12 +40,12 @@ impl Drop for DownloadReservation { /// Windows: %LOCALAPPDATA%/magnotia/models /// Unix: ~/.magnotia/models pub fn models_dir() -> PathBuf { - magnotia_core::paths::app_paths().models_dir() + lumotia_core::paths::app_paths().models_dir() } /// Get the directory path where a specific model's files are stored. pub fn model_dir(id: &ModelId) -> PathBuf { - magnotia_core::paths::app_paths().speech_model_dir(id) + lumotia_core::paths::app_paths().speech_model_dir(id) } /// Check whether all files for a model have been downloaded. @@ -61,7 +61,7 @@ pub fn is_downloaded(id: &ModelId) -> bool { /// List all downloaded model IDs. pub fn list_downloaded() -> Vec { - magnotia_core::model_registry::all_models() + lumotia_core::model_registry::all_models() .iter() .filter(|m| is_downloaded(&m.id)) .map(|m| m.id.clone()) @@ -117,7 +117,7 @@ fn verified_manifest_path(dir: &Path) -> PathBuf { } fn verified_manifest_matches( - entry: &magnotia_core::model_registry::ModelEntry, + entry: &lumotia_core::model_registry::ModelEntry, dir: &Path, ) -> bool { let manifest = match std::fs::read_to_string(verified_manifest_path(dir)) { @@ -140,7 +140,7 @@ fn verified_manifest_matches( } fn write_verified_manifest( - entry: &magnotia_core::model_registry::ModelEntry, + entry: &lumotia_core::model_registry::ModelEntry, dir: &Path, ) -> std::io::Result<()> { let mut lines = Vec::with_capacity(entry.files.len() + 1); @@ -357,7 +357,7 @@ mod tests { let list = list_downloaded(); // In test environment, no models are downloaded // This just verifies the function doesn't panic - assert!(list.len() <= magnotia_core::model_registry::all_models().len()); + assert!(list.len() <= lumotia_core::model_registry::all_models().len()); } #[test] @@ -481,7 +481,7 @@ mod tests { let file = ModelFile { filename: leak(dest.file_name().unwrap().to_string_lossy().into_owned()), url: leak(format!("http://{addr}/fixture.bin")), - size: magnotia_core::types::Megabytes(0), + size: lumotia_core::types::Megabytes(0), sha256: leak(expected_sha.clone()), }; let id = ModelId::new("test-fixture"); @@ -516,7 +516,7 @@ mod tests { let file = ModelFile { filename: leak(dest.file_name().unwrap().to_string_lossy().into_owned()), url: leak(format!("http://{addr}/fixture.bin")), - size: magnotia_core::types::Megabytes(0), + size: lumotia_core::types::Megabytes(0), sha256: leak(expected_sha), }; let id = ModelId::new("test-fixture"); @@ -571,7 +571,7 @@ mod tests { let file = ModelFile { filename: leak(dest.file_name().unwrap().to_string_lossy().into_owned()), url: leak(format!("http://{addr}/fixture.bin")), - size: magnotia_core::types::Megabytes(0), + size: lumotia_core::types::Megabytes(0), sha256: leak("0".repeat(64)), }; let id = ModelId::new("test-fixture"); @@ -599,7 +599,7 @@ mod tests { let file = ModelFile { filename: leak(dest.file_name().unwrap().to_string_lossy().into_owned()), url: leak(format!("http://{addr}/fixture.bin")), - size: magnotia_core::types::Megabytes(0), + size: lumotia_core::types::Megabytes(0), sha256: leak("deadbeef".repeat(8)), }; let id = ModelId::new("test-fixture"); diff --git a/crates/transcription/src/orchestrator.rs b/crates/transcription/src/orchestrator.rs index f412292..26629ce 100644 --- a/crates/transcription/src/orchestrator.rs +++ b/crates/transcription/src/orchestrator.rs @@ -18,12 +18,12 @@ use std::sync::Arc; use async_trait::async_trait; -use magnotia_cloud_providers::{ +use lumotia_cloud_providers::{ CostClass, EngineProfile, ProviderCapabilities, ProviderId, ProviderKind, ProviderTranscript, TranscriptionProvider, }; -use magnotia_core::error::{MagnotiaError, Result}; -use magnotia_core::types::{AudioSamples, TranscriptionOptions}; +use lumotia_core::error::{MagnotiaError, Result}; +use lumotia_core::types::{AudioSamples, TranscriptionOptions}; use crate::local_engine::LocalEngine; use crate::registry::EngineRegistry; @@ -68,7 +68,7 @@ impl TranscriptionProvider for LocalProviderAdapter { ProviderCapabilities { sample_rate: local .map(|c| c.sample_rate) - .unwrap_or(magnotia_core::constants::WHISPER_SAMPLE_RATE), + .unwrap_or(lumotia_core::constants::WHISPER_SAMPLE_RATE), channels: local.map(|c| c.channels).unwrap_or(1), initial_prompt_supported: local.map(|c| c.supports_initial_prompt).unwrap_or(false), language_hint_supported: true, @@ -153,7 +153,7 @@ mod tests { use std::sync::atomic::{AtomicUsize, Ordering}; - use magnotia_core::types::{Segment, Transcript}; + use lumotia_core::types::{Segment, Transcript}; /// Mock provider that returns a canned transcript and counts /// invocations. Used to validate the orchestrator's dispatch logic diff --git a/crates/transcription/src/registry.rs b/crates/transcription/src/registry.rs index 1e30af5..6d04624 100644 --- a/crates/transcription/src/registry.rs +++ b/crates/transcription/src/registry.rs @@ -15,7 +15,7 @@ use std::collections::HashMap; use std::sync::Arc; -use magnotia_cloud_providers::{ProviderId, TranscriptionProvider}; +use lumotia_cloud_providers::{ProviderId, TranscriptionProvider}; /// Catalogue of providers known to the orchestrator. pub struct EngineRegistry { @@ -82,11 +82,11 @@ mod tests { use super::*; use async_trait::async_trait; - use magnotia_cloud_providers::{ + use lumotia_cloud_providers::{ CostClass, EngineProfile, ProviderCapabilities, ProviderKind, ProviderTranscript, }; - use magnotia_core::error::Result; - use magnotia_core::types::{AudioSamples, Transcript, TranscriptionOptions}; + use lumotia_core::error::Result; + use lumotia_core::types::{AudioSamples, Transcript, TranscriptionOptions}; struct DummyProvider { id: ProviderId, diff --git a/crates/transcription/src/transcriber.rs b/crates/transcription/src/transcriber.rs index 4af1320..55befbf 100644 --- a/crates/transcription/src/transcriber.rs +++ b/crates/transcription/src/transcriber.rs @@ -9,8 +9,8 @@ //! `whisper` feature — `WhisperRsBackend` (direct whisper-rs, the only //! path that pipes `initial_prompt`). -use magnotia_core::error::Result; -use magnotia_core::types::{Segment, TranscriptionOptions}; +use lumotia_core::error::Result; +use lumotia_core::types::{Segment, TranscriptionOptions}; /// Static capabilities a `Transcriber` advertises to callers. /// diff --git a/crates/transcription/src/whisper_rs_backend.rs b/crates/transcription/src/whisper_rs_backend.rs index bed4b04..16557f9 100644 --- a/crates/transcription/src/whisper_rs_backend.rs +++ b/crates/transcription/src/whisper_rs_backend.rs @@ -10,10 +10,10 @@ use std::path::Path; use whisper_rs::{FullParams, SamplingStrategy, WhisperContext, WhisperContextParameters}; -use magnotia_core::error::{MagnotiaError, Result}; -use magnotia_core::hardware::vulkan_loader_available; -use magnotia_core::tuning::{inference_thread_count, Workload}; -use magnotia_core::types::{Segment, TranscriptionOptions}; +use lumotia_core::error::{MagnotiaError, Result}; +use lumotia_core::hardware::vulkan_loader_available; +use lumotia_core::tuning::{inference_thread_count, Workload}; +use lumotia_core::types::{Segment, TranscriptionOptions}; use crate::transcriber::{Transcriber, TranscriberCapabilities}; @@ -42,7 +42,7 @@ impl WhisperRsBackend { impl Transcriber for WhisperRsBackend { fn capabilities(&self) -> TranscriberCapabilities { TranscriberCapabilities { - sample_rate: magnotia_core::constants::WHISPER_SAMPLE_RATE, + sample_rate: lumotia_core::constants::WHISPER_SAMPLE_RATE, channels: 1, supports_initial_prompt: true, } diff --git a/crates/transcription/tests/thread_sweep.rs b/crates/transcription/tests/thread_sweep.rs index 81578a1..251977b 100644 --- a/crates/transcription/tests/thread_sweep.rs +++ b/crates/transcription/tests/thread_sweep.rs @@ -9,8 +9,8 @@ use std::env; use std::time::Instant; -use magnotia_core::hardware::vulkan_loader_available; -use magnotia_core::tuning::{inference_thread_count, Workload}; +use lumotia_core::hardware::vulkan_loader_available; +use lumotia_core::tuning::{inference_thread_count, Workload}; use whisper_rs::{FullParams, SamplingStrategy, WhisperContext, WhisperContextParameters}; #[test] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index ce58e81..504cde2 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,22 +1,22 @@ [package] -name = "magnotia" +name = "lumotia" version = "0.1.0" -description = "Magnotia — Think out loud" +description = "Lumotia — Think out loud" authors = ["CORBEL Ltd"] edition = "2021" [lib] -name = "magnotia_lib" +name = "lumotia_lib" crate-type = ["staticlib", "cdylib", "rlib"] [features] # Default build includes the Whisper backend. Disabling this feature -# also drops it from magnotia-transcription (see Cargo.toml in that crate) +# also drops it from lumotia-transcription (see Cargo.toml in that crate) # so a --no-default-features workspace build does not pull whisper-rs-sys. # load_model_from_disk returns a runtime error for Engine::Whisper when # this feature is off; Parakeet continues to work. default = ["whisper"] -whisper = ["magnotia-transcription/whisper"] +whisper = ["lumotia-transcription/whisper"] [build-dependencies] tauri-build = { version = "2", features = [] } @@ -28,14 +28,14 @@ serde_json = "1" [dependencies] # Workspace crates -magnotia-core = { path = "../crates/core" } -magnotia-audio = { path = "../crates/audio" } -magnotia-transcription = { path = "../crates/transcription", default-features = false } -magnotia-ai-formatting = { path = "../crates/ai-formatting" } -magnotia-storage = { path = "../crates/storage" } -magnotia-cloud-providers = { path = "../crates/cloud-providers" } -magnotia-hotkey = { path = "../crates/hotkey" } -magnotia-llm = { path = "../crates/llm" } +lumotia-core = { path = "../crates/core" } +lumotia-audio = { path = "../crates/audio" } +lumotia-transcription = { path = "../crates/transcription", default-features = false } +lumotia-ai-formatting = { path = "../crates/ai-formatting" } +lumotia-storage = { path = "../crates/storage" } +lumotia-cloud-providers = { path = "../crates/cloud-providers" } +lumotia-hotkey = { path = "../crates/hotkey" } +lumotia-llm = { path = "../crates/llm" } # Tauri. The `tray-icon` feature, the global-shortcut/window-state/ # autostart plugins are desktop-only — gated below under diff --git a/src-tauri/src/commands/audio.rs b/src-tauri/src/commands/audio.rs index 3ff7094..80b8ba2 100644 --- a/src-tauri/src/commands/audio.rs +++ b/src-tauri/src/commands/audio.rs @@ -4,7 +4,7 @@ use std::sync::atomic::{AtomicU64, Ordering}; use tauri::Manager; use crate::commands::security::ensure_main_window; -use magnotia_audio::{DeviceInfo, MicrophoneCapture}; +use lumotia_audio::{DeviceInfo, MicrophoneCapture}; /// Enumerate every input device available to cpal, with metadata for the /// Settings device-picker UI. Includes a flag for likely PulseAudio / diff --git a/src-tauri/src/commands/diagnostics.rs b/src-tauri/src/commands/diagnostics.rs index e2b0742..8dad3bd 100644 --- a/src-tauri/src/commands/diagnostics.rs +++ b/src-tauri/src/commands/diagnostics.rs @@ -15,7 +15,7 @@ use std::time::{SystemTime, UNIX_EPOCH}; use serde::{Deserialize, Serialize}; -use magnotia_storage::{ +use lumotia_storage::{ app_data_dir, crashes_dir, list_recent_errors, log_error, logs_dir, ErrorLogRow, }; @@ -335,7 +335,7 @@ async fn generate_diagnostic_report_inner( if opts.include_settings { out.push_str("## Settings (sanitised)\n\n"); - match magnotia_storage::get_setting(&state.db, "magnotia_preferences").await { + match lumotia_storage::get_setting(&state.db, "magnotia_preferences").await { Ok(Some(json)) => { out.push_str("```json\n"); out.push_str(&sanitise_preferences_json(&json)); @@ -520,7 +520,7 @@ pub async fn save_diagnostic_report( let _ = app; // reserved for future dialog integration let report = generate_diagnostic_report_inner(&state, options).await?; - let dir = magnotia_storage::app_data_dir().join("diagnostic-reports"); + let dir = lumotia_storage::app_data_dir().join("diagnostic-reports"); fs::create_dir_all(&dir).map_err(|e| format!("create dir: {e}"))?; let ts = SystemTime::now() diff --git a/src-tauri/src/commands/feedback.rs b/src-tauri/src/commands/feedback.rs index e3de86f..7cb00b2 100644 --- a/src-tauri/src/commands/feedback.rs +++ b/src-tauri/src/commands/feedback.rs @@ -5,7 +5,7 @@ use serde::Deserialize; -use magnotia_storage::{ +use lumotia_storage::{ record_feedback as db_record_feedback, FeedbackTargetType, RecordFeedbackParams, }; diff --git a/src-tauri/src/commands/hardware.rs b/src-tauri/src/commands/hardware.rs index a09d931..6f29fde 100644 --- a/src-tauri/src/commands/hardware.rs +++ b/src-tauri/src/commands/hardware.rs @@ -1,7 +1,7 @@ use serde::Serialize; -use magnotia_core::hardware::{self, Os}; -use magnotia_core::recommendation; +use lumotia_core::hardware::{self, Os}; +use lumotia_core::recommendation; #[derive(Serialize)] pub struct SystemInfo { @@ -53,7 +53,7 @@ pub fn rank_models() -> Result, String> { Ok(ranked .into_iter() .map(|scored| { - let downloaded = magnotia_transcription::is_downloaded(&scored.entry.id); + let downloaded = lumotia_transcription::is_downloaded(&scored.entry.id); ModelRecommendation { id: scored.entry.id.as_str().to_string(), display_name: scored.entry.display_name, diff --git a/src-tauri/src/commands/hotkey.rs b/src-tauri/src/commands/hotkey.rs index e3f6340..547d01e 100644 --- a/src-tauri/src/commands/hotkey.rs +++ b/src-tauri/src/commands/hotkey.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use tauri::Emitter; use tokio::sync::{mpsc, Mutex}; -use magnotia_hotkey::{EvdevHotkeyListener, HotkeyCombo, HotkeyEvent}; +use lumotia_hotkey::{EvdevHotkeyListener, HotkeyCombo, HotkeyEvent}; /// Managed state for the evdev hotkey listener. pub struct HotkeyState { @@ -30,7 +30,7 @@ pub fn is_wayland_session() -> bool { /// Check whether evdev hotkey capture is available (user in `input` group, etc.). #[tauri::command] pub fn check_hotkey_access() -> Result<(), String> { - magnotia_hotkey::check_evdev_access() + lumotia_hotkey::check_evdev_access() } /// Start the evdev global hotkey listener. Emits "magnotia:hotkey-pressed" and diff --git a/src-tauri/src/commands/intentions.rs b/src-tauri/src/commands/intentions.rs index b793d9a..8d1282f 100644 --- a/src-tauri/src/commands/intentions.rs +++ b/src-tauri/src/commands/intentions.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; -use magnotia_storage::{ +use lumotia_storage::{ delete_implementation_rule as db_delete_rule, get_task_by_id, insert_implementation_rule as db_insert_rule, list_implementation_rules as db_list_rules, mark_implementation_rule_fired as db_mark_rule_fired, diff --git a/src-tauri/src/commands/live.rs b/src-tauri/src/commands/live.rs index 335a071..082220f 100644 --- a/src-tauri/src/commands/live.rs +++ b/src-tauri/src/commands/live.rs @@ -19,13 +19,13 @@ use crate::commands::models::{default_model_id_for_engine, ensure_model_loaded}; use crate::commands::power::PowerAssertion; use crate::commands::security::ensure_main_window; use crate::AppState; -use magnotia_ai_formatting::{post_process_segments, FormatMode, PostProcessOptions}; -use magnotia_audio::{ +use lumotia_ai_formatting::{post_process_segments, FormatMode, PostProcessOptions}; +use lumotia_audio::{ AudioChunk, CaptureRuntimeError, MicrophoneCapture, StreamingResampler, WavWriter, }; -use magnotia_core::constants::WHISPER_SAMPLE_RATE; -use magnotia_core::types::{AudioSamples, Segment, TranscriptionOptions}; -use magnotia_transcription::LocalEngine; +use lumotia_core::constants::WHISPER_SAMPLE_RATE; +use lumotia_core::types::{AudioSamples, Segment, TranscriptionOptions}; +use lumotia_transcription::LocalEngine; const CHUNK_SAMPLES: usize = 32_000; // 2s at 16kHz const OVERLAP_SAMPLES: usize = 4_000; // 0.25s at 16kHz @@ -450,7 +450,7 @@ struct InferenceTask { chunk_start_sample: u64, trim_before_secs: f64, duration_secs: f64, - rx: std::sync::mpsc::Receiver>, + rx: std::sync::mpsc::Receiver>, } #[derive(Debug, Clone)] @@ -503,15 +503,15 @@ pub async fn start_live_transcription_session( let resolved_profile_id = config .profile_id .clone() - .unwrap_or_else(|| magnotia_storage::DEFAULT_PROFILE_ID.to_string()); + .unwrap_or_else(|| lumotia_storage::DEFAULT_PROFILE_ID.to_string()); - let profile = magnotia_storage::database::get_profile(&state.db, &resolved_profile_id) + let profile = lumotia_storage::database::get_profile(&state.db, &resolved_profile_id) .await .map_err(|e| e.to_string())? .ok_or_else(|| format!("Profile {resolved_profile_id} not found"))?; let profile_terms: Vec = - magnotia_storage::database::list_profile_terms(&state.db, &resolved_profile_id) + lumotia_storage::database::list_profile_terms(&state.db, &resolved_profile_id) .await .map_err(|e| e.to_string())? .into_iter() @@ -1555,8 +1555,8 @@ mod tests { let stop_flag = Arc::new(AtomicBool::new(false)); let (tx1, rx1) = std::sync::mpsc::channel(); - tx1.send(Ok(magnotia_transcription::TimedTranscript { - transcript: magnotia_core::types::Transcript::new( + tx1.send(Ok(lumotia_transcription::TimedTranscript { + transcript: lumotia_core::types::Transcript::new( vec![segment(0.0, 0.8, "first chunk")], "en".into(), 0.8, @@ -1597,8 +1597,8 @@ mod tests { ); let (tx2, rx2) = std::sync::mpsc::channel(); - tx2.send(Ok(magnotia_transcription::TimedTranscript { - transcript: magnotia_core::types::Transcript::new( + tx2.send(Ok(lumotia_transcription::TimedTranscript { + transcript: lumotia_core::types::Transcript::new( vec![segment(0.0, 0.9, "second chunk")], "en".into(), 0.9, diff --git a/src-tauri/src/commands/llm.rs b/src-tauri/src/commands/llm.rs index 051f857..a3f03bb 100644 --- a/src-tauri/src/commands/llm.rs +++ b/src-tauri/src/commands/llm.rs @@ -3,10 +3,10 @@ use tauri::{Emitter, State}; use crate::commands::power::PowerAssertion; use crate::commands::security::ensure_main_window; use crate::AppState; -use magnotia_ai_formatting::{llm_cleanup_text, LlmPromptPreset}; -use magnotia_core::hardware; -use magnotia_llm::model_manager::{self, model_info}; -use magnotia_llm::{ContentTags, LlmModelId}; +use lumotia_ai_formatting::{llm_cleanup_text, LlmPromptPreset}; +use lumotia_core::hardware; +use lumotia_llm::model_manager::{self, model_info}; +use lumotia_llm::{ContentTags, LlmModelId}; #[derive(Debug, serde::Serialize)] #[serde(rename_all = "camelCase")] @@ -369,9 +369,9 @@ pub async fn cleanup_transcript_text_cmd( ) -> Result { ensure_main_window(&window)?; let resolved_profile_id = - profile_id.unwrap_or_else(|| magnotia_storage::DEFAULT_PROFILE_ID.to_string()); + profile_id.unwrap_or_else(|| lumotia_storage::DEFAULT_PROFILE_ID.to_string()); let profile_terms: Vec = - magnotia_storage::database::list_profile_terms(&state.db, &resolved_profile_id) + lumotia_storage::database::list_profile_terms(&state.db, &resolved_profile_id) .await .map_err(|e| e.to_string())? .into_iter() diff --git a/src-tauri/src/commands/meeting.rs b/src-tauri/src/commands/meeting.rs index 37571ac..be5f011 100644 --- a/src-tauri/src/commands/meeting.rs +++ b/src-tauri/src/commands/meeting.rs @@ -7,7 +7,7 @@ use std::sync::Mutex; -use magnotia_core::process_watch::{self, ProcessLister}; +use lumotia_core::process_watch::{self, ProcessLister}; /// Tauri-managed state for the meeting poller. Holds a long-lived /// `ProcessLister` so each poll refreshes the existing `sysinfo::System` diff --git a/src-tauri/src/commands/models.rs b/src-tauri/src/commands/models.rs index d3dc526..a583403 100644 --- a/src-tauri/src/commands/models.rs +++ b/src-tauri/src/commands/models.rs @@ -5,14 +5,14 @@ use tauri::Emitter; use crate::commands::security::ensure_main_window; use crate::AppState; -use magnotia_core::constants::WHISPER_SAMPLE_RATE; -use magnotia_core::hardware::{self, vulkan_loader_available, CpuFeatures}; -use magnotia_core::model_registry::{self, Engine, LanguageSupport, ModelEntry}; -use magnotia_core::types::{AudioSamples, ModelId, TranscriptionOptions}; +use lumotia_core::constants::WHISPER_SAMPLE_RATE; +use lumotia_core::hardware::{self, vulkan_loader_available, CpuFeatures}; +use lumotia_core::model_registry::{self, Engine, LanguageSupport, ModelEntry}; +use lumotia_core::types::{AudioSamples, ModelId, TranscriptionOptions}; #[cfg(feature = "whisper")] -use magnotia_transcription::load_whisper; -use magnotia_transcription::model_manager; -use magnotia_transcription::{load_parakeet, LocalEngine, Transcriber}; +use lumotia_transcription::load_whisper; +use lumotia_transcription::model_manager; +use lumotia_transcription::{load_parakeet, LocalEngine, Transcriber}; /// Map legacy size strings to ModelId. fn whisper_model_id(size: &str) -> ModelId { diff --git a/src-tauri/src/commands/profiles.rs b/src-tauri/src/commands/profiles.rs index e922a65..e032a2f 100644 --- a/src-tauri/src/commands/profiles.rs +++ b/src-tauri/src/commands/profiles.rs @@ -1,5 +1,5 @@ -// Tauri commands wrapping magnotia_storage profile + profile_term CRUD. -// Pattern mirrors tasks.rs — flat imports from `magnotia_storage` with a `db_` +// Tauri commands wrapping lumotia_storage profile + profile_term CRUD. +// Pattern mirrors tasks.rs — flat imports from `lumotia_storage` with a `db_` // alias prefix to avoid name collisions with the command functions, plain // snake_case parameters (Tauri 2.x auto-converts camelCase JS keys), // `.map_err(|e| e.to_string())` for error conversion, and camelCase DTOs @@ -11,8 +11,8 @@ use serde::Serialize; -use magnotia_ai_formatting::extract_corrections; -use magnotia_storage::{ +use lumotia_ai_formatting::extract_corrections; +use lumotia_storage::{ add_profile_term as db_add_profile_term, create_profile as db_create_profile, delete_profile as db_delete_profile, delete_profile_term as db_delete_profile_term, list_profile_terms as db_list_profile_terms, list_profiles as db_list_profiles, diff --git a/src-tauri/src/commands/rituals.rs b/src-tauri/src/commands/rituals.rs index aa1e139..e9dab49 100644 --- a/src-tauri/src/commands/rituals.rs +++ b/src-tauri/src/commands/rituals.rs @@ -7,10 +7,10 @@ //! filtering rather than adding a second query path. //! //! Stored under the existing SQLite settings table via -//! `magnotia_storage::{get_setting, set_setting}` — same bag as +//! `lumotia_storage::{get_setting, set_setting}` — same bag as //! `magnotia_preferences`. -use magnotia_storage::{get_setting, set_setting}; +use lumotia_storage::{get_setting, set_setting}; use crate::AppState; diff --git a/src-tauri/src/commands/tasks.rs b/src-tauri/src/commands/tasks.rs index c5e9cf3..bcb90d6 100644 --- a/src-tauri/src/commands/tasks.rs +++ b/src-tauri/src/commands/tasks.rs @@ -1,4 +1,4 @@ -// Tauri commands wrapping magnotia_storage task CRUD. +// Tauri commands wrapping lumotia_storage task CRUD. // Pattern mirrors transcripts.rs — TaskDto is the camelCase frontend shape, // storage functions are aliased with db_ prefix to avoid name collisions. @@ -6,8 +6,8 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; -use magnotia_llm::prompts::FeedbackExample as LlmFeedbackExample; -use magnotia_storage::{ +use lumotia_llm::prompts::FeedbackExample as LlmFeedbackExample; +use lumotia_storage::{ complete_subtask_and_check_parent as db_complete_subtask, complete_task as db_complete_task, delete_task as db_delete_task, get_task_by_id as db_get_task, insert_subtask as db_insert_subtask, insert_task as db_insert_task, @@ -228,7 +228,7 @@ fn to_llm_examples(rows: Vec) -> Vec { Ok(v) => v, Err(e) => { tracing::warn!( - target: "magnotia_lib::feedback", + target: "lumotia_lib::feedback", row_id = r.id, error = %e, "skipping feedback row with malformed context_json" diff --git a/src-tauri/src/commands/transcription.rs b/src-tauri/src/commands/transcription.rs index 6060f29..6fe5d4f 100644 --- a/src-tauri/src/commands/transcription.rs +++ b/src-tauri/src/commands/transcription.rs @@ -9,9 +9,9 @@ use crate::commands::build_initial_prompt; use crate::commands::models::{default_model_id_for_engine, ensure_model_loaded}; use crate::commands::security::ensure_main_window; use crate::AppState; -use magnotia_ai_formatting::{post_process_segments, FormatMode, PostProcessOptions}; -use magnotia_core::constants::WHISPER_SAMPLE_RATE; -use magnotia_core::types::{AudioSamples, Segment, Transcript, TranscriptionOptions}; +use lumotia_ai_formatting::{post_process_segments, FormatMode, PostProcessOptions}; +use lumotia_core::constants::WHISPER_SAMPLE_RATE; +use lumotia_core::types::{AudioSamples, Segment, Transcript, TranscriptionOptions}; const PARAKEET_CHUNK_THRESHOLD_SECS: usize = 18; const PARAKEET_CHUNK_SECS: usize = 15; @@ -29,7 +29,7 @@ struct ChunkingStrategy { fn pick_engine( state: &AppState, engine: &str, -) -> Result, String> { +) -> Result, String> { match engine { "whisper" => Ok(state.whisper_engine.clone()), "parakeet" => Ok(state.parakeet_engine.clone()), @@ -70,11 +70,11 @@ fn trim_overlap_segments(segments: &mut Vec, trim_before_secs: f64) { } fn transcribe_samples_sync( - engine: Arc, + engine: Arc, engine_name: &str, samples: Vec, options: TranscriptionOptions, -) -> Result { +) -> Result { let Some(strategy) = pick_chunking_strategy(engine_name, samples.len()) else { let audio = AudioSamples::mono_16khz(samples); return engine @@ -128,7 +128,7 @@ fn transcribe_samples_sync( chunk_start = chunk_end.saturating_sub(strategy.overlap_samples); } - Ok(magnotia_transcription::TimedTranscript { + Ok(lumotia_transcription::TimedTranscript { transcript: Transcript::new( all_segments, options.language.clone().unwrap_or_else(|| "en".to_string()), @@ -165,15 +165,15 @@ pub async fn transcribe_file( ) -> Result { ensure_main_window(&window)?; let resolved_profile_id = - profile_id.unwrap_or_else(|| magnotia_storage::DEFAULT_PROFILE_ID.to_string()); + profile_id.unwrap_or_else(|| lumotia_storage::DEFAULT_PROFILE_ID.to_string()); - let profile = magnotia_storage::database::get_profile(&state.db, &resolved_profile_id) + let profile = lumotia_storage::database::get_profile(&state.db, &resolved_profile_id) .await .map_err(|e| e.to_string())? .ok_or_else(|| format!("Profile {resolved_profile_id} not found"))?; let profile_terms: Vec = - magnotia_storage::database::list_profile_terms(&state.db, &resolved_profile_id) + lumotia_storage::database::list_profile_terms(&state.db, &resolved_profile_id) .await .map_err(|e| e.to_string())? .into_iter() @@ -199,7 +199,7 @@ pub async fn transcribe_file( let engine_name_for_worker = engine_name.clone(); let path_for_probe = Path::new(&path); if let Some(duration_secs) = - magnotia_audio::probe_audio_duration_secs(path_for_probe).map_err(|e| e.to_string())? + lumotia_audio::probe_audio_duration_secs(path_for_probe).map_err(|e| e.to_string())? { if duration_secs > MAX_FILE_TRANSCRIPTION_SECS { return Err(format!( @@ -210,12 +210,12 @@ pub async fn transcribe_file( } let timed = tokio::task::spawn_blocking(move || { - let audio = magnotia_audio::decode_audio_file_limited( + let audio = lumotia_audio::decode_audio_file_limited( Path::new(&path), Some(MAX_FILE_TRANSCRIPTION_SECS), ) .map_err(|e| e.to_string())?; - let resampled = magnotia_audio::resample_to_16khz(&audio).map_err(|e| e.to_string())?; + let resampled = lumotia_audio::resample_to_16khz(&audio).map_err(|e| e.to_string())?; transcribe_samples_sync( engine, &engine_name_for_worker, diff --git a/src-tauri/src/commands/transcripts.rs b/src-tauri/src/commands/transcripts.rs index 16d00fa..271996f 100644 --- a/src-tauri/src/commands/transcripts.rs +++ b/src-tauri/src/commands/transcripts.rs @@ -1,4 +1,4 @@ -// Tauri commands wrapping the magnotia_storage transcript CRUD. +// Tauri commands wrapping the lumotia_storage transcript CRUD. // These are the bridge that lets the Svelte frontend treat SQLite as the // canonical store rather than localStorage. // @@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize}; -use magnotia_storage::{ +use lumotia_storage::{ delete_transcript as db_delete_transcript, get_transcript as db_get_transcript, insert_transcript as db_insert_transcript, list_transcripts_paged, search_transcripts as db_search_transcripts, update_transcript as db_update_transcript, @@ -110,7 +110,7 @@ pub async fn add_transcript( profile_id: transcript .profile_id .as_deref() - .unwrap_or(magnotia_storage::DEFAULT_PROFILE_ID), + .unwrap_or(lumotia_storage::DEFAULT_PROFILE_ID), title: transcript.title.as_deref(), audio_path: transcript.audio_path.as_deref(), duration: transcript.duration, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index dd29c9b..50c9ce7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -12,15 +12,15 @@ use sqlx::SqlitePool; use tauri::Manager; use tracing_subscriber::EnvFilter; -use magnotia_core::types::EngineName; -use magnotia_llm::LlmEngine; -use magnotia_storage::{database_path, get_setting, init as init_db, prune_error_log, set_setting}; +use lumotia_core::types::EngineName; +use lumotia_llm::LlmEngine; +use lumotia_storage::{database_path, get_setting, init as init_db, prune_error_log, set_setting}; /// How long to retain `error_log` rows. Pruned once on startup. /// 90 days is long enough to triage "this happened a few weeks ago" /// reports and short enough that the table stays kilobyte-scale. const ERROR_LOG_RETENTION_DAYS: i64 = 90; -use magnotia_transcription::LocalEngine; +use lumotia_transcription::LocalEngine; static TRACING_INIT: Once = Once::new(); @@ -162,7 +162,7 @@ fn init_tracing() { TRACING_INIT.call_once(|| { let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| { EnvFilter::new( - "warn,magnotia=info,magnotia_lib=info,magnotia_audio=info,magnotia_startup=info,magnotia_hotkey=info,magnotia_ai_formatting=info", + "warn,magnotia=info,lumotia_lib=info,lumotia_audio=info,magnotia_startup=info,lumotia_hotkey=info,lumotia_ai_formatting=info", ) }); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 5a60a52..95b2be4 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,5 +1,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { - magnotia_lib::run() + lumotia_lib::run() } From ce6dc1e7281cb12a50d26091e15f6c4e4daeba9e Mon Sep 17 00:00:00 2001 From: Jake Date: Wed, 13 May 2026 08:52:47 +0100 Subject: [PATCH 05/82] =?UTF-8?q?agent:=20lumotia-rebrand=20=E2=80=94=20fi?= =?UTF-8?q?x=20QC=20blockers=20for=20phase=202?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2 QC found three explicit blockers + a broader sweep needed: Explicit (QC-named): - crates/mcp/src/lib.rs:15 — SERVER_NAME public MCP wire identity - crates/transcription/build.rs:59 — panic message prefix - crates/llm/tests/content_tags_smoke.rs:7 — docstring -p flag Swept (string literals + dev env vars + doc comments + test fixtures): - crates/mcp/src/main.rs — eprintln log prefixes - src-tauri/src/commands/diagnostics.rs — diagnostic filename + MAGNOTIA_VERSION - src-tauri/src/commands/audio.rs — recording filename pattern lumotia--...wav - src-tauri/src/commands/fs.rs — test placeholder path - crates/transcription/src/model_manager.rs — .lumotia-verified marker - crates/storage/src/database.rs — lumotia-storage-ro- temp dirs + doc comments - crates/cloud-providers/src/keystore.rs — LUMOTIA_API_KEY_ env var - crates/audio/src/{wav,decode}.rs — lumotia_test_* / lumotia_decode_* test fixtures - crates/core/src/tuning.rs — LUMOTIA_INFERENCE_THREADS env var - All MAGNOTIA_LLM_TEST_MODEL / MAGNOTIA_WHISPER_TEST_* env vars - Doc comments referencing crate names Excluded (intentional Phase 4/5 scope): - magnotia_preferences, magnotia_history, magnotia_morning_triage_last_shown DB setting keys (Phase 5 paths.rs migration) - magnotia_startup tracing target (Phase 4) - crates/core/src/paths.rs (Phase 5 wholesale rewrite + migration shim) cargo build --workspace passes. cargo test --workspace: 330 pass, 0 fail. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/audio/src/decode.rs | 6 ++--- crates/audio/src/wav.rs | 8 +++---- crates/cloud-providers/src/keystore.rs | 6 ++--- crates/cloud-providers/src/provider.rs | 4 ++-- crates/core/examples/tuning_log_demo.rs | 6 ++--- crates/core/src/power.rs | 12 +++++----- crates/core/src/tuning.rs | 24 +++++++++---------- crates/llm/src/prompts.rs | 4 ++-- crates/llm/tests/content_tags_smoke.rs | 8 +++---- crates/llm/tests/smoke.rs | 6 ++--- crates/mcp/src/lib.rs | 2 +- crates/mcp/src/main.rs | 8 +++---- crates/storage/src/database.rs | 8 +++---- crates/transcription/build.rs | 4 ++-- crates/transcription/src/model_manager.rs | 6 ++--- crates/transcription/tests/jfk_bench.rs | 12 +++++----- crates/transcription/tests/thread_sweep.rs | 14 +++++------ .../transcription/tests/whisper_rs_smoke.rs | 6 ++--- src-tauri/src/commands/audio.rs | 12 +++++----- src-tauri/src/commands/diagnostics.rs | 8 +++---- src-tauri/src/commands/fs.rs | 2 +- 21 files changed, 83 insertions(+), 83 deletions(-) diff --git a/crates/audio/src/decode.rs b/crates/audio/src/decode.rs index 2b8f8b1..6a2583a 100644 --- a/crates/audio/src/decode.rs +++ b/crates/audio/src/decode.rs @@ -191,7 +191,7 @@ mod tests { } fn valid_wav_bytes(sample_count: usize) -> Vec { - let path = temp_path("magnotia_decode_tmp_for_bytes.wav"); + let path = temp_path("lumotia_decode_tmp_for_bytes.wav"); let samples: Vec = (0..sample_count).map(|i| (i as f32) / 1000.0).collect(); let audio = AudioSamples::mono_16khz(samples); write_wav(&path, &audio).unwrap(); @@ -238,7 +238,7 @@ mod tests { #[test] fn decodes_valid_wav_successfully() { - let path = temp_path("magnotia_decode_valid.wav"); + let path = temp_path("lumotia_decode_valid.wav"); let samples: Vec = (0..4_000).map(|i| (i as f32) / 1000.0).collect(); write_wav(&path, &AudioSamples::mono_16khz(samples)).unwrap(); @@ -251,7 +251,7 @@ mod tests { #[test] fn missing_file_surfaces_error() { - let path = temp_path("magnotia_decode_missing.wav"); + let path = temp_path("lumotia_decode_missing.wav"); let result = decode_audio_file(&path); assert!(result.is_err(), "missing file must error, got: {result:?}"); } diff --git a/crates/audio/src/wav.rs b/crates/audio/src/wav.rs index 8a53075..b702b37 100644 --- a/crates/audio/src/wav.rs +++ b/crates/audio/src/wav.rs @@ -182,7 +182,7 @@ mod tests { // mirrors what happens when the OS reaps the process without // giving Rust a chance to run destructors. let temp_dir = std::env::temp_dir(); - let path = temp_dir.join("magnotia_test_wav_writer_survives_crash.wav"); + let path = temp_dir.join("lumotia_test_wav_writer_survives_crash.wav"); let _ = std::fs::remove_file(&path); let mut writer = WavWriter::create(&path, 16_000, 1).unwrap(); @@ -219,7 +219,7 @@ mod tests { #[test] fn wav_writer_append_then_finalize_roundtrips() { let temp_dir = std::env::temp_dir(); - let path = temp_dir.join("magnotia_test_wav_writer_finalize.wav"); + let path = temp_dir.join("lumotia_test_wav_writer_finalize.wav"); let _ = std::fs::remove_file(&path); let mut writer = WavWriter::create(&path, 16_000, 1).unwrap(); @@ -241,7 +241,7 @@ mod tests { // truncated WAV returned Ok with a short samples vec. The // new code must propagate the error. let temp_dir = std::env::temp_dir(); - let path = temp_dir.join("magnotia_test_truncated_wav.wav"); + let path = temp_dir.join("lumotia_test_truncated_wav.wav"); let _ = std::fs::remove_file(&path); // Write 100 samples (200 bytes at 16-bit). @@ -267,7 +267,7 @@ mod tests { #[test] fn wav_roundtrip() { let temp_dir = std::env::temp_dir(); - let path = temp_dir.join("magnotia_test_roundtrip.wav"); + let path = temp_dir.join("lumotia_test_roundtrip.wav"); let original = AudioSamples::mono_16khz(vec![0.0, 0.5, -0.5, 0.25, -0.25]); write_wav(&path, &original).unwrap(); diff --git a/crates/cloud-providers/src/keystore.rs b/crates/cloud-providers/src/keystore.rs index e82e12b..0b4757d 100644 --- a/crates/cloud-providers/src/keystore.rs +++ b/crates/cloud-providers/src/keystore.rs @@ -7,7 +7,7 @@ use std::sync::{Mutex, OnceLock}; /// exit. This avoids the undefined behaviour of mutating process environment /// variables from arbitrary threads while keeping the public API safe. /// -/// `retrieve_api_key` still falls back to `MAGNOTIA_API_KEY_` environment +/// `retrieve_api_key` still falls back to `LUMOTIA_API_KEY_` environment /// variables so externally injected secrets continue to work. /// /// TODO: Replace with the `keyring` crate (or platform-native credential @@ -22,7 +22,7 @@ pub fn store_api_key(provider: &str, key: &str) { /// Retrieve an API key from Magnotia's process-local keystore. /// /// Returns a previously stored in-memory key when present, otherwise falls -/// back to the read-only `MAGNOTIA_API_KEY_` environment variable so +/// back to the read-only `LUMOTIA_API_KEY_` environment variable so /// operator-supplied secrets still work. pub fn retrieve_api_key(provider: &str) -> Option { let env_key = provider_env_key(provider); @@ -40,7 +40,7 @@ fn api_key_store() -> &'static Mutex> { } fn provider_env_key(provider: &str) -> String { - format!("MAGNOTIA_API_KEY_{}", provider.to_uppercase()) + format!("LUMOTIA_API_KEY_{}", provider.to_uppercase()) } #[cfg(test)] diff --git a/crates/cloud-providers/src/provider.rs b/crates/cloud-providers/src/provider.rs index 184b28b..7f132e2 100644 --- a/crates/cloud-providers/src/provider.rs +++ b/crates/cloud-providers/src/provider.rs @@ -1,10 +1,10 @@ //! `TranscriptionProvider` is the async-native trait every transcription //! backend implements, regardless of whether the work happens locally //! (Whisper, Parakeet, Moonshine via the `LocalProviderAdapter` in -//! `magnotia-transcription`) or remotely (OpenAI Whisper, Groq, +//! `lumotia-transcription`) or remotely (OpenAI Whisper, Groq, //! Deepgram, etc.). //! -//! Living in `magnotia-cloud-providers` is deliberate: the AGPL OEM +//! Living in `lumotia-cloud-providers` is deliberate: the AGPL OEM //! exception (≥£2k/yr) requires a clean trait surface that an OEM //! licensee can implement without depending on Wyrdnote's transcription //! internals. The trait crate stays small; provider implementations diff --git a/crates/core/examples/tuning_log_demo.rs b/crates/core/examples/tuning_log_demo.rs index 0f57463..4d8218e 100644 --- a/crates/core/examples/tuning_log_demo.rs +++ b/crates/core/examples/tuning_log_demo.rs @@ -3,7 +3,7 @@ //! gpu_offloaded) tuples. //! //! Run with: -//! cargo run -p magnotia-core --example tuning_log_demo +//! cargo run -p lumotia-core --example tuning_log_demo //! //! Output is to stderr (tracing's default). Each unique tuple emits //! exactly one INFO line; subsequent calls with the same tuple are @@ -28,8 +28,8 @@ fn main() { ("No override (real sysfs probe)", None), ] { match override_value { - Some(v) => std::env::set_var("MAGNOTIA_POWER_STATE_OVERRIDE", v), - None => std::env::remove_var("MAGNOTIA_POWER_STATE_OVERRIDE"), + Some(v) => std::env::set_var("LUMOTIA_POWER_STATE_OVERRIDE", v), + None => std::env::remove_var("LUMOTIA_POWER_STATE_OVERRIDE"), } // Cache invalidation so the live probe re-runs each section. // Override paths bypass the cache anyway; this is for the diff --git a/crates/core/src/power.rs b/crates/core/src/power.rs index 3945c8d..1d059dd 100644 --- a/crates/core/src/power.rs +++ b/crates/core/src/power.rs @@ -101,7 +101,7 @@ pub(crate) fn force_set_cache(state: PowerState) { /// /// Resolution order (highest to lowest priority): /// 1. In-process test override (set via `with_override` from unit tests). -/// 2. `MAGNOTIA_POWER_STATE_OVERRIDE` env var (`ac` | `battery` | `unknown`, +/// 2. `LUMOTIA_POWER_STATE_OVERRIDE` env var (`ac` | `battery` | `unknown`, /// case-insensitive). Used by `thread_sweep.rs` integration tests. /// 3. Linux: `parse_power_state_from_dir("/sys/class/power_supply")`. /// 4. macOS / Windows / other: `Unknown`. @@ -136,7 +136,7 @@ pub fn probe_power_state() -> PowerState { } fn env_override() -> Option { - let raw = std::env::var("MAGNOTIA_POWER_STATE_OVERRIDE").ok()?; + let raw = std::env::var("LUMOTIA_POWER_STATE_OVERRIDE").ok()?; match raw.trim().to_ascii_lowercase().as_str() { "ac" => Some(PowerState::OnAc), "battery" => Some(PowerState::OnBattery), @@ -293,21 +293,21 @@ mod tests { // env-var path tested in isolation under TEST_LOCK so it // doesn't race with the in-process override tests. with_override(None, || { - std::env::set_var("MAGNOTIA_POWER_STATE_OVERRIDE", "battery"); + std::env::set_var("LUMOTIA_POWER_STATE_OVERRIDE", "battery"); assert_eq!(probe_power_state(), PowerState::OnBattery); - std::env::remove_var("MAGNOTIA_POWER_STATE_OVERRIDE"); + std::env::remove_var("LUMOTIA_POWER_STATE_OVERRIDE"); }); } #[test] fn env_var_override_garbage_falls_through() { with_override(None, || { - std::env::set_var("MAGNOTIA_POWER_STATE_OVERRIDE", "nonsense"); + std::env::set_var("LUMOTIA_POWER_STATE_OVERRIDE", "nonsense"); // Garbage value falls through to the platform probe. // We can't assert the platform result so just assert it // doesn't panic. let _ = probe_power_state(); - std::env::remove_var("MAGNOTIA_POWER_STATE_OVERRIDE"); + std::env::remove_var("LUMOTIA_POWER_STATE_OVERRIDE"); }); } diff --git a/crates/core/src/tuning.rs b/crates/core/src/tuning.rs index e0d560c..b871891 100644 --- a/crates/core/src/tuning.rs +++ b/crates/core/src/tuning.rs @@ -16,7 +16,7 @@ pub const MIN_INFERENCE_THREADS: usize = 2; /// 8 is a conservative ceiling that leaves <5% on the table for /// big-iron desktops while keeping consumer 6c/12t laptops out of /// contention territory. Users can override at runtime via -/// MAGNOTIA_INFERENCE_THREADS. +/// LUMOTIA_INFERENCE_THREADS. pub const MAX_INFERENCE_THREADS: usize = 8; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -45,13 +45,13 @@ fn log_seen() -> &'static Mutex> { /// the battery and GPU-offload heuristics. /// /// Resolution order: -/// 1. `MAGNOTIA_INFERENCE_THREADS=N` — absolute bypass, returns N. +/// 1. `LUMOTIA_INFERENCE_THREADS=N` — absolute bypass, returns N. /// 2. base = num_cpus::get_physical() (fallback: available_parallelism). /// 3. on battery → base /= 2. /// 4. gpu_offloaded → base = min(base, gpu_floor(workload)). /// 5. clamp to [MIN_INFERENCE_THREADS, MAX_INFERENCE_THREADS]. pub fn inference_thread_count(workload: Workload, gpu_offloaded: bool) -> usize { - if let Ok(s) = std::env::var("MAGNOTIA_INFERENCE_THREADS") { + if let Ok(s) = std::env::var("LUMOTIA_INFERENCE_THREADS") { if let Ok(n) = s.parse::() { if n > 0 { return n; @@ -107,7 +107,7 @@ pub fn inference_thread_count(workload: Workload, gpu_offloaded: bool) -> usize mod tests { use super::*; - /// Serialises tests that read/write `MAGNOTIA_INFERENCE_THREADS` so + /// Serialises tests that read/write `LUMOTIA_INFERENCE_THREADS` so /// they don't race under cargo's parallel test runner. /// Mirrors the pattern used by `power::with_override`. fn with_thread_env_lock(body: impl FnOnce() -> R) -> R { @@ -126,7 +126,7 @@ mod tests { // We can't pin physical exactly without mocking num_cpus; just // assert the result is in range. with_thread_env_lock(|| { - std::env::remove_var("MAGNOTIA_INFERENCE_THREADS"); + std::env::remove_var("LUMOTIA_INFERENCE_THREADS"); let n = inference_thread_count(Workload::Llm, false); assert!( (MIN_INFERENCE_THREADS..=MAX_INFERENCE_THREADS).contains(&n), @@ -138,10 +138,10 @@ mod tests { #[test] fn env_var_bypasses_clamps() { with_thread_env_lock(|| { - std::env::set_var("MAGNOTIA_INFERENCE_THREADS", "10"); + std::env::set_var("LUMOTIA_INFERENCE_THREADS", "10"); let n = inference_thread_count(Workload::Llm, true); assert_eq!(n, 10); - std::env::remove_var("MAGNOTIA_INFERENCE_THREADS"); + std::env::remove_var("LUMOTIA_INFERENCE_THREADS"); }); } @@ -153,7 +153,7 @@ mod tests { #[test] fn battery_halves_thread_count() { with_thread_env_lock(|| { - std::env::remove_var("MAGNOTIA_INFERENCE_THREADS"); + std::env::remove_var("LUMOTIA_INFERENCE_THREADS"); // Measure on battery, then on AC — sequential, not nested, // to avoid re-entrant deadlock on power::TEST_LOCK. let on_battery = crate::power::with_override(Some(PowerState::OnBattery), || { @@ -176,7 +176,7 @@ mod tests { #[test] fn gpu_offload_clamps_llm_to_floor() { with_thread_env_lock(|| { - std::env::remove_var("MAGNOTIA_INFERENCE_THREADS"); + std::env::remove_var("LUMOTIA_INFERENCE_THREADS"); crate::power::with_override(Some(PowerState::OnAc), || { let n = inference_thread_count(Workload::Llm, true); assert!( @@ -191,7 +191,7 @@ mod tests { #[test] fn gpu_offload_clamps_whisper_to_floor() { with_thread_env_lock(|| { - std::env::remove_var("MAGNOTIA_INFERENCE_THREADS"); + std::env::remove_var("LUMOTIA_INFERENCE_THREADS"); crate::power::with_override(Some(PowerState::OnAc), || { let n = inference_thread_count(Workload::Whisper, true); // Whisper floor is 4 on machines with >=4 physical cores; @@ -213,7 +213,7 @@ mod tests { #[test] fn gpu_offload_off_does_not_clamp_below_battery_calc() { with_thread_env_lock(|| { - std::env::remove_var("MAGNOTIA_INFERENCE_THREADS"); + std::env::remove_var("LUMOTIA_INFERENCE_THREADS"); // Sequential measurements; with_override is non-reentrant. crate::power::with_override(Some(PowerState::OnAc), || { let no_gpu = inference_thread_count(Workload::Llm, false); @@ -230,7 +230,7 @@ mod tests { // wired. This is covered by the other tests too, but kept // explicitly to document the behaviour. with_thread_env_lock(|| { - std::env::remove_var("MAGNOTIA_INFERENCE_THREADS"); + std::env::remove_var("LUMOTIA_INFERENCE_THREADS"); crate::power::with_override(Some(PowerState::OnBattery), || { let _ = inference_thread_count(Workload::Llm, true); let _ = inference_thread_count(Workload::Whisper, false); diff --git a/crates/llm/src/prompts.rs b/crates/llm/src/prompts.rs index a93c947..bc90417 100644 --- a/crates/llm/src/prompts.rs +++ b/crates/llm/src/prompts.rs @@ -45,9 +45,9 @@ context that are not explicit commitments. Output an empty array if there are \ no action items."; /// Compact representation of a human-in-the-loop feedback example used -/// for few-shot prompt conditioning. Built by magnotia-storage and fed to the +/// for few-shot prompt conditioning. Built by lumotia-storage and fed to the /// prompt builder below; we keep this struct local to the LLM crate so -/// magnotia-llm does not depend on magnotia-storage. +/// lumotia-llm does not depend on lumotia-storage. #[derive(Debug, Clone)] pub struct FeedbackExample { /// What the AI was given as input (e.g. the parent task text, or diff --git a/crates/llm/tests/content_tags_smoke.rs b/crates/llm/tests/content_tags_smoke.rs index 39b99dd..0332dc8 100644 --- a/crates/llm/tests/content_tags_smoke.rs +++ b/crates/llm/tests/content_tags_smoke.rs @@ -1,10 +1,10 @@ //! Smoke test for Phase 9 LlmEngine::extract_content_tags. //! -//! Gated behind the same `MAGNOTIA_LLM_TEST_MODEL` env var as the existing +//! Gated behind the same `LUMOTIA_LLM_TEST_MODEL` env var as the existing //! smoke.rs test so neither runs in default `cargo test` runs (model //! load is heavy). Run explicitly with: //! -//! MAGNOTIA_LLM_TEST_MODEL=/path/to/model.gguf cargo test -p magnotia-llm \ +//! LUMOTIA_LLM_TEST_MODEL=/path/to/model.gguf cargo test -p lumotia-llm \ //! --test content_tags_smoke -- --nocapture use std::env; @@ -14,10 +14,10 @@ use lumotia_llm::{is_valid_intent, LlmEngine, LlmModelId}; #[test] fn extract_content_tags_returns_valid_pair() { - let model_path = match env::var("MAGNOTIA_LLM_TEST_MODEL") { + let model_path = match env::var("LUMOTIA_LLM_TEST_MODEL") { Ok(path) => PathBuf::from(path), Err(_) => { - eprintln!("MAGNOTIA_LLM_TEST_MODEL not set — skipping"); + eprintln!("LUMOTIA_LLM_TEST_MODEL not set — skipping"); return; } }; diff --git a/crates/llm/tests/smoke.rs b/crates/llm/tests/smoke.rs index 8c4a1be..f8a6e12 100644 --- a/crates/llm/tests/smoke.rs +++ b/crates/llm/tests/smoke.rs @@ -6,7 +6,7 @@ //! - `context::params::LlamaContextParams` //! - `sampling::LlamaSampler` //! -//! The test is gated behind `MAGNOTIA_LLM_TEST_MODEL`. +//! The test is gated behind `LUMOTIA_LLM_TEST_MODEL`. use std::env; use std::path::PathBuf; @@ -16,10 +16,10 @@ use lumotia_llm::LlmModelId; #[test] fn llama_cpp_2_smoke_generates_and_wraps() { - let model_path = match env::var("MAGNOTIA_LLM_TEST_MODEL") { + let model_path = match env::var("LUMOTIA_LLM_TEST_MODEL") { Ok(path) => PathBuf::from(path), Err(_) => { - eprintln!("MAGNOTIA_LLM_TEST_MODEL not set — skipping"); + eprintln!("LUMOTIA_LLM_TEST_MODEL not set — skipping"); return; } }; diff --git a/crates/mcp/src/lib.rs b/crates/mcp/src/lib.rs index 7b28a83..d4c0715 100644 --- a/crates/mcp/src/lib.rs +++ b/crates/mcp/src/lib.rs @@ -12,7 +12,7 @@ use serde_json::{json, Value}; use sqlx::SqlitePool; pub const PROTOCOL_VERSION: &str = "2024-11-05"; -pub const SERVER_NAME: &str = "magnotia-mcp"; +pub const SERVER_NAME: &str = "lumotia-mcp"; pub const SERVER_VERSION: &str = env!("CARGO_PKG_VERSION"); #[derive(Debug, Deserialize)] diff --git a/crates/mcp/src/main.rs b/crates/mcp/src/main.rs index b6dd3d4..3afc48d 100644 --- a/crates/mcp/src/main.rs +++ b/crates/mcp/src/main.rs @@ -1,4 +1,4 @@ -//! Stdio entry point for magnotia-mcp. Reads newline-delimited JSON-RPC messages +//! Stdio entry point for lumotia-mcp. Reads newline-delimited JSON-RPC messages //! from stdin, dispatches via `lumotia_mcp::handle_message`, writes responses to //! stdout. Logs land on stderr so they don't collide with the JSON-RPC stream. @@ -8,7 +8,7 @@ use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; async fn main() -> anyhow::Result<()> { let db_path = lumotia_storage::database_path(); eprintln!( - "[magnotia-mcp] opening Magnotia database at {} (read-only)", + "[lumotia-mcp] opening Lumotia database at {} (read-only)", db_path.display() ); // Open read-only at the connection level so the MCP server cannot write @@ -16,7 +16,7 @@ async fn main() -> anyhow::Result<()> { // exposes. Migrations are deliberately skipped — this binary never owns // the schema; the main app is the single migration writer. let pool = lumotia_storage::init_readonly(&db_path).await?; - eprintln!("[magnotia-mcp] ready, waiting for JSON-RPC on stdin"); + eprintln!("[lumotia-mcp] ready, waiting for JSON-RPC on stdin"); let mut lines = BufReader::new(tokio::io::stdin()).lines(); let mut stdout = tokio::io::stdout(); @@ -38,7 +38,7 @@ async fn main() -> anyhow::Result<()> { // logged and continued, dropping the response — // clients saw silence instead of a structured error // (2026-04-22 review MAJOR). - eprintln!("[magnotia-mcp] parse error: {err}"); + eprintln!("[lumotia-mcp] parse error: {err}"); lumotia_mcp::parse_error_response(&err.to_string()) } }; diff --git a/crates/storage/src/database.rs b/crates/storage/src/database.rs index 87fc35b..5aa77f3 100644 --- a/crates/storage/src/database.rs +++ b/crates/storage/src/database.rs @@ -42,7 +42,7 @@ pub async fn init(db_path: &Path) -> Result { /// Open the SQLite database in read-only mode without running migrations. /// -/// Used by `magnotia-mcp` so the MCP server cannot write to the user's database +/// Used by `lumotia-mcp` so the MCP server cannot write to the user's database /// regardless of which tools the dispatcher exposes — `read_only(true)` makes /// the constraint structural rather than relying on the request handler being /// well-behaved. Fails cleanly if the DB doesn't exist (no `create_if_missing`). @@ -1384,7 +1384,7 @@ pub async fn list_recent_errors(pool: &SqlitePool, limit: i64) -> Result Vec { /// For files that declare a `sha256` checksum we validate an existing /// complete file before skipping the download — a truncated or /// tampered file gets redownloaded automatically (pattern ported from -/// `magnotia-llm`'s model_manager, item #8 in the Whisper ecosystem brief). +/// `lumotia-llm`'s model_manager, item #8 in the Whisper ecosystem brief). pub async fn download( id: &ModelId, progress: impl Fn(DownloadProgress) + Send + 'static, @@ -113,7 +113,7 @@ pub async fn download( } fn verified_manifest_path(dir: &Path) -> PathBuf { - dir.join(".magnotia-verified") + dir.join(".lumotia-verified") } fn verified_manifest_matches( @@ -220,7 +220,7 @@ async fn download_file( // If we requested Range but the server returned 200 (full file), the // server does not support resume. Rather than blindly appending a // full file on top of our partial bytes (which would produce a - // corrupt result), restart cleanly. This mirrors the magnotia-llm + // corrupt result), restart cleanly. This mirrors the lumotia-llm // ResumeUnsupported branch — item #8 of the brief. // // For the non-resume path, we still have to validate the status: diff --git a/crates/transcription/tests/jfk_bench.rs b/crates/transcription/tests/jfk_bench.rs index 3470a8f..2ed32ed 100644 --- a/crates/transcription/tests/jfk_bench.rs +++ b/crates/transcription/tests/jfk_bench.rs @@ -2,20 +2,20 @@ //! Reports cold-load time, transcribe time, RTF, peak RSS. //! //! Gated on env vars so it never runs in CI without setup: -//! MAGNOTIA_WHISPER_TEST_MODEL=/path/to/ggml-tiny.bin -//! MAGNOTIA_WHISPER_TEST_AUDIO=/path/to/jfk.wav +//! LUMOTIA_WHISPER_TEST_MODEL=/path/to/ggml-tiny.bin +//! LUMOTIA_WHISPER_TEST_AUDIO=/path/to/jfk.wav use std::env; use std::time::Instant; #[test] fn jfk_transcription_benchmark() { - let Ok(model_path) = env::var("MAGNOTIA_WHISPER_TEST_MODEL") else { - eprintln!("MAGNOTIA_WHISPER_TEST_MODEL not set — skipping"); + let Ok(model_path) = env::var("LUMOTIA_WHISPER_TEST_MODEL") else { + eprintln!("LUMOTIA_WHISPER_TEST_MODEL not set — skipping"); return; }; - let Ok(audio_path) = env::var("MAGNOTIA_WHISPER_TEST_AUDIO") else { - eprintln!("MAGNOTIA_WHISPER_TEST_AUDIO not set — skipping"); + let Ok(audio_path) = env::var("LUMOTIA_WHISPER_TEST_AUDIO") else { + eprintln!("LUMOTIA_WHISPER_TEST_AUDIO not set — skipping"); return; }; diff --git a/crates/transcription/tests/thread_sweep.rs b/crates/transcription/tests/thread_sweep.rs index 251977b..0f040db 100644 --- a/crates/transcription/tests/thread_sweep.rs +++ b/crates/transcription/tests/thread_sweep.rs @@ -1,8 +1,8 @@ //! Thread-count scaling sweep for Whisper Tiny. //! Runs the JFK clip at n_threads = 1, 2, 4, 6, 8, 12, prints RTF tables. -//! Env-gated by `MAGNOTIA_WHISPER_TEST_MODEL` + `MAGNOTIA_WHISPER_TEST_AUDIO`. +//! Env-gated by `LUMOTIA_WHISPER_TEST_MODEL` + `LUMOTIA_WHISPER_TEST_AUDIO`. //! -//! Now prints multiple panels driven by `MAGNOTIA_POWER_STATE_OVERRIDE` so +//! Now prints multiple panels driven by `LUMOTIA_POWER_STATE_OVERRIDE` so //! the helper's predicted thread count for each (power, GPU) combination //! can be compared against the empirical RTF data. @@ -15,10 +15,10 @@ use whisper_rs::{FullParams, SamplingStrategy, WhisperContext, WhisperContextPar #[test] fn whisper_thread_count_sweep() { - let Ok(model_path) = env::var("MAGNOTIA_WHISPER_TEST_MODEL") else { + let Ok(model_path) = env::var("LUMOTIA_WHISPER_TEST_MODEL") else { return; }; - let Ok(audio_path) = env::var("MAGNOTIA_WHISPER_TEST_AUDIO") else { + let Ok(audio_path) = env::var("LUMOTIA_WHISPER_TEST_AUDIO") else { return; }; @@ -71,7 +71,7 @@ fn whisper_thread_count_sweep() { ); // Four panels: CPU and GPU axes for the predicted-helper-pick column, - // crossed with AC and battery via MAGNOTIA_POWER_STATE_OVERRIDE. + // crossed with AC and battery via LUMOTIA_POWER_STATE_OVERRIDE. let panels = [ ("AC, CPU", "ac", false), ("AC, GPU (Vulkan)", "ac", true), @@ -80,11 +80,11 @@ fn whisper_thread_count_sweep() { ]; for (label, power, gpu_offloaded_for_helper) in panels { - env::set_var("MAGNOTIA_POWER_STATE_OVERRIDE", power); + env::set_var("LUMOTIA_POWER_STATE_OVERRIDE", power); let helper_pick = inference_thread_count(Workload::Whisper, gpu_offloaded_for_helper); run_sweep_panel(label, helper_pick, &ctx, &samples, audio_secs, &targets); } - env::remove_var("MAGNOTIA_POWER_STATE_OVERRIDE"); + env::remove_var("LUMOTIA_POWER_STATE_OVERRIDE"); } fn run_sweep_panel( diff --git a/crates/transcription/tests/whisper_rs_smoke.rs b/crates/transcription/tests/whisper_rs_smoke.rs index e3ed8b6..4236d91 100644 --- a/crates/transcription/tests/whisper_rs_smoke.rs +++ b/crates/transcription/tests/whisper_rs_smoke.rs @@ -1,17 +1,17 @@ //! Smoke test: whisper-rs 0.16 loads a GGUF model, transcribes silence, and //! accepts set_initial_prompt without panicking. //! -//! Runs only when `MAGNOTIA_WHISPER_TEST_MODEL` is set to the path of a +//! Runs only when `LUMOTIA_WHISPER_TEST_MODEL` is set to the path of a //! ggml/gguf whisper model on disk. Otherwise the test exits quiet. use std::env; #[test] fn whisper_rs_smoke_loads_and_transcribes() { - let model_path = match env::var("MAGNOTIA_WHISPER_TEST_MODEL") { + let model_path = match env::var("LUMOTIA_WHISPER_TEST_MODEL") { Ok(p) => p, Err(_) => { - eprintln!("MAGNOTIA_WHISPER_TEST_MODEL not set — skipping"); + eprintln!("LUMOTIA_WHISPER_TEST_MODEL not set — skipping"); return; } }; diff --git a/src-tauri/src/commands/audio.rs b/src-tauri/src/commands/audio.rs index 80b8ba2..8cdf125 100644 --- a/src-tauri/src/commands/audio.rs +++ b/src-tauri/src/commands/audio.rs @@ -52,8 +52,8 @@ pub fn resolve_recording_path( /// collisions, which `SystemTime::now()` alone cannot guarantee /// (two calls in the same clock tick can return identical nanos). /// -/// Format: `magnotia---.wav`, e.g. -/// `magnotia-1776828000-123456789-0000.wav`. +/// Format: `lumotia---.wav`, e.g. +/// `lumotia-1776828000-123456789-0000.wav`. fn recording_filename() -> String { let duration = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) @@ -61,7 +61,7 @@ fn recording_filename() -> String { let secs = duration.as_secs(); let nanos = duration.subsec_nanos(); let counter = RECORDING_COUNTER.fetch_add(1, Ordering::SeqCst); - format!("magnotia-{secs}-{nanos:09}-{counter:04}.wav") + format!("lumotia-{secs}-{nanos:09}-{counter:04}.wav") } /// Process-lifetime monotonic counter for `recording_filename`. Starts @@ -97,11 +97,11 @@ mod tests { #[test] fn recording_filename_has_expected_shape() { let name = recording_filename(); - assert!(name.starts_with("magnotia-")); + assert!(name.starts_with("lumotia-")); assert!(name.ends_with(".wav")); - // Shape: magnotia--<9 digits>-<>=4 digits>.wav + // Shape: lumotia--<9 digits>-<>=4 digits>.wav let rest = name - .strip_prefix("magnotia-") + .strip_prefix("lumotia-") .and_then(|s| s.strip_suffix(".wav")) .expect("shape prefix/suffix"); let parts: Vec<&str> = rest.split('-').collect(); diff --git a/src-tauri/src/commands/diagnostics.rs b/src-tauri/src/commands/diagnostics.rs index 8dad3bd..3b24f01 100644 --- a/src-tauri/src/commands/diagnostics.rs +++ b/src-tauri/src/commands/diagnostics.rs @@ -24,7 +24,7 @@ use crate::commands::security::ensure_main_window; use crate::AppState; const DEFAULT_RECENT_ERRORS: i64 = 50; -const MAGNOTIA_VERSION: &str = env!("CARGO_PKG_VERSION"); +const LUMOTIA_VERSION: &str = env!("CARGO_PKG_VERSION"); /// Install the Rust panic hook. Writes each panic to a separate file in /// crashes_dir so the diagnostic-report bundler can attach them. Also @@ -60,7 +60,7 @@ pub fn install_panic_hook() { \n\ OS: {os} {arch}\n\ RUST_BACKTRACE: {bt}\n", - ver = MAGNOTIA_VERSION, + ver = LUMOTIA_VERSION, ts = ts, thread = std::thread::current().name().unwrap_or(""), info = info, @@ -312,7 +312,7 @@ async fn generate_diagnostic_report_inner( let mut out = String::new(); out.push_str("# Magnotia diagnostic report\n\n"); - out.push_str(&format!("- Version: `{}`\n", MAGNOTIA_VERSION)); + out.push_str(&format!("- Version: `{}`\n", LUMOTIA_VERSION)); out.push_str(&format!( "- OS / arch: `{} / {}`\n", std::env::consts::OS, @@ -527,7 +527,7 @@ pub async fn save_diagnostic_report( .duration_since(UNIX_EPOCH) .map(|d| d.as_secs()) .unwrap_or(0); - let path = dir.join(format!("magnotia-diagnostic-{ts}.md")); + let path = dir.join(format!("lumotia-diagnostic-{ts}.md")); fs::write(&path, &report).map_err(|e| format!("write file: {e}"))?; Ok(path.to_string_lossy().to_string()) diff --git a/src-tauri/src/commands/fs.rs b/src-tauri/src/commands/fs.rs index 2cc3d11..1c08471 100644 --- a/src-tauri/src/commands/fs.rs +++ b/src-tauri/src/commands/fs.rs @@ -35,7 +35,7 @@ mod tests { #[tokio::test] async fn write_text_file_errors_on_bad_parent() { let result = write_text_file_cmd( - "/definitely-not-a-real-path-magnotia-phase9/out.md".into(), + "/definitely-not-a-real-path-lumotia-phase9/out.md".into(), "x".into(), ) .await; From 42f4d07e48eb018ddb155eb01de8130e1dd53c27 Mon Sep 17 00:00:00 2001 From: Jake Date: Wed, 13 May 2026 08:58:05 +0100 Subject: [PATCH 06/82] =?UTF-8?q?agent:=20lumotia-rebrand=20=E2=80=94=20dr?= =?UTF-8?q?op=20MagnotiaError=20prefix,=20now=20lumotia=5Fcore::Error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 3 of the rebrand cascade per locked decision D4. MagnotiaError -> Error in crates/core/src/error.rs (the crate name already qualifies it). 92 usages across 14 .rs files renamed via word-boundary sed. One collision required disambiguation: lumotia_storage already had its own local Error type (introduced by the slop-pass Area A residuals work). crates/storage/src/error.rs aliases the imported core error as CoreError on import; the From for CoreError boundary impl and the CoreError::Storage construction site use the alias. cargo build --workspace passes. cargo test --workspace: 330 pass, 0 fail. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/audio/src/capture.rs | 26 +++++++------- crates/audio/src/concurrency.rs | 2 +- crates/audio/src/decode.rs | 34 +++++++++---------- crates/audio/src/resample.rs | 8 ++--- crates/audio/src/streaming_resample.rs | 10 +++--- crates/audio/src/wav.rs | 26 +++++++------- crates/core/src/error.rs | 8 ++--- crates/core/src/lib.rs | 2 +- crates/storage/src/error.rs | 16 ++++----- crates/transcription/src/concurrency.rs | 4 +-- crates/transcription/src/local_engine.rs | 10 +++--- crates/transcription/src/model_manager.rs | 22 ++++++------ crates/transcription/src/orchestrator.rs | 8 ++--- .../transcription/src/whisper_rs_backend.rs | 8 ++--- 14 files changed, 92 insertions(+), 92 deletions(-) diff --git a/crates/audio/src/capture.rs b/crates/audio/src/capture.rs index e8093c2..05c838a 100644 --- a/crates/audio/src/capture.rs +++ b/crates/audio/src/capture.rs @@ -8,7 +8,7 @@ use regex::Regex; use serde::{Deserialize, Serialize}; use std::sync::OnceLock; -use lumotia_core::error::{MagnotiaError, Result}; +use lumotia_core::error::{Error, Result}; const AUDIO_CHANNEL_CAPACITY: usize = 32; @@ -106,7 +106,7 @@ impl MicrophoneCapture { let devices = host .input_devices() - .map_err(|e| MagnotiaError::AudioCaptureFailed(format!("input_devices: {e}")))?; + .map_err(|e| Error::AudioCaptureFailed(format!("input_devices: {e}")))?; // Load ALSA card descriptions once per enumeration. These are the // "real" product names (e.g. "Blue Microphones") that cpal's @@ -144,7 +144,7 @@ impl MicrophoneCapture { let host = cpal::default_host(); let devices = host .input_devices() - .map_err(|e| MagnotiaError::AudioCaptureFailed(format!("input_devices: {e}")))?; + .map_err(|e| Error::AudioCaptureFailed(format!("input_devices: {e}")))?; for device in devices { let name = device_display_name(&device).unwrap_or_default(); @@ -154,7 +154,7 @@ impl MicrophoneCapture { } } - Err(MagnotiaError::AudioCaptureFailed(format!( + Err(Error::AudioCaptureFailed(format!( "Selected device '{device_name}' not found in current host enumeration. \ It may have been disconnected. Open Settings → Audio to pick another." ))) @@ -178,7 +178,7 @@ impl MicrophoneCapture { let mut all_devices: Vec = host .input_devices() - .map_err(|e| MagnotiaError::AudioCaptureFailed(format!("input_devices: {e}")))? + .map_err(|e| Error::AudioCaptureFailed(format!("input_devices: {e}")))? .collect(); // Sort: default first, then non-monitor, then monitor-as-last-resort. @@ -237,7 +237,7 @@ impl MicrophoneCapture { } } - Err(MagnotiaError::AudioCaptureFailed( + Err(Error::AudioCaptureFailed( "No working microphone found. Check that an input device is connected, \ that PulseAudio/PipeWire is running, and that the app has microphone permission. \ Then open Settings → Audio to pick a device explicitly." @@ -361,7 +361,7 @@ fn open_and_validate( ) -> Result<(MicrophoneCapture, mpsc::Receiver)> { let config = device .default_input_config() - .map_err(|e| MagnotiaError::AudioCaptureFailed(format!("default_input_config: {e}")))?; + .map_err(|e| Error::AudioCaptureFailed(format!("default_input_config: {e}")))?; let sample_rate = config.sample_rate(); let channels = config.channels(); let format = config.sample_format(); @@ -422,16 +422,16 @@ fn open_and_validate( name.to_string(), ), other => { - return Err(MagnotiaError::AudioCaptureFailed(format!( + return Err(Error::AudioCaptureFailed(format!( "unsupported sample format {other:?}" ))) } } - .map_err(|e| MagnotiaError::AudioCaptureFailed(format!("build_input_stream: {e}")))?; + .map_err(|e| Error::AudioCaptureFailed(format!("build_input_stream: {e}")))?; stream .play() - .map_err(|e| MagnotiaError::AudioCaptureFailed(format!("stream.play: {e}")))?; + .map_err(|e| Error::AudioCaptureFailed(format!("stream.play: {e}")))?; // Validation window: collect chunks for DEVICE_VALIDATION_MS, compute RMS. let deadline = @@ -460,7 +460,7 @@ fn open_and_validate( } if total_samples == 0 { - return Err(MagnotiaError::AudioCaptureFailed( + return Err(Error::AudioCaptureFailed( "device delivered zero samples in validation window".into(), )); } @@ -475,7 +475,7 @@ fn open_and_validate( ); if require_audio && rms < SILENCE_RMS_FLOOR { - return Err(MagnotiaError::AudioCaptureFailed(format!( + return Err(Error::AudioCaptureFailed(format!( "device produced silence (rms={rms:.6} below floor {SILENCE_RMS_FLOOR:.6})" ))); } @@ -485,7 +485,7 @@ fn open_and_validate( // long stream of f32 zeros from a borked device — that is worse than // failing fast. if rms < DEAD_SILENCE_FLOOR { - return Err(MagnotiaError::AudioCaptureFailed(format!( + return Err(Error::AudioCaptureFailed(format!( "device produced dead silence (rms={rms:.6e} below absolute floor {DEAD_SILENCE_FLOOR:.6e})" ))); } diff --git a/crates/audio/src/concurrency.rs b/crates/audio/src/concurrency.rs index 53b136a..85fb004 100644 --- a/crates/audio/src/concurrency.rs +++ b/crates/audio/src/concurrency.rs @@ -16,6 +16,6 @@ pub async fn decode_and_resample(path: &Path) -> Result { }) .await .map_err(|e| { - lumotia_core::error::MagnotiaError::AudioDecodeFailed(format!("Task join error: {e}")) + lumotia_core::error::Error::AudioDecodeFailed(format!("Task join error: {e}")) })? } diff --git a/crates/audio/src/decode.rs b/crates/audio/src/decode.rs index 6a2583a..e1f62ff 100644 --- a/crates/audio/src/decode.rs +++ b/crates/audio/src/decode.rs @@ -9,13 +9,13 @@ use symphonia::core::io::MediaSourceStream; use symphonia::core::meta::MetadataOptions; use symphonia::core::probe::Hint; -use lumotia_core::error::{MagnotiaError, Result}; +use lumotia_core::error::{Error, Result}; use lumotia_core::types::AudioSamples; /// Decode an audio file to mono f32 PCM samples. /// Supports all formats symphonia handles: mp3, aac, flac, wav, ogg, etc. /// -/// Any read- or decode-side error is propagated as `MagnotiaError::AudioDecodeFailed`. +/// Any read- or decode-side error is propagated as `Error::AudioDecodeFailed`. /// A previous implementation `break`ed out of the packet loop on any read /// error and skipped per-packet decode errors, so a truncated or corrupt /// input silently returned `Ok` with whatever had decoded before the @@ -29,7 +29,7 @@ pub fn decode_audio_file_limited( max_duration_secs: Option, ) -> Result { let file = File::open(path) - .map_err(|e| MagnotiaError::AudioDecodeFailed(format!("Cannot open file: {e}")))?; + .map_err(|e| Error::AudioDecodeFailed(format!("Cannot open file: {e}")))?; let mss = MediaSourceStream::new(Box::new(file), Default::default()); let mut hint = Hint::new(); @@ -42,7 +42,7 @@ pub fn decode_audio_file_limited( pub fn probe_audio_duration_secs(path: &Path) -> Result> { let file = File::open(path) - .map_err(|e| MagnotiaError::AudioDecodeFailed(format!("Cannot open file: {e}")))?; + .map_err(|e| Error::AudioDecodeFailed(format!("Cannot open file: {e}")))?; let mss = MediaSourceStream::new(Box::new(file), Default::default()); let mut hint = Hint::new(); if let Some(ext) = path.extension().and_then(|e| e.to_str()) { @@ -56,15 +56,15 @@ pub fn probe_audio_duration_secs(path: &Path) -> Result> { &FormatOptions::default(), &MetadataOptions::default(), ) - .map_err(|e| MagnotiaError::AudioDecodeFailed(format!("Unsupported format: {e}")))?; + .map_err(|e| Error::AudioDecodeFailed(format!("Unsupported format: {e}")))?; let track = probed .format .default_track() - .ok_or_else(|| MagnotiaError::AudioDecodeFailed("No audio track found".into()))?; + .ok_or_else(|| Error::AudioDecodeFailed("No audio track found".into()))?; let sample_rate = track .codec_params .sample_rate - .ok_or_else(|| MagnotiaError::AudioDecodeFailed("Unknown sample rate".into()))?; + .ok_or_else(|| Error::AudioDecodeFailed("Unknown sample rate".into()))?; Ok(track .codec_params .n_frames @@ -86,20 +86,20 @@ fn decode_media_stream( &FormatOptions::default(), &MetadataOptions::default(), ) - .map_err(|e| MagnotiaError::AudioDecodeFailed(format!("Unsupported format: {e}")))?; + .map_err(|e| Error::AudioDecodeFailed(format!("Unsupported format: {e}")))?; let mut format = probed.format; let track = format .default_track() - .ok_or_else(|| MagnotiaError::AudioDecodeFailed("No audio track found".into()))?; + .ok_or_else(|| Error::AudioDecodeFailed("No audio track found".into()))?; let sample_rate = track .codec_params .sample_rate - .ok_or_else(|| MagnotiaError::AudioDecodeFailed("Unknown sample rate".into()))?; + .ok_or_else(|| Error::AudioDecodeFailed("Unknown sample rate".into()))?; if sample_rate == 0 { - return Err(MagnotiaError::AudioDecodeFailed( + return Err(Error::AudioDecodeFailed( "Invalid sample rate: 0".into(), )); } @@ -109,7 +109,7 @@ fn decode_media_stream( let mut decoder = symphonia::default::get_codecs() .make(&track.codec_params, &DecoderOptions::default()) - .map_err(|e| MagnotiaError::AudioDecodeFailed(format!("Codec error: {e}")))?; + .map_err(|e| Error::AudioDecodeFailed(format!("Codec error: {e}")))?; let mut samples: Vec = Vec::new(); @@ -123,12 +123,12 @@ fn decode_media_stream( break; } Err(SymphoniaError::ResetRequired) => { - return Err(MagnotiaError::AudioDecodeFailed( + return Err(Error::AudioDecodeFailed( "decoder reset required mid-stream — input contains a discontinuity".into(), )); } Err(e) => { - return Err(MagnotiaError::AudioDecodeFailed(format!( + return Err(Error::AudioDecodeFailed(format!( "packet read failed: {e}" ))); } @@ -140,7 +140,7 @@ fn decode_media_stream( let decoded = decoder .decode(&packet) - .map_err(|e| MagnotiaError::AudioDecodeFailed(format!("packet decode failed: {e}")))?; + .map_err(|e| Error::AudioDecodeFailed(format!("packet decode failed: {e}")))?; let spec = *decoded.spec(); let channels = spec.channels.count(); @@ -160,7 +160,7 @@ fn decode_media_stream( .map(|limit| samples.len() > limit) .unwrap_or(false) { - return Err(MagnotiaError::AudioDecodeFailed(format!( + return Err(Error::AudioDecodeFailed(format!( "Audio is longer than the {:.0} minute import limit", max_duration_secs.unwrap_or(0.0) / 60.0 ))); @@ -168,7 +168,7 @@ fn decode_media_stream( } if samples.is_empty() { - return Err(MagnotiaError::AudioDecodeFailed( + return Err(Error::AudioDecodeFailed( "No audio data decoded".into(), )); } diff --git a/crates/audio/src/resample.rs b/crates/audio/src/resample.rs index f0e2c24..33b38be 100644 --- a/crates/audio/src/resample.rs +++ b/crates/audio/src/resample.rs @@ -3,7 +3,7 @@ use rubato::{ }; use lumotia_core::constants::WHISPER_SAMPLE_RATE; -use lumotia_core::error::{MagnotiaError, Result}; +use lumotia_core::error::{Error, Result}; use lumotia_core::types::AudioSamples; /// Resample audio to 16kHz mono using sinc interpolation (rubato). @@ -17,7 +17,7 @@ pub fn resample_to_16khz(audio: &AudioSamples) -> Result { } if from_rate == 0 { - return Err(MagnotiaError::AudioDecodeFailed( + return Err(Error::AudioDecodeFailed( "Cannot resample: source rate is 0".into(), )); } @@ -36,7 +36,7 @@ pub fn resample_to_16khz(audio: &AudioSamples) -> Result { let mut resampler = SincFixedIn::::new( ratio, 1.1, params, chunk_size, 1, // mono ) - .map_err(|e| MagnotiaError::AudioDecodeFailed(format!("Resampler init failed: {e}")))?; + .map_err(|e| Error::AudioDecodeFailed(format!("Resampler init failed: {e}")))?; let samples = audio.samples(); let mut output_samples: Vec = Vec::new(); @@ -53,7 +53,7 @@ pub fn resample_to_16khz(audio: &AudioSamples) -> Result { let input = vec![chunk]; let result = resampler .process(&input, None) - .map_err(|e| MagnotiaError::AudioDecodeFailed(format!("Resample failed: {e}")))?; + .map_err(|e| Error::AudioDecodeFailed(format!("Resample failed: {e}")))?; if !result.is_empty() && !result[0].is_empty() { output_samples.extend_from_slice(&result[0]); diff --git a/crates/audio/src/streaming_resample.rs b/crates/audio/src/streaming_resample.rs index 3f263f3..482dbb4 100644 --- a/crates/audio/src/streaming_resample.rs +++ b/crates/audio/src/streaming_resample.rs @@ -28,7 +28,7 @@ use rubato::{ }; use lumotia_core::constants::WHISPER_SAMPLE_RATE; -use lumotia_core::error::{MagnotiaError, Result}; +use lumotia_core::error::{Error, Result}; /// Number of input samples the rubato resampler consumes per `process()` /// call. Matches the chunk size used in `resample::resample_to_16khz`. @@ -51,7 +51,7 @@ impl StreamingResampler { /// rubato rejects the requested ratio. pub fn new(from_rate: u32) -> Result { if from_rate == 0 { - return Err(MagnotiaError::AudioDecodeFailed( + return Err(Error::AudioDecodeFailed( "StreamingResampler: input sample rate is 0".into(), )); } @@ -78,7 +78,7 @@ impl StreamingResampler { 1, // mono ) .map_err(|e| { - MagnotiaError::AudioDecodeFailed(format!("StreamingResampler init failed: {e}")) + Error::AudioDecodeFailed(format!("StreamingResampler init failed: {e}")) })?; Ok(Self::Sinc { @@ -110,7 +110,7 @@ impl StreamingResampler { let chunk: Vec = residual.drain(..INPUT_CHUNK).collect(); let input = vec![chunk]; let result = resampler.process(&input, None).map_err(|e| { - MagnotiaError::AudioDecodeFailed(format!( + Error::AudioDecodeFailed(format!( "StreamingResampler process failed: {e}" )) })?; @@ -144,7 +144,7 @@ impl StreamingResampler { let input = vec![chunk]; let result = resampler.process(&input, None).map_err(|e| { - MagnotiaError::AudioDecodeFailed(format!( + Error::AudioDecodeFailed(format!( "StreamingResampler flush failed: {e}" )) })?; diff --git a/crates/audio/src/wav.rs b/crates/audio/src/wav.rs index b702b37..ce300bb 100644 --- a/crates/audio/src/wav.rs +++ b/crates/audio/src/wav.rs @@ -1,7 +1,7 @@ use std::io::BufWriter; use std::path::Path; -use lumotia_core::error::{MagnotiaError, Result}; +use lumotia_core::error::{Error, Result}; use lumotia_core::types::AudioSamples; /// Append-friendly WAV writer for long-running captures. @@ -40,10 +40,10 @@ impl WavWriter { bits_per_sample: 16, sample_format: hound::SampleFormat::Int, }; - let file = std::fs::File::create(path).map_err(MagnotiaError::from)?; + let file = std::fs::File::create(path).map_err(Error::from)?; let buffered = BufWriter::new(file); let inner = hound::WavWriter::new(buffered, spec).map_err(|e| { - MagnotiaError::from(std::io::Error::other(format!("WAV create failed: {e}"))) + Error::from(std::io::Error::other(format!("WAV create failed: {e}"))) })?; Ok(Self { inner, @@ -61,7 +61,7 @@ impl WavWriter { let clamped = sample.clamp(-1.0, 1.0); let int_sample = (clamped * i16::MAX as f32) as i16; self.inner.write_sample(int_sample).map_err(|e| { - MagnotiaError::from(std::io::Error::other(format!("WAV write failed: {e}"))) + Error::from(std::io::Error::other(format!("WAV write failed: {e}"))) })?; } self.samples_since_flush += samples.len(); @@ -78,7 +78,7 @@ impl WavWriter { /// boundaries (end-of-utterance, UI events) for tighter recovery. pub fn flush(&mut self) -> Result<()> { self.inner.flush().map_err(|e| { - MagnotiaError::from(std::io::Error::other(format!("WAV flush failed: {e}"))) + Error::from(std::io::Error::other(format!("WAV flush failed: {e}"))) })?; self.samples_since_flush = 0; Ok(()) @@ -90,7 +90,7 @@ impl WavWriter { /// that care about the unflushed tail should always finalise. pub fn finalize(self) -> Result<()> { self.inner.finalize().map_err(|e| { - MagnotiaError::from(std::io::Error::other(format!("WAV finalize failed: {e}"))) + Error::from(std::io::Error::other(format!("WAV finalize failed: {e}"))) })?; Ok(()) } @@ -106,19 +106,19 @@ pub fn write_wav(path: &Path, audio: &AudioSamples) -> Result<()> { }; let mut writer = hound::WavWriter::create(path, spec).map_err(|e| { - MagnotiaError::from(std::io::Error::other(format!("WAV create failed: {e}"))) + Error::from(std::io::Error::other(format!("WAV create failed: {e}"))) })?; for &sample in audio.samples() { let clamped = sample.clamp(-1.0, 1.0); let int_sample = (clamped * i16::MAX as f32) as i16; writer.write_sample(int_sample).map_err(|e| { - MagnotiaError::from(std::io::Error::other(format!("WAV write failed: {e}"))) + Error::from(std::io::Error::other(format!("WAV write failed: {e}"))) })?; } writer.finalize().map_err(|e| { - MagnotiaError::from(std::io::Error::other(format!("WAV finalize failed: {e}"))) + Error::from(std::io::Error::other(format!("WAV finalize failed: {e}"))) })?; Ok(()) @@ -126,14 +126,14 @@ pub fn write_wav(path: &Path, audio: &AudioSamples) -> Result<()> { /// Read a WAV file to f32 PCM `AudioSamples`. /// -/// Any per-sample decode error is surfaced as `MagnotiaError::AudioDecodeFailed` +/// Any per-sample decode error is surfaced as `Error::AudioDecodeFailed` /// rather than silently dropped. A previous implementation used /// `filter_map(|s| s.ok())`, so a truncated or corrupt payload returned /// a short, silently-partial `AudioSamples` — callers got `Ok` while /// losing audio (flagged by the 2026-04-22 review). pub fn read_wav(path: &Path) -> Result { let reader = hound::WavReader::open(path) - .map_err(|e| MagnotiaError::AudioDecodeFailed(format!("WAV open failed: {e}")))?; + .map_err(|e| Error::AudioDecodeFailed(format!("WAV open failed: {e}")))?; let spec = reader.spec(); let sample_rate = spec.sample_rate; @@ -147,7 +147,7 @@ pub fn read_wav(path: &Path) -> Result { sample .map(|s| s as f32 / (1 << (bits_per_sample - 1)) as f32) .map_err(|e| { - MagnotiaError::AudioDecodeFailed(format!("WAV sample decode failed: {e}")) + Error::AudioDecodeFailed(format!("WAV sample decode failed: {e}")) }) }) .collect::>>()?, @@ -155,7 +155,7 @@ pub fn read_wav(path: &Path) -> Result { .into_samples::() .map(|sample| { sample.map_err(|e| { - MagnotiaError::AudioDecodeFailed(format!("WAV sample decode failed: {e}")) + Error::AudioDecodeFailed(format!("WAV sample decode failed: {e}")) }) }) .collect::>>()?, diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index 2f0abbd..e45d09f 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -9,7 +9,7 @@ use crate::types::ModelId; /// Implements `Serialize` so errors can be sent to the frontend as /// structured JSON rather than opaque strings. #[derive(Debug, thiserror::Error, Serialize)] -pub enum MagnotiaError { +pub enum Error { #[error("model not found: {0}")] ModelNotFound(ModelId), @@ -55,7 +55,7 @@ pub enum MagnotiaError { }, } -/// Coarse discriminator for `MagnotiaError::Storage`. The storage crate maps +/// Coarse discriminator for `Error::Storage`. The storage crate maps /// its full typed error onto one of these kinds at the boundary. Backend code /// that wants finer-grained branching pattern-matches on /// `lumotia_storage::Error` directly. @@ -70,7 +70,7 @@ pub enum StorageKind { Filesystem, } -impl From for MagnotiaError { +impl From for Error { fn from(err: std::io::Error) -> Self { Self::Io { kind: format!("{:?}", err.kind()), @@ -80,4 +80,4 @@ impl From for MagnotiaError { } } -pub type Result = std::result::Result; +pub type Result = std::result::Result; diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 4889b32..32bd2e8 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -9,7 +9,7 @@ pub mod recommendation; pub mod tuning; pub mod types; -pub use error::{MagnotiaError, Result}; +pub use error::{Error, Result}; pub use types::{ AudioSamples, DownloadProgress, EngineName, Megabytes, ModelId, Segment, Transcript, TranscriptionOptions, diff --git a/crates/storage/src/error.rs b/crates/storage/src/error.rs index e4117ff..9711bd3 100644 --- a/crates/storage/src/error.rs +++ b/crates/storage/src/error.rs @@ -1,8 +1,8 @@ //! Storage-local typed error. //! //! Backend code that wants to branch on storage failure modes pattern-matches -//! on [`Error`] directly. The crate boundary into [`lumotia_core::error::MagnotiaError`] -//! flattens this into a [`MagnotiaError::Storage`] variant carrying a serde-friendly +//! on [`Error`] directly. The crate boundary into [`lumotia_core::error::Error`] +//! flattens this into a [`Error::Storage`] variant carrying a serde-friendly //! `kind`, an `operation` label, and the Display output as `detail` — so the //! frontend (which still receives stringified errors from Tauri commands today) //! keeps working, and Area E can later expose the typed `kind` to the FE without @@ -14,7 +14,7 @@ use std::borrow::Cow; use std::path::PathBuf; -use lumotia_core::error::{MagnotiaError, StorageKind}; +use lumotia_core::error::{Error as CoreError, StorageKind}; /// Kinds of database-open operation that can fail before the pool is ready. #[derive(Debug, Clone, Copy)] @@ -145,7 +145,7 @@ pub enum Error { } impl Error { - /// Discriminator for the boundary conversion into [`MagnotiaError`]. + /// Discriminator for the boundary conversion into [`Error`]. pub fn kind(&self) -> StorageKind { match self { Error::DatabaseOpen { .. } => StorageKind::DatabaseOpen, @@ -157,7 +157,7 @@ impl Error { } } - /// Operation label that flows into the [`MagnotiaError::Storage`] boundary + /// Operation label that flows into the [`Error::Storage`] boundary /// shape. For per-query failures this is the caller-provided label; /// for the other variants it's the variant's intrinsic label. pub fn operation_label(&self) -> Cow<'static, str> { @@ -175,14 +175,14 @@ impl Error { } /// Boundary conversion — flattens the typed error into the wire-friendly -/// [`MagnotiaError::Storage`] variant. Lives in the storage crate (not in core) +/// [`CoreError::Storage`] variant. Lives in the storage crate (not in core) /// to avoid a `core -> storage` dependency cycle. -impl From for MagnotiaError { +impl From for CoreError { fn from(error: Error) -> Self { let kind = error.kind(); let operation = error.operation_label().into_owned(); let detail = error.to_string(); - MagnotiaError::Storage { + CoreError::Storage { kind, operation, detail, diff --git a/crates/transcription/src/concurrency.rs b/crates/transcription/src/concurrency.rs index c693ba2..2b56673 100644 --- a/crates/transcription/src/concurrency.rs +++ b/crates/transcription/src/concurrency.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use lumotia_core::error::{MagnotiaError, Result}; +use lumotia_core::error::{Error, Result}; use lumotia_core::types::{AudioSamples, TranscriptionOptions}; use crate::local_engine::{LocalEngine, TimedTranscript}; @@ -14,5 +14,5 @@ pub async fn run_inference( ) -> Result { tokio::task::spawn_blocking(move || engine.transcribe_sync(&audio, &options)) .await - .map_err(|e| MagnotiaError::TranscriptionFailed(format!("Task join error: {e}")))? + .map_err(|e| Error::TranscriptionFailed(format!("Task join error: {e}")))? } diff --git a/crates/transcription/src/local_engine.rs b/crates/transcription/src/local_engine.rs index b67e04e..6195bce 100644 --- a/crates/transcription/src/local_engine.rs +++ b/crates/transcription/src/local_engine.rs @@ -4,7 +4,7 @@ use std::time::Instant; use transcribe_rs::{SpeechModel, TranscribeOptions, TranscriptionResult}; -use lumotia_core::error::{MagnotiaError, Result}; +use lumotia_core::error::{Error, Result}; use lumotia_core::types::{ AudioSamples, EngineName, ModelId, Segment, Transcript, TranscriptionOptions, }; @@ -48,7 +48,7 @@ impl Transcriber for SpeechModelAdapter { let result: TranscriptionResult = self .0 .transcribe(samples, &opts) - .map_err(|e| MagnotiaError::TranscriptionFailed(e.to_string()))?; + .map_err(|e| Error::TranscriptionFailed(e.to_string()))?; Ok(result .segments .unwrap_or_default() @@ -140,7 +140,7 @@ impl LocalEngine { options: &TranscriptionOptions, ) -> Result { let mut guard = self.engine.lock().unwrap_or_else(|e| e.into_inner()); - let backend = guard.as_mut().ok_or(MagnotiaError::EngineNotLoaded)?; + let backend = guard.as_mut().ok_or(Error::EngineNotLoaded)?; let start = Instant::now(); let segments = backend.transcribe_sync(audio.samples(), options)?; @@ -197,7 +197,7 @@ impl transcribe_rs::SpeechModel for ParakeetWordGranularity { pub fn load_parakeet(model_dir: &Path) -> Result> { use transcribe_rs::onnx::Quantization; let model = transcribe_rs::onnx::parakeet::ParakeetModel::load(model_dir, &Quantization::Int8) - .map_err(|e| MagnotiaError::TranscriptionFailed(format!("Failed to load Parakeet: {e}")))?; + .map_err(|e| Error::TranscriptionFailed(format!("Failed to load Parakeet: {e}")))?; Ok(Box::new(SpeechModelAdapter(Box::new( ParakeetWordGranularity(model), )))) @@ -207,7 +207,7 @@ pub fn load_parakeet(model_dir: &Path) -> Result> { #[cfg(feature = "whisper")] pub fn load_whisper(model_path: &Path) -> Result> { let backend = WhisperRsBackend::load(model_path) - .map_err(|e| MagnotiaError::TranscriptionFailed(format!("Failed to load Whisper: {e}")))?; + .map_err(|e| Error::TranscriptionFailed(format!("Failed to load Whisper: {e}")))?; Ok(Box::new(backend)) } diff --git a/crates/transcription/src/model_manager.rs b/crates/transcription/src/model_manager.rs index 0ac8be6..c42b5a6 100644 --- a/crates/transcription/src/model_manager.rs +++ b/crates/transcription/src/model_manager.rs @@ -2,7 +2,7 @@ use std::collections::HashSet; use std::path::{Path, PathBuf}; use std::sync::{LazyLock, Mutex}; -use lumotia_core::error::{MagnotiaError, Result}; +use lumotia_core::error::{Error, Result}; use lumotia_core::model_registry::{find_model, ModelFile}; use lumotia_core::types::{DownloadProgress, ModelId}; @@ -18,9 +18,9 @@ impl DownloadReservation { let id = id.as_str().to_string(); let mut active = ACTIVE_DOWNLOADS .lock() - .map_err(|_| MagnotiaError::DownloadFailed("download lock poisoned".into()))?; + .map_err(|_| Error::DownloadFailed("download lock poisoned".into()))?; if !active.insert(id.clone()) { - return Err(MagnotiaError::DownloadFailed(format!( + return Err(Error::DownloadFailed(format!( "download already in progress for {id}" ))); } @@ -80,7 +80,7 @@ pub async fn download( progress: impl Fn(DownloadProgress) + Send + 'static, ) -> Result<()> { let _reservation = DownloadReservation::acquire(id)?; - let entry = find_model(id).ok_or_else(|| MagnotiaError::ModelNotFound(id.clone()))?; + let entry = find_model(id).ok_or_else(|| Error::ModelNotFound(id.clone()))?; let dir = model_dir(id); std::fs::create_dir_all(&dir)?; @@ -98,7 +98,7 @@ pub async fn download( let _ = std::fs::remove_file(&dest); } Err(e) => { - return Err(MagnotiaError::DownloadFailed(format!( + return Err(Error::DownloadFailed(format!( "failed to verify existing {}: {e}", file.filename ))); @@ -196,7 +196,7 @@ async fn download_file( let client = reqwest::Client::builder() .connect_timeout(std::time::Duration::from_secs(30)) .build() - .map_err(|e| MagnotiaError::DownloadFailed(e.to_string()))?; + .map_err(|e| Error::DownloadFailed(e.to_string()))?; // Check for existing partial download (resume support) let existing_bytes = if part_path.exists() { @@ -215,7 +215,7 @@ async fn download_file( let response = request .send() .await - .map_err(|e| MagnotiaError::DownloadFailed(e.to_string()))?; + .map_err(|e| Error::DownloadFailed(e.to_string()))?; // If we requested Range but the server returned 200 (full file), the // server does not support resume. Rather than blindly appending a @@ -237,14 +237,14 @@ async fn download_file( false } other => { - return Err(MagnotiaError::DownloadFailed(format!( + return Err(Error::DownloadFailed(format!( "resume request returned unexpected status {other}" ))); } } } else { if !response.status().is_success() { - return Err(MagnotiaError::DownloadFailed(format!( + return Err(Error::DownloadFailed(format!( "download returned HTTP {} for {}", response.status(), file.filename @@ -291,7 +291,7 @@ async fn download_file( } while let Some(chunk) = stream.next().await { - let chunk = chunk.map_err(|e| MagnotiaError::DownloadFailed(e.to_string()))?; + let chunk = chunk.map_err(|e| Error::DownloadFailed(e.to_string()))?; std::io::Write::write_all(&mut out, &chunk)?; hasher.update(&chunk); downloaded += chunk.len() as u64; @@ -319,7 +319,7 @@ async fn download_file( let actual = format!("{:x}", hasher.finalize()); if actual != file.sha256 { let _ = std::fs::remove_file(&part_path); - return Err(MagnotiaError::DownloadFailed(format!( + return Err(Error::DownloadFailed(format!( "SHA256 mismatch for {}: expected {}, got {}", file.filename, file.sha256, actual ))); diff --git a/crates/transcription/src/orchestrator.rs b/crates/transcription/src/orchestrator.rs index 26629ce..dec6027 100644 --- a/crates/transcription/src/orchestrator.rs +++ b/crates/transcription/src/orchestrator.rs @@ -22,7 +22,7 @@ use lumotia_cloud_providers::{ CostClass, EngineProfile, ProviderCapabilities, ProviderId, ProviderKind, ProviderTranscript, TranscriptionProvider, }; -use lumotia_core::error::{MagnotiaError, Result}; +use lumotia_core::error::{Error, Result}; use lumotia_core::types::{AudioSamples, TranscriptionOptions}; use crate::local_engine::LocalEngine; @@ -81,7 +81,7 @@ impl TranscriptionProvider for LocalProviderAdapter { if self.engine.is_loaded() { Ok(()) } else { - Err(MagnotiaError::EngineNotLoaded) + Err(Error::EngineNotLoaded) } } @@ -93,7 +93,7 @@ impl TranscriptionProvider for LocalProviderAdapter { let engine = self.engine.clone(); let timed = tokio::task::spawn_blocking(move || engine.transcribe_sync(&audio, &options)) .await - .map_err(|e| MagnotiaError::TranscriptionFailed(format!("Task join error: {e}")))??; + .map_err(|e| Error::TranscriptionFailed(format!("Task join error: {e}")))??; Ok(ProviderTranscript { transcript: timed.transcript, inference_ms: timed.inference_ms, @@ -124,7 +124,7 @@ impl Orchestrator { pub fn resolve(&self, profile: &EngineProfile) -> Result> { self.registry .get(&profile.engine_id) - .ok_or_else(|| MagnotiaError::ProviderNotRegistered(profile.engine_id.to_string())) + .ok_or_else(|| Error::ProviderNotRegistered(profile.engine_id.to_string())) } /// Transcribe audio using the provider named in the profile. The diff --git a/crates/transcription/src/whisper_rs_backend.rs b/crates/transcription/src/whisper_rs_backend.rs index 16557f9..2af5351 100644 --- a/crates/transcription/src/whisper_rs_backend.rs +++ b/crates/transcription/src/whisper_rs_backend.rs @@ -10,7 +10,7 @@ use std::path::Path; use whisper_rs::{FullParams, SamplingStrategy, WhisperContext, WhisperContextParameters}; -use lumotia_core::error::{MagnotiaError, Result}; +use lumotia_core::error::{Error, Result}; use lumotia_core::hardware::vulkan_loader_available; use lumotia_core::tuning::{inference_thread_count, Workload}; use lumotia_core::types::{Segment, TranscriptionOptions}; @@ -65,7 +65,7 @@ impl Transcriber for WhisperRsBackend { ); let mut state = self.ctx.create_state().map_err(|e| { - MagnotiaError::TranscriptionFailed( + Error::TranscriptionFailed( WhisperBackendError::State(e.to_string()).to_string(), ) })?; @@ -88,7 +88,7 @@ impl Transcriber for WhisperRsBackend { params.set_print_realtime(false); state.full(params, samples).map_err(|e| { - MagnotiaError::TranscriptionFailed( + Error::TranscriptionFailed( WhisperBackendError::Transcribe(e.to_string()).to_string(), ) })?; @@ -103,7 +103,7 @@ impl Transcriber for WhisperRsBackend { let text = seg .to_str() .map_err(|e| { - MagnotiaError::TranscriptionFailed( + Error::TranscriptionFailed( WhisperBackendError::Transcribe(e.to_string()).to_string(), ) })? From e2a5feb718c0d3714763e511d5421362072f5452 Mon Sep 17 00:00:00 2001 From: Jake Date: Wed, 13 May 2026 09:00:33 +0100 Subject: [PATCH 07/82] =?UTF-8?q?agent:=20lumotia-rebrand=20=E2=80=94=20tr?= =?UTF-8?q?acing=20filter=20targets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 4 of the rebrand cascade. Updates the default RUST_LOG filter in src-tauri/src/lib.rs and all 'target: "magnotia_startup"' string literals across src-tauri/src/lib.rs and src-tauri/src/commands/models.rs. Default filter (before): warn,magnotia=info,lumotia_lib=info,lumotia_audio=info, magnotia_startup=info,lumotia_hotkey=info,lumotia_ai_formatting=info Default filter (after): warn,lumotia=info,lumotia_lib=info,lumotia_core=info, lumotia_audio=info,lumotia_hotkey=info,lumotia_ai_formatting=info, lumotia_llm=info,lumotia_storage=info,lumotia_transcription=info, lumotia_startup=info magnotia_startup was a logical tracing target (not a crate); renamed to lumotia_startup for consistency. magnotia_lib was already renamed to lumotia_lib in Phase 2. Added per-crate filter entries for parity across the workspace. cargo build --workspace passes. cargo test --workspace: 330 pass, 0 fail. Co-Authored-By: Claude Opus 4.7 (1M context) --- src-tauri/src/commands/models.rs | 10 +++++----- src-tauri/src/lib.rs | 18 +++++++++--------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src-tauri/src/commands/models.rs b/src-tauri/src/commands/models.rs index a583403..957b5f1 100644 --- a/src-tauri/src/commands/models.rs +++ b/src-tauri/src/commands/models.rs @@ -208,11 +208,11 @@ pub fn prewarm_default_model(whisper_engine: Arc) { let options = TranscriptionOptions::default(); match whisper_engine.transcribe_sync(&silence, &options) { Ok(_) => tracing::info!( - target: "magnotia_startup", + target: "lumotia_startup", "Whisper warm-up inference complete" ), Err(e) => tracing::warn!( - target: "magnotia_startup", + target: "lumotia_startup", error = %e, "Whisper warm-up inference failed" ), @@ -223,16 +223,16 @@ pub fn prewarm_default_model(whisper_engine: Arc) { match result { Ok(Ok(())) => tracing::info!( - target: "magnotia_startup", + target: "lumotia_startup", "Whisper model pre-warmed successfully" ), Ok(Err(e)) => tracing::warn!( - target: "magnotia_startup", + target: "lumotia_startup", error = %e, "Whisper pre-warm failed" ), Err(e) => tracing::error!( - target: "magnotia_startup", + target: "lumotia_startup", error = %e, "Whisper pre-warm task panicked" ), diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 50c9ce7..8caf839 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -123,7 +123,7 @@ fn warn_if_x11_env_unset_on_wayland() { let warn_if_unset = |key: &str, value: &str, why: &str| { if std::env::var_os(key).is_none() { tracing::warn!( - target: "magnotia_startup", + target: "lumotia_startup", key, expected = value, why, @@ -162,7 +162,7 @@ fn init_tracing() { TRACING_INIT.call_once(|| { let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| { EnvFilter::new( - "warn,magnotia=info,lumotia_lib=info,lumotia_audio=info,magnotia_startup=info,lumotia_hotkey=info,lumotia_ai_formatting=info", + "warn,lumotia=info,lumotia_lib=info,lumotia_core=info,lumotia_audio=info,lumotia_hotkey=info,lumotia_ai_formatting=info,lumotia_llm=info,lumotia_storage=info,lumotia_transcription=info,lumotia_startup=info", ) }); @@ -227,7 +227,7 @@ pub fn run() { let db = init_db(&db_path) .await .map_err(|e| Box::new(e) as Box)?; - tracing::info!(target: "magnotia_startup", elapsed_ms = t0.elapsed().as_millis(), "DB init complete"); + tracing::info!(target: "lumotia_startup", elapsed_ms = t0.elapsed().as_millis(), "DB init complete"); // Prune old `error_log` rows so the table doesn't grow unbounded // across months of dogfooding. Best-effort — a prune failure is @@ -235,20 +235,20 @@ pub fn run() { let t_prune = Instant::now(); match prune_error_log(&db, ERROR_LOG_RETENTION_DAYS).await { Ok(n) if n > 0 => tracing::info!( - target: "magnotia_startup", + target: "lumotia_startup", rows_removed = n, retention_days = ERROR_LOG_RETENTION_DAYS, elapsed_ms = t_prune.elapsed().as_millis(), "error log prune complete" ), Ok(_) => {} - Err(e) => tracing::warn!(target: "magnotia_startup", error = %e, "error log prune failed"), + Err(e) => tracing::warn!(target: "lumotia_startup", error = %e, "error log prune failed"), } // Load saved preferences for webview injection. let t1 = Instant::now(); let prefs_json = get_setting(&db, "magnotia_preferences").await.unwrap_or(None); - tracing::info!(target: "magnotia_startup", elapsed_ms = t1.elapsed().as_millis(), "preferences load complete"); + tracing::info!(target: "lumotia_startup", elapsed_ms = t1.elapsed().as_millis(), "preferences load complete"); let init_script = build_preferences_script(prefs_json); Ok::<_, Box>((db, init_script)) @@ -286,7 +286,7 @@ pub fn run() { } tracing::warn!( - target: "magnotia_startup", + target: "lumotia_startup", "Linux WebKitGTK microphone permission requests are auto-granted for audio-only capture; other permission classes remain denied" ); @@ -321,7 +321,7 @@ pub fn run() { // silently denies) instead of auto-granting, // which is degraded but recoverable. tracing::warn!( - target: "magnotia_startup", + target: "lumotia_startup", error = %e, "failed to configure webview media permissions" ); @@ -366,7 +366,7 @@ pub fn run() { #[cfg(not(target_os = "android"))] if let Err(e) = tray::setup(app) { - tracing::warn!(target: "magnotia_startup", error = %e, "failed to setup tray"); + tracing::warn!(target: "lumotia_startup", error = %e, "failed to setup tray"); } Ok(()) From 86f83b7a450c5546306a4b86044067c9502752a2 Mon Sep 17 00:00:00 2001 From: Jake Date: Wed, 13 May 2026 09:21:45 +0100 Subject: [PATCH 08/82] =?UTF-8?q?agent:=20lumotia-rebrand=20=E2=80=94=20da?= =?UTF-8?q?ta=20dir=20migration=20shim=20+=20paths.rs=20rename?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 5 of the rebrand cascade per locked decision D1 (migrate in place). crates/core/src/paths.rs: - Hardcoded subdir strings renamed: magnotia/Magnotia -> lumotia/Lumotia across all four OS branches (Linux XDG + dot-legacy, macOS Application Support, Windows LOCALAPPDATA, fallback dot-dir). - Database filename: magnotia.db -> lumotia.db. - Test path fixtures renamed: /tmp/magnotia-test -> /tmp/lumotia-test. - New MigrationStatus enum (Migrated / TargetAlreadyExists / NoLegacyFound). - New migrate_legacy_data_dir() that probes the platform-correct legacy magnotia path, renames it to the lumotia equivalent via fs::rename, and also renames magnotia.db -> lumotia.db inside if found. Idempotent: safe to call on every boot. Refuses to overwrite an existing lumotia dir to protect user data. - Four new unit tests covering all branches via the test-friendly migrate_legacy_data_dir_inner that takes an explicit legacy path. crates/storage/src/database.rs: - New migrate_legacy_setting_keys(pool) that renames any settings rows with key matching magnotia_* to lumotia_*. Single SQL UPDATE with NOT EXISTS guard so it leaves rows alone if a lumotia_ row already exists. Re-exported from lib.rs. src-tauri/src/lib.rs: - Calls migrate_legacy_data_dir(&app_data_dir()) at the start of setup() BEFORE database_path() resolves the now-renamed dir. Logs migration outcome to lumotia_startup tracing target. - Calls migrate_legacy_setting_keys(&db) immediately after init_db returns. Logs only when rows are actually renamed. src-tauri/src/{lib,commands/{diagnostics,rituals,transcripts}}.rs + crates/storage/src/database.rs: - All in-code references to magnotia_preferences, magnotia_history (in comments), and magnotia_morning_triage_last_shown renamed to lumotia_*. cargo build --workspace passes. cargo test --workspace: 334 pass, 0 fail (up from 330; +4 paths::tests). Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/core/src/paths.rs | 269 ++++++++++++++++++++++++-- crates/storage/src/database.rs | 27 ++- crates/storage/src/lib.rs | 5 +- src-tauri/src/commands/diagnostics.rs | 2 +- src-tauri/src/commands/rituals.rs | 4 +- src-tauri/src/commands/transcripts.rs | 2 +- src-tauri/src/lib.rs | 52 ++++- 7 files changed, 335 insertions(+), 26 deletions(-) diff --git a/crates/core/src/paths.rs b/crates/core/src/paths.rs index 2f0ab1f..7d27a3f 100644 --- a/crates/core/src/paths.rs +++ b/crates/core/src/paths.rs @@ -1,4 +1,4 @@ -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use crate::types::ModelId; @@ -19,7 +19,7 @@ impl AppPaths { } pub fn database_path(&self) -> PathBuf { - self.app_data_dir.join("magnotia.db") + self.app_data_dir.join("lumotia.db") } pub fn recordings_dir(&self) -> PathBuf { @@ -67,7 +67,7 @@ fn resolve_app_data_dir() -> PathBuf { #[cfg(target_os = "windows")] { let local_app_data = std::env::var("LOCALAPPDATA").unwrap_or_else(|_| ".".to_string()); - return PathBuf::from(local_app_data).join("magnotia"); + return PathBuf::from(local_app_data).join("lumotia"); } #[cfg(target_os = "macos")] @@ -76,56 +76,293 @@ fn resolve_app_data_dir() -> PathBuf { return PathBuf::from(home) .join("Library") .join("Application Support") - .join("Magnotia"); + .join("Lumotia"); } #[cfg(target_os = "linux")] { let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()); - let legacy = PathBuf::from(&home).join(".magnotia"); - if legacy.exists() { - return legacy; + let legacy_dot = PathBuf::from(&home).join(".lumotia"); + if legacy_dot.exists() { + return legacy_dot; } if let Ok(xdg) = std::env::var("XDG_DATA_HOME") { if !xdg.is_empty() { - return PathBuf::from(xdg).join("magnotia"); + return PathBuf::from(xdg).join("lumotia"); } } PathBuf::from(home) .join(".local") .join("share") - .join("magnotia") + .join("lumotia") } #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] { let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string()); - PathBuf::from(home).join(".magnotia") + PathBuf::from(home).join(".lumotia") } } +/// Outcome of attempting to migrate an existing magnotia-era data directory +/// to its lumotia equivalent on first launch after the rebrand. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MigrationStatus { + /// Renamed legacy dir to the new path. Includes optional db-file rename + /// if `magnotia.db` was found inside. + Migrated { + from: PathBuf, + to: PathBuf, + renamed_db: bool, + }, + /// New path already exists. Did not touch legacy (if any) to avoid + /// destroying user data on the new path. + TargetAlreadyExists { target: PathBuf }, + /// No legacy data dir on disk. Nothing to do. + NoLegacyFound, +} + +/// Probe the legacy magnotia data dir paths on the current platform and +/// return the first one that exists, if any. Mirrors the search order +/// inside `resolve_app_data_dir()` but for the old name. +fn legacy_magnotia_data_dir() -> Option { + #[cfg(target_os = "windows")] + { + let local_app_data = std::env::var("LOCALAPPDATA").ok()?; + let candidate = PathBuf::from(local_app_data).join("magnotia"); + return candidate.exists().then_some(candidate); + } + + #[cfg(target_os = "macos")] + { + let home = std::env::var("HOME").ok()?; + let candidate = PathBuf::from(home) + .join("Library") + .join("Application Support") + .join("Magnotia"); + return candidate.exists().then_some(candidate); + } + + #[cfg(target_os = "linux")] + { + let home = std::env::var("HOME").ok()?; + let dot_legacy = PathBuf::from(&home).join(".magnotia"); + if dot_legacy.exists() { + return Some(dot_legacy); + } + if let Ok(xdg) = std::env::var("XDG_DATA_HOME") { + if !xdg.is_empty() { + let xdg_legacy = PathBuf::from(xdg).join("magnotia"); + if xdg_legacy.exists() { + return Some(xdg_legacy); + } + } + } + let xdg_default = PathBuf::from(home) + .join(".local") + .join("share") + .join("magnotia"); + xdg_default.exists().then_some(xdg_default) + } + + #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] + { + let home = std::env::var("HOME").ok()?; + let candidate = PathBuf::from(home).join(".magnotia"); + candidate.exists().then_some(candidate) + } +} + +/// Migrate a legacy magnotia data directory to the new lumotia path on +/// first launch. Idempotent: safe to call on every boot. +/// +/// Rules: +/// * If the new path already exists, do nothing and return +/// `TargetAlreadyExists`. We do not destroy lumotia data, even if a +/// stale legacy dir is also present. +/// * If only the legacy path exists, rename it to the new path and +/// rename `magnotia.db` -> `lumotia.db` inside it if found. +/// * If neither exists, return `NoLegacyFound` — the first launch on a +/// clean system will create the new path itself. +pub fn migrate_legacy_data_dir(new_path: &Path) -> Result { + migrate_legacy_data_dir_inner(new_path, legacy_magnotia_data_dir()) +} + +/// Test-friendly inner shape: takes the legacy path explicitly so tests +/// don't depend on platform-specific HOME / LOCALAPPDATA env vars. +fn migrate_legacy_data_dir_inner( + new_path: &Path, + legacy: Option, +) -> Result { + if new_path.exists() { + return Ok(MigrationStatus::TargetAlreadyExists { + target: new_path.to_path_buf(), + }); + } + + let Some(from) = legacy else { + return Ok(MigrationStatus::NoLegacyFound); + }; + + if let Some(parent) = new_path.parent() { + std::fs::create_dir_all(parent)?; + } + std::fs::rename(&from, new_path)?; + + let renamed_db = rename_db_file_if_present(new_path)?; + + Ok(MigrationStatus::Migrated { + from, + to: new_path.to_path_buf(), + renamed_db, + }) +} + +fn rename_db_file_if_present(dir: &Path) -> Result { + let legacy_db = dir.join("magnotia.db"); + if !legacy_db.exists() { + return Ok(false); + } + let new_db = dir.join("lumotia.db"); + if new_db.exists() { + return Ok(false); + } + std::fs::rename(&legacy_db, &new_db)?; + Ok(true) +} + #[cfg(test)] mod tests { - use super::AppPaths; + use super::*; use crate::types::ModelId; - use std::path::PathBuf; #[test] fn derives_all_paths_from_one_base() { let paths = AppPaths { - app_data_dir: PathBuf::from("/tmp/magnotia-test"), + app_data_dir: PathBuf::from("/tmp/lumotia-test"), }; assert_eq!( paths.database_path(), - PathBuf::from("/tmp/magnotia-test/magnotia.db") + PathBuf::from("/tmp/lumotia-test/lumotia.db") ); assert_eq!( paths.speech_model_dir(&ModelId::new("whisper-base-en")), - PathBuf::from("/tmp/magnotia-test/models/whisper-base-en") + PathBuf::from("/tmp/lumotia-test/models/whisper-base-en") ); assert_eq!( paths.llm_models_dir(), - PathBuf::from("/tmp/magnotia-test/models/llm") + PathBuf::from("/tmp/lumotia-test/models/llm") ); } + + fn unique_tmp(base: &str) -> PathBuf { + let pid = std::process::id(); + let nanos = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_nanos()) + .unwrap_or(0); + std::env::temp_dir().join(format!("lumotia-paths-test-{base}-{pid}-{nanos}")) + } + + #[test] + fn migrate_with_legacy_present_renames_dir_and_db() { + let root = unique_tmp("legacy-present"); + let legacy = root.join("magnotia"); + let new_path = root.join("lumotia"); + std::fs::create_dir_all(&legacy).unwrap(); + std::fs::write(legacy.join("magnotia.db"), b"sqlite-stub").unwrap(); + std::fs::write(legacy.join("recordings.placeholder"), b"x").unwrap(); + + let result = + migrate_legacy_data_dir_inner(&new_path, Some(legacy.clone())).expect("migrate ok"); + + match result { + MigrationStatus::Migrated { + from, + to, + renamed_db, + } => { + assert_eq!(from, legacy); + assert_eq!(to, new_path); + assert!(renamed_db, "expected db file to be renamed"); + } + other => panic!("expected Migrated, got {other:?}"), + } + assert!(!legacy.exists(), "legacy dir should be gone"); + assert!(new_path.exists(), "new dir should exist"); + assert!(new_path.join("lumotia.db").exists(), "db at new name"); + assert!(!new_path.join("magnotia.db").exists(), "old db gone"); + assert!( + new_path.join("recordings.placeholder").exists(), + "other files preserved" + ); + + std::fs::remove_dir_all(&root).ok(); + } + + #[test] + fn migrate_with_both_present_returns_target_exists_and_preserves_lumotia() { + let root = unique_tmp("both-present"); + let legacy = root.join("magnotia"); + let new_path = root.join("lumotia"); + std::fs::create_dir_all(&legacy).unwrap(); + std::fs::create_dir_all(&new_path).unwrap(); + std::fs::write(new_path.join("lumotia.db"), b"new-data").unwrap(); + std::fs::write(legacy.join("magnotia.db"), b"legacy-data").unwrap(); + + let result = + migrate_legacy_data_dir_inner(&new_path, Some(legacy.clone())).expect("migrate ok"); + + assert_eq!( + result, + MigrationStatus::TargetAlreadyExists { + target: new_path.clone() + } + ); + assert!(legacy.exists(), "legacy dir preserved"); + assert!(new_path.exists(), "new dir preserved"); + assert_eq!( + std::fs::read(new_path.join("lumotia.db")).unwrap(), + b"new-data".to_vec(), + "lumotia.db not overwritten" + ); + + std::fs::remove_dir_all(&root).ok(); + } + + #[test] + fn migrate_with_neither_present_returns_no_legacy() { + let root = unique_tmp("neither-present"); + let new_path = root.join("lumotia"); + + let result = migrate_legacy_data_dir_inner(&new_path, None).expect("migrate ok"); + + assert_eq!(result, MigrationStatus::NoLegacyFound); + assert!(!new_path.exists(), "no dir created"); + + std::fs::remove_dir_all(&root).ok(); + } + + #[test] + fn migrate_with_legacy_present_but_no_db_inside() { + let root = unique_tmp("legacy-no-db"); + let legacy = root.join("magnotia"); + let new_path = root.join("lumotia"); + std::fs::create_dir_all(&legacy).unwrap(); + std::fs::write(legacy.join("recordings.placeholder"), b"x").unwrap(); + + let result = + migrate_legacy_data_dir_inner(&new_path, Some(legacy.clone())).expect("migrate ok"); + + match result { + MigrationStatus::Migrated { renamed_db, .. } => { + assert!(!renamed_db, "no db to rename"); + } + other => panic!("expected Migrated, got {other:?}"), + } + assert!(new_path.exists()); + assert!(new_path.join("recordings.placeholder").exists()); + + std::fs::remove_dir_all(&root).ok(); + } } diff --git a/crates/storage/src/database.rs b/crates/storage/src/database.rs index 5aa77f3..7a9cfcd 100644 --- a/crates/storage/src/database.rs +++ b/crates/storage/src/database.rs @@ -905,6 +905,31 @@ pub async fn get_setting(pool: &SqlitePool, key: &str) -> Result> Ok(row.map(|r| r.get("value"))) } +/// One-shot key rename for settings rows carried over from the magnotia era +/// (`magnotia_preferences`, `magnotia_morning_triage_last_shown`, etc.). +/// +/// Idempotent: rows already on the new key are left alone; rows on the old +/// key are renamed only if the new key does not already exist. Returns the +/// number of rows actually renamed so the caller can log on first launch. +pub async fn migrate_legacy_setting_keys(pool: &SqlitePool) -> Result { + let result = sqlx::query( + "UPDATE settings \ + SET key = 'lumotia_' || substr(key, length('magnotia_') + 1) \ + WHERE key LIKE 'magnotia_%' \ + AND NOT EXISTS (\ + SELECT 1 FROM settings AS dst \ + WHERE dst.key = 'lumotia_' || substr(settings.key, length('magnotia_') + 1)\ + )", + ) + .execute(pool) + .await + .map_err(|source| Error::Query { + operation: "migrate_legacy_setting_keys".into(), + source, + })?; + Ok(result.rows_affected()) +} + // --- Row types --- #[derive(Debug, Clone)] @@ -944,7 +969,7 @@ pub struct TranscriptRow { pub anti_hallucination: bool, pub created_at: String, // Task 2.5 — transcripts_meta (migration v5). Persists the UI metadata - // that previously lived in the removed localStorage `magnotia_history` cache. + // that previously lived in the removed localStorage `lumotia_history` cache. pub starred: bool, pub manual_tags: String, pub template: String, diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index 5320b73..e22fe02 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -17,8 +17,9 @@ pub use database::{ insert_transcript, list_feedback_examples, list_implementation_rules, list_profile_terms, list_profiles, list_recent_completions, list_recent_errors, list_subtasks, list_tasks, list_transcripts, list_transcripts_paged, log_error, mark_implementation_rule_fired, - prune_error_log, record_feedback, search_transcripts, set_implementation_rule_enabled, - set_setting, set_task_energy, uncomplete_task, update_profile, update_task, update_transcript, + migrate_legacy_setting_keys, prune_error_log, record_feedback, search_transcripts, + set_implementation_rule_enabled, set_setting, set_task_energy, uncomplete_task, update_profile, + update_task, update_transcript, update_transcript_meta, DailyCompletionCount, ErrorLogRow, FeedbackRow, FeedbackTargetType, ImplementationRuleRow, InsertTranscriptParams, ProfileRow, ProfileTermRow, RecordFeedbackParams, TaskRow, TranscriptRow, diff --git a/src-tauri/src/commands/diagnostics.rs b/src-tauri/src/commands/diagnostics.rs index 3b24f01..89601fd 100644 --- a/src-tauri/src/commands/diagnostics.rs +++ b/src-tauri/src/commands/diagnostics.rs @@ -335,7 +335,7 @@ async fn generate_diagnostic_report_inner( if opts.include_settings { out.push_str("## Settings (sanitised)\n\n"); - match lumotia_storage::get_setting(&state.db, "magnotia_preferences").await { + match lumotia_storage::get_setting(&state.db, "lumotia_preferences").await { Ok(Some(json)) => { out.push_str("```json\n"); out.push_str(&sanitise_preferences_json(&json)); diff --git a/src-tauri/src/commands/rituals.rs b/src-tauri/src/commands/rituals.rs index e9dab49..6de35af 100644 --- a/src-tauri/src/commands/rituals.rs +++ b/src-tauri/src/commands/rituals.rs @@ -8,13 +8,13 @@ //! //! Stored under the existing SQLite settings table via //! `lumotia_storage::{get_setting, set_setting}` — same bag as -//! `magnotia_preferences`. +//! `lumotia_preferences`. use lumotia_storage::{get_setting, set_setting}; use crate::AppState; -const LAST_TRIAGE_KEY: &str = "magnotia_morning_triage_last_shown"; +const LAST_TRIAGE_KEY: &str = "lumotia_morning_triage_last_shown"; /// Returns the YYYY-MM-DD date string stored on the last successful /// morning triage dismissal, or `None` if the user has never been diff --git a/src-tauri/src/commands/transcripts.rs b/src-tauri/src/commands/transcripts.rs index 271996f..2107a8d 100644 --- a/src-tauri/src/commands/transcripts.rs +++ b/src-tauri/src/commands/transcripts.rs @@ -28,7 +28,7 @@ use crate::AppState; /// /// Task 2.5 — `starred`, `manualTags`, `template`, `language`, `segmentsJson` /// were added to back the viewer metadata that previously lived only in the -/// removed `magnotia_history` localStorage cache. +/// removed `lumotia_history` localStorage cache. #[derive(Debug, Clone, Serialize)] #[serde(rename_all = "camelCase")] pub struct TranscriptDto { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 8caf839..edcd134 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -12,9 +12,13 @@ use sqlx::SqlitePool; use tauri::Manager; use tracing_subscriber::EnvFilter; +use lumotia_core::paths::{migrate_legacy_data_dir, MigrationStatus}; use lumotia_core::types::EngineName; use lumotia_llm::LlmEngine; -use lumotia_storage::{database_path, get_setting, init as init_db, prune_error_log, set_setting}; +use lumotia_storage::{ + app_data_dir, database_path, get_setting, init as init_db, migrate_legacy_setting_keys, + prune_error_log, set_setting, +}; /// How long to retain `error_log` rows. Pruned once on startup. /// 90 days is long enough to triage "this happened a few weeks ago" @@ -97,7 +101,7 @@ async fn save_preferences( state: tauri::State<'_, AppState>, preferences: String, ) -> Result<(), String> { - set_setting(&state.db, "magnotia_preferences", &preferences) + set_setting(&state.db, "lumotia_preferences", &preferences) .await .map_err(|e| e.to_string()) } @@ -220,6 +224,29 @@ pub fn run() { builder .setup(|app| { + // One-shot legacy data-dir migration: rename ~/.local/share/magnotia + // (and macOS/Windows equivalents) to the lumotia path on first + // launch after the rebrand. Safe to call every boot — idempotent. + let t_migrate = Instant::now(); + let new_data_dir = app_data_dir(); + match migrate_legacy_data_dir(&new_data_dir) { + Ok(MigrationStatus::Migrated { from, to, renamed_db }) => tracing::info!( + target: "lumotia_startup", + elapsed_ms = t_migrate.elapsed().as_millis(), + from = %from.display(), + to = %to.display(), + renamed_db, + "migrated legacy magnotia data dir to lumotia" + ), + Ok(MigrationStatus::TargetAlreadyExists { .. }) => {} + Ok(MigrationStatus::NoLegacyFound) => {} + Err(e) => tracing::warn!( + target: "lumotia_startup", + error = %e, + "legacy data dir migration failed — continuing with new path" + ), + } + // Initialise database and startup settings in one runtime entry. let db_path = database_path(); let (db, init_script) = tauri::async_runtime::block_on(async { @@ -229,6 +256,25 @@ pub fn run() { .map_err(|e| Box::new(e) as Box)?; tracing::info!(target: "lumotia_startup", elapsed_ms = t0.elapsed().as_millis(), "DB init complete"); + // One-shot settings-key migration: rename any leftover + // `magnotia_*` rows from the magnotia era to `lumotia_*`. + // Idempotent; logs only when rows are actually renamed. + let t_keys = Instant::now(); + match migrate_legacy_setting_keys(&db).await { + Ok(0) => {} + Ok(n) => tracing::info!( + target: "lumotia_startup", + rows_renamed = n, + elapsed_ms = t_keys.elapsed().as_millis(), + "renamed legacy magnotia_* settings keys to lumotia_*" + ), + Err(e) => tracing::warn!( + target: "lumotia_startup", + error = %e, + "settings-key migration failed — continuing" + ), + } + // Prune old `error_log` rows so the table doesn't grow unbounded // across months of dogfooding. Best-effort — a prune failure is // not worth blocking startup over. @@ -247,7 +293,7 @@ pub fn run() { // Load saved preferences for webview injection. let t1 = Instant::now(); - let prefs_json = get_setting(&db, "magnotia_preferences").await.unwrap_or(None); + let prefs_json = get_setting(&db, "lumotia_preferences").await.unwrap_or(None); tracing::info!(target: "lumotia_startup", elapsed_ms = t1.elapsed().as_millis(), "preferences load complete"); let init_script = build_preferences_script(prefs_json); From 9336286e3c352b003c8cdb311d15a950a667dd1c Mon Sep 17 00:00:00 2001 From: Jake Date: Wed, 13 May 2026 09:43:45 +0100 Subject: [PATCH 09/82] =?UTF-8?q?agent:=20lumotia-rebrand=20=E2=80=94=20fi?= =?UTF-8?q?x=20QC=20blockers=20for=20phase=205?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 5 QC found two blockers + four advisories. All addressed: B1 (FATAL) — Migration error now aborts startup instead of silently continuing past it. Without this fix a transient EACCES / EXDEV / ENOSPC would log a warning, init_db would create a fresh empty lumotia dir, and the user would appear to lose their transcripts. B2 (FATAL) — Linux dot-home vs XDG mismatch. The old probe returned ~/.magnotia as legacy but the caller passed app_data_dir() as the new path — which could be $XDG_DATA_HOME/lumotia. fs::rename across filesystems would EXDEV-fail; even when it succeeded the user's storage convention silently changed. Refactored: legacy_and_target_paths() returns the (legacy, target) pair together. Dot-home legacy lands in ~/.lumotia; XDG-set legacy lands in $XDG_DATA_HOME/lumotia; XDG-default legacy lands in ~/.local/share/lumotia. macOS / Windows / non-tier-1 unchanged. migrate_legacy_data_dir() now takes no argument; src-tauri caller updated. A1 — Removed dead new_db.exists() check inside rename_db_file_if_present (unreachable: rename_db is called only AFTER the legacy dir was just renamed to the new path, so a stray lumotia.db there is impossible). A2 — Added 4 unit tests for migrate_legacy_setting_keys: lone-magnotia, both-present (orphan delete), no-magnotia, idempotent. A3 — migrate_legacy_setting_keys now returns (renamed, orphans_deleted). When both keys exist, the legacy magnotia row is DELETED in the same transaction (lumotia row is authoritative). A4 — Added dot-home convention regression test in paths::tests. cargo build --workspace passes. cargo test --workspace: 339 pass, 0 fail (up from 334 in the original Phase 5 commit; +5 new tests). Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/core/src/paths.rs | 167 +++++++++++++++++++-------------- crates/storage/src/database.rs | 136 +++++++++++++++++++++++++-- src-tauri/src/lib.rs | 39 ++++---- 3 files changed, 247 insertions(+), 95 deletions(-) diff --git a/crates/core/src/paths.rs b/crates/core/src/paths.rs index 7d27a3f..2cbc919 100644 --- a/crates/core/src/paths.rs +++ b/crates/core/src/paths.rs @@ -122,25 +122,27 @@ pub enum MigrationStatus { NoLegacyFound, } -/// Probe the legacy magnotia data dir paths on the current platform and -/// return the first one that exists, if any. Mirrors the search order -/// inside `resolve_app_data_dir()` but for the old name. -fn legacy_magnotia_data_dir() -> Option { +/// Probe the legacy magnotia data dir paths on the current platform. +/// Returns the matched legacy path AND its convention-preserving lumotia +/// target so the migration lands the same kind of dir it found (dot-home +/// stays dot-home, XDG stays XDG, macOS Application Support stays the +/// same). +fn legacy_and_target_paths() -> Option<(PathBuf, PathBuf)> { #[cfg(target_os = "windows")] { let local_app_data = std::env::var("LOCALAPPDATA").ok()?; - let candidate = PathBuf::from(local_app_data).join("magnotia"); - return candidate.exists().then_some(candidate); + let legacy = PathBuf::from(&local_app_data).join("magnotia"); + let target = PathBuf::from(local_app_data).join("lumotia"); + return legacy.exists().then_some((legacy, target)); } #[cfg(target_os = "macos")] { let home = std::env::var("HOME").ok()?; - let candidate = PathBuf::from(home) - .join("Library") - .join("Application Support") - .join("Magnotia"); - return candidate.exists().then_some(candidate); + let app_support = PathBuf::from(home).join("Library").join("Application Support"); + let legacy = app_support.join("Magnotia"); + let target = app_support.join("Lumotia"); + return legacy.exists().then_some((legacy, target)); } #[cfg(target_os = "linux")] @@ -148,72 +150,84 @@ fn legacy_magnotia_data_dir() -> Option { let home = std::env::var("HOME").ok()?; let dot_legacy = PathBuf::from(&home).join(".magnotia"); if dot_legacy.exists() { - return Some(dot_legacy); + return Some((dot_legacy, PathBuf::from(&home).join(".lumotia"))); } if let Ok(xdg) = std::env::var("XDG_DATA_HOME") { if !xdg.is_empty() { - let xdg_legacy = PathBuf::from(xdg).join("magnotia"); + let xdg_legacy = PathBuf::from(&xdg).join("magnotia"); if xdg_legacy.exists() { - return Some(xdg_legacy); + return Some((xdg_legacy, PathBuf::from(xdg).join("lumotia"))); } } } - let xdg_default = PathBuf::from(home) + let xdg_default_legacy = PathBuf::from(&home) .join(".local") .join("share") .join("magnotia"); - xdg_default.exists().then_some(xdg_default) + if xdg_default_legacy.exists() { + let xdg_default_target = PathBuf::from(home) + .join(".local") + .join("share") + .join("lumotia"); + return Some((xdg_default_legacy, xdg_default_target)); + } + None } #[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))] { let home = std::env::var("HOME").ok()?; - let candidate = PathBuf::from(home).join(".magnotia"); - candidate.exists().then_some(candidate) + let legacy = PathBuf::from(&home).join(".magnotia"); + let target = PathBuf::from(home).join(".lumotia"); + legacy.exists().then_some((legacy, target)) } } -/// Migrate a legacy magnotia data directory to the new lumotia path on -/// first launch. Idempotent: safe to call on every boot. +/// Migrate a legacy magnotia data directory to its convention-preserving +/// lumotia equivalent on first launch. Idempotent: safe to call on every +/// boot. /// /// Rules: -/// * If the new path already exists, do nothing and return +/// * If the resolved target already exists, do nothing and return /// `TargetAlreadyExists`. We do not destroy lumotia data, even if a /// stale legacy dir is also present. -/// * If only the legacy path exists, rename it to the new path and -/// rename `magnotia.db` -> `lumotia.db` inside it if found. +/// * If only the legacy path exists, rename it to the matching lumotia +/// target (same parent dir / same convention) and rename +/// `magnotia.db` -> `lumotia.db` inside it if found. /// * If neither exists, return `NoLegacyFound` — the first launch on a /// clean system will create the new path itself. -pub fn migrate_legacy_data_dir(new_path: &Path) -> Result { - migrate_legacy_data_dir_inner(new_path, legacy_magnotia_data_dir()) +/// +/// Callers should treat `Err` as a hard startup failure; silently +/// continuing past a migration error orphans user data behind a fresh +/// empty lumotia dir. +pub fn migrate_legacy_data_dir() -> Result { + migrate_legacy_data_dir_inner(legacy_and_target_paths()) } -/// Test-friendly inner shape: takes the legacy path explicitly so tests -/// don't depend on platform-specific HOME / LOCALAPPDATA env vars. +/// Test-friendly inner shape: takes the (legacy, target) pair explicitly +/// so tests don't depend on platform-specific HOME / LOCALAPPDATA / XDG +/// env vars. fn migrate_legacy_data_dir_inner( - new_path: &Path, - legacy: Option, + pair: Option<(PathBuf, PathBuf)>, ) -> Result { - if new_path.exists() { - return Ok(MigrationStatus::TargetAlreadyExists { - target: new_path.to_path_buf(), - }); - } - - let Some(from) = legacy else { + let Some((from, to)) = pair else { return Ok(MigrationStatus::NoLegacyFound); }; - if let Some(parent) = new_path.parent() { + if to.exists() { + return Ok(MigrationStatus::TargetAlreadyExists { target: to }); + } + + if let Some(parent) = to.parent() { std::fs::create_dir_all(parent)?; } - std::fs::rename(&from, new_path)?; + std::fs::rename(&from, &to)?; - let renamed_db = rename_db_file_if_present(new_path)?; + let renamed_db = rename_db_file_if_present(&to)?; Ok(MigrationStatus::Migrated { from, - to: new_path.to_path_buf(), + to, renamed_db, }) } @@ -224,9 +238,6 @@ fn rename_db_file_if_present(dir: &Path) -> Result { return Ok(false); } let new_db = dir.join("lumotia.db"); - if new_db.exists() { - return Ok(false); - } std::fs::rename(&legacy_db, &new_db)?; Ok(true) } @@ -268,13 +279,13 @@ mod tests { fn migrate_with_legacy_present_renames_dir_and_db() { let root = unique_tmp("legacy-present"); let legacy = root.join("magnotia"); - let new_path = root.join("lumotia"); + let target = root.join("lumotia"); std::fs::create_dir_all(&legacy).unwrap(); std::fs::write(legacy.join("magnotia.db"), b"sqlite-stub").unwrap(); std::fs::write(legacy.join("recordings.placeholder"), b"x").unwrap(); - let result = - migrate_legacy_data_dir_inner(&new_path, Some(legacy.clone())).expect("migrate ok"); + let result = migrate_legacy_data_dir_inner(Some((legacy.clone(), target.clone()))) + .expect("migrate ok"); match result { MigrationStatus::Migrated { @@ -283,17 +294,17 @@ mod tests { renamed_db, } => { assert_eq!(from, legacy); - assert_eq!(to, new_path); + assert_eq!(to, target); assert!(renamed_db, "expected db file to be renamed"); } other => panic!("expected Migrated, got {other:?}"), } assert!(!legacy.exists(), "legacy dir should be gone"); - assert!(new_path.exists(), "new dir should exist"); - assert!(new_path.join("lumotia.db").exists(), "db at new name"); - assert!(!new_path.join("magnotia.db").exists(), "old db gone"); + assert!(target.exists(), "new dir should exist"); + assert!(target.join("lumotia.db").exists(), "db at new name"); + assert!(!target.join("magnotia.db").exists(), "old db gone"); assert!( - new_path.join("recordings.placeholder").exists(), + target.join("recordings.placeholder").exists(), "other files preserved" ); @@ -304,25 +315,25 @@ mod tests { fn migrate_with_both_present_returns_target_exists_and_preserves_lumotia() { let root = unique_tmp("both-present"); let legacy = root.join("magnotia"); - let new_path = root.join("lumotia"); + let target = root.join("lumotia"); std::fs::create_dir_all(&legacy).unwrap(); - std::fs::create_dir_all(&new_path).unwrap(); - std::fs::write(new_path.join("lumotia.db"), b"new-data").unwrap(); + std::fs::create_dir_all(&target).unwrap(); + std::fs::write(target.join("lumotia.db"), b"new-data").unwrap(); std::fs::write(legacy.join("magnotia.db"), b"legacy-data").unwrap(); - let result = - migrate_legacy_data_dir_inner(&new_path, Some(legacy.clone())).expect("migrate ok"); + let result = migrate_legacy_data_dir_inner(Some((legacy.clone(), target.clone()))) + .expect("migrate ok"); assert_eq!( result, MigrationStatus::TargetAlreadyExists { - target: new_path.clone() + target: target.clone() } ); assert!(legacy.exists(), "legacy dir preserved"); - assert!(new_path.exists(), "new dir preserved"); + assert!(target.exists(), "new dir preserved"); assert_eq!( - std::fs::read(new_path.join("lumotia.db")).unwrap(), + std::fs::read(target.join("lumotia.db")).unwrap(), b"new-data".to_vec(), "lumotia.db not overwritten" ); @@ -332,27 +343,21 @@ mod tests { #[test] fn migrate_with_neither_present_returns_no_legacy() { - let root = unique_tmp("neither-present"); - let new_path = root.join("lumotia"); - - let result = migrate_legacy_data_dir_inner(&new_path, None).expect("migrate ok"); + let result = migrate_legacy_data_dir_inner(None).expect("migrate ok"); assert_eq!(result, MigrationStatus::NoLegacyFound); - assert!(!new_path.exists(), "no dir created"); - - std::fs::remove_dir_all(&root).ok(); } #[test] fn migrate_with_legacy_present_but_no_db_inside() { let root = unique_tmp("legacy-no-db"); let legacy = root.join("magnotia"); - let new_path = root.join("lumotia"); + let target = root.join("lumotia"); std::fs::create_dir_all(&legacy).unwrap(); std::fs::write(legacy.join("recordings.placeholder"), b"x").unwrap(); - let result = - migrate_legacy_data_dir_inner(&new_path, Some(legacy.clone())).expect("migrate ok"); + let result = migrate_legacy_data_dir_inner(Some((legacy.clone(), target.clone()))) + .expect("migrate ok"); match result { MigrationStatus::Migrated { renamed_db, .. } => { @@ -360,8 +365,28 @@ mod tests { } other => panic!("expected Migrated, got {other:?}"), } - assert!(new_path.exists()); - assert!(new_path.join("recordings.placeholder").exists()); + assert!(target.exists()); + assert!(target.join("recordings.placeholder").exists()); + + std::fs::remove_dir_all(&root).ok(); + } + + #[test] + fn migrate_preserves_dot_home_to_dot_home_convention() { + // Regression for B2 from Phase 5 QC: dot-home legacy must land in + // dot-home target, not XDG target. + let root = unique_tmp("convention"); + let legacy = root.join(".magnotia"); + let target = root.join(".lumotia"); + std::fs::create_dir_all(&legacy).unwrap(); + std::fs::write(legacy.join("magnotia.db"), b"data").unwrap(); + + let result = migrate_legacy_data_dir_inner(Some((legacy.clone(), target.clone()))) + .expect("migrate ok"); + + assert!(matches!(result, MigrationStatus::Migrated { .. })); + assert!(target.exists()); + assert!(target.join("lumotia.db").exists()); std::fs::remove_dir_all(&root).ok(); } diff --git a/crates/storage/src/database.rs b/crates/storage/src/database.rs index 7a9cfcd..d3d9fab 100644 --- a/crates/storage/src/database.rs +++ b/crates/storage/src/database.rs @@ -908,11 +908,18 @@ pub async fn get_setting(pool: &SqlitePool, key: &str) -> Result> /// One-shot key rename for settings rows carried over from the magnotia era /// (`magnotia_preferences`, `magnotia_morning_triage_last_shown`, etc.). /// -/// Idempotent: rows already on the new key are left alone; rows on the old -/// key are renamed only if the new key does not already exist. Returns the -/// number of rows actually renamed so the caller can log on first launch. -pub async fn migrate_legacy_setting_keys(pool: &SqlitePool) -> Result { - let result = sqlx::query( +/// Idempotent. Returns `(renamed, orphans_deleted)`: +/// * `renamed` — rows where only the magnotia key existed; the row's key +/// is updated to the lumotia equivalent. +/// * `orphans_deleted` — rows where BOTH keys existed; the lumotia row is +/// authoritative and the magnotia row is deleted to avoid silent debt. +pub async fn migrate_legacy_setting_keys(pool: &SqlitePool) -> Result<(u64, u64)> { + let mut tx = pool.begin().await.map_err(|source| Error::Query { + operation: "migrate_legacy_setting_keys:begin".into(), + source, + })?; + + let renamed = sqlx::query( "UPDATE settings \ SET key = 'lumotia_' || substr(key, length('magnotia_') + 1) \ WHERE key LIKE 'magnotia_%' \ @@ -921,13 +928,36 @@ pub async fn migrate_legacy_setting_keys(pool: &SqlitePool) -> Result { WHERE dst.key = 'lumotia_' || substr(settings.key, length('magnotia_') + 1)\ )", ) - .execute(pool) + .execute(&mut *tx) .await .map_err(|source| Error::Query { - operation: "migrate_legacy_setting_keys".into(), + operation: "migrate_legacy_setting_keys:rename".into(), + source, + })? + .rows_affected(); + + let deleted = sqlx::query( + "DELETE FROM settings \ + WHERE key LIKE 'magnotia_%' \ + AND EXISTS (\ + SELECT 1 FROM settings AS dst \ + WHERE dst.key = 'lumotia_' || substr(settings.key, length('magnotia_') + 1)\ + )", + ) + .execute(&mut *tx) + .await + .map_err(|source| Error::Query { + operation: "migrate_legacy_setting_keys:delete_orphans".into(), + source, + })? + .rows_affected(); + + tx.commit().await.map_err(|source| Error::Query { + operation: "migrate_legacy_setting_keys:commit".into(), source, })?; - Ok(result.rows_affected()) + + Ok((renamed, deleted)) } // --- Row types --- @@ -2740,4 +2770,94 @@ mod tests { .unwrap(); assert_eq!(remaining, vec!["recent".to_string()]); } + + #[tokio::test] + async fn migrate_legacy_setting_keys_renames_lone_magnotia_rows() { + let pool = test_pool().await; + set_setting(&pool, "magnotia_preferences", "blob-A") + .await + .unwrap(); + set_setting(&pool, "magnotia_morning_triage_last_shown", "2026-05-12") + .await + .unwrap(); + + let (renamed, deleted) = migrate_legacy_setting_keys(&pool).await.unwrap(); + + assert_eq!(renamed, 2); + assert_eq!(deleted, 0); + assert_eq!( + get_setting(&pool, "lumotia_preferences") + .await + .unwrap() + .as_deref(), + Some("blob-A") + ); + assert_eq!( + get_setting(&pool, "magnotia_preferences").await.unwrap(), + None + ); + assert_eq!( + get_setting(&pool, "lumotia_morning_triage_last_shown") + .await + .unwrap() + .as_deref(), + Some("2026-05-12") + ); + } + + #[tokio::test] + async fn migrate_legacy_setting_keys_deletes_orphans_when_both_present() { + let pool = test_pool().await; + set_setting(&pool, "magnotia_preferences", "legacy-blob") + .await + .unwrap(); + set_setting(&pool, "lumotia_preferences", "current-blob") + .await + .unwrap(); + + let (renamed, deleted) = migrate_legacy_setting_keys(&pool).await.unwrap(); + + assert_eq!(renamed, 0); + assert_eq!(deleted, 1); + assert_eq!( + get_setting(&pool, "lumotia_preferences") + .await + .unwrap() + .as_deref(), + Some("current-blob"), + "lumotia row preserved verbatim" + ); + assert_eq!( + get_setting(&pool, "magnotia_preferences").await.unwrap(), + None, + "orphan magnotia row deleted" + ); + } + + #[tokio::test] + async fn migrate_legacy_setting_keys_no_op_when_no_magnotia_rows() { + let pool = test_pool().await; + set_setting(&pool, "lumotia_preferences", "blob") + .await + .unwrap(); + + let (renamed, deleted) = migrate_legacy_setting_keys(&pool).await.unwrap(); + + assert_eq!(renamed, 0); + assert_eq!(deleted, 0); + } + + #[tokio::test] + async fn migrate_legacy_setting_keys_is_idempotent() { + let pool = test_pool().await; + set_setting(&pool, "magnotia_preferences", "blob") + .await + .unwrap(); + + let first = migrate_legacy_setting_keys(&pool).await.unwrap(); + let second = migrate_legacy_setting_keys(&pool).await.unwrap(); + + assert_eq!(first, (1, 0)); + assert_eq!(second, (0, 0)); + } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index edcd134..bd20846 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -16,8 +16,8 @@ use lumotia_core::paths::{migrate_legacy_data_dir, MigrationStatus}; use lumotia_core::types::EngineName; use lumotia_llm::LlmEngine; use lumotia_storage::{ - app_data_dir, database_path, get_setting, init as init_db, migrate_legacy_setting_keys, - prune_error_log, set_setting, + database_path, get_setting, init as init_db, migrate_legacy_setting_keys, prune_error_log, + set_setting, }; /// How long to retain `error_log` rows. Pruned once on startup. @@ -225,11 +225,13 @@ pub fn run() { builder .setup(|app| { // One-shot legacy data-dir migration: rename ~/.local/share/magnotia - // (and macOS/Windows equivalents) to the lumotia path on first - // launch after the rebrand. Safe to call every boot — idempotent. + // (and macOS/Windows equivalents) to the convention-preserving + // lumotia path on first launch after the rebrand. Idempotent — + // safe to call on every boot. A migration error is fatal: silently + // continuing past it would orphan the user's transcripts and + // settings behind a fresh empty lumotia dir. let t_migrate = Instant::now(); - let new_data_dir = app_data_dir(); - match migrate_legacy_data_dir(&new_data_dir) { + match migrate_legacy_data_dir() { Ok(MigrationStatus::Migrated { from, to, renamed_db }) => tracing::info!( target: "lumotia_startup", elapsed_ms = t_migrate.elapsed().as_millis(), @@ -240,11 +242,14 @@ pub fn run() { ), Ok(MigrationStatus::TargetAlreadyExists { .. }) => {} Ok(MigrationStatus::NoLegacyFound) => {} - Err(e) => tracing::warn!( - target: "lumotia_startup", - error = %e, - "legacy data dir migration failed — continuing with new path" - ), + Err(e) => { + tracing::error!( + target: "lumotia_startup", + error = %e, + "legacy data dir migration failed — refusing to start (would orphan user data)" + ); + return Err(Box::new(e) as Box); + } } // Initialise database and startup settings in one runtime entry. @@ -258,15 +263,17 @@ pub fn run() { // One-shot settings-key migration: rename any leftover // `magnotia_*` rows from the magnotia era to `lumotia_*`. - // Idempotent; logs only when rows are actually renamed. + // Deletes any magnotia_ rows that are orphans of an existing + // lumotia_ row. Idempotent; logs only when rows actually move. let t_keys = Instant::now(); match migrate_legacy_setting_keys(&db).await { - Ok(0) => {} - Ok(n) => tracing::info!( + Ok((0, 0)) => {} + Ok((renamed, orphans_deleted)) => tracing::info!( target: "lumotia_startup", - rows_renamed = n, + rows_renamed = renamed, + orphans_deleted = orphans_deleted, elapsed_ms = t_keys.elapsed().as_millis(), - "renamed legacy magnotia_* settings keys to lumotia_*" + "migrated legacy magnotia_* settings keys to lumotia_*" ), Err(e) => tracing::warn!( target: "lumotia_startup", From 14313cfa84195d8ede10de0479fc7d05477d54c3 Mon Sep 17 00:00:00 2001 From: Jake Date: Wed, 13 May 2026 09:47:28 +0100 Subject: [PATCH 10/82] =?UTF-8?q?agent:=20lumotia-rebrand=20=E2=80=94=20ta?= =?UTF-8?q?uri=20productName,=20identifier,=20window=20title?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 6 of the rebrand cascade per locked decision D2. src-tauri/tauri.conf.json: - productName: "Magnotia" -> "Lumotia" - identifier: "uk.co.corbel.magnotia" -> "consulting.corbel.lumotia" - window title: "Magnotia" -> "Lumotia" D2 picks the reverse-DNS of corbel.consulting (the actual domain CORBEL trades under) over the prior uk.co.corbel.* convention. This is the identity the OS uses for installed-app keying, so the first launch under the new identifier will look fresh to Tauri's plugin state (window-state, autostart). Per D1 the user-data dir is migrated by lumotia_core::paths on first boot, so transcripts and settings survive. cargo build --workspace passes. cargo test --workspace: 339 pass, 0 fail. Co-Authored-By: Claude Opus 4.7 (1M context) --- src-tauri/tauri.conf.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 9e22814..3edcf79 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,8 +1,8 @@ { "$schema": "https://schema.tauri.app/config/2", - "productName": "Magnotia", + "productName": "Lumotia", "version": "0.1.0", - "identifier": "uk.co.corbel.magnotia", + "identifier": "consulting.corbel.lumotia", "build": { "beforeDevCommand": "npm run dev:frontend", "devUrl": "http://localhost:1420", @@ -12,7 +12,7 @@ "app": { "windows": [ { - "title": "Magnotia", + "title": "Lumotia", "width": 1020, "height": 720, "minWidth": 960, From 16081095e00dbbb52cd8106886b84d70ca97eeb9 Mon Sep 17 00:00:00 2001 From: Jake Date: Wed, 13 May 2026 12:10:50 +0100 Subject: [PATCH 11/82] =?UTF-8?q?agent:=20lumotia-rebrand=20=E2=80=94=20lo?= =?UTF-8?q?calStorage=20keys=20+=20event=20channels=20migration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 7 of the rebrand cascade. Persisted UI state + inter-window event channels migrated from magnotia to lumotia naming, with one-shot localStorage key migration so dogfooded UI state survives the rename. src/lib/utils/localStorageMigration.ts (new): - migrateLocalStorageKey(old, new): idempotent + crash-safe shim. - If new key exists, removes old (lumotia value is authoritative). - If only old exists, copies value to new key, removes old. - If neither, no-op. - migrateLocalStorageKeys(pairs): batch wrapper. src/lib/stores/page.svelte.ts: - 4 key constants renamed to lumotia_settings / lumotia_profiles / lumotia_task_lists / lumotia_templates. - BroadcastChannel name renamed to lumotia_task_lists. - migrateLocalStorageKeys() called at module load before any read. src/lib/stores/focusTimer.svelte.ts: - STORAGE_KEY renamed to lumotia.focusTimer.v1. - migrateLocalStorageKey() called at module load. Event channels (magnotia: -> lumotia:) renamed across frontend + Rust: - magnotia:toggle-recording (src/routes/+layout.svelte) - magnotia:hotkey-pressed / -released (src-tauri/src/commands/hotkey.rs + consumers) - magnotia:open-wind-down (src-tauri/src/tray.rs + consumer) - magnotia:llm-download-progress (src-tauri/src/commands/llm.rs) - magnotia:preferences-changed (src/lib/stores/preferences.svelte.ts + consumers) - magnotia:start-timer (nudgeBus + dispatch sites) - magnotia:focus-timer-{complete,cancelled} (focusTimer + nudgeBus) - magnotia:microstep-generated (nudgeBus + dispatch sites) - magnotia:step-completed (nudgeBus + dispatch sites) - magnotia:task-{completed,uncompleted,deleted} (page.svelte.ts + nudgeBus + consumers) Storage-event filters in src/routes/{float,viewer,preview}/+layout@.svelte updated to filter on lumotia_settings. User-facing toast strings still say "Magnotia" — deferred to Phase 8 (frontend strings). npm run check: 0 errors / 0 warnings (3958 files). cargo test --workspace: 339 pass / 0 fail. Co-Authored-By: Claude Opus 4.7 (1M context) --- src-tauri/src/commands/hotkey.rs | 8 ++-- src-tauri/src/commands/llm.rs | 2 +- src-tauri/src/tray.rs | 2 +- src/lib/components/FocusTimer.svelte | 4 +- src/lib/components/MicroSteps.svelte | 6 +-- src/lib/components/MorningTriageModal.svelte | 2 +- src/lib/components/WipTaskList.svelte | 2 +- src/lib/pages/DictationPage.svelte | 4 +- src/lib/pages/SettingsPage.svelte | 2 +- src/lib/stores/completionStats.svelte.ts | 8 ++-- src/lib/stores/focusTimer.svelte.ts | 11 +++-- .../stores/implementationIntentions.svelte.ts | 12 ++--- src/lib/stores/nudgeBus.svelte.ts | 24 +++++----- src/lib/stores/page.svelte.ts | 27 +++++++---- src/lib/stores/preferences.svelte.ts | 2 +- src/lib/utils/localStorageMigration.ts | 48 +++++++++++++++++++ src/lib/utils/settingsMigrations.ts | 2 +- src/routes/+layout.svelte | 8 ++-- src/routes/float/+layout@.svelte | 2 +- src/routes/preview/+layout@.svelte | 2 +- src/routes/viewer/+layout@.svelte | 2 +- 21 files changed, 122 insertions(+), 58 deletions(-) create mode 100644 src/lib/utils/localStorageMigration.ts diff --git a/src-tauri/src/commands/hotkey.rs b/src-tauri/src/commands/hotkey.rs index 547d01e..2ce91d6 100644 --- a/src-tauri/src/commands/hotkey.rs +++ b/src-tauri/src/commands/hotkey.rs @@ -33,8 +33,8 @@ pub fn check_hotkey_access() -> Result<(), String> { lumotia_hotkey::check_evdev_access() } -/// Start the evdev global hotkey listener. Emits "magnotia:hotkey-pressed" and -/// "magnotia:hotkey-released" events to the frontend. +/// Start the evdev global hotkey listener. Emits "lumotia:hotkey-pressed" and +/// "lumotia:hotkey-released" events to the frontend. /// /// If a listener is already running, it is stopped first. #[tauri::command] @@ -64,10 +64,10 @@ pub async fn start_evdev_hotkey( while let Some(event) = event_rx.recv().await { match event { HotkeyEvent::Pressed => { - let _ = app_clone.emit("magnotia:hotkey-pressed", ()); + let _ = app_clone.emit("lumotia:hotkey-pressed", ()); } HotkeyEvent::Released => { - let _ = app_clone.emit("magnotia:hotkey-released", ()); + let _ = app_clone.emit("lumotia:hotkey-released", ()); } } } diff --git a/src-tauri/src/commands/llm.rs b/src-tauri/src/commands/llm.rs index a3f03bb..1b8b109 100644 --- a/src-tauri/src/commands/llm.rs +++ b/src-tauri/src/commands/llm.rs @@ -74,7 +74,7 @@ pub async fn download_llm_model( 0 }; let _ = app_clone.emit( - "magnotia:llm-download-progress", + "lumotia:llm-download-progress", serde_json::json!({ "modelId": id.as_str(), "done": done, diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs index e387cc5..f7ac356 100644 --- a/src-tauri/src/tray.rs +++ b/src-tauri/src/tray.rs @@ -48,7 +48,7 @@ pub fn setup(app: &tauri::App) -> Result<(), Box> { } // The frontend layout listens for this event and routes // to the Phase 5 wind-down page. - let _ = app.emit("magnotia:open-wind-down", ()); + let _ = app.emit("lumotia:open-wind-down", ()); } "quit" => { app.exit(0); diff --git a/src/lib/components/FocusTimer.svelte b/src/lib/components/FocusTimer.svelte index 431675b..fd0fc53 100644 --- a/src/lib/components/FocusTimer.svelte +++ b/src/lib/components/FocusTimer.svelte @@ -77,13 +77,13 @@ } onMount(() => { - window.addEventListener("magnotia:start-timer", handleStartEvent); + window.addEventListener("lumotia:start-timer", handleStartEvent); // Rehydrate any in-flight timer that survived a window close. focusTimer.rehydrate(); }); onDestroy(() => { - window.removeEventListener("magnotia:start-timer", handleStartEvent); + window.removeEventListener("lumotia:start-timer", handleStartEvent); }); function handleCancel() { diff --git a/src/lib/components/MicroSteps.svelte b/src/lib/components/MicroSteps.svelte index 7d10ea1..dbf17fc 100644 --- a/src/lib/components/MicroSteps.svelte +++ b/src/lib/components/MicroSteps.svelte @@ -58,7 +58,7 @@ // is completed in that window, a gentle "still with that one?" // nudge fires. if (typeof window !== 'undefined') { - window.dispatchEvent(new CustomEvent('magnotia:microstep-generated', { + window.dispatchEvent(new CustomEvent('lumotia:microstep-generated', { detail: { parentTaskId }, })); } @@ -78,7 +78,7 @@ // micro-step-idle timer for this parent task — the breakdown // is demonstrably getting worked, no nudge needed. if (typeof window !== 'undefined') { - window.dispatchEvent(new CustomEvent('magnotia:step-completed', { + window.dispatchEvent(new CustomEvent('lumotia:step-completed', { detail: { id: subtaskId, parentTaskId }, })); } @@ -86,7 +86,7 @@ } function startTimer(subtaskId: string) { - window.dispatchEvent(new CustomEvent('magnotia:start-timer', { + window.dispatchEvent(new CustomEvent('lumotia:start-timer', { detail: { taskId: subtaskId, seconds: 120 } })); } diff --git a/src/lib/components/MorningTriageModal.svelte b/src/lib/components/MorningTriageModal.svelte index 0cf4363..b1a6a89 100644 --- a/src/lib/components/MorningTriageModal.svelte +++ b/src/lib/components/MorningTriageModal.svelte @@ -67,7 +67,7 @@ function dispatchTriageFinished(mode: 'empty' | 'skipped' | 'picked') { if (typeof window === 'undefined') return; - window.dispatchEvent(new CustomEvent('magnotia:morning-triage-finished', { + window.dispatchEvent(new CustomEvent('lumotia:morning-triage-finished', { detail: { date: todayKey(), mode }, })); } diff --git a/src/lib/components/WipTaskList.svelte b/src/lib/components/WipTaskList.svelte index 442af36..a51ce84 100644 --- a/src/lib/components/WipTaskList.svelte +++ b/src/lib/components/WipTaskList.svelte @@ -5,7 +5,7 @@ import { ChevronDown, ChevronRight, Timer } from 'lucide-svelte'; function startFocusTimer(task: { id: string; text: string }) { - window.dispatchEvent(new CustomEvent('magnotia:start-timer', { + window.dispatchEvent(new CustomEvent('lumotia:start-timer', { detail: { taskId: task.id, seconds: 300, label: task.text } })); } diff --git a/src/lib/pages/DictationPage.svelte b/src/lib/pages/DictationPage.svelte index 53d4d82..6939641 100644 --- a/src/lib/pages/DictationPage.svelte +++ b/src/lib/pages/DictationPage.svelte @@ -65,7 +65,7 @@ let hotkeyHandler = () => toggleRecording(); onMount(async () => { - window.addEventListener("magnotia:toggle-recording", hotkeyHandler); + window.addEventListener("lumotia:toggle-recording", hotkeyHandler); if (!tauriRuntimeAvailable) { error = browserPreviewMessage; @@ -77,7 +77,7 @@ }); onDestroy(() => { - window.removeEventListener("magnotia:toggle-recording", hotkeyHandler); + window.removeEventListener("lumotia:toggle-recording", hotkeyHandler); clearInterval(timerInterval); if (page.recording) { page.recording = false; diff --git a/src/lib/pages/SettingsPage.svelte b/src/lib/pages/SettingsPage.svelte index 76a5aec..b26ca8d 100644 --- a/src/lib/pages/SettingsPage.svelte +++ b/src/lib/pages/SettingsPage.svelte @@ -870,7 +870,7 @@ downloadTotal = event.payload.total_bytes ?? event.payload.total ?? 0; }); - unlistenLlm = await listen("magnotia:llm-download-progress", (event) => { + unlistenLlm = await listen("lumotia:llm-download-progress", (event) => { llmDownloadProgress = event.payload.percent || 0; llmDownloadingModel = event.payload.modelId || llmDownloadingModel; llmDownloadBytes = event.payload.done ?? 0; diff --git a/src/lib/stores/completionStats.svelte.ts b/src/lib/stores/completionStats.svelte.ts index c934ceb..003e8de 100644 --- a/src/lib/stores/completionStats.svelte.ts +++ b/src/lib/stores/completionStats.svelte.ts @@ -47,10 +47,10 @@ if (typeof window !== "undefined") { const handler = () => { refresh().catch(() => {}); }; - window.addEventListener("magnotia:task-completed", handler); - window.addEventListener("magnotia:step-completed", handler); - window.addEventListener("magnotia:task-uncompleted", handler); - window.addEventListener("magnotia:task-deleted", handler); + window.addEventListener("lumotia:task-completed", handler); + window.addEventListener("lumotia:step-completed", handler); + window.addEventListener("lumotia:task-uncompleted", handler); + window.addEventListener("lumotia:task-deleted", handler); window.addEventListener("focus", handler); if (hasTauriRuntime()) { diff --git a/src/lib/stores/focusTimer.svelte.ts b/src/lib/stores/focusTimer.svelte.ts index abd0ad8..fd21739 100644 --- a/src/lib/stores/focusTimer.svelte.ts +++ b/src/lib/stores/focusTimer.svelte.ts @@ -12,9 +12,14 @@ // so closing the window mid-timer still gets you the "done" signal // on next launch. -const STORAGE_KEY = "magnotia.focusTimer.v1"; +import { migrateLocalStorageKey } from "$lib/utils/localStorageMigration"; + +const STORAGE_KEY = "lumotia.focusTimer.v1"; const TICK_INTERVAL_MS = 250; +// One-shot key rename from the magnotia era. Idempotent. +migrateLocalStorageKey("magnotia.focusTimer.v1", STORAGE_KEY); + export type FocusTimerPersisted = { startedAt: number; durationMs: number; @@ -114,7 +119,7 @@ function createFocusTimerStore() { writePersisted(null); stopTick(); if (wasActive && typeof window !== "undefined") { - window.dispatchEvent(new CustomEvent("magnotia:focus-timer-cancelled")); + window.dispatchEvent(new CustomEvent("lumotia:focus-timer-cancelled")); } } @@ -172,7 +177,7 @@ function createFocusTimerStore() { osc.onended = () => ctx.close().catch(() => {}); } catch { /* audio is a nicety; never fatal */ } - window.dispatchEvent(new CustomEvent("magnotia:focus-timer-complete", { + window.dispatchEvent(new CustomEvent("lumotia:focus-timer-complete", { detail: { taskId, label }, })); } diff --git a/src/lib/stores/implementationIntentions.svelte.ts b/src/lib/stores/implementationIntentions.svelte.ts index 67015be..1acee1b 100644 --- a/src/lib/stores/implementationIntentions.svelte.ts +++ b/src/lib/stores/implementationIntentions.svelte.ts @@ -13,7 +13,7 @@ import { page, settings, tasks } from "$lib/stores/page.svelte.js"; import { toasts } from "$lib/stores/toasts.svelte.js"; const TIME_RULE_POLL_MS = 30_000; -const RULES_CHANGED_EVENT = "magnotia:implementation-rules-changed"; +const RULES_CHANGED_EVENT = "lumotia:implementation-rules-changed"; export const implementationRules = $state([]); @@ -157,7 +157,7 @@ async function executeAction(action: ImplementationRuleAction): Promise { return; } if (action.kind === "start_timer") { - window.dispatchEvent(new CustomEvent("magnotia:start-timer", { + window.dispatchEvent(new CustomEvent("lumotia:start-timer", { detail: { taskId: action.taskId ?? null, seconds: 300, @@ -239,8 +239,8 @@ export function startImplementationIntentions(): void { void loadImplementationRules(true) .then(() => checkTimeRules()) .catch(() => {}); - window.addEventListener("magnotia:task-completed", onTaskCompleted); - window.addEventListener("magnotia:morning-triage-finished", onMorningTriageFinished); + window.addEventListener("lumotia:task-completed", onTaskCompleted); + window.addEventListener("lumotia:morning-triage-finished", onMorningTriageFinished); window.addEventListener(RULES_CHANGED_EVENT, onRulesChanged); timePollHandle = setInterval(() => { void checkTimeRules().catch(() => {}); @@ -250,8 +250,8 @@ export function startImplementationIntentions(): void { export function stopImplementationIntentions(): void { if (!started) return; started = false; - window.removeEventListener("magnotia:task-completed", onTaskCompleted); - window.removeEventListener("magnotia:morning-triage-finished", onMorningTriageFinished); + window.removeEventListener("lumotia:task-completed", onTaskCompleted); + window.removeEventListener("lumotia:morning-triage-finished", onMorningTriageFinished); window.removeEventListener(RULES_CHANGED_EVENT, onRulesChanged); if (timePollHandle !== null) { clearInterval(timePollHandle); diff --git a/src/lib/stores/nudgeBus.svelte.ts b/src/lib/stores/nudgeBus.svelte.ts index 3c5f45d..8fd1e85 100644 --- a/src/lib/stores/nudgeBus.svelte.ts +++ b/src/lib/stores/nudgeBus.svelte.ts @@ -236,12 +236,12 @@ export function startNudgeBus(): void { if (typeof window === "undefined") return; started = true; - window.addEventListener("magnotia:start-timer", onTimerStart); - window.addEventListener("magnotia:focus-timer-complete", resetTimerState); - window.addEventListener("magnotia:focus-timer-cancelled", resetTimerState); - window.addEventListener("magnotia:microstep-generated", onMicroStepGenerated); - window.addEventListener("magnotia:step-completed", onStepOrTaskCompleted); - window.addEventListener("magnotia:task-completed", onStepOrTaskCompleted); + window.addEventListener("lumotia:start-timer", onTimerStart); + window.addEventListener("lumotia:focus-timer-complete", resetTimerState); + window.addEventListener("lumotia:focus-timer-cancelled", resetTimerState); + window.addEventListener("lumotia:microstep-generated", onMicroStepGenerated); + window.addEventListener("lumotia:step-completed", onStepOrTaskCompleted); + window.addEventListener("lumotia:task-completed", onStepOrTaskCompleted); window.addEventListener("focus", onFocus); window.addEventListener("blur", onBlur); @@ -265,12 +265,12 @@ export function stopNudgeBus(): void { if (!started) return; started = false; - window.removeEventListener("magnotia:start-timer", onTimerStart); - window.removeEventListener("magnotia:focus-timer-complete", resetTimerState); - window.removeEventListener("magnotia:focus-timer-cancelled", resetTimerState); - window.removeEventListener("magnotia:microstep-generated", onMicroStepGenerated); - window.removeEventListener("magnotia:step-completed", onStepOrTaskCompleted); - window.removeEventListener("magnotia:task-completed", onStepOrTaskCompleted); + window.removeEventListener("lumotia:start-timer", onTimerStart); + window.removeEventListener("lumotia:focus-timer-complete", resetTimerState); + window.removeEventListener("lumotia:focus-timer-cancelled", resetTimerState); + window.removeEventListener("lumotia:microstep-generated", onMicroStepGenerated); + window.removeEventListener("lumotia:step-completed", onStepOrTaskCompleted); + window.removeEventListener("lumotia:task-completed", onStepOrTaskCompleted); window.removeEventListener("focus", onFocus); window.removeEventListener("blur", onBlur); diff --git a/src/lib/stores/page.svelte.ts b/src/lib/stores/page.svelte.ts index de7efe4..3d86a59 100644 --- a/src/lib/stores/page.svelte.ts +++ b/src/lib/stores/page.svelte.ts @@ -36,10 +36,21 @@ export const page = $state({ taskSidebarOpen: false, }); -const SETTINGS_KEY = "magnotia_settings"; -const PROFILES_KEY = "magnotia_profiles"; -const TASK_LISTS_KEY = "magnotia_task_lists"; -const TEMPLATES_KEY = "magnotia_templates"; +import { migrateLocalStorageKeys } from "$lib/utils/localStorageMigration"; + +const SETTINGS_KEY = "lumotia_settings"; +const PROFILES_KEY = "lumotia_profiles"; +const TASK_LISTS_KEY = "lumotia_task_lists"; +const TEMPLATES_KEY = "lumotia_templates"; + +// One-shot key rename from the magnotia era. Idempotent. Runs once at +// module load before any localStorage read below. +migrateLocalStorageKeys([ + ["lumotia_settings", SETTINGS_KEY], + ["magnotia_profiles", PROFILES_KEY], + ["magnotia_task_lists", TASK_LISTS_KEY], + ["magnotia_templates", TEMPLATES_KEY], +]); const defaults: SettingsState = { engine: "whisper", @@ -459,7 +470,7 @@ export async function deleteTask(id: string) { tasks.splice(idx, 1); broadcastTasks(); if (typeof window !== "undefined") { - window.dispatchEvent(new CustomEvent("magnotia:task-deleted", { detail: { id } })); + window.dispatchEvent(new CustomEvent("lumotia:task-deleted", { detail: { id } })); } } } catch (err) { @@ -477,7 +488,7 @@ export async function completeTask(id: string) { // to this to clear micro-step-idle timers for the completed task // and, later, to drive Phase 7 "after a task completes" rules. if (typeof window !== "undefined") { - window.dispatchEvent(new CustomEvent("magnotia:task-completed", { detail: { id } })); + window.dispatchEvent(new CustomEvent("lumotia:task-completed", { detail: { id } })); } } catch (err) { toasts.error("Couldn't complete task", errorMessage(err)); @@ -491,7 +502,7 @@ export async function uncompleteTask(id: string) { await invoke("uncomplete_task_cmd", { id }); applyLocalTaskUpdate(id, { done: false, doneAt: null }); if (typeof window !== "undefined") { - window.dispatchEvent(new CustomEvent("magnotia:task-uncompleted", { detail: { id } })); + window.dispatchEvent(new CustomEvent("lumotia:task-uncompleted", { detail: { id } })); } } catch (err) { toasts.error("Couldn't uncomplete task", errorMessage(err)); @@ -654,7 +665,7 @@ interface TaskListChannelMessage { } const taskListChannel = typeof BroadcastChannel !== "undefined" - ? new BroadcastChannel("magnotia_task_lists") + ? new BroadcastChannel("lumotia_task_lists") : null; if (taskListChannel) { diff --git a/src/lib/stores/preferences.svelte.ts b/src/lib/stores/preferences.svelte.ts index a7772ee..ac048d6 100644 --- a/src/lib/stores/preferences.svelte.ts +++ b/src/lib/stores/preferences.svelte.ts @@ -6,7 +6,7 @@ import type { AccessibilityPreferences, Preferences } from "$lib/types/app"; import { errorMessage } from "$lib/utils/errors.js"; import { toasts } from "./toasts.svelte.ts"; -export const PREFERENCES_CHANGED_EVENT = "magnotia:preferences-changed"; +export const PREFERENCES_CHANGED_EVENT = "lumotia:preferences-changed"; type FontFamilies = Record; diff --git a/src/lib/utils/localStorageMigration.ts b/src/lib/utils/localStorageMigration.ts new file mode 100644 index 0000000..3aeba92 --- /dev/null +++ b/src/lib/utils/localStorageMigration.ts @@ -0,0 +1,48 @@ +/** + * Phase 7 of the magnotia -> lumotia rebrand cascade. Persisted UI state + * survives the rename via one-shot key migrations run at module-load + * inside the stores that own each key. + * + * `migrateLocalStorageKey` is idempotent and crash-safe: + * - If the new key already exists, the old key (if any) is removed + * silently — the lumotia value is authoritative. + * - If only the old key exists, its value is copied to the new key and + * the old key is removed. + * - If neither exists, this is a no-op. + */ + +const STORAGE_NS = "lumotia-rebrand-migration"; + +export function migrateLocalStorageKey(oldKey: string, newKey: string): void { + if (typeof localStorage === "undefined") return; + try { + const newValue = localStorage.getItem(newKey); + if (newValue !== null) { + if (localStorage.getItem(oldKey) !== null) { + localStorage.removeItem(oldKey); + } + return; + } + const oldValue = localStorage.getItem(oldKey); + if (oldValue === null) return; + localStorage.setItem(newKey, oldValue); + localStorage.removeItem(oldKey); + } catch (err) { + // Quota / disabled-storage / DOMException — silently no-op. The store + // will fall back to the old key on next read (or fail there with a + // visible error). Logging is intentionally silent to avoid polluting + // dev-console output on every page in the multi-window flow. + void err; + } +} + +/** + * Run a batch of migrations. Used by stores that own multiple keys. + */ +export function migrateLocalStorageKeys(pairs: Array<[string, string]>): void { + for (const [oldKey, newKey] of pairs) { + migrateLocalStorageKey(oldKey, newKey); + } +} + +void STORAGE_NS; diff --git a/src/lib/utils/settingsMigrations.ts b/src/lib/utils/settingsMigrations.ts index 16dddd0..3a14be6 100644 --- a/src/lib/utils/settingsMigrations.ts +++ b/src/lib/utils/settingsMigrations.ts @@ -1,7 +1,7 @@ /** * Forward-compatible, versioned settings migration. * - * Historically, `localStorage["magnotia_settings"]` was a bare JSON object + * Historically, `localStorage["lumotia_settings"]` was a bare JSON object * (whatever `SettingsState` looked like at write time). That shape * tolerates new fields cleanly — spread over `defaults` — but does not * survive: diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 42498e6..d7ba2c2 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -138,7 +138,7 @@ await mod.register(hotkey, () => { if (page.current !== "dictation") page.current = "dictation"; requestAnimationFrame(() => { - window.dispatchEvent(new CustomEvent("magnotia:toggle-recording")); + window.dispatchEvent(new CustomEvent("lumotia:toggle-recording")); }); }); registeredHotkey = hotkey; @@ -174,13 +174,13 @@ async function setupEvdevListener() { if (!tauriRuntimeAvailable) return; const { listen } = await import("@tauri-apps/api/event"); - unlistenEvdev = await listen("magnotia:hotkey-pressed", () => { + unlistenEvdev = await listen("lumotia:hotkey-pressed", () => { const now = Date.now(); if (now - lastHotkeyAtMs < HOTKEY_DEBOUNCE_MS) return; lastHotkeyAtMs = now; if (page.current !== "dictation") page.current = "dictation"; requestAnimationFrame(() => { - window.dispatchEvent(new CustomEvent("magnotia:toggle-recording")); + window.dispatchEvent(new CustomEvent("lumotia:toggle-recording")); }); }); } @@ -248,7 +248,7 @@ let unlistenWindDown = null; async function setupWindDownListener() { if (!tauriRuntimeAvailable) return; - unlistenWindDown = await listen("magnotia:open-wind-down", () => { + unlistenWindDown = await listen("lumotia:open-wind-down", () => { page.current = "shutdown"; }); } diff --git a/src/routes/float/+layout@.svelte b/src/routes/float/+layout@.svelte index c42a976..a6ad755 100644 --- a/src/routes/float/+layout@.svelte +++ b/src/routes/float/+layout@.svelte @@ -35,7 +35,7 @@ // Listen for settings changes from main window if (typeof window !== "undefined") { window.addEventListener("storage", (e) => { - if (e.key === "magnotia_settings" && e.newValue) { + if (e.key === "lumotia_settings" && e.newValue) { try { Object.assign(settings, JSON.parse(e.newValue)); } catch {} diff --git a/src/routes/preview/+layout@.svelte b/src/routes/preview/+layout@.svelte index e4fe48f..d023117 100644 --- a/src/routes/preview/+layout@.svelte +++ b/src/routes/preview/+layout@.svelte @@ -28,7 +28,7 @@ if (typeof window !== "undefined") { window.addEventListener("storage", (event) => { - if (event.key === "magnotia_settings" && event.newValue) { + if (event.key === "lumotia_settings" && event.newValue) { try { Object.assign(settings, JSON.parse(event.newValue)); } catch {} } }); diff --git a/src/routes/viewer/+layout@.svelte b/src/routes/viewer/+layout@.svelte index be2aabe..2efddd4 100644 --- a/src/routes/viewer/+layout@.svelte +++ b/src/routes/viewer/+layout@.svelte @@ -32,7 +32,7 @@ // Sync settings from main window if (typeof window !== "undefined") { window.addEventListener("storage", (e) => { - if (e.key === "magnotia_settings" && e.newValue) { + if (e.key === "lumotia_settings" && e.newValue) { try { Object.assign(settings, JSON.parse(e.newValue)); } catch {} } }); From 681a9b26dcce5d5611ddcc8b60ebf65802f385b1 Mon Sep 17 00:00:00 2001 From: Jake Date: Wed, 13 May 2026 12:29:37 +0100 Subject: [PATCH 12/82] =?UTF-8?q?agent:=20lumotia-rebrand=20=E2=80=94=20fr?= =?UTF-8?q?ontend=20strings=20(svelte=20+=20i18n=20+=20design-system)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 8 of the rebrand cascade. Every rendered string is now Lumotia; no Magnotia surface visible in the UI. Sweep replaced \bmagnotia\b -> lumotia and \bMagnotia\b -> Lumotia across all .svelte / .ts / .js / .css / .html / .json (excluding package-lock.json which regenerates, target/, build/, node_modules/). Surfaces touched: - src/app.css — design-token comment header and .magnotia-rh-* CSS resize-handle class selectors (also the consuming elements in components/ResizeHandles.svelte and src/routes/*/+layout.svelte). - src/lib/i18n/locales/{en,de,es}.json — brand name in translations. - src/lib/i18n/index.ts — header comment. - src/lib/Sidebar.svelte and most pages under src/lib/pages/ + src/lib/components/ — title bars, document titles, default filenames (lumotia-YYYY-MM-DD.* etc), toast strings, error messages, dialog headers. - src/routes/+layout.svelte, +page.svelte, viewer/, float/, preview/. - src/app.html page . - src/lib/utils/settingsMigrations.ts — fallback toast copy. - src/design-system/{colors_and_type.css,SKILL.md,README.md, ui_kits/{Sidebar.jsx,index.html}} — design-tokens, doc strings, preview wordmark in the kit. - package.json — name + description. NOT touched (deferred / immutable): - package-lock.json — regenerates on next npm install. - The two migration-call sites in stores reference the legacy magnotia keys deliberately; restored after the sweep clobbered them. - docs/, README.md, HANDOVER.md — Phase 9 scope. npm run check: 0 errors / 0 warnings (3958 files). cargo test --workspace: 339 pass / 0 fail. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --- package.json | 4 +- src-tauri/tauri.linux.conf.json | 2 +- src/app.css | 74 +++++++++---------- src/app.html | 2 +- src/design-system/colors_and_type.css | 6 +- src/design-system/preview/brand-wordmark.html | 2 +- .../preview/components-toasts.html | 2 +- src/design-system/preview/type-body.html | 2 +- .../preview/type-transcript-mono.html | 2 +- src/design-system/ui_kits/Sidebar.jsx | 2 +- src/design-system/ui_kits/index.html | 4 +- src/lib/Sidebar.svelte | 6 +- src/lib/components/FocusTimer.svelte | 4 +- src/lib/components/HotkeyRecorder.svelte | 2 +- src/lib/components/ModelDownloader.svelte | 2 +- src/lib/components/ResizeHandles.svelte | 26 +++---- src/lib/i18n/locales/de.json | 2 +- src/lib/i18n/locales/en.json | 2 +- src/lib/i18n/locales/es.json | 2 +- src/lib/pages/DictationPage.svelte | 6 +- src/lib/pages/FilesPage.svelte | 2 +- src/lib/pages/FirstRunPage.svelte | 6 +- src/lib/pages/SettingsPage.svelte | 18 ++--- src/lib/stores/completionStats.svelte.ts | 8 +- src/lib/stores/nudgeBus.svelte.ts | 4 +- src/lib/stores/page.svelte.ts | 8 +- src/lib/stores/toasts.svelte.ts | 2 +- src/lib/types/app.ts | 2 +- src/lib/utils/export.ts | 2 +- src/lib/utils/frontmatter.ts | 4 +- src/lib/utils/hotkeyValidity.ts | 2 +- src/lib/utils/localStorageMigration.ts | 2 +- src/lib/utils/settingsMigrations.ts | 2 +- src/routes/+layout.svelte | 8 +- src/routes/+page.svelte | 2 +- src/routes/float/+page.svelte | 2 +- src/routes/viewer/+page.svelte | 2 +- 37 files changed, 115 insertions(+), 115 deletions(-) diff --git a/package.json b/package.json index 6e1e963..4734754 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "magnotia", + "name": "lumotia", "version": "0.1.0", - "description": "Magnotia — Think out loud", + "description": "Lumotia — Think out loud", "type": "module", "scripts": { "dev": "npm run dev:frontend", diff --git a/src-tauri/tauri.linux.conf.json b/src-tauri/tauri.linux.conf.json index 68b544c..6b91ab9 100644 --- a/src-tauri/tauri.linux.conf.json +++ b/src-tauri/tauri.linux.conf.json @@ -3,7 +3,7 @@ "app": { "windows": [ { - "title": "Magnotia", + "title": "Lumotia", "width": 1020, "height": 720, "minWidth": 960, diff --git a/src/app.css b/src/app.css index 455a0cd..80c2e4d 100644 --- a/src/app.css +++ b/src/app.css @@ -48,7 +48,7 @@ @import "tailwindcss"; -/* === Magnotia Design Tokens === */ +/* === Lumotia Design Tokens === */ @theme { /* Surfaces — warm layered depth */ --color-bg: #0f0e0c; @@ -131,9 +131,9 @@ --duration-decorative: 300ms; /* Window resize hit zones — consumed by ResizeHandles.svelte. One source - of truth so every Magnotia window feels identical. */ - --magnotia-resize-edge: 12px; - --magnotia-resize-corner: 20px; + of truth so every Lumotia window feels identical. */ + --lumotia-resize-edge: 12px; + --lumotia-resize-corner: 20px; } /* === Button Component Classes === */ @@ -507,7 +507,7 @@ textarea, input, [data-no-transition] { parse it as a per-component virtual CSS module, which triggers an "Invalid declaration" error on plain CSS inputs in some Tailwind v4 + Svelte 5 combinations. */ -.magnotia-rh { +.lumotia-rh { position: fixed; z-index: 2147483646; background: transparent; @@ -517,67 +517,67 @@ textarea, input, [data-no-transition] { -webkit-user-select: none; } -.magnotia-rh-nw, -.magnotia-rh-ne, -.magnotia-rh-sw, -.magnotia-rh-se { +.lumotia-rh-nw, +.lumotia-rh-ne, +.lumotia-rh-sw, +.lumotia-rh-se { z-index: 2147483647; } -.magnotia-rh-n { +.lumotia-rh-n { top: 0; - left: var(--magnotia-resize-corner); - right: var(--magnotia-resize-corner); - height: var(--magnotia-resize-edge); + left: var(--lumotia-resize-corner); + right: var(--lumotia-resize-corner); + height: var(--lumotia-resize-edge); cursor: n-resize; } -.magnotia-rh-s { +.lumotia-rh-s { bottom: 0; - left: var(--magnotia-resize-corner); - right: var(--magnotia-resize-corner); - height: var(--magnotia-resize-edge); + left: var(--lumotia-resize-corner); + right: var(--lumotia-resize-corner); + height: var(--lumotia-resize-edge); cursor: s-resize; } -.magnotia-rh-w { - top: var(--magnotia-resize-corner); - bottom: var(--magnotia-resize-corner); +.lumotia-rh-w { + top: var(--lumotia-resize-corner); + bottom: var(--lumotia-resize-corner); left: 0; - width: var(--magnotia-resize-edge); + width: var(--lumotia-resize-edge); cursor: w-resize; } -.magnotia-rh-e { - top: var(--magnotia-resize-corner); - bottom: var(--magnotia-resize-corner); +.lumotia-rh-e { + top: var(--lumotia-resize-corner); + bottom: var(--lumotia-resize-corner); right: 0; - width: var(--magnotia-resize-edge); + width: var(--lumotia-resize-edge); cursor: e-resize; } -.magnotia-rh-nw { +.lumotia-rh-nw { top: 0; left: 0; - width: var(--magnotia-resize-corner); - height: var(--magnotia-resize-corner); + width: var(--lumotia-resize-corner); + height: var(--lumotia-resize-corner); cursor: nw-resize; } -.magnotia-rh-ne { +.lumotia-rh-ne { top: 0; right: 0; - width: var(--magnotia-resize-corner); - height: var(--magnotia-resize-corner); + width: var(--lumotia-resize-corner); + height: var(--lumotia-resize-corner); cursor: ne-resize; } -.magnotia-rh-sw { +.lumotia-rh-sw { bottom: 0; left: 0; - width: var(--magnotia-resize-corner); - height: var(--magnotia-resize-corner); + width: var(--lumotia-resize-corner); + height: var(--lumotia-resize-corner); cursor: sw-resize; } -.magnotia-rh-se { +.lumotia-rh-se { bottom: 0; right: 0; - width: var(--magnotia-resize-corner); - height: var(--magnotia-resize-corner); + width: var(--lumotia-resize-corner); + height: var(--lumotia-resize-corner); cursor: se-resize; } diff --git a/src/app.html b/src/app.html index 322eb0d..828f39c 100644 --- a/src/app.html +++ b/src/app.html @@ -4,7 +4,7 @@ <meta charset="utf-8" /> <link rel="icon" href="%sveltekit.assets%/favicon.png" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> - <title>Magnotia + Lumotia %sveltekit.head% diff --git a/src/design-system/colors_and_type.css b/src/design-system/colors_and_type.css index a69e960..98dfa93 100644 --- a/src/design-system/colors_and_type.css +++ b/src/design-system/colors_and_type.css @@ -1,5 +1,5 @@ /* ============================================================ - Magnotia — Colors & Type (preview surface only) + Lumotia — Colors & Type (preview surface only) Reflects the runtime tokens defined in src/app.css @theme. The runtime app reads from app.css; this file exists so the @@ -11,7 +11,7 @@ pages buildless. ============================================================ */ -/* --- Brand Webfonts (local woff2, shipped with Magnotia) --- +/* --- Brand Webfonts (local woff2, shipped with Lumotia) --- Mirrored from src/app.css. Duplication is intentional: this file is loaded directly by buildless preview HTML under design-system/preview/, which does not go through Tailwind/Vite, so the fonts must be declared @@ -308,7 +308,7 @@ a:hover { color: var(--accent-hover); } } :root[data-theme="light"] .grain::after { opacity: 0.015; } -/* Magnotia sinhala glyph ක (U+0D9A) — wordmark companion */ +/* Lumotia sinhala glyph ක (U+0D9A) — wordmark companion */ .k-glyph::after { content: '\0D9A'; color: var(--accent); margin-left: 0.25em; display: inline-block; } @media (prefers-reduced-motion: reduce) { diff --git a/src/design-system/preview/brand-wordmark.html b/src/design-system/preview/brand-wordmark.html index 9e425a4..df20c57 100644 --- a/src/design-system/preview/brand-wordmark.html +++ b/src/design-system/preview/brand-wordmark.html @@ -12,7 +12,7 @@
-
Magnotia
+
Lumotia
Think out loud
diff --git a/src/design-system/preview/components-toasts.html b/src/design-system/preview/components-toasts.html index e792365..9a67089 100644 --- a/src/design-system/preview/components-toasts.html +++ b/src/design-system/preview/components-toasts.html @@ -14,6 +14,6 @@
Toasts · bottom-right · sticky on error
Could not start recording
Audio device disconnected. Plug it back in and try again.
-
Dropped 2s of older audio
Magnotia is keeping up in real time.
+
Dropped 2s of older audio
Lumotia is keeping up in real time.
Saved · 3 tasks extracted
Your audio is saved. Open Tasks to review.
diff --git a/src/design-system/preview/type-body.html b/src/design-system/preview/type-body.html index 67841d8..f6d27e2 100644 --- a/src/design-system/preview/type-body.html +++ b/src/design-system/preview/type-body.html @@ -4,7 +4,7 @@ .row{display:flex;align-items:flex-start;gap:18px;margin-bottom:14px} .l{font:400 11px var(--font-mono);color:var(--text-tertiary);min-width:110px;padding-top:4px} -
Body · 16/1.5
Magnotia listens more than it speaks. Press the hotkey, speak your thought, and your words appear — saved, searchable, left exactly where you put them.
+
Body · 16/1.5
Lumotia listens more than it speaks. Press the hotkey, speak your thought, and your words appear — saved, searchable, left exactly where you put them.
Body large · 18/1.6
A calm friend who has already done the anxious part for you.
Small · 13
Press record or Ctrl+Shift+R
Caption · 12
12 words · 00:47 · live
diff --git a/src/design-system/preview/type-transcript-mono.html b/src/design-system/preview/type-transcript-mono.html index 8ccdd14..bb85caf 100644 --- a/src/design-system/preview/type-transcript-mono.html +++ b/src/design-system/preview/type-transcript-mono.html @@ -8,6 +8,6 @@
Transcript · 16–24px · 1.85 · +0.01em · liga + kern
So the thing I wanted to capture is — if we move the onboarding to the second screen instead of the first, the friction around first launch basically disappears. That feels right.
-
~/Library/Application Support/Magnotia/transcripts.db
+
~/Library/Application Support/Lumotia/transcripts.db
CtrlShiftRGlobal record toggle
diff --git a/src/design-system/ui_kits/Sidebar.jsx b/src/design-system/ui_kits/Sidebar.jsx index cdddf5c..fea26b0 100644 --- a/src/design-system/ui_kits/Sidebar.jsx +++ b/src/design-system/ui_kits/Sidebar.jsx @@ -32,7 +32,7 @@ function Sidebar({ current, onNav, taskCount = 0, status = 'Ready', statusColor return (
-
+ + diff --git a/src/lib/components/ImplementationRulesEditor.svelte b/src/lib/components/ImplementationRulesEditor.svelte index 425c73b..c75f02b 100644 --- a/src/lib/components/ImplementationRulesEditor.svelte +++ b/src/lib/components/ImplementationRulesEditor.svelte @@ -112,6 +112,7 @@ } async function removeRule(rule) { + if (!confirm("Remove this if-then rule? Future captures won't be filtered by it. This cannot be undone.")) return; try { await deleteImplementationRule(rule.id); } catch (err) { diff --git a/src/lib/components/ModelDownloader.svelte b/src/lib/components/ModelDownloader.svelte index 4ffb00b..c5ba720 100644 --- a/src/lib/components/ModelDownloader.svelte +++ b/src/lib/components/ModelDownloader.svelte @@ -92,7 +92,7 @@ {:else} + + {/each} + + + {/if} + + + {#if hasMicroSteps} +
+

MicroSteps

+
    + {#each microSteps! as step} +
  1. {step}
  2. + {/each} +
+
+ {/if} + + +
+ + + +
+ + + diff --git a/src/lib/components/StatusPill.svelte b/src/lib/components/StatusPill.svelte new file mode 100644 index 0000000..f5d2bf0 --- /dev/null +++ b/src/lib/components/StatusPill.svelte @@ -0,0 +1,123 @@ + + + + + + {displayLabel} + + + diff --git a/src/lib/components/WipTaskList.svelte b/src/lib/components/WipTaskList.svelte index a51ce84..5457b6f 100644 --- a/src/lib/components/WipTaskList.svelte +++ b/src/lib/components/WipTaskList.svelte @@ -106,7 +106,7 @@ @@ -912,17 +979,24 @@ {page.timerText} - - + + {#if page.recording} - - - REC - + {:else if modelLoading} - Loading + + {:else if transcribing} + + {:else if saved} + + {:else if error} + {#if transcriptionFailed} + + {:else} + + {/if} {:else} - Ready + {/if} @@ -941,13 +1015,41 @@ Tasks {#if taskCount > 0} - + {taskCount} {/if} + + {#if !page.recording} +
+

+ Profile: {profilesStore.active?.name ?? page.activeProfile ?? '—'} · Model: {selectedModelId()} +

+ + {#if history.length > 0} +
+ + + Last capture + +
+

{history[0]?.date ?? ''}

+

+ {(history[0]?.text ?? '').slice(0, 80)}{(history[0]?.text ?? '').length > 80 ? '…' : ''} +

+ +
+
+ {/if} +
+ {/if} +
@@ -1024,11 +1126,30 @@
{/if} - + {#if error}
-
- {error} +
+
+ {#if transcriptionFailed} + + {:else} + + {/if} +

{error}

+
+ {#if errorDetail} +
+ Technical details +
{errorDetail}
+
+ {/if} +
+ +
{/if} @@ -1041,6 +1162,25 @@
{/if} + + {#if showPostCaptureCard && lastCapture && !page.recording} +
+ { + page.taskSidebarOpen = true; + }} + onExport={() => handleExport('txt')} + onStartFirstMicroStep={() => {}} + onOpenInHistory={() => { page.current = 'history'; }} + /> +
+ {/if} +
@@ -1055,7 +1195,8 @@ {#if showScrollHint} +
+ + + + {:else if testStep === "done_skip"} +
+ +

Almost there

+

Taking you to the app now.

{:else if ritualsStep === "morning"} @@ -199,7 +295,7 @@ onclick={() => answerMorning(false)} >No thanks @@ -223,7 +319,7 @@ onclick={() => answerEvening(false)} >No thanks @@ -280,12 +376,29 @@ {:else} +

Welcome to Lumotia

Press the button. Start talking. That's it.

{#if error} -
{error}
+ +
+

Something went wrong

+

{error}

+
+ + +
+
{/if} {#if systemInfo} diff --git a/src/lib/pages/SettingsPage.svelte b/src/lib/pages/SettingsPage.svelte index d13362c..d3d0310 100644 --- a/src/lib/pages/SettingsPage.svelte +++ b/src/lib/pages/SettingsPage.svelte @@ -10,6 +10,7 @@ import HotkeyRecorder from "$lib/components/HotkeyRecorder.svelte"; import ImplementationRulesEditor from "$lib/components/ImplementationRulesEditor.svelte"; import SettingsGroup from "$lib/components/SettingsGroup.svelte"; + import StatusPill from "$lib/components/StatusPill.svelte"; import ZonePicker from "$lib/components/ZonePicker.svelte"; import AccessibilityControls from "$lib/components/AccessibilityControls.svelte"; import { getPreferences, updatePreferences } from "$lib/stores/preferences.svelte.js"; @@ -186,7 +187,7 @@ try { audioDevices = await invoke("list_audio_devices"); } catch (err) { - audioDevicesError = "Could not enumerate audio devices: " + (err?.message || err); + audioDevicesError = String(err?.message ?? err); audioDevices = []; } } @@ -197,6 +198,7 @@ // the invoke payload's initialPrompt is empty. let vocabulary = $state([]); let vocabularyError = $state(null); + let vocabularyErrorDetail = $state(""); let newVocabTerm = $state(""); let newVocabNote = $state(""); let showBulkVocab = $state(false); @@ -214,11 +216,13 @@ async function refreshVocabulary() { vocabularyError = null; + vocabularyErrorDetail = ""; try { const profileId = profilesStore.activeProfileId; vocabulary = await profilesStore.listTerms(profileId); } catch (err) { - vocabularyError = "Could not load vocabulary: " + (err?.message || err); + vocabularyError = "We couldn't load your custom vocabulary. Your existing transcripts are unaffected."; + vocabularyErrorDetail = String(err?.message ?? err); } } @@ -235,7 +239,8 @@ newVocabNote = ""; await refreshVocabulary(); } catch (err) { - vocabularyError = "Could not save term: " + (err?.message || err); + vocabularyError = "Could not save that term. Your other terms are unaffected."; + vocabularyErrorDetail = String(err?.message ?? err); } } @@ -256,7 +261,8 @@ candidates.push(trimmed); } if (candidates.length === 0) { - vocabularyError = "Nothing to import, paste one term per line or separated by commas."; + vocabularyError = "Nothing to import — paste one term per line or separated by commas."; + vocabularyErrorDetail = ""; return; } @@ -268,6 +274,7 @@ bulkVocabBusy = true; vocabularyError = null; + vocabularyErrorDetail = ""; const failed = []; try { for (const term of toAdd) { @@ -291,7 +298,8 @@ if (skipped > 0) parts.push(`skipped ${skipped} duplicate${skipped === 1 ? "" : "s"}`); if (failed.length > 0) parts.push(`${failed.length} failed`); if (failed.length > 0) { - vocabularyError = `Some terms failed: ${failed.map((f) => f.term).join(", ")}`; + vocabularyError = `${failed.length} term${failed.length === 1 ? "" : "s"} couldn't be saved. Your other terms are unaffected.`; + vocabularyErrorDetail = failed.map((f) => `${f.term}: ${f.message}`).join("\n"); } if (parts.length > 0) { toasts.info("Vocabulary import", parts.join(" · ")); @@ -299,11 +307,13 @@ } async function deleteVocabTerm(id) { + if (!confirm("Remove this vocabulary term? Future transcripts won't get the custom spelling. This cannot be undone.")) return; try { await profilesStore.deleteTerm(id); await refreshVocabulary(); } catch (err) { - vocabularyError = "Could not delete term: " + (err?.message || err); + vocabularyError = "We couldn't remove that term. Your other terms are unaffected."; + vocabularyErrorDetail = String(err?.message ?? err); } } @@ -357,6 +367,7 @@ async function deleteActiveProfile() { const active = profilesStore.active; if (!active || active.id === DEFAULT_PROFILE_ID) return; + if (!confirm("Delete the active profile? Its templates, vocabulary, and settings will be removed. This cannot be undone.")) return; await profilesStore.delete(active.id); } @@ -366,11 +377,15 @@ // automatically. let diagnosticReport = $state(""); let diagnosticReportError = $state(null); + let diagnosticReportErrorDetail = $state(""); let diagnosticReportSavedTo = $state(null); let diagnosticReportLoading = $state(false); + let diagnosticBundleLoading = $state(false); + async function generateDiagnosticReport() { diagnosticReportError = null; + diagnosticReportErrorDetail = ""; diagnosticReportSavedTo = null; diagnosticReportLoading = true; try { @@ -383,7 +398,8 @@ }, }); } catch (err) { - diagnosticReportError = "Could not generate report: " + (err?.message || err); + diagnosticReportError = "Generating the diagnostic bundle didn't work. Your data is unchanged."; + diagnosticReportErrorDetail = String(err?.message ?? err); } finally { diagnosticReportLoading = false; } @@ -395,12 +411,14 @@ await navigator.clipboard.writeText(diagnosticReport); diagnosticReportSavedTo = "Copied to clipboard."; } catch (err) { - diagnosticReportError = "Clipboard copy failed: " + (err?.message || err); + diagnosticReportError = "Copying to clipboard didn't work. Try saving as a file instead."; + diagnosticReportErrorDetail = String(err?.message ?? err); } } async function saveDiagnosticReport() { diagnosticReportError = null; + diagnosticReportErrorDetail = ""; try { const path = await invoke("save_diagnostic_report", { options: { @@ -412,7 +430,35 @@ }); diagnosticReportSavedTo = "Saved to " + path; } catch (err) { - diagnosticReportError = "Save failed: " + (err?.message || err); + diagnosticReportError = "Saving the report file didn't work. Your data is unchanged."; + diagnosticReportErrorDetail = String(err?.message ?? err); + } + } + + async function generateDiagnosticBundle() { + const { save } = await import("@tauri-apps/plugin-dialog"); + const outputPath = await save({ + filters: [{ name: "Zip", extensions: ["zip"] }], + }); + if (!outputPath) return; + diagnosticBundleLoading = true; + try { + const result = await invoke<{ path: string; bytes: number; included: string[]; excluded: string[] }>( + "generate_diagnostic_bundle", + { outputPath }, + ); + const kb = Math.round(result.bytes / 1024); + toasts.success( + "Diagnostic bundle saved", + `${kb} KB saved. You can attach this to a GitHub issue.`, + ); + } catch (err) { + toasts.error( + "Could not save diagnostic bundle", + String(err?.message ?? err), + ); + } finally { + diagnosticBundleLoading = false; } } @@ -665,6 +711,7 @@ } async function deleteSelectedLlmModel() { + if (!confirm("Delete the selected LLM model file? Lumotia won't be able to use it until you re-download. This cannot be undone.")) return; const modelId = selectedLlmModelId(); try { await invoke("delete_llm_model", { modelId }); @@ -826,6 +873,48 @@ page.current = "shutdown"; } + // --- Activation log (Privacy section) --- + interface LumotiaEvent { + id: number; + kind: string; + occurred_at: number; + payload: string | null; + } + + let activationEvents = $state([]); + let loadingEvents = $state(false); + let eventsError = $state(null); + + function formatTimestamp(unixSecs: number): string { + try { + return new Date(unixSecs * 1000).toLocaleString(); + } catch { + return String(unixSecs); + } + } + + async function loadActivationEvents() { + loadingEvents = true; + eventsError = null; + try { + activationEvents = await invoke('list_lumotia_events'); + } catch (err) { + eventsError = String(err); + } finally { + loadingEvents = false; + } + } + + async function confirmAndClear() { + if (!confirm('Clear all activation events? This cannot be undone.')) return; + try { + await invoke('clear_lumotia_events'); + await loadActivationEvents(); + } catch (err) { + eventsError = String(err); + } + } + onMount(async () => { try { await refreshRuntimeCapabilities(); @@ -848,6 +937,9 @@ // reflects reality rather than last-saved intent. syncAutostartFromOs(); + // Activation log — loaded eagerly so Privacy section is ready + loadActivationEvents(); + // Vocabulary is loaded reactively via the $effect that tracks the // active profile id (set once profilesStore.load() resolves in the // root layout). No eager fetch needed here. @@ -1166,17 +1258,14 @@
+ the global hotkey above the eight groups. -->

Theme

@@ -1202,66 +1291,182 @@
- +
-
-

Microphone

-
+ +
+

Microphone

+
+ + +
+ {#if audioDevicesError} +
+

+ + We couldn't list your audio devices. Plug a microphone in or grant Lumotia microphone permission in your OS settings, then try again. +

+
+ Technical details +
{audioDevicesError}
+
+ +
+ {:else if visibleAudioDevices.length === 0} +

No input devices detected. Check that a microphone is connected and PulseAudio/PipeWire is running.

+ {:else} +

+ Auto mode tries the system default first, then any other real input. Speaker-monitor sources (loopback) are skipped. If dictation is silent, pick the device explicitly here. +

+ {/if} +
+ + +
+

Language

+
+ {#if currentModelIsEnglishOnly()} - -
- {#if audioDevicesError} -

{audioDevicesError}

- {:else if visibleAudioDevices.length === 0} -

No input devices detected. Check that a microphone is connected and PulseAudio/PipeWire is running.

{:else} -

- Auto mode tries the system default first, then any other real input. Speaker-monitor sources (loopback) are skipped because they record system audio rather than the microphone. If dictation is silent, pick the device explicitly here. -

+ + {/if} + {#if settings.language === "en"} + {/if}
+

+ {#if currentModelIsEnglishOnly()} + The selected model only supports English in this build. + {:else} + Auto-detect is only meaningful with multilingual models. + {/if} +

+ + +
+

Engine

+ +

+ {settings.engine === "whisper" + ? "Whisper with the currently shipped English-only models in this build" + : "Parakeet CTC 0.6B. English-only, fast when the model is installed"} +

+
+
- + +
+ +
+

Format Mode

+ +

+ {settings.formatMode === "Raw" ? "Exact Whisper output, no formatting" : + settings.formatMode === "Clean" ? "Grouped into paragraphs, punctuation tidied" : + "Structured with lists, headings, and sections"} +

+
+ + +
+

Post-processing

+
+ + +
+
+
+ +

@@ -1404,7 +1609,7 @@ >Add

- +
{#if !showBulkVocab} +
{/if} {#if vocabulary.length === 0} @@ -1472,19 +1693,14 @@ {/if}
- + +
- - - +
- - + + {#if settings.engine === "whisper"}
-

Engine

- -

- {settings.engine === "whisper" - ? "Whisper with the currently shipped English-only models in this build" - : "Parakeet CTC 0.6B. English-only, fast when the model is installed"} -

-
+

Whisper Model

+ +

{modelDescriptions[settings.modelSize]}

- -
-

Format Mode

- -

- {settings.formatMode === "Raw" ? "Exact Whisper output, no formatting" : - settings.formatMode === "Clean" ? "Grouped into paragraphs, punctuation tidied" : - "Structured with lists, headings, and sections"} -

-
- - - {#if settings.engine === "whisper"} -
-

Whisper Model

- -

{modelDescriptions[settings.modelSize]}

- -
- {#if isModelLoaded(settings.modelSize)} - - - Model loaded - - {:else if isModelDownloaded(settings.modelSize)} - - - Downloaded - - - {:else if downloadingModel === whisperModelId(settings.modelSize)} - - {downloadProgress}% - {#if downloadTotal > 0} - · {formatBytes(downloadBytes)} / {formatBytes(downloadTotal)} +
+ {#if isModelLoaded(settings.modelSize)} + + + Model loaded + + {:else if isModelDownloaded(settings.modelSize)} + + + Downloaded + + + {:else if downloadingModel === whisperModelId(settings.modelSize)} + + {downloadProgress}% + {#if downloadTotal > 0} + · {formatBytes(downloadBytes)} / {formatBytes(downloadTotal)} + {/if} + {#if downloadProgress > 0 && downloadStartedAt > 0} + {@const remaining = etaSecondsFromPercent(downloadProgress, downloadStartedAt)} + {#if remaining > 1} + · {formatDuration(remaining)} left {/if} - {#if downloadProgress > 0 && downloadStartedAt > 0} - {@const remaining = etaSecondsFromPercent(downloadProgress, downloadStartedAt)} - {#if remaining > 1} - · {formatDuration(remaining)} left - {/if} - {/if} - - {:else} - - {/if} -
+ {/if} +
+ {:else} + + {/if}
- {:else} -
-

Parakeet Model

-

Parakeet CTC 0.6B (int8). ~613MB, near-instant transcription

+
+ {:else} + +
+

Parakeet Model

+

Parakeet CTC 0.6B (int8). ~613MB, near-instant transcription

-
- {#if parakeetOk} - - - Model loaded - - {:else if parakeetDownloaded} - - - Downloaded - - - {:else if parakeetDownloading} - {parakeetProgress}% downloading... - {:else} - - {/if} -
+
+ {#if parakeetOk} + + + Model loaded + + {:else if parakeetDownloaded} + + + Downloaded + + + {:else if parakeetDownloading} + {parakeetProgress}% downloading... + {:else} + + {/if} +
+
+ {/if} + + +
+

AI Model Tier

+
+ {#each LLM_MODELS as model} + + {/each} +
+
+ +
+
+
+

+ {LLM_MODELS.find((model) => model.id === selectedLlmModelId())?.subtitle || "Local model"} +

+

{llmStatus}

+ {#if llmTestHint} +

{llmTestHint}

+ {/if} +
+ +
+ {#if llmDownloadingModel === selectedLlmModelId()} + + {llmDownloadProgress}% + {#if llmDownloadTotal > 0} + · {formatBytes(llmDownloadBytes)} / {formatBytes(llmDownloadTotal)} + {/if} + {#if llmDownloadProgress > 0 && llmDownloadStartedAt > 0} + {@const remaining = etaSecondsFromPercent(llmDownloadProgress, llmDownloadStartedAt)} + {#if remaining > 1} + · {formatDuration(remaining)} left + {/if} + {/if} + + {:else if !llmModelDownloaded(selectedLlmModelId())} + + {:else if !llmModelLoaded(selectedLlmModelId())} + + + + {:else} + + + + {/if} +
+
+ +

+ Recommended for this machine: + + {LLM_MODELS.find((model) => model.id === (settings.llmModelId || "qwen3_5_4b"))?.subtitle || "Qwen3.5 4B"} + + {#if systemInfo} + · {Math.round((systemInfo.ram_mb || 0) / 1024)} GB RAM detected + {/if} +

+
+ +
+ +
+
+ + + + +
+

+ How the Tasks page surfaces progress. Always additive. +

+ + + +
+ + {#if settings.nudgesEnabled} +
+ + +
+ {/if} +
+
+
+ + + +
+ +
+
+ + + +
+ +
+ + All data stays on your machine. No telemetry, no cloud, no accounts. +
+ + +
+

AI use

+

+ The local LLM is used for transcript cleanup and task extraction only — both run fully offline after download. No voice, transcript, or task data is sent to any server. + See privacy-and-ai-use.md for details. +

+
+ + +
+

Activation log

+

+ This log lives on your device only. We never send it anywhere. It records anonymous milestones (first capture, first export, first task extracted) so you can see your own usage shape. +

+ + + + {#if loadingEvents} +

Loading…

+ {:else if eventsError} +

Couldn't load activation log.

+ {:else if activationEvents.length === 0} +

No activation events recorded yet.

+ {:else} +
+ + + + + + + + + {#each activationEvents as ev} + + + + + {/each} + +
KindWhen
{ev.kind}{formatTimestamp(ev.occurred_at)}
{/if} - -
-

Compute Device

- {#if hasGpuAcceleration()} - -

Only accelerators built into this binary are shown here.

- {:else} -
- This build is CPU-only. GPU controls appear in GPU-enabled builds. -
- {/if} -
- - -
-

Language

-
- {#if currentModelIsEnglishOnly()} - - {:else} - - {/if} - {#if settings.language === "en"} - - {/if} -
-

- {#if currentModelIsEnglishOnly()} - The selected model only supports English in this build. - {:else} - Auto-detect is only meaningful with multilingual models. - {/if} -

-
+
+ + +
+

Data location

+

+ All transcripts, models, and settings are stored in your OS app-data directory. No files are written outside that path. +

+
+
- + +
-
- - -
-
-
- - -
-

- Local LLM for transcript cleanup, smart task extraction, and task breakdown. Runs fully offline after the model is downloaded. -

- +

Feature Tier

@@ -1893,213 +2247,85 @@

+
-

Model Tier

-
- {#each LLM_MODELS as model} - - {/each} -
-
- -
-
-
-

- {LLM_MODELS.find((model) => model.id === selectedLlmModelId())?.subtitle || "Local model"} -

-

{llmStatus}

- {#if llmTestHint} -

{llmTestHint}

- {/if} -
- -
- {#if llmDownloadingModel === selectedLlmModelId()} - - {llmDownloadProgress}% - {#if llmDownloadTotal > 0} - · {formatBytes(llmDownloadBytes)} / {formatBytes(llmDownloadTotal)} - {/if} - {#if llmDownloadProgress > 0 && llmDownloadStartedAt > 0} - {@const remaining = etaSecondsFromPercent(llmDownloadProgress, llmDownloadStartedAt)} - {#if remaining > 1} - · {formatDuration(remaining)} left - {/if} - {/if} - - {:else if !llmModelDownloaded(selectedLlmModelId())} - - {:else if !llmModelLoaded(selectedLlmModelId())} - - - - {:else} - - - - {/if} -
-
- -

- Recommended for this machine: - - {LLM_MODELS.find((model) => model.id === (settings.llmModelId || "qwen3_5_4b"))?.subtitle || "Qwen3.5 4B"} - - {#if systemInfo} - · {Math.round((systemInfo.ram_mb || 0) / 1024)} GB RAM detected +

Cleanup preset

+ +

+ {#if settings.llmPromptPreset === "email"} + Formats output as an email paragraph, tight sentences, no markdown, no auto-added greeting or signoff. + {:else if settings.llmPromptPreset === "notes"} + Formats action items as a bullet list led by imperative verbs; keeps prose informational sentences as prose. + {:else if settings.llmPromptPreset === "code"} + Preserves technical terms, variable names, file paths, and symbols exactly as spoken. No translation of identifiers. + {:else} + No preset, the active profile's prompt governs tone alone. {/if}

-
- +
+

GPU concurrency

+ +

+ {#if settings.aiGpuConcurrency === "sequential"} + On tight-VRAM cards (≤6 GB), loading Whisper + LLM together OOMs. Sequential mode frees the other model before loading; adds a small reload pause between transcribe and cleanup. + {:else} + Both models stay resident in GPU memory. Faster transitions, but needs enough VRAM to hold both at once. + {/if} +

- - -
- -
-

Cleanup preset

- -

- {#if settings.llmPromptPreset === "email"} - Formats output as an email paragraph, tight sentences, no markdown, no auto-added greeting or signoff. - {:else if settings.llmPromptPreset === "notes"} - Formats action items as a bullet list led by imperative verbs; keeps prose informational sentences as prose. - {:else if settings.llmPromptPreset === "code"} - Preserves technical terms, variable names, file paths, and symbols exactly as spoken. No translation of identifiers. - {:else} - No preset, the active profile's prompt governs tone alone. - {/if} -

+ +
+

Compute Device

+ {#if hasGpuAcceleration()} + +

Only accelerators built into this binary are shown here.

+ {:else} +
+ This build is CPU-only. GPU controls appear in GPU-enabled builds.
- - -
-

GPU concurrency

- -

- {#if settings.aiGpuConcurrency === "sequential"} - On tight-VRAM cards (≤6 GB), loading Whisper + LLM together OOMs. Sequential mode frees the other model before loading; adds a small reload pause between transcribe and cleanup. - {:else} - Both models stay resident in GPU memory. Faster transitions, but needs enough VRAM to hold both at once. - {/if} -

-
-
- + {/if} +
+ + +
+ +
+
- -
- -
-
- - - - + -
-
+ -
-

- How the Tasks page surfaces progress. Always additive. -

+ +
+

+ Uses your operating system's built-in voices. No audio leaves the machine. +

- - -
-
- - -
-

- Gentle, anticipatory reminders. Capped at 3 per hour. Never fires while you're looking at Lumotia. -

- - - - {#if settings.nudgesEnabled} -
- - -
- {/if} -
-
- - - - - -
-

- Uses your operating system's built-in voices. No audio leaves the machine. -

- -
- - - {#if ttsVoicesError} -

{ttsVoicesError}

- {:else if ttsVoicesLoaded && ttsVoices.length === 0} -

- No additional voices reported by the system synth. Install extra voices through your OS accessibility settings. -

- {/if} -
- -
- - -
- Slower - Normal - Faster -
-
- - -
-
- - -
-
- - - - - {#if settings.meetingAutoCapture} -
-

Apps to watch (comma-separated)

- { - const raw = event.currentTarget.value; - settings.meetingAutoCaptureApps = raw - .split(",") - .map((entry) => entry.trim().toLowerCase()) - .filter((entry) => entry.length > 0); - }} - /> -
- {/if} - - {#if settings.soundCues} -
- - - {Math.round(settings.soundCueVolume * 100)}% - -
- {/if} - - - {#if settings.saveAudio} -
-

Output Folder

-
-
-

- {outputFolderPreview} -

-
+
+ + + {#if ttsVoicesError} +
+

+ + We couldn't load the text-to-speech voices. The rest of Settings still works. +

+
+ Technical details +
{ttsVoicesError}
+
- {#if settings.outputFolder} - - {/if} + class="mt-1.5 text-[11px] text-accent underline underline-offset-2 hover:text-accent/80" + onclick={refreshTtsVoices} + type="button" + >Try again
-
- {/if} -
-
- - + {:else if ttsVoicesLoaded && ttsVoices.length === 0} +

+ No additional voices reported by the system synth. Install extra voices through your OS accessibility settings. +

+ {/if} +
- - +
+ + +
+ Slower + Normal + Faster +
+
+ + +
+
+ + +
+
+ + + + + {#if settings.meetingAutoCapture} +
+

Apps to watch (comma-separated)

+ { + const raw = event.currentTarget.value; + settings.meetingAutoCaptureApps = raw + .split(",") + .map((entry) => entry.trim().toLowerCase()) + .filter((entry) => entry.length > 0); + }} + /> +
+ {/if} + + {#if settings.soundCues} +
+ + + {Math.round(settings.soundCueVolume * 100)}% + +
+ {/if} + + + {#if settings.saveAudio} +
+

Output Folder

+
+
+

+ {outputFolderPreview} +

+
+ + {#if settings.outputFolder} + + {/if} +
+
+ {/if} +
+
+
+
+ +
- -
- -
-
- +
-
{engineStatus} @@ -2515,7 +2673,6 @@

Lumotia v1.0 · Powered by whisper.cpp · Built by CORBEL Ltd

-

Diagnostics

@@ -2542,7 +2699,23 @@ {/if}

{#if diagnosticReportError} -

{diagnosticReportError}

+
+

+ + {diagnosticReportError} +

+ {#if diagnosticReportErrorDetail} +
+ Technical details +
{diagnosticReportErrorDetail}
+
+ {/if} + +
{/if} {#if diagnosticReportSavedTo}

{diagnosticReportSavedTo}

@@ -2557,6 +2730,62 @@
+ + + + +
+
+

+ Need to walk through onboarding again? Replay the first-run tutorial and + the gate will skip past your existing data. +

+ +
+ +
+ +

+ Logs + system info + redacted preferences. Never includes audio or transcript content. +

+
+ +
+

+ Known limitations for this release are documented in + KNOWN-ISSUES.md. + Bugs and feature requests can be filed at + github.com/jakeadriansames/lumotia/issues. +

+
+
+
diff --git a/src/lib/pages/ShutdownRitualPage.svelte b/src/lib/pages/ShutdownRitualPage.svelte index 78c890d..2bf3488 100644 --- a/src/lib/pages/ShutdownRitualPage.svelte +++ b/src/lib/pages/ShutdownRitualPage.svelte @@ -158,7 +158,7 @@
@@ -686,7 +687,7 @@ aria-label="Delete completed task" class="mt-0.5 text-text-tertiary hover:text-danger opacity-0 group-hover:opacity-100 focus:opacity-100" style="transition: opacity var(--duration-ui)" - onclick={() => deleteTask(task.id)} + onclick={() => { if (confirm("Delete this task? This cannot be undone.")) deleteTask(task.id); }} >
- + >{autostartApplying ? 'Saving…' : 'Yes, launch at login'}
@@ -363,11 +353,8 @@

Downloading model

{downloadingModel}

-
-
+
+

{downloadProgress}%

{#if estimatedMinutes > 2} @@ -383,21 +370,18 @@ {#if error} -
-

Something went wrong

-

{error}

-
- - -
+
+ +

{error}

+
+ { error = ""; probe(); }}> + Try again + + + Skip this step + +
+
{/if} From b6c065ffd1c6fedb85ec93d46b954f5404155f01 Mon Sep 17 00:00:00 2001 From: Jake Date: Fri, 15 May 2026 08:57:37 +0100 Subject: [PATCH 76/82] =?UTF-8?q?v0.2=20Phase=207.4:=20TasksPage=20?= =?UTF-8?q?=E2=80=94=20minimal=20wrapper=20sweep?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Targeted migration per plan ("wrap, don't rewrite"). The page's identity surfaces (energy chips, search input, quick-capture input, bucket tabs, WipTaskList) stay verbatim — their rich ARIA and custom radio-group semantics outweigh wrapper coherence here. - Dead Card import removed (never used in markup) - EmptyState → LumotiaEmptyState - "Pop out" toolbar button → LumotiaButton variant=tertiary WipTaskList, CompletionSparkline, EnergyChip stay bespoke per docs/release/v0.2-frontend-overhaul.md §6.3. Per-page gate: npm run check (0/0/5704 files). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/pages/TasksPage.svelte | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lib/pages/TasksPage.svelte b/src/lib/pages/TasksPage.svelte index f175860..b8de465 100644 --- a/src/lib/pages/TasksPage.svelte +++ b/src/lib/pages/TasksPage.svelte @@ -10,11 +10,11 @@ } from "$lib/stores/page.svelte.js"; import { recentCompletions, todayCount } from "$lib/stores/completionStats.svelte"; import WipTaskList from '$lib/components/WipTaskList.svelte'; - import EmptyState from '$lib/components/EmptyState.svelte'; + import LumotiaEmptyState from '$lib/ui/LumotiaEmptyState.svelte'; + import LumotiaButton from '$lib/ui/LumotiaButton.svelte'; import CompletionSparkline from "$lib/components/CompletionSparkline.svelte"; import EnergyChip from '$lib/components/EnergyChip.svelte'; import { SquareCheck, Search, ExternalLink, ChevronLeft, ArrowUpDown, Plus, X, ChevronRight, Zap } from 'lucide-svelte'; - import Card from "$lib/components/Card.svelte"; import { formatTimestamp } from "$lib/utils/time.js"; import { BUCKET_COLORS, EFFORT_LABELS, EFFORT_ORDER } from "$lib/utils/constants.js"; @@ -360,15 +360,15 @@
- +
@@ -576,7 +576,7 @@
{#if filteredTasks.length === 0} - Date: Fri, 15 May 2026 08:58:42 +0100 Subject: [PATCH 77/82] =?UTF-8?q?v0.2=20Phase=207.5:=20HistoryPage=20?= =?UTF-8?q?=E2=80=94=20wrapper=20sweep?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Targeted migration on a 1 225-LOC page. The FTS5 search input stays a plain (LumotiaCombobox needs an options list; free-text search doesn't fit the API cleanly enough to justify a rewrite for v0.2). Row patterns + clear-all modal stay verbatim — their bespoke ARIA and inline arm-confirm state are core to the page's identity. - Card import → LumotiaCard (4 use sites bulk-swapped) - EmptyState import → LumotiaEmptyState (4 use sites) - LumotiaButton import added for selective use in follow-up sweeps Bespoke surfaces left verbatim: VirtualSegmentList, audio player, clear-all type-the-word modal, tag-chip filter bar. Per-page gate: npm run check (0/0/5704 files). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/lib/pages/HistoryPage.svelte | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/lib/pages/HistoryPage.svelte b/src/lib/pages/HistoryPage.svelte index 7ead932..f6eb36f 100644 --- a/src/lib/pages/HistoryPage.svelte +++ b/src/lib/pages/HistoryPage.svelte @@ -22,8 +22,9 @@ import { clampTextLines, measurePreWrap } from "$lib/utils/textMeasure.js"; import { bodyPretextLineHeight, pretextFontShorthand } from "$lib/utils/accessibilityTypography.js"; import { buildCumulativeOffsets, findVisibleRange } from "$lib/utils/virtualList.js"; - import Card from "$lib/components/Card.svelte"; - import EmptyState from "$lib/components/EmptyState.svelte"; + import LumotiaCard from "$lib/ui/LumotiaCard.svelte"; + import LumotiaEmptyState from "$lib/ui/LumotiaEmptyState.svelte"; + import LumotiaButton from "$lib/ui/LumotiaButton.svelte"; import { formatTime, formatDuration } from "$lib/utils/time.js"; import { PLAYBACK_SPEEDS } from "$lib/utils/constants.js"; import { Search, Clock, Play, Pause, FileText, Mic, ChevronDown, ExternalLink, Star, Tag } from 'lucide-svelte'; @@ -835,7 +836,7 @@ {#if viewMode === "live"}
- +
-
+
@@ -928,9 +929,9 @@
- + {#if filtered.length === 0} - @@ -1164,7 +1165,7 @@
{/if} - +
{:else} {#if page.recording} - + {:else if modelLoading} - + {:else if transcribing} - + {:else if saved} - + {:else if error} {#if transcriptionFailed} - + {:else} - + {/if} {:else} - + {/if} @@ -1132,9 +1133,9 @@
{#if transcriptionFailed} - + {:else} - + {/if}

{error}

@@ -1156,16 +1157,14 @@ {#if liveWarning && !error}
-
- {liveWarning} -
+ {liveWarning}
{/if} {#if showPostCaptureCard && lastCapture && !page.recording}
-
- + {#if transcriptionFailed && !transcript.trim() && !page.recording}
- +
{:else if !transcript.trim() && !page.recording && !transcribing}
- +