fix(tui): prevent stale model indicator after /model

This commit is contained in:
Vignesh Natarajan
2026-03-05 17:39:19 -08:00
parent d326861eb4
commit cec5535096
3 changed files with 163 additions and 6 deletions

View File

@@ -111,4 +111,162 @@ describe("tui session actions", () => {
expect(updateFooter).toHaveBeenCalledTimes(2);
expect(requestRender).toHaveBeenCalledTimes(2);
});
it("keeps patched model selection when a refresh returns an older snapshot", async () => {
const listSessions = vi.fn().mockResolvedValue({
ts: Date.now(),
path: "/tmp/sessions.json",
count: 1,
defaults: {},
sessions: [
{
key: "agent:main:main",
model: "old-model",
modelProvider: "ollama",
updatedAt: 100,
},
],
});
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: {
model: "old-model",
modelProvider: "ollama",
updatedAt: 100,
},
initialSessionApplied: true,
isConnected: true,
autoMessageSent: false,
toolsExpanded: false,
showThinking: false,
connectionStatus: "connected",
activityStatus: "idle",
statusTimeout: null,
lastCtrlCAt: 0,
};
const { applySessionInfoFromPatch, 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(),
});
applySessionInfoFromPatch({
key: "agent:main:main",
entry: {
sessionId: "session-1",
model: "new-model",
modelProvider: "openai",
updatedAt: 200,
},
});
expect(state.sessionInfo.model).toBe("new-model");
expect(state.sessionInfo.modelProvider).toBe("openai");
await refreshSessionInfo();
expect(state.sessionInfo.model).toBe("new-model");
expect(state.sessionInfo.modelProvider).toBe("openai");
expect(state.sessionInfo.updatedAt).toBe(200);
});
it("accepts older session snapshots after switching session keys", async () => {
const listSessions = vi.fn().mockResolvedValue({
ts: Date.now(),
path: "/tmp/sessions.json",
count: 1,
defaults: {},
sessions: [
{
key: "agent:main:other",
model: "session-model",
modelProvider: "openai",
updatedAt: 50,
},
],
});
const loadHistory = vi.fn().mockResolvedValue({
sessionId: "session-2",
messages: [],
});
const state: TuiStateAccess = {
agentDefaultId: "main",
sessionMainKey: "agent:main:main",
sessionScope: "global",
agents: [],
currentAgentId: "main",
currentSessionKey: "agent:main:main",
currentSessionId: null,
activeChatRunId: null,
historyLoaded: true,
sessionInfo: {
model: "previous-model",
modelProvider: "anthropic",
updatedAt: 500,
},
initialSessionApplied: true,
isConnected: true,
autoMessageSent: false,
toolsExpanded: false,
showThinking: false,
connectionStatus: "connected",
activityStatus: "idle",
statusTimeout: null,
lastCtrlCAt: 0,
};
const { setSession } = createSessionActions({
client: {
listSessions,
loadHistory,
} as unknown as GatewayChatClient,
chatLog: {
addSystem: vi.fn(),
clearAll: 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((raw?: string) => raw ?? "agent:main:main"),
updateHeader: vi.fn(),
updateFooter: vi.fn(),
updateAutocompleteProvider: vi.fn(),
setActivityStatus: vi.fn(),
});
await setSession("agent:main:other");
expect(loadHistory).toHaveBeenCalledWith({
sessionKey: "agent:main:other",
limit: 200,
});
expect(state.currentSessionKey).toBe("agent:main:other");
expect(state.sessionInfo.model).toBe("session-model");
expect(state.sessionInfo.modelProvider).toBe("openai");
expect(state.sessionInfo.updatedAt).toBe(50);
});
});

View File

@@ -151,17 +151,12 @@ export function createSessionActions(context: SessionActionContext) {
const entryUpdatedAt = entry?.updatedAt ?? null;
const currentUpdatedAt = state.sessionInfo.updatedAt ?? null;
const modelChanged =
(entry?.modelProvider !== undefined &&
entry.modelProvider !== state.sessionInfo.modelProvider) ||
(entry?.model !== undefined && entry.model !== state.sessionInfo.model);
if (
!params.force &&
entryUpdatedAt !== null &&
currentUpdatedAt !== null &&
entryUpdatedAt < currentUpdatedAt &&
!defaultsChanged &&
!modelChanged
!defaultsChanged
) {
return;
}
@@ -362,6 +357,9 @@ export function createSessionActions(context: SessionActionContext) {
state.currentSessionKey = nextKey;
state.activeChatRunId = null;
state.currentSessionId = null;
// Session keys can move backwards in updatedAt ordering; drop previous session freshness
// so refresh data for the newly selected session isn't rejected as stale.
state.sessionInfo.updatedAt = null;
state.historyLoaded = false;
clearLocalRunIds?.();
updateHeader();