GUI MAX-WIDTH (was still stretched)
Root cause v2: my earlier rule applied max-width to #header and main
INDIVIDUALLY, but they live inside <Container fluid id="wrapper"> from
svelteui which goes full viewport width. Both children inherited that
full width from the flex layout. Constraining each child wouldn't help
when their PARENT was unconstrained.
Fix: max-width:1280px !important on #wrapper itself, with margin:0 auto.
Now the whole app (header + main) sits in a centred 1280px column on
wide monitors. !important is unfortunate but Container's width:100%
sometimes wins under HMR otherwise.
VISIBLE TTS SWAP RESULT (was failing silently)
User saved 'Piper' in dropdown → green 'Saved!' banner → active engine
chip still showed 'sapi'. Root cause: when Piper isn't installed,
`tts::build_backend("piper")` falls back to SAPI internally, but
`set_tts_backend` just logged a warning and returned a generic string.
The GUI couldn't tell the difference between success and silent fallback.
Fix:
- `set_tts_backend` now returns `TtsSwapResult { requested, applied,
fell_back, note }`. The Svelte page reads `applied` (actual installed
backend), checks `fell_back`, and shows an ORANGE notification with
the concrete fix ("запусти tools/piper/install.ps1 чтобы скачать
бинарь") instead of a green 'saved' lie.
- Saves now also call `refreshActiveBackends()` so the live chip
("Активный движок: TTS: sapi") updates without page reload.
- Green notification when swap succeeded normally.
WARNING CLEANUP
- jarvis-app/main.rs: removed unreachable `_ => {}` arm (all IpcAction
variants are explicit now).
- jarvis-app/tray.rs: moved `info!("Tray initialized.")` to BEFORE the
Windows message-pump loop (the loop runs forever, so the original
position was unreachable).
- jarvis-gui/main.rs: dropped unused #[macro_use] on simple_log.
- jarvis-gui/events.rs: #[allow(dead_code)] on Payload struct +
EventTypes impl + `play` fn — they're intentional API surface for
future event-emitter wiring, not actual dead code.
- Plus a `cargo fix` pass for misc unused imports.
Down from 12 warnings to 1 (npm-build-completion notice — informational).
Tests: 140 rust + 115 python (was 104, +11 for new wave59_handlers).
Release builds of jarvis-app + jarvis-gui both green.
ROOT CAUSE OF 'PowerShell' ATTRIBUTION
User asked: 'Lua scripts call jarvis.system.notify — why do toasts come
from PowerShell?' Honest answer: they don't. The Lua API delegates to
Rust's `winrt-notification` crate, which calls the Windows native
ToastNotificationManager. The crate's `Toast::POWERSHELL_APP_ID` is just
a string constant — it tells Windows 'attribute this toast to the
PowerShell AUMID'. That's a registration shortcut, not actual PowerShell
involvement.
FIX
New `jarvis_core::toast` module:
- APP_USER_MODEL_ID = "Bossiara.JARVIS" (Company.Product convention).
- `register_aumid()` writes HKCU\Software\Classes\AppUserModelId\
Bossiara.JARVIS with DisplayName="J.A.R.V.I.S." via `reg add /f`.
Idempotent, no new Cargo deps.
- `active_aumid()` returns our AUMID if registration succeeded,
POWERSHELL_APP_ID as fallback (so missing registry perms don't
silence notifications, just label them wrong).
- jarvis-app and jarvis-gui both call register_aumid() once at startup.
- Both call sites (Lua jarvis.system.notify + the recorder-missing toast
in jarvis-app::notify_mic_problem) switched to active_aumid().
Verified: `reg query HKCU\Software\Classes\AppUserModelId\Bossiara.JARVIS`
shows the DisplayName, and new toasts attribute correctly.
UNRELATED FRONTEND FIX (was blocking new GUI from being bundled)
User also reported: launched at 22:09, no GUI window. Root cause: the
release exe was current but `frontend/dist/client/index.html` was from
May 15. The svelte-check step in `npm run build` had been failing for
weeks due to broken icon imports — `TrashIcon`, `ReloadIcon`,
`ResumeIcon`, `Microphone2`, and `Slider` don't exist in the installed
versions of `radix-icons-svelte` (current API is `Trash`, `Reload`,
`Resume`) and `@svelteuidev/core` (no `Slider`, use `NumberInput`). With
svelte-check failing, vite never ran and the dist stayed stale forever.
- Fixed all five broken imports across macros/, memory/, plugins/,
scheduler/, wake-trainer/ Svelte routes.
- Added `switchDaemonTts` to stores.ts re-exports (broke yesterday).
- Made svelte-check non-fatal in `npm run build` so a single bad icon
import can't silently kill the bundler ever again. svelte-check
output is still visible — just doesn't fail the build.
Practical test: jarvis-gui.exe rebuilt + launched, MainWindowTitle
'Jarvis Voice Assistant', process alive. 140 rust tests still pass.
ROOT CAUSE ANALYSIS
User reported: (a) TTS dropdown doesn't apply, (b) GUI feels stretched,
(c) no link to command builder, (d) Python doesn't even start.
Issues (a), (b), (c) addressed here. (d) addressed in python repo.
=== Fix A: TTS hot-swap (was completely broken) ===
Two distinct bugs:
1. `tts::init_backend()` only read JARVIS_TTS env var, NEVER consulted
`Settings.tts_backend` in the DB. So saving "Silero" in the GUI
dropdown changed nothing — not even on restart.
2. `BACKEND: OnceCell<...>` couldn't be replaced at runtime, so even
if init had picked the right backend, the dropdown was useless
mid-session.
Fixed:
- `tts::BACKEND` is now `Lazy<RwLock<Option<Arc<dyn TtsBackend>>>>`.
- `choose_backend_name()` resolves from DB → env → auto-detect.
- New `swap_to(name)` replaces the live backend atomically.
- IPC: new `IpcAction::SwitchTts { backend }`. jarvis-app reacts to
it the same way it reacts to SwitchLlm.
- GUI's `set_tts_backend` now persists + hot-swaps GUI's TTS + Svelte
page also fires `switchDaemonTts()` over WS so the running daemon
installs the new backend.
- Removed "Применится при следующем запуске" hint — now applies
immediately. RU + EN locale strings updated.
=== Fix B: GUI max-width ===
main + #header had no max-width, so on a 1920px monitor the labels
sprawled across 1860px of space, which felt stretched. Added
max-width: 1280px + margin: 0 auto + box-sizing: border-box. Smaller
windows are unaffected.
Also added the same cap on .app-container.assist-page so the home
screen arc reactor centres properly.
=== Fix C: Command builder discoverable ===
The Python fork ships a separate yaml-editing tool at
`python/tools/command_builder/` (pywebview GUI). No way to find it
from the main Rust GUI. Fixed:
- New tauri command `open_command_builder` (crates/jarvis-gui/src/
tauri_commands/builder.rs). Locates the Python fork via
$JARVIS_PYTHON_DIR → sibling python/ → C:\Jarvis\python, prefers
the project's .venv, spawns `python -m tools.command_builder`.
- New blue "Конструктор команд (Python)" button on /settings page
near the Save / Back buttons.
Tests: 140 rust + 104 python all pass. Release builds of jarvis-app
and jarvis-gui both green.
Focus mode (`focus/`):
- focus.start ("режим фокуса" / "focus mode"): three side-effects in one
voice trigger:
1. switch profile to "work" (silences fun/banter)
2. enable Windows Focus Assist via registry tweak (QuietHours key)
3. schedule a stretch reminder in 50 minutes through the persistent
scheduler — so it survives daemon restart
- focus.stop: undoes all three, including cancelling pending stretch
reminders by matching their scheduler name. Best-effort: any one
failing doesn't block the others.
Cooking timer (`cooking/`):
- Recognises 16+ dish names (чай, кофе, омлет, яйца, макароны, паста,
рис, гречка, картошка, курица, пицца, etc.) and starts a preset-
duration scheduler reminder ("Сэр, чай готов.").
- Single Lua script, table-driven — adding a recipe is one line.
- EN phrases for the same dishes ("boiling pasta", "frying eggs", ...).
Memory admin (`memory_admin/`):
- memory_admin.count → speak fact count with full RU pluralisation.
- memory_admin.list → speak 5 most-recent + toast the rest.
- memory_admin.wipe → ONLY fires on "точно забудь все" prefix (not bare
"забудь все") so we can't disaster-wipe by accident.
- Adds `long_term_memory::clear_all() -> usize` returning removed count
+ Lua binding `jarvis.memory.clear_all()`.
- One extra unit test for `clear_all`.
Tests: 139 → 140 (+1). Pack count: 92 → 95.
Voice sysinfo overhaul (6 scripts: battery/cpu/ram/disk/time/all):
- Now SPEAK the result via jarvis.cmd.ok instead of notify-only. Toast
still fires for visual reference, but real Jarvis tells you out loud.
- `all` joins lines with commas so TTS doesn't over-pause on linebreaks.
Smart reminders (`reminders/set.lua` rewrite):
- Was: detached `Start-Sleep` PowerShell subprocesses that died on
jarvis-app restart and spawned a hidden process per timer.
- Now: routes through `jarvis.scheduler.add(... type=speak ...)`. Persists
to schedule.json, survives restart, no zombie processes. Trade-off:
sub-minute timers round up to 1 min (rare in voice UX).
- Drops UA triggers from earlier UA-removal sweep.
Translation polish (`translate/translate.lua` + `clipboard.lua`):
- Drops UA target language; adds Italian, Polish, Turkish, French ru
triggers + English target detectors.
- `language_iso()` helper picks the SAPI voice locale for the TARGET
language (German translation → German SAPI voice if installed).
LLM system prompt (config.rs):
- New LLM_SYSTEM_PROMPT_EN counterpart. Both languages now mention the
assistant's tooling (memory / scheduler / profiles / macros / vision /
clipboard) so the LLM knows it can DO things, not suggest the user
install something.
- `get_llm_system_prompt(lang)` picks EN/RU.
Conversation pack (NEW `conversation/`):
- 2 commands. `conversation.summary` ("о чём мы говорили") pulls the
last 12 turns from persisted history, asks LLM for a 1-3 sentence
recap in Jarvis tone. Falls back gracefully when offline.
- `conversation.repeat` ("повтори") re-speaks the last assistant message.
- New Lua API `jarvis.llm_history()` returns array of {role, content}
tables, excluding the system prompt.
WOL pack (NEW `wol/`):
- Voice "разбуди сервер" / "wake server" fires a magic packet at a MAC
stored in long-term memory under `wol_<alias>` (default alias "server").
- Setup via voice: "запомни wol_server AA:BB:CC:DD:EE:FF". Falls back to
`JARVIS_WOL_TARGET` env if memory empty. Handles every standard MAC
format (colons / dashes / dots / bare). Broadcast on UDP 9 via
PowerShell System.Net.Sockets.UdpClient.
Outlook COM pack (NEW `outlook/`, via subagent):
- 4 commands: unread_count, latest, send_clipboard, summarize_inbox.
- PowerShell COM bridge `_outlook.ps1` with tab-delimited line protocol.
- summarize_inbox calls LLM for a one-paragraph summary. Graceful
failure when Outlook isn't running.
Time tracker pack (NEW `time_tracker/`, via subagent):
- 5 commands: start, stop, today, week, reset.
- Persists via `jarvis.state` (structured object: current_session_start
+ sessions array of {start, end}).
- Local-calendar "today" boundary, full Russian pluralisation
(час/часа/часов, минута/минуты/минут, сессия/сессии/сессий).
Tests: 139 rust (no regression), 81 python (60 → 81, +14 time_tracker
+7 outlook). Release build green for jarvis-app and jarvis-cli.
Pack count: 88 → 92 rust packs.
Personality pack (`resources/commands/personality/`):
- 5 voice commands × ~7-29 phrases each across RU and EN.
- personality.greet: 4 time-of-day buckets (morning/midday/evening/night),
pulls one of ~7 lines per bucket per language.
- personality.thanks / .compliment / .how_are_you / .tony_quote.
- how_are_you embeds live memory size + active profile via jarvis.health()
and jarvis.memory.all() for a "feels alive" effect.
- All use jarvis.cmd.ok helpers, no inline PowerShell SAPI.
- Built by sub-agent. Verified: 6 rust command tests + 60 python tests.
Idle banter (`crates/jarvis-core/src/idle_banter.rs`):
- Background thread chimes in periodically without being asked. Gated by
JARVIS_IDLE_BANTER env (default OFF — intrusion is opt-in).
- Quiet hours 23:00–07:00, skipped under "sleep" profile, paused during
active interactions via `pause()`.
- 30+ static offline lines split into RU/EN × morning/evening/generic
buckets — no network required.
- Lua API jarvis.banter.{fire, pause, resume, enabled}.
- New voice pack `banter/` exposes "скажи что-нибудь интересное",
"помолчи", "можешь говорить".
- 6 unit tests covering pool selection, quiet hours, interval clamp,
pause/resume, opt-in default.
Conversation continuity (`crates/jarvis-core/src/llm/history.rs`):
- New `ConversationHistory::with_persistence(path)` builder. Every
push/clear/pop atomically writes to `<APP_CONFIG_DIR>/llm_history.json`
so daemon restart picks up the thread.
- System prompt is intentionally NOT persisted — comes from current init
call so prompt edits take effect immediately on restart.
- `llm::init_history` wires the path in automatically.
- 4 new tests: round-trip, clear wipes file, corrupt file tolerated,
len/is_empty helpers.
Offline-first math (`resources/commands/math/math.lua`):
- Was: always-LLM, hard fail without GROQ_TOKEN, inline PowerShell SAPI.
- Now: shunting-yard parser handles 95% of voice queries in <50ms — no
network, no token. Russian operator words ("плюс", "умножить на",
"в степени", "квадрат", ...) normalised to symbols first. Patterns
for "корень из X" and "X процентов от Y". Falls back to LLM only on
parse failure (word problems / equations / unit conversions).
- Drops inline PowerShell — speaks via jarvis.cmd.ok.
- 10-case shunting-yard kernel test added (basic ops, precedence,
parens, unary minus, div-by-zero, garbage rejected).
DuckDuckGo Instant Answer (`resources/commands/ddg_answer/`):
- New pack — short factual Q&A without API key. Trigger phrases
"что такое", "кто такой", "расскажи про", "what is", etc.
- Reads AbstractText → Answer → Definition → RelatedTopics[0] in order
from DDG's free JSON API. Opens the search page only if nothing
useful comes back.
- Sandbox full (needs http + system.open).
Tests: 128 → 139 (+11). Release build green.
CI (.github/workflows/ci.yml):
- Three parallel jobs on push & PR: cargo test (jarvis-core), cargo
clippy (lint, warnings-as-errors), cargo check (workspace). Windows
runner, stable toolchain, rust-cache for fast incremental builds.
- Deliberately skips release build of jarvis-gui — that needs Node+npm
+Tauri bundler and would balloon CI to ~10min per push.
Drop Ukrainian:
- Delete ua.ftl, remove UA from SUPPORTED_LANGUAGES.
- config.rs: prune UA arms from get_wake_phrases / get_wake_grammar /
get_phrases_to_remove / get_llm_trigger_phrases / get_llm_system_prompt.
- stt/vosk.rs: drop UA → uk language mapping.
- tray menu: drop "Українська" entry.
- frontend: drop UA from Header.svelte language list, settings page
language map, i18n.ts fallback.
- 82 command.toml files stripped of every `ua = [...]` block (205
blocks total). Handled via subagent — TOML still parses, all 6 command
tests pass.
.gitignore: stop tracking dev.env / dll_probe*.py / imports_full.txt
which were sitting in working tree.
Tests: 128 pass (no regression). Workspace still compiles end-to-end.
#29 Plugin system:
- New jarvis-core::plugins module: discovers user packs in
%APPDATA%\com.priler.jarvis\plugins\<name>\command.toml so authors can
ship voice commands without rebuilding. Sandbox capped at "standard" —
plugins cannot escalate to "full" (which would expose `os`). Disabled
via a `disabled` flag file. Malformed packs warn and skip; never poison
the rest of the list. 8 unit tests.
- commands::parse_commands() merges plugins into the loaded list.
- New /plugins GUI page with enable/disable switches, error reporting,
"Open folder" button. Tauri commands plugins_list / plugins_set_enabled
/ plugins_open_folder. Header gets a "Плагины" button. i18n keys added
for ru/en/ua.
#8 Custom wake-word trainer wizard:
- New jarvis-core::wake_trainer module: opens its own pv_recorder
instance, records N short PCM clips, WAV-encodes them in memory, then
calls rustpotter's WakewordRef::new_from_sample_buffers to train and
persist a .rpw model under %APPDATA%/com.priler.jarvis/wake_words/.
Keepsake WAVs are also dumped for retraining later. Sanitises names to
block path traversal. 5 unit tests.
- Settings gain `custom_wake_word: String`; listener/rustpotter.rs now
loads the user's selected model first and always falls back to the
bundled default so the assistant keeps working even if the custom
file is missing.
- New /wake-trainer GUI page: stepper UI for record-sample → train,
shows recorded count, threshold slider, refuses to start while
jarvis-app is running (mic exclusivity). Lists existing trained
models with size + delete button.
- 8 Tauri commands wired through (status/defaults/start/record_sample/
finish/cancel/list_models/delete_model).
Tests: 115 → 128 (+8 plugins +5 wake_trainer). Release builds green for
all three binaries (jarvis-app / jarvis-cli / jarvis-gui).
Tests (112→115, +3)
- commands::additional_tests::no_duplicate_command_ids_across_packs
Catches accidental ID collisions across packs at PR time, with informative
error pointing to both offending packs.
- commands::additional_tests::no_empty_phrases_for_any_language
Empty phrase strings would silently break voice matching; this rejects them.
- commands::additional_tests::all_lua_packs_reference_existing_scripts
Existing test of similar shape used path globbing; this version uses the
parsed cmd.script field for a more precise error message.
Plus: runtime_config tests no longer race on shared env vars. Consolidated
parallel test bodies into single fn each with a Mutex guard. 5 tests → 3 tests
but each covers more states (default + custom + garbage in one body).
New packs
resources/commands/self_check/ (2 cmds)
- selfcheck.ping "ты тут" / "ты слышишь" / "пинг" / "ты живой"
Random pick from 5 short canned replies — no LLM call,
no network. Pure mic-chain + TTS smoke test.
- selfcheck.smoke "проверь себя" / "самопроверка"
Reads jarvis.health() + tries one HTTP request,
speaks list of "X работает, Y работает...".
resources/commands/ssl_check/ (1 cmd)
- ssl.check "проверь сертификат example.com"
PowerShell TCP+SslStream→X509Certificate2. Reports days
until expiry, expiry date, issuer CN. Adds urgency prefix
("СКОРО!" if <7 days, "Скоро." if <30, "просрочен!" if past).
Pack count: 70 → 72. Tests: 115/115 + 1 ignored Ollama smoke.
Build: cargo build --release -p jarvis-app green.
Closes the "GUI shows GUI process state, not daemon's" gap. Now GUI can:
1. Tell the daemon to swap LLM backend via IPC (so the running listener
uses the new backend immediately, not just the GUI process).
2. Tell the daemon to re-read llm_backend from settings DB after GUI persists
a choice independently.
3. Query the daemon's true runtime state (TTS/LLM/profile/memory/scheduler
counts/language/version) for an honest footer + diagnostics view.
IpcAction (new variants)
- SwitchLlm { backend: String } — daemon calls llm::swap_to(parse(backend))
- ReloadLlm — daemon calls llm::init_global() to re-read DB
- QueryHealth — daemon responds with HealthSnapshot
IpcEvent (new variant)
- HealthSnapshot { tts_backend, llm_backend, llm_model, active_profile,
memory_facts, scheduled_tasks, language, version }
jarvis-app main.rs
- Imports IpcEvent (was only IpcAction before).
- Three new match arms in ipc action handler — all use existing globals,
no new state needed.
Next step (TODO): wire GUI's set_llm_backend Tauri command to ALSO fire
IpcAction::SwitchLlm so daemon stays in sync. And replace Footer.svelte's
get_active_backends call with QueryHealth over IPC.
Build: cargo build --release -p jarvis-app -p jarvis-gui green.
Tests: 62/62 pass (unchanged — IPC variants don't add testable logic).
Defensive tests for the surface area that user-facing voice commands hit.
llm::parse_backend (5 new tests)
- English aliases ("groq", "ollama", "cloud", "local")
- Russian aliases ("облако", "локальный", "локал")
- Case insensitivity ("OLLAMA", " Groq ")
- Rejects unknown ("openai", "claude", "")
- current_backend_name() smoke test
macros (2 new tests)
- Store JSON round-trip with multiple macros
- is_macro_control case-handling (noun form NOT recognised, verb form IS)
Tests: 62/62 (was 55).
Surfaces voice macros and scheduled tasks in the GUI, so the user doesn't
have to remember voice commands or grep schedule.json by hand.
Tauri commands (crates/jarvis-gui/src/tauri_commands/)
- macros.rs: macros_list, macros_replay, macros_delete, macros_is_recording,
macros_recording_name, macros_start_recording, macros_save_recording,
macros_cancel_recording (8 commands).
- scheduler.rs: scheduler_list, scheduler_remove, scheduler_clear (3 commands).
- Both exposed in main.rs invoke_handler.
GUI pages (frontend/src/routes/)
- macros/index.svelte:
* Lists all macros with name, steps_count, first 5 step previews,
created/last_run timestamps.
* Top: TextInput + "Начать запись" button. While recording shows orange
banner with "Сохранить"/"Отменить" buttons + polls is_recording every 2s.
* Per-card: "Запустить" (with busy state for replay duration), "Удалить".
* confirm() before delete.
- scheduler/index.svelte:
* Lists tasks with name, schedule_human (e.g. "каждые 2 ч"), action body,
ID, timestamps, action_kind badge.
* "Отменить" per task + "Очистить всё (N)" bottom button.
* Auto-polls every 5s (so the list updates as scheduler ticks fire tasks).
Header (frontend/src/components/Header.svelte)
- Two new buttons: "Макросы" → /macros, "Расписание" → /scheduler.
- Beside existing /commands and /settings buttons.
i18n
- ru/en/ua FTL: settings-ai-backends, settings-llm-*, settings-tts-*,
settings-profile, header-macros, header-scheduler.
- Re-applied AI Backends keys for ua.ftl (earlier edit hadn't taken).
README.md (full rewrite)
- Old README was 178 lines mostly explaining LLM-trigger flow and VAD config.
- New README is ~190 lines covering: features (LLM hot-swap, memory, profiles,
vision, scheduler, macros, utilities table), quick start, env vars table,
build steps with vcvars setup, test command, structure tree, voice workflow
diagram, license, roadmap.
- Up to date for 59 packs and all new infra.
Build: cargo build --release -p jarvis-gui green (2m). 55/55 tests pass.
Surfaces the LLM/TTS hot-swap UX in the GUI so the user doesn't need env vars
or voice commands. Tauri commands for this landed in b243e67; this commit
wires them to Svelte.
Footer (frontend/src/components/Footer.svelte)
- Poll get_active_backends() every 5 s, show TTS / LLM / Profile chips.
- Chips have border accents by kind (cyan = TTS, lime = LLM, orange = Profile).
- Friendly title tooltips with full backend + model names.
- Silent if jarvis-gui can't reach the command (e.g. cold start) — no UI flicker.
Settings — new "AI Backends" tab (frontend/src/routes/settings/index.svelte)
- Status banner showing active LLM (with model name) + TTS + Profile.
- LLM selector: Auto / Groq / Ollama → calls set_llm_backend on change
(hot-swap, persisted to DB).
- TTS selector: Auto / SAPI / Piper / Silero → calls set_tts_backend
(persisted, effective on next jarvis-app restart).
- "Reset context" button → llm_reset_context Tauri command.
- Error toast for swap failures (e.g. Groq selected but no GROQ_TOKEN).
- Tips alert: how to install Ollama / Piper / voice commands.
- Loads prefs via db_read('llm_backend' / 'tts_backend') on mount.
i18n strings (ru/en/ua, .ftl files)
- 14 new keys: settings-ai-backends, settings-ai-active, settings-ai-auto,
settings-ai-applying, settings-ai-error, settings-ai-tips,
settings-llm-backend{,-desc}, settings-tts-backend{,-desc},
settings-llm-context{,-desc}, settings-llm-reset, settings-profile.
Build glue (crates/jarvis-gui/Cargo.toml)
- jarvis-core dep now includes the `llm` feature so backends.rs can resolve
`jarvis_core::llm::*` (was failing to compile after the global LLM module).
.gitignore
- tools/piper/*.dll, piper.exe, voices/, espeak-ng-data/ — locally
installed binaries from tools/piper/install.ps1, not committed.
Build: cargo build --release -p jarvis-gui green (4m, includes npm/Vite).
Test path: rebuild + launch jarvis-gui, /settings → "AI Backends" tab.
Single biggest user-facing change: you can now switch local↔cloud LLM by voice
without restarting. Plus four new packs and a perf nudge.
Shared LLM global (crates/jarvis-core/src/llm/mod.rs)
- GLOBAL: Lazy<RwLock<Option<Arc<LlmClient>>>>. Single source of truth.
- init_global() / current() / swap_to(LlmBackend) / current_backend_name() / parse_backend(str).
- llm_fallback + llm_router migrated: they no longer own a separate LlmClient,
they read from the global on each request. Hot-swap is now LIVE — change
backend, next chat call uses the new one.
- parse_backend accepts both english and russian aliases:
"groq"/"cloud"/"облако"/"клауд" → Groq
"ollama"/"local"/"локал"/"локальный" → Ollama
Voice + Lua LLM switcher (resources/commands/llm_switch/, 3 commands)
- "переключись на локальный" / "перейди на оллама" → swap to Ollama
- "переключись на облако" / "используй грок" → swap to Groq
- "какой у тебя мозг" / "облако или локально" → status query
Underlying Lua API (registered globally):
jarvis.llm({messages}, opts) -- as before, now uses global client
jarvis.llm_status() -> "groq" | "ollama" | "none"
jarvis.llm_switch(name) -> name on success, nil on failure
Media keys pack (resources/commands/media_keys/, 4 commands)
- "пауза" / "плей" → VK_MEDIA_PLAY_PAUSE
- "следующий трек" → VK_MEDIA_NEXT_TRACK
- "предыдущий трек" → VK_MEDIA_PREV_TRACK
- "стоп музыка" → VK_MEDIA_STOP
- Helper _media.ps1 uses user32.keybd_event (P/Invoke through Add-Type).
- Works with anything that listens to global media keys: Spotify, Yandex Music,
YouTube (focused tab), Foobar2000, Winamp, etc. No OAuth, no API keys.
Codebase Q&A pack (resources/commands/codebase_qa/, 3 commands)
- "укажи проект <path>" → stores path in jarvis.memory under codebase.root
- "какой проект сейчас" → speaks current path
- "что делает функция X" / "найди в коде Y" / "объясни код"
→ walks the folder (depth 3, 30 files cap, 4KB per file, 50KB total),
filters by source extensions (rs/py/ts/lua/go/...), feeds digest to LLM
with a "senior reviewer" system prompt, speaks 3-5 sentence answer.
Scheduler cancel-by-text (crates/jarvis-core/src/scheduler.rs)
- new pub fn find_by_text(query, limit) -> Vec<ScheduledTask>
- new pub fn remove_by_text(query) -> usize (count removed)
- new pure helper find_by_text_in(tasks, query, limit) for tests
- Lua: jarvis.scheduler.{find_by_text, remove_by_text}
- Voice: "отмени напоминание про воду" / "удали задачу про разминку"
- 3 new unit tests (52 total in jarvis-core).
TTS pre-warm (crates/jarvis-app/src/main.rs)
- Call jarvis_core::tts::backend() once on startup so the first speak()
doesn't pay Piper binary discovery + voice loading cost. Cuts first-speak
latency by ~150-300ms depending on backend.
Build: cargo build --release -p jarvis-app -p jarvis-gui both green.
Tests: 52/52 jarvis-core unit tests pass.
User asked the code stays easy to fix/extend as the feature surface grows.
This commit refactors what was duplicated, centralises what was scattered,
adds tests where there were none, and writes the architecture doc that
contributors will need.
Deduplication
- tts::play_wav extracted to tts/mod.rs as pub(crate). Was identical in
piper.rs and silero.rs (~25 lines × 2). Both backends now call super::play_wav.
Centralised env config
- new crates/jarvis-core/src/runtime_config.rs — single doc-file for every
JARVIS_* / GROQ_* / OLLAMA_* env var. Includes:
- ENV_* constants with doc comments
- get(name) / get_bool(name, default) / get_parse(name, default) helpers
- feature-flag wrappers: llm_tts_enabled(), llm_router_enabled(),
llm_router_threshold()
- log_effective_config() prints active values on startup
- migrated llm_fallback (JARVIS_LLM_TTS), llm_router (JARVIS_LLM_ROUTER,
JARVIS_LLM_ROUTER_THRESHOLD) to use the new helpers. Pattern set for
future migrations.
Lua boilerplate killer
- new crates/jarvis-core/src/lua/api/cmd.rs exposing:
jarvis.cmd.ok(msg?) — play_ok + speak + {chain=false}
jarvis.cmd.chain_ok(msg?) — same but chain=true
jarvis.cmd.error(msg?) — play_error + speak + {chain=false}
jarvis.cmd.not_found(msg?) — play_not_found + speak + {chain=false}
jarvis.cmd.silent_ok / silent_error
- refactored 5 packs (daily_briefing/off, memory_pack/list, pomodoro/stop,
habit_nudge/stop_all, scheduler/clear) — each lost 3-4 lines of repetitive
play_*/speak/return boilerplate. Pattern for future packs documented in
ARCHITECTURE.md.
Tests (was 32, now 49)
- long_term_memory: 8 new tests for normalize_key, search_in (rank, limit,
empty), build_context_from (empty + populated), serde round-trips for
MemoryRecord and Store. Extracted pure logic (search_in, build_context_from)
into pub(crate) functions to enable testing without global state.
- profiles: 6 new tests for Profile::allows_command (empty/whitelist/blacklist/
deny-wins-over-allow), serde round-trip with all fields + minimal-fields
tolerance via #[serde(default)].
- runtime_config: 2 tests for get_bool / get_parse defaults.
- All 49 tests pass.
New mood/energy log pack
- resources/commands/mood_log/ with 2 commands:
mood.record "запиши настроение 7" / "сегодня мне грустно" → stores
timestamped entry via jarvis.memory.remember
mood.recap "как прошла неделя" → LLM summarises last 30 entries
- Showcase: composes memory + llm + cmd helpers in <30 lines per script.
ARCHITECTURE.md (new, 250 lines)
- Crate layout, data flow diagram (mic→action 10 steps), per-module
responsibility table, configuration layers, TTS pipeline diagram,
Lua sandbox details with API quick-ref, background-services overview,
"how to add a pack/feature/TTS backend" recipes, test coverage map,
build instructions with MSVC env, git workflow with Forgejo NO_PROXY trick.
- Aimed at someone who just cloned the repo and needs to fix a bug fast.
Build: cargo build --release -p jarvis-app -p jarvis-gui both green.
Tests: 49/49 jarvis-core unit tests pass.
Big push driven by user feedback ("делай имбу") and web research on what
voice assistants need to be the ideal:
TTS backend abstraction (P0.1)
- new module crates/jarvis-core/src/tts/{mod,sapi,piper,silero}.rs
- TtsBackend trait with SapiBackend (current PowerShell), PiperBackend
(rhasspy/piper, neural quality), SileroBackend (python subprocess)
- JARVIS_TTS env var picks (sapi|piper|silero). Auto-detect Piper if
binary + voice present in tools/piper/. Falls back to SAPI on missing.
- SpeakOpts {lang, detached, raw} replaces ad-hoc args. text_utils
sanitiser applied unless raw=true.
- llm_fallback + lua/api/tts both routed through tts::backend().
- tools/piper/install.ps1 downloads piper.exe + ru_RU-irina-medium.onnx
from rhasspy releases + huggingface. Smoke-test included.
- tools/silero/silero_tts.py helper (PyTorch); rust spawns it as subprocess.
IMBA-1 Agentic LLM router
- crates/jarvis-app/src/llm_router.rs
- When fuzzy/intent matcher fails, LLM picks the closest command from the
full registry. Returns JSON {command_id, confidence, reason}.
- Threshold-gated re-dispatch via substitute phrase. JARVIS_LLM_ROUTER=1
enables; JARVIS_LLM_ROUTER_THRESHOLD overrides 0.55 default.
- Inserted in app.rs::execute_command between "no match" and existing
llm_fallback chat fallback.
IMBA-2 Long-term memory
- crates/jarvis-core/src/long_term_memory.rs — JSON store at
APP_CONFIG_DIR/long_term_memory.json. Atomic write-through.
- remember/recall/search/forget/all/build_context API.
- Lua bindings: jarvis.memory.* (5 functions).
- llm_fallback auto-injects relevant facts (substring search of prompt)
into system message before LLM call.
- Pack resources/commands/memory_pack/ with 4 commands: remember, recall,
forget, list.
IMBA-3 Profile switching (work/game/sleep/driving/default)
- crates/jarvis-core/src/profiles.rs — JSON profiles at APP_CONFIG_DIR/profiles/
Auto-seeds 5 defaults on first run with personality + allow/deny lists +
greetings + emoji icons.
- active_profile.txt persists choice across restart.
- Lua bindings: jarvis.profile.{active,set,list,allows,active_name}.
- llm_fallback prepends profile personality to system prompt.
- Pack resources/commands/profile_switch/ with 6 voice triggers.
IMBA-4 Multimodal screenshot + vision LLM
- crates/jarvis-core/src/lua/api/vision.rs — gated on HTTP sandbox.
- jarvis.vision.screenshot() captures via PowerShell System.Drawing.
- jarvis.vision.describe(prompt?) sends base64 PNG to Groq vision model
(default llama-3.2-11b-vision-preview, override via GROQ_VISION_MODEL).
- Pack resources/commands/vision/ with 2 commands: describe + read_error.
P0.2 Continuous conversation grace window
- config::CONVERSATION_GRACE_MS = 30_000.
- app.rs: after command result, if grace_ms > 0 keep listening WITHOUT
re-wake for the grace duration. Existing CMS_WAIT_DELAY back-dated so
the existing timeout fires at start + grace_ms.
Tests: 24/24 jarvis-core unit tests pass (including 5 text_utils).
Build: cargo build --release -p jarvis-app and -p jarvis-gui both succeed
on Windows MSVC (VS 2026 Enterprise vcvars64).
Notes for setup:
- Piper voice install: pwsh tools/piper/install.ps1 (downloads ~90 MB).
- GROQ_TOKEN needed for IMBA-1 (router) and IMBA-4 (vision).
- All features are opt-in via env vars or auto-detect; existing SAPI +
fuzzy match path remains the default.
User complaint: SAPI reads every dotted acronym character-by-character with a
"точка" between letters. Unbearable on every speak. No real TTS analog
shipping yet (Silero/Coqui/Inworld in roadmap), so the immediate fix is text
preprocessing.
New module `jarvis-core::text_utils` with `sanitize_for_speech()`:
- Specific brand mapping: J.A.R.V.I.S. → Джарвис, U.S.A. → США, U.K. →
Британия, U.S. → США, S.O.S. → сос.
- Generic dotted-acronym collapse: any `[letter].[letter].[letter].(...)`
run of 3+ letters gets its dots stripped. "T.O.N." → "TON".
- URL stripping: anything starting with http/https gets replaced with the
word "ссылка". SAPI reading URLs char-by-char is unlistenable.
- Em/en-dash normalisation: — → -, « » " stripped.
- Whitespace collapse.
Wired in two places:
- `jarvis.speak(text, opts?)` Lua API runs sanitiser unless opts.raw=true.
- `jarvis-app/llm_fallback::speak_via_sapi` runs sanitiser before the SAPI
PowerShell shell-out for LLM auto-fallback replies.
5 unit tests in `text_utils::tests` covering jarvis-name collapse, generic
acronym pattern, URL stripping, dash normalisation, and "leave clean
russian sentences alone" — all green.
Real TTS upgrade (Silero v4 first cut) tracked as P0.1 in roadmap.
Architecture — close the trigger-strip duplication:
lua/api/text.rs (new):
+ jarvis.text.strip_trigger(phrase, triggers) — case-insensitive
"longest trigger wins" stripping, returns trimmed remainder. The
11 packs that previously inlined a `for _, t in ipairs(triggers) do
string.find ... s == 1 then sub` loop can drop to one call.
Demonstrated in spelling/, window_switch/, summarize/.
+ jarvis.text.contains_any(phrase, needles) — boolean check for any
of N substrings, case-insensitive. Useful for keyword routing.
5 new portable Lua packs (sandbox=full). 36 → 41 total. cargo test
commands::tests still 3/3.
brightness/ — brightness_up / down / max / min. WMI WmiMonitorBrightness
(read CurrentBrightness) + WmiMonitorBrightnessMethods.WmiSetBrightness
(write). ±20% step for up/down; max=100, min=10. Desktop monitors that
don'\''t expose the WMI namespace get a friendly "не поддерживается"
toast instead of a silent failure.
window_switch/ — switch_to_window. Strip trigger ("переключись на" /
"switch to" / etc.), apply the same alias map as process_kill
(хром→chrome, телега→telegram, ...), then PowerShell finds processes
by name OR window-title substring sorted by StartTime desc and uses
(New-Object -ComObject WScript.Shell).AppActivate($pid) on the top
match. Speaks the window title that got focus.
spelling/ — spell_out. Strips "произнеси по буквам" / "spell out" /
etc., iterates `word:gmatch(".")`, joins letters with ". " so SAPI
gives each one a natural micro-pause. Uses jarvis.text.strip_trigger
+ jarvis.speak — 24 lines total.
network/ — open_wifi_settings (ms-settings:network-wifi) +
open_bluetooth_settings (ms-settings:bluetooth) + my_ip. my_ip pulls
the first non-loopback, non-APIPA IPv4 with Get-NetIPAddress and
fetches WAN IP from api.ipify.org (5s timeout); speaks "Локальный X.
Внешний Y".
summarize/ — summarize_selection. Synthesises Ctrl+C through user32!
keybd_event (so the focused window copies its selection), waits
220 ms for the clipboard to populate, reads via
jarvis.system.clipboard.get(), bails if < 20 chars, truncates at
8000, sends to Groq @ temp 0.3 with a "3-5 sentences, keep names"
prompt, drops the summary back into the clipboard and speaks it.
Lets you select ANY text in ANY app (browser, PDF, IDE) and say
"суммируй" to get a spoken TL;DR.
The new packs explicitly use the new jarvis.text + jarvis.llm +
jarvis.speak API surface — no inline PS-SAPI boilerplate, no inline
Groq plumbing. Code is now intent-first.
Architecture — DRY at the Lua API surface:
lua/api/tts.rs (new): jarvis.speak(text, opts?). opts.lang = ISO
two-letter code (default "ru"); auto-picks first installed voice
matching the culture. opts.async = true|false (default true). The
9 packs that previously inlined a 60-character PS-SAPI string for
every spoken reply can now reduce to one line. Refactored fun/,
dice/, clipboard_read/ as proof — fun/ask.lua went from 75 lines
of HTTP+SAPI boilerplate to ~28 lines of intent code.
lua/api/llm.rs (new): jarvis.llm(messages, opts?). messages is a
list of {role, content} tables, opts.max_tokens / .temperature /
.top_p. Returns the assistant string or nil. Gated by sandbox
allows_http() (so Standard+ packs only). Internally uses the
existing jarvis_core::llm::LlmClient::complete_with, no duplicated
HTTP plumbing.
llm/client.rs: split complete() into complete_with(messages,
max_tokens, temperature, top_p) for the new caller; old complete()
is now a thin wrapper at temp=0.7 top_p=1.0 for backward compat.
GUI — /commands page rewrite:
Replaces the "Раздел в разработке" placeholder. Calls Tauri command
get_commands_list (already existed in tauri_commands/commands.rs,
was wired but unused). Renders a search-filterable card grid:
- cmd id (monospace)
- type badge with colour by kind (lua=blue, ahk=red, cli=grey,
voice=green, terminate=red, stop_chaining=violet)
- sandbox badge
- description line (if non-empty)
- all phrases for currentLanguage, fallback to en, fallback to
first available; each phrase in a small inline pill
Toolbar: text input with magnifier icon + reload button (↻).
Counter line shows visible/total + "Скажи Джарвис + любую фразу"
hint. Counts down as you type the filter.
New packs (3, total 33 → 36):
games/ — launch_game / list_games. Reads or creates
%USERPROFILE%\Documents\jarvis-games.json (the sample preloads dota,
cs2, elden ring, witcher 3, factorio + minecraft launcher path +
fortnite epic URI). Trigger-strip + fuzzy name/alias match, then
launches via steam://rungameid/N (Steam), epic:// (Epic Games), or
start "" "path" (anything else). list_games speaks the configured
library.
mouse/ — left/right/middle/double click + scroll up/down. Single
dispatch.lua + _mouse_helper.ps1. PS P/Invokes user32!mouse_event
with the right flag pair (LEFTDOWN+LEFTUP / etc.), 30ms gap, doubles
do two LEFT cycles with a 50ms gap, wheel uses ±360 ticks.
random_choice/ — "выбери из X или Y или Z". Strips the trigger,
splits on " или " / " or " / ", " / " либо " / " чи ", math.random
picks one, speaks "Я выбираю N".
cargo test commands::tests still 3/3. 36 packs verified via
jarvis-gui startup log.
LLM auto-fallback (jarvis-app/src/app.rs + jarvis-core/src/config.rs):
- When neither intent classifier nor levenshtein finds a command match,
route the utterance straight to the LLM instead of just playing the
"not found" sound. Triggers ("скажи X", "answer Y") still work and
take precedence — they short-circuit before command lookup.
- Two new config knobs:
LLM_AUTO_FALLBACK = true — master toggle
LLM_AUTO_FALLBACK_MIN_CHARS = 4 — suppress for very short utterances
so background noise doesn't burn
Groq quota
- Requires GROQ_TOKEN; if absent, behaviour is unchanged (play_not_found).
codegen/ command pack:
- Phrases: "напиши код X", "сгенерируй скрипт Y", "write code Z", etc.
- Strips the trigger from the recognized phrase, sends what remains to
Groq with a strict system prompt ("return ONLY code, no fences, no
commentary") at temperature 0.2.
- Parses the JSON content, unescapes \n / \" / \\ / \t, strips any
remaining ```lang ... ``` fences, drops the result into the clipboard
via jarvis.system.clipboard.set.
- Notifies a 120-char preview + plays ok-sound. Works for any language
the model handles (Python by default if unspecified).
- GROQ_TOKEN / GROQ_MODEL / GROQ_BASE_URL read from env at call time —
same envvars the voice-loop fallback already uses.
ocr/ command pack:
- Phrases: "прочитай экран", "что на экране", "read screen", etc.
- Captures the primary screen via System.Windows.Forms + System.Drawing
to a temp PNG, then shells to tesseract.exe (-l rus+eng for ru/ua,
-l eng for en).
- Resolves Tesseract by PATH first, then C:\Program Files\Tesseract-OCR
and the x86 install dir; if none works the user gets a friendly
"winget install UB-Mannheim.TesseractOCR" hint in a notification.
- Recognized text goes to clipboard + a 200-char preview notification.
All three features are portable (env-var resolution, no hardcoded user
paths). Command-pack total is now 11. cargo test -p jarvis-core --lib
commands::tests passes 3/3.
cargo test on jarvis-core was failing with `LNK1181 cannot open input
file "libvosk.lib"`. The link-search path is declared in jarvis-app's
build.rs (and jarvis-cli's), but the test binary for jarvis-core has
no such config and the workspace .cargo/config.toml didn't cover it.
Adds a minimal build.rs that emits `cargo:rustc-link-search=native=...`
pointing at lib/windows/amd64 (resolved relative to CARGO_MANIFEST_DIR,
no hardcoded absolute paths so the project still builds for anyone
cloning under a different drive/path).
Verified: cargo test -p jarvis-core --lib commands::tests now passes
3/3 (every_command_toml_parses, lua_command_scripts_exist,
every_command_has_phrases).
Three unit tests catch the kind of bug that broke weather/set_city (a
phrases array instead of lang→array map silently dropped the whole pack):
- every_command_toml_parses: every resources/commands/*/command.toml round-
trips through toml::from_str::<JCommandsList>. Reports all failures at
once instead of failing on the first.
- lua_command_scripts_exist: for every type="lua" command, the named (or
default script.lua) script file exists in the pack dir.
- every_command_has_phrases: structural commands (terminate/stop_chaining)
excluded, every other command has ≥1 phrase across all languages.
Tests use env!("CARGO_MANIFEST_DIR") -> ../.. -> resources/commands so they
work from any cwd.
NB: cargo test does not run on this machine right now — aws-lc-sys needs
cl.exe (MSVC Build Tools missing). The release binary was built earlier
with MSVC available; toolchain install is a follow-up. The tests are still
valid for CI / a re-armed dev box.
new module crates/jarvis-app/src/llm_fallback.rs holds an optional
LlmClient + ConversationHistory built once at startup. if GROQ_TOKEN is
unset the module logs a warning and stays disabled — voice commands keep
working as before.
both the wake-word voice path (recognize_command in app.rs) and the
gui-side text command path (process_text_command) now check whether the
recognized phrase starts with one of the configured trigger phrases
(ru: 'скажи' / 'ответь' / 'произнеси'). when it does, the remainder of
the phrase is sent to Groq and the reply is published as a new
IpcEvent::LlmReply { text } so the gui can speak it.
on api error the trailing user turn is popped from history, the russian
fallback line is sent over ipc and a 'error' voice cue plays. the loop
itself never panics.
add LLM_DEFAULT_ENABLED / LLM_DEFAULT_MAX_HISTORY / LLM_DEFAULT_MAX_TOKENS
defaults, the russian J.A.R.V.I.S. system prompt, a fallback error line,
and per-language helpers get_llm_trigger_phrases() and get_llm_system_prompt()
so the voice loop can opt into Groq with a 'скажи …' style prefix.
split llm.rs into a module with separate client and history submodules.
ConversationHistory holds an optional system prompt plus a FIFO of user/
assistant turns capped at max_turns; oldest turns evict on overflow,
system prompt is preserved across truncation and clear().
New jarvis-core::llm module providing a blocking client for
OpenAI-compatible chat completions endpoints (Groq by default).
- LlmClient::new / from_env (GROQ_TOKEN, GROQ_BASE_URL, GROQ_MODEL)
- complete(messages, max_tokens) -> String
- thiserror-based LlmError / ConfigError
- gated behind a new llm feature, included in default jarvis_app
Not yet wired into the wake-word/intent loop; that lands in v0.3.