Skip to content

feat(desktop): Windows tray/Tauri integration, retire legacy Slint UI - #213

Closed
elasticdotventures wants to merge 19 commits into
mainfrom
feat/windows-desktop-dogfood
Closed

feat(desktop): Windows tray/Tauri integration, retire legacy Slint UI#213
elasticdotventures wants to merge 19 commits into
mainfrom
feat/windows-desktop-dogfood

Conversation

@elasticdotventures

@elasticdotventures elasticdotventures commented Aug 29, 2026

Copy link
Copy Markdown
Member

Summary

Continues the Windows desktop dogfood work (PR #170, merged) with a consolidation pass: host-tauri.exe (the main app) previously had a minimal Windows tray stub with no toast/notification support at all, while a separate standalone host-tray.exe binary had the full feature set but pointed "Show Window" at a legacy Slint UI (host-window.exe). This PR merges them into one implementation.

  • Made the tray's "show window" action injectable instead of hardcoding a spawn of host-window.exe
  • Wired host-tauri.exe's Windows tray through the same shared, tested runtime the standalone tray used — fixes the reported "toast notifications don't work" bug (they were never wired on the main app's tray)
  • Retired host-window.exe/host-tray.exe as build targets (source kept under src/bin/legacy/ for reference, no longer compiled)
  • Fixed a real bug found during live validation: "Show Window" didn't un-minimize a minimized window (SW_SHOW vs SW_RESTORE)
  • Fixed a real bug found during final review: any tray command error silently killed the whole app with exit code 0 (no visible diagnostics)
  • Fixed a real bug found during final review: the settings-backend registry fix below leaked permanent Windows registry keys on every test run
  • Fixed a real gap found during final review: two tray-toggleable settings (start_minimized_to_tray, window_visible_on_start) were never read by the main app at startup
  • Fixed a pre-existing bug: the Windows registry settings backend ignored its path argument entirely, silently sharing one global mutable key across every SettingsStore instance
  • Grouped the four notification-event toggles into a "Notify me for" submenu; removed dead code
  • Replaced the BurntToast PowerShell module dependency with a native windows-rs toast implementation (notify::native::NativeToastNotifier, wrapping the already-present but previously-unwired notification::windows_toast::ToastNotifier) — no external process, no module install required on the target machine. NotificationBackend::PowerShell renamed to Native (serde alias preserves reading settings persisted under the old name).

Design doc and implementation plan: see docs/superpowers/specs/2026-08-29-tray-tauri-integration-design.md and docs/superpowers/plans/2026-08-29-tray-tauri-integration.md.

Test plan

  • cargo test -p ledgerr-host --lib --tests --bins — 111 passing, 0 failed
  • Live launch of host-tauri.exe: tray shows the full menu (not just Show/Exit), test toast fires the real notification code path, Show Window restores a minimized window, no host-window.exe/host-tray.exe process ever spawned
  • BurntToast module confirmed not installed on the validation machine; notify-test.exe --backend native returned a real "status": "ready"/"message": "toast sent" result via the native windows::UI::Notifications API — the exact case that previously failed with BurntToastUnavailable

🤖 Generated with a multi-agent build+review process (build/test implementer, independent live-validation agent, whole-branch reviewer, fix-wave implementer, scoped re-reviewer — all per-task reviews and the final review passed clean).

https://claude.ai/code/session_015kUr7Kf9wN15TPqDiKsiKw

promptexecutionerr and others added 18 commits August 15, 2026 21:58
Design for consolidating three divergent tray implementations into one:
host-tauri.exe's Windows tray gets full parity with the standalone
host-tray.exe (which is retired), by making the window-show action
injectable instead of hardcoding a spawn of the now-deprecated Slint
host-window.exe. Explains and fixes the reported "toast doesn't work" —
host-tauri's tray never wired notifications at all.
5 tasks: make ShowWindow injectable, wire host-tauri through the
shared runtime, retire host-window/host-tray binaries, fix fallout in
Justfile, merge PR #209 + final live verification checklist.
Also includes a pre-existing, previously-uncommitted refactor found
already sitting in this file's working tree before this task started
(not written as part of this task): negate()/apply_toggle() helpers
collapsing the 7 near-identical toggle handlers in handle_command, plus
their corresponding round-trip tests.

Replaces the hardcoded spawn of host-window.exe with a caller-supplied
closure, so run()/handle_command can be reused by host-tauri (which
will show its own webview) without any dependency on the legacy Slint
binary. host-tray.exe's own behavior is unchanged (its main() now
supplies the old spawn-host-window.exe closure explicitly).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015kUr7Kf9wN15TPqDiKsiKw
create_backend() always preferred the registry backend on Windows,
which ignored the path argument entirely and always used one fixed
global key (HKCU\Software\b00t\settings) — meaning any two
SettingsStore instances, including two tests running in parallel,
silently shared and clobbered the same mutable state. Derive a
path-scoped subkey instead, keeping the real production path resolved
to the exact same key as before (no migration for existing users).

