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 <noreply@anthropic.com>
This commit is contained in:
cnb 2026-03-07 15:34:21 +08:00
parent b85353502c
commit ee35f50550
2 changed files with 74 additions and 5 deletions

View File

@ -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");
});
});

View File

@ -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,