Install an agent from the picker, on this Mac as well as on a box - #219
Merged
Merged
Conversation
A launch runs `<agent>; exec $SHELL -l` in a PTY, so a missing binary
became one line of shell output in a pane that then looked healthy: the
card was filed `running`, a loop tick reported "run N started", and
nothing said the agent never ran. An agent installed yesterday and gone
today (an uninstall, a PATH that moved under a reboot) failed silently
every hour.
Every launch now asks the login shell whether the binary resolves before
anything is written down. The probe uses the same shell and flags the
launch does, because probing /bin/sh with the daemon's own PATH answers a
different question and produces false absents on exactly the Macs whose
boot-time PATH probe missed. It reports present/absent/unknown and only a
definite absent refuses: a shell that fails to answer knows nothing about
the binary, and blocking work on that would be worse than the bug.
Three callers did irreversible bookkeeping before spawning, which a
refusal would have stranded:
- a loop persisted its task link only after the launch, so a failing
tick left it unlinked and the next one built another worktree;
- restore stamped the tab `restored` first, and that is a one-way door
off the restorable strip;
- the loop form's <select> omitted an uninstalled agent, so it showed a
different one while saving the missing one straight back.
Also gates the phone's agent picker, which offered agents the box hasn't
got while the desktop's has always disabled them.
The probe is a seam on Services so tests don't assert which CLIs happen
to be installed on the machine running them (a CI runner has none).
The Install button only ever appeared for a box, because the whole flow was SSH: `installAgent(alias, agentId)` couldn't express the local machine, and its implementation was `ssh <box> "bash -lc '<install> && command -v <bin>'"`. That shape came from #108, "install coding agents on a box", and it left the Mac and the phone with no way in at all. It is now an engine RPC, the way editor:install already installs code-server on the task's own machine: the engine doing the installing IS the machine that has to end up with the binary, so one implementation serves this Mac, a box, and the phone. Routed by the engine's projectId, the key that already lands a task on a given environment. SSH was never a constraint here: installAgent already required a connected backend and ran after the engine was up. The button would have lied without the second half. `ensureLoginEnv` resolves the PATH once at startup and returns on the first success, so the PATH this process spawns everything with is a snapshot of the machine as it was when the app opened. A CLI installed afterwards is invisible until a restart, whoever installed it. Seen for real: opencode's installer appends to ~/.zshrc, a fresh login shell finds it, the running app cannot. So `refreshLoginPath` re-resolves and re-adopts, rate-limited because an interactive login shell can take ten seconds to answer. Two call sites, and it needs both: - the agent catalog, because a greyed-out row is unselectable, so no launch can ever be attempted to discover the agent has arrived; - the launch guard before it refuses, because a loop tick never opens the picker that would otherwise have refreshed it. Verified on a Mac whose PATH predates the install: probe absent, refresh finds it, re-probe present, launch spawns. No restart. Installing keeps `&& command -v <bin>` as its acceptance test, so "done" can't contradict the next launch, and reports the installer's own last lines when it fails.
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.
Follow-up to #217. Asked as "why is the option not showing the Install button if I wanted to install it directly from Ateam?"
Why there was no button
The whole flow was SSH, so it could only ever mean "a box".
installAgent(alias: string, agentId)(shared/host.ts:163) cannot express the local machine, and the implementation isssh <box> "bash -lc '<install> && command -v <bin>'"(main/host.ts:597). The picker suppressed the button accordingly:canInstall = !avail && alias !== null && …. That shape came from #108, "install coding agents on a box", and left this Mac and the phone with no way in.SSH was never a constraint here:
installAgentalready requires a connected backend (host.ts:594) and runs after the engine is up (host.ts:574-581).What it is now
An engine RPC, the way
editor:installalready installs code-server on the task's own machine (dispatcher.ts, aggregate.ts). The engine doing the installing is the machine that has to end up with the binary, so one implementation serves this Mac, a box, and eventually the phone, which has never had an install path. Routed by the engine'sprojectId, the key that already lands a task on a given environment (unify.ts).Installing keeps
&& command -v <bin>as its acceptance test, so a "done" cannot contradict the next launch, and reports the installer's own last lines when it fails, asinstallCodeServerdoes.The half without which the button would lie
ensureLoginEnvresolves the PATH once at startup and returns on the first success (engine.ts:137, login-env.ts:165-168); the retry backoff exists only for a probe that failed. So the PATH this process spawns everything with is a snapshot of the machine as it was when the app opened, and a CLI installed afterwards is invisible until a restart, whoever installed it.Seen for real: opencode's installer appends to
~/.zshrc, so a fresh interactive login shell finds it and the running app cannot.refreshLoginPathre-resolves and re-adopts, rate-limited because an interactive login shell can takePROBE_TIMEOUT_MSto answer. Two call sites, and it needs both:Verification
probe → absent,refreshLoginPath → changed,probe → present, and a realspawnAgentInTask("opencode")spawns and files the cardrunning. No restart.installAgentCliexercised against a synthetic agent: resolves when the binary lands, rejects whencommand -vdoesn't find it.Three things the build corrected along the way: box installs do stream today, so dropping streaming was a regression rather than a limitation (followed
installCodeServerinstead: spinner on success, installer's last lines on failure); a test caught two clocks fighting in my own rate limiter; anddispatcher.test.tsdidn't stub the refresh, so it ran a live login shell and mutated the test process's PATH, breaking three later tests that shell out to git.