feat(desktop): the model chip and list_models on a Claude Code agent

Merged
CH
6d ago
+900-56

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 model line 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) and update_agent follow the same rule, and update_agent refuses 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 / runtimeChoiceRefusalWords check 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.runtime is set only for a native runtime and left out on eve. The store reads it with nativeRuntimesOfReached(conversationIds), read beside the turn standing in standingsOf and in Start's answer.

  • Chip. useAgentModel passes the agent's runtime to the chip. The chip draws the menu from runtimeModels(...), so the Context row drops out because the narrowed models carry no windows. A new offersFast prop on ModelMenu hides the Fast switch. The Start menu and Settings are unchanged.

  • PATCH /api/agents/{id}. A disallowed change is refused as invalid_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 same runnerChoiceApplied.

  • list_models. It takes an optional agentId and lists what that agent may move to. Without one, it lists what a start takes, as today. Start still offers every model.

  • Runner. runnerSessions has a new retune(sessionId, conversationId), typed as NativeSessions, which NativeAgentSessions now answers. Where the runner is live, it writes the harness's model lines alone, taken from ahead, to stdin. Otherwise it answers not_live, and the next send's ahead or relaunch carries the change. runnerChoiceApplied reaches the harness acting for the starter. It does nothing for eve, or where the function provides no NativeAgentSessions, and it never fails its caller.

Review fixes

  • list_models named an agent says whether the default is among the models that agent may move to (Devin).
  • A native send re-reads the agent's choice just before it writes its model line, so a stale send no longer undoes a retune that 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_code agent was already in place, in the PATCH change procedure and in update_agent, and is tested in coding-agents-app.test.ts and coder-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_models for 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_agent refuses a GPT model, a fast version, and contextWindow (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 names runtime: claude_code. An eve agent takes GPT with no runtime.
  • claude-code-sessions.test.ts: after a choice change, retune writes 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: runnerChoiceApplied reaches the Claude Code harness's retune with its session, acting for the starter. Eve hears nothing for the eve agent. Without the harness, it answers not_live.
  • coding-agent-store.test.ts: nativeRuntimesOfReached names 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):

Model menu on a Claude Code agent: Opus, Sonnet, Haiku, Effort only

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

Model menu on an eve agent

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


Devin Review