fix(desktop): model chip updates after a switch (session.info republish) - #818
Merged
Conversation
Selecting a different provider/model left the composer chip showing the OLD model — the picker marked the new one, the chip disagreed. Why: the chip reads the LIVE SESSION's model (PRIMARY_SESSION_VIEW.$model = primaryField(state.model, $currentModel)), not the composer draft. The renderer's own comment says the backend 'stamps model/provider on EVERY session.info' and that a switch 're-syncs every surface' by publishing one — but serve only ever emitted session.info at system/init, so the session state kept its spawn-time model forever. The optimistic $currentModel paint was shadowed by that stale session state. The picker/chip disagreement had a second cause: _init_session_info never sent , and currentPickerSelection only prefers the session's selection when BOTH model and provider are set — so the picker silently fell back to the catalog's (correct) current while the chip showed the session's (stale) model. Fixes: - _init_session_info now includes provider. - New DesktopSession.publish_session_info(): re-reads get_settings and broadcasts a full session.info (model/provider/effort/approval_mode). Awaited after config.set model/provider/effort, so the chip, picker and settings all reconcile to the REAL post-switch state — including a cross-provider switch that lands somewhere other than requested, or fails outright (no false 'switched' display). - Turn end schedules the same refresh. Scheduled, never awaited from the pump: the control response is routed BY the pump, so awaiting there would deadlock until the 30s timeout. Tasks are tracked and cancelled on shutdown. - Drive-by: import Any in desktop_audio (used in annotations). Verified live in the app on a real session: chip goes 'Deepseek V4 Flash' -> 'Deepseek Chat' on switch, and a failed cross-provider switch (expired Claude OAuth) correctly keeps showing the real model instead of lying. 4 new pytest cases incl. an explicit no-deadlock turn test; 69 desktop tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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
Selecting a different provider/model left the composer chip showing the old model — the picker marked the new one, the chip disagreed (screenshot: picker ✓ on GPT-5.6-luna, chip still "Deepseek V4 Pro").
Why
The chip reads the live session's model (
PRIMARY_SESSION_VIEW.$model = primaryField(state.model, $currentModel)), not the composer draft. The renderer's own comment states the backend "stamps model/provider on EVERY session.info" and that a switch "publishes session.info when it lands, and that is what re-syncs every surface" — but serve only emittedsession.infoatsystem/init, so the session state kept its spawn-time model forever. The optimistic$currentModelpaint was shadowed by that stale session state.The picker/chip disagreement had a second cause:
_init_session_infonever sentprovider, andcurrentPickerSelectiononly prefers the session's selection when both model and provider are set — so the picker fell back to the catalog's (correct) current while the chip showed the session's (stale) model.Fixes
_init_session_infonow includesprovider.DesktopSession.publish_session_info()— re-readsget_settingsand broadcasts a fullsession.info(model/provider/effort/approval_mode). Awaited afterconfig.setof model/provider/effort, so chip, picker and settings reconcile to the real post-switch state — including a cross-provider switch that lands elsewhere, or fails outright (no false "switched" display).Verified live (real session, real backend)
4 new pytest cases (provider in init info, switch publishes session.info, turn-end republish + explicit no-deadlock assertion); 69 desktop tests green.
🤖 Generated with Claude Code