Merge PR #8428: fix(tui): welcome banner shows stale model name after model changes (@jacobleft)

This commit is contained in:
can1357
2026-08-16 02:03:17 +02:00
3 changed files with 63 additions and 0 deletions
+3
View File
@@ -72,6 +72,9 @@
### Fixed
- Fixed Agent Hub parking a mid-spawn child JSONL (title + session header only) so `task` then fails with `already owned by another session generation` and the row cannot be revived.
### Fixed
- Fixed the welcome banner displaying a stale model name when the session's active model changes after startup (e.g. after a delayed config load or an explicit `/model` switch). The banner now subscribes to `model_changed` events and calls `WelcomeComponent.setModel()`, which already existed but was never wired up.
## [17.3.0] - 2026-08-13
@@ -1177,6 +1177,9 @@ export class InteractiveMode implements InteractiveModeContext {
this.#eventBusUnsubscribers.push(
this.session.subscribe(event => {
if (event.type === "model_changed") {
this.#updateWelcomeModel();
}
void this.#handleGoalSessionEvent(event);
}),
onStatusLineSessionAccentChanged(() => {
@@ -1184,6 +1187,11 @@ export class InteractiveMode implements InteractiveModeContext {
this.#handleSessionAccentInputsChanged();
}),
);
// Resync the welcome banner to the live model: init-time reconciliations
// (#reconcileModeFromSession, #enterPlanMode for plan.defaultOnStartup)
// can change the model before this subscription exists, so the
// model_changed events they emit are never observed by the handler above.
this.#updateWelcomeModel();
this.#eventBusUnsubscribers.push(
onModelRolesChanged(() => {
void this.#reapplyPlanModeModelOnRoleChange();
@@ -4430,6 +4438,18 @@ export class InteractiveMode implements InteractiveModeContext {
);
}
#updateWelcomeModel(): void {
if (!this.#welcomeComponent) {
return;
}
this.#welcomeComponent.setModel(
this.session.model?.name ?? "Unknown",
this.session.model?.provider ?? "Unknown",
);
this.ui.requestRender();
}
#updateWelcomeLspServers(): void {
if (!this.#welcomeComponent) {
return;
@@ -62,3 +62,43 @@ describe("WelcomeComponent tips", () => {
expect(pickWeightedTip([], 0.5)).toBe("");
});
});
// Regression coverage for the stale welcome banner bug: the component is
// constructed with the session model at init time, but init-time model changes
// (plan.defaultOnStartup, #reconcileModeFromSession, delayed config load) can
// swap the model before the model_changed subscription exists. The catch-up
// resync in InteractiveMode.init() calls setModel() after those steps; these
// tests verify setModel() produces the correct rendered output.
describe("WelcomeComponent model name", () => {
beforeAll(async () => {
await Settings.init({ inMemory: true });
await initTheme(false);
});
it("updates the rendered model name after setModel()", () => {
const welcome = new WelcomeComponent("1.0.0", "DeepSeek V4 Pro", "deepseek");
const before = welcome.render(100).join("\n");
expect(before).toContain("DeepSeek V4 Pro");
welcome.setModel("GLM-5.2", "zhipu-coding-plan");
const after = welcome.render(100).join("\n");
expect(after).toContain("GLM-5.2");
expect(after).not.toContain("DeepSeek V4 Pro");
});
it("resyncs to the live model after init-time model switches (catch-up path)", () => {
// Banner constructed with the pre-config fallback model (the startup race
// picks an alphabetically-first provider before modelRoles.default loads).
const welcome = new WelcomeComponent("1.0.0", "DeepSeek V4 Pro", "deepseek");
expect(welcome.render(100).join("\n")).toContain("DeepSeek V4 Pro");
// Config loads, session switches to the real default — the model_changed
// event fires before the subscription exists, so InteractiveMode.init()
// calls setModel() explicitly as a catch-up resync.
welcome.setModel("GLM-5.2", "zhipu-coding-plan");
const rendered = welcome.render(100).join("\n");
expect(rendered).toContain("GLM-5.2");
expect(rendered).toContain("zhipu-coding-plan");
expect(rendered).not.toContain("DeepSeek");
});
});