fix(lifecycle): hide main window to tray on close, Linux#451
fix(lifecycle): hide main window to tray on close, Linux#451lizthegrey wants to merge 2 commits intoaaddrick:mainfrom
Conversation
Electron's default window-all-closed handler quits the app on Linux. The existing tray icon and Ctrl+Q patches keep the app reachable while a window is alive, but as soon as the last window is closed (stray click on X, or a sign-out flow that closes mainWindow) the app exits and the tray goes with it — taking any in-app schedulers / MCP servers / cron tasks (/schedule skill) down silently until the user re-launches. Intercept BrowserWindow.close on main windows (not popups; Quick Entry and About already dismiss via hide(), never emit close) and preventDefault + hide unless app is in a real quit path. The quit path is detected via before-quit: Ctrl+Q, tray Quit, cmd+Q, SIGTERM and app.quit() from anywhere all emit before-quit, which arms app._quittingIntentionally so the close handler lets the window actually close. Gated by CLOSE_TO_TRAY, default on. Set CLAUDE_QUIT_ON_CLOSE=1 to restore the Electron-default behaviour. Fixes aaddrick#448 Co-Authored-By: Claude <claude@anthropic.com>
|
Hey Liz — one flag on the test plan after 4e2b9d7 landed. The "Ctrl+Q while window is hidden → app fully quits" checkbox is probably a false pass now. Ctrl+Q is handled through Could you re-run that one with the main window genuinely hidden and unfocused (not just minimized or behind another window) and update the test plan with what you actually observe? If it doesn't quit from there it's not a blocker — tray → Quit still works — but worth documenting the real behavior rather than carrying an inaccurate pass. Written by Claude Opus 4.7 (1M context) via Claude Code |
Ah yep my bad, so I'm indeed unable to close the main window and get Claude application to be focused to then control Q it. HOWEVER, I am able to double click the Claude application icon to get it to show the "How can claude help?" mini pop-up" and control-q works to close from there indeed. |
|
Ah I see the source of my and Claude's confusion. on OS X the claude app remains command tabbable even with no windows open, which is why we thought that was something we had to test |
Summary
BrowserWindow.closeon main windows andpreventDefault + hideso the app survives a stray click on X (or a sign-out flow that closesmainWindow). Popups (Quick Entry, About) already dismiss viahide()and never see close events, so they're unaffected.app._quittingIntentionallyin anapp.on('before-quit')listener so the close handler still lets windows actually close when the user picks a real quit path — Ctrl+Q global shortcut (registered in this same file), tray Quit, cmd+Q, SIGTERM, or any otherapp.quit().CLOSE_TO_TRAY(const at the top of the file, logged on startup). Default is on;CLAUDE_QUIT_ON_CLOSE=1restores Electron-default behaviour for users who prefer X to quit.Motivation
The existing tray patches keep the icon alive while the app runs, but Electron's default
window-all-closed → app.quit()means the app dies the moment the last window closes. In-app schedulers (notably the/scheduleskill) are in-processsetTimeouts, so a missed overnight window = silently missed cron firings the next morning. My own~/.config/Claude/logs/main.logcaught this: cleanbeforeQuitat 15:21 PDT one afternoon, no restart until I noticed ~21 hours later, and an 8am scheduled task that simply never ran.Diagnosis and design are in #448.
Interaction with existing patches
Fixes: #321) — unchanged;app.quit()→before-quitarms the flag → windows close normally.scripts/patches/tray.sh) — its Quit menu item callsapp.quit(), same flow as Ctrl+Q.if (!popup)already gates the main-window-only code path; the close listener lives there, so Quick Entry / About are untouched.Fixes
#448
Test plan
node --check scripts/frame-fix-wrapper.jsclean../build.sh --build deb --clean no.pgrep -f 'app\\.asar'still shows a live non-helper Electron PID.beforeQuitfires inmain.log, all Electron /cowork-vm-servicePIDs exit./scheduletask, close main window, wait → task fires on time.CLAUDE_QUIT_ON_CLOSE=1, close window → app exits (env escape hatch works).BrowserWindow.getAllWindows().win.webContents.close()/ signout flow (if exercised via the UI) doesn't leave the app in a weird half-state.Known trade-offs worth calling out in review
win.close()— if the app itself callsmainWindow.close()for a non-quit reason (e.g., a hypothetical reset flow), our handler will hide instead of close. I don't think the current app does this outside of the real quit path (all the upstream call sites I've seen go throughapp.quit()), but worth watching for during test.~/.config/autostart/integration. With both merged, a user can enable autostart and have the app survive stray closes, which is the combination needed for reliable overnight/scheduleruns. Neither PR depends on the other.Generated with Claude Code
Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com
75% AI / 25% Human
Claude: pulled log evidence from
~/.config/Claude/logs, worked out the close/quit event ordering, drafted the wrapper patch and commit/PR copy, rannode --check.Human: reported the missed-cron symptom that surfaced the bug, made the opt-out-vs-opt-in default call, and will run the manual test plan / own the merge decision.