Commit Graph
21 Commits
Author SHA1 Message Date
Codeman maintainer 97f5208e0b Merge #773: an agent can be added from the app later, under its real menu name 2026-11-08 10:41:02 +01:00
Codeman maintainer ac59cfa9ca docs(desktop): an agent can be added from the app later, under its real menu name
The desktop README's Known limits still said an agent is added by hand after the install, and the wiki gave the menu item a shorter name than the one the app shows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 07:32:23 +01:00
Codeman maintainer 31ce0c88cd Merge ci/macos-app-release: build, sign, notarize and attach Codeman.dmg on every release
release.yml and docs/releasing.md take the branch's text, which is master's text plus the macOS app additions (its first commit copied both files from master).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 04:07:05 +01:00
Codeman maintainer 98c4a3d386 Merge #778 fix(cli): a running server notices a CLI the installer just added
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 04:05:41 +01:00
Codeman maintainer 4b349b57f6 Merge #773 feat(desktop): install Codeman from the app, with no Terminal and no password
Conflicts resolved as on test/desktop-macbook: install.sh usage() lists CODEMAN_NO_ADMIN=1 or 0 and #773's CODEMAN_AI_CLI and CODEMAN_PROGRESS=1; probe.rs keeps #781's agent() with TIMEOUT, MAX_BODY and probe_within all pub; the desktop README keeps both known-limits entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 04:05:38 +01:00
Codeman maintainer 94912241bc feat(desktop): sign-macos.sh signs, notarizes and staples the app and its DMG
The release DMG is signed with the owner's Developer ID and notarized, so a
downloaded copy opens after macOS's usual confirmation instead of stopping at
Gatekeeper. The work is a script of its own, so CI and a Mac run it the same way:

  SIGN_IDENTITY=... NOTARY_PROFILE=<profile> \
    packages/desktop/scripts/sign-macos.sh <Codeman.app> <out-dir>

It re-signs the app with the hardened runtime, a secure timestamp and
macos/entitlements.plist, checks the signature is a Developer ID one with the
runtime flag, has notarytool notarize it and staples the ticket, builds the DMG
around the stapled app, signs it, notarizes and staples it too, and has spctl
assess both. Codeman.dmg and Codeman.dmg.sha256 reach <out-dir> only after every
check passed; a failed run removes an older pair there rather than leave one to
upload by mistake. A submission Apple does not accept stops the script with its
id and Apple's log. The notary login is either a notarytool keychain profile
(NOTARY_PROFILE, NOTARY_KEYCHAIN) or APPLE_ID, APPLE_APP_PASSWORD and
APPLE_TEAM_ID; `sign-macos.sh --check` tests that without doing anything.

The DMG layout (the app next to an /Applications link, UDZO, volume "Codeman")
moves to make-dmg.sh, which both scripts call, and gains a retry: hosted macOS
runners now and then fail hdiutil create with "Resource busy". bundle-macos.sh
keeps its SIGN_IDENTITY path as it was; its new --notarize checks the
environment before building, then hands the app to sign-macos.sh.
packages/desktop/README.md says the release DMG is notarized and how to do
the same by hand, and drops the CI build and notarization from its ideas list.

All three scripts are bash 3.2 clean, and shellcheck reports nothing. With the
macOS tools stubbed, sign-macos.sh was run under bash 3.2 through a full
signing, a notarization rejected for the app and for the DMG, two hdiutil
failures then a pass, three failures, a relative out-dir and every missing
setting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 03:57:29 +01:00
Codeman maintainer 7edf1a1c27 feat(desktop): a blink style for each state of the agents
The logo said one thing: agents are working. It now has a style of its own
for each state the tabs show: at work, finished and waiting for you (yellow),
and stopped on a permission or a question (red), set under View > Blink Apple
Logo. The most urgent state with a style wins, red then yellow then work, and
a state set to Off falls through to the next instead of silencing the logo.

Red and yellow come from `GET /api/approvals`, the list the page rebuilds
those tabs from after a reload, read natively for the reason the session list
is: WebKit stalls a hidden page's stream. An idle item stops counting once it
is acknowledged, so the logo stops when the tab's colour goes. The request is
skipped while neither alert has a style, a 404 from an older server reads as
no alerts, and a request that fails keeps the last answer. A dialog on screen
costs the server one tmux capture per poll.

Alarm is new: three sharp flashes and a short rest. It stays under the limit
of three flashes in any second, so its urgency comes from its shape.

A style saved by the earlier version stays as the one for work, and yellow
and red start as Wink and Alarm, since whoever chose one asked for a logo that
talks. `SETTLE` goes from 2 s to 3 s, the time the server takes to judge a new
dialog, so a prompt answered before its hook landed never flashes red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 16:26:16 +01:00
Codeman maintainer cd4cee15a7 feat(desktop): blink the lid Apple logo while agents work
On a MacBook Pro with a PROLED XDR display, the lit Apple logo on the lid
can blink while agents work and stop when they finish, so a run can be
read from across the room. View > Blink Apple Logo While Agents Work
picks a style (Calm, Slow, Wink, Heartbeat, Fast). It is off by default,
and the menu exists only on a Mac that has the logo.

Apple documents one control for the logo, `sudo defaults write
com.apple.CoreBrightness LEBacklightMode -int 0`. The same setting is a
property of BrightnessSystemClient in the private CoreBrightness
framework, which a process of the logged-in user can set with no
administrator, so nothing here needs a helper or a password.

The setting is system-wide and survives a reboot, and the release profile
has panic = "abort", so the work runs in a child process (this binary
with --logo-blink) tied to the app by a pipe. When the pipe closes,
however the app ended, the child restores the logo and exits. A marker
file covers power loss and a SIGKILL of the child itself.

The child asks api/sessions itself: WebKit stalls the event stream of a
hidden page, which lost whole turns and ran a blink 39 s past the end.
It sends the session cookie of its first answer with every poll, because
a good password without a cookie creates a login session each time and
Codeman evicts the oldest of 100.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 05:06:39 +01:00
Codeman maintainer 59af305e84 feat(desktop): find the Codeman servers running on this Mac and offer to connect
When a Codeman server of this user ran at an address the app could not open
as its default one, the This Mac tab only named host and port and sent the
user to Another machine to type the address again. A second instance beside
the one that answers (a beta, a `codeman web --port 4000` in a terminal) was
not shown at all.

The look at this Mac now works out an address for every listening socket of
this user's Codeman servers (`found_addresses`, pure): 127.0.0.1 for a bind
to loopback or to every interface, `[::1]` when that is the only loopback
socket, the address itself for a bind to one; `https` and the base path come
from `--https` and `--base-url` on the command line. A server at the default
address with no `--base-url` of its own keeps the default URL, since
install.sh's launchd job has the base path in its environment, where `ps`
does not show it. The default address comes first, then the others by port.

Each address other than the default one is asked once, all at the same time,
with no login. The default probe's answer is reused for its own row. When
nothing answers at the default address, the first found server that answers
is still what Open is for, so each gets the probe's usual 5 s; next to a
Codeman that answers they are only listed and get 2 s. `LocalStatus.found`
replaces `elsewhere`.

The tab lists them: "Codeman is already running on this Mac." with a row
each when none answers at the default address, and "Also running on this
Mac" under Open otherwise. A row has the address, the version and Connect,
or the reason the app cannot open it. Connect fills the address in and runs
the page's one connect(), so the app checks it, uses its saved login and
remembers it like an address typed under Another machine. Nothing connects
by itself, and a refused connect from a row never arms the reopen that Open
has for a starting server.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-07 04:27:16 +01:00
Codeman maintainer d6226385c0 fix(cli): a running server notices a CLI the installer just added
A running Codeman remembers a missing CLI for up to 5 minutes, so a status
poll never spawns a login shell, and its Run button reads that same memory.
So "Run fails, add the agent, Run again" kept saying the CLI was not
installed, right after the app had said it was ready.

install.sh now touches `cli-installed` in the data dir once a CLI has landed,
and resolve() leaves the backoff when that file's mtime changes. It is one
stat, inside the backoff branch, and buys one run of the chain, so a resolved
CLI never pays for it and a CLI that is still missing goes back to waiting.
Re-walking PATH during the backoff would have found hand installs too, but it
would put a synchronous stat of every PATH entry on the request path, where
one dead network mount freezes the server (#516), and execute candidates that
were already refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 14:25:34 +01:00
Codeman maintainer 84d5c8de52 Merge branch 'feat/graphical-installer' into integrate/desktop-763-with-gui-installer 2026-11-06 13:46:36 +01:00
Codeman maintainer 1157aa9ecd fix(install): say why an added agent failed on a Mac with no other agent
After "Skip for now" there is no other agent, so a failed add hit
install_offered_cli's gate first, and the app showed its first-install text
("re-run the installer") instead of naming the agent and its command. That
is the main path of adding an agent.

run_offered_cli now runs a trusted command and looks again; the gate stays in
install_offered_cli for the install, and `agent` judges its own run. The one
line that executes a trusted command is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 13:46:31 +01:00
Codeman maintainer da2f12fdbd merge the agent chooser (feat/graphical-installer) into the integration branch
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 13:25:27 +01:00
Codeman maintainer dd07126dd8 feat(desktop): add a coding agent once Codeman is installed
The agent chooser only appeared during the install, and only on a Mac with
no agent. After "Skip for now", or with Codeman already installed, there was
no screen for it.

The same chooser now works after the install: "Add a coding agent" on the
This Mac tab, "Add a Coding Agent to This Mac..." in the app menu (it also
works from a Codeman page), and after a Skip the app says Codeman has no
agent yet before it opens Codeman.

`install.sh agent <id>` installs one agent and nothing else. It checks that
THAT agent landed: install_offered_cli is fatal only when no CLI is left, so
with another one present a failed install would have reported success.

The app runs it as a run of its own (`gui_install::Run::Agent`), in its own
temporary folder, so `ps` never reads it as "Codeman is being installed". It
may run next to a live server, which an install must refuse, and stops only
for another run. `--no-admin` goes to both `preflight` and `agent`: it decides
what counts as installable, so the two must agree.

CI: the bash 3.2 preflight check never tested anything. Its child run
inherited CODEMAN_INSTALL_SH_LIB and stopped at the sourcing guard, and a
false [[ ]] does not trip set -e on bash 3.2. Both are fixed for that check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 13:23:56 +01:00
Codeman maintainer c04955ca68 merge feat/desktop-app (the PR #763 review fixes) into the graphical installer branch
Both branches changed the connect page and three Rust files. How the
conflicts were settled:

- Layout (connect.html): both sides fixed the page being cut off above a
  small window. One layout is kept, the one that also holds the header still:
  a fixed top margin on `main`. The wizard's own top margin is dropped, since
  the page is anchored from the top in every state now and a second margin
  would move the header when the wizard opens. `#elsewhere` is a child of the
  card since the status line moved above it, so the wizard hides it too.
- State (connect.html): `gui` from the installer next to `askedFrom`,
  `reopen` and the starting-server counters from the review.
- setup.rs: `running_install` for the port, `install_in_app` for the flag.
- local_server.rs: `login_environment` and `in_home` both stay.
- README: the installer's This Mac row with the sentence about a starting
  server; both new paragraphs.

test/desktop-connect-page.browser.test.ts reaches the Terminal install the
way the page now offers it (Install, a failed preparation, "Install in
Terminal instead"), since Install opens the graphical install here.

Checked on the merged tree: 138 Rust unit tests, clippy and fmt on the Mac
mini; test/desktop-connect-install.test.ts (jsdom) and
test/desktop-connect-page.browser.test.ts (Chromium) both pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-06 04:02:20 +01:00
Codeman maintainer 92797c2619 test(desktop): unit tests for main.rs's decisions and a browser test for the connect page
main.rs was the only source file without tests, and it held three decisions
with real failure modes:

- Which login a connect goes out with. The choice was a closure body inside
  the probe worker; it is now choose_login, tested for every row: typed wins,
  then the Keychain, then the local Codeman's own login, which is used only
  when no saved password was expected and only for the same username.
- What the Keychain keeps afterwards (update_keychain): saved or deleted by
  Remember, never a copy of the local server's login, never a password the
  server wants changed, and the old user's item deleted before the new save.
  The in-memory Keychain store moves out of credentials' test module so other
  test modules can use it.
- When a commit in the main window means the server page is loaded.

The connect page had only substring checks in the repository; the checks that
drove it lived in a scratch script. They are now
test/desktop-connect-page.browser.test.ts: Chromium, the page's real CSP, a
stubbed native side, five ordered walks through every state of both tabs
(first run, logins, failures, redirects, Start, the install wait, the SSH
install, layout, double clicks, keys, a starting server, a WebKit too old).
Listed in BROWSER_TEST_GLOBS, so the nightly browser suite runs it and
`npm test` does not.

Also pins the install one-liner the page shows to the one Copy copies.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-06 03:54:49 +01:00
Codeman maintainer a42ce9c79f fix(desktop): a starting server, windows that open where expected, and the review's smaller findings
Found by the three-part review of this branch. Each was checked against the
source first; every decision that can run without a window has a unit test.

This Mac:
- A Codeman that runs but listens nowhere yet (the first seconds after login,
  a restart) was reported as "already running ... it listens at process N,
  which this app cannot open", and Start was refused for it. It is now its own
  state: the tab says "starting", looks again every second, and opens Codeman
  once it is up when a connect to this Mac had just been refused. The state
  table is a pure function with a test (setup::local_state).
- A server the app starts runs in the home folder, like one started by launchd
  or in a terminal. It ran in the app's own directory (`/` after a Finder
  launch), which Codeman then used as the default for sessions.
- Server detection accepts any entry word that `web` follows, so a dev server
  under a folder named codeman is seen and never gets a second server beside
  it.
- A login shell is polled from 5 ms up instead of every 100 ms, and the port
  after a launchd start every 100 ms instead of 500: both were pure waiting.
- The last line of stdout is no longer glued to the first line of stderr.

Addresses and policy:
- A tailnet name typed with a port gets http://, not https://. With a port it
  is Codeman's own listener; https there ended in a TLS error with no hint.
- within_base refuses `..%2F` and the other encoded ways out of the base path,
  so a sibling app behind a decoding proxy never sees the login.
- A status answer whose body stalls is a timeout, not "something else answers
  here".
- The folder pop-out pattern matches the two routes the server has.
- A malformed `window` in desktop.json no longer costs the server list.

Windows:
- Pop-outs step off the window in use. tao centres a window with no position,
  so every pop-out opened exactly on top of the one before.
- A window opened by a script no longer brings the app to the front: wry
  activated the application for it, whatever the window itself was told.
- The first-run window sits between the menu bar and the Dock. Centred in the
  whole display, its bottom was under the Dock on a 900 pt high one.
- The window frame is saved once after a move or resize ends, not once a
  second during the drag.

Also makes the connect page independent of TextEncoder and scrollIntoView, so
it runs in jsdom as well as in a web view.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-06 03:40:53 +01:00
Codeman maintainer 116db3ba66 fix(desktop): a connect page that holds still, answers a press and never strands the user
Layout:
- The header no longer moves. The page was centred with align-items, so every
  change of the card's height moved everything (up to 150px between states),
  and a page taller than its window lost its top above the scroll origin,
  where no scrolling reaches it. The top margin is now fixed, the card grows
  downward and has a minimum height, and the page scrolls from its top. A
  short window gets a compact header.
- The status line sits right under the action it reports on, above the
  "not installed there yet?" section, takes no room while empty, and is
  scrolled into view in a small window. It was below the fold there.
- Looking at this Mac again keeps what was found on screen (the pane used to
  collapse to "Looking..." and grow back).

Feedback and keys:
- Buttons answer hover and press, every control shows keyboard focus, the
  primary button and the footer meet WCAG AA contrast, "Opening Codeman" keeps
  its spinner while the server page loads.
- Return opens or starts the Codeman on this Mac, Esc cancels a connect.
  Install always takes a click.
- The This Mac result is a live region, Forget buttons are named after their
  server, a long unbroken token wraps inside the card, the status text can be
  selected.

Correctness:
- The second click of a double click acts only on the control that took the
  first. It could connect to the server that slid under the pointer after a
  Forget, or flip "Connect automatically".
- An address, username or password too long for the app is refused with a
  reason. The app drops such a message without an answer, which left the page
  on "Connecting..." (the limits are pinned to policy.rs by a test).
- A connect answer goes to the tab that asked, a failed SSH install to its
  tab, and the login block no longer claims a saved password for another
  server.
- An automatic connect to the Codeman on this Mac no longer looks at this Mac
  first; a failed connect, Cancel or an error asks for that look.
- A login over plain http:// to another machine says that it is unencrypted.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-06 03:26:57 +01:00
Codeman maintainer 01d3379b74 fix(desktop): recover a crashed page, name every dialog's frame, and four smaller review fixes
The findings of the 2026-11-05 review of this branch, each with a unit test
where the code can be tested without a window:

- A window whose web content process ended (a WebKit crash, or macOS taking
  the process back under memory pressure) stayed blank: wry answers WebKit's
  notice itself, which turns WebKit's own reload off. The window now loads its
  page again. A second end within 30 seconds sends the main window back to the
  connect page and closes a pop-out, so it cannot loop (policy::recovery).
- A JavaScript dialog from a frame nothing names (about:srcdoc, a data: or
  blob: URL under an opaque origin) showed the page's text as its bold first
  line. The first line is now always the app's own, the "embedded" flag is set
  when the origin names no host (it was the wrong way round), and every
  sub-frame counts as embedded, a trusted web tab on Codeman's origin included.
- The connect page lost the text of an error that needs the login fields: the
  tab switch cleared the status it had just set.
- Start for http://localhost:PORT missed the LaunchAgent (the compare was
  exact against 127.0.0.1) and ran a second, unsupervised server on its port.
- The path picker's preview Open link (api/filesystem/preview) did nothing: it
  was the one page Codeman opens in a window that the allowlist lacked.
- The port of a running install was lost whenever the Terminal script that
  wraps the installer came first in ps.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-11-06 03:20:01 +01:00
Codeman maintainer b51632e54b feat(desktop): install Codeman from the app, with no Terminal and no password
The This Mac tab runs install.sh --no-admin itself. The two questions only a
person can answer move into the app: which coding agent to install, shown only
when this Mac has none, and who may reach Codeman. The installer's output goes
to a file, so a running install survives the app quitting and is picked up
again when it reopens.

install.sh gains --ai-cli, --progress and a preflight subcommand, which is what
the app reads its choices from, so it keeps no copy of the CLI catalogue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-06 02:51:00 +01:00
Codeman maintainer 8ba5134174 feat(desktop): native macOS app that runs, installs or connects to Codeman
Adds packages/desktop, a small native app (Rust: tao, wry/WKWebView, muda,
objc2) that opens Codeman in its own window. The universal app is about 5 MB
(3.2 MB as a DMG) and runs on macOS 11.3 or later.

- Connect screen with two tabs. This Mac finds the local Codeman and offers
  Open, Start or Install. Start uses launchd for a LaunchAgent install and
  codeman web -d otherwise. It never starts a second server while one runs,
  because two servers on one data dir share live sessions. Another machine
  connects by address (recent servers, Keychain logins) and can install
  Codeman there over SSH.
- Installs run Codeman's install.sh in Terminal from a self-deleting
  .command script. The installer needs a terminal for sudo, Homebrew and
  ssh. A local install uses --local --service and the first free port from
  3000, and the app opens Codeman once it answers.
- The local Codeman's login is read from its LaunchAgent or
  ~/.codeman/.env. It is sent only to 127.0.0.1 on that port, only while
  the user's own Codeman process holds it, and is never saved.
- Native JS dialogs (alert, confirm, prompt) and HTTP Basic challenge
  answers, both scoped to Codeman's own pages, so the page keeps working
  after a server restart. App cookies are cleared before every connect.
- Pop-outs through window.CodemanHost, with an allowlist (webview/ never
  opens a window), external links in the default browser, downloads to
  ~/Downloads, real full screen and window state that is remembered.
- The connect page refuses a WebKit older than Safari 16.4, which Codeman's
  terminal needs.

Codeman side: viewSubagentTranscript shows a toast when the pop-up is
refused instead of failing, with zh-CN strings and a test that pins them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 00:36:49 +01:00