feat(desktop): composer model/mode pills, and the three switches that never worked (R6) - #573
Merged
Conversation
… never worked (R6)
Two pills beside the context ring, in the composer's own row: which model
is about to answer, and which permission mode it runs under. An agent that
advertises its choices gets a menu; claude, which advertises none, gets a
text entry. A switch on the respawn route is previewed before it fires —
it restarts the agent, and the pill says so rather than letting the
director find out.
Building the consumer measured the producer, and three of the four
(family, field) pairs the Companion drives could never have switched
anything:
claude-code/model --model in every claude template → works
claude-code/mode --permission-mode real flag, in NO template → 422
codex/model --model `codex app-server` has none
codex/mode --approval-policy not a codex flag at all
codex-cli 0.147.0: "error: unexpected argument '--approval-policy' found;
tip: a similar argument exists: '--approve-for-me'" — the real one is
`-a, --ask-for-approval`. `respawn` rewrites a flag that must already be
in backend.cmd, so the last three answered a 422 reading "pick a fresh
template that exposes it" — advice for a template that cannot exist.
The routing table could not express this: one token answered for two
independent capabilities. Families now declare `runtime_switch_fields:
{mode, model}` alongside the route, the hub checks it before routing, and
`GET /agent-families` publishes it so the client hides a control instead
of discovering the refusal from an error. This is NOT the `agents.kind`
defect fixed in df24415 — that gate matched no family at all; this one
matched the right family and asked it a question it could not express.
Claude's mode pill is read-only on purpose, not deferred: L3a measured
that `--permission-mode plan` does not stop Bash under `--print`, so an M2
pill offering it would name a boundary the engine does not enforce.
Codex's real path is `thread/start.config` (measured in L4c), which is
driver work — task #243; flipping the registry bit lights the pill up with
no UI change.
Porting mobile's picker exactly would have hidden the pill whenever the
agent advertises nothing, leaving a claude session with no model indicator
at all. The desktop adds a read-only state mobile has no equivalent for,
so "cannot be changed" and "unknown" stop looking identical.
Tests: 10 new state tests, both mutation-checked — same-event list capture
and a route-only gate each fail them. The Go case set separates the mask
from the route (one family, one route, two different answers).
Known gap, filed as #242: a respawn mints a new agent id. SessionsPanel
follows it (keyed on session:agent from the digest); FocusRegion and
ProjectBoard mount a fixed id and stay pointed at the terminated agent —
the same exposure the pause/stop actions already have.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…solver The resolver test pins the mask's VALUES — (claude-code, mode)=false and both codex fields false — but nothing exercised the handler actually consulting them: deleting the `!switchFields[field]` gate left the whole server suite green while turning every masked switch back into the doomed respawn attempt (a 500 "no live session" in the test rig; the misleading 422-with-template-advice in production) that R6 exists to remove. Three cases, one per wrong (family, field) pair, each asserting the typed 422 lands BEFORE the route and that a refusal writes no input event row. Verified by mutation: neutralizing the gate fails all three with `status = 500 want 422`. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
physercoe
pushed a commit
that referenced
this pull request
Aug 17, 2026
…atus docs Both PRs rewrote the plan's W4 status paragraph and appended under the changelog's Unreleased headings — the collision called out at review time. - plan: united to "W4 complete (2026-08-17)" naming all of F4 / L4a–L4c / R5 / R6, remaining = L3c only (deferrable). - changelog: union — both Added entries kept (R5 above R6), both Fixed entries under one heading. Everything else auto-merged; verified on the merged tree: agentfamilies + hostrunner drift/parity tests (the auto-merged generated artifact matches a regeneration of the merged YAML), electron 720/0, frontend 908/0, docs lint 323. Co-Authored-By: Claude Fable 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.
Closes the R6 wedge of
docs/plans/desktop-companion-vision-parity.md. W4 now has only R5 left.What the director gets
Two pills beside the context ring, in the composer's own row — these are questions asked while typing ("which model is about to answer this?"), not facts looked up in a details panel.
respawnroute restarts the agent. The pill previews that and asks first (IAA) instead of doing it and letting you find out.Building the consumer measured the producer
Three of the four (family, field) pairs the Companion drives could never have switched anything:
--model--permission-modeacceptEdits|auto|bypassPermissions|manual|dontAsk|plan), in no template--modelcodex app-server, the M2 argv, accepts none--approval-policy--approve-for-me"; the real one is-a, --ask-for-approvalrespawnrewrites a flag that must already be inbackend.cmd, so the last three answered a 422 reading "pick a fresh template that exposes it" — advice pointing at a template that cannot exist.Root cause: one routing token answered for two independent capabilities. Families now declare
runtime_switch_fields: {mode, model}alongside the route; the hub checks it before routing;GET /agent-familiespublishes it so the client hides a control rather than discovering the refusal from an error.This is not the
agents.kinddefect fixed indf24415c— that gate matched no family at all. This one matched the right family and asked it a question the data could not express.Two deliberate non-fixes
--permission-modeis necessary but not sufficient: L3a measured that--permission-mode plandoes not stop Bash under--print(claudewire.test.ts:56), so an M2 pill offeringplanwould name a boundary the engine does not enforce.thread/start.configtakes a map of config overrides (measured in L4c), as does-c model=…. Driver work, filed as i18n: localize team / profile switcher menu (#236) #243. Flipping the registry bit lights the pill up with no UI change.Verification
respawn, so a mask that mirrored the route would have passed;(claude-code, model)=truevs(claude-code, mode)=falseis the separating input. Mutation-verified.go test ./internal/server ./internal/hostrunner ./internal/agentfamilies✅ (the schema-coverage guard caught the undeclared field; schema updated)node --test: 886 pass, 0 fail · electron shell: 715 pass, 0 fail, 6 skippedtsc --noEmit✅ · fullvite build✅ · token ratchet clean, no phantom tokens (five of my first-draft tokens did not exist)lint-openapi.sh(openapi.yaml updated for the new field + the second gate)Known gap — #242
A respawn mints a new agent id. SessionsPanel follows it (it keys the transcript on
session:agentfrom the digest'scurrent_agent_id, and this PR invalidates those caches).FocusRegionandProjectBoardmount a fixed id and stay pointed at the terminated agent — the same exposure the pause/stop lifecycle actions already have. Filed rather than half-built here.🤖 Generated with Claude Code