Repository navigation
fix(native-chat): a new chat's model and effort pickers match their first frame before the agent starts - #27031
Conversation
…e before the agent starts A chat with no model pick runs the catalog's default model at that model's default effort. The at-rest options answer now says so for every agent, not only those whose live child reports the model's default, so the effort pill no longer blanks and then jumps when the agent starts.
…s in The at-rest options read asked the model catalog with no workspace, so it always named the account's default model and effort, even in a repo whose own agent config could pick another. The new chat's first frame, read for that repo, names nothing there. The at-rest read now asks for the session's own folder, so both frames agree on the model and the effort.
…t take no model A client that needs a model in every options answer (mobile today, older desktops) refuses an answer naming none, and loses that read's commands with it. Only a client advertising the optional-model capability gets the answer for the chat's own folder; any other keeps today's account-default answer.
… main drops the tier map
…fault for every client The folder-scoped catalog read now decides the default for every client. Where it names one, the answer names that model and its effort. Where it names none, a client that takes a model-less answer gets none, and one that needs a model gets the account default model with no effort: exactly what it got before. The reader is now required, and the first frame and the at-rest read share one rule for which locations name a local folder.
Only in-process callers omit the reader at the host's entry, and they read as the RPC gate treats an in-process caller. The shared read itself still requires one. This leaves main's newer test call sites unconflicted.
…t-rest folder read
Review summaryReviews: three rounds. The last round found no P0, P1 or P2.
Visual proof (in the body): before and after on a macOS dev build with stand-in Codex, Grok and Pi. Each frame's time was checked against the app's own logs and the stand-in logs.
CI: all 15 checks pass at faf3aae. The PR merges cleanly with current main. Still out of scope, listed in the body:
Not verified: OpenCode and OMP live (they share Grok's code path), mobile, SSH/WSL hosts, and real agent CLIs. |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Priority: ⬇️ Low Merge Risk: ⚪ Minimal · up to This change makes a new chat's model and effort pickers consistent with the first frame before the agent starts. No merge-blocking risk was found, and tests cover the main scenarios. Pre-merge checks |
|
…irst frame before the agent starts (#27031) * fix(native-chat): a new chat's effort pill keeps its first-frame value before the agent starts A chat with no model pick runs the catalog's default model at that model's default effort. The at-rest options answer now says so for every agent, not only those whose live child reports the model's default, so the effort pill no longer blanks and then jumps when the agent starts. * fix(native-chat): read a chat's at-rest options for the folder it runs in The at-rest options read asked the model catalog with no workspace, so it always named the account's default model and effort, even in a repo whose own agent config could pick another. The new chat's first frame, read for that repo, names nothing there. The at-rest read now asks for the session's own folder, so both frames agree on the model and the effort. * fix(native-chat): scope the at-rest options read only for clients that take no model A client that needs a model in every options answer (mobile today, older desktops) refuses an answer naming none, and loses that read's commands with it. Only a client advertising the optional-model capability gets the answer for the chat's own folder; any other keeps today's account-default answer. * test: run the at-rest workspace-scope RPC test in the Node SQLite project * test: build catalog listings so the fixtures compile before and after main drops the tier map * fix(native-chat): one at-rest rule — the chat's folder decides its default for every client The folder-scoped catalog read now decides the default for every client. Where it names one, the answer names that model and its effort. Where it names none, a client that takes a model-less answer gets none, and one that needs a model gets the account default model with no effort: exactly what it got before. The reader is now required, and the first frame and the at-rest read share one rule for which locations name a local folder. * fix(native-chat): keep test callers of the host's options read untouched Only in-process callers omit the reader at the host's entry, and they read as the RPC gate treats an in-process caller. The shared read itself still requires one. This leaves main's newer test call sites unconflicted. * refactor: one native-local location check for Claude, Codex and the at-rest folder read (cherry picked from commit 7dfa4d6)
ELI5
When you open a new Grok, OpenCode, OMP, Pi or Codex chat, the effort picker can show the right value for a moment (for example "Medium"), then go blank ("Effort"), then jump back to "Medium" once you send your first message and the agent starts. The host already knew the right value and dropped it in one place. It now keeps it. In a repo whose own agent config could choose a different model or effort, the picker also stops naming the account's default before the start, because that default may not be what the chat runs.
What Changed
The problem you saw. In a new Grok, OpenCode, OMP, Pi or Codex chat, the effort picker showed the empty "Effort" placeholder until the agent started, then jumped to the real effort ("Medium"). Claude chats did not do this.
Why it happened.
.codex/config.tomlsettingmodel_reasoning_effort, or.pi/settings.json) can choose another model or effort. In that repo the first frame deliberately names nothing.User-facing change. Before the first message:
Mechanism. One rule, in the host's at-rest read (
structured-agent-session-options-read.ts). It applies when the chat has no saved model:agent-model-catalog-service.ts, newinSessionWorkspaceread option). It uses the folder lookup the host already uses to learn an account's default from a chat.agent-session.options.optional-model.v1) gets none.Other details:
agentSession.optionsRPC handler passes the client's capability into the read. That capability only chooses the fallback; it never changes the folder scope.isNativeLocalLocationinexecution-host.ts). A WSL chat or a chat on another host names no folder default, the same as its first frame. The Claude and Codex structured-location checks and the Codex launch guard now call the same helper, so there is one definition.Still out of scope (pre-existing, not changed here).
Why
Compatibility.
Possible follow-up: let mobile take the folder-scoped answer. Mobile's options code applies the answer only through the shared picker helpers desktop uses (
applyStructuredAgentSessionOptions,structuredAgentSessionOptionSnapshot), and those already handle an answer with no model by showing the quiet "Model" placeholder. So mobile looks able to advertiseagent-session.options.optional-model.v1. That isn't done here and would want a mobile test of the model-less answer first.Linked Issue
None. This is a maintainer follow-up to a bug found on main, and there is no existing issue.
Visual Proof
Hidden dev builds on macOS, driven over the Chrome DevTools Protocol with stand-in agents (no real agent ran). Before = this PR's merge-base with main (
9764dc8b976); after = this PR (faf3aae2b75). Each run first let one Codex, Grok and Pi chat start with no pick, so the saved model list knew each account's default model and effort (the Grok stand-in's account default is Grok Build at Medium; its listing marks High as the model default, so "Medium" can only come from the learned account default).Timing method. Each "first frame" was captured while the chat's first at-rest options read (
agentSession.options) was held at its call site, before it was sent. The "at rest" frame was taken ~4.5 s after the chat opened, after that read had answered. The read's send and answer times came from debugger logpoints in the renderer hook that issues it. Start times come from the stand-in agents' own logs. All times are UTC on 2026-10-10.1. Plain folder: new chat, nothing sent, then the first send
gpt-6.1-solat medium, and the picker did not change. The at-rest frame is from 21.747.grok-buildat medium, with no change. The at-rest frame is from 35.158.2. Folder with its own agent config (
.codex/config.toml:model_reasoning_effort = "high";.pi/settings.json:defaultThinkingLevel: "high")gpt-6.1-soland no effort, and the picker switched to "GPT-6.1 Sol · Effort" at 59.248 (at-rest frame 16:03.752).effort=highat 16:29.618. The picker went to "Model" at 29.635, then to "GPT-6.1 Sol · High" at 29.672 (frame 35.437).effort=highat 44.813, and the picker went straight from "Model" to "GPT-6.1 Sol · High" at 44.863 (frame 50.651).Not covered here: OpenCode and OMP, mobile, a remote or WSL host, and real agent CLIs. The Codex and Pi stand-ins were made to apply the folder's config at start the way the real CLIs do; that behavior was not checked against the real Codex or Pi.
Testing
New
src/main/runtime/structured-agent-new-chat-effort-at-rest.test.ts. It builds a chat that never sent a message, in a temporary repo, with a warm model list and the real "could this repo's config change the model" check. It reads the first frame for that repo, runs the real at-rest options read, applies the answer the way the picker does, and checks that both pills match the first frame. Cases:.codex/config.tomlsettingmodel_reasoning_effort, and Pi with.pi/settings.jsonsetting a thinking level: both frames name no model and no effort.New
src/main/runtime/rpc/methods/structured-agent-session-options-workspace-scope.test.tsgoes through the real RPC handler and host, for a Codex chat at rest:.codex/config.tomlsets the effort:ok, the account defaultgpt-5.5with no effort, and the slash commands. There is no refusal and no agent start.ok, the commands, and no model or effort.gpt-5.5at Medium.Each fix fails without itself:
gpt-5.5at Medium, oropenai/gpt-6at High, where the first frame named nothing.Existing tests pass: the model-list service tests, the at-rest options read (host and RPC), the optional-model client gate, Pi, ACP and Claude configured defaults, Claude effort at rest, the first-frame tests (host and shared picker), the renderer first-frame and learned-default hooks, the Codex options tests, and chat create.
pnpm tc:node, oxlint and the repo's static CI checks also pass locally. Only main-process code changed.Platforms: unit tests on macOS only. The change is host logic with no new paths or processes. WSL chats are covered by the existing rule that names no default for them.
AI Disclosure
AI coding agents (Claude Code).
Review
Agent skill upstream boundary
Notes
Ensure no issues in: Security, Cross-platoform support (Linux, Windows, Mac), Remote SSH, Mobile, general backwards compatibility, performance
Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)