fix(lifecycle): hide main window to tray on close, Linux (#451)

* fix(lifecycle): hide main window to tray on close, Linux (#448)

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 #448

Co-Authored-By: Claude <claude@anthropic.com>

* fix(frame-fix-wrapper): drop superseded globalShortcut Ctrl+Q

Removes the globalShortcut.register('CommandOrControl+Q') block
that #484 superseded with the per-window
webContents.on('before-input-event') listener. Auto-merging main
into this branch left both registrations in place, which would
re-introduce the AZERTY physical-keycode grab and system-wide
shortcut steal that #484 fixed. The focus-scoped listener
already covers the original #321 hidden-menu-bar use case.

Also updates the close-to-tray comment to reference the new
listener path instead of the removed global shortcut.

Co-Authored-By: Claude <claude@anthropic.com>

* docs(readme): credit lizthegrey for close-to-tray contribution

Co-Authored-By: Claude <claude@anthropic.com>

---------

Co-authored-by: Claude <claude@anthropic.com>
Co-authored-by: aaddrick <aaddrick@gmail.com>
This commit is contained in:
Liz Fong-Jones
2026-04-28 15:16:56 -07:00
committed by GitHub
parent 4cc63bff7a
commit 8530342b2e
2 changed files with 42 additions and 1 deletions

View File

@@ -181,7 +181,9 @@ Special thanks to:
- Quick window submit fix
- **[janfrederik](https://github.com/janfrederik)** for the `--exe` flag to use a local installer
- **[MrEdwards007](https://github.com/MrEdwards007)** for discovering the OAuth token cache fix
- **[lizthegrey](https://github.com/lizthegrey)** for version update contributions
- **[lizthegrey](https://github.com/lizthegrey)**
- Version update contributions
- Close-to-tray on Linux to keep in-app schedulers, MCP servers, and the tray icon alive across window close
- **[mathys-lopinto](https://github.com/mathys-lopinto)**
- AUR package
- Automated deployment

View File

@@ -38,6 +38,18 @@ if (resolvedMode !== rawMenuBarMode) {
}
console.log(`[Frame Fix] Menu bar mode: ${MENU_BAR_MODE}`);
// Keep the app alive when the main window is closed (hide to tray),
// so in-app schedulers / MCP servers / the tray icon survive a
// stray click on X. Explicit quit paths (Ctrl+Q via the focused
// webContents listener above, tray menu Quit, File > Quit, cmd+Q,
// SIGTERM) still go through app.quit() → before-quit, which arms
// the flag so the close handler lets the windows actually close.
// Set CLAUDE_QUIT_ON_CLOSE=1 to restore the Electron-default
// "closing the last window quits the app" behaviour.
const CLOSE_TO_TRAY = process.platform === 'linux'
&& process.env.CLAUDE_QUIT_ON_CLOSE !== '1';
console.log(`[Frame Fix] Close-to-tray: ${CLOSE_TO_TRAY ? 'on' : 'off'}`);
// Detect if a window intends to be frameless (popup/Quick Entry/About)
// Quick Entry: titleBarStyle:"", skipTaskbar:true, transparent:true, resizable:false
// About: titleBarStyle:"", skipTaskbar:true, resizable:false
@@ -156,6 +168,22 @@ Module.prototype.require = function(id) {
}
if (!popup) {
// Close-to-tray: intercept close on main windows and hide
// instead. app.on('before-quit') below sets the flag when
// the user picks an explicit quit path, so real quits still
// let the window actually close. Popups (Quick Entry,
// About) already dismiss via hide() in the upstream code;
// they never see close events, so they're unaffected.
// Fixes: #448
if (CLOSE_TO_TRAY) {
this.on('close', (e) => {
if (!result.app._quittingIntentionally && !this.isDestroyed()) {
e.preventDefault();
this.hide();
}
});
}
// Directly set child view bounds to match content size.
// This bypasses Chromium's stale LayoutManagerBase cache
// (only invalidated via _NET_WM_STATE atom changes, which
@@ -334,6 +362,17 @@ Module.prototype.require = function(id) {
}
};
// Arm the close-to-tray flag on every real quit path
// (app.quit() from Ctrl+Q, tray Quit, cmd+Q, SIGTERM). The
// BrowserWindow close handler above checks this flag to
// decide whether to hide or actually close. Harmless when
// CLOSE_TO_TRAY is off (the close handler is never attached).
if (CLOSE_TO_TRAY) {
result.app.on('before-quit', () => {
result.app._quittingIntentionally = true;
});
}
console.log('[Frame Fix] Patches built successfully');
}