Skip to content

feat(desktop): Windows system tray with live running-agent count - #8423

Open
SkyVence wants to merge 10 commits into
pingdotgg:mainfrom
SkyVence:feat/windows-tray-background
Open

feat(desktop): Windows system tray with live running-agent count#8423
SkyVence wants to merge 10 commits into
pingdotgg:mainfrom
SkyVence:feat/windows-tray-background

Conversation

@SkyVence

@SkyVence SkyVence commented Aug 27, 2026

Copy link
Copy Markdown

What Changed

Adds a Windows system tray for the desktop app:

  • Close-to-tray (default on): closing the window hides T3 Code to the hidden icons area (system tray) and keeps the local backend and its agents running. Minimize-to-tray is optional. Both toggles live in Settings → Connections under the WSL backend row, alongside the other "This environment" backend controls.
  • The tray header and tooltip show a live count of agents actively working on this machine, derived from thread shell snapshots. Scope is the local machine only: the primary backend plus desktop-local secondaries (WSL). Remote, SSH, and relay environments are excluded.
  • Known limitation: background subagent work that outlives a turn is not counted; the tray reads idle once the session settles.

Why

Closing the desktop window on Windows killed all running agents — there was no way to keep long-running work alive in the background. The tray keeps the backend running and makes "is anything still working?" visible at a glance without reopening the window.

UI Changes

System tray (new) — the header shows the live running-agent count; before this fix it always read "No agents running":

Tray menu showing 1 agent running

Settings placement — tray toggles under Connections → This environment, below the WSL backend row:

Before After
Connections settings without tray toggles Connections settings with Keep in system tray on close and Minimize to tray under WSL backend

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes
  • I included a video for animation/interaction changes (not applicable — no motion changes)

Built with Claude Fable 5 via Zed agent.

Note

Add Windows system tray with live running-agent count and close-to-tray settings

  • Introduces DesktopTray and ElectronTray services to manage the system tray icon, context menu, and tooltip on Windows.
  • Adds closeToTray and minimizeToTray boolean settings to DesktopSettings and persists them via IPC.
  • Adds useTrayRunningCountSync hook in the web app to push the local running agent count to the tray.
  • Behavioral Change: On Windows, closing the last window or minimizing it now hides the app to the tray instead of quitting when closeToTray or minimizeToTray are enabled (default closeToTray is true). DesktopLifecycle suppresses app.quit on window-all-closed under these conditions.

Macroscope summarized 41d4231.

…ns support

Add ElectronTray Effect wrapper and DesktopTray service that creates
a system tray icon (Windows hidden icons / taskbar) using the T3
icon from DesktopAssets. Tray shows running agents count, offers
Show/Settings/Pause agents/Enable background service/Quit actions,
and syncs tooltip. Close/minimize to tray gated by new
DesktopAppSettings fields closeToTray/minimizeToTray (persisted via
electron-store) and only on win32 to avoid changing darwin behavior.
DesktopWindow hides on close/minimize when tray enabled instead of
destroying; DesktopLifecycle window-all-closed is suppressed when
closeToTray is active so the backend pool stays alive. Wired via
main.ts layers and registered after app.whenReady. Adds unit tests
for tray menu building and fixes pre-existing typecheck issues in
orchestration and DesktopClerk.

Background service keeps agents alive after UI dismissed; Pause
agents toggle provides the requested disable button rather than a
header control. Tested with vp exec vitest for DesktopAppSettings,
DesktopLifecycle, DesktopWindow, DesktopTray.
…ttings toggle, sync icons

- Fix blank Show by using desktopWindow.activate instead of
  revealOrCreateMain; fixes stuck background-color window on first
  tray Show after hide.
- Fix Settings via desktopWindow.dispatchMenuAction('open-settings')
  (MENU_ACTION_CHANNEL) instead of raw 't3-menu-action' send.
- Simplify tray menu: remove Pause agents / Enable background toggles
  from tray per feedback; keep only header count + Show/Settings/Quit.
  Background controls belong in Settings.
- Add desktop tray settings to UI: new IPC tray.ts
  (GET_TRAY_SETTINGS, SET_CLOSE/MINIMIZE) + preload + contracts
  DesktopBridge types, and DesktopTraySettings section in
  GeneralSettingsPanel (Desktop group) with switches for
  closeToTray/minimizeToTray (persisted via DesktopAppSettings).
  Positions considered: General→Desktop (chosen, near confirmQuit),
  Appearance→Window, System→Desktop.
- Fix icon sync nightly/stable: DesktopTray now checks
  settings.updateChannel and prefers assets/nightly/nightly-windows.ico
  for unpacked dev when nightly, otherwise uses DesktopAssets
  iconPaths (packaged builds already bundle correct per-channel icon
  via electron-builder resources).
- Fix tray lifetime: remove Effect.scoped that destroyed tray right
  after register (now forkScoped only).
- Keep T3_TRAY_TEST_BYPASS gate for parallel nightly testing.
- Add IPC SET_TRAY_RUNNING_COUNT (tray.ts, channels, preload,
  DesktopIpcHandlers, contracts DesktopBridge)
- Add useTrayRunningCountSync hook in web (AppRoot) that counts
  threads where session.status is running/starting via
  appAtomRegistry + environmentThreadDetails.detailAtom and pushes
  to main via window.desktopBridge.setTrayRunningCount
- Tray header/tooltip now shows live N agents running instead of 0
The tray count was computed from per-thread detail atoms that are only
mounted for the open thread, read imperatively with no subscription, so
it stayed at 0 and the tray always said no agents were running.

Derive the count from thread shell snapshots instead, which carry every
thread's session status, and subscribe reactively. Scope it to this
machine only: the primary backend plus desktop-local secondaries such
as WSL. Background liveness (subagents outliving the turn) is a known
limitation and is not counted.
The tray toggles configure whether the desktop-hosted backend keeps
running after the window closes, so they belong with the other backend
controls in Connections (under the WSL backend rows) rather than a
lone Desktop section on the General page.
@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e8aba250-e92a-4d76-9ed2-001397d2be4a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 27, 2026
wslBackendEnabled: false,
wslDistro: null,
wslOnly: false,
closeToTray: true,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 High settings/DesktopAppSettings.ts:93

On Linux, closing the last window leaves the process and backend running with no restorable window. closeToTray: true suppresses app.quit, but the close-to-tray handler only hides windows on win32; default this setting to true only on Windows.

Suggested change
closeToTray: true,
closeToTray: process.platform === "win32",
Also found in 1 other location(s)

apps/desktop/src/app/DesktopLifecycle.ts:242

window-all-closed suppresses app.quit for every non-macOS platform when closeToTray is true, but the window close handler only hides instead of destroys on win32. Since the default settings set closeToTray: true without a platform override, closing the last Linux window destroys it and then leaves the Linux process/backend alive (and no normal window) instead of exiting.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/desktop/src/settings/DesktopAppSettings.ts around line 93:

On Linux, closing the last window leaves the process and backend running with no restorable window. `closeToTray: true` suppresses `app.quit`, but the close-to-tray handler only hides windows on `win32`; default this setting to `true` only on Windows.

Also found in 1 other location(s):
- apps/desktop/src/app/DesktopLifecycle.ts:242 -- `window-all-closed` suppresses `app.quit` for every non-macOS platform when `closeToTray` is true, but the window close handler only hides instead of destroys on `win32`. Since the default settings set `closeToTray: true` without a platform override, closing the last Linux window destroys it and then leaves the Linux process/backend alive (and no normal window) instead of exiting.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c0734e9 by gating the behavior rather than the default: the window-all-closed suppression now requires win32 (with lifecycle tests for both paths), so the flag is inert off Windows everywhere it is read (DesktopWindow hide-on-close, DesktopLifecycle, and the settings UI). The default itself stays platform-independent because this repo's lint forbids process.platform outside the HostProcessPlatform service, which is not available at this module's top level.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

ipcRenderer.invoke(IpcChannels.SET_WSL_BACKEND_ENABLED_CHANNEL, enabled),
setWslDistro: (distro) => ipcRenderer.invoke(IpcChannels.SET_WSL_DISTRO_CHANNEL, distro),
setWslOnly: (enabled) => ipcRenderer.invoke(IpcChannels.SET_WSL_ONLY_CHANNEL, enabled),
getTraySettings: () => ipcRenderer.invoke(IpcChannels.GET_TRAY_SETTINGS_CHANNEL),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium src/preload.ts:103

getTraySettings is exposed on macOS and Linux, so DesktopTrayRows displays Windows-only close/minimize-to-tray toggles that can be persisted but have no effect because the window handlers only honor them on environment.platform === "win32". Gate this bridge method (and the related tray setters) by platform, or make the UI visibility check explicitly require Windows.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/desktop/src/preload.ts around line 103:

`getTraySettings` is exposed on macOS and Linux, so `DesktopTrayRows` displays Windows-only close/minimize-to-tray toggles that can be persisted but have no effect because the window handlers only honor them on `environment.platform === "win32"`. Gate this bridge method (and the related tray setters) by platform, or make the UI visibility check explicitly require Windows.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c0734e9 via the UI-gate option: DesktopTrayRows now also requires isWindowsPlatform(navigator.platform), the tray count sync hook is a no-op off Windows, and the tray itself only registers on win32. The preload stays platform-independent because the lint bans process.platform there.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I'm unable to act on this request because you do not have permissions within this repository.

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Effect service conventions review of the tray feature. Five findings, mostly around the new DesktopTray service's error model and runtime boundary, plus two collateral changes that weaken existing code.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/desktop/src/app/DesktopClerk.ts Outdated
Comment thread apps/desktop/src/app/DesktopLifecycle.ts
Comment thread apps/desktop/src/app/DesktopTray.ts Outdated
Comment thread apps/desktop/src/app/DesktopTray.ts Outdated
Comment thread packages/contracts/src/orchestration.ts Outdated

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the in-scope web UI changes (AppRoot.tsx, ConnectionsSettings.tsx, hooks/useTrayRunningCountSync.*). The new rows correctly reuse SettingsRow + the Switch primitive with aria-labels, and the tray count hook stays local-environment scoped. Three consistency points on the new settings rows and AppRoot.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx Outdated
Comment thread apps/web/src/AppRoot.tsx
Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx Outdated
SkyVence and others added 2 commits August 27, 2026 19:41
Address PR review findings:

- Suppress window-all-closed quit only on win32; elsewhere the window
  handlers destroy on close, so suppressing stranded a windowless app.
  Covered by new lifecycle tests for both paths.
- Register the tray only on win32 and hide the tray settings rows and
  running-count sync off Windows, where the toggles had no effect.
- Run tray Electron callbacks with the captured fiber context so
  logging and tracing stay wired to the app runtime.
- Derive DesktopTrayError's message from the class instead of a
  singleton reason field; drop the (error as any).message casts.
- Revert the single-instance test bypass and the contracts schema
  cast that leaked in from manual testing.
- Keep AppRoot's doc comment attached to AppRoot.
- Render tray settings rows only after the persisted state loads,
  disable them while a write is in flight, and surface failures.
@SkyVence
SkyVence marked this pull request as ready for review August 27, 2026 17:43
Comment thread apps/desktop/src/app/DesktopLifecycle.ts Outdated

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Effect service conventions review of the tray changes. The earlier findings on the tray error's singleton reason, the bare Effect.runPromise in the tray callbacks, the win32 gating of the window-all-closed suppression (now covered by tests), and the unrelated DesktopClerk/contracts edits all look resolved. Three remaining items below, all in apps/desktop.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/desktop/src/window/DesktopWindow.ts Outdated
Comment thread apps/desktop/src/app/DesktopTray.ts Outdated
Comment thread apps/desktop/src/app/DesktopTray.ts Outdated

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Effect service conventions review of the tray changes. The earlier findings on the tray error's singleton reason, the bare Effect.runPromise in the tray callbacks, the win32 gating of the window-all-closed suppression (now covered by tests), and the unrelated DesktopClerk/contracts edits all look resolved. Three remaining items below, all in apps/desktop.

Posted via Macroscope — Effect Service Conventions

Comment thread apps/desktop/src/window/DesktopWindow.ts Outdated
Comment thread apps/desktop/src/app/DesktopTray.ts Outdated
Comment thread apps/desktop/src/app/DesktopTray.ts Outdated
@macroscopeapp

macroscopeapp Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR adds a substantial Windows tray workflow with native Electron integration, persistent settings, IPC, live renderer synchronization, and changed close/minimize/shutdown behavior. The cross-platform lifecycle surface and unresolved platform-specific findings require human verification.

Not approved because:

  • 2 blocking correctness issues found at or above your repo's Minimum Blocking Severity

Adjust the Minimum Blocking Severity for this repo — including turning it Off — in Settings. You can add or adjust custom eligibility rules. Learn more.

@macroscopeapp macroscopeapp Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One finding on the new Windows tray settings rows: the write-failure message is only wired into the first row, so a failed "Minimize to tray" write reports under the neighbouring setting.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/settings/ConnectionsSettings.tsx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant