fix(tui): improve responsive footer details (#38116)
This commit is contained in:
parent
023f76f6fc
commit
99b2e78d75
4 changed files with 73 additions and 18 deletions
|
|
@ -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) {
|
|||
</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>{" "}
|
||||
|
|
@ -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>{" "}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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: {},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue