Skip to content

fix(native-chat): a new chat's model and effort pickers match their first frame before the agent starts - #27031

Merged
brennanb2025 merged 8 commits into
mainfrom
brennanb2025/new-chat-effort-first-frame
Oct 11, 2026
Merged

brennanb2025 merged 8 commits into
mainfrom
brennanb2025/new-chat-effort-first-frame

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 6 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​503 $\color{#cf222e}{\Huge{\mathbf{−}}}$​13 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​490
Prod 10 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​91 $\color{#cf222e}{\Huge{\mathbf{−}}}$​33 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​58

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.

  • A new chat's agent starts when the first message is delivered, not when the chat is created (since Save a new chat's first message during create #26332, which creates a blank chat without starting its agent). Until then the chat is "at rest".
  • The picker paints its first frame from the host's model list for the chat's folder. When the host knows the account's default model and effort, and nothing in that folder can change them, the first frame names both.
  • Right after that, the chat asks the host for its own options. With no agent running, the host builds the answer from the same model list, with two gaps:
    1. It named the default model, but added the effort only for agents marked as "a running agent reports the model's default effort". Only Claude has that mark, so every other agent's answer had a model and no effort.
    2. It read the model list without naming the chat's folder, so it always named the account's default. That was true even in a repo whose own config (for example .codex/config.toml setting model_reasoning_effort, or .pi/settings.json) can choose another model or effort. In that repo the first frame deliberately names nothing.
  • The picker takes the at-rest answer as the chat's real state. So the effort showed as unknown, and in such a repo the picker could name a model and effort the chat then didn't run.

User-facing change. Before the first message:

  • In a folder whose own config cannot change the model or effort: desktop and mobile now both show the default model and its default effort, the same as desktop's first frame. Before, the effort went blank, then jumped at the start.
  • In a folder whose own config could choose another model or effort:
    • Current desktop now names nothing, matching its first frame. Before, it could show the account default, which then changed at the start.
    • Mobile and older desktop builds see the same picker as main: the account default model with no effort.

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:

  1. The model list for the chat's own folder decides the default, for every client (agent-model-catalog-service.ts, new inSessionWorkspace read option). It uses the folder lookup the host already uses to learn an account's default from a chat.
  2. If that list names a default, the answer names that model and its default effort, as the first frame does.
  3. If it names none, the answer depends on the client:
    • A client that accepts an answer with no model (it advertises agent-session.options.optional-model.v1) gets none.
    • Any other client gets the account default model with no effort, so it sees the same picker as main and never gets a refusal.

Other details:

  • A saved effort still wins. A saved model with no saved effort still leaves the effort to the agent, as before.
  • The agentSession.options RPC handler passes the client's capability into the read. That capability only chooses the fallback; it never changes the folder scope.
  • The shared read requires the caller to say which kind of client it is answering. Only the host's in-process entry has a default, and that default reads as the RPC gate already treats an in-process caller (it accepts an answer with no model).
  • The folder lookup and the first frame's lookup now share one check for whether a location is a folder on this machine (isNativeLocalLocation in execution-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).

  • After a model pick at rest, the effort stays blank until the start. This is deliberate. For a model the user picked, the effort the agent will run can differ from the model's listed default (Codex applies the effort from its own config), so the host doesn't guess.
  • With no saved model list yet for the account (a cold catalog), the picker shows the placeholder, then the value at the start. No answer before the start has anything to name.
  • The first Codex chat on an account, before Orca has learned that account's default. Orca's Codex listing applies the model and effort from Codex's own config, but an older Codex that can't report its config falls back to the listing's default. That default can differ from what the chat then runs.

Why

  • Host side, not only the desktop picker. Desktop and mobile both use the answer the host builds. Fixing only the desktop picker would leave mobile with the same jump.
  • Not "skip the options read until the agent starts". That would also leave mobile unfixed. It would also delay the other things that answer carries before the first message: the slash-command list and the goal.
  • Reading the model list for the chat's folder. The at-rest answer and the first frame then come from the same question, so they agree on both model and effort by construction. Patching only the effort would leave the wrong model and effort in repos with their own agent config.
  • One rule, not per-client scoping. The folder always decides. A client's capability only chooses what an answer that would otherwise name nothing falls back to. So no client can get an effort its folder doesn't run.

Compatibility.

  • The answer's shape is unchanged. An older client may now get an effort the answer used to leave out. That is an optional field it already reads, and only where the folder runs that effort.
  • A client without the capability (every current mobile build, older desktop builds) never gets an answer worse than before:
    • In a folder whose config could change the model, it sees the same picker as main: the account default model, no effort, and the slash-command list. It never gets the "unsupported" refusal.
    • One difference on the wire, with no effect on display: for a chat with a saved model in such a folder, the answer's model list marks no row as the default, where main marked the account default.
    • In a plain folder, it now also gets the default effort.
  • A newer client talking to an older host behaves as it does today until that host updates.

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 advertise agent-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

Before (main) After (this PR)
Codex: first frame
Codex: at rest
Codex: after start
Grok: first frame
Grok: at rest
Grok: after start
Pi: at rest
  • Codex, before: shows "GPT-6.1 Sol · Medium" at 10:10:02.935. The first frame was captured at 03.123, before the read was sent. The read answered at 03.132 with a model and no effort, and the effort went blank ("Effort") at 03.136. The at-rest frame is from 07.610. After the send, the stand-in started its thread at 33.491, the effort jumped back to "Medium" at 33.507, and the after-start frame is from 38.864.
  • Codex, after: shows "GPT-6.1 Sol · Medium" at 10:00:17.068. The first frame was captured at 17.239, before the read was sent. The read answered at 17.252 with gpt-6.1-sol at medium, and the picker did not change. The at-rest frame is from 21.747.
    • The after-start frame comes from an earlier chat in the same run. It was also "GPT-6.1 Sol · Medium" at rest. Its thread started at 09:54:42.849, the frame is from 48.696, and its picker never changed across the start.
  • Grok, before: shows "Grok Build · Medium" at 10:11:14.345. The first frame was captured at 14.519. The read answered at 14.525 with no effort, and the effort went blank ("Effort") at 14.534. The at-rest frame is from 19.058. The session started at 44.216, the effort jumped back to "Medium" at 44.237, and the after-start frame is from 49.911.
  • Grok, after: shows "Grok Build · Medium" at 10:00:30.498. The first frame was captured at 30.683. The read answered at 30.687 with grok-build at medium, with no change. The at-rest frame is from 35.158.
    • The after-start frame comes from an earlier chat in the same run (session started 09:55:35.125, frame 40.919). It showed "Medium" before and after the start.
  • Pi: before, "Medium" went blank at 10:14:38.837, right after the read answered with no effort at 38.834 (frame 43.222). After, the read answered at 10:00:43.146 with medium and the picker stayed "GPT-6.1 Sol · Medium" (frame 47.586).

2. Folder with its own agent config (.codex/config.toml: model_reasoning_effort = "high"; .pi/settings.json: defaultThinkingLevel: "high")

Before (main) After (this PR)
Codex: first frame
Codex: at rest
Codex: after start
Pi: at rest
  • Codex, before: shows the "Model" placeholder at 10:15:59.062. The first frame was captured at 59.232. The read answered at 59.238 with the account default gpt-6.1-sol and no effort, and the picker switched to "GPT-6.1 Sol · Effort" at 59.248 (at-rest frame 16:03.752).
    • After the send, the stand-in started its thread with the folder's effort=high at 16:29.618. The picker went to "Model" at 29.635, then to "GPT-6.1 Sol · High" at 29.672 (frame 35.437).
  • Codex, after: shows the "Model" placeholder at 10:02:25.238. The first frame was captured at 25.396. The read answered at 25.401 with no model and no effort, and the picker stayed "Model" (at-rest frame 29.973).
    • The thread started with effort=high at 44.813, and the picker went straight from "Model" to "GPT-6.1 Sol · High" at 44.863 (frame 50.651).
  • Pi, before: the at-rest read named the account default, so the picker showed "Openai Codex GPT 6.1 Sol · Effort" (10:17:16.799). At the start it went "Model", then "GPT-6.1 Sol · High".
  • Pi, after: the picker stayed "Model" through the at-rest read (answered 10:03:03.550 with nothing). At the start it went straight to "GPT-6.1 Sol · High" (17.858).

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

  • I manually tested these changes locally
  • Automated tests added/updated, or explained why not below

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, Grok (an ACP agent) and Pi in a plain repo: the model and effort stay the same. Grok's learned effort (max) differs from its listing's default (medium), so the test tells the two sources apart.
  • Codex with .codex/config.toml setting model_reasoning_effort, and Pi with .pi/settings.json setting a thinking level: both frames name no model and no effort.
  • The same Codex repo, read for a client that needs a model: the same picker as main (account default model, no effort, plus the slash commands).
  • A plain Codex repo, read for a client that needs a model: the default model and its effort.
  • A saved effort still wins, and a saved Codex model with no saved effort still names no effort.

New src/main/runtime/rpc/methods/structured-agent-session-options-workspace-scope.test.ts goes through the real RPC handler and host, for a Codex chat at rest:

  • In a repo whose .codex/config.toml sets the effort:
    • A mobile-style client without the capability gets ok, the account default gpt-5.5 with no effort, and the slash commands. There is no refusal and no agent start.
    • A client with the capability gets ok, the commands, and no model or effort.
  • In a plain repo, the mobile-style client gets gpt-5.5 at Medium.

Each fix fails without itself:

  • Without the effort change, the three plain-repo cases fail: the effort goes from a value to blank.
  • Without the folder scoping, the two repo-config cases fail: the at-rest answer names gpt-5.5 at Medium, or openai/gpt-6 at High, where the first frame named nothing.
  • With the effort given to every chat that has no pick (the previous rule), the mobile-style client in the configured repo gets Medium where it got no effort before. That fails both the host-level and RPC-level checks.
  • With main's effort rule, the plain-repo mobile case fails: no effort.

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

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Ensure no issues in: Security, Cross-platoform support (Linux, Windows, Mac), Remote SSH, Mobile, general backwards compatibility, performance

  • Security: none. This only changes which already-known model and effort values are reported.
  • Cross-platform: folder paths come from the existing resolver.
  • SSH and remote hosts: the host that runs the chat answers for its own folder.
  • Mobile: gets the effort in plain folders. In a folder with its own agent config, it sees the same picker as main (see Compatibility).
  • Performance: each at-rest read now runs the same small project-config check the first frame's read already runs for that folder.

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, pnpm test, and pnpm build pass (or CI will cover; local preferred)

…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.
@brennanb2025 brennanb2025 changed the title fix(native-chat): a new chat's effort picker no longer blanks, then jumps, before the agent starts fix(native-chat): a new chat's model and effort pickers match their first frame before the agent starts Oct 10, 2026
…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.
…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.
@brennanb2025

Copy link
Copy Markdown
Contributor Author

Review summary

Reviews: three rounds. The last round found no P0, P1 or P2.

  • Round 1: the answer for a chat whose agent hasn't started ignored the chat's folder. In a repo with its own agent config, a new chat could show the account default's effort, then jump to the configured one at start. Fixed by scoping that answer to the chat's folder.
  • Round 2: released mobile and older desktop builds would have shown a wrong effort in such repos. Fixed with one rule in one place: the folder decides the default for every app. When the folder names none, an app that needs a model gets the account default with no effort, so it shows the same picker as main.
  • Round 3: cleanups. One shared "is this chat on this computer" check replaces three copies, and the PR description wording is tightened.
  • Each fix has a test that fails without it.

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.

  • Plain folder: on main, the effort picker goes blank before the agent starts, then fills in after the first send. With this PR it shows the same model and effort from the first frame through the start.
  • Folder with its own Codex/Pi config: on main, the picker shows the account default with a blank effort, then jumps at start. With this PR it stays on the placeholder until the start, then shows the configured value.
  • Real agent homes were hashed before and after; nothing changed.

CI: all 15 checks pass at faf3aae. The PR merges cleanly with current main.

Still out of scope, listed in the body:

  • After you pick a model before sending, the effort stays blank until the start.
  • The very first chat, before the model list has loaded, shows no picker.
  • The first Codex chat on an account shows the default before Orca has learned it.

Not verified: OpenCode and OMP live (they share Grok's code path), mobile, SSH/WSL hosts, and real agent CLIs.

@brennanb2025
brennanb2025 marked this pull request as ready for review October 10, 2026 10:25
@coderabbitai

coderabbitai Bot commented Oct 10, 2026

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 911e8161-1b48-4867-b7a1-b6ece965f988

📥 Commits

Reviewing files that changed from the base of the PR and between 9a175d6 and faf3aae.


📒 Files selected for processing (16)
  • config/scripts/vitest-sqlite-runtime-files.mjs
  • src/main/claude/claude-context-usage-unloaded-turn.test.ts
  • src/main/claude/claude-structured-effort-default-at-rest.test.ts
  • src/main/claude/claude-structured-location-support.ts
  • src/main/codex/codex-structured-launch-resolution.ts
  • src/main/codex/codex-structured-location-support.ts
  • src/main/native-chat/agent-model-catalog/agent-model-catalog-service.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-host-mutations.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-options-read.test.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-options-read.ts
  • src/main/native-chat/agent-session-wire/structured-agent-session-registered-definition.test.ts
  • src/main/runtime/orca-runtime-get-structured-agent-session-create-support.ts
  • src/main/runtime/rpc/methods/structured-agent-session-options-read.ts
  • src/main/runtime/rpc/methods/structured-agent-session-options-workspace-scope.test.ts
  • src/main/runtime/structured-agent-new-chat-effort-at-rest.test.ts
  • src/shared/execution-host.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 5 remain after this review.



📝 Walkthrough

Walkthrough

Structured session option reads now use a reader policy and can resolve model and effort defaults from the session workspace catalog. RPC handling passes the client’s model-optional capability into the read. A shared predicate now identifies native-local locations and is used by structured-location support, launch resolution, and workspace-path resolution. Tests cover workspace-scoped options and at-rest defaults across several agents.


Priority: ⬇️ Low

Merge Risk: ⚪ Minimal · up to faf3a

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 | Passed 4 | Failed 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 48.15% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 27 functions across 16 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: keeping new chats’ model and effort pickers consistent with their initial state before the agent starts.
Description check Passed The description is comprehensive and covers the user impact, implementation, rationale, compatibility, testing, visual proof, AI disclosure, and platform considerations. The missing issue link is expl…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@brennanb2025
brennanb2025 merged commit 7dfa4d6 into main Oct 11, 2026
56 checks passed
brennanb2025 added a commit that referenced this pull request Oct 15, 2026
…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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant