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)