fix(tui): simplify narrow interrupt footer (#46533)

Co-authored-by: jlongster <17031+jlongster@users.noreply.github.com>
This commit is contained in:
David Hill 2026-09-01 09:06:25 -06:00 committed by GitHub
parent 30f998c5b9
commit c596a8f11d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 115 additions and 12 deletions

View file

@ -156,7 +156,11 @@ export interface Page {
readonly render: (input: { readonly data?: Record<string, any> }) => JSX.Element
}
type PromptFooterInput = { readonly sessionID?: string; readonly mode: "normal" | "shell" }
type PromptFooterInput = {
readonly sessionID?: string
readonly mode: "normal" | "shell"
readonly showDetails: boolean
}
/**
* The host UI's slot tree. Every path is one slot: a named boundary a plugin

View file

@ -1595,7 +1595,11 @@ export function Prompt(props: PromptProps) {
if (agentLabel()) revealedPromptMetadata.add(local)
})
const borderHighlight = createMemo(() => tint(theme.border.default, highlight(), agentMetaAlpha()))
const footerInput = () => ({ sessionID: props.sessionID, mode: store.mode })
const footerInput = () => ({
sessionID: props.sessionID,
mode: store.mode,
showDetails: store.interrupt === 0 || dimensions().width >= 80,
})
const placeholderText = createMemo(() => {
if (props.showPlaceholder === false) return undefined

View file

@ -8,7 +8,12 @@ const money = new Intl.NumberFormat("en-US", {
currency: "USD",
})
export function PromptFooter(props: { context: Plugin.Context; sessionID?: string; mode: "normal" | "shell" }) {
export function PromptFooter(props: {
context: Plugin.Context
sessionID?: string
mode: "normal" | "shell"
showDetails: boolean
}) {
const dimensions = useTerminalDimensions()
const [liveHovered, setLiveHovered] = createSignal(false)
const subagents = createMemo(() => {
@ -69,7 +74,7 @@ export function PromptFooter(props: { context: Plugin.Context; sessionID?: strin
</text>
</box>
</Show>
<Show when={status().length > 0}>
<Show when={props.showDetails && status().length > 0}>
<text fg={props.context.theme.text.subdued} wrapMode="none" truncate flexShrink={1}>
<Show when={live()}> · </Show>
{status().join(" · ")}
@ -77,14 +82,14 @@ export function PromptFooter(props: { context: Plugin.Context; sessionID?: strin
</Show>
</box>
</Match>
<Match when={dimensions().width >= 44}>
<Match when={props.showDetails && dimensions().width >= 44}>
<text fg={props.context.theme.text.default} flexShrink={0}>
{shortcut("agent.cycle")} <span style={{ fg: props.context.theme.text.subdued }}>agents</span>
</text>
</Match>
</Switch>
<Show when={dimensions().width >= 44}>
<text fg={props.context.theme.text.default} flexShrink={0}>
<Show when={props.showDetails}>
<text fg={props.context.theme.text.default} wrapMode="none" flexShrink={0}>
{shortcut("command.palette.show")} <span style={{ fg: props.context.theme.text.subdued }}>commands</span>
</text>
</Show>
@ -106,7 +111,14 @@ export default Plugin.define({
setup(context) {
context.ui.slot({
append: "prompt.footer",
render: (props) => <PromptFooter context={context} sessionID={props.sessionID} mode={props.mode} />,
render: (props) => (
<PromptFooter
context={context}
sessionID={props.sessionID}
mode={props.mode}
showDetails={props.showDetails}
/>
),
})
},
})

View file

@ -3,6 +3,7 @@ import { expect, test } from "bun:test"
import { RGBA, TextRenderable } from "@opentui/core"
import { testRender } from "@opentui/solid"
import type { Context } from "@opencode-ai/plugin/tui/context"
import { createSignal } from "solid-js"
import { PromptFooter } from "../../src/feature-plugins/prompt/footer"
test("prompt footer separates simultaneous subagent, shell, and usage status", async () => {
@ -38,10 +39,13 @@ test("prompt footer separates simultaneous subagent, shell, and usage status", a
},
},
} as unknown as Context
const app = await testRender(() => <PromptFooter context={context} sessionID="session" mode="normal" />, {
width: 80,
height: 2,
})
const app = await testRender(
() => <PromptFooter context={context} sessionID="session" mode="normal" showDetails={true} />,
{
width: 80,
height: 2,
},
)
try {
await app.renderOnce()
@ -59,3 +63,82 @@ test("prompt footer separates simultaneous subagent, shell, and usage status", a
app.renderer.destroy()
}
})
test("prompt footer can hide details", async () => {
const color = RGBA.fromInts(200, 200, 200)
const context = {
location: { directory: "/workspace" },
theme: {
text: {
default: color,
subdued: color,
},
},
keymap: {
shortcuts: (id: string) => {
if (id === "command.palette.show") return ["ctrl+p"]
if (id === "agent.cycle") return ["shift+tab"]
return []
},
},
data: {
session: {
family: () => ["session"],
status: () => "running",
get: () => ({ id: "session", location: { directory: "/workspace" } }),
cost: () => 1,
message: {
list: () => [
{
id: "message",
type: "assistant",
model: { providerID: "provider", id: "model" },
tokens: { input: 1_000, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
},
],
},
},
shell: { list: () => [] },
location: {
model: { list: () => [{ providerID: "provider", id: "model", limit: { context: 10_000 } }] },
},
},
} as unknown as Context
const [showDetails, setShowDetails] = createSignal(true)
const [sessionID, setSessionID] = createSignal<string | undefined>("session")
const app = await testRender(
() => (
<box width="100%" flexDirection="row" justifyContent="space-between" gap={2}>
<PromptFooter
context={context}
sessionID={sessionID()}
mode="normal"
showDetails={showDetails()}
/>
</box>
),
{
width: 80,
height: 1,
},
)
try {
await app.renderOnce()
expect(app.captureCharFrame()).toContain("1.0K (10%) · $1.00")
expect(app.captureCharFrame()).toContain("ctrl+p commands")
setShowDetails(false)
await app.renderOnce()
const frame = app.captureCharFrame()
expect(frame).not.toContain("1.0K (10%)")
expect(frame).not.toContain("$1.00")
expect(frame).not.toContain("ctrl+p commands")
setSessionID(undefined)
await app.renderOnce()
expect(app.captureCharFrame()).not.toContain("shift+tab agents")
} finally {
app.renderer.destroy()
}
})