Desktop composer model selection reverting or not switching mid-session
Cluster 65335 · apps/desktop. Data as of
1 open item. Fix merged in #66603 on 20 Jul 2026. Check it covers your case.
The short answer
- Known?
- Yes. 6 related items grouped here, 3 issues already closed.
- Fix event
- A fix was merged to main in PR #66603. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
partially resolved, deciding PR #66603. That is separate from the fix event. - Still open
- 1 issue and 0 PRs.
Next step: read #66603, then check whether your report is the one still open.
Read #66603Pick up a Lab taskMost members describe the Desktop app's model/provider selection not persisting or not being changeable in an existing session. Several point to session.info gateway events overwriting the composer's choice, and a couple of the items appear to be fix attempts. Reports cover different session contexts (historical, stopped, resumed Telegram), so root causes may not be identical.
Open issues1
- Issue#65590Desktop: can't switch model/provider mid-session (regression ~v2026.7.7)
Open PRs0
Fixes on record
-
#66603 fix(desktop): stop session.info from overwriting composer model
files:
apps/desktop/src/app/session/hooks/use-message-stream/composer-model-event.test.tsx,apps/desktop/src/app/session/hooks/use-message-stream/gateway-event.tsShow the diff
-
apps/desktop/src/app/session/hooks/use-message-stream/composer-model-event.test.tsx@@ -0,0 +1,99 @@ +import { QueryClient } from '@tanstack/react-query' +import { act, cleanup, render, waitFor } from '@testing-library/react' +import { useEffect, useRef } from 'react' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import type { ClientSessionState } from '@/app/types' +import { createClientSessionState } from '@/lib/chat-runtime' +import { + $currentModel, + $currentProvider, + setCurrentModel, + setCurrentModelSource, + setCurrentProvider +} from '@/store/session' +import type { RpcEvent } from '@/types/hermes' + +import { useMessageStream } from './index' + +let handleEvent: ((event: RpcEvent) => void) | null = null + +function Harness({ activeSessionId }: { activeSessionId: string | null }) { + const sessionIdRef = useRef<string | null>(activeSessionId) + const sessionStateByRuntimeIdRef = useRef(new Map<string, ClientSessionState>()) + const queryClientRef = useRef(new QueryClient()) + + sessionIdRef.current = activeSessionId + + const stream = useMessageStream({ + activeSessionIdRef: sessionIdRef, + hydrateFromStoredSession: vi.fn(async () => undefined), + queryClient: queryClientRef.current, + refreshHermesConfig: vi.fn(async () => undefined), + refreshSessions: vi.fn(async () => undefined), + sessionStateByRuntimeIdRef, + updateSessionState: (sessionId, updater) => { + const current = sessionStateByRuntimeIdRef.current.get(sessionId) ?? createClientSessionState() + const next = updater(current) + sessionStateByRuntimeIdRef.current.set(sessionId, next) + + return next + } + }) + + useEffect(() => { + handleEvent = stream.handleGatewayEvent + }, [stream.handleGatewayEvent]) + + return null +} + +async function mountStream(activeSessionId: string | null) { + render(<Harness activeSessionId={activeSessionId} />) + await waitFor(() => expect(handleEvent).not.toBeNull()) +} + +describe('session.info does not clobber composer model selection', () => { + beforeEach(() => { + handleEvent = null + setCurrentModel('deepseek-v4-flash') + setCurrentProvider('deepseek') + setCurrentModelSource('manual') + }) + + afterEach(() => { + cleanup() + vi.restoreAllMocks() + setCurrentModel('') + setCurrentProvider('') + setCurrentModelSource('') + }) + + it('keeps a sticky manual pick when a global session.info carries the profile default', async () => { + await mountStream(null) + + act(() => + handleEvent!({ + payload: { model: 'deepseek-chat', provider: 'deepseek' }, + type: 'session.info' + }) + ) + + expect($currentModel.get()).toBe('deepseek-v4-flash') + expect($currentProvider.get()).toBe('deepseek') + }) + + it('keeps the composer pick when an unscoped session.info arrives with no live session', async () => { + await mountStream(null) + + act(() => + handleEvent!({ + payload: { cwd: '/tmp/project', model: 'deepseek-chat', provider: 'deepseek' }, + type: 'session.info' + }) + ) + + expect($currentModel.get()).toBe('deepseek-v4-flash') + expect($currentProvider.get()).toBe('deepseek') + }) +}) -
apps/desktop/src/app/session/hooks/use-message-stream/gateway-event.ts@@ -32,9 +32,7 @@ import { setCurrentBranch, setCurrentCwd, setCurrentFastMode, - setCurrentModel, setCurrentPersonality, - setCurrentProvider, setCurrentReasoningEffort, setCurrentServiceTier, setCurrentUsage, @@ -219,13 +217,12 @@ export function useGatewayEventHandler(deps: GatewayEventDeps) { } if (apply) { - if (modelChanged) { - setCurrentModel(payload!.model || '') - } - - if (providerChanged) { - setCurrentProvider(payload!.provider || '') - } + // Do not call setCurrentModel / setCurrentProvider here. Composer + // model/provider is sticky UI state (localStorage + manual picks). + // Periodic session.info heartbeats often carry the profile default + // (or a stale session model) and would silently revert the dropdown. + // Active-session model/provider still flows through the session state + // cache via updateSessionState → syncRuntimeMetadataToView below. if (typeof payload?.cwd === 'string') { // The active session's agent can relocate itself (new repo/worktree @@ -284,7 +281,7 @@ export function useGatewayEventHandler(deps: GatewayEventDeps) { // The running→busy transition must reach EVERY session, not just the // active one. The `apply` gate above correctly scopes view-only side - // effects (setCurrentModel, setCurrentCwd, etc.) to the focused chat, + // effects (setCurrentCwd, etc.) to the focused chat, // but the per-session busy state is what drives the sidebar working // indicator — a background session's turn start/finish must update // its dot without the user opening it. updateSessionState only
-
1 PR closed without merging
From the close comments: superseded by another PR 1
-
#102467 fix: Desktop: can't switch model/provider mid-session (regression ~v2026.7.7) (#65590) · superseded by another PR →
#102467,#65590
Closed issues · 3
- Closed#65335Desktop: Model selector instantly reverts when resuming Telegram session from sidebar
- Closed#65359[Bug]: Desktop cannot switch models in a stopped historical session even though the composer accepts messages
- Closed#66265Desktop: composer model selection overridden by session.info gateway event on every poll
How this cluster was grouped
-
Link evidence (
partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text. - Members outside that structural core may belong to a different underlying problem.
-
Pairwise link density
0.21: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Subsystem
- apps/desktop
- Merged PRs
- #66603
- Closed issues
- 3
- Attempts
- 1 PR closed without merging
- Fix link checked
- 20 Jul 2026