mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-06 20:13:24 +00:00
mini: pack statusline by content width (#38646)
This commit is contained in:
parent
ea010ab3a4
commit
00f063b381
7 changed files with 230 additions and 115 deletions
|
|
@ -104,7 +104,8 @@ type RunFooterOptions = {
|
|||
|
||||
export function resolveRunAgent(agents: RunAgent[], current: string | undefined) {
|
||||
const selectable = agents.filter((agent) => agent.mode !== "subagent" && !agent.hidden)
|
||||
return selectable.find((agent) => agent.id === current) ?? selectable.at(0)
|
||||
if (current === undefined) return selectable.at(0)
|
||||
return selectable.find((agent) => agent.id === current)
|
||||
}
|
||||
|
||||
const PERMISSION_ROWS = 12
|
||||
|
|
@ -327,6 +328,7 @@ export class RunFooter implements FooterApi {
|
|||
providers: footer.providers,
|
||||
currentAgent: footer.currentAgent,
|
||||
currentAgentID: footer.currentAgentID,
|
||||
currentAgentExplicit: () => selectedAgentID() !== undefined,
|
||||
currentModel: footer.currentModel,
|
||||
variants: footer.variants,
|
||||
currentVariant: footer.currentVariant,
|
||||
|
|
|
|||
|
|
@ -29,10 +29,11 @@ import { RunPromptBody, createPromptState } from "./footer.prompt"
|
|||
import { RunPermissionBody } from "./footer.permission"
|
||||
import { RunFormBody } from "./footer.form"
|
||||
import { createFormBodyState, type FormBodyState } from "./form.shared"
|
||||
import { footerWidthPolicy } from "./footer.width"
|
||||
import { footerStatuslinePolicy } from "./footer.width"
|
||||
import { Keymap } from "../context/keymap"
|
||||
import { modelInfo } from "./variant.shared"
|
||||
import { monoShortcut } from "./mono"
|
||||
import { stringWidth } from "../util/string-width"
|
||||
|
||||
import type {
|
||||
FooterPromptRoute,
|
||||
|
|
@ -79,6 +80,7 @@ type RunFooterViewProps = {
|
|||
providers: () => RunProvider[] | undefined
|
||||
currentAgent: () => string
|
||||
currentAgentID: () => string | undefined
|
||||
currentAgentExplicit: () => boolean
|
||||
currentModel: () => RunInput["model"]
|
||||
variants: () => string[]
|
||||
currentVariant: () => string | undefined
|
||||
|
|
@ -116,7 +118,6 @@ type RunFooterViewProps = {
|
|||
export function RunFooterView(props: RunFooterViewProps) {
|
||||
const term = useTerminalDimensions()
|
||||
const width = createMemo(() => term().width)
|
||||
const responsive = createMemo(() => footerWidthPolicy(width()))
|
||||
const active = createMemo<FooterView>(() => props.view?.() ?? { type: "prompt" })
|
||||
const subagent = createMemo<FooterSubagentState>(() => {
|
||||
return (
|
||||
|
|
@ -410,19 +411,19 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
return shell() ? "Shell mode" : ""
|
||||
})
|
||||
const activityMeta = createMemo(() => {
|
||||
if (!footerDetails() || !responsive().statusline.showActivityMeta || usage().length === 0) {
|
||||
return ""
|
||||
}
|
||||
|
||||
if (!footerDetails()) return ""
|
||||
return props.mono ? usage().replaceAll(" · ", " - ") : usage()
|
||||
})
|
||||
const agentStatus = createMemo(() => {
|
||||
if (!footerDetails() || !prompt() || shell() || !props.currentAgentExplicit()) return undefined
|
||||
return props.currentAgent()
|
||||
})
|
||||
const modelStatus = createMemo(() => {
|
||||
const current = model() ?? props.state().model.trim()
|
||||
if (!footerDetails() || !prompt() || shell() || !responsive().statusline.showModel || !current) return
|
||||
if (!footerDetails() || !prompt() || shell() || !current) return
|
||||
return {
|
||||
agent: props.currentAgent(),
|
||||
model: current,
|
||||
variant: responsive().statusline.showModelVariant ? props.currentVariant() : undefined,
|
||||
variant: props.currentVariant(),
|
||||
}
|
||||
})
|
||||
const statusColor = createMemo(() => {
|
||||
|
|
@ -441,32 +442,26 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
return theme().muted
|
||||
})
|
||||
const statuslineBackground = createMemo(() => theme().status)
|
||||
const hasActivityMeta = createMemo(() => activityMeta().length > 0)
|
||||
const hasModelStatus = createMemo(() => Boolean(modelStatus()))
|
||||
const contextHints = createMemo(() => {
|
||||
if (!footerDetails() || !prompt() || shell() || !responsive().statusline.showContextHints) {
|
||||
const contextHintCandidates = createMemo(() => {
|
||||
if (!footerDetails() || !prompt() || shell()) {
|
||||
return []
|
||||
}
|
||||
|
||||
const items: Array<{ kind: string; key: string; label: string }> = []
|
||||
const items: Array<{ key: string; label: string }> = []
|
||||
if (foregroundSubagents() && backgroundShortcut()) {
|
||||
items.push({ kind: "background", key: backgroundShortcut(), label: "background" })
|
||||
items.push({ key: backgroundShortcut(), label: "background" })
|
||||
}
|
||||
if (queuedPrompts().length > 0 && queuedShortcut()) {
|
||||
items.push({ kind: "queued", key: queuedShortcut(), label: `${queuedPrompts().length} pending` })
|
||||
items.push({ key: queuedShortcut(), label: `${queuedPrompts().length} pending` })
|
||||
}
|
||||
if (activeTabs().length > 0 && subagentShortcut()) {
|
||||
items.push({ kind: "subagents", key: subagentShortcut(), label: "subagents" })
|
||||
items.push({ key: subagentShortcut(), label: "subagents" })
|
||||
}
|
||||
|
||||
const limit = responsive().statusline.contextHintLimit
|
||||
return limit === undefined ? items : items.slice(0, limit)
|
||||
return items
|
||||
})
|
||||
const hasContextHints = createMemo(() => contextHints().length > 0)
|
||||
const commandHint = createMemo(() => {
|
||||
if (!prompt() || !responsive().statusline.showCommandHint) {
|
||||
return
|
||||
}
|
||||
if (!prompt()) return
|
||||
|
||||
if (shell()) {
|
||||
return { key: "esc", label: "normal" }
|
||||
|
|
@ -476,6 +471,49 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
return { key: command(), label: "cmd" }
|
||||
}
|
||||
})
|
||||
const commandHintWidth = createMemo(() => {
|
||||
const hint = commandHint()
|
||||
return hint ? stringWidth(`${hint.key} ${hint.label}`) : 0
|
||||
})
|
||||
const statuslineText = createMemo(() =>
|
||||
busy() && !exiting() && (footerDetails() || armed())
|
||||
? `${interruptLabel() ? `${interruptLabel()} ` : ""}${statusText()}`
|
||||
: statusText(),
|
||||
)
|
||||
const statuslineMainWidth = createMemo(() => {
|
||||
const mode = modeLabel()
|
||||
const modeWidth = mode ? stringWidth(mode) + (props.mono ? 1 : 2) : 0
|
||||
const spinnerWidth = footerDetails() && busy() && !exiting() ? stringWidth(spin().frames[0] ?? "") + 1 : 0
|
||||
return modeWidth + Math.max(12, (props.mono ? 1 : 2) + spinnerWidth + stringWidth(statuslineText()))
|
||||
})
|
||||
const visibleModeLabel = createMemo(() => {
|
||||
const mode = modeLabel()
|
||||
if (!mode || width() - commandHintWidth() < stringWidth(mode) + (props.mono ? 1 : 2)) return undefined
|
||||
return mode
|
||||
})
|
||||
const statuslineMainAvailable = createMemo(() => {
|
||||
const mode = visibleModeLabel()
|
||||
return width() - commandHintWidth() - (mode ? stringWidth(mode) + (props.mono ? 1 : 2) : 0)
|
||||
})
|
||||
const statuslineLayout = createMemo(() => {
|
||||
const agent = agentStatus()
|
||||
const info = modelStatus()
|
||||
return footerStatuslinePolicy({
|
||||
width: width(),
|
||||
mainWidth: statuslineMainWidth(),
|
||||
commandWidth: commandHint() ? commandHintWidth() : undefined,
|
||||
agentWidth: agent ? stringWidth(agent) : undefined,
|
||||
contextWidths: contextHintCandidates().map((item) => stringWidth(`${item.key} ${item.label}`)),
|
||||
modelWidth: info ? stringWidth(info.model) : undefined,
|
||||
variantWidth: info?.variant ? stringWidth(` ${info.variant}`) : undefined,
|
||||
usageWidth: activityMeta() ? stringWidth(activityMeta()) : undefined,
|
||||
})
|
||||
})
|
||||
const contextHints = createMemo(() => contextHintCandidates().slice(0, statuslineLayout().contextCount))
|
||||
const hasStatuslineInfo = createMemo(() => {
|
||||
const layout = statuslineLayout()
|
||||
return layout.showUsage || layout.showAgent || layout.showModel
|
||||
})
|
||||
const sectionSeparator = () => <span style={{ fg: theme().muted }}>{props.mono ? "- " : "· "}</span>
|
||||
|
||||
createEffect(() => {
|
||||
|
|
@ -876,7 +914,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
flexShrink={0}
|
||||
backgroundColor={statuslineBackground()}
|
||||
>
|
||||
<Show when={modeLabel()}>
|
||||
<Show when={visibleModeLabel()}>
|
||||
{(label) => (
|
||||
<box
|
||||
paddingLeft={props.mono ? 0 : 1}
|
||||
|
|
@ -896,12 +934,21 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
gap={1}
|
||||
flexGrow={1}
|
||||
flexShrink={1}
|
||||
minWidth={12}
|
||||
paddingLeft={props.mono ? 0 : 1}
|
||||
paddingRight={1}
|
||||
minWidth={0}
|
||||
paddingLeft={statuslineMainAvailable() >= 2 && !props.mono ? 1 : 0}
|
||||
paddingRight={statuslineMainAvailable() >= (props.mono ? 1 : 2) ? 1 : 0}
|
||||
backgroundColor="transparent"
|
||||
overflow="hidden"
|
||||
>
|
||||
<Show when={footerDetails() && busy() && !exiting()}>
|
||||
<Show
|
||||
when={
|
||||
footerDetails() &&
|
||||
busy() &&
|
||||
!exiting() &&
|
||||
statuslineMainAvailable() >=
|
||||
(props.mono ? 1 : 2) + stringWidth(spin().frames[0] ?? "") + 1 + stringWidth(statuslineText())
|
||||
}
|
||||
>
|
||||
<box flexShrink={0}>
|
||||
<spinner color={spin().color} frames={spin().frames} interval={40} />
|
||||
</box>
|
||||
|
|
@ -917,29 +964,36 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
</text>
|
||||
</box>
|
||||
|
||||
<Show when={activityMeta().length > 0}>
|
||||
<box paddingRight={1} backgroundColor="transparent" flexShrink={1}>
|
||||
<text fg={theme().muted} wrapMode="none" truncate>
|
||||
{activityMeta()}
|
||||
</text>
|
||||
</box>
|
||||
<Show when={statuslineLayout().showUsage && activityMeta()}>
|
||||
{(usage) => (
|
||||
<box paddingRight={1} backgroundColor="transparent" flexShrink={0}>
|
||||
<text fg={theme().muted} wrapMode="none">
|
||||
{usage()}
|
||||
</text>
|
||||
</box>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<Show when={modelStatus()}>
|
||||
<Show when={statuslineLayout().showAgent && agentStatus()}>
|
||||
{(agent) => (
|
||||
<box paddingRight={1} backgroundColor="transparent" flexShrink={0}>
|
||||
<text fg={theme().text} wrapMode="none">
|
||||
<Show when={statuslineLayout().showUsage}>{sectionSeparator()}</Show>
|
||||
{agent()}
|
||||
</text>
|
||||
</box>
|
||||
)}
|
||||
</Show>
|
||||
|
||||
<Show when={statuslineLayout().showModel && modelStatus()}>
|
||||
{(info) => (
|
||||
<box
|
||||
minWidth={8}
|
||||
paddingRight={1}
|
||||
backgroundColor="transparent"
|
||||
flexShrink={1}
|
||||
>
|
||||
<text fg={theme().text} wrapMode="none" truncate>
|
||||
<Show when={responsive().statusline.showAgent}>
|
||||
{info().agent}
|
||||
<span style={{ fg: theme().muted }}>{props.mono ? " - " : " · "}</span>
|
||||
<box paddingRight={1} backgroundColor="transparent" flexShrink={0}>
|
||||
<text fg={theme().text} wrapMode="none">
|
||||
<Show when={statuslineLayout().showUsage || statuslineLayout().showAgent}>
|
||||
{sectionSeparator()}
|
||||
</Show>
|
||||
{info().model}
|
||||
<Show when={info().variant}>
|
||||
<Show when={statuslineLayout().showVariant && info().variant}>
|
||||
{(variant) => <span style={{ fg: theme().warning, bold: true }}> {variant()}</span>}
|
||||
</Show>
|
||||
</text>
|
||||
|
|
@ -949,25 +1003,20 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
|
||||
<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() || hasModelStatus()) && index() === 0)}>
|
||||
{sectionSeparator()}
|
||||
</Show>
|
||||
<box paddingRight={1} backgroundColor="transparent" flexShrink={0}>
|
||||
<text fg={theme().text} wrapMode="none">
|
||||
<Show when={index() > 0 || (hasStatuslineInfo() && index() === 0)}>{sectionSeparator()}</Show>
|
||||
<span style={{ fg: theme().text }}>{hint.key}</span>{" "}
|
||||
<span style={{ fg: theme().muted }}>{hint.label}</span>
|
||||
</text>
|
||||
</box>
|
||||
)}
|
||||
</For>
|
||||
|
||||
<Show when={commandHint()}>
|
||||
{(hint) => (
|
||||
<box paddingRight={1} backgroundColor="transparent" flexShrink={0} maxWidth={18}>
|
||||
<text fg={theme().text} wrapMode="none" truncate>
|
||||
<Show when={hasActivityMeta() || hasModelStatus() || hasContextHints()}>
|
||||
{sectionSeparator()}
|
||||
</Show>
|
||||
<box backgroundColor="transparent" flexShrink={0}>
|
||||
<text fg={theme().text} wrapMode="none">
|
||||
<Show when={hasStatuslineInfo() || contextHints().length > 0}>{sectionSeparator()}</Show>
|
||||
<span style={{ fg: theme().text }}>{hint().key}</span>{" "}
|
||||
<span style={{ fg: theme().muted }}>{hint().label}</span>
|
||||
</text>
|
||||
|
|
|
|||
|
|
@ -1,31 +1,52 @@
|
|||
// Shared responsive width policy
|
||||
|
||||
const FOOTER_WIDTH_BREAKPOINTS = {
|
||||
commandHint: 24,
|
||||
model: 32,
|
||||
modelVariant: 40,
|
||||
compact: 80,
|
||||
context: 120,
|
||||
spacious: 150,
|
||||
} as const
|
||||
|
||||
export function footerWidthPolicy(width: number) {
|
||||
const compact = width >= FOOTER_WIDTH_BREAKPOINTS.compact
|
||||
const context = width >= FOOTER_WIDTH_BREAKPOINTS.context
|
||||
const spacious = width >= FOOTER_WIDTH_BREAKPOINTS.spacious
|
||||
|
||||
return {
|
||||
dialog: {
|
||||
narrow: !compact,
|
||||
},
|
||||
statusline: {
|
||||
showActivityMeta: compact,
|
||||
showAgent: 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,
|
||||
narrow: width < 80,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function footerStatuslinePolicy(input: {
|
||||
width: number
|
||||
mainWidth: number
|
||||
commandWidth?: number
|
||||
agentWidth?: number
|
||||
contextWidths: number[]
|
||||
modelWidth?: number
|
||||
variantWidth?: number
|
||||
usageWidth?: number
|
||||
}) {
|
||||
let remaining = input.width - input.mainWidth - (input.commandWidth ?? 0)
|
||||
let hasSection = input.commandWidth !== undefined
|
||||
const include = (width: number | undefined) => {
|
||||
if (width === undefined) return false
|
||||
const required = width + (hasSection ? 3 : 1)
|
||||
if (remaining < required) return false
|
||||
remaining -= required
|
||||
hasSection = true
|
||||
return true
|
||||
}
|
||||
|
||||
const showModel = include(input.modelWidth)
|
||||
const showAgent = include(input.agentWidth)
|
||||
const hiddenContext = input.contextWidths.findIndex((width) => !include(width))
|
||||
const contextCount = hiddenContext === -1 ? input.contextWidths.length : hiddenContext
|
||||
const contextComplete = contextCount === input.contextWidths.length
|
||||
const variantWidth = input.variantWidth
|
||||
const showVariant = showModel && contextComplete && variantWidth !== undefined && remaining >= variantWidth
|
||||
if (showVariant) remaining -= variantWidth
|
||||
const showUsage =
|
||||
(showModel || input.modelWidth === undefined) &&
|
||||
(showAgent || input.agentWidth === undefined) &&
|
||||
contextComplete &&
|
||||
(showVariant || input.variantWidth === undefined) &&
|
||||
include(input.usageWidth)
|
||||
|
||||
return {
|
||||
showAgent,
|
||||
contextCount,
|
||||
showModel,
|
||||
showVariant,
|
||||
showUsage,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ test("down opens subagents from an empty prompt", async () => {
|
|||
providers={() => undefined}
|
||||
currentAgent={() => "Build"}
|
||||
currentAgentID={() => "build"}
|
||||
currentAgentExplicit={() => false}
|
||||
currentModel={() => undefined}
|
||||
variants={() => []}
|
||||
currentVariant={() => undefined}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ test("coalesces progress only within the same message and tool state", () => {
|
|||
)
|
||||
})
|
||||
|
||||
test("resolves the first selectable agent when none is selected", () => {
|
||||
test("falls back only when no agent is selected", () => {
|
||||
const agents: RunAgent[] = [
|
||||
{ id: "task", name: "Task", mode: "subagent", hidden: false },
|
||||
{ id: "secret", name: "Secret", mode: "primary", hidden: true },
|
||||
|
|
@ -34,5 +34,5 @@ test("resolves the first selectable agent when none is selected", () => {
|
|||
|
||||
expect(resolveRunAgent(agents, undefined)?.id).toBe("build")
|
||||
expect(resolveRunAgent(agents, "plan")?.id).toBe("plan")
|
||||
expect(resolveRunAgent(agents, "missing")?.id).toBe("build")
|
||||
expect(resolveRunAgent(agents, "missing")).toBeUndefined()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -157,6 +157,7 @@ async function renderFooter(
|
|||
providers={() => input.providers}
|
||||
currentAgent={() => input.currentAgent ?? "Build"}
|
||||
currentAgentID={() => input.currentAgent?.toLowerCase() ?? "build"}
|
||||
currentAgentExplicit={() => input.currentAgent !== undefined}
|
||||
currentModel={() => input.currentModel}
|
||||
variants={() => []}
|
||||
currentVariant={() => input.currentVariant}
|
||||
|
|
@ -208,11 +209,13 @@ async function renderFooter(
|
|||
}
|
||||
}
|
||||
|
||||
test("direct footer shows the generic default model before resolution", async () => {
|
||||
test("direct footer shows the default model without the fallback agent", async () => {
|
||||
const app = await renderFooter({ state: { model: "Default model" } })
|
||||
try {
|
||||
await app.renderOnce()
|
||||
expect(app.captureCharFrame()).toContain("Default model")
|
||||
const frame = app.captureCharFrame()
|
||||
expect(frame).toContain("Default model")
|
||||
expect(frame).not.toContain("Build")
|
||||
} finally {
|
||||
app.cleanup()
|
||||
}
|
||||
|
|
@ -1179,6 +1182,7 @@ test("direct footer shows authoritative pending work while running", async () =>
|
|||
providers={() => undefined}
|
||||
currentAgent={() => "Build"}
|
||||
currentAgentID={() => "build"}
|
||||
currentAgentExplicit={() => false}
|
||||
currentModel={() => ({
|
||||
providerID: "opencode",
|
||||
modelID: "a-model-name-long-enough-to-force-responsive-truncation",
|
||||
|
|
@ -1276,14 +1280,13 @@ test("direct footer progressively adds model details after the command hint", as
|
|||
for (const expected of [
|
||||
{ width: 24, agent: false, model: false, variant: false },
|
||||
{ width: 32, agent: false, model: true, variant: false },
|
||||
{ width: 40, agent: false, model: true, variant: true },
|
||||
{ width: 80, agent: true, model: true, variant: true },
|
||||
{ width: 40, agent: true, model: true, variant: false },
|
||||
{ width: 48, agent: true, model: true, variant: true },
|
||||
]) {
|
||||
const app = await renderFooter({
|
||||
providers: [provider()],
|
||||
currentAgent: "Plan",
|
||||
currentModel: { providerID: "opencode", modelID: "gpt-5" },
|
||||
currentVariant: "xhigh",
|
||||
state: { model: "GPT-5" },
|
||||
width: expected.width,
|
||||
})
|
||||
|
||||
|
|
@ -1303,6 +1306,66 @@ test("direct footer progressively adds model details after the command hint", as
|
|||
}
|
||||
})
|
||||
|
||||
test("direct footer keeps commands and active work ahead of usage under width pressure", async () => {
|
||||
const app = await renderFooter({
|
||||
currentAgent: "Plan",
|
||||
subagents: {
|
||||
tabs: [subagent({ sessionID: "s-1", label: "Explore", description: "Inspect auth flow" })],
|
||||
details: {},
|
||||
permissions: [],
|
||||
forms: [],
|
||||
},
|
||||
state: {
|
||||
phase: "running",
|
||||
model: "a-model-name-long-enough-to-force-responsive-truncation",
|
||||
usage: "159.6K (16%) · $4.23",
|
||||
},
|
||||
width: 80,
|
||||
})
|
||||
|
||||
try {
|
||||
await app.renderOnce()
|
||||
const frame = app.captureCharFrame()
|
||||
|
||||
expect(frame).toContain("Plan")
|
||||
expect(frame).toContain("ctrl+b background")
|
||||
expect(frame).toContain("↓ subagents")
|
||||
expect(frame).toContain("ctrl+p cmd")
|
||||
expect(frame).not.toContain("a-model-name")
|
||||
expect(frame).not.toContain("159.6K")
|
||||
expect(frame).not.toContain("$4.23")
|
||||
} finally {
|
||||
app.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
test("direct footer keeps the command hint at its minimum width", async () => {
|
||||
const app = await renderFooter({ state: { phase: "running" }, width: 10 })
|
||||
|
||||
try {
|
||||
await app.renderOnce()
|
||||
expect(app.captureCharFrame()).toContain("ctrl+p cmd")
|
||||
} finally {
|
||||
app.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
test("direct footer keeps complete status text ahead of the spinner", async () => {
|
||||
const app = await renderFooter({
|
||||
tuiConfig: createTuiResolvedConfig({ keybinds: { session_interrupt: "none" } }),
|
||||
state: { phase: "running" },
|
||||
width: 22,
|
||||
})
|
||||
|
||||
try {
|
||||
await app.renderOnce()
|
||||
expect(app.captureCharFrame()).toContain("interrupt")
|
||||
expect(boxPath(footerStatusline(app.renderer.root), "SpinnerRenderable")).toBeUndefined()
|
||||
} finally {
|
||||
app.cleanup()
|
||||
}
|
||||
})
|
||||
|
||||
test("direct footer always offers backgrounding for a foreground subagent", async () => {
|
||||
const app = await renderFooter({
|
||||
subagents: {
|
||||
|
|
|
|||
|
|
@ -2,29 +2,8 @@ import { describe, expect, test } from "bun:test"
|
|||
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.showAgent).toBe(false)
|
||||
expect(narrow.statusline.showContextHints).toBe(false)
|
||||
expect(narrow.statusline.contextHintLimit).toBe(0)
|
||||
|
||||
const compact = footerWidthPolicy(80)
|
||||
expect(compact.dialog.narrow).toBe(false)
|
||||
expect(compact.statusline.showActivityMeta).toBe(true)
|
||||
expect(compact.statusline.showAgent).toBe(true)
|
||||
expect(compact.statusline.showContextHints).toBe(true)
|
||||
expect(compact.statusline.contextHintLimit).toBe(1)
|
||||
|
||||
const context = footerWidthPolicy(120)
|
||||
expect(context.statusline.contextHintLimit).toBe(2)
|
||||
|
||||
const spacious = footerWidthPolicy(150)
|
||||
expect(spacious.statusline.contextHintLimit).toBeUndefined()
|
||||
test("preserves the dialog breakpoint", () => {
|
||||
expect(footerWidthPolicy(79).dialog.narrow).toBe(true)
|
||||
expect(footerWidthPolicy(80).dialog.narrow).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue