Compare commits

...
Author SHA1 Message Date
Codeman maintainer eef02ab00a docs(desktop): the lid-closed blink was seen on a real MacBook
The README still said nobody had watched this app blink the logo under a
closed lid. The owner ran release candidate 6 of 1.50.1 on a MacBook Pro
and reported that it works. It also names what the LogoLight handover was
checked against.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 23:27:24 +01:00
Codeman maintainer 5cd8fa049a feat(desktop): blink the logo while a LogoLight that steps aside runs
LogoLight drives the lid logo with the same DisplayLogicalState fades,
so Codeman used to leave the logo to it whenever it ran: the owner had
LogoLight open and the release candidate never breathed.

LogoLight steps aside instead since its 1.3.0 ("LogoLight steps aside
while Codeman shows its agents on the logo, and takes the logo back
afterwards"). This is Codeman's side of that contract, which lives in the
marker the blinker already keeps for crash recovery:
- it now holds the blinker's process ID;
- it goes down right before the first write rather than when the blink
  starts, so a blink waiting for the display does not pause LogoLight for
  nothing; under a closed lid the first write is telling the logo the lid
  is open, so the marker and the wait come before that;
- with a LogoLight that says LogoLightStepsAsideForCodeman in its
  Info.plist, the blinker waits two seconds after writing it, since
  LogoLight looks every second. A blink that starts within a second of
  the last one's end (work, then a permission) skips the wait: LogoLight
  takes the logo back only after the marker has been gone for 1.5 s.

The key is read from the running LogoLight's bundle by process ID
(proc_pidpath, then the plist file, never NSBundle, which would keep a
stale copy across a LogoLight update). An older LogoLight is still left
the logo, and the warning when the switch is turned on now says to update
it. At launch, a logo a killed child left is put back as before, but with
any LogoLight running the lid state is left to it.

Tests: 229 pass on the Mac mini, and six deliberate breaks of the
handover each fail one. The Info.plist lookup was run against a stand-in
process carrying the Info.plist of the released LogoLight 1.3.1: it reads
"steps aside". Not seen with a real LogoLight on a real logo yet.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 23:10:28 +01:00
Codeman maintainer 979275af1f fix(desktop): blink under a closed lid the way LogoLight 1.3.1 measured it
LogoLight 1.3.1 lights the logo behind a closed lid on a real MacBook kept
awake with no display attached, and its research notes say why the earlier
tries here stayed dark and what must not be written:

- Kept awake like that, the built-in display stays online and
  CGDisplayIsAsleep says 0, so the lid path was never taken ("not tried
  yet"). The lid sensor decides now (in since the last commit).
- DisplayPowerStateEvent is gone. A client's DidSleep keeps the logo dark,
  lid open too, until a DidWake is written; the last commit wrote DidSleep
  when a blink ended, and at launch with a marker and the lid closed.
- After ClamshellUpdate 0, each display in CBDisplayList is told the lid
  is closed again (setProperty:withKey:andDisplay:), as each keyboard is:
  told it is open, True Tone tints an external display warm and dim.
- The logo is told once, when the daemon has handled the lid
  (CBClamshellState, bounded to two seconds of waiting), not at every
  cycle, which would toggle those display modules with every beat. It is
  told again when it is found dark at the start of a beat, or when a rise
  did not move it: three times in a row, then every half minute, with
  "stayed dark" in the menu line.
- Unplugging under the lid tells the logo the lid is closed again.
- "The Mac went to sleep" is only said while the lid is closed.

The keep-awake side needs nothing new: pmset disablesleep holds the Mac at
full wake, which is the state LogoLight measured; an assertion against
system sleep alone gives a dark wake, where the logo stays dark.

221 tests pass on the Mac mini, and 13 deliberate breaks of the lid rules
each fail one. Not seen from this app on a real lid yet.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-10 23:06:29 +01:00
Codeman maintainer ede99b309c fix(desktop): the lid sensor decides, and every lid close leaves a reason
On RC4 the owner closed the lid with LogoLight not running and the menu
line still said "not tried yet": the child never wrote anything. Three
ways that could happen left no trace: the display list still showing the
built-in display awake under the closed lid (the blink then never took
the lid path), no blink running because no agent was at work, and the
Mac going to sleep at the lid close.

The lid sensor now wins: a closed lid counts as closed whatever the
display list says. While no blink runs and the lid is closed, the line
says why (nothing to show, no agent at work; the logo switched off;
LogoLight has the logo; a refused change). The child notices a sleep as
wall-clock time that passed while Instant stood still, and the line then
says "the Mac went to sleep". With no child at all (switch off, no
server open) the app says nothing watches the lid.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 22:36:50 +01:00
Codeman maintainer c307d63d3b fix(desktop): under a closed lid, just keep blinking, and say what happened
RC3's lid path went dark on the real MacBook without a word: it waited for
CBClamshellState, gave up after two tries, and wrote ClamshellUpdate 1
back on every failed proof, and none of its silent paths left a trace
without a Terminal launch.

Now, while the lid sensor (AppleClamshellState) says closed, the built-in
display is off and the Mac is on its power adapter, the logo is told the
lid is open (and each keyboard that it is closed) at the start of every
cycle, with no tries and no give-up: monotone, so nothing is toggled in
between. With no other display on the display sleeps, which also keeps
the logo dark, so if the first rise does not move it the logo is told the
display woke (DisplayPowerStateEvent 2); DidSleep is written back at the
end only if that was sent. The end of a blink, and the marker recovery,
still put the logo back to dark as a closed lid has it.

What the closed lid did last is a line in the View menu, under Apple Logo
Blink Styles: "Lid Closed: blinking", "blinking (display told awake)",
"stayed dark, macOS keeps the logo off", or why it was not tried (on
battery, no keyboard to keep dark, no lid sensor), with the time. The
child writes it to logo-lid next to desktop.json, with the readings on a
second line; the app reads it every two seconds.

The battery and keyboard gates stay: the lid helper only holds on the
adapter, and a keyboard told the lid is open would light under it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 22:25:22 +01:00
Codeman maintainer 4c53d424a7 fix(desktop): a firmer proof under the closed lid, and a stderr note that cannot abort
The proof that the logo moves under the closed lid now needs a fade level
above 0.01, not just above 0, so no drift of a fade held at 0 passes for a
rise; the slowest rise (the breath's) is past it in about 80 ms. The note
on stderr is a writeln! whose error is dropped: eprintln! panics on a
failed write, and the blink child has panic = "abort".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 21:41:49 +01:00
Codeman maintainer 39f2e847d8 feat(desktop): the lid logo keeps blinking with the lid closed
With the lid closed, corebrightnessd tells every module so and keeps the
logo dark, so the blink stopped as soon as the lid came down, which is
exactly when the logo on the lid is the one thing in view.

While the Mac stays awake with the lid closed on its power adapter (Keep
This Mac Awake's lid helper, or an external display), the blink child now
does what LogoLight's Lit with Lid Closed measured on the real MacBook:
once the daemon has handled the lid (CBClamshellState), it tells the logo
the lid is open (ClamshellUpdate 0) and each keyboard alone that it is
closed, so the keyboard stays dark and is never relit after a Blank.

The first rise after that has to move the logo within 1.5 s. If it does
not (with no display on, the daemon may still hold the logo dark), the
lid is told closed again, tried once more, then left dark until the lid
opens, a display comes on or the agents' state changes. A logo the daemon
takes back (it resends the lid state when a display or keyboard comes up)
is told again. Ending a blink under the closed lid hands the logo back to
normal and then writes ClamshellUpdate 1, since normal there is dark; the
marker recovery does the same for a killed child. Never on battery (a
dark wake must not light the logo in a bag), never without the keyboard
IDs, and nothing is written when the lid opens. Every step is a line on
stderr so a real-Mac run shows which one stopped it.

Not seen on a real MacBook yet; the README says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 21:36:32 +01:00
4 changed files with 1518 additions and 53 deletions

No files matched your search

+62 -2
View File
@@ -40,7 +40,7 @@ cannot do.
| **Links** | On Codeman's own pages (the dashboard and session windows), a link to another site opens in your default browser, also on `⌘`-click or a middle click. Pages can send at most 3 links to the browser at once, then 1 per second. Inside a rendered HTML preview or a web tab, a plain link to another site does nothing, while one that asks for a new window (`target="_blank"`, `window.open`) opens in your default browser within the same budget (see Known limits). |
| **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. **View > Blink Apple Logo** is the one switch, off until you turn it on. The more the logo moves, the more it needs you: it breathes (a slow fade out and back in) while agents work, blinks every two seconds for a yellow tab and flashes three times, then rests, for a red one. Each state can take another style under **View > Apple Logo Blink Styles**: Off, Breathe, Calm, Slow, Wink, Heartbeat, Fast, or Alarm. Picking a style turns the switch on, and turning the switch on with every state off brings back the recommended three (Breathe, Calm, Alarm). When several states hold, the most urgent one with a style shows: red, then yellow, then work. Styles chosen in 1.50.0 are kept, with the switch on if any of them was. 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. The logo moves on its own hardware fades, the ones [LogoLight](https://github.com/Ark0N/LogoLight) uses, so a blink writes no setting, and a logo you switched off yourself stays off. While LogoLight runs, Codeman leaves the logo to it. While the app is closed nothing blinks, and while the display sleeps or the lid is closed the blink pauses and writes nothing. |
| **Apple logo** | On a MacBook Pro with a PROLED XDR display, the lit Apple logo on the lid says what the agents are doing. **View > Blink Apple Logo** is the one switch, off until you turn it on. The more the logo moves, the more it needs you: it breathes (a slow fade out and back in) while agents work, blinks every two seconds for a yellow tab and flashes three times, then rests, for a red one. Each state can take another style under **View > Apple Logo Blink Styles**: Off, Breathe, Calm, Slow, Wink, Heartbeat, Fast, or Alarm. Picking a style turns the switch on, and turning the switch on with every state off brings back the recommended three (Breathe, Calm, Alarm). When several states hold, the most urgent one with a style shows: red, then yellow, then work. Styles chosen in 1.50.0 are kept, with the switch on if any of them was. 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. The logo moves on its own hardware fades, the ones [LogoLight](https://github.com/Ark0N/LogoLight) uses, so a blink writes no setting, and a logo you switched off yourself stays off. LogoLight (1.3 or later) steps aside while Codeman shows its agents on the logo, and takes the logo back afterwards, with whatever it showed before; while an older LogoLight runs, Codeman leaves the logo to it. While the app is closed nothing blinks. With the lid closed it just goes on while the Mac stays awake on its power adapter (**Keep This Mac Awake > With the Lid Closed…**, or an external display): macOS keeps the logo dark under a closed lid, so the app tells the logo the lid is open and the keyboard that it is closed, which stays dark, as LogoLight's *Lit with Lid Closed* does. The light sensor is covered then, so the logo glows as it would in a dark room. The line under **Apple Logo Blink Styles** says what the closed lid did last (blinking, or why not). On battery, and while the display only sleeps with the lid open, the blink pauses and writes nothing. |
| **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)
@@ -249,6 +249,56 @@ changing on screen. So the short dark beats (Alarm's, Heartbeat's, Wink's, Fast'
while their fall is still under way and never quite reach dark, and a long one (Calm's,
Slow's, the breath's rest) times its light from when the rise starts.
With the lid closed the daemon tells every module so (`ClamshellUpdate` 1), and the logo's
target is then 0 whatever `DisplayLogicalState` says. A client's `ClamshellUpdate` reaches the
same modules, and LogoLight measured on the real MacBook what follows from that, first with an
external display and, for its 1.3.1, on a Mac kept awake with none:
- Kept awake with the lid closed and no other display, the built-in display stays online and
`CGDisplayIsAsleep` says 0. So the lid sensor (`AppleClamshellState` on `IOPMrootDomain`)
decides that the logo is out of sight, not the display list.
- Once the daemon has handled the lid (`CBClamshellState` reads 1, a moment after the sensor),
`ClamshellUpdate` 0 relights the logo and fades work as with the lid open. The child then
writes `ClamshellUpdate` 1 to each keyboard alone, which stays dark, and to each display in
`CBDisplayList` alone (`setProperty:withKey:andDisplay:`): told the lid is open, True Tone
adapts an external display to the covered light sensor and tints it warm and dim.
- The daemon sends the real lid state again when a display or a keyboard comes up. Every beat
ends lit, so a logo found dark at the start of the next one is told again.
- `DisplayPowerStateEvent` is never written. A client's DidSleep keeps the logo dark, with the
lid open too, until someone writes DidWake.
The child does this only on the power adapter (on battery a closed MacBook sleeps, and a dark
wake must not light the logo in a bag) and only with every keyboard reachable. The first rise
has to move the logo within 1.5 s. If it does not, the logo is told again, three times in a
row and then every half minute, and the menu line says it stayed dark. Ending a blink with the
lid closed hands the logo back to normal (the automatic duration, then Unblank, which a held
level needs) and then writes `ClamshellUpdate` 1, since normal for a closed lid is dark; the
marker covers a child killed in between. When the lid opens, the daemon tells every module
itself and nothing is written. What the closed lid did last (blinking, stayed dark, nothing to
show with no agent at work, not tried and why, or that the Mac went to sleep under it, which
the child sees as wall-clock time that passed while its monotonic clock stood still) goes to a
file next to `desktop.json` (`logo-lid`, with the readings behind it on its second line), and
the app shows its first line as **Lid Closed: …** under View > Apple Logo Blink Styles, or
says nothing watches the lid while no child runs.
Whether the Mac stays awake under the lid is not the logo's business: Keep This Mac Awake's
lid helper does that (`pmset disablesleep`, which holds the Mac at full wake), or LogoLight's
own Lit with Lid Closed, or an external display. An assertion against system sleep alone would
not do: LogoLight saw the Mac enter a dark wake with it, where the logo stays dark.
LogoLight drives the logo the same way, so the two would fight over every fade. The marker the
child keeps next to the app's settings while it moves the logo (`logo-fade`, which also lets the
next launch put back a logo a killed child left dark) holds the child's process ID, goes down
right before the first write and comes off once the logo is back to normal. A LogoLight that says
`LogoLightStepsAsideForCodeman` in its Info.plist (read from the running app's bundle, by its
process ID) looks at that file every second and pauses while it names a running
`codeman-desktop`, so the child waits two seconds after writing it before its first fade.
LogoLight takes the logo back only once the file has been gone for a second and a half, so a
blink that starts within a second of the last one's end (work, then a permission) does not
wait again. An older LogoLight is left the logo, as before. LogoLight ships its side since
1.3.0. With the lid closed, the two tell the logo the same thing (the lid is open), so a
LogoLight that lights the logo under the lid and a blink do not undo each other; when a blink
ends there the logo goes dark, and LogoLight lights it again as it takes it back.
The app must not end with the logo dark, and the release profile has `panic = "abort"`, so no
cleanup code of the app can be relied on. So all of it runs as this same binary with
`--logo-blink`, for as long as the switch is on and a server is open, tied to the app by
@@ -329,8 +379,18 @@ crosses, so the login is never on a command line.
outright (SIGKILL) leaves the logo where it was, perhaps dark, until the
display next sleeps and wakes or the app next starts. Another app that drives
the logo the same way and is not LogoLight is not recognised, and the two
fight. `api/sessions` does not say which turns are Codeman's own, so with
fight. The handover with a LogoLight that steps aside was checked against a
stand-in process carrying LogoLight 1.3.1's Info.plist and against LogoLight's
own tests, and ran in that release candidate next to LogoLight 1.3.1; its
timing on a real logo was not measured. `api/sessions` does not say which turns are Codeman's own, so with
prompt-cache keep-warm on, a keep-alive turn blinks too.
- **Blinking with the lid closed was seen on one MacBook Pro so far** (macOS
27.2, on power, a release candidate of 1.50.1 on 2026-11-10; the method is
LogoLight's, which its author measured for LogoLight 1.3.1). Nothing was
measured in that run, it was watched. Not checked, by LogoLight or here: the
built-in display's brightness when the lid opens again, and an Apple display
with its own light sensor. If the logo does not move under a closed lid, the
menu line says "stayed dark".
- **Self-signed HTTPS is refused.** A server started with `codeman web --https`
uses a self-signed certificate, and WKWebView rejects it (wry has no trust
override). Use `http://127.0.0.1:3000` for a server on this Mac. For another
File diff suppressed because it is too large. Load diff
+25 -2
View File
@@ -110,8 +110,7 @@ enter it again (a single-user Codeman accepts the right password at once), or wa
const KEYCHAIN_UNREADABLE: &str = "The saved password could not be read from the Keychain. Enter it again.";
const PAGE_KEEPS_STOPPING: &str = "Codeman's page stopped twice in a row, so it was closed. Connect again.";
const LOGO_SWITCHED_OFF: &str = "The Apple logo is switched off on this Mac, so it cannot blink.";
const LOGO_DRIVEN_ELSEWHERE: &str =
"LogoLight is driving the Apple logo, so Codeman leaves it alone. Quit LogoLight to see your agents on it.";
const LOGO_DRIVEN_ELSEWHERE: &str = "An older LogoLight is driving the Apple logo, so Codeman leaves it alone. Update LogoLight to 1.3 or later (or quit it) to see your agents on it.";
/// What a download's toast calls a file whose name is not known.
const UNNAMED_FILE: &str = "the file";
const START_REFUSED: &str = "Codeman can only be started here for an http://127.0.0.1 or http://localhost address.";
@@ -184,6 +183,8 @@ enum UserEvent {
/// install opened in Terminal, the install command copied): the message
/// goes to the page if it is still the one choosing a server.
ToConnectPage(serde_json::Value),
/// What the closed lid did with the logo last (logo::watch_lid_status).
LogoLidStatus(String),
}
struct ProbeDone {
@@ -401,6 +402,13 @@ fn main() {
logo_watch: logo::Watcher::default(),
};
if app.menu.logo_lid.is_some() {
let lid_proxy = app.proxy.clone();
logo::watch_lid_status(move |line| {
let _ = lid_proxy.send_event(UserEvent::LogoLidStatus(line));
});
}
event_loop.run(move |event, target, control_flow| app.handle(event, target, control_flow));
}
@@ -823,6 +831,7 @@ impl App {
self.to_connect_page(msg);
}
}
UserEvent::LogoLidStatus(line) => self.show_lid_status(line),
UserEvent::ServerStartDone {
generation,
url,
@@ -1040,6 +1049,20 @@ impl App {
let server = phase.loaded().filter(|_| styles.active() && logo::available());
self.logo_watch
.sync(server.map(|url| (url.as_str(), self.auth.as_deref(), styles)));
drop(phase);
self.show_lid_status(logo::lid_status_line());
}
/// The line under the logo's styles: what the closed lid did last, or that
/// nothing watches it now.
fn show_lid_status(&self, line: String) {
if let Some(item) = &self.menu.logo_lid {
if self.logo_watch.watching() {
item.set_text(line);
} else {
item.set_text(logo::LID_NOT_WATCHED);
}
}
}
fn choose_logo_blink(&mut self, signal: Signal, style: BlinkStyle) {
+8
View File
@@ -32,6 +32,9 @@ pub struct AppMenu {
pub logo_switch: Option<CheckMenuItem>,
/// One item per state and style, empty without the switch.
pub logo_styles: Vec<(Signal, BlinkStyle, CheckMenuItem)>,
/// What the closed lid did with the logo last, a line that cannot be
/// clicked. None without the switch.
pub logo_lid: Option<MenuItem>,
}
pub fn build_menu(config: &Config) -> muda::Result<AppMenu> {
@@ -117,6 +120,7 @@ pub fn build_menu(config: &Config) -> muda::Result<AppMenu> {
// style for each state behind it.
let mut logo_styles = Vec::new();
let mut logo_switch = None;
let mut logo_lid = None;
if logo::available() {
let switch = CheckMenuItem::with_id(
menu_id::LOGO_BLINK,
@@ -140,9 +144,12 @@ pub fn build_menu(config: &Config) -> muda::Result<AppMenu> {
submenu.append(&group)?;
}
// Right under "Always Open in Full Screen", before the separator.
let lid = MenuItem::new(logo::lid_status_line(), false, None);
view.insert(&switch, 4)?;
view.insert(&submenu, 5)?;
view.insert(&lid, 6)?;
logo_switch = Some(switch);
logo_lid = Some(lid);
}
let window_menu = Submenu::with_items(
"Window",
@@ -172,5 +179,6 @@ pub fn build_menu(config: &Config) -> muda::Result<AppMenu> {
start_fullscreen,
logo_switch,
logo_styles,
logo_lid,
})
}