Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 34 additions & 2 deletions packages/tui/src/mini/footer.view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 []
Expand Down Expand Up @@ -819,11 +833,29 @@ export function RunFooterView(props: RunFooterViewProps) {
</box>
</Show>

<Show when={modelStatus()}>
{(info) => (
<box
minWidth={8}
paddingRight={1}
backgroundColor="transparent"
flexShrink={1}
>
<text fg={theme().text} wrapMode="none" truncate>
{info().model}
<Show when={info().variant}>
{(variant) => <span style={{ fg: theme().warning, bold: true }}> {variant()}</span>}
</Show>
</text>
</box>
)}
</Show>

<For each={contextHints()}>
{(hint, index) => (
<box paddingRight={1} backgroundColor="transparent" flexShrink={0} maxWidth={24}>
<text fg={theme().text} wrapMode="none" truncate>
<Show when={index() > 0 || (hasActivityMeta() && index() === 0)}>
<Show when={index() > 0 || ((hasActivityMeta() || hasModelStatus()) && index() === 0)}>
{sectionSeparator()}
</Show>
<span style={{ fg: theme().text }}>{hint.key}</span>{" "}
Expand All @@ -837,7 +869,7 @@ export function RunFooterView(props: RunFooterViewProps) {
{(hint) => (
<box paddingRight={1} backgroundColor="transparent" flexShrink={0} maxWidth={18}>
<text fg={theme().text} wrapMode="none" truncate>
<Show when={hasActivityMeta() || hasContextHints()}>
<Show when={hasActivityMeta() || hasModelStatus() || hasContextHints()}>
{sectionSeparator()}
</Show>
<span style={{ fg: theme().text }}>{hint().key}</span>{" "}
Expand Down
6 changes: 5 additions & 1 deletion packages/tui/src/mini/footer.width.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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,
},
Expand Down
38 changes: 30 additions & 8 deletions packages/tui/test/mini/footer.view.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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: {},
Expand All @@ -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: {},
Expand Down
11 changes: 4 additions & 7 deletions packages/tui/test/mini/footer.width.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading