Files
orca/src/shared/model-alias-rows.ts
T
Brennan Benson 61c069aac0 Keep a Claude chat's effort changeable after its first reply (#38272)
* fix(native-chat): keep a Claude chat's effort changeable after its first turn

After its first turn a Claude chat saves the model id Claude reports it ran
(claude-opus-5-5), while the model list names that model only by its alias
(opus). Every capability lookup matched on the row id, so the chat's model
had no effort choices and effort turned into display-only text on desktop
and phone.

A listed row now describes a model id by its own id or by the model it
resolves to. The match supplies capabilities only; the chat keeps showing and
sending the exact model it ran, so it never drifts when the alias moves. The
host applies the same rule to the row it adds for the chat's model, for
clients that read the answer's models.

* test(native-chat): cover exact-row precedence for a described model id

* fix(native-chat): remember an after-reply effort without changing the new-chat model

An effort pick in a chat on the resolved id it ran is remembered under the
listed row that runs that model and leaves the new-chat model untouched, as
the common pattern keeps the sticky model and updates only its options. The
resolved id is never stored as a default.

* refactor(native-chat): move the remembered-pick model choice into its own module

* fix(native-chat): keep a held effort pick from adopting a new-chat model

* test(native-chat): run the held-effort case as a Claude launch

* revert(native-chat): drop the host-side capability copy no live answer can use

Claude's live session listing carries no resolvedModel, so the host never matched there, and phones draw the host-served chat bundle, so every client already runs the shared lookup.

* Key Claude's model rows by the model each one runs

Claude's listing named rows by alias (opus), while a chat saves the model
it ran (claude-opus-5-5) after its first turn, so equivalent chats held two
kinds of model value and a chat's effort could go read-only. Rows are now
keyed by the versioned model they run, the common pattern: a pick saves the
same id the CLI reports. Aliases stay as their own rows with their own
effort levels; the picker lists one only while the chat is on it or a
search matches it, and the phone list leaves them out. A saved alias is
kept as the user picked it. A configured alias default marks the versioned
row it runs, and an effort picked in an existing or resumed chat no longer
changes the model new chats start on, whatever ids the chat and listing use.

* test: give the delayed-report alias test's timeout its reason

* Remember a new chat's picks by its first turn, and keep aliases reachable everywhere

- A chat counts as existing once it has a turn or a send, or when it was
  opened rather than launched here, not once its launch published: an empty
  new chat's effort pick adopts its model for new chats again, and every
  agent's existing chat keeps the remembered new-chat model.
- Only a row that names itself as the model it runs covers an alias, so an
  older host's alias rows stay listed beside a chat's stand-in row.
- The phone lists an alias of a listed model behind a More models toggle.
- The terminal-chat and commit-message model lists carry each row's
  resolvedModel, so they list aliases the same way; an older remote that
  omits it keeps its full list.

* fix(mobile): keep the More models toggle's state without an effect reset

* Keep commit-message aliases selectable and their resolved model out of saved settings

The Source Control model Select has no search, so it lists alias rows after
the versioned ones instead of hiding them. Discovered models are saved
without an alias row's resolvedModel, and the refresh compares that same
saved shape, so the settings cache gains no field.

* Drop an unrelated audit-tools lockfile change; reset the phone's More models toggle when its drawer closes

* test: build the saved-models test's settings without a cast
2026-11-05 00:43:23 -08:00

13 lines
551 B
TypeScript

type ListedRow = { id: string; resolvedModel?: string | null }
/** An alias row whose versioned row the same list holds: reachable by search, not listed beside it.
* Only a row naming itself as the model it runs counts; a chat's stand-in row for its own id does not. */
export function isCoveredModelAlias(model: ListedRow, models: readonly ListedRow[]): boolean {
const resolved = model.resolvedModel
return (
!!resolved &&
resolved !== model.id &&
models.some((row) => row.id === resolved && row.resolvedModel === resolved)
)
}