feat(gui): wire Footer + Settings to daemon over IPC (closes the gap from a6a098d)

a6a098d added daemon-side IPC handlers (SwitchLlm, ReloadLlm, QueryHealth)
but the GUI was still talking only to its own process. This commit wires
the frontend to actually use those handlers.

frontend/src/lib/ipc.ts
  - New DaemonHealth interface + daemonHealth: writable<DaemonHealth | null>.
  - handleEvent: "health_snapshot" case fills daemonHealth from daemon's IpcEvent.
  - New senders:
      switchDaemonLlm(backend)  → action "switch_llm"
      reloadDaemonLlm()          → action "reload_llm"
      queryDaemonHealth()        → action "query_health"

frontend/src/stores.ts
  - Re-exports daemonHealth + the three new senders + DaemonHealth type.

frontend/src/components/Footer.svelte
  - Polls every 5s but PREFERS IPC: if daemon connected, calls queryDaemonHealth
    (snapshot arrives via daemonHealth store).
  - Falls back to invoke("get_active_backends") if daemon offline — shows
    GUI-process view in that case, with the chip tooltip distinguishing
    "daemon" vs "gui-process" source.

frontend/src/routes/settings/index.svelte (AI Backends tab)
  - applyLlmBackend now ALSO fires switchDaemonLlm so the running listener
    picks the new backend without restart.
  - "Auto" (empty) path calls reloadDaemonLlm so daemon re-reads DB.

End-to-end flow now:
  GUI dropdown → set_llm_backend (Tauri, persists DB + swaps GUI proc)
              ↘ switchDaemonLlm (IPC, swaps daemon proc too)
              ↘ daemon emits health_snapshot on next QueryHealth
              ↘ daemonHealth store updates
              ↘ Footer chips re-render with new state

Build: cargo build --release -p jarvis-gui green.
This commit is contained in:
Bossiara13 2026-05-15 17:42:40 +03:00
parent a6a098de15
commit a2dfadf5c1
4 changed files with 90 additions and 6 deletions

View file

@ -2,7 +2,10 @@
import { onMount, onDestroy } from "svelte" import { onMount, onDestroy } from "svelte"
import { invoke } from "@tauri-apps/api/core" 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) $: t = (key: string) => translate($translations, key)
@ -29,10 +32,30 @@
let backends: ActiveBackends | null = null let backends: ActiveBackends | null = null
let pollId: ReturnType<typeof setInterval> | null = null let pollId: ReturnType<typeof setInterval> | 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() { 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 { try {
backends = await invoke<ActiveBackends>("get_active_backends") const fallback = await invoke<ActiveBackends>("get_active_backends")
if (!backends) backends = fallback
} catch (err) { } catch (err) {
// ignore // ignore
} }
@ -49,9 +72,10 @@
function chipTitle(key: "tts" | "llm"): string { function chipTitle(key: "tts" | "llm"): string {
if (!backends) return "" 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})` : "" 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 { function chipLabel(value: string): string {

View file

@ -12,6 +12,21 @@ export const lastRecognizedText = writable("")
export const lastExecutedCommand = writable("") export const lastExecutedCommand = writable("")
export const lastError = 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<DaemonHealth | null>(null)
// ### CONNECTION ### // ### CONNECTION ###
const IPC_URL = "ws://127.0.0.1:9712" const IPC_URL = "ws://127.0.0.1:9712"
@ -133,6 +148,19 @@ function handleEvent(data: any) {
// bring window to foreground // bring window to foreground
revealWindow() revealWindow()
break 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 }) 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() { async function revealWindow() {
try { try {
const window = getCurrentWindow() const window = getCurrentWindow()

View file

@ -5,7 +5,10 @@
import { setTimeout } from "worker-timers" import { setTimeout } from "worker-timers"
import { showInExplorer } from "@/functions" 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 HDivider from "@/components/elements/HDivider.svelte"
import Footer from "@/components/Footer.svelte" import Footer from "@/components/Footer.svelte"
@ -113,6 +116,7 @@
if (target === "") { if (target === "") {
try { try {
await invoke("db_write", { key: "llm_backend", val: "" }) await invoke("db_write", { key: "llm_backend", val: "" })
reloadDaemonLlm() // tell daemon to re-read DB (now auto)
} catch (err) { } catch (err) {
backendSwapError = String(err) backendSwapError = String(err)
} }
@ -121,7 +125,11 @@
} }
backendSwapBusy = true backendSwapBusy = true
try { try {
// 1. Swap on GUI process (persists to DB).
await invoke<string>("set_llm_backend", { name: target }) await invoke<string>("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() await refreshActiveBackends()
} catch (err) { } catch (err) {
backendSwapError = String(err) backendSwapError = String(err)

View file

@ -8,6 +8,7 @@ export {
lastRecognizedText, lastRecognizedText,
lastExecutedCommand, lastExecutedCommand,
lastError, lastError,
daemonHealth,
connectIpc, connectIpc,
enableIpc, enableIpc,
disableIpc, disableIpc,
@ -16,9 +17,14 @@ export {
sendIpcMessage, sendIpcMessage,
sendTextCommand, sendTextCommand,
stopJarvisApp, stopJarvisApp,
reloadCommands reloadCommands,
switchDaemonLlm,
reloadDaemonLlm,
queryDaemonHealth,
} from "./lib/ipc" } from "./lib/ipc"
export type { DaemonHealth } from "./lib/ipc"
// re-export i18n // re-export i18n
export { export {
translations, translations,