From 430bf6217b51634a6b51e80426607f4b5137e15d Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Mon, 9 Nov 2026 07:57:40 +0100 Subject: [PATCH] fix(desktop): closing the window no longer ends Keep This Mac Awake Keep This Mac Awake lives in the app and ends when the app quits, and closing the main window quit the app. So with a mode on, the red button (or Cmd+W) followed by closing the lid let the MacBook sleep: the lid request file was cleared and the helper released disablesleep at its next run. While a mode is on, closing the main window now hides the app the way Cmd+H does (hide_application), so full screen and pop-outs come back as they were on a click on the Dock icon. Cmd+Q and Quit from the Dock still end it, through LoopDestroyed as before. With keep-awake off, closing the window quits as it always did. Co-Authored-By: Claude Opus 5.5 --- packages/desktop/README.md | 2 +- packages/desktop/src/keep_awake.rs | 22 +++++++++++++++++- packages/desktop/src/main.rs | 37 ++++++++++++++++++++++++++---- 3 files changed, 54 insertions(+), 7 deletions(-) diff --git a/packages/desktop/README.md b/packages/desktop/README.md index 70aab9a6..8030edee 100644 --- a/packages/desktop/README.md +++ b/packages/desktop/README.md @@ -41,7 +41,7 @@ cannot do. | **Downloads** | Codeman's Download links, and downloads Codeman opens in a new window, save the file to `~/Downloads` whatever its type, and a toast in Codeman names the file. Files the web view cannot display are saved the same way. | | **A page that stops** | When the web content process of a window ends (a WebKit crash, or macOS taking the process back under memory pressure), the window loads its page again instead of staying blank. A second time within 30 seconds, the main window goes back to the connect screen and a pop-out closes, so a page that cannot stay up never becomes a reload loop. | | **Apple logo** | On a MacBook Pro with a PROLED XDR display, the lit Apple logo on the lid says what the agents are doing, with a blink style of your own for each state: **View > Blink Apple Logo > While Working**, **Waiting for You (Yellow)** and **Needs Permission (Red)**. Off, Calm, Slow, Wink, Heartbeat, Fast, or Alarm, three sharp flashes for a state that needs you. All three are off until you choose. When several states hold, the most urgent one with a style shows: red, then yellow, then work. A style chosen in an earlier version stays as the one for work, and yellow and red start as Wink and Alarm. The menu only exists on a Mac with that logo. No password is needed. Shell tabs do not count. The app asks the server every two seconds, and a change has to hold for three seconds before the logo follows it, so an agent between two turns does not flicker it. It keeps working while the window is hidden, and with any Codeman server. A logo you switched off yourself stays off, and if the setting changes while a blink runs, the blink ends and leaves it as it found it. While the app is closed nothing blinks, and macOS keeps the logo dark while the display sleeps or the lid is closed, so the blink pauses then and writes nothing. | -| **Keep awake** | **Codeman > Keep This Mac Awake** keeps the Mac from going to sleep while Codeman's agents work on it: **While Agents Work** (an agent at work, and for 5 minutes after the last one stopped, so the gap between two turns or a reply from your phone does not let it drop off), or **Always** (while the app is open). Off until you choose. Only for a Codeman on this Mac (`127.0.0.1`, `localhost`, or one of this Mac's own addresses); for a server on another machine the menu says so. The display still sleeps. No password is needed for this part: the app holds a power assertion of its own, which macOS shows in Activity Monitor and in `pmset -g assertions` as *Codeman keeps this Mac awake while its agents work*, and which ends with the app, a crash or a Force Quit included. On a MacBook, **Only When Plugged In** (on by default) pauses it on battery, and **With the Lid Closed…** keeps a closed MacBook awake as well: nothing an app holds can do that, so ticking it asks for your administrator password and installs a small helper (see Decisions), and unticking it asks again and removes the helper. The lid is never kept awake on battery, whatever the settings, and while the lid is covered *Sleep* in the Apple menu does nothing. A line at the bottom of the menu says what it does right now. It lives in the app, so it ends when the app quits: Codeman keeps running as a LaunchAgent, but nothing keeps the Mac awake then. | +| **Keep awake** | **Codeman > Keep This Mac Awake** keeps the Mac from going to sleep while Codeman's agents work on it: **While Agents Work** (an agent at work, and for 5 minutes after the last one stopped, so the gap between two turns or a reply from your phone does not let it drop off), or **Always** (while the app is open). Off until you choose. Only for a Codeman on this Mac (`127.0.0.1`, `localhost`, or one of this Mac's own addresses); for a server on another machine the menu says so. The display still sleeps. No password is needed for this part: the app holds a power assertion of its own, which macOS shows in Activity Monitor and in `pmset -g assertions` as *Codeman keeps this Mac awake while its agents work*, and which ends with the app, a crash or a Force Quit included. On a MacBook, **Only When Plugged In** (on by default) pauses it on battery, and **With the Lid Closed…** keeps a closed MacBook awake as well: nothing an app holds can do that, so ticking it asks for your administrator password and installs a small helper (see Decisions), and unticking it asks again and removes the helper. The lid is never kept awake on battery, whatever the settings, and while the lid is covered *Sleep* in the Apple menu does nothing. A line at the bottom of the menu says what it does right now. It lives in the app, so it ends when the app quits: Codeman keeps running as a LaunchAgent, but nothing keeps the Mac awake then. That is why, while it is on, closing the Codeman window hides the app instead of quitting it, as Cmd+H does: a click on its Dock icon brings it back, and only Cmd+Q (or Quit from the Dock) ends it. | | **Window** | Size, position and full-screen state are remembered. A saved window that hangs off the screen it opens on (it was last on a larger display that is gone, say) is pulled back on and shrunk to fit; one whose title bar is on no screen at all opens at the default size instead. Clicking the Dock icon brings back a minimized window. | ## Build (on a Mac) diff --git a/packages/desktop/src/keep_awake.rs b/packages/desktop/src/keep_awake.rs index 139585be..646f5d58 100644 --- a/packages/desktop/src/keep_awake.rs +++ b/packages/desktop/src/keep_awake.rs @@ -33,7 +33,9 @@ //! another, so no separate "remove" item and no helper left behind unasked. //! //! It lives in the app, so it ends with the app: Codeman's LaunchAgent keeps the server -//! running after the app quits, but nothing keeps the Mac awake then. +//! running after the app quits, but nothing keeps the Mac awake then. So while a mode is +//! on, closing the main window hides the app instead of quitting it +//! (`Mode::close_hides_app`): only Cmd+Q (or Quit from the Dock) ends it. //! //! The agents are asked natively, like the logo blink does (`agents`): WebKit stalls the //! event stream of a hidden page. A worker thread does the asking and the holding, and @@ -123,6 +125,14 @@ impl Mode { pub fn from_menu_id(id: &str) -> Option { Self::ALL.into_iter().find(|mode| mode.menu_id() == id) } + + /// Whether closing the main window hides the app instead of quitting it. + /// Keep-awake ends with the app, so while a mode is on, the red button (or + /// Cmd+W) must not end it: the app hides as Cmd+H does, a click on its Dock + /// icon brings it back, and Cmd+Q still quits. + pub fn close_hides_app(self) -> bool { + available() && self != Mode::Off + } } /// The `keepAwake` object in `desktop.json`. @@ -1221,6 +1231,16 @@ mod tests { assert!(Status::Recently.text().contains("5 minutes")); } + #[test] + fn closing_the_window_hides_the_app_only_while_a_mode_is_on() { + // Off quits as it always did; a mode that keeps the Mac awake must not + // end with the red button, since keep-awake ends with the app. + assert!(!Mode::Off.close_hides_app()); + for mode in [Mode::WhileWorking, Mode::Always] { + assert_eq!(mode.close_hides_app(), available(), "{mode:?}"); + } + } + #[test] fn a_menu_id_names_one_mode() { for mode in Mode::ALL { diff --git a/packages/desktop/src/main.rs b/packages/desktop/src/main.rs index d572c0a7..8cf8e647 100644 --- a/packages/desktop/src/main.rs +++ b/packages/desktop/src/main.rs @@ -725,6 +725,18 @@ fn build_webview( Ok(webview) } +/// Hides the whole app, as Cmd+H does. A click on the Dock icon (or Cmd+Tab) +/// shows it again with every window as it was, a full-screen one included, +/// which hiding the main window alone would not do. +#[cfg(target_os = "macos")] +fn hide_app(target: &EventLoopWindowTarget) { + use tao::platform::macos::EventLoopWindowTargetExtMacOS; + target.hide_application(); +} + +#[cfg(not(target_os = "macos"))] +fn hide_app(_target: &EventLoopWindowTarget) {} + impl App { fn handle( &mut self, @@ -741,7 +753,7 @@ impl App { self.menu.help_menu.set_as_help_menu_for_nsapp(); } } - Event::WindowEvent { window_id, event, .. } => self.on_window_event(window_id, event, control_flow), + Event::WindowEvent { window_id, event, .. } => self.on_window_event(window_id, event, target, control_flow), Event::UserEvent(event) => self.on_user_event(event, target), // A click on the Dock icon while no window is visible, which is // the main window minimized: tao only reports it, so bring the @@ -813,18 +825,33 @@ impl App { self.fullscreen_retry = Some((now + FULLSCREEN_RETRY_EVERY, attempts + 1)); } - fn on_window_event(&mut self, id: WindowId, event: WindowEvent, control_flow: &mut ControlFlow) { + fn on_window_event( + &mut self, + id: WindowId, + event: WindowEvent, + target: &EventLoopWindowTarget, + control_flow: &mut ControlFlow, + ) { match event { WindowEvent::CloseRequested => { - if id == self.main_id { + if id != self.main_id { + self.remove_window(id); + } else if self.config.keep_awake.mode.close_hides_app() { + // Keep This Mac Awake ends with the app, so closing the + // window while it is on hides Codeman instead, as Cmd+H + // does (full screen and pop-outs included): the Mac stays + // awake, a click on the Dock icon brings it back, and + // Cmd+Q still quits. + self.record_main_frame(); + self.save_config(); + hide_app(target); + } else { // Closing the main window quits; the server and its // sessions keep running, so nothing is lost. self.keep_awake.shutdown(); self.record_main_frame(); self.save_config(); *control_flow = ControlFlow::Exit; - } else { - self.remove_window(id); } } WindowEvent::Moved(_) | WindowEvent::Resized(_) if id == self.main_id => self.record_main_frame(),