fix(tui): restore session header card

This commit is contained in:
Xubin Ren
2026-08-17 20:56:10 +08:00
parent 8395bc825b
commit f3386d965b
3 changed files with 27 additions and 2 deletions
+8 -1
View File
@@ -283,9 +283,11 @@ describe("NanobotTui layout", () => {
composer: { backgroundColor: { intent: string }; textColor: { toInts(): number[] } } composer: { backgroundColor: { intent: string }; textColor: { toInts(): number[] } }
transcript: { transcript: {
markdown: Set<{ syntaxStyle: object }> markdown: Set<{ syntaxStyle: object }>
frames: Set<{ borderColor: { toInts(): number[] } }>
} }
} }
const markdown = [...internals.transcript.markdown][0] const markdown = [...internals.transcript.markdown][0]
const sessionFrame = [...internals.transcript.frames][0]
const darkSyntax = markdown?.syntaxStyle const darkSyntax = markdown?.syntaxStyle
setup.renderer.emit(CliRenderEvents.THEME_MODE, "light") setup.renderer.emit(CliRenderEvents.THEME_MODE, "light")
@@ -299,6 +301,7 @@ describe("NanobotTui layout", () => {
expect(internals.shell.backgroundColor.intent).toBe("default") expect(internals.shell.backgroundColor.intent).toBe("default")
expect(internals.composer.backgroundColor.intent).toBe("default") expect(internals.composer.backgroundColor.intent).toBe("default")
expect(internals.composer.textColor.toInts().slice(0, 3)).toEqual([24, 24, 27]) 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) 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 userLine = frame.split("\n").find((line) => line.includes("User question")) || ""
const agentLine = frame.split("\n").find((line) => line.includes("Agent **answer**")) || "" const agentLine = frame.split("\n").find((line) => line.includes("Agent **answer**")) || ""
const headerLine = frame.split("\n").find((line) => line.includes(">_ nanobot")) || "" 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(userLine).toContain(" User question")
expect(agentLine).toContain("• Agent **answer**") 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 () => { test("keeps footer status and shortcuts visually separated", async () => {
+1
View File
@@ -117,6 +117,7 @@ function transcriptTheme(palette: Palette): TranscriptTheme {
error: palette.error, error: palette.error,
user: palette.user, user: palette.user,
assistant: palette.accent, assistant: palette.accent,
border: palette.border,
syntax: syntaxStyle(palette), syntax: syntaxStyle(palette),
} }
} }
+18 -1
View File
@@ -17,6 +17,7 @@ export interface TranscriptTheme {
error: string error: string
user: string user: string
assistant: string assistant: string
border: string
syntax: SyntaxStyle syntax: SyntaxStyle
} }
@@ -47,6 +48,7 @@ export class Transcript {
}> = [] }> = []
private readonly markdown = new Set<MarkdownRenderable>() private readonly markdown = new Set<MarkdownRenderable>()
private readonly activities = new Set<Activity>() private readonly activities = new Set<Activity>()
private readonly frames = new Set<BoxRenderable>()
private wrote = false private wrote = false
private nextId = 0 private nextId = 0
@@ -84,6 +86,7 @@ export class Transcript {
this.theme = theme this.theme = theme
for (const { renderable, tone } of this.styledText) renderable.fg = theme[tone] for (const { renderable, tone } of this.styledText) renderable.fg = theme[tone]
for (const renderable of this.markdown) renderable.syntaxStyle = theme.syntax 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 // Markdown may still be rendering this frame. Release the prior native
// style only after the renderer reaches idle, matching OpenCode's retained // style only after the renderer reaches idle, matching OpenCode's retained
// theme lifecycle and avoiding both leaks and use-after-free transitions. // theme lifecycle and avoiding both leaks and use-after-free transitions.
@@ -91,15 +94,27 @@ export class Transcript {
} }
header(options: TranscriptHeader): void { 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 title = this.createText(`>_ nanobot v${options.version}`, "text", true)
const context = this.createText([ const context = this.createText([
"",
`${options.model} · ${options.access}`, `${options.model} · ${options.access}`,
options.workspace, options.workspace,
].join("\n"), "muted") ].join("\n"), "muted")
row.add(title) row.add(title)
row.add(context) row.add(context)
this.root.add(row) this.root.add(row)
this.frames.add(row)
this.wrote = true this.wrote = true
} }
@@ -113,6 +128,7 @@ export class Transcript {
this.styledText.length = 0 this.styledText.length = 0
this.markdown.clear() this.markdown.clear()
this.activities.clear() this.activities.clear()
this.frames.clear()
this.wrote = false this.wrote = false
this.nextId = 0 this.nextId = 0
this.header(header) this.header(header)
@@ -229,6 +245,7 @@ export class Transcript {
destroy(): void { destroy(): void {
this.live = null this.live = null
this.activity = null this.activity = null
this.frames.clear()
this.theme.syntax.destroy() this.theme.syntax.destroy()
} }