From 99b2e78d750275fa13af5f81b7f3ed7a21610b5d Mon Sep 17 00:00:00 2001 From: Simon Klee Date: Tue, 21 Jul 2026 19:00:21 +0200 Subject: [PATCH] fix(tui): improve responsive footer details (#38116) --- packages/tui/src/mini/footer.view.tsx | 36 +++++++++++++++++-- packages/tui/src/mini/footer.width.ts | 6 +++- packages/tui/test/mini/footer.view.test.tsx | 38 ++++++++++++++++----- packages/tui/test/mini/footer.width.test.ts | 11 +++--- 4 files changed, 73 insertions(+), 18 deletions(-) diff --git a/packages/tui/src/mini/footer.view.tsx b/packages/tui/src/mini/footer.view.tsx index 2ddeef1ec0..ff5199e89f 100644 --- a/packages/tui/src/mini/footer.view.tsx +++ b/packages/tui/src/mini/footer.view.tsx @@ -29,6 +29,7 @@ import { RunFormBody } from "./footer.form" import { createFormBodyState, type FormBodyState } from "./form.shared" import { footerWidthPolicy } from "./footer.width" import { Keymap } from "../context/keymap" +import { modelInfo } from "./variant.shared" import type { FooterPromptRoute, @@ -157,6 +158,10 @@ export function RunFooterView(props: RunFooterViewProps) { return tabs().findIndex((item) => item.sessionID === sessionID) + 1 }) const foregroundSubagents = createMemo(() => activeTabs().some((item) => !item.background)) + const model = createMemo(() => { + const current = props.currentModel() + return current ? modelInfo(props.providers(), current).model : undefined + }) const detail = createMemo(() => { const current = route() return current.type === "subagent" ? subagent().details[current.sessionID] : undefined @@ -364,6 +369,14 @@ export function RunFooterView(props: RunFooterViewProps) { return usage() }) + const modelStatus = createMemo(() => { + const current = model() + if (!prompt() || !responsive().statusline.showModel || !current) return + return { + model: current, + variant: responsive().statusline.showModelVariant ? props.currentVariant() : undefined, + } + }) const statusColor = createMemo(() => { if (exiting()) { return theme().error @@ -381,6 +394,7 @@ export function RunFooterView(props: RunFooterViewProps) { }) const statuslineBackground = createMemo(() => theme().status) const hasActivityMeta = createMemo(() => activityMeta().length > 0) + const hasModelStatus = createMemo(() => Boolean(modelStatus())) const contextHints = createMemo(() => { if (!prompt() || shell() || !responsive().statusline.showContextHints) { return [] @@ -819,11 +833,29 @@ export function RunFooterView(props: RunFooterViewProps) { + + {(info) => ( + + + {info().model} + + {(variant) => {variant()}} + + + + )} + + {(hint, index) => ( - 0 || (hasActivityMeta() && index() === 0)}> + 0 || ((hasActivityMeta() || hasModelStatus()) && index() === 0)}> {sectionSeparator()} {hint.key}{" "} @@ -837,7 +869,7 @@ export function RunFooterView(props: RunFooterViewProps) { {(hint) => ( - + {sectionSeparator()} {hint().key}{" "} diff --git a/packages/tui/src/mini/footer.width.ts b/packages/tui/src/mini/footer.width.ts index b0979858e4..35670d0ef7 100644 --- a/packages/tui/src/mini/footer.width.ts +++ b/packages/tui/src/mini/footer.width.ts @@ -1,8 +1,10 @@ // Shared responsive width policy const FOOTER_WIDTH_BREAKPOINTS = { + commandHint: 24, + model: 32, + modelVariant: 40, compact: 80, - commandHint: 66, context: 120, spacious: 150, } as const @@ -19,6 +21,8 @@ export function footerWidthPolicy(width: number) { statusline: { showActivityMeta: compact, showCommandHint: width >= FOOTER_WIDTH_BREAKPOINTS.commandHint, + showModel: width >= FOOTER_WIDTH_BREAKPOINTS.model, + showModelVariant: width >= FOOTER_WIDTH_BREAKPOINTS.modelVariant, showContextHints: compact, contextHintLimit: !compact ? 0 : spacious ? undefined : context ? 2 : 1, }, diff --git a/packages/tui/test/mini/footer.view.test.tsx b/packages/tui/test/mini/footer.view.test.tsx index b8c191ceb0..41e3e13568 100644 --- a/packages/tui/test/mini/footer.view.test.tsx +++ b/packages/tui/test/mini/footer.view.test.tsx @@ -1074,8 +1074,8 @@ test("direct footer shows authoritative pending work while running", async () => const statusItems = statusline.getChildren().filter((item): item is BoxRenderable => item instanceof BoxRenderable) const main = statusItems[0] const spinner = main.getChildren()[0] - const background = statusItems[1] - const queued = statusItems[2] + const background = statusItems[2] + const queued = statusItems[3] const hint = statusItems.at(-1)! expect(spinner).toBeDefined() @@ -1098,11 +1098,36 @@ test("direct footer shows authoritative pending work while running", async () => } }) +test("direct footer progressively adds model details after the command hint", async () => { + for (const expected of [ + { width: 24, model: false, variant: false }, + { width: 32, model: true, variant: false }, + { width: 40, model: true, variant: true }, + ]) { + const app = await renderFooter({ + providers: [provider()], + currentModel: { providerID: "opencode", modelID: "gpt-5" }, + currentVariant: "xhigh", + width: expected.width, + }) + + try { + await app.renderOnce() + const frame = app.captureCharFrame() + expect({ + width: expected.width, + command: frame.includes("ctrl+p cmd"), + model: frame.includes("GPT-5"), + variant: frame.includes("xhigh"), + }).toEqual({ ...expected, command: true }) + } finally { + app.cleanup() + } + } +}) + test("direct footer always offers backgrounding for a foreground subagent", async () => { const app = await renderFooter({ - providers: [provider()], - currentModel: { providerID: "opencode", modelID: "gpt-5" }, - currentVariant: "xhigh", subagents: { tabs: [subagent({ sessionID: "s-1", label: "Explore", description: "Inspect auth flow" })], details: {}, @@ -1125,9 +1150,6 @@ test("direct footer always offers backgrounding for a foreground subagent", asyn test("direct footer hides the subagent hint when only completed subagents remain", async () => { const app = await renderFooter({ - providers: [provider()], - currentModel: { providerID: "opencode", modelID: "gpt-5" }, - currentVariant: "xhigh", subagents: { tabs: [subagent({ sessionID: "s-1", label: "Explore", description: "Inspect auth flow", status: "completed" })], details: {}, diff --git a/packages/tui/test/mini/footer.width.test.ts b/packages/tui/test/mini/footer.width.test.ts index 3f9809d356..fa8ec5d31a 100644 --- a/packages/tui/test/mini/footer.width.test.ts +++ b/packages/tui/test/mini/footer.width.test.ts @@ -3,19 +3,16 @@ import { footerWidthPolicy } from "../../src/mini/footer.width" describe("run footer width", () => { test("preserves shared dialog and statusline breakpoints", () => { + expect([23, 24].map((width) => footerWidthPolicy(width).statusline.showCommandHint)).toEqual([false, true]) + expect([31, 32].map((width) => footerWidthPolicy(width).statusline.showModel)).toEqual([false, true]) + expect([39, 40].map((width) => footerWidthPolicy(width).statusline.showModelVariant)).toEqual([false, true]) + const narrow = footerWidthPolicy(79) expect(narrow.dialog.narrow).toBe(true) expect(narrow.statusline.showActivityMeta).toBe(false) - expect(narrow.statusline.showCommandHint).toBe(true) expect(narrow.statusline.showContextHints).toBe(false) expect(narrow.statusline.contextHintLimit).toBe(0) - const command = footerWidthPolicy(65) - expect(command.statusline.showCommandHint).toBe(false) - - const commandHint = footerWidthPolicy(66) - expect(commandHint.statusline.showCommandHint).toBe(true) - const compact = footerWidthPolicy(80) expect(compact.dialog.narrow).toBe(false) expect(compact.statusline.showActivityMeta).toBe(true)