mini: add hideable footer details (#38192)

This commit is contained in:
Simon Klee 2026-07-21 23:28:40 +02:00 committed by GitHub
commit 2ed8fe5960
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 130 additions and 16 deletions

View file

@ -133,6 +133,9 @@ export const Info = Schema.Struct({
turn_summary: Schema.optional(Schema.Literals(["show", "hide"])).annotate({
description: "Show or hide the agent, model, and duration summary in scrollback",
}),
footer: Schema.optional(Schema.Literals(["show", "hide"])).annotate({
description: "Show or hide persistent activity, model, usage, and context details in the footer",
}),
mono: Schema.optional(Schema.Boolean).annotate({
description: "Use monochrome ASCII output",
}),

View file

@ -26,6 +26,7 @@ type CommandEntry =
| (PanelEntry & { action: "skill" })
| (PanelEntry & { action: "queued" })
| (PanelEntry & { action: "subagent" })
| (PanelEntry & { action: "status" })
| (PanelEntry & { action: "variant.cycle" })
| (PanelEntry & { action: "variant.list" })
| (PanelEntry & { action: "settings" })
@ -363,6 +364,7 @@ export function RunCommandMenuBody(props: {
onQueued: () => void
onVariant: () => void
onVariantCycle: () => void
onStatus: () => void
onSettings: () => void
onCommand: (name: string) => void
onNew: () => void
@ -381,6 +383,12 @@ export function RunCommandMenuBody(props: {
footer: "/editor",
keywords: "editor compose draft external editor",
},
{
action: "status",
category: "Session",
display: "Show status",
keywords: "status activity model context usage footer",
},
...(props.subagents().length > 0
? [
{
@ -526,6 +534,11 @@ export function RunCommandMenuBody(props: {
return
}
if (item.action === "status") {
props.onStatus()
return
}
if (item.action === "settings") {
props.onSettings()
return
@ -613,6 +626,13 @@ export function RunSettingsBody(props: {
keywords: `turn summary agent model duration ${props.settings().turn_summary}`,
key: "turn_summary",
},
{
category: "Terminal",
display: "Footer details",
footer: saving() === "footer" ? "saving" : props.settings().footer,
keywords: `footer status activity model context usage ${props.settings().footer}`,
key: "footer",
},
{
category: "Terminal",
display: "Monochrome UI",

View file

@ -56,6 +56,8 @@ import type { RunTheme } from "./theme"
registerOpencodeSpinner()
const FOOTER_DETAIL_DURATION = 3000
const EMPTY_BORDER = {
topLeft: "",
bottomLeft: "",
@ -189,6 +191,7 @@ export function RunFooterView(props: RunFooterViewProps) {
const armed = createMemo(() => props.state().interrupt > 0)
const exiting = createMemo(() => props.state().exit > 0)
const usage = createMemo(() => props.state().usage)
const footerDetails = createMemo(() => props.miniSettings().footer === "show")
const interruptLabel = createMemo(() => {
if (!interrupt()) {
return
@ -217,6 +220,33 @@ export function RunFooterView(props: RunFooterViewProps) {
color: createColors(options),
}
})
const footerStatus = createMemo(() => {
const current = model() ?? props.state().model.trim()
const variant = props.currentVariant()
const details = [busy() ? "running" : "idle"]
if (current) details.push(variant ? `${current} ${variant}` : current)
if (usage()) details.push(props.mono ? usage().replaceAll(" · ", " - ") : usage())
if (queuedPrompts().length > 0) details.push(`${queuedPrompts().length} pending`)
if (activeTabs().length > 0) details.push(`${activeTabs().length} subagent${activeTabs().length === 1 ? "" : "s"}`)
return details.join(props.mono ? " - " : " · ")
})
const [footerNotice, setFooterNotice] = createSignal("")
let footerNoticeTimeout: ReturnType<typeof setTimeout> | undefined
let previousFooterStatus: string | undefined
const showFooterStatus = () => {
if (footerNoticeTimeout) clearTimeout(footerNoticeTimeout)
setFooterNotice(footerStatus())
footerNoticeTimeout = setTimeout(() => {
footerNoticeTimeout = undefined
setFooterNotice("")
}, FOOTER_DETAIL_DURATION)
}
createEffect(() => {
const current = footerStatus()
if (previousFooterStatus !== undefined && previousFooterStatus !== current && !footerDetails()) showFooterStatus()
previousFooterStatus = current
})
const permission = createMemo<Extract<FooterView, { type: "permission" }> | undefined>(() => {
const view = active()
return view.type === "permission" ? view : undefined
@ -375,9 +405,11 @@ export function RunFooterView(props: RunFooterViewProps) {
return `Press ${clearShortcut() || "ctrl+c"} again to exit`
}
if (busy()) {
return armed() ? "again to interrupt" : "interrupt"
}
if (busy() && armed()) return "again to interrupt"
if (footerNotice()) return footerNotice()
if (busy()) return "interrupt"
if (stateStatus().length > 0) {
return stateStatus()
@ -386,7 +418,7 @@ export function RunFooterView(props: RunFooterViewProps) {
return shell() ? "Shell mode" : ""
})
const activityMeta = createMemo(() => {
if (!responsive().statusline.showActivityMeta || usage().length === 0) {
if (!footerDetails() || !responsive().statusline.showActivityMeta || usage().length === 0) {
return ""
}
@ -394,7 +426,7 @@ export function RunFooterView(props: RunFooterViewProps) {
})
const modelStatus = createMemo(() => {
const current = model()
if (!prompt() || !responsive().statusline.showModel || !current) return
if (!footerDetails() || !prompt() || !responsive().statusline.showModel || !current) return
return {
model: current,
variant: responsive().statusline.showModelVariant ? props.currentVariant() : undefined,
@ -409,7 +441,7 @@ export function RunFooterView(props: RunFooterViewProps) {
return theme().highlight
}
if (busy() || stateStatus().length > 0) {
if (busy() || footerNotice().length > 0 || stateStatus().length > 0) {
return theme().text
}
@ -419,7 +451,7 @@ export function RunFooterView(props: RunFooterViewProps) {
const hasActivityMeta = createMemo(() => activityMeta().length > 0)
const hasModelStatus = createMemo(() => Boolean(modelStatus()))
const contextHints = createMemo(() => {
if (!prompt() || shell() || !responsive().statusline.showContextHints) {
if (!footerDetails() || !prompt() || shell() || !responsive().statusline.showContextHints) {
return []
}
@ -459,6 +491,7 @@ export function RunFooterView(props: RunFooterViewProps) {
onCleanup(() => {
props.onRequestExit?.(undefined)
if (footerNoticeTimeout) clearTimeout(footerNoticeTimeout)
})
Keymap.createLayer(() => ({
@ -699,6 +732,10 @@ export function RunFooterView(props: RunFooterViewProps) {
props.onCycle()
closePanel()
}}
onStatus={() => {
showFooterStatus()
closePanel()
}}
onCommand={(name) => {
composer.submitText(`/${name}`)
closePanel()
@ -857,7 +894,7 @@ export function RunFooterView(props: RunFooterViewProps) {
paddingRight={1}
backgroundColor="transparent"
>
<Show when={busy() && !exiting()}>
<Show when={footerDetails() && busy() && !exiting()}>
<box flexShrink={0}>
<spinner color={spin().color} frames={spin().frames} interval={40} />
</box>

View file

@ -89,6 +89,7 @@ export function resolveMiniSettings(config?: { mini?: Partial<MiniSettings> }):
thinking: config?.mini?.thinking ?? "hide",
shell_output: config?.mini?.shell_output ?? "hide",
turn_summary: config?.mini?.turn_summary ?? "show",
footer: config?.mini?.footer ?? "show",
mono: config?.mini?.mono ?? false,
}
}

View file

@ -398,6 +398,7 @@ export type MiniSettings = {
thinking: "show" | "hide"
shell_output: "show" | "hide"
turn_summary: "show" | "hide"
footer: "show" | "hide"
mono: boolean
}