settings_atomicity.rs's two tests assumed SettingsStore always hits a
JSON file, which is only true on non-Windows platforms — rewritten to
test JsonFileBackend directly, which is what they actually meant to
test.
…ubmenu

native.rs: drop the unused push_check helper and DYNAMIC_TEXT_IDS
constant, fix a few compiler-flagged unused-mut/unused-must-use
warnings.

Group the four notification-event toggles (approval/submitted/failed/
completed) into a "Notify me for" submenu instead of sitting flat
among the other 10 top-level menu items — cuts the flat menu from 14
items to 11 and keeps the related settings together.
….exe

Replaces the minimal Show/Exit-only stub with the shared
tray::runtime::run(), injecting 'show the Tauri webview' as the
window-show action. host-tauri now has the full toggle/notification/
toast menu that was previously only in the standalone host-tray.exe —
this is the fix for toast notifications never having been wired up on
the main app's tray.
Both are superseded by host-tauri.exe (Task 1-2). Source moved to
src/bin/legacy/ for reference — outside Cargo's src/bin/*.rs
auto-discovery, so neither compiles as part of any normal build.
Those binaries no longer exist as build targets (see prior commit).
Recipes that built/launched them now target host-tauri instead;
recipes with no host-tauri equivalent (the Slint-window-specific ones)
are removed.
window.show() maps to Win32 SW_SHOW, which does not restore a
minimized window (SW_MINIMIZE state). A validation pass on Task 2
reproduced the tray's "Show Window" item staying inert on a
minimized main window. Add window.unminimize() (SW_RESTORE) before
show(); it is safe to call unconditionally when the window is not
minimized.

The hidden-window case (window.hide()) is not fixed here: nothing in
this codebase currently calls .hide(), and reproducing it requires
hiding via a raw Win32 ShowWindow(SW_HIDE) call that bypasses tao's
own tracked window-flags state, which desyncs tao's diff-based
show()/hide() dispatch (tao-0.35.2
src/platform_impl/windows/window_state.rs apply_diff: it no-ops when
its tracked flags already match the requested flags) and isn't
reachable through Tauri's WebviewWindow API. Documented as a known
limitation in the task report.
handle_command's Result was propagated via `?` out of run()'s event
loop, so any store.load()/save() failure from any toggle (not just
Show Window) tore down the entire tray runtime. tray.rs then logged
the error with eprintln! (which goes nowhere under
windows_subsystem="windows") and called app_handle.exit(0), making a
real failure look like a clean quit to anything watching the process.

- run()'s loop now matches on handle_command's result: only Ok(true)
  (an actual Quit command) breaks the loop; Err logs and continues.
- The show_window closure's unminimize()/show()/set_focus() calls no
  longer propagate via `?` -- each failure is logged individually and
  the closure always returns Ok(()), mirroring the old stub's
  `let _ = window.show();` but with a log line instead of silent
  discard.
- If run() still returns Err for something more fundamental (e.g.
  tray/window creation itself failing), tray.rs now calls
  std::process::exit(1) instead of app_handle.exit(0), so a real
  failure gets a non-zero exit code.

Adds a regression test that injects a real failure (a SettingsStore
backed by a directory instead of a file) and confirms handle_command
returns Err without panicking, and that a subsequent command against
a healthy store still succeeds -- proving one failure doesn't corrupt
shared tray state for later commands.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015kUr7Kf9wN15TPqDiKsiKw
The path-scoped subkey hashing in WindowsRegistryBackend::subkey_for
hashed any non-default settings path into a brand new permanent
registry subkey under software\b00t\settings-scoped\<hash>, and
nothing ever deleted them. Every test that opened a SettingsStore over
a tempfile::tempdir() path (~20 across this crate) leaked one of these
on every run -- confirmed 171 orphaned subkeys accumulated under
HKCU:\software\b00t\settings-scoped from prior sessions before this
fix. DefaultHasher is also not guaranteed stable across Rust releases,
and the `path == default_settings_path()` special case was a fragile
exact comparison.

Converges on the pattern tests/settings_atomicity.rs already
established: test the backend directly instead of hashing a scoping
key into production code.

- SettingsStore::with_backend(path, Box<dyn SettingsBackend>) lets a
  caller inject an explicit backend, bypassing create_backend's
  platform auto-selection. SettingsStore::new (still create_backend
  under the hood) is unchanged for real production use.
