fix(tui): repoint the provider label on a model switch, and pick effort in /model - #836
Merged
Merged
Conversation
…rt in /model The stats line renders `provider · model` as one phrase, but the provider half was written exactly once — from the backend's init frame — while every switch patched `model` alone. A cross-provider selection therefore showed the new model beside the old provider (`anthropic · gpt-5.6-luna`). Three layers each dropped it, and the reported case hit all three: the `set_model` reply omitted `provider` (the fusion and `set_provider` paths already echoed it), `applyModel` discarded it even when sent, and both UI apply sites spread `...info` and overwrote only `model`. On the cross-provider path the backend had already answered `set_provider` with the new provider and the client threw it away. Threaded end to end, with one shared helper so the two apply sites cannot drift again. An absent provider means "unchanged", so a same-provider switch never blanks a still-correct label. /model also grows a third step. Provider and model are now followed by the effort levels that model actually accepts, resolved from the same predicates that gate the wire: sonnet-4-6 is offered no `xhigh` (it 400s on it, and a 400 on the effort level is retried nowhere), OpenAI reasoning models no `max`, and providers with no per-model table keep the full ladder. A model with no ladder at all — haiku, gpt-4o — keeps the two-step flow instead of being shown a list whose every row is a silent no-op, as does the new-prompt-session picker, which captures a draft and has no session to set an effort on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Test Results 4 files 966 suites 33m 32s ⏱️ Results for commit 2a32ff4. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
The session-stats line renders
provider · modelas one phrase, but the provider half was written exactly once — from the backend'sinitframe — while every model switch patchedmodelalone. A cross-provider selection showed the new model beside the old provider:Three layers each dropped the provider, and the reported case hit all three:
_do_set_modelreplied{ok, model}with noprovider— while both sibling paths,_do_set_fusion_modeland_do_set_provider, already echoed it.applyModelreturned only{value, warning}, discarding it even when sent.session.tsanduseMainApp.tsboth did{ ...state.info, model }, never touchingprofile_name.On the cross-provider path the backend had already answered
set_providerwith{ok: true, provider: "openai"}— it said where the session moved, and the client threw the answer away.Fix: threaded end to end, with one shared helper (
domain/modelSwitch.ts) so the two apply sites cannot drift again. An absent provider means "unchanged", not "unknown", so a same-provider switch never blanks a still-correct label. The/modelpicker re-enters/model <value>, so it rides the same path./modelgains a third stepProvider → model → effort, offering the levels that model actually accepts. The ladder is resolved from the same predicates that gate the wire, so the list is what the model will really take rather than the union ladder
/effortvalidates against:claude-opus-5,claude-opus-4-8claude-sonnet-4-6claude-haiku-4-5,gpt-4o,gpt-5-chat-latestgpt-5.6-luna(OpenAI reasoning)The asymmetry drives the design: offering a level the model rejects is fatal (a 400 on the effort level is retried or downgraded nowhere, so every subsequent request fails), while omitting one merely hides a choice. So it errs narrow wherever the codebase actually knows, and falls back to the full ladder only where it does not.
Design decisions worth flagging for review:
allowEffortStep={false}), which captures a draft and has no session to set an effort on.autois the picker's own row, not a backend level — it means "clear the override", and emits no/effortat all./effortalready is; the^gglobal toggle stays model-only.list_model_providers— the ladder is a property of the model, and some providers enumerate hundreds.Verification
effort_optionscontrol, the RPC mapping, the picker's stage transitions (real keys through a mounted component), and the command expansion.step 2/3was already in the buffer from the earlier visit and the assertion proved nothing. Now asserts the last painted frame.origin/main— baseline captured and compared, zero new. Note ui-tui has no CI gate; that suite was run locally.🤖 Generated with Claude Code