feat(tui): add mini splash setting (#38317)

Fix #38010
This commit is contained in:
Simon Klee 2026-07-22 15:46:50 +02:00 committed by GitHub
commit 8fad13365b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 89 additions and 18 deletions

View file

@ -131,7 +131,7 @@ test("updates a config draft while preserving JSONC comments", async () => {
const service = yield* Config.Service
return yield* service.update((draft) => {
draft.prompt = { paste: "compact" }
draft.mini = { thinking: "hide", shell_output: "hide", turn_summary: "hide", mono: true }
draft.mini = { thinking: "hide", shell_output: "hide", turn_summary: "hide", splash: "hide", mono: true }
})
}),
)
@ -139,7 +139,7 @@ test("updates a config draft while preserving JSONC comments", async () => {
expect(config).toEqual({
animations: true,
prompt: { paste: "compact" },
mini: { thinking: "hide", shell_output: "hide", turn_summary: "hide", mono: true },
mini: { thinking: "hide", shell_output: "hide", turn_summary: "hide", splash: "hide", mono: true },
})
expect(await Bun.file(path.join(directory, "cli.json")).text()).toContain("// Keep this comment")
} finally {

View file

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

View file

@ -684,6 +684,13 @@ export function RunSettingsBody(props: {
keywords: `footer status activity model context usage ${props.settings().footer}`,
key: "footer",
},
{
category: "Terminal",
display: "Splash",
footer: saving() === "splash" ? "saving" : props.settings().splash,
keywords: `splash entry exit banner ${props.settings().splash}`,
key: "splash",
},
{
category: "Terminal",
display: "Monochrome UI",

View file

@ -616,6 +616,10 @@ export class RunFooter implements FooterApi {
return this.theme()
}
public currentMiniSettings(): MiniSettings {
return this.miniSettings()
}
private destroyTheme(theme: RunTheme): void {
const index = this.themes.indexOf(theme)
if (index === -1) {

View file

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

View file

@ -196,13 +196,15 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
renderer,
state,
"entry",
entrySplash({
...meta,
theme: theme.splash,
showSession: splash.showSession,
detail: directoryLabel(input.getDirectory(), input.host.paths.home),
mono,
}),
miniSettings.splash === "show"
? entrySplash({
...meta,
theme: theme.splash,
showSession: splash.showSession,
detail: directoryLabel(input.getDirectory(), input.host.paths.home),
mono,
})
: undefined,
)
await renderer.idle().catch(() => {})
@ -224,7 +226,9 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
history: input.history,
theme,
mono,
wrote,
// The transcript always starts one row below the terminal's prior output,
// even when the entry splash itself is hidden.
wrote: wrote || miniSettings.splash === "hide",
tuiConfig,
miniSettings: {
current: miniSettings,
@ -306,8 +310,7 @@ export async function createRuntimeLifecycle(input: LifecycleInput): Promise<Lif
try {
await footer.idle().catch(() => {})
const show = renderer.isDestroyed ? false : next.showExit
if (!renderer.isDestroyed && show) {
if (!renderer.isDestroyed && next.showExit && footer.currentMiniSettings().splash === "show") {
const sessionID = next.sessionID || input.getSessionID?.() || input.sessionID
const splash = splashInfo(next.sessionTitle ?? input.sessionTitle, next.history ?? input.history)
wroteExit = queueSplash(

View file

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

View file

@ -56,7 +56,14 @@ test("down opens subagents from an empty prompt", async () => {
view={view}
subagent={subagents}
theme={() => RUN_THEME_FALLBACK}
miniSettings={() => ({ thinking: "hide", shell_output: "hide", turn_summary: "show", footer: "show", mono: false })}
miniSettings={() => ({
thinking: "hide",
shell_output: "hide",
turn_summary: "show",
footer: "show",
splash: "show",
mono: false,
})}
mono={false}
onSubmit={() => true}
onPermissionReply={() => {}}

View file

@ -136,7 +136,14 @@ async function renderFooter(
const [state, setState] = footerState(input.state)
const config = input.tuiConfig ?? tuiConfig
const [miniSettings] = createSignal<MiniSettings>(
input.miniSettings ?? { thinking: "hide", shell_output: "hide", turn_summary: "show", footer: "show", mono: false },
input.miniSettings ?? {
thinking: "hide",
shell_output: "hide",
turn_summary: "show",
footer: "show",
splash: "show",
mono: false,
},
)
function Harness() {
return (
@ -471,6 +478,7 @@ test("direct settings panel changes Mini transcript preferences", async () => {
shell_output: "hide",
turn_summary: "show",
footer: "show",
splash: "show",
mono: false,
})
const app = await testRender(
@ -499,6 +507,7 @@ test("direct settings panel changes Mini transcript preferences", async () => {
expect(frame).toContain("Shell")
expect(frame).toContain("Turn summary")
expect(frame).toContain("Footer details")
expect(frame).toContain("Splash")
expect(frame).toContain("Monochrome UI")
expect(frame).toContain("left/right change")
expect(frame).not.toMatch(/[^\x00-\x7F]/)
@ -506,16 +515,35 @@ test("direct settings panel changes Mini transcript preferences", async () => {
app.mockInput.pressKey("ARROW_RIGHT")
await app.renderOnce()
expect(settings()).toEqual({ thinking: "show", shell_output: "hide", turn_summary: "show", footer: "show", mono: false })
expect(settings()).toEqual({
thinking: "show",
shell_output: "hide",
turn_summary: "show",
footer: "show",
splash: "show",
mono: false,
})
app.mockInput.pressKey("ARROW_DOWN")
app.mockInput.pressKey("ARROW_DOWN")
app.mockInput.pressKey("ARROW_RIGHT")
await app.renderOnce()
expect(settings()).toEqual({ thinking: "show", shell_output: "hide", turn_summary: "hide", footer: "show", mono: false })
expect(settings()).toEqual({
thinking: "show",
shell_output: "hide",
turn_summary: "hide",
footer: "show",
splash: "show",
mono: false,
})
app.mockInput.pressKey("ARROW_DOWN")
app.mockInput.pressKey("ARROW_DOWN")
app.mockInput.pressKey("ARROW_RIGHT")
await app.renderOnce()
expect(settings().splash).toBe("hide")
app.mockInput.pressKey("ARROW_DOWN")
app.mockInput.pressKey("ARROW_RIGHT")
await app.renderOnce()
@ -1169,7 +1197,14 @@ test("direct footer shows authoritative pending work while running", async () =>
},
]}
theme={() => RUN_THEME_FALLBACK}
miniSettings={() => ({ thinking: "hide", shell_output: "hide", turn_summary: "show", footer: "show", mono: false })}
miniSettings={() => ({
thinking: "hide",
shell_output: "hide",
turn_summary: "show",
footer: "show",
splash: "show",
mono: false,
})}
mono={false}
onSubmit={() => true}
onPermissionReply={() => {}}
@ -1357,6 +1392,7 @@ test("direct footer hides routine activity and shows explicit notices", async ()
shell_output: "hide",
turn_summary: "show",
footer: "hide",
splash: "show",
mono: true,
},
mono: true,

View file

@ -106,17 +106,26 @@ describe("run runtime boot", () => {
shell_output: "hide",
turn_summary: "show",
footer: "show",
splash: "show",
mono: false,
})
expect(
resolveMiniSettings({
mini: { thinking: "show", shell_output: "show", turn_summary: "hide", footer: "hide", mono: true },
mini: {
thinking: "show",
shell_output: "show",
turn_summary: "hide",
footer: "hide",
splash: "hide",
mono: true,
},
}),
).toEqual({
thinking: "show",
shell_output: "show",
turn_summary: "hide",
footer: "hide",
splash: "hide",
mono: true,
})
})