Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions apps/desktop/plans/20260405-quit-tray-lifecycle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# macOS Quit & Tray Lifecycle

## Decision (2025-04-05)
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.

⚠️ Potential issue | 🟡 Minor

Fix the decision date typo.

Line 3 says 2025-04-05, but the filename is 20260405-quit-tray-lifecycle.md. This should be 2026-04-05 to keep the decision log chronologically correct.

Suggested edit
-## Decision (2025-04-05)
+## Decision (2026-04-05)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
## Decision (2025-04-05)
## Decision (2026-04-05)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/plans/20260405-quit-tray-lifecycle.md` at line 3, Update the
decision date string in the markdown header: change the text "## Decision
(2025-04-05)" to "## Decision (2026-04-05)" in the file named
20260405-quit-tray-lifecycle.md so the logged decision date matches the filename
and chronological ordering.


All quit paths fully exit the app. No background-to-tray behavior for now.
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 6, 2026

Choose a reason for hiding this comment

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

P2: This decision line documents the opposite of the behavior introduced in this PR (implicit macOS quit/close backgrounding to tray), so the plan becomes immediately stale and misleading.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/plans/20260405-quit-tray-lifecycle.md, line 5:

<comment>This decision line documents the opposite of the behavior introduced in this PR (implicit macOS quit/close backgrounding to tray), so the plan becomes immediately stale and misleading.</comment>

<file context>
@@ -0,0 +1,78 @@
+
+## Decision (2025-04-05)
+
+All quit paths fully exit the app. No background-to-tray behavior for now.
+
+The tray exists while the app is running and provides host-service management and explicit quit actions. When the app quits, the tray goes away.
</file context>
Suggested change
All quit paths fully exit the app. No background-to-tray behavior for now.
All implicit macOS quit paths background to tray; explicit lifecycle intents (`exit_release`, `exit_stop`, `install_update`, `restart`) perform full exit behavior.
Fix with Cubic


The tray exists while the app is running and provides host-service management and explicit quit actions. When the app quits, the tray goes away.

### What shipped

- **Lifecycle intents** (`exit_release`, `exit_stop`, `restart`) replace the overloaded `QuitMode` (`"release" | "stop"`). Explicit intents skip the confirm-on-quit dialog and route directly to the exit path.
- **Updater fix**: `installUpdate()` uses `prepareIntent("exit_release")` so `before-quit` skips the confirm dialog and exits cleanly. The old `prepareQuit("release")` was intercepted by the macOS background-to-tray block when services were active, preventing updates from installing.
- **Tray menu rename**: "Quit (Keep Services Running)" is now "Quit Superset" for clarity.
- **Restart consolidation**: `restartApp` tRPC endpoint uses `requestExit("restart")` instead of manual `app.relaunch()` + `app.exit(0)`.
- **Removed macOS background-to-tray block** from `before-quit`. The old block prevented quit and kept tray alive when `hasActiveInstances()` was true, but left the dock icon visible (confusing UX).

### What was deferred

Background-to-tray on macOS (Cmd+Q destroys windows but keeps tray alive) is the ideal target but was deferred because:

1. **Dock icon stays visible** — macOS shows the dock icon as long as the Electron process is alive. Backgrounding to tray looks like the app is still running, which is confusing.
2. **Solving the dock icon requires a process split** — hiding the dock icon via `app.dock.hide()` has side effects (loses menu bar, loses Cmd+Tab). A clean solution requires a separate lightweight tray-host process, which is significant work.

## Current behavior

### Quit paths

| Action | Behavior |
|--------|----------|
| Cmd+Q | Full exit (release services, dispose tray, exit) |
| Dock right-click Quit | Same |
| App menu Quit | Same |
| Window close (red-X / Cmd+W) | macOS: hide window (standard behavior). Non-macOS: close window, then app quits. |
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 6, 2026

Choose a reason for hiding this comment

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

P2: The window-close row describes old macOS hide behavior, not the new destroy-and-recreate flow, so the lifecycle documentation is inaccurate.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/desktop/plans/20260405-quit-tray-lifecycle.md, line 33:

<comment>The window-close row describes old macOS hide behavior, not the new destroy-and-recreate flow, so the lifecycle documentation is inaccurate.</comment>

<file context>
@@ -0,0 +1,78 @@
+| Cmd+Q | Full exit (release services, dispose tray, exit) |
+| Dock right-click Quit | Same |
+| App menu Quit | Same |
+| Window close (red-X / Cmd+W) | macOS: hide window (standard behavior). Non-macOS: close window, then app quits. |
+| Tray "Quit Superset" | `requestExit("exit_release")` — release services, full exit |
+| Tray "Quit & Stop Services" | `requestExit("exit_stop")` — stop services, full exit |
</file context>
Fix with Cubic

| Tray "Quit Superset" | `requestExit("exit_release")` — release services, full exit |
| Tray "Quit & Stop Services" | `requestExit("exit_stop")` — stop services, full exit |
| Tray host-service "Stop" | Stops individual service, app stays running |
| Settings "Restart App" | `requestExit("restart")` — release services, relaunch, exit |
| Update install | `prepareIntent("exit_release")` + `quitAndInstall()` — full exit, updater handles install |

### Host-service lifecycle on quit

- **Release** (`exit_release`, implicit quit): services keep running as detached processes. On next app launch, they are re-adopted via manifest files.
- **Stop** (`exit_stop`): services are terminated via `SIGTERM`.

### Key files

- `src/main/lib/lifecycle.ts` — lifecycle intent model
- `src/main/index.ts` — `before-quit` handler
- `src/main/windows/main.ts` — window close behavior
- `src/main/lib/tray/index.ts` — tray menu and actions
- `src/main/lib/auto-updater.ts` — update install flow
- `src/lib/electron-app/factories/app/setup.ts` — `activate` / `window-all-closed` handlers

## Future: tray-resident background

If we want the tray to persist after quit (like Docker Desktop), there are two viable architectures:

### Option A: Electron tray host + separate UI Electron

A small Electron process owns the tray and spawns the main UI Electron app on demand.

- Pros: shared JS/TS stack, easiest evolution from current code
- Cons: two Electron runtimes, packaging/update complexity

### Option B: Native Swift tray host + Electron UI

A native macOS menu bar app owns the tray. The Electron app is launched/attached on demand.

- Pros: smallest memory footprint, cleanest separation
- Cons: native code, signing, IPC complexity

Either option requires:
1. A separate long-lived process that owns the tray icon
2. Socket/named-pipe IPC between tray host and UI
3. A launch-on-login mechanism (launchd)
4. Update coordination between two processes

This is medium-term work and not needed for the current product requirements.
16 changes: 1 addition & 15 deletions apps/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,21 +190,6 @@ app.on("before-quit", async (event) => {
const quitMode = pendingQuitMode;
pendingQuitMode = null;

const manager = getHostServiceManager();

// macOS: close windows & keep tray alive when services should stay running
if (
PLATFORM.IS_MAC &&
(quitMode === null || quitMode === "release") &&
manager.hasActiveInstances()
) {
event.preventDefault();
for (const win of BrowserWindow.getAllWindows()) {
win.destroy();
}
return;
}

const isDev = process.env.NODE_ENV === "development";
if (quitMode === null && !isDev && getConfirmOnQuitSetting()) {
event.preventDefault();
Expand All @@ -228,6 +213,7 @@ app.on("before-quit", async (event) => {
}

isQuitting = true;
const manager = getHostServiceManager();
if (quitMode === "stop") {
manager.stopAll();
} else {
Expand Down
Loading