- Every SettingsStore::new(...) call site that existed purely for test
  isolation over a temp path is repointed to
  SettingsStore::with_backend(path, Box::new(JsonFileBackend::new(path)))
  via a small store_over() helper: tray/runtime.rs's and
  settings/store.rs's own test modules, tests/settings_roundtrip.rs,
  and tests/tray_wiring_smoke.rs. Grepped for every remaining
  SettingsStore::new( call site -- the ones left all use
  default_settings_path() from real production binaries
  (bin/tauri/main.rs, bin/tauri/tray.rs, bin/legacy/host-*.rs,
  bin/notify-test.rs). settings_backend.rs has no test module of its
  own testing create_backend's platform-selection logic, so there was
  nothing to exempt from this conversion.
- WindowsRegistryBackend drops subkey_for/DefaultHasher/the
  -scoped\<hash> namespace entirely and always opens the single fixed
  SETTINGS_PATH key. Deletes the 3 tests that existed solely to test
  subkey_for, since that mechanism is gone.

Verified: settings-scoped subkey count under HKCU is unchanged
(171 -> 171) across a full `cargo test -p ledgerr-host --lib --tests
--bins -j 2` run with this fix in place -- zero new leaked keys.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015kUr7Kf9wN15TPqDiKsiKw
…t startup

The tray menu exposed start_minimized_to_tray and
window_visible_on_start as real, persisted, toggleable settings, but
main.rs never read either -- the main window was always built with
.visible(true) hardcoded, so both toggles were inert.

Settings are now loaded once, before the window is built.
window_visible_on_start decides ordinary startup visibility;
start_minimized_to_tray overrides it to force the window hidden even
when window_visible_on_start is true (it does not force the window
visible when window_visible_on_start is false). On a settings load
failure, falls back to the previous hardcoded .visible(true) rather
than hiding the window unexpectedly. The existing enable_tray check
for tray::setup_tray now reuses this same loaded settings value
instead of loading a second time.

Note: TrayState::from_settings (tray/state.rs) does not itself
combine these two fields today -- it threads window_visible_on_start
through alone -- so this override is implemented fresh here based on
the fields' intent, not mirrored from existing logic.

Close-to-tray (hiding instead of quitting on the window's close
button) is deliberately NOT implemented: there is no existing settings
field for it, and overloading start_minimized_to_tray -- a toggle
explicitly labeled about startup behavior -- to also gate close-button
behavior would be a surprising semantic stretch. Fixed only startup
visibility, the more clearly in-scope half of this finding; see
final-fix-report.md for full reasoning.

Verified live on the Windows build machine: built host-tauri.exe,
toggled start_minimized_to_tray via the tray's own WM_COMMAND
(CMD_START_MINIMIZED=105) PostMessageW'd to the tray window, and
restarted the process four times across both setting values --
confirmed via real EnumWindows/IsWindowVisible that the main window's
visibility now reproducibly tracks the setting in both directions.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015kUr7Kf9wN15TPqDiKsiKw
Comment on lines +31 to +35
impl Default for InstallScope {
fn default() -> Self {
Self::PerUser
}
}
Comment on lines +31 to +35
impl Default for InstallScope {
fn default() -> Self {
Self::PerUser
}
}
…ndows-rs toasts

The tray's toast notifications shelled out to powershell.exe running
Import-Module BurntToast; New-BurntToastNotification — requiring a
separately-installed PowerShell module on every machine, and the actual
cause of live-validation's "Test Toast" failure (BurntToastUnavailable)
on a machine without it installed.

A native windows::UI::Notifications-based ToastNotifier already existed
in this crate (notification::windows_toast) but was never wired into
the tray's actual notification path — only its own unused tests
exercised it. Added notify::native::NativeToastNotifier, implementing
the same notify::types::Notifier trait the tray already consumes,
wrapping that existing native implementation (and the stderr fallback
on non-Windows) instead of shelling out to any external process.

Renamed NotificationBackend::PowerShell -> Native (serde alias keeps
reading settings persisted under the old name). Verified live via
notify-test.exe --backend native: real "toast sent"/"ready" result
with BurntToast confirmed not installed on this machine.
@elasticdotventures

Copy link
Copy Markdown
Member Author

Superseded by #216. This branch was built against a pre-client/server SettingsStore snapshot of main — while it was in progress, main independently landed a settings-service refactor (ledgrrr-settings crate, SettingsClient/ledgrrr-service) as part of the broader ufo-types/sysml-v2/SHACL work. Rather than force-merge conflicting settings architectures, #216 ports the same fixes (tray/Tauri integration, native toast, error-handling hardening) directly onto the new client/server model.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants