Repository navigation
feat(desktop): install Codeman from the app, with no Terminal and no password - #773
Merged
Merged
Conversation
…deman 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>
…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>
…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>
…ver 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>
…d 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>
…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>
…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>
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>
…on branch Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
This was referenced Nov 6, 2026
… 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>
This was referenced Nov 7, 2026
…al 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>
Ark0N
pushed a commit
that referenced
this pull request
Nov 8, 2026
…nal 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this does
Installing Codeman from the macOS app used to mean opening Terminal, typing a Mac password and answering the installer's questions there. With this PR the app does it in its own window: no Terminal, no password. If the Mac has no coding agent, the app asks which one you want and installs it along with Codeman.
The steps on a Mac without Codeman:
The app can be closed during the install. It keeps going, and the app picks it up when it opens again.
The same chooser works after the install. Add a coding agent is on the This Mac tab whenever Codeman is on this Mac, running or not, and Add a Coding Agent to This Mac… is in the app menu, so it can be reached from a Codeman page too. It lists what can be added and what is already here, installs one agent with the same checklist and Cancel, and ends on Open Codeman, Add another or Done. It needs no password and leaves a running Codeman alone. After Skip for now, the app says Codeman has no agent yet and offers this before it opens Codeman.
Finding a Codeman that is already running
The This Mac tab now lists every Codeman server of this user on this Mac, with its address and version. When nothing answers at the default address it says Codeman is already running on this Mac. with a row each. Otherwise they sit under Open Codeman as Also running on this Mac. A second instance beside the main one (a beta, a
codeman web --port 4000in a terminal) used to go unseen, and a server at another address sent the user to Another machine to type it in.A row has Connect, or the reason the app cannot open it. Connect fills in the address and runs the page's one
connect(), so the app checks it, uses its saved login and remembers it like a typed address. Nothing connects by itself.found_addressesinsrc/setup.rsis pure and works out an address for each listening socket of the user's servers, withhttpsand the base path from the command line. Each address other than the default one is asked once, all at the same time, with no login: 2 seconds next to a Codeman that answers, the usual 5 when none does.Stacked on #763
This cannot work without #763, which is the app. It also needs #768,
install.sh --no-admin, the route that needs no password, which is what makes a terminal-free install possible at all. #768 has shipped in 1.49.0.feat/desktop-app(feat(desktop): native macOS app that runs, installs or connects to Codeman #763), so feat(desktop): native macOS app that runs, installs or connects to Codeman #763 is not in this diff.feat(desktop): install Codeman from the app...,feat(desktop): add a coding agent once Codeman is installed,fix(install): say why an added agent failed...andfeat(desktop): find the Codeman servers running on this Mac.... The merges between them fit this work onto feat(desktop): native macOS app that runs, installs or connects to Codeman #763's review commits.git, which opens the window that offers to install them.masteroncefeat/desktop-appis merged and deleted.getcodeman.com/installisinstall.shonmaster, so the shipped app can only use this route once this is merged. Until then the app explains that and offers Terminal.No changeset: COM writes it.
How it works
The app runs the installer and supplies the answers.
src/gui_install.rsrunsinstall.sh --no-admin --progress --service --ai-cli <id>|nonewithCODEMAN_NONINTERACTIVE=1and stdin closed. The login shell's environment goes along, so it finds the Node.js, git and agents that are already there. The network password travels in the environment, never in the arguments, wherepswould show it.The installer's output goes to a file, never a pipe, and it runs in its own process group. With a pipe, quitting the app would kill the installer at its next write; in the app's group, launchd would kill it with the app. The file holds the whole run, so a reloaded page or a reopened app finds the process in
psand replays the file from the top. The app keeps no other state about a run.The app has no list of coding agents. It reads them from
install.sh preflight, so a new agent needs no new app, and the list always matches what can be installed on that machine. On the no-admin route that meansomp(its macOS install is Homebrew) anddeepseek(a launcher alone cannot drive a pane) are not offered.Adding an agent is a run of its own.
install.sh agent <id>touches neither Codeman nor its service, so it may run next to a live server, which an install must refuse: the service step would replace that server under its sessions. It has its own temporary folder, sopstells the two apart, because everything that reads a running install takes it to mean "Codeman is being installed". One run goes at a time.--no-admingoes to bothpreflightandagent: it decides what counts as installable, so without it on both the page could offer an agent the run refuses.Terminal stays as the fallback.
no_admin_blockerdecides: on a Mac older than 15 with no tmux, the tmux project's build cannot run, so the app says why and offers Terminal. Install in Terminal instead is always on the last question, and the install over SSH on another machine is unchanged.Changes to
install.shAll usable from a terminal too, and all documented in
docs/wiki/Installation.md.--ai-cli <id>|none/CODEMAN_AI_CLICODEMAN_NONINTERACTIVE=1installs the first entry of the catalogue, and a GUI must never rely on that. A typo stops before anything runs; a name this machine cannot install warns and carries on, and never installs a different one.install.sh agent <id>done: the install's own check is fatal only when no agent is left, so with another one present a failed install would have reported success. An agent that is already here is a success that runs nothing.install.sh preflight--no-admincannot finish (if so), every agent withinstalledandinstallable. Prompts for nothing and writes nothing.--progress/CODEMAN_PROGRESS=1@@codeman phase <name>and@@codeman error <text>lines. A run that ends withoutphase donefailed.cli_offerableis now the one rule for what can be installed here, so the menu,--ai-cliandpreflightcannot disagree. A step's failure now readsBuilding Codeman failed (exit 1).and theLast lines of ...:header moved to its own line, so the app can show the sentence alone.Tested
Clicked through on a fresh MacBook Pro by the maintainer (M5 Pro, 48 GB, macOS 27.2), in the real WKWebView against the real Rust side. That Mac had no Node.js, tmux, Homebrew or developer tools and no
~/.codeman. It did have Claude Code, so the agent step did not show.127.0.0.1:3000, and Codeman opened. One second after the server started, Apple's developer tools window came up: that is the bug fix(macos): never run Apple's placeholder git, which opens the developer tools window #779 fixes.On a real Mac (Apple Silicon, macOS 27.2, stock
/bin/bash3.2), through the Rust code the page calls. Under a throwawayHOMEwhose login PATH holds no Node.js or tmux, withxcode-selectstubbed to fail the way a Mac without developer tools does, so git counts as missing:packagesleft no installer process behind. Installing again finished the job.~/.local/bin.2.1.318 (Claude Code),tmux 3.7c, and for the other runcodex-cli 0.166.0.Adding an agent, on the same Mac, after a real install with Skip for now, with that install's server running:
codex-cli 0.167.0and2.1.318 (Claude Code).curl: (56)). The app reported it as failed with Codex already present, which is the case the old check would have called a success. The retry worked.The process side, in
cargo teston that Mac: a reader that goes away leaves the install running, the next reader replays it, Cancel kills what the installer started, including subshells, and a failure reports its first error. An agent run is never taken for an install, and a second run of either kind is refused. 130 pass, pluscargo clippy -D warnings.The screens:
test/desktop-connect-install.test.tsruns the realconnect.htmlagainst a fake native side, 72 tests, including the exact spelling of every message the page sends. That spelling matters:deny_unknown_fieldson the Rust side silently drops a command with a misspelled field, sopolicy.rspins the same spelling from its side.The installer: new drives of the real functions in
test/install-sh-invariants.test.ts, a step in CI'sbash:3.2container, and the phase order is pinned against the page. That container'spreflightcheck from this PR's first commit never tested anything: its child run inheritedCODEMAN_INSTALL_SH_LIBand stopped at the sourcing guard, and a false[[ ]]does not tripset -eon bash 3.2. It is fixed here, and both checks were shown to fail when they should.Not tested
cargo test.Known limits, left for later
/usr/local/lib/node_modulesbelongs to root, so they would be expected to fail with npm's permission error, while the agents with their own installer still work. This has not been tried: both Mac runs used the Node.js the installer downloaded..zshrccan find (nvm, say) counts as missing, because the app reads the login shell's environment, as its own server start does. Installing it again is harmless.[[ ]]checks from earlier installer work that a failure would walk past, for the reason above. They are not changed here.The README's limits and
docs/architecture-invariants.mdcarry the rest.