From 52bc8405d8479ea892fb7c8b2b1167a149a6c38d Mon Sep 17 00:00:00 2001 From: DatTheMaster Date: Wed, 8 Jul 2026 19:03:52 -0500 Subject: [PATCH 1/2] fix: session-scoped model switches persist globally The TUI and Desktop model pickers send model changes without the --session flag that parse_model_flags() respects, causing session-only switches to write to config.yaml instead of being scoped to the current session. - TUI: modelValueForConfigSet() was stripping --tui-session to a bare model name. Now converts it to --session before sending. - Desktop: config.set value now includes --session suffix. Fixes #61190 --- apps/desktop/src/app/session/hooks/use-model-controls.ts | 2 +- ui-tui/src/app/slash/commands/session.ts | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/app/session/hooks/use-model-controls.ts b/apps/desktop/src/app/session/hooks/use-model-controls.ts index dba30dd8d1401..b302a5fa45546 100644 --- a/apps/desktop/src/app/session/hooks/use-model-controls.ts +++ b/apps/desktop/src/app/session/hooks/use-model-controls.ts @@ -96,7 +96,7 @@ export function useModelControls({ activeSessionId, queryClient, requestGateway await requestGateway('config.set', { session_id: activeSessionId, key: 'model', - value: `${selection.model} --provider ${selection.provider}` + value: `${selection.model} --provider ${selection.provider} --session` }) void queryClient.invalidateQueries({ queryKey: ['model-options', activeSessionId] }) diff --git a/ui-tui/src/app/slash/commands/session.ts b/ui-tui/src/app/slash/commands/session.ts index 6b1fe55481bc2..35eb3c36aa483 100644 --- a/ui-tui/src/app/slash/commands/session.ts +++ b/ui-tui/src/app/slash/commands/session.ts @@ -32,7 +32,10 @@ const modelValueForConfigSet = (arg: string) => { } if (TUI_SESSION_MODEL_RE.test(trimmed)) { - return stripTuiSessionFlag(trimmed) + // Convert --tui-session → --session so the gateway's parse_model_flags() + // recognises the intent as session-scoped instead of persisting globally. + const stripped = stripTuiSessionFlag(trimmed) + return `${stripped} --session`.trim() } return trimmed From 628ccfcd896f9248fa85ddf9e4d1e6ba76ec297d Mon Sep 17 00:00:00 2001 From: DatTheMaster Date: Fri, 10 Jul 2026 09:35:11 -0500 Subject: [PATCH 2/2] test: update model-picker assertions for --session + add persistence regressions The scope fix appends --session to the config.set value in both the Desktop hook (use-model-controls.ts) and the TUI slash path (session.ts), but the existing assertions still expected the pre-fix value, so the Desktop suite and the TUI picker test failed on this branch. - desktop: expect the config.set value to include --session - tui: expect the rewritten --tui-session -> --session value - add persistence regression tests: the switch is session-scoped (--session present, --global absent) and the raw --tui-session sentinel is never forwarded to the gateway --- .../session/hooks/use-model-controls.test.tsx | 24 +++++++++++++- .../src/__tests__/createSlashHandler.test.ts | 32 +++++++++++++++++-- 2 files changed, 53 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx b/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx index 92888b4780dfc..30c2d64c61584 100644 --- a/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx +++ b/apps/desktop/src/app/session/hooks/use-model-controls.test.tsx @@ -127,11 +127,33 @@ describe('useModelControls', () => { expect(requestGateway).toHaveBeenCalledWith('config.set', { session_id: 'session-1', key: 'model', - value: 'claude-sonnet-4.6 --provider anthropic' + value: 'claude-sonnet-4.6 --provider anthropic --session' }) expect(requestGateway).not.toHaveBeenCalledWith('slash.exec', expect.anything()) }) + it('keeps an active-session picker change session-scoped, never persisting to global config', async () => { + const requestGateway = vi.fn(async () => ({ key: 'model', value: 'claude-sonnet-4.6' }) as never) + let controls!: Controls + + render( + (controls = value)} requestGateway={requestGateway} /> + ) + + await controls.selectModel({ model: 'claude-sonnet-4.6', provider: 'anthropic' }) + + // Regression: the switch MUST carry --session so parse_model_flags() scopes + // it to the session instead of writing the profile default in config.yaml. + expect(requestGateway).toHaveBeenCalledWith( + 'config.set', + expect.objectContaining({ value: expect.stringContaining('--session') }) + ) + expect(requestGateway).toHaveBeenCalledWith( + 'config.set', + expect.objectContaining({ value: expect.not.stringContaining('--global') }) + ) + }) + it('stores a no-session pick as UI state with no gateway or global write', async () => { const requestGateway = vi.fn() let controls!: Controls diff --git a/ui-tui/src/__tests__/createSlashHandler.test.ts b/ui-tui/src/__tests__/createSlashHandler.test.ts index ca1af4cd9abed..d2862660bb562 100644 --- a/ui-tui/src/__tests__/createSlashHandler.test.ts +++ b/ui-tui/src/__tests__/createSlashHandler.test.ts @@ -192,7 +192,7 @@ describe('createSlashHandler', () => { expect(ctx.gateway.rpc).not.toHaveBeenCalled() }) - it('honors TUI picker session scope without adding --global', async () => { + it('rewrites the TUI picker sentinel to --session so the switch stays session-scoped', async () => { patchUiState({ sid: 'sid-abc' }) const ctx = buildCtx({ @@ -209,10 +209,38 @@ describe('createSlashHandler', () => { confirm_expensive_model: false, key: 'model', session_id: 'sid-abc', - value: 'anthropic/claude-sonnet-4.6 --provider openrouter' + value: 'anthropic/claude-sonnet-4.6 --provider openrouter --session' }) }) + it('never forwards the raw TUI sentinel and scopes the pick to the session', async () => { + patchUiState({ sid: 'sid-abc' }) + + const ctx = buildCtx({ + gateway: { + ...buildGateway(), + rpc: vi.fn(() => Promise.resolve({ value: 'anthropic/claude-sonnet-4.6' })) + } + }) + + createSlashHandler(ctx)(`/model anthropic/claude-sonnet-4.6 --provider openrouter ${TUI_SESSION_MODEL_FLAG}`) + + // Regression: the UI-only sentinel must be rewritten, and the result must be + // session-scoped — not leaked to the gateway and not persisted globally. + expect(ctx.gateway.rpc).toHaveBeenCalledWith( + 'config.set', + expect.objectContaining({ value: expect.not.stringContaining(TUI_SESSION_MODEL_FLAG) }) + ) + expect(ctx.gateway.rpc).toHaveBeenCalledWith( + 'config.set', + expect.objectContaining({ value: expect.stringContaining('--session') }) + ) + expect(ctx.gateway.rpc).toHaveBeenCalledWith( + 'config.set', + expect.objectContaining({ value: expect.not.stringContaining('--global') }) + ) + }) + it('does not duplicate --global for explicit persistent model switches', () => { patchUiState({ sid: 'sid-abc' }) const ctx = buildCtx()