Przeglądaj źródła

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

Fix #38010
Simon Klee 3 tygodni temu
rodzic
commit
8fad13365b

+ 2 - 2
packages/cli/test/config.test.ts

@@ -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 {

+ 3 - 0
packages/tui/src/config/index.tsx

@@ -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",
       }),

+ 7 - 0
packages/tui/src/mini/footer.command.tsx

@@ -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",

+ 4 - 0
packages/tui/src/mini/footer.ts

@@ -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) {

+ 1 - 0
packages/tui/src/mini/runtime.boot.ts

@@ -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,
   }
 }

+ 13 - 10
packages/tui/src/mini/runtime.lifecycle.ts

@@ -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(

+ 1 - 0
packages/tui/src/mini/types.ts

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

+ 8 - 1
packages/tui/test/mini/footer-keymap.test.tsx

@@ -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={() => {}}

+ 40 - 4
packages/tui/test/mini/footer.view.test.tsx

@@ -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,

+ 10 - 1
packages/tui/test/mini/runtime.boot.test.ts

@@ -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,
     })
   })