From 61b2f169a8cba23243d7ec6b865616eaa29fa24a Mon Sep 17 00:00:00 2001 From: Xubin Ren <52506698+Re-bin@users.noreply.github.com> Date: Thu, 3 Sep 2026 17:02:40 +0800 Subject: [PATCH] fix(tui): inherit markdown foreground colors Pass the active transcript foreground into retained Markdown renderables so unhighlighted fenced code remains visible on light terminal backgrounds. Update existing renderables during theme changes and cover the history hydration path that exposed the regression. --- tui/src/app.test.ts | 39 ++++++++++++++++++++++++++++++++++++++- tui/src/transcript.ts | 6 +++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/tui/src/app.test.ts b/tui/src/app.test.ts index 92c908c5b..73ae30541 100644 --- a/tui/src/app.test.ts +++ b/tui/src/app.test.ts @@ -2102,6 +2102,42 @@ describe("NanobotTui layout", () => { ))).toBeTrue() }) + test("renders fenced plain text from light-theme history", async () => { + setup = await createRenderer({ width: 100, height: 30, screenMode: "alternate-screen" }) + const app = NanobotTui.mount( + setup.renderer, + { ...options, theme: "light" }, + client(), + new MockTreeSitterClient({ autoResolveTimeout: 0 }), + ) + const response = [ + "Commit types:", + "", + "```text", + "feat:", + "fix:", + "perf:", + "docs:", + "test:", + "refactor:", + "chore:", + "```", + "", + "Include the reason in the body.", + ].join("\n") + const transcript = (app as unknown as { transcript: Transcript }).transcript + + transcript.history([{ role: "assistant", content: response }]) + await setup.flush() + + const code = setup.captureSpans().lines + .flatMap((line) => line.spans) + .find((span) => span.text.includes("feat:")) + + expect(setup.captureCharFrame()).toContain("feat:") + expect(code?.fg.toInts().slice(0, 3)).toEqual([24, 24, 27]) + }) + test("renders assistant LaTeX as Unicode text without changing code", async () => { setup = await createRenderer({ width: 96, height: 24, screenMode: "alternate-screen" }) const app = mount(setup) @@ -2152,7 +2188,7 @@ describe("NanobotTui layout", () => { syntaxStyle: { getStyle(name: string): { fg?: { toInts(): number[] } } | undefined } | null } transcript: { - markdown: Set<{ syntaxStyle: object }> + markdown: Set<{ fg?: { toInts(): number[] }; syntaxStyle: object }> frames: Set<{ borderColor: { toInts(): number[] } }> userRows: Set<{ backgroundColor: { intent: string; toInts(): number[] } }> userMessages: Set<{ renderable: TextRenderable }> @@ -2186,6 +2222,7 @@ describe("NanobotTui layout", () => { expect(internals.composer.textColor.toInts().slice(0, 3)).toEqual([24, 24, 27]) expect(sessionFrame?.borderColor.toInts().slice(0, 3)).toEqual([212, 212, 216]) expect(userRow?.backgroundColor.toInts().slice(0, 3)).toEqual([240, 240, 240]) + expect(markdown?.fg?.toInts().slice(0, 3)).toEqual([24, 24, 27]) expect(markdown?.syntaxStyle).not.toBe(darkSyntax) expect(internals.composer.syntaxStyle).not.toBe(darkComposerSyntax) expect(internals.composer.syntaxStyle?.getStyle("image.placeholder")?.fg?.toInts().slice(0, 3)) diff --git a/tui/src/transcript.ts b/tui/src/transcript.ts index 094a42624..ec1879871 100644 --- a/tui/src/transcript.ts +++ b/tui/src/transcript.ts @@ -183,7 +183,10 @@ export class Transcript { message.displayContent, ) } - for (const renderable of this.markdown) renderable.syntaxStyle = theme.syntax + for (const renderable of this.markdown) { + renderable.fg = theme.text + renderable.syntaxStyle = theme.syntax + } for (const frame of this.frames) frame.borderColor = theme.border for (const row of this.userRows) { row.backgroundColor = theme.userBackground @@ -664,6 +667,7 @@ export class Transcript { minWidth: 0, flexGrow: 1, flexShrink: 1, + fg: this.theme.text, syntaxStyle: this.theme.syntax, streaming, internalBlockMode: "top-level",