From 509017eda9ec7a183e5362eecef727b233ded33a Mon Sep 17 00:00:00 2001 From: roboomp Date: Wed, 22 Jul 2026 19:24:32 +0000 Subject: [PATCH] fix(tui): honored observed fallback flag on live hub rows - Merged the executor-reported fallback flag into the live-session badge path so a Fireworks Fast to base degrade (which arms no session retry state) keeps its provenance. - Added a live-row regression test for a fallback that populates no retryFallbackModel. Fixes #6316 --- .../src/modes/components/agent-hub.ts | 21 ++++++---- .../test/agent-hub-ordering.test.ts | 42 +++++++++++++++++++ 2 files changed, 54 insertions(+), 9 deletions(-) diff --git a/packages/coding-agent/src/modes/components/agent-hub.ts b/packages/coding-agent/src/modes/components/agent-hub.ts index 6c22cd764..07fb449bb 100644 --- a/packages/coding-agent/src/modes/components/agent-hub.ts +++ b/packages/coding-agent/src/modes/components/agent-hub.ts @@ -95,21 +95,24 @@ function formatResolvedModelBadge(resolved: string, preserveProvider = false): s * (e.g. a parked historical agent restored from disk). */ function modelBadge(ref: AgentRef, observed: ObservableSession | undefined): string | undefined { - const fallback = ref.session?.retryFallbackModel; - if (fallback) { - return `${theme.fg("warning", "fallback →")} ${formatResolvedModelBadge(fallback, true)}`; + const progress = observed?.progress; + // Prefer the live session's own resolved fallback selector; else honor the + // executor-reported fallback flag. The latter covers observer-only rows (no + // live session) AND live rows whose fallback armed no session retry state — + // e.g. the Fireworks Fast → base degrade, which emits `retry_fallback_applied` + // without populating `#activeRetryFallback`, so `retryFallbackModel` is undefined. + const fallbackSelector = + ref.session?.retryFallbackModel ?? (progress?.resolvedModelIsFallback ? progress.resolvedModel : undefined); + if (fallbackSelector) { + return `${theme.fg("warning", "fallback →")} ${formatResolvedModelBadge(fallbackSelector, true)}`; } const model = ref.session?.model; if (model) { const level = model.thinking ? ref.session?.thinkingLevel : undefined; return formatModelBadge(model.id, level); } - const resolved = observed?.progress?.resolvedModel; - if (!resolved) return undefined; - if (observed?.progress?.resolvedModelIsFallback) { - return `${theme.fg("warning", "fallback →")} ${formatResolvedModelBadge(resolved, true)}`; - } - return formatResolvedModelBadge(resolved); + const resolved = progress?.resolvedModel; + return resolved ? formatResolvedModelBadge(resolved) : undefined; } /** Result of one host-backed transcript read for the Agent Hub viewer. */ diff --git a/packages/coding-agent/test/agent-hub-ordering.test.ts b/packages/coding-agent/test/agent-hub-ordering.test.ts index c1ea67112..2588dd3eb 100644 --- a/packages/coding-agent/test/agent-hub-ordering.test.ts +++ b/packages/coding-agent/test/agent-hub-ordering.test.ts @@ -204,4 +204,46 @@ describe("Agent hub row ordering", () => { hub.dispose(); } }); + + it("flags a fallback badge for a live row whose fallback armed no session retry state", () => { + geometry = stubStdoutGeometry(120); + const agents = new AgentRegistry(); + // Live session with a resolved model but no `retryFallbackModel` — the + // Fireworks Fast → base degrade emits `retry_fallback_applied` without + // arming `#activeRetryFallback`, so the badge must fall back to the + // executor-reported progress flag. + const session = { model: { id: "kimi-k2" }, retryFallbackModel: undefined } as unknown as AgentSession; + agents.register({ id: "FastAgent", displayName: "Fast Agent", kind: "sub", session }); + + const observers = new SessionObserverRegistry(); + vi.spyOn(observers, "getSessions").mockReturnValue([ + { + id: "FastAgent", + kind: "subagent", + label: "Subagent", + status: "active", + lastUpdate: Date.now(), + progress: { + resolvedModel: "fireworks/kimi-k2", + resolvedModelIsFallback: true, + } as never, + }, + ]); + + const hub = new AgentHubOverlayComponent({ + observers, + hubKeys: [], + onDone: () => {}, + requestRender: () => {}, + registry: agents, + irc: new IrcBus(agents), + focusAgent: async () => {}, + }); + + try { + expect(Bun.stripANSI(hub.render(120).join("\n"))).toContain("fallback → fireworks/kimi-k2"); + } finally { + hub.dispose(); + } + }); });