feat(desktop): the model chip and list_models on a Claude Code agent
P12 of the rollout in "Bringing Codex or Claude Code Max plans into Luke" (https://tryluke.dev/stage/plan/89f19bfc-88b5-471a-886a-84b00180aa09). Stacked on #4313 (P11).
What the prompt asked
On an agent whose runtime is claude_code:
- the model chip offers Claude models only, with no fast variants and no standard/long context control;
- a change of model or effort reaches the running runner as a
modelline through the native sessions, not through eve's path, or is applied at the next send if no runner is live; list_models(for that agent) andupdate_agentfollow the same rule, andupdate_agentrefuses a GPT model, a fast variant, or a context choice with a clear message;- the panel learns the agent's runtime through the narrowest read.
The rule had to be designed so P24 can add Codex in one place, and eve agents had to stay unchanged.
How it works
-
One rule lives in
@luke/features/agent-runtime-models:runtimeModels(models, runtime)narrows a catalog;runtimeChoiceRefusal/runtimeChoiceRefusalWordscheck a change.
A runtime's rule is one row of
NATIVE_MODEL_RULE. Claude Code's row is Anthropic models, no fast version, no context choice. P24 adds Codex's row. A runtime with no row is eve's, and it gets the catalog back untouched. -
Runtime on the wire.
CodingAgentSummary.runtimeis set only for a native runtime and left out on eve. The store reads it withnativeRuntimesOfReached(conversationIds), read beside the turn standing instandingsOfand in Start's answer. -
Chip.
useAgentModelpasses the agent's runtime to the chip. The chip draws the menu fromruntimeModels(...), so the Context row drops out because the narrowed models carry no windows. A newoffersFastprop onModelMenuhides the Fast switch. The Start menu and Settings are unchanged. -
PATCH
/api/agents/{id}. A disallowed change is refused asinvalid_request. After the row is written,runnerChoiceApplied(agent)runs. -
update_agent. It refuses with words, for example "Not changed: that agent runs on Claude Code, which runs Claude models alone; …". Nothing asked beside the refused change is written. After a write, it calls the samerunnerChoiceApplied. -
list_models. It takes an optionalagentIdand lists what that agent may move to. Without one, it lists what a start takes, as today. Start still offers every model. -
Runner.
runnerSessionshas a newretune(sessionId, conversationId), typed asNativeSessions, whichNativeAgentSessionsnow answers. Where the runner is live, it writes the harness'smodellines alone, taken fromahead, to stdin. Otherwise it answersnot_live, and the next send'saheador relaunch carries the change.runnerChoiceAppliedreaches the harness acting for the starter. It does nothing for eve, or where the function provides noNativeAgentSessions, and it never fails its caller.
Review fixes
list_modelsnamed an agent says whether the default is among the models that agent may move to (Devin).- A native
sendre-reads the agent's choice just before it writes itsmodelline, so a stale send no longer undoes aretunethat landed while it was renewing the credential (Devin). A window of one store read and one stdin write remains. Closing it fully would need a per-runner lock across function instances; I judged that out of proportion here, since the next message re-applies the row's choice anyway. - Server-side refusal of a GPT model, a fast version, or a context window on a
claude_codeagent was already in place, in the PATCHchangeprocedure and inupdate_agent, and is tested incoding-agents-app.test.tsandcoder-agent-tools.test.ts. This is the requirement carried over from P11's review. - Merged P11's latest head (fc3bcc484) with a merge commit.
Decision made (small)
list_models is narrowed only when it is named an agentId. A Claude Code agent's own list_models still lists every model, because a start from it may run on any model, and the brief says Start offers everything.
Tests
agent-model-chip.test.ts: on a Claude Code agent the menu lists Opus and Sonnet only, with Effort but no Fast or Context. On eve, the same catalog lists every model, with Context and Fast.coder-agent-tools.test.ts: on the same catalog,list_modelsfor a Claude Code agent lists[opus, sonnet]. A start's list and an eve agent's list both stay[opus, opus-fast, sonnet, sol].update_agentrefuses a GPT model, a fast version, andcontextWindow(standard and long) on a Claude Code agent, each in words and with nothing written. It takes Sonnet. An eve agent still takes a fast version, the standard window, and GPT.coding-agents-app.test.ts: PATCH refuses the three disallowed changes on a Claude Code agent, takes a Claude model, and the answer namesruntime: claude_code. An eve agent takes GPT with no runtime.claude-code-sessions.test.ts: after a choice change,retunewrites exactly{type:"model", model:"claude-sonnet-5-5", effort:"low"}to the running runner. A session with no runner takes nothing.runtime-sessions.test.ts:runnerChoiceAppliedreaches the Claude Code harness'sretunewith its session, acting for the starter. Eve hears nothing for the eve agent. Without the harness, it answersnot_live.coding-agent-store.test.ts:nativeRuntimesOfReachednames only the native agents' conversations.
Screenshot
Model menu on a Claude Code agent, rendered with the repository's real CSS in a throwaway Vite harness (the Codex mark is missing there only because the harness serves no assets):

The same catalog on an eve agent, unchanged (every model, Effort, Context, Fast):

Checks
pnpm check:affected: run locally (see comment).- The developer waived Preview runs for this rollout.
Out of scope: Start's payer (P14), statuses (P15), Codex (P24).
Made with Luke