feat(desktop): complete system tray lifecycle - #187
Conversation
Reviewer's GuideImplements a complete desktop system tray lifecycle across Rust/Tauri, React UI, Playwright tests, and docs: introduces a dedicated desktop module for tray/menu/notification handling, wires it into launcher config and window lifecycle, exposes tray-related commands to the frontend, adds UI controls and tests for tray behavior, and documents the feature in English and Chinese manuals. Sequence diagram for tray quick launch from system traysequenceDiagram
actor User
participant TrayIcon
participant AppHandle
participant TrayController
participant InstanceStore
participant GameStore
User->>TrayIcon: select quick-launch menu item
TrayIcon->>AppHandle: on_menu_event(id)
AppHandle->>AppHandle: emit("tray-quick-launch", TrayLaunchRequest)
AppHandle-->>TrayController: event "tray-quick-launch"
TrayController->>TrayController: quickLaunch(request)
TrayController->>InstanceStore: useInstanceStore.getState()
alt instance missing
TrayController->>InstanceStore: instanceState.refresh()
TrayController->>InstanceStore: useInstanceStore.getState()
end
TrayController->>InstanceStore: instanceState.setActiveInstance(instance)
TrayController->>GameStore: startGame(request.instanceId, request.versionId)
GameStore-->>TrayController: result
alt result === null
TrayController->>AppHandle: showMainWindow()
end
Sequence diagram for download progress updates and tray notificationssequenceDiagram
participant DownloadStore
participant TrayController
participant AppHandle
participant TrayDownloadState
participant Notification
DownloadStore-->>TrayController: state change(active, percentage, status)
TrayController->>TrayController: useEffect
TrayController->>AppHandle: update_tray_download_status(status)
AppHandle->>TrayDownloadState: lock state.0
TrayDownloadState-->>TrayDownloadState: TrayDownloadStatus.normalized()
TrayDownloadState-->>AppHandle: updated status
AppHandle->>AppHandle: refresh_tray(app)
AppHandle-->>TrayController: command result
alt completed && enable_system_tray
AppHandle->>Notification: notification().builder().title(download_complete_title).body(download_complete_body).show()
end
File-Level Changes
Assessment against linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Workspace change through: 34d7f861 changesets found Planned changes to release
|
There was a problem hiding this comment.
Hey - I've left some high level feedback:
- The
current_confighelper indesktop.rsclones the entireLauncherConfigon every call, including within hot paths likerefresh_trayand notifications; consider passing a borrowed config or caching it per operation to reduce redundant cloning and lock contention. - In
TrayController, thequickLaunchhelper re-fetches and searches instances from the store even though a subscription is already driving tray refresh; consider reusing the subscribed state or precomputing a lookup map to avoid repeated full-list scans on each tray launch.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The `current_config` helper in `desktop.rs` clones the entire `LauncherConfig` on every call, including within hot paths like `refresh_tray` and notifications; consider passing a borrowed config or caching it per operation to reduce redundant cloning and lock contention.
- In `TrayController`, the `quickLaunch` helper re-fetches and searches instances from the store even though a subscription is already driving tray refresh; consider reusing the subscribed state or precomputing a lookup map to avoid repeated full-list scans on each tray launch.Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
|
Thanks for the review. Both choices are intentional: the config snapshot releases the ConfigState mutex before native menu and notification calls, and the launcher config is small with progress updates already throttled. The tray launch handler reads the live Zustand state so a click cannot use a stale instance map; the subscription only synchronizes native menu labels. |
There was a problem hiding this comment.
Pull request overview
Implements a complete “desktop lifecycle” system tray experience for the Tauri launcher, including tray-managed window visibility, quick-launch of recent instances, bounded download progress + notifications, and corresponding UI/config/docs updates.
Changes:
- Added a new Rust
desktopmodule that owns tray menu construction, refresh logic, download progress badging, and desktop notifications. - Extended launcher settings/config (Rust + UI bindings + Settings page) with progressive tray lifecycle toggles (close-to-tray, start-minimized, minimize-after-launch).
- Added UI tray controller wiring, Playwright coverage, and bilingual documentation for the system tray workflow.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src-tauri/src/main.rs | Wires tray module into app lifecycle, adds notification plugin, updates launch/close behaviors, and exposes new tray-related commands. |
| src-tauri/src/desktop.rs | New module implementing tray menu, quick launch targets, download badge rendering, and system notifications. |
| src-tauri/src/core/config.rs | Adds tray lifecycle config fields, defaults, sanitization, and legacy-defaults test. |
| packages/ui/tests/launcher-fixtures.spec.ts | Adds Playwright tests validating tray settings progression + tray quick-launch recovery behavior. |
| packages/ui/src/types/bindings/core.ts | Adds TrayDownloadStatus binding type for tray download status updates. |
| packages/ui/src/types/bindings/config.ts | Adds tray lifecycle fields to LauncherConfig bindings. |
| packages/ui/src/pages/settings.tsx | Updates settings layout constraints and adds system tray lifecycle switches + hint text. |
| packages/ui/src/pages/index.tsx | Mounts the new TrayController at the app shell level. |
| packages/ui/src/locales/zh-CN.json | Adds/updates Chinese strings for tray lifecycle settings and hints. |
| packages/ui/src/locales/en.json | Adds/updates English strings for tray lifecycle settings and hints. |
| packages/ui/src/lib/launcher-runtime.ts | Extends supported fixture names for tray quick-launch scenarios. |
| packages/ui/src/fixtures/launcher.ts | Updates fixture config defaults, adds new scenarios, and tracks invoked commands for assertions. |
| packages/ui/src/components/updater.tsx | Emits a desktop notification when an update is available (tray-enabled). |
| packages/ui/src/components/tray-controller.tsx | New component bridging tray events to UI actions and pushing download progress updates to the tray. |
| packages/ui/src/components/config-editor.tsx | Extends config editor schema to include new tray lifecycle fields. |
| packages/ui/src/client.ts | Adds client wrappers for new tray/notification commands. |
| packages/docs/content/zh/manual/index.mdx | Mentions the new desktop lifecycle capability in the Chinese manual index. |
| packages/docs/content/zh/manual/features/system-tray.mdx | New Chinese feature page documenting the tray workflow. |
| packages/docs/content/zh/manual/features/meta.json | Adds the system tray feature page to the Chinese manual nav metadata. |
| packages/docs/content/zh/manual/features/index.mdx | Adds system tray to the Chinese features index and feature matrix. |
| packages/docs/content/en/manual/index.mdx | Mentions the new desktop lifecycle capability in the English manual index. |
| packages/docs/content/en/manual/features/system-tray.mdx | New English feature page documenting the tray workflow. |
| packages/docs/content/en/manual/features/meta.json | Adds the system tray feature page to the English manual nav metadata. |
| packages/docs/content/en/manual/features/index.mdx | Adds system tray to the English features index and feature matrix. |
| .changes/alpha9-system-tray.md | Adds a changeset entry describing the new tray lifecycle feature set. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 25 changed files in this pull request and generated no new comments.
Suppressed comments (3)
src-tauri/src/main.rs:1869
save_raw_configwrites the normalized config to disk and updates in-memory state, but will still return an error ifrefresh_trayfails. That can report a failed save even though the config is already persisted. Consider treating the tray refresh as best-effort here too.
// Update in-memory state
*state.config.lock().unwrap() = new_config;
desktop::refresh_tray(&app)?;
Ok(())
src-tauri/src/main.rs:1828
save_settingsnow fails if refreshing the system tray menu fails, even though the config has already been written to disk. That changes the command’s error semantics and can surface spurious “save failed” UI states unrelated to persistence. Consider making the tray refresh best-effort (consistent withstart_game).
This issue also appears on line 1865 of the same file.
config.sanitize();
*state.config.lock().unwrap() = config;
state.save()?;
desktop::refresh_tray(&app)?;
Ok(())
src-tauri/src/desktop.rs:491
update_tray_download_statuscallscurrent_config(&app)twice, which means two mutex locks + clones on a hot path (download progress updates) and could even observe different config values between the two reads. Read config once and reuse it for the enable check + language selection.
if completed && current_config(&app).enable_system_tray {
let labels = tray_labels(¤t_config(&app).language);
let _ = app
9958a17 to
b982949
Compare
|
Rebased onto the released Alpha 8 mainline and completed the final review hardening in b982949: persisted config saves no longer report failure when a best-effort tray refresh fails, and download completion reuses one config snapshot. Quick launch already reuses the subscribed instance store and refreshes only when the requested target is missing. Validation: 233 Rust tests passed (1 external fixture ignored), 138/138 Playwright tests passed across both window profiles, UI unit/build and docs build passed, and full Prek/Clippy passed. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 25 out of 25 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
packages/ui/src/components/tray-controller.tsx:21
- TrayController defines its own
TrayLaunchRequestinterface, but other cross-boundary payloads in this repo are exported from Rust viats_rsinto the generated bindings (e.g.,GameExitedEvent), then consumed from@/types. Keeping this tray payload as an ad-hoc TS interface risks drift between the Rust emitter and the UI listener over time.
Consider exporting the Rust TrayLaunchRequest from src-tauri into the bindings (with #[derive(TS)] + #[ts(export, export_to = "core.ts")]) and importing that generated type here instead of redefining it.
interface TrayLaunchRequest {
instanceId: string;
instanceName: string;
versionId: string;
}
Summary
Platform behavior
Validation
cargo test --manifest-path src-tauri/Cargo.toml: 199 passed, 1 ignoredcargo clippy --manifest-path src-tauri/Cargo.tomlpnpm -C packages/ui lintpnpm -C packages/ui buildpnpm -C packages/docs buildpnpm deploy:docs:dry-runpnpm exec prek run --all-filesCloses #48