# Recognition history page
New /history route in the Tauri GUI. Every voice/text phrase that
reaches the dispatcher gets a row with: timestamp, the phrase, what
happened, command id + confidence (if matched), which matcher fired
(intent / fuzzy / LLM-router / LLM-fallback).
Color coding:
- Green: command matched AND executed successfully
- Red: command matched but failed, OR dispatcher error
- Blue: LLM fallback handled it (Jarvis spoke a free-form reply)
- Orange: no match at all ("Не понял")
Live updates: GUI polls every 2s from the on-disk log, so the page
shows daemon writes in near real time even though they're separate
processes. Filter box for searching by phrase or command id.
Architecture:
- New core module `recognition_log` with ring buffer (cap 500) +
atomic JSON write-through to `<APP_CONFIG_DIR>/recognition_log.json`.
- `record(phrase, source, outcome)` is the single call-site from the
daemon's `execute_command()` — hooked into the 4 outcome paths
(matched-ok, matched-fail, not-found, llm-handled, error).
- `recent(limit)` reads the in-memory buffer (daemon's view).
- `recent_from_disk(limit)` re-reads the JSON file — GUI uses this
since the GUI process has its own buffer that doesn't see the
daemon's writes.
- 5 new unit tests covering ring buffer trimming, outcome serde
roundtrip, missing/corrupt/oversized file recovery.
GUI:
- `crates/jarvis-gui/src/tauri_commands/history.rs`: history_recent,
history_clear. Flattens the Outcome enum into a single struct that's
easier for the Svelte template to render.
- `frontend/src/routes/history/index.svelte`: ~270 lines. Stats badges
(✓ N matched / ✗ N misses / total), filter input, virtual list of
entry cards with color-coded left border. Polls every 2s.
- Header gets a new "История" / "History" button (between Plugins and
Settings). Russian + English locale entries added.
# Multi wake-word loading
Was: `init()` loaded the bundled `jarvis-default.rpw` + at most ONE
custom (from `settings.custom_wake_word`). User had to pick a single
trained model.
Now: loads the bundled default PLUS every .rpw in
`APP_CONFIG_DIR/wake_words/` simultaneously. Rustpotter natively
supports multiple wake-word triggers — each adds robustness for
different voice profiles. The legacy `custom_wake_word` field is
checked for back-compat but is a no-op if it points inside the
already-loaded directory.
User-facing impact: train the wake-word once via /wake-trainer →
restart daemon → detection improves automatically without picking a
single "active" model.
# Settings → "Обучить wake-word" button
Added a purple button on the settings page that links to
/wake-trainer. The trainer existed but had no in-GUI link, so users
couldn't find it without typing the URL. Now sits next to the
"Конструктор команд (Python)" button.
Tests: 140 → 145 rust core tests (+5 recognition_log). Frontend
rebuilds in 6.2s. Release builds of jarvis-app + jarvis-gui green.
Practical test: GUI launches (MainWindowTitle confirmed), seed log
file written + visible to the page.
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.
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).
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.
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.
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.