fix(tui): quiet hidden mini footer (#38279)
This commit is contained in:
parent
58d18be590
commit
648183cecb
5 changed files with 31 additions and 61 deletions
|
|
@ -194,8 +194,6 @@ export class RunFooter implements FooterApi {
|
|||
private interruptTimeout: NodeJS.Timeout | undefined
|
||||
private exitTimeout: NodeJS.Timeout | undefined
|
||||
private noticeTimeout: NodeJS.Timeout | undefined
|
||||
private noticeRestoreStatus = ""
|
||||
private statusVersion = 0
|
||||
private requestExitHandler: (() => boolean) | undefined
|
||||
private scrollback: RunScrollbackStream
|
||||
private themes: RunTheme[]
|
||||
|
|
@ -225,6 +223,7 @@ export class RunFooter implements FooterApi {
|
|||
const [state, setState] = createSignal<FooterState>({
|
||||
phase: "idle",
|
||||
status: "",
|
||||
notice: "",
|
||||
model: options.modelLabel,
|
||||
usage: "",
|
||||
first: options.first,
|
||||
|
|
@ -449,6 +448,7 @@ export class RunFooter implements FooterApi {
|
|||
if (patch) {
|
||||
if (typeof patch.status === "string") {
|
||||
this.clearNoticeTimer()
|
||||
patch.notice = ""
|
||||
}
|
||||
if (next.type === "turn.send") {
|
||||
this.clearInterruptTimer()
|
||||
|
|
@ -479,12 +479,10 @@ export class RunFooter implements FooterApi {
|
|||
}
|
||||
|
||||
const prev = this.state()
|
||||
if (typeof next.status === "string") {
|
||||
this.statusVersion++
|
||||
}
|
||||
const state = {
|
||||
phase: next.phase ?? prev.phase,
|
||||
status: typeof next.status === "string" ? next.status : prev.status,
|
||||
notice: typeof next.notice === "string" ? next.notice : prev.notice,
|
||||
model: typeof next.model === "string" ? next.model : prev.model,
|
||||
usage: typeof next.usage === "string" ? next.usage : prev.usage,
|
||||
first: typeof next.first === "boolean" ? next.first : prev.first,
|
||||
|
|
@ -635,26 +633,13 @@ export class RunFooter implements FooterApi {
|
|||
}
|
||||
|
||||
private setNotice(status: string): void {
|
||||
const restore = this.noticeTimeout ? this.noticeRestoreStatus : this.state().status
|
||||
this.clearNoticeTimer(false)
|
||||
this.patch({ status })
|
||||
if (!status) {
|
||||
this.noticeRestoreStatus = ""
|
||||
return
|
||||
}
|
||||
this.clearNoticeTimer()
|
||||
this.patch({ notice: status })
|
||||
if (!status) return
|
||||
|
||||
this.noticeRestoreStatus = restore
|
||||
const version = this.statusVersion
|
||||
this.noticeTimeout = setTimeout(() => {
|
||||
this.noticeTimeout = undefined
|
||||
if (this.isGone || version !== this.statusVersion) {
|
||||
this.noticeRestoreStatus = ""
|
||||
return
|
||||
}
|
||||
|
||||
const next = this.noticeRestoreStatus
|
||||
this.noticeRestoreStatus = ""
|
||||
this.patch({ status: next })
|
||||
this.patch({ notice: "" })
|
||||
}, NOTICE_DURATION)
|
||||
}
|
||||
|
||||
|
|
@ -895,19 +880,9 @@ export class RunFooter implements FooterApi {
|
|||
this.interruptTimeout = undefined
|
||||
}
|
||||
|
||||
private clearNoticeTimer(reset = true): void {
|
||||
if (!this.noticeTimeout) {
|
||||
if (reset) {
|
||||
this.noticeRestoreStatus = ""
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
clearTimeout(this.noticeTimeout)
|
||||
private clearNoticeTimer(): void {
|
||||
if (this.noticeTimeout) clearTimeout(this.noticeTimeout)
|
||||
this.noticeTimeout = undefined
|
||||
if (reset) {
|
||||
this.noticeRestoreStatus = ""
|
||||
}
|
||||
}
|
||||
|
||||
private armInterruptTimer(): void {
|
||||
|
|
|
|||
|
|
@ -55,8 +55,6 @@ import type { RunTheme } from "./theme"
|
|||
|
||||
registerOpencodeSpinner()
|
||||
|
||||
const FOOTER_DETAIL_DURATION = 3000
|
||||
|
||||
const EMPTY_BORDER = {
|
||||
topLeft: "",
|
||||
bottomLeft: "",
|
||||
|
|
@ -228,23 +226,6 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
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
|
||||
|
|
@ -379,6 +360,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
const shell = createMemo(() => prompt() && composer.shell())
|
||||
const menu = createMemo(() => prompt() && composer.visible())
|
||||
const stateStatus = createMemo(() => props.state().status.trim())
|
||||
const notice = createMemo(() => props.state().notice.trim())
|
||||
const modeLabel = createMemo(() => {
|
||||
if (exiting()) {
|
||||
return "EXIT"
|
||||
|
|
@ -404,7 +386,9 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
|
||||
if (busy() && armed()) return "again to interrupt"
|
||||
|
||||
if (footerNotice()) return footerNotice()
|
||||
if (notice()) return notice()
|
||||
|
||||
if (!footerDetails()) return shell() ? "Shell mode" : ""
|
||||
|
||||
if (busy()) return "interrupt"
|
||||
|
||||
|
|
@ -438,7 +422,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
return theme().highlight
|
||||
}
|
||||
|
||||
if (busy() || footerNotice().length > 0 || stateStatus().length > 0) {
|
||||
if (busy() || notice().length > 0 || stateStatus().length > 0) {
|
||||
return theme().text
|
||||
}
|
||||
|
||||
|
|
@ -488,7 +472,6 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
|
||||
onCleanup(() => {
|
||||
props.onRequestExit?.(undefined)
|
||||
if (footerNoticeTimeout) clearTimeout(footerNoticeTimeout)
|
||||
})
|
||||
|
||||
Keymap.createLayer(() => ({
|
||||
|
|
@ -730,7 +713,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
closePanel()
|
||||
}}
|
||||
onStatus={() => {
|
||||
showFooterStatus()
|
||||
props.onStatus(footerStatus())
|
||||
closePanel()
|
||||
}}
|
||||
onCommand={(name) => {
|
||||
|
|
@ -898,7 +881,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
|||
</Show>
|
||||
|
||||
<text fg={statusColor()} wrapMode="none" truncate flexGrow={1} flexShrink={1}>
|
||||
<Show when={busy() && !exiting()} fallback={statusText()}>
|
||||
<Show when={busy() && !exiting() && (footerDetails() || armed())} fallback={statusText()}>
|
||||
<Show when={interruptLabel()}>
|
||||
{(label) => <span style={{ fg: armed() ? statusColor() : theme().muted }}>{label()} </span>}
|
||||
</Show>
|
||||
|
|
|
|||
|
|
@ -166,6 +166,7 @@ type FooterPhase = "idle" | "running"
|
|||
export type FooterState = {
|
||||
phase: FooterPhase
|
||||
status: string
|
||||
notice: string
|
||||
model: string
|
||||
usage: string
|
||||
first: boolean
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ test("down opens subagents from an empty prompt", async () => {
|
|||
const [state] = createSignal<FooterState>({
|
||||
phase: "idle",
|
||||
status: "",
|
||||
notice: "",
|
||||
model: "gpt-5",
|
||||
usage: "",
|
||||
first: false,
|
||||
|
|
|
|||
|
|
@ -94,6 +94,7 @@ function footerState(input: Partial<FooterState> = {}) {
|
|||
return createSignal<FooterState>({
|
||||
phase: "idle",
|
||||
status: "",
|
||||
notice: "",
|
||||
model: "gpt-5",
|
||||
usage: "",
|
||||
first: false,
|
||||
|
|
@ -1109,6 +1110,7 @@ test("direct footer shows authoritative pending work while running", async () =>
|
|||
const [state] = createSignal<FooterState>({
|
||||
phase: "running",
|
||||
status: "",
|
||||
notice: "",
|
||||
model: "gpt-5",
|
||||
usage: "",
|
||||
first: false,
|
||||
|
|
@ -1324,7 +1326,7 @@ test("direct footer shows full usage metadata when room is available", async ()
|
|||
}
|
||||
})
|
||||
|
||||
test("direct footer can hide persistent details and briefly reveal changes", async () => {
|
||||
test("direct footer hides routine activity and shows explicit notices", async () => {
|
||||
const app = await renderFooter({
|
||||
state: { usage: "159.6K (16%) · $4.23" },
|
||||
miniSettings: {
|
||||
|
|
@ -1345,13 +1347,21 @@ test("direct footer can hide persistent details and briefly reveal changes", asy
|
|||
expect(initial).not.toContain("gpt-5")
|
||||
expect(initial).not.toContain("159.6K")
|
||||
|
||||
app.setState((state) => ({ ...state, phase: "running" }))
|
||||
app.setState((state) => ({ ...state, phase: "running", status: "assistant responding" }))
|
||||
await app.renderOnce()
|
||||
const changed = app.captureCharFrame()
|
||||
const statusline = footerStatusline(app.renderer.root)
|
||||
|
||||
expect(changed).toContain("running - gpt-5 - 159.6K (16%) - $4.23")
|
||||
expect(changed).not.toContain("running")
|
||||
expect(changed).not.toContain("assistant responding")
|
||||
expect(changed).not.toContain("interrupt")
|
||||
expect(changed).not.toContain("gpt-5")
|
||||
expect(changed).not.toContain("159.6K")
|
||||
expect(boxPath(statusline, "SpinnerRenderable")).toBeUndefined()
|
||||
|
||||
app.setState((state) => ({ ...state, notice: "variant high" }))
|
||||
await app.renderOnce()
|
||||
expect(app.captureCharFrame()).toContain("variant high")
|
||||
} finally {
|
||||
app.cleanup()
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue