Skip to content

feat(desktop): keep the app running in the tray when the main window closes - #1712

Merged
github-actions[bot] merged 2 commits into
plan/tray-keep-alivefrom
tray-keep-alive/7105f152-42dd-4b92-8d53-72cdfe58850d
Nov 10, 2026
Merged

github-actions[bot] merged 2 commits into
plan/tray-keep-alivefrom
tray-keep-alive/7105f152-42dd-4b92-8d53-72cdfe58850d

Conversation

@JSdotNet

Copy link
Copy Markdown
Owner

What changed

This builds amended local ADR 0043 §5: closing the desktop app's main window hides it to the notification area instead of quitting. The process and the MCP listener keep running, so Claude sessions keep the backlog tools.

Part Where
A pure close policy: Hide when the setting is on, no quit was asked for, and the tray icon is shown; otherwise Quit Backlog.Desktop.UI/Shell/MainWindowClosePolicy.cs
AppWindow.Closing handling. On Hide: cancel the close, flush the navigation and bounds stores, hide the window; popped-out windows stay open. On Quit: the existing flush, close every window, quit. ActivateMainWindow shows the window first. RequestQuit is the tray's Quit Backlog.Desktop/Services/ShellWindows.cs
The tray icon: hand-written Shell_NotifyIcon interop with no package. It uses the exe's icon and the window title as tooltip. Menu: Open Backlog / Quit. Double-click or Enter opens the window. The icon is re-added on TaskbarCreated and removed on exit Backlog.Desktop/Services/TrayIcon.cs (new)
A plain second launch is redirected to the running instance, which shows its main window; the link path is unchanged. A StartupTask activation opens to the tray Backlog.Desktop/Services/BacklogLinkInstance.cs, App.xaml.cs
A toast press opens the centre through ActivateMainWindow. The first hide ever shows a one-time toast, recorded per machine Backlog.Desktop/Services/WindowsAppNotifications.cs
KeepRunningInTray (default true), TrayHintShown and TrayChanged, saved as a tray row only while either value differs from its default WorkspaceSettingsStore.cs
An "In the background" card with two shared Toggles: Keep running in the tray when closed and Start Backlog when I sign in McpServerSettingsPage.razor, SettingsPages.cs
The IStartupRegistration port, backed by the MSIX StartupTask; it answers Unavailable when the process has no package identity Shell/IStartupRegistration.cs, Services/WindowsStartupRegistration.cs
uap5:StartupTask with TaskId BacklogStartup, Enabled="false", DisplayName Backlog Platforms/Windows/Package.appxmanifest

Acceptance criteria and how each was verified

  1. Close policy, unit-tested. MainWindowClosePolicyTests (5 cases).
  2. ✕ / Alt+F4 hides, flushes, and leaves pop-outs open; Quit keeps today's path. Desktop smoke: after WM_CLOSE the window is hidden and the process is alive. The Quit path is unchanged code (Destroying → Flush + CloseEveryOtherWindowAndQuit).
  3. ActivateMainWindow re-shows a hidden window. Desktop smoke: a relaunch and a tray double-click each set the window visible again.
  4. Tray icon. Desktop smoke: the tray window exists; a double-click and NIN_KEYSELECT each open the window. The right-click menu was not driven (see below).
  5. Toasts. The first hide wrote tray.hintShown: true to settings.json, so the toast path ran. OpenCentre now goes through ActivateMainWindow; that was checked by reading the code.
  6. Plain second launch redirected. Desktop smoke: the second process exits 0 within 8 s, the first process's window is visible, and one process remains.
  7. Settings round-trip. WorkspaceTraySettingsTests (7 tests).
  8. Settings page states against a fake port. McpServerBackgroundSettingsTests (9 tests) covers default on, saving, a change made elsewhere, sign-in on/off, Unavailable, DisabledByUser and DisabledByPolicy, and the no-port host. The harness screenshot shows the card in a host without the port.
  9. Manifest and hidden start at sign-in. An MSIX publish with signing off built Backlog.Desktop_1.0.0.1_x64.msix, so MakeAppx accepted the manifest. The hidden start itself was not run (see below).
  10. Unchanged paths. MsixAppUpdateService and BrowserTabShellWindows have no diff, and the mobile head has none either.
  11. Builds and suite. See below.

Build & Test

  • dotnet build Backlog.sln (Debug): 0 warnings, 0 errors.
  • dotnet build Backlog.WithoutAppHeads.slnf -c Release -p:TreatWarningsAsErrors=true: 0 warnings, 0 errors.
  • Desktop head, Release, with CI's packaging overrides and TreatWarningsAsErrors: 0 warnings, 0 errors.
  • MSIX publish (AppxPackageSigningEnabled=false): package built. The only warning is the known mspdbcmf.exe one.
  • dotnet test --solution Backlog.sln: 22,924 total, 22,903 passed, 19 skipped, 2 failed. Both failures are C4WorkspaceTests (No_chapter_carries_a_reference_the_knowledge_index_generator_would_reject, Every_embedded_view_exists_in_the_committed_workspace). They fail on .devbook/arc42/adr/0046-… from the base branch plan/tray-keep-alive, and this PR touches no .devbook/ file. main has newer C4 fixes (fix(c4-zoom): close the C4 zoom navigation review gaps #1707) that the plan branch does not have yet.

Review findings left open

The fresh-context review ran 2 rounds. Round 1 found 2 blockers, both fixed: lock-file churn, and a hide with no tray icon (the policy now requires the icon). The advisories fixed were a cancelled shutdown forcing quit, keyboard select, remembered windows popping up at a sign-in start, and an NIM_MODIFY fallback on TaskbarCreated. Still open, both advisory:

  • SetKeepRunningInTray and MarkTrayHintShown write the settings file without a lock, like every other setter in the store already does.
  • A sign-in start that runs before Explorer is ready stays hidden with no icon until TaskbarCreated arrives; a relaunch brings the window back.

What could not be proved

Please validate these by hand in the installed (MSIX) app:

  • Right-click the tray icon. Open Backlog opens the window. Quit ends the process and removes the icon.
  • Turn Keep running in the tray when closed off, then ✕: the app quits. Restart the app: the switch is still off.
  • Start Backlog when I sign in: on → sign out and in → Backlog starts with no window, the tray icon shows, and the MCP server answers. Disable it in Task Manager → Startup apps → the switch is disabled with "Turned off in Task Manager's Startup apps".
  • In a Debug build, the sign-in switch is disabled with "Available in the installed app". Unit-tested, but not seen in the real head.
  • Hide the window, then sign out or shut down: the app ends cleanly, with nothing lost and no shutdown prompt. An MSIX update still restarts the app.
  • The first-hide toast shows once with the agreed text, and never again. The flag was written; nobody saw the toast.
  • Hide the window, then open a backlog:// link and press a toast: each brings the main window back.
  • Hide the window: open_window from a session still opens a pop-out.
  • Restart Explorer: the icon comes back.

Assumptions

  • The tray window is a never-shown top-level window, not a message-only one. The entry asked for a message-only window, but those receive no broadcasts. TaskbarCreated, which the entry also asks for, and the sign-out and shutdown messages are all broadcasts.
  • The close policy takes a third input, whether the tray icon is shown. Without it, a refused icon would leave a hidden window with no way to quit.
  • The tray icon shows whether or not keep-in-tray is on, so Quit and Open Backlog are always there.
  • The sign-out query (WM_QUERYENDSESSION) only flushes. The confirmed end (WM_ENDSESSION) makes the next close quit, so a shutdown another app cancels does not change ✕.
  • A start at sign-in defers the remembered pop-out windows to the first time the main window is shown.
  • The settings card is titled "In the background" and is drawn in every host. A host with no IStartupRegistration (the web harness) shows a note instead of switches. This keeps SettingsSection.CardTitles true everywhere.
  • The entry's "Update Backlog's own devbook once this prompt lands" is left for after merge. These still say "not built yet": ADR 0043's Status and History, ADR 0012's History row, and the runtime view's "Main window close, hide and quit". That edit goes through flow-spec.
  • The smoke test set tray.hintShown: true in the shared %LOCALAPPDATA%\Backlog.Debug\settings.json, so other Debug worktrees will not show the first-hide toast.

Opened by an unattended execute-plan run (plan tray-keep-alive, entry 7105f152-42dd-4b92-8d53-72cdfe58850d). Personal Validation is still pending; this draft stands in for that gate.

🤖 Generated with Claude Code

…closes

Implements amended local ADR 0043 §5. Closing the main window hides it to a
notification-area icon, so the process and its MCP listener keep answering.

- MainWindowClosePolicy decides Hide or Quit.
- ShellWindows handles AppWindow.Closing: when the policy says Hide, it
  cancels the close, flushes and hides. ActivateMainWindow shows the window
  first.
- TrayIcon is hand-written Shell_NotifyIcon interop with an Open Backlog /
  Quit menu, double-click and Enter, a re-add on TaskbarCreated, and removal
  on exit.
- A plain second launch is redirected to the running instance. A start by
  the sign-in startup task opens to the tray.
- The first hide shows a one-time toast. Toast presses go through
  ActivateMainWindow.
- WorkspaceSettingsStore gains KeepRunningInTray and TrayHintShown.
- The MCP server settings page gains "Keep running in the tray when closed"
  and "Start Backlog when I sign in", through IStartupRegistration, which
  the MSIX StartupTask backs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…4 embed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 10027f6)
@JSdotNet
JSdotNet marked this pull request as ready for review November 10, 2026 21:26
@github-actions
github-actions Bot merged commit bd76f6f into plan/tray-keep-alive Nov 10, 2026
8 checks passed
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.

1 participant