diff --git a/frontend/src/components/Footer.svelte b/frontend/src/components/Footer.svelte index 4c53fb9..439dc96 100644 --- a/frontend/src/components/Footer.svelte +++ b/frontend/src/components/Footer.svelte @@ -2,7 +2,10 @@ import { onMount, onDestroy } from "svelte" import { invoke } from "@tauri-apps/api/core" - import { appInfo, translations, translate } from "@/stores" + import { + appInfo, translations, translate, + daemonHealth, ipcConnected, queryDaemonHealth, + } from "@/stores" $: t = (key: string) => translate($translations, key) @@ -29,10 +32,30 @@ let backends: ActiveBackends | null = null let pollId: ReturnType | null = null + let connected = false + + ipcConnected.subscribe(v => { connected = v }) + daemonHealth.subscribe(h => { + if (h) { + backends = { + tts: h.tts_backend, + llm: h.llm_backend, + llm_model: h.llm_model, + profile: h.active_profile, + } + } + }) async function refreshBackends() { + // Prefer IPC — daemon's actual state. + if (connected) { + queryDaemonHealth() // response arrives via daemonHealth store + return + } + // Fallback to GUI-process view if daemon offline. try { - backends = await invoke("get_active_backends") + const fallback = await invoke("get_active_backends") + if (!backends) backends = fallback } catch (err) { // ignore } @@ -49,9 +72,10 @@ function chipTitle(key: "tts" | "llm"): string { if (!backends) return "" - if (key === "tts") return `TTS backend: ${backends.tts}` + const src = connected ? "daemon" : "gui-process" + if (key === "tts") return `TTS backend: ${backends.tts} (${src})` const model = backends.llm_model ? ` (${backends.llm_model})` : "" - return `LLM backend: ${backends.llm}${model}` + return `LLM backend: ${backends.llm}${model} (${src})` } function chipLabel(value: string): string { diff --git a/frontend/src/lib/ipc.ts b/frontend/src/lib/ipc.ts index e4e1beb..f20202d 100644 --- a/frontend/src/lib/ipc.ts +++ b/frontend/src/lib/ipc.ts @@ -12,6 +12,21 @@ export const lastRecognizedText = writable("") export const lastExecutedCommand = writable("") export const lastError = writable("") +// Daemon runtime state — populated by health_snapshot events from jarvis-app. +// `null` means we never received one (daemon not running or doesn't support +// QueryHealth yet). +export interface DaemonHealth { + tts_backend: string + llm_backend: string + llm_model: string | null + active_profile: string + memory_facts: number + scheduled_tasks: number + language: string + version: string | null +} +export const daemonHealth = writable(null) + // ### CONNECTION ### const IPC_URL = "ws://127.0.0.1:9712" @@ -133,6 +148,19 @@ function handleEvent(data: any) { // bring window to foreground revealWindow() break + + case "health_snapshot": + daemonHealth.set({ + tts_backend: data.tts_backend ?? "none", + llm_backend: data.llm_backend ?? "none", + llm_model: data.llm_model ?? null, + active_profile: data.active_profile ?? "default", + memory_facts: data.memory_facts ?? 0, + scheduled_tasks: data.scheduled_tasks ?? 0, + language: data.language ?? "ru", + version: data.version ?? null, + }) + break } } @@ -175,6 +203,24 @@ export function sendTextCommand(text: string): boolean { return sendAction("text_command", { text }) } +// IMBA: hot-swap daemon's LLM backend over IPC so the running listener picks +// the new backend immediately. Returns false if daemon not connected. +export function switchDaemonLlm(backend: "groq" | "ollama"): boolean { + return sendAction("switch_llm", { backend }) +} + +// Tell daemon to re-read llm_backend from settings DB. Use after GUI changes +// the DB independently. +export function reloadDaemonLlm(): boolean { + return sendAction("reload_llm") +} + +// Ask daemon for a runtime snapshot. Daemon will respond with a +// "health_snapshot" event which fills the `daemonHealth` store. +export function queryDaemonHealth(): boolean { + return sendAction("query_health") +} + async function revealWindow() { try { const window = getCurrentWindow() diff --git a/frontend/src/routes/settings/index.svelte b/frontend/src/routes/settings/index.svelte index d6a3f6b..e635afa 100644 --- a/frontend/src/routes/settings/index.svelte +++ b/frontend/src/routes/settings/index.svelte @@ -5,7 +5,10 @@ import { setTimeout } from "worker-timers" import { showInExplorer } from "@/functions" - import { appInfo, assistantVoice, translations, translate } from "@/stores" + import { + appInfo, assistantVoice, translations, translate, + switchDaemonLlm, reloadDaemonLlm, + } from "@/stores" import HDivider from "@/components/elements/HDivider.svelte" import Footer from "@/components/Footer.svelte" @@ -113,6 +116,7 @@ if (target === "") { try { await invoke("db_write", { key: "llm_backend", val: "" }) + reloadDaemonLlm() // tell daemon to re-read DB (now auto) } catch (err) { backendSwapError = String(err) } @@ -121,7 +125,11 @@ } backendSwapBusy = true try { + // 1. Swap on GUI process (persists to DB). await invoke("set_llm_backend", { name: target }) + // 2. Tell daemon to swap too, so the running listener uses the + // new backend immediately. No-op if daemon not connected. + switchDaemonLlm(target as "groq" | "ollama") await refreshActiveBackends() } catch (err) { backendSwapError = String(err) diff --git a/frontend/src/stores.ts b/frontend/src/stores.ts index 56eaae1..562538f 100644 --- a/frontend/src/stores.ts +++ b/frontend/src/stores.ts @@ -8,6 +8,7 @@ export { lastRecognizedText, lastExecutedCommand, lastError, + daemonHealth, connectIpc, enableIpc, disableIpc, @@ -16,9 +17,14 @@ export { sendIpcMessage, sendTextCommand, stopJarvisApp, - reloadCommands + reloadCommands, + switchDaemonLlm, + reloadDaemonLlm, + queryDaemonHealth, } from "./lib/ipc" +export type { DaemonHealth } from "./lib/ipc" + // re-export i18n export { translations,