diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 222d24370..13686d399 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -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 diff --git a/packages/coding-agent/src/modes/interactive-mode.ts b/packages/coding-agent/src/modes/interactive-mode.ts index 88c0d14dc..4baf76c67 100644 --- a/packages/coding-agent/src/modes/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive-mode.ts @@ -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; diff --git a/packages/coding-agent/test/modes/components/welcome.test.ts b/packages/coding-agent/test/modes/components/welcome.test.ts index f1d5997d1..bb8adfec2 100644 --- a/packages/coding-agent/test/modes/components/welcome.test.ts +++ b/packages/coding-agent/test/modes/components/welcome.test.ts @@ -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"); + }); +});