From ee35f505503fe0c3964dd91007b55d9ade9912ae Mon Sep 17 00:00:00 2001 From: cnb Date: Sat, 7 Mar 2026 15:34:21 +0800 Subject: [PATCH] fix(tui): align resolveModelSelection priority with gateway Reorder conditions in resolveModelSelection so that modelOverride is checked before runtime model/modelProvider, matching the priority in resolveSessionModelRef (session-utils.ts). Without this change, a session with both a runtime model (from a previous run) and a modelOverride (from /model) would display the stale runtime model in the footer instead of the user's selection. Add a test case covering override precedence over runtime model. Co-Authored-By: Claude Opus 4.6 --- src/tui/tui-session-actions.test.ts | 65 +++++++++++++++++++++++++++++ src/tui/tui-session-actions.ts | 14 ++++--- 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/src/tui/tui-session-actions.test.ts b/src/tui/tui-session-actions.test.ts index e1e491e7ce1..8b6b490f06d 100644 --- a/src/tui/tui-session-actions.test.ts +++ b/src/tui/tui-session-actions.test.ts @@ -331,4 +331,69 @@ describe("tui session actions", () => { expect(state.sessionInfo.modelProvider).toBe("my-copilot"); expect(state.sessionInfo.contextTokens).toBe(200000); }); + + it("prefers modelOverride over runtime model in session entry", async () => { + // After /model switch, the entry has both a runtime model (from last run) + // and a modelOverride (user's intent for next run). The override should win. + const listSessions = vi.fn().mockResolvedValue({ + ts: Date.now(), + path: "/tmp/sessions.json", + count: 1, + defaults: {}, + sessions: [ + { + key: "agent:main:main", + model: "gpt-5.3-codex", + modelProvider: "openai-codex", + modelOverride: "claude-opus-4-6", + providerOverride: "anthropic", + updatedAt: 200, + }, + ], + }); + + const state: TuiStateAccess = { + agentDefaultId: "main", + sessionMainKey: "agent:main:main", + sessionScope: "global", + agents: [], + currentAgentId: "main", + currentSessionKey: "agent:main:main", + currentSessionId: null, + activeChatRunId: null, + historyLoaded: false, + sessionInfo: {}, + initialSessionApplied: true, + isConnected: true, + autoMessageSent: false, + toolsExpanded: false, + showThinking: false, + connectionStatus: "connected", + activityStatus: "idle", + statusTimeout: null, + lastCtrlCAt: 0, + }; + + const { refreshSessionInfo } = createSessionActions({ + client: { listSessions } as unknown as GatewayChatClient, + chatLog: { addSystem: vi.fn() } as unknown as import("./components/chat-log.js").ChatLog, + tui: { requestRender: vi.fn() } as unknown as import("@mariozechner/pi-tui").TUI, + opts: {}, + state, + agentNames: new Map(), + initialSessionInput: "", + initialSessionAgentId: null, + resolveSessionKey: vi.fn(), + updateHeader: vi.fn(), + updateFooter: vi.fn(), + updateAutocompleteProvider: vi.fn(), + setActivityStatus: vi.fn(), + }); + + await refreshSessionInfo(); + + // Override should take precedence over runtime model + expect(state.sessionInfo.model).toBe("claude-opus-4-6"); + expect(state.sessionInfo.modelProvider).toBe("anthropic"); + }); }); diff --git a/src/tui/tui-session-actions.ts b/src/tui/tui-session-actions.ts index eacc1ee7fa3..ddb224cae5a 100644 --- a/src/tui/tui-session-actions.ts +++ b/src/tui/tui-session-actions.ts @@ -118,17 +118,21 @@ export function createSessionActions(context: SessionActionContext) { entry?: SessionInfoEntry, defaults?: SessionInfoDefaults | null, ) => { + // 1. Explicit user override (set via /model) wins over last-run runtime model. + // This matches the priority in resolveSessionModelRef (session-utils.ts). + const overrideModel = entry?.modelOverride?.trim(); + if (overrideModel) { + const overrideProvider = entry?.providerOverride?.trim() || state.sessionInfo.modelProvider; + return { modelProvider: overrideProvider, model: overrideModel }; + } + // 2. Fall back to runtime model from last run. if (entry?.modelProvider || entry?.model) { return { modelProvider: entry.modelProvider ?? state.sessionInfo.modelProvider, model: entry.model ?? state.sessionInfo.model, }; } - const overrideModel = entry?.modelOverride?.trim(); - if (overrideModel) { - const overrideProvider = entry?.providerOverride?.trim() || state.sessionInfo.modelProvider; - return { modelProvider: overrideProvider, model: overrideModel }; - } + // 3. Configured defaults. if (defaults?.modelProvider || defaults?.model) { return { modelProvider: defaults.modelProvider ?? state.sessionInfo.modelProvider,