From f3386d965be4784319c7ece2fd75ea16b44ddd31 Mon Sep 17 00:00:00 2001 From: Xubin Ren <52506698+Re-bin@users.noreply.github.com> Date: Thu, 13 Aug 2026 02:31:17 +0900 Subject: [PATCH] fix(tui): restore session header card --- tui/src/app.test.ts | 9 ++++++++- tui/src/app.ts | 1 + tui/src/transcript.ts | 19 ++++++++++++++++++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/tui/src/app.test.ts b/tui/src/app.test.ts index dcaf12c5e..0c87c17b7 100644 --- a/tui/src/app.test.ts +++ b/tui/src/app.test.ts @@ -283,9 +283,11 @@ describe("NanobotTui layout", () => { composer: { backgroundColor: { intent: string }; textColor: { toInts(): number[] } } transcript: { markdown: Set<{ syntaxStyle: object }> + frames: Set<{ borderColor: { toInts(): number[] } }> } } const markdown = [...internals.transcript.markdown][0] + const sessionFrame = [...internals.transcript.frames][0] const darkSyntax = markdown?.syntaxStyle setup.renderer.emit(CliRenderEvents.THEME_MODE, "light") @@ -299,6 +301,7 @@ describe("NanobotTui layout", () => { expect(internals.shell.backgroundColor.intent).toBe("default") expect(internals.composer.backgroundColor.intent).toBe("default") expect(internals.composer.textColor.toInts().slice(0, 3)).toEqual([24, 24, 27]) + expect(sessionFrame?.borderColor.toInts().slice(0, 3)).toEqual([212, 212, 216]) expect(markdown?.syntaxStyle).not.toBe(darkSyntax) }) @@ -316,10 +319,14 @@ describe("NanobotTui layout", () => { const userLine = frame.split("\n").find((line) => line.includes("User question")) || "" const agentLine = frame.split("\n").find((line) => line.includes("Agent **answer**")) || "" const headerLine = frame.split("\n").find((line) => line.includes(">_ nanobot")) || "" + const headerBorder = frame.split("\n").find((line) => line.includes("╭")) || "" expect(userLine).toContain("› User question") expect(agentLine).toContain("• Agent **answer**") - expect(headerLine).not.toMatch(/[╭╮│]/u) + expect(userLine).not.toContain("│") + expect(agentLine).not.toContain("│") + expect(headerLine).toContain("│") + expect(headerBorder.trim().length).toBeLessThanOrEqual(62) }) test("keeps footer status and shortcuts visually separated", async () => { diff --git a/tui/src/app.ts b/tui/src/app.ts index 86083c61b..dbb0438c6 100644 --- a/tui/src/app.ts +++ b/tui/src/app.ts @@ -117,6 +117,7 @@ function transcriptTheme(palette: Palette): TranscriptTheme { error: palette.error, user: palette.user, assistant: palette.accent, + border: palette.border, syntax: syntaxStyle(palette), } } diff --git a/tui/src/transcript.ts b/tui/src/transcript.ts index 59ae7ea8a..13adaceb4 100644 --- a/tui/src/transcript.ts +++ b/tui/src/transcript.ts @@ -17,6 +17,7 @@ export interface TranscriptTheme { error: string user: string assistant: string + border: string syntax: SyntaxStyle } @@ -47,6 +48,7 @@ export class Transcript { }> = [] private readonly markdown = new Set() private readonly activities = new Set() + private readonly frames = new Set() private wrote = false private nextId = 0 @@ -84,6 +86,7 @@ export class Transcript { this.theme = theme for (const { renderable, tone } of this.styledText) renderable.fg = theme[tone] for (const renderable of this.markdown) renderable.syntaxStyle = theme.syntax + for (const frame of this.frames) frame.borderColor = theme.border // Markdown may still be rendering this frame. Release the prior native // style only after the renderer reaches idle, matching OpenCode's retained // theme lifecycle and avoiding both leaks and use-after-free transitions. @@ -91,15 +94,27 @@ export class Transcript { } header(options: TranscriptHeader): void { - const row = this.createRow("header") + const row = new BoxRenderable(this.renderer, { + id: this.id("header-row"), + width: "100%", + maxWidth: 62, + flexDirection: "column", + border: true, + borderStyle: "rounded", + borderColor: this.theme.border, + paddingLeft: 1, + paddingRight: 1, + }) const title = this.createText(`>_ nanobot v${options.version}`, "text", true) const context = this.createText([ + "", `${options.model} · ${options.access}`, options.workspace, ].join("\n"), "muted") row.add(title) row.add(context) this.root.add(row) + this.frames.add(row) this.wrote = true } @@ -113,6 +128,7 @@ export class Transcript { this.styledText.length = 0 this.markdown.clear() this.activities.clear() + this.frames.clear() this.wrote = false this.nextId = 0 this.header(header) @@ -229,6 +245,7 @@ export class Transcript { destroy(): void { this.live = null this.activity = null + this.frames.clear() this.theme.syntax.destroy() }