feat(desktop): the agent's model in the message box, and no sticky header on an agent tab

Merged
DA
8d ago
+1446-515

Dean: "put a model picker in the bottom left of the chat composer" and "remove the sticky header on a coding agent tab that shows model name and status."

What changes

The model chip. The agent box's foot's left slot (#1933's shared composer, where New Plan keeps its repository chip) now holds a chip in the composer's chip style: the provider's mark and "Claude Opus 5.5 · High", with "· Fast" where the fast version is on (planning/agent-model-chip.tsx). A press opens the same menu as the Start ▾ chevron, now one component the two share rather than a fork: planning/model-menu.tsx holds the searchable list, newest-first base models under their marks, the Effort › submenu, and the Fast switch, lifted out of start-agent-button.tsx word for word; the Start button keeps what the menu chooses as the account's default, the chip changes its agent. The chip hangs its menu above itself, since the box stands at the panel's foot.

Choosing changes this agent for its next step. PATCH /api/agents/{id} takes { model, effort }, owner-only (another account's agent is not found), validated against the catalog through the same acceptedChoice a Start uses (an unknown model or effort, or a model without its effort, is invalid-request), writes the choice on the agent's row (updateCodingAgentChoice), and answers the updated summary. The account's default is untouched. The coder agent's defineDynamic already reads the row at every step, so a change mid-turn is the next step's and an idle agent's next turn takes it; coder-host.test.ts proves the read through the host's model seam. The route is routes/agents/agent.ts with its rewrite (/api/agents/([^/]+), after the agent's sub-paths), the client's choose, the gateway method codingAgents.choose, and the act CODING_AGENTS_CHOOSE.

Optimistic, with a rollback. use-agent-model.ts draws the pick on the chip the moment it is made and sends it; the agent the service answers replaces the list's (use-coding-agents.ts's onChoose), and a refusal takes the pick off again with why under the box, in the composer's note slot ("That model isn't offered any more. Choose another." / "The model could not be changed. Try again."), with no Retry since the chip is the retry. The chip reads the same whether the agent runs or idles.

No header. AgentHeader and its CSS are gone. Its contents:

  • Model and effort — the chip.
  • Status — the tab's dot and the transcript's "Working…" shimmer while running; a turn that ended stopped or failed now ends the transcript on one quiet line, "Stopped" or "Failed: …". For the words, the transcript page answer gains an optional failureReason, one of a fixed set (CODING_AGENT_FAILURE: model, sandbox, github, abandoned, other), mapped server-side by coder-host/status.ts's codingAgentFailureOf from the turn row's failure word and the host's own refusal sentences in its detail, so nothing eve or a provider said reaches the desktop or the session replay (the orchestrator's call). The desktop words each: "Failed: the model call failed", "…the sandbox or checkout failed", "…GitHub refused or could not be reached", "…the turn ran past its bound and was ended", bare "Failed" for other.
  • PR pill and ⋯ — one chip after the model chip in the same style (agent-published.tsx's PublishedChip): GitHub mark, #123, the state colour and check dot; the branch's name with no pull request; nothing with neither. A press opens the small menu with #1903's items, Open pull request, Copy branch name, Copy checkout command, View changes on GitHub, behaviour and tests kept.

Tests

One test per behavior, through the public seam, after a trim at the orchestrator's ask:

  • agent-composer.test.ts: the chip's model, effort, and Fast; a pick drawn at once and sent for this agent, read back from the answer, a refusal rolled back with the note and no Retry.
  • use-coding-agents.test.ts: onChoose asks CODING_AGENTS_CHOOSE, the answered agent takes its place, and of two changes answered out of order the one made last stands (Devin's finding).
  • agent-tab.test.ts: no header, the chip at the box's foot, the Stopped and Failed end lines; the PR and branch chips and the chip's menu items word for word; an agent that ended before its first message says how rather than "starting" (Devin's finding).
  • coding-agents-app.test.ts: PATCH writes the row, answers the summary, leaves the default; refused for another account, an unknown model, an unknown effort, a choice missing its effort; the page carries failureReason.
  • coder-host.test.ts: a change to the row is what the next step is told. coder-host-model.test.ts: the failure reasons as one table.
  • Client, act rows, composer, and transcript hook each gain the one call or field.

Counts

+−
Whole PR1446515
Tests437121
Generated (goldens, rewrites)530

Review

Devin's three findings are fixed in the third commit: the newest change of model stands when answers cross; an agent that failed before its first message no longer reads "The agent is starting"; and the chip's menu hangs from the card's own edge at the card's width, so a panel at its narrowest no longer clips it.

Screenshots

Headless Chrome over the real components and the renderer stylesheet (Linux fonts), on pr-assets.

Idle, with the model chipThe model menu openWith a PR chip
idlemenupr
The PR chip's menuThe tab without a header, running
pr menutab

Verification

./scripts/check.sh exits 0 locally. CI could not verify the Mac: no macOS job runs on a pull request, and Dean waived ./scripts/verify.sh for this work, so the Mac rendering is unverified beyond the headless captures above.

🤖 Generated with Claude Code