feat(desktop): Windows tray/Tauri integration, retire legacy Slint UI - #213
Closed
elasticdotventures wants to merge 19 commits into
Closed
feat(desktop): Windows tray/Tauri integration, retire legacy Slint UI#213elasticdotventures wants to merge 19 commits into
elasticdotventures wants to merge 19 commits into
Conversation
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.
3 tasks
Member
Author
|
Superseded by #216. This branch was built against a pre-client/server |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 standalonehost-tray.exebinary had the full feature set but pointed "Show Window" at a legacy Slint UI (host-window.exe). This PR merges them into one implementation.host-window.exehost-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)host-window.exe/host-tray.exeas build targets (source kept undersrc/bin/legacy/for reference, no longer compiled)SW_SHOWvsSW_RESTORE)start_minimized_to_tray,window_visible_on_start) were never read by the main app at startuppathargument entirely, silently sharing one global mutable key across everySettingsStoreinstancewindows-rstoast implementation (notify::native::NativeToastNotifier, wrapping the already-present but previously-unwirednotification::windows_toast::ToastNotifier) — no external process, no module install required on the target machine.NotificationBackend::PowerShellrenamed toNative(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.mdanddocs/superpowers/plans/2026-08-29-tray-tauri-integration.md.Test plan
cargo test -p ledgerr-host --lib --tests --bins— 111 passing, 0 failedhost-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, nohost-window.exe/host-tray.exeprocess ever spawnedBurntToastmodule confirmed not installed on the validation machine;notify-test.exe --backend nativereturned a real"status": "ready"/"message": "toast sent"result via the nativewindows::UI::NotificationsAPI — the exact case that previously failed withBurntToastUnavailable🤖 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