fix(tui): show configured default model instead of "unknown" for new sessions
Two issues caused the TUI status bar to show "unknown" or a stale model: 1. `resolveModelSelection` in tui-session-actions did not receive the `defaults` parameter from `applySessionInfo`. When a session has no entry in the store yet (brand-new, never-run session), both `entry` and `state.sessionInfo` are empty, so the footer fell back to `undefined` → "unknown". Now `defaults` (containing the configured agent default model) is passed through and used as a fallback. 2. `resolveSessionModelRef` in session-utils preferred the last-run runtime `model/modelProvider` over the explicit `modelOverride/ providerOverride`. After `/model` switches back to default (which clears the override), the stale runtime model was still returned. The priority is now: override → runtime → configured defaults. Supersedes: #27735 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a5c07fa115
commit
b85353502c
@ -330,7 +330,7 @@ describe("gateway session utils", () => {
|
||||
});
|
||||
|
||||
describe("resolveSessionModelRef", () => {
|
||||
test("prefers runtime model/provider from session entry", () => {
|
||||
test("prefers override over runtime model when both are present", () => {
|
||||
const cfg = createModelDefaultsConfig({
|
||||
primary: "anthropic/claude-opus-4-6",
|
||||
});
|
||||
@ -344,7 +344,8 @@ describe("resolveSessionModelRef", () => {
|
||||
providerOverride: "anthropic",
|
||||
});
|
||||
|
||||
expect(resolved).toEqual({ provider: "openai-codex", model: "gpt-5.3-codex" });
|
||||
// Override reflects user intent for the *next* run and wins over last-run runtime model.
|
||||
expect(resolved).toEqual({ provider: "anthropic", model: "claude-opus-4-6" });
|
||||
});
|
||||
|
||||
test("preserves openrouter provider when model contains vendor prefix", () => {
|
||||
@ -413,6 +414,38 @@ describe("resolveSessionModelRef", () => {
|
||||
|
||||
expect(resolved).toEqual({ provider: "anthropic", model: "claude-sonnet-4-6" });
|
||||
});
|
||||
|
||||
test("preserves providerOverride for slash-containing modelOverride (wrapper providers)", () => {
|
||||
const cfg = createModelDefaultsConfig({
|
||||
primary: "anthropic/claude-opus-4-6",
|
||||
});
|
||||
|
||||
const resolved = resolveSessionModelRef(cfg, {
|
||||
sessionId: "s-wrapper",
|
||||
updatedAt: Date.now(),
|
||||
modelOverride: "anthropic/claude-sonnet-4-6",
|
||||
providerOverride: "openrouter",
|
||||
});
|
||||
|
||||
// providerOverride should be preserved as-is; re-parsing the model string
|
||||
// would incorrectly treat "anthropic" as the provider.
|
||||
expect(resolved).toEqual({ provider: "openrouter", model: "anthropic/claude-sonnet-4-6" });
|
||||
});
|
||||
|
||||
test("uses runtime model when no override is set", () => {
|
||||
const cfg = createModelDefaultsConfig({
|
||||
primary: "anthropic/claude-opus-4-6",
|
||||
});
|
||||
|
||||
const resolved = resolveSessionModelRef(cfg, {
|
||||
sessionId: "s-runtime-only",
|
||||
updatedAt: Date.now(),
|
||||
modelProvider: "openai-codex",
|
||||
model: "gpt-5.3-codex",
|
||||
});
|
||||
|
||||
expect(resolved).toEqual({ provider: "openai-codex", model: "gpt-5.3-codex" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveSessionModelIdentityRef", () => {
|
||||
|
||||
@ -639,10 +639,33 @@ export function resolveSessionModelRef(
|
||||
defaultModel: DEFAULT_MODEL,
|
||||
});
|
||||
|
||||
// Prefer the last runtime model recorded on the session entry.
|
||||
// This is the actual model used by the latest run and must win over defaults.
|
||||
// Prefer explicit per-session override (set via /model or at spawn time).
|
||||
// This reflects the user's intent for the next run and should take precedence
|
||||
// over the runtime model recorded from a previous run.
|
||||
let provider = resolved.provider;
|
||||
let model = resolved.model;
|
||||
const storedModelOverride = entry?.modelOverride?.trim();
|
||||
if (storedModelOverride) {
|
||||
const explicitOverrideProvider = entry?.providerOverride?.trim();
|
||||
if (explicitOverrideProvider) {
|
||||
// Provider is explicitly set alongside the override — use it directly.
|
||||
// Re-parsing would incorrectly split vendor-prefixed model names
|
||||
// (e.g. modelOverride="anthropic/claude-sonnet-4-6" with providerOverride="openrouter").
|
||||
return { provider: explicitOverrideProvider, model: storedModelOverride };
|
||||
}
|
||||
const overrideProvider = provider || DEFAULT_PROVIDER;
|
||||
const parsedOverride = parseModelRef(storedModelOverride, overrideProvider);
|
||||
if (parsedOverride) {
|
||||
provider = parsedOverride.provider;
|
||||
model = parsedOverride.model;
|
||||
} else {
|
||||
provider = overrideProvider;
|
||||
model = storedModelOverride;
|
||||
}
|
||||
return { provider, model };
|
||||
}
|
||||
|
||||
// Fall back to the last runtime model recorded on the session entry.
|
||||
const runtimeModel = entry?.model?.trim();
|
||||
const runtimeProvider = entry?.modelProvider?.trim();
|
||||
if (runtimeModel) {
|
||||
@ -664,21 +687,6 @@ export function resolveSessionModelRef(
|
||||
}
|
||||
return { provider, model };
|
||||
}
|
||||
|
||||
// Fall back to explicit per-session override (set at spawn/model-patch time),
|
||||
// then finally to configured defaults.
|
||||
const storedModelOverride = entry?.modelOverride?.trim();
|
||||
if (storedModelOverride) {
|
||||
const overrideProvider = entry?.providerOverride?.trim() || provider || DEFAULT_PROVIDER;
|
||||
const parsedOverride = parseModelRef(storedModelOverride, overrideProvider);
|
||||
if (parsedOverride) {
|
||||
provider = parsedOverride.provider;
|
||||
model = parsedOverride.model;
|
||||
} else {
|
||||
provider = overrideProvider;
|
||||
model = storedModelOverride;
|
||||
}
|
||||
}
|
||||
return { provider, model };
|
||||
}
|
||||
|
||||
|
||||
@ -271,4 +271,64 @@ describe("tui session actions", () => {
|
||||
expect(state.sessionInfo.modelProvider).toBe("openai");
|
||||
expect(state.sessionInfo.updatedAt).toBe(50);
|
||||
});
|
||||
|
||||
it("falls back to defaults model when session entry is not found", async () => {
|
||||
// When the current session key has no entry in the store (brand-new session),
|
||||
// the TUI should show the configured default model, not "unknown".
|
||||
const listSessions = vi.fn().mockResolvedValue({
|
||||
ts: Date.now(),
|
||||
path: "/tmp/sessions.json",
|
||||
count: 0,
|
||||
defaults: {
|
||||
modelProvider: "my-copilot",
|
||||
model: "claude-opus-4.6",
|
||||
contextTokens: 200000,
|
||||
},
|
||||
sessions: [],
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
expect(state.sessionInfo.model).toBe("claude-opus-4.6");
|
||||
expect(state.sessionInfo.modelProvider).toBe("my-copilot");
|
||||
expect(state.sessionInfo.contextTokens).toBe(200000);
|
||||
});
|
||||
});
|
||||
|
||||
@ -114,7 +114,10 @@ export function createSessionActions(context: SessionActionContext) {
|
||||
}
|
||||
};
|
||||
|
||||
const resolveModelSelection = (entry?: SessionInfoEntry) => {
|
||||
const resolveModelSelection = (
|
||||
entry?: SessionInfoEntry,
|
||||
defaults?: SessionInfoDefaults | null,
|
||||
) => {
|
||||
if (entry?.modelProvider || entry?.model) {
|
||||
return {
|
||||
modelProvider: entry.modelProvider ?? state.sessionInfo.modelProvider,
|
||||
@ -126,6 +129,12 @@ export function createSessionActions(context: SessionActionContext) {
|
||||
const overrideProvider = entry?.providerOverride?.trim() || state.sessionInfo.modelProvider;
|
||||
return { modelProvider: overrideProvider, model: overrideModel };
|
||||
}
|
||||
if (defaults?.modelProvider || defaults?.model) {
|
||||
return {
|
||||
modelProvider: defaults.modelProvider ?? state.sessionInfo.modelProvider,
|
||||
model: defaults.model ?? state.sessionInfo.model,
|
||||
};
|
||||
}
|
||||
return {
|
||||
modelProvider: state.sessionInfo.modelProvider,
|
||||
model: state.sessionInfo.model,
|
||||
@ -194,7 +203,7 @@ export function createSessionActions(context: SessionActionContext) {
|
||||
next.updatedAt = entry.updatedAt;
|
||||
}
|
||||
|
||||
const selection = resolveModelSelection(entry);
|
||||
const selection = resolveModelSelection(entry, defaults);
|
||||
if (selection.modelProvider !== undefined) {
|
||||
next.modelProvider = selection.modelProvider;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user