Skip to content

A new Pi chat names the model it will run on its first frame - #26821

Merged
brennanb2025 merged 5 commits into
mainfrom
brennanb2025/pi-learned-default
Oct 9, 2026
Merged

brennanb2025 merged 5 commits into
mainfrom
brennanb2025/pi-learned-default

Conversation

@brennanb2025

@brennanb2025 brennanb2025 commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

ELI5

When you open a new Pi chat without having picked a model, the model pill first says "Model" and then, a few seconds later, changes to the model Pi actually started on (for example "GPT-6 · high"). That jump happens on every new Pi chat. Claude, Grok, OpenCode and OMP stopped doing this in #26407: once one chat has started with no pick, Orca remembers which model the agent runs by default and shows it on the next new chat straight away. Pi was left out of that change (listed there as temporary). This PR gives Pi the same behaviour.

What Changed

What you see

  • Before: every new Pi chat with no remembered model pick showed the plain "Model" placeholder, then switched to the model and thinking level Pi reported once its session started.
  • After: the first such Pi chat still shows "Model" until Pi reports (nothing is known yet). After that, every new Pi chat shows that model and thinking level on its very first frame, with no intermediate label, in any workspace whose own Pi settings don't choose a model.
  • A workspace whose .pi/settings.json picks a model or thinking level, or that has project extensions (which can choose a model themselves), keeps the "Model" placeholder until its chat reports, as before. Orca can't know what that project's settings resolve to, so it doesn't guess.
  • A signed-out Pi (one that lists no models) teaches nothing and doesn't erase what an earlier chat taught.
  • A model that changes in the middle of a chat (one you pick, or one a Pi extension switches to) is never learned as the default. Only the model the chat started on counts.
  • A learned xhigh or max thinking level stays named after Orca re-lists Pi's models in the background, even though Pi's model listing only shows levels up to high. The "Pi isn't signed in" notice for that chat (Show "Pi isn't signed in" in native chat before the first send #26743) is unchanged.

Mechanism

  • One shared rule. The rule for "what does a chat started with no pick tell us about the configured default" lived inside the ACP code (Grok, OpenCode, OMP). It now sits beside the catalog's entry types (unpickedSessionConfiguredChoice), and both ACP and Pi call it. The rule: only a new session (not one resumed or forked from a saved conversation) that restored no model pick and was sent none reports the model it runs; it reports the thinking level too unless that was picked. It reports nothing when the agent runs no model (fix(acp): start chats for agents that report no model #26587's no-model chats). It also reports nothing when the agent lists no models. That check is only a safeguard: the host never saves an empty listing, so it never learned or forgot a default from one before this PR either, and Grok/OpenCode/OMP behave exactly as before.
  • Pi reports through the same host step, from its start only. When a new Pi session (no --session / --fork) finishes starting and restoring the chat's saved picks, it applies the shared rule once to what Pi then reports, and keeps that answer. Every later options read hands the host that same answer alongside the current model list, exactly as Grok/OpenCode/OMP report theirs. A model changed later in the chat, by a pick or by an extension calling Pi's setModel, can't change it. The host saves it as the account's configured default only after the workspace check below passes, so the next new chat's saved-list read already names it.
  • Pi's project config is parsed, not just found. Pi reads project settings only from .pi/settings.json in the chat's folder (Orca checks from the folder up to its repository root, a wider check than Pi's own). It counts as the project choosing the model if it sets defaultProvider, defaultModel, defaultThinkingLevel, modelThinkingLevels or enabledModels (Pi starts on the first enabledModels match), lists extensions or packages, or can't be parsed. A non-empty .pi/extensions/ folder also counts, because extension code can register providers or set the model. Settings for permissions, tools, themes and so on don't count. Key names come from Pi 1.0.4's own settings manager, startup model code (findInitialModel) and settings docs, read from the installed package without running Pi. Pi's own account folder (PI_CODING_AGENT_DIR) is account config, not a project layer.
  • A learned thinking level survives a coarser re-list. The saved list takes each model's effort menu from the newer of its two listings. Pi's --list-models menu stops at high; a chat's own menu adds xhigh/max for models whose thinking map allows them. When the learned effort is only in the other listing's menu, the saved list now keeps that menu, so a background re-list can't drop it (before, the first frame fell back to no named effort). This is in the shared merge, but only changes anything when a newer menu lacks a learned effort.
  • Nothing changes on the wire or on the phone: the catalog answer already carries the learned default, and every client already reads it.

Why

This reuses #26407's single path instead of adding a Pi-specific one: the same no-pick rule, the same host save step, the same project-config gate and the same saved-list preload. A Pi-only variant would have been a second copy of the rule to keep in sync. Pi's model listing (--list-models) can't be the answer because it marks no model as the default, and Pi's startup choice depends on account settings, saved credentials and environment API keys that only Pi resolves, so learning it from a chat Pi itself started is the only source that matches what Pi runs.

Orca never sends Pi a model it didn't pick: picks go through Pi's RPC set_model, which (per Pi's source) doesn't rewrite Pi's saved default, so Orca's own picks never change what Pi starts on.

Differences from the common pattern (the composer draws its pills once, from data the client already holds)

  • Intended: the default is learned from the first chat started with no pick rather than read from a listing, because Pi's listing names no default. Safe: before any such chat the pill is the quiet placeholder, never a guess.
  • Intended: a project with Pi extensions or a packages list keeps the placeholder even if its extensions never touch the model. Safe: that is today's behaviour; the cost is one transition in those projects, never a wrong model name.
  • Temporary (same as Show model and effort choices for every agent before its session starts #26407 for OpenCode/OMP/Claude): a new Pi chat in a worktree the app hasn't read this run shows "Model" until the host answers its workspace check. Follow-up: load the workspace check for open worktrees with the saved-only read.
  • Temporary: Pi's project trust isn't modelled; an untrusted project's .pi/settings.json (which Pi ignores) still keeps the placeholder. Follow-up: read Pi's trust store if this shows up in practice.
  • Intended: Pi learns only from what a chat started on, so a model switched to later (by a pick or an extension) is never learned.
  • Temporary: Grok, OpenCode and OMP still derive the learned default from the session's current state on each read, so a model their agent switches to on its own mid-chat could be learned. Follow-up: take the same start-only snapshot in the shared ACP options code.

Linked Issue

Follow-up to #26407 (its "Pi names no model until a chat reports one" temporary item). No separate issue.

Visual Proof

Pending coordinator QA. A hidden capture needs a built app plus a stand-in Pi on an isolated profile, which this lane didn't run. The before/after is the same as #26407's Grok/OpenCode/OMP case: the second new Pi chat should open showing the model and thinking level the first one ran, with no "Model" frame.

Testing

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

New and updated tests, run by file name (macOS):

  • src/main/pi/rpc-configured-default.test.ts (new, runs on the Node runtime): through Pi's real listing parser, session adapter, catalog store and service, with a scripted Pi child. Pi's listing names nothing (placeholder); a chat with no pick runs openai/gpt-6 at high (not the listing's first row); the next new chat's first frame, in that workspace and another, names openai/gpt-6 · high. A workspace whose .pi/settings.json picks a model keeps the placeholder; a chat in a workspace with .pi/extensions teaches nothing; a chat that restored a model pick, resumed a conversation or was forked from one teaches nothing; a chat whose model an extension switches after start, or where you pick another model, teaches the model it started on; a learned xhigh survives a newer --list-models re-list; a signed-out Pi (empty model list) saves nothing and doesn't erase a learned default.

  • agent-project-model-override.test.ts: each Pi key, extensions, invalid JSON and .pi/extensions count; a BOM-prefixed settings file that sets only tools/theme, another agent's settings, and Pi's own account folder don't.

  • use-structured-agent-session-options.learned-default.test.tsx: adds Pi to the renderer case (second new chat's first frame names the model and effort, no intermediate frame).

  • Fail without the change, checked by putting back the old source and running the tests:

    • Pi learning at all (adapter/session and override sources restored to main): the learned-default case, the signed-out case's "doesn't erase" half (it fails on main only because Pi learned nothing there; the empty-list safeguard itself isn't what it tests), and the "ignores Pi settings that pick no model" override case.
    • Start-only snapshot (Pi session restored to the earlier commit that re-read the current state): the extension-switch case and the pick-in-chat case. Both also fail with that code minus its pick tracking.
    • Forked chats: the forked case fails if a forked launch counts as a new session.
    • Effort menu: the xhigh case fails with the merge restored (no effort named).
    • The other new cases guard against the fix being too broad and pass either way. That includes the renderer Pi case and the configured-workspace case, which pass on main.
  • Also passing: ACP options, ACP host configured-default, OpenCode/OMP learned-default, Pi adapter, options, capture replay, registration, probe, turn races, prompt delivery, launch, catalog service/store/unavailable/prewarm, first-frame, Claude/Codex configured-default and effort-at-rest, discovery freshness and catalog-application tests (29 files, 315 tests), and the SQLite runtime boundary test.

  • pnpm tc:node, pnpm tc:web, mobile pnpm typecheck and test-typecheck ratchet, and oxlint/oxfmt on changed files pass. orca-ci-checks: 22 of 23 pass; the one failure is audit:code-quality:native, the existing import-cycle warnings in mobile/src/transport and mobile/src/terminal/document (no file of this PR is involved).

  • CI on b2dfc088715: all 16 run checks pass (static analysis and typecheck, all 5 test shards, relay integration, runtime on macOS/Ubuntu/Windows, packaging); 17 skipped as not selected for this change.

Not verified: the running app, a real Pi CLI, SSH/WSL hosts beyond the shared code paths (the host step and workspace check already run on the execution host).

AI Disclosure

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

  • Remote/SSH: the learned default and the workspace check run on the machine that runs Pi, as for the other agents. No new wire fields.
  • Mixed versions: older clients already read the learned default from the catalog answer; older hosts simply never learn one for Pi.
  • Performance: one extra readdir and one small file read per workspace check for Pi.

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)

…s configured default

ACP's rule (only a new session that restored no model pick and was sent none
runs its config's default; an effort it picked says nothing) moves beside the
catalog entry types so other agents' adapters report through the same step.
An empty listing (a signed-out agent) now says nothing instead of "no listed
model", so it can never retire a saved default.
…t model

Pi reads `.pi/settings.json` in the chat's folder and loads project
extensions from `.pi/extensions` (and the `extensions`/`packages` lists).
A settings file that sets defaultProvider, defaultModel,
defaultThinkingLevel, modelThinkingLevels or enabledModels, loads extension
code, or can't be parsed counts as the project picking the model; extension
code may register providers or set the model itself. Key names come from Pi
1.0.4's own settings manager and docs, read without running Pi.
…i starts on

Pi's model listing marks no model as the one it runs, so a new Pi chat with no
saved pick showed "Model" and then changed to the model its session reported.
A new Pi session (not one resumed or forked from a saved conversation) that
restored no model pick and was sent none now reports the model and, unless it
picked one, the thinking level it started on, through the same host step as
Grok, OpenCode and OMP. The host saves them as the account's default behind
the project-config check, so the next new Pi chat names them on its first
frame. A signed-out Pi, which lists no model, saves nothing.
…t ran

The saved list takes each model's effort menu from the newer listing. Pi's
`--list-models` menu stops at high, while a chat's own menu adds xhigh or max
for models whose thinking map allows them. After a background re-list, a
learned xhigh was no longer in the menu, so the first frame dropped it. When
the configured effort is only in the other listing's menu, that menu is kept.
…rted on

A Pi chat re-derived its configured default from Pi's current state on every
options read, so a model an extension switched to mid-chat (extensions can
call setModel) was saved as the account default, and the next new chat named
the wrong model. The choice is now computed once, after start-up restores the
chat's saved picks, and every later read reports that snapshot. A pick made in
the chat no longer matters to it. Tests cover an extension switch, a pick made
in the chat, a forked chat, and a learned xhigh surviving a newer re-list.
@brennanb2025
brennanb2025 marked this pull request as ready for review October 9, 2026 20:00
@brennanb2025
brennanb2025 merged commit 8defd66 into main Oct 9, 2026
57 checks passed
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