From 7412daf31b13450d07ae761090a458dff6154a6e Mon Sep 17 00:00:00 2001 From: Kevin Thomas Date: Wed, 9 Sep 2026 23:37:37 -0700 Subject: [PATCH 01/24] fix(tui): soften assistant output text color --- .../coding-agent/.changes/ui-chat-output.md | 1 + packages/coding-agent/docs/themes.md | 11 ++++-- packages/coding-agent/docs/tui.md | 2 +- .../components/assistant-message.ts | 19 +++++++--- .../src/modes/interactive/theme/dark.json | 1 + .../src/modes/interactive/theme/light.json | 1 + .../src/modes/interactive/theme/prime.json | 1 + .../modes/interactive/theme/theme-schema.json | 5 +++ .../src/modes/interactive/theme/theme.ts | 4 +- .../test/assistant-message.test.ts | 37 +++++++++++++++++++ 10 files changed, 72 insertions(+), 10 deletions(-) create mode 100644 packages/coding-agent/.changes/ui-chat-output.md diff --git a/packages/coding-agent/.changes/ui-chat-output.md b/packages/coding-agent/.changes/ui-chat-output.md new file mode 100644 index 0000000000..c2fd1beea2 --- /dev/null +++ b/packages/coding-agent/.changes/ui-chat-output.md @@ -0,0 +1 @@ +- Changed assistant message body text to a new softer `mdBody` theme color, easing the wall of bright default-foreground text while headings, links, and code keep their styling. diff --git a/packages/coding-agent/docs/themes.md b/packages/coding-agent/docs/themes.md index 44b8f108e5..740d4c339d 100644 --- a/packages/coding-agent/docs/themes.md +++ b/packages/coding-agent/docs/themes.md @@ -79,9 +79,12 @@ vim ~/.prime/agent/themes/my-theme.json "toolPendingBg": "#1e1e2e", "toolSuccessBg": "#1e2e1e", "toolErrorBg": "#2e1e1e", + "toolDiffAddedBg": "#0a2f0a", + "toolDiffRemovedBg": "#2f0a0a", "toolPanelBg": "#2d2d38", "toolTitle": "primary", "toolOutput": "", + "mdBody": "", "mdHeading": "#ffaa00", "mdLink": "primary", "mdLinkUrl": "secondary", @@ -94,6 +97,7 @@ vim ~/.prime/agent/themes/my-theme.json "mdListBullet": "#00ffff", "toolDiffAdded": "#00ff00", "toolDiffRemoved": "#ff0000", + "toolDiffText": "secondary", "toolDiffContext": "secondary", "syntaxComment": "secondary", "syntaxKeyword": "primary", @@ -140,13 +144,13 @@ vim ~/.prime/agent/themes/my-theme.json - `name` is required and must be unique. - `vars` is optional. Define reusable colors here, then reference them in `colors`. -- `colors` must define all 51 required tokens. +- `colors` must define all 56 required tokens. The `$schema` field enables editor auto-completion and validation. ## Color Tokens -Every theme must define all 51 color tokens. There are no optional colors. +Every theme must define all 56 color tokens. There are no optional colors. ### Core UI (11 colors) @@ -181,10 +185,11 @@ Every theme must define all 51 color tokens. There are no optional colors. | `toolTitle` | Tool title | | `toolOutput` | Tool output text | -### Markdown (10 colors) +### Markdown (11 colors) | Token | Purpose | |-------|---------| +| `mdBody` | Assistant output body text | | `mdHeading` | Headings | | `mdLink` | Link text | | `mdLinkUrl` | Link URL | diff --git a/packages/coding-agent/docs/tui.md b/packages/coding-agent/docs/tui.md index 5e9c371255..4e2d2a48e7 100644 --- a/packages/coding-agent/docs/tui.md +++ b/packages/coding-agent/docs/tui.md @@ -413,7 +413,7 @@ renderResult(result, options, theme, context) { | Messages | `userMessageText`, `customMessageText`, `customMessageLabel` | | Tools | `toolTitle`, `toolOutput` | | Diffs | `toolDiffAdded`, `toolDiffRemoved`, `toolDiffContext` | -| Markdown | `mdHeading`, `mdLink`, `mdLinkUrl`, `mdCode`, `mdCodeBlock`, `mdCodeBlockBorder`, `mdQuote`, `mdQuoteBorder`, `mdHr`, `mdListBullet` | +| Markdown | `mdBody`, `mdHeading`, `mdLink`, `mdLinkUrl`, `mdCode`, `mdCodeBlock`, `mdCodeBlockBorder`, `mdQuote`, `mdQuoteBorder`, `mdHr`, `mdListBullet` | | Syntax | `syntaxComment`, `syntaxKeyword`, `syntaxFunction`, `syntaxVariable`, `syntaxString`, `syntaxNumber`, `syntaxType`, `syntaxOperator`, `syntaxPunctuation` | | Thinking | `thinkingOff`, `thinkingMinimal`, `thinkingLow`, `thinkingMedium`, `thinkingHigh`, `thinkingXhigh` | | Modes | `bashMode` | diff --git a/packages/coding-agent/src/modes/interactive/components/assistant-message.ts b/packages/coding-agent/src/modes/interactive/components/assistant-message.ts index 0d90836547..9c0cef5be6 100644 --- a/packages/coding-agent/src/modes/interactive/components/assistant-message.ts +++ b/packages/coding-agent/src/modes/interactive/components/assistant-message.ts @@ -292,11 +292,20 @@ export class AssistantMessageComponent extends Container { // Set paddingY=0 to avoid extra spacing before tool executions const mermaidTransform = this.mermaidTransform; const isStreaming = this.isStreaming; - const markdown = new Markdown(content.text.trim(), 1, 0, this.markdownTheme, undefined, { - baseUrl: this.baseUrl, - transform: - mermaidTransform && ((md, availableWidth) => mermaidTransform(md, availableWidth, isStreaming)), - }); + const markdown = new Markdown( + content.text.trim(), + 1, + 0, + this.markdownTheme, + { + color: (text: string) => theme.fg("mdBody", text), + }, + { + baseUrl: this.baseUrl, + transform: + mermaidTransform && ((md, availableWidth) => mermaidTransform(md, availableWidth, isStreaming)), + }, + ); this.blockMarkdowns.set(i, markdown); this.lastBlockTexts.set(i, content.text.trim()); this.contentContainer.addChild(markdown); diff --git a/packages/coding-agent/src/modes/interactive/theme/dark.json b/packages/coding-agent/src/modes/interactive/theme/dark.json index 342efc81b7..c50f6f06a5 100644 --- a/packages/coding-agent/src/modes/interactive/theme/dark.json +++ b/packages/coding-agent/src/modes/interactive/theme/dark.json @@ -48,6 +48,7 @@ "toolTitle": "", "toolOutput": "gray", + "mdBody": "#d8d8d8", "mdHeading": "#b294bb", "mdLink": "#81a2be", "mdLinkUrl": "dimGray", diff --git a/packages/coding-agent/src/modes/interactive/theme/light.json b/packages/coding-agent/src/modes/interactive/theme/light.json index fa75668844..36365bbaf8 100644 --- a/packages/coding-agent/src/modes/interactive/theme/light.json +++ b/packages/coding-agent/src/modes/interactive/theme/light.json @@ -47,6 +47,7 @@ "toolTitle": "", "toolOutput": "mediumGray", + "mdBody": "#333333", "mdHeading": "#7e57c2", "mdLink": "blue", "mdLinkUrl": "dimGray", diff --git a/packages/coding-agent/src/modes/interactive/theme/prime.json b/packages/coding-agent/src/modes/interactive/theme/prime.json index 9234278279..8c38d98cdb 100644 --- a/packages/coding-agent/src/modes/interactive/theme/prime.json +++ b/packages/coding-agent/src/modes/interactive/theme/prime.json @@ -53,6 +53,7 @@ "toolPanelBg": "surface", "toolTitle": "", "toolOutput": "muted", + "mdBody": "#e4e4e7", "mdHeading": "primarySoft", "mdLink": "info", "mdLinkUrl": "dim", diff --git a/packages/coding-agent/src/modes/interactive/theme/theme-schema.json b/packages/coding-agent/src/modes/interactive/theme/theme-schema.json index 93931797f6..6a3a960fac 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme-schema.json +++ b/packages/coding-agent/src/modes/interactive/theme/theme-schema.json @@ -60,6 +60,7 @@ "toolPanelBg", "toolTitle", "toolOutput", + "mdBody", "mdHeading", "mdLink", "mdLinkUrl", @@ -192,6 +193,10 @@ "$ref": "#/$defs/colorValue", "description": "Tool execution box output text color" }, + "mdBody": { + "$ref": "#/$defs/colorValue", + "description": "Markdown body text (assistant output)" + }, "mdHeading": { "$ref": "#/$defs/colorValue", "description": "Markdown heading text" diff --git a/packages/coding-agent/src/modes/interactive/theme/theme.ts b/packages/coding-agent/src/modes/interactive/theme/theme.ts index dd4a48f7f7..2d93176e2b 100644 --- a/packages/coding-agent/src/modes/interactive/theme/theme.ts +++ b/packages/coding-agent/src/modes/interactive/theme/theme.ts @@ -63,7 +63,8 @@ const ThemeJsonSchema = Type.Object({ toolPanelBg: ColorValueSchema, toolTitle: ColorValueSchema, toolOutput: ColorValueSchema, - // Markdown (10 colors) + // Markdown (11 colors) + mdBody: ColorValueSchema, mdHeading: ColorValueSchema, mdLink: ColorValueSchema, mdLinkUrl: ColorValueSchema, @@ -141,6 +142,7 @@ export type ThemeColor = | "customMessageLabel" | "toolTitle" | "toolOutput" + | "mdBody" | "mdHeading" | "mdLink" | "mdLinkUrl" diff --git a/packages/coding-agent/test/assistant-message.test.ts b/packages/coding-agent/test/assistant-message.test.ts index f1bb1c8fdf..47d5bc9f19 100644 --- a/packages/coding-agent/test/assistant-message.test.ts +++ b/packages/coding-agent/test/assistant-message.test.ts @@ -320,3 +320,40 @@ describe("AssistantMessageComponent streaming identity", () => { } }); }); + +describe("AssistantMessageComponent body text color", () => { + test("renders assistant body text in the softened mdBody color", () => { + initTheme("dark"); + + const component = new AssistantMessageComponent( + createAssistantMessage([{ type: "text", text: "Plain answer." }]), + ); + const raw = component.render(120).join("\n"); + + expect(stripAnsi(raw)).toContain("Plain answer."); + expect(raw).toContain(theme.getFgAnsi("mdBody")); + }); + + test("keeps heading and inline code colors on top of mdBody", () => { + initTheme("dark"); + + const message = createAssistantMessage([{ type: "text", text: "## Title\n\nBody with `code`." }]); + const raw = new AssistantMessageComponent(message).render(120).join("\n"); + + expect(raw).toContain(theme.getFgAnsi("mdHeading")); + expect(raw).toContain(theme.getFgAnsi("mdCode")); + expect(raw).toContain(theme.getFgAnsi("mdBody")); + }); + + test("keeps thinking text on thinkingText instead of mdBody", () => { + initTheme("dark"); + setKeybindings(new KeybindingsManager()); + + const message = createAssistantMessage([{ type: "thinking", thinking: "Quiet reasoning." }]); + const raw = new AssistantMessageComponent(message, false).render(120).join("\n"); + + expect(stripAnsi(raw)).toContain("Quiet reasoning."); + expect(raw).toContain(theme.getFgAnsi("thinkingText")); + expect(raw).not.toContain(theme.getFgAnsi("mdBody")); + }); +}); From 7776db740d568682c972764676f30ceb647d4c28 Mon Sep 17 00:00:00 2001 From: Kevin Thomas Date: Thu, 10 Sep 2026 00:07:42 -0700 Subject: [PATCH 02/24] fix(tui): dim the assistant output text further --- packages/coding-agent/.changes/ui-chat-output.md | 2 +- packages/coding-agent/src/modes/interactive/theme/dark.json | 2 +- packages/coding-agent/src/modes/interactive/theme/light.json | 2 +- packages/coding-agent/src/modes/interactive/theme/prime.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/coding-agent/.changes/ui-chat-output.md b/packages/coding-agent/.changes/ui-chat-output.md index c2fd1beea2..81f0632247 100644 --- a/packages/coding-agent/.changes/ui-chat-output.md +++ b/packages/coding-agent/.changes/ui-chat-output.md @@ -1 +1 @@ -- Changed assistant message body text to a new softer `mdBody` theme color, easing the wall of bright default-foreground text while headings, links, and code keep their styling. +- Changed assistant message body text to a new dimmed `mdBody` theme color, easing the wall of bright default-foreground text while headings, links, and code keep their styling. diff --git a/packages/coding-agent/src/modes/interactive/theme/dark.json b/packages/coding-agent/src/modes/interactive/theme/dark.json index c50f6f06a5..9891c9e551 100644 --- a/packages/coding-agent/src/modes/interactive/theme/dark.json +++ b/packages/coding-agent/src/modes/interactive/theme/dark.json @@ -48,7 +48,7 @@ "toolTitle": "", "toolOutput": "gray", - "mdBody": "#d8d8d8", + "mdBody": "#c4c4c4", "mdHeading": "#b294bb", "mdLink": "#81a2be", "mdLinkUrl": "dimGray", diff --git a/packages/coding-agent/src/modes/interactive/theme/light.json b/packages/coding-agent/src/modes/interactive/theme/light.json index 36365bbaf8..40f6b3791a 100644 --- a/packages/coding-agent/src/modes/interactive/theme/light.json +++ b/packages/coding-agent/src/modes/interactive/theme/light.json @@ -47,7 +47,7 @@ "toolTitle": "", "toolOutput": "mediumGray", - "mdBody": "#333333", + "mdBody": "#454545", "mdHeading": "#7e57c2", "mdLink": "blue", "mdLinkUrl": "dimGray", diff --git a/packages/coding-agent/src/modes/interactive/theme/prime.json b/packages/coding-agent/src/modes/interactive/theme/prime.json index 8c38d98cdb..10d426dba4 100644 --- a/packages/coding-agent/src/modes/interactive/theme/prime.json +++ b/packages/coding-agent/src/modes/interactive/theme/prime.json @@ -53,7 +53,7 @@ "toolPanelBg": "surface", "toolTitle": "", "toolOutput": "muted", - "mdBody": "#e4e4e7", + "mdBody": "#d8d8dc", "mdHeading": "primarySoft", "mdLink": "info", "mdLinkUrl": "dim", From 101952046b37af549c0b79a73437c0a235999878 Mon Sep 17 00:00:00 2001 From: Kevin Thomas Date: Thu, 10 Sep 2026 00:12:06 -0700 Subject: [PATCH 03/24] fix(tui): lighten the thinking block styling --- .../coding-agent/.changes/ui-chat-output.md | 1 + .../components/assistant-message.ts | 8 ++--- .../test/assistant-message.test.ts | 34 ++++++++++++++++--- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/packages/coding-agent/.changes/ui-chat-output.md b/packages/coding-agent/.changes/ui-chat-output.md index 81f0632247..dd08be553b 100644 --- a/packages/coding-agent/.changes/ui-chat-output.md +++ b/packages/coding-agent/.changes/ui-chat-output.md @@ -1 +1,2 @@ - Changed assistant message body text to a new dimmed `mdBody` theme color, easing the wall of bright default-foreground text while headings, links, and code keep their styling. +- Changed the thinking block styling: the "Thinking..." label is no longer bold, and the visible reasoning trace renders one step dimmer while the label and recap keep the thinkingText color. diff --git a/packages/coding-agent/src/modes/interactive/components/assistant-message.ts b/packages/coding-agent/src/modes/interactive/components/assistant-message.ts index 9c0cef5be6..57dae293c1 100644 --- a/packages/coding-agent/src/modes/interactive/components/assistant-message.ts +++ b/packages/coding-agent/src/modes/interactive/components/assistant-message.ts @@ -36,7 +36,7 @@ export interface AssistantMessageComponentOptions { } function getThinkingMarkdownTheme(baseTheme: MarkdownTheme): MarkdownTheme { - const quiet = (text: string) => theme.fg("thinkingText", text); + const quiet = (text: string) => theme.fg("dim", text); return { ...baseTheme, heading: quiet, @@ -316,9 +316,9 @@ export class AssistantMessageComponent extends Container { .slice(i + 1) .some((c) => (c?.type === "text" && c.text.trim()) || (c?.type === "thinking" && c.thinking.trim())); - const thinkingLabel = theme.bold(theme.fg("thinkingText", this.hiddenThinkingLabel)); + const thinkingLabel = theme.fg("thinkingText", this.hiddenThinkingLabel); if (this.hideThinkingBlock) { - // Collapsed row: bold label, a one-line recap of the trace, and the + // Collapsed row: quiet label, a one-line recap of the trace, and the // hint. The row truncates the recap to the render width so it never // wraps onto a second line on narrow terminals. const recap = thinkingRecap(content.thinking, this.hiddenThinkingLabel); @@ -340,7 +340,7 @@ export class AssistantMessageComponent extends Container { 0, getThinkingMarkdownTheme(this.markdownTheme), { - color: (text: string) => theme.fg("thinkingText", text), + color: (text: string) => theme.fg("dim", text), }, { baseUrl: this.baseUrl }, ); diff --git a/packages/coding-agent/test/assistant-message.test.ts b/packages/coding-agent/test/assistant-message.test.ts index 47d5bc9f19..0676236e40 100644 --- a/packages/coding-agent/test/assistant-message.test.ts +++ b/packages/coding-agent/test/assistant-message.test.ts @@ -216,7 +216,7 @@ describe("AssistantMessageComponent streaming identity", () => { } }); - test("collapsed thinking shows a bold label, recap, and bracketed hint", () => { + test("collapsed thinking shows a quiet label, recap, and bracketed hint", () => { initTheme("dark"); setKeybindings(new KeybindingsManager()); @@ -233,14 +233,24 @@ describe("AssistantMessageComponent streaming identity", () => { { type: "thinking", thinking }, { type: "text", text: "Answer." }, ]); - const rendered = stripAnsi(new AssistantMessageComponent(message, true).render(120).join("\n")); + const collapsedRaw = new AssistantMessageComponent(message, true).render(120).join("\n"); + const rendered = stripAnsi(collapsedRaw); expect(rendered).toContain("Thinking... · Deciding the approach (Ctrl+T to expand)"); expect(rendered).not.toContain("Some detail"); + // The label keeps the thinkingText color but is no longer bolded. + expect(collapsedRaw).toContain(theme.getFgAnsi("thinkingText")); + expect(collapsedRaw).not.toContain("\x1b[1m"); - const expanded = stripAnsi(new AssistantMessageComponent(message, false).render(120).join("\n")); + const expandedRaw = new AssistantMessageComponent(message, false).render(120).join("\n"); + const expanded = stripAnsi(expandedRaw); expect(expanded).toContain("Thinking... (Ctrl+T to collapse)"); expect(expanded).toContain("Some detail about the options."); + // The visible trace renders one step dimmer than the label. + expect(expandedRaw).toContain(theme.getFgAnsi("dim")); + const labelLine = expandedRaw.split("\n").find((line) => line.includes("Thinking...")); + expect(labelLine).toBeDefined(); + expect(labelLine).not.toContain("\x1b[1m"); // A whitespace-only trace falls back to the label instead of an empty recap. expect(thinkingRecap(" \n\t\n", "Thinking...")).toBe("Thinking..."); @@ -345,7 +355,7 @@ describe("AssistantMessageComponent body text color", () => { expect(raw).toContain(theme.getFgAnsi("mdBody")); }); - test("keeps thinking text on thinkingText instead of mdBody", () => { + test("renders the thinking trace dimmer than the label, never mdBody", () => { initTheme("dark"); setKeybindings(new KeybindingsManager()); @@ -353,7 +363,23 @@ describe("AssistantMessageComponent body text color", () => { const raw = new AssistantMessageComponent(message, false).render(120).join("\n"); expect(stripAnsi(raw)).toContain("Quiet reasoning."); + expect(raw).toContain(theme.getFgAnsi("dim")); expect(raw).toContain(theme.getFgAnsi("thinkingText")); expect(raw).not.toContain(theme.getFgAnsi("mdBody")); }); + + test("keeps assistant prose on mdBody while the thinking trace stays dim", () => { + initTheme("dark"); + setKeybindings(new KeybindingsManager()); + + const message = createAssistantMessage([ + { type: "thinking", thinking: "Trace line." }, + { type: "text", text: "Answer prose." }, + ]); + const raw = new AssistantMessageComponent(message, false).render(120).join("\n"); + + expect(raw).toContain(theme.getFgAnsi("dim")); + expect(raw).toContain(theme.getFgAnsi("mdBody")); + expect(theme.getFgAnsi("dim")).not.toBe(theme.getFgAnsi("mdBody")); + }); }); From 48b971e415905e78a94b6042eb73b83e94cfdb86 Mon Sep 17 00:00:00 2001 From: Kevin Thomas Date: Thu, 10 Sep 2026 00:17:42 -0700 Subject: [PATCH 04/24] fix(tui): dim the collapsed tool call previews --- .../coding-agent/.changes/ui-chat-output.md | 1 + .../interactive/components/ipython-cell.ts | 12 +-- .../src/modes/interactive/theme/prime.json | 2 +- .../test/ipython-cell-preview.test.ts | 94 +++++++++++++++++++ 4 files changed, 99 insertions(+), 10 deletions(-) create mode 100644 packages/coding-agent/test/ipython-cell-preview.test.ts diff --git a/packages/coding-agent/.changes/ui-chat-output.md b/packages/coding-agent/.changes/ui-chat-output.md index dd08be553b..8fd5781fd9 100644 --- a/packages/coding-agent/.changes/ui-chat-output.md +++ b/packages/coding-agent/.changes/ui-chat-output.md @@ -1,2 +1,3 @@ - Changed assistant message body text to a new dimmed `mdBody` theme color, easing the wall of bright default-foreground text while headings, links, and code keep their styling. - Changed the thinking block styling: the "Thinking..." label is no longer bold, and the visible reasoning trace renders one step dimmer while the label and recap keep the thinkingText color. +- Changed collapsed tool-call previews to render plain and dim instead of green or syntax-highlighted code while expanded blocks keep full highlighting, and moved prime-theme inline code to a darker neutral so it stays distinct from body text. diff --git a/packages/coding-agent/src/modes/interactive/components/ipython-cell.ts b/packages/coding-agent/src/modes/interactive/components/ipython-cell.ts index 97be0c4c11..0839eaea02 100644 --- a/packages/coding-agent/src/modes/interactive/components/ipython-cell.ts +++ b/packages/coding-agent/src/modes/interactive/components/ipython-cell.ts @@ -396,7 +396,9 @@ export class IPythonCellComponent implements Component { const parts = [`${this.marker(details)} ${theme.fg("muted", languageLabel)}`]; if (preview.text) { - parts.push(this.highlightInputLine(preview.text, preview.language === "bash")); + // Collapsed preview stays plain and dim so the one-line summary reads as + // quiet metadata; the expanded block below keeps full highlighting. + parts.push(theme.fg("dim", preview.text)); } else if (!this.state.executionStarted) { parts.push(theme.fg("muted", "waiting for code")); } @@ -523,14 +525,6 @@ export class IPythonCellComponent implements Component { return true; } - private highlightInputLine(line: string, isBashCell: boolean): string { - if (isBashCell || MAGIC_LINE_PATTERN.test(line) || parseIpythonBashCell(line) !== undefined) { - return theme.fg("bashMode", line); - } - const highlighted = highlightCode(line, "python"); - return highlighted[0] ?? theme.fg("mdCodeBlock", line); - } - // Only runs when expanded — shows full output below the code, no previews. private renderOutput(lines: string[], width: number, details: IpythonDetails, hasCode: boolean): void { const blocks = this.state.content ?? []; diff --git a/packages/coding-agent/src/modes/interactive/theme/prime.json b/packages/coding-agent/src/modes/interactive/theme/prime.json index 10d426dba4..4eba256634 100644 --- a/packages/coding-agent/src/modes/interactive/theme/prime.json +++ b/packages/coding-agent/src/modes/interactive/theme/prime.json @@ -57,7 +57,7 @@ "mdHeading": "primarySoft", "mdLink": "info", "mdLinkUrl": "dim", - "mdCode": "neutral", + "mdCode": "#c8c8cd", "mdCodeBlock": "stringMint", "mdCodeBlockBorder": "grid", "mdQuote": "muted", diff --git a/packages/coding-agent/test/ipython-cell-preview.test.ts b/packages/coding-agent/test/ipython-cell-preview.test.ts new file mode 100644 index 0000000000..7fc0344f04 --- /dev/null +++ b/packages/coding-agent/test/ipython-cell-preview.test.ts @@ -0,0 +1,94 @@ +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { IPythonCellComponent } from "../src/modes/interactive/components/ipython-cell.js"; +import { initTheme, preloadCodeHighlighter, theme } from "../src/modes/interactive/theme/theme.js"; + +type CellState = ConstructorParameters[0]; + +/** + * The collapsed summary row shows a one-line code preview next to the status + * marker. It renders plain and dim (matching the quieted thinking trace) so it + * reads as metadata; the expanded block keeps full highlighting. + */ +describe("IPythonCellComponent collapsed preview styling", () => { + let previousColorTerm: string | undefined; + + beforeAll(async () => { + previousColorTerm = process.env.COLORTERM; + await preloadCodeHighlighter(); + }); + + afterAll(() => { + if (previousColorTerm === undefined) { + delete process.env.COLORTERM; + } else { + process.env.COLORTERM = previousColorTerm; + } + }); + + function useTruecolor(): void { + process.env.COLORTERM = "truecolor"; + initTheme("dark"); + } + + function renderCell(state: CellState): string { + return new IPythonCellComponent(state).render(100).join("\n"); + } + + it("renders the collapsed python preview plain and dim, without syntax colors", () => { + useTruecolor(); + const raw = renderCell({ + code: 'print("hello world")', + content: [{ type: "text", text: "hello world" }], + details: { status: "ok", durationMs: 12, stdout: "hello world" }, + executionStarted: true, + argsComplete: true, + expanded: false, + }); + + expect(raw).toContain(theme.fg("dim", 'print("hello world")')); + expect(raw).not.toContain(theme.getFgAnsi("syntaxString")); + }); + + it("renders the collapsed bash preview dim instead of bash-mode green", () => { + useTruecolor(); + const raw = renderCell({ + code: "!git status --short", + content: [{ type: "text", text: "ok" }], + details: { status: "ok", durationMs: 12, stdout: "ok" }, + executionStarted: true, + argsComplete: true, + expanded: false, + }); + + expect(raw).toContain(theme.fg("dim", "!git status --short")); + expect(raw).not.toContain(theme.fg("bashMode", "!git status --short")); + }); + + it("keeps syntax highlighting on the expanded python code block", () => { + useTruecolor(); + const raw = renderCell({ + code: 'print("hello world")', + content: [{ type: "text", text: "hello world" }], + details: { status: "ok", durationMs: 12, stdout: "hello world" }, + executionStarted: true, + argsComplete: true, + expanded: true, + }); + + expect(raw).toContain(theme.getFgAnsi("syntaxString")); + }); + + it("keeps bash-mode green on the expanded bash code block", () => { + useTruecolor(); + const raw = renderCell({ + code: "!git status --short", + content: [{ type: "text", text: "ok" }], + details: { status: "ok", durationMs: 12, stdout: "ok" }, + executionStarted: true, + argsComplete: true, + expanded: true, + }); + + expect(raw).toContain(theme.fg("bashMode", "!git status --short")); + }); +}); From ba8b4dc63dadbeed57e5d61361c8b969db340384 Mon Sep 17 00:00:00 2001 From: Kevin Thomas Date: Thu, 10 Sep 2026 00:27:14 -0700 Subject: [PATCH 05/24] fix(tui): lighten bold usage in the conversation surface --- .../coding-agent/.changes/ui-chat-output.md | 1 + packages/coding-agent/src/core/tools/bash.ts | 2 +- packages/coding-agent/src/core/tools/edit.ts | 2 +- .../interactive/components/bash-execution.ts | 4 ++-- .../interactive/components/config-selector.ts | 4 ++-- .../components/configuration-menu.ts | 6 +----- .../components/context-tree-format.ts | 8 ++++---- .../components/earendil-announcement.ts | 2 +- .../interactive/components/login-dialog.ts | 4 ++-- .../modes/interactive/components/menu-panel.ts | 2 +- .../components/scoped-models-selector.ts | 2 +- .../components/settings-selector.ts | 2 +- .../interactive/components/side-question.ts | 2 +- .../interactive/components/tool-execution.ts | 2 +- .../interactive/components/tree-selector.ts | 2 +- .../components/user-message-selector.ts | 2 +- .../src/modes/interactive/interactive-mode.ts | 18 +++++++++--------- .../src/modes/shared/startup-notices.ts | 4 ++-- .../test/bash-execution-width.test.ts | 12 +++++++++++- 19 files changed, 44 insertions(+), 37 deletions(-) diff --git a/packages/coding-agent/.changes/ui-chat-output.md b/packages/coding-agent/.changes/ui-chat-output.md index 8fd5781fd9..c19e5efc69 100644 --- a/packages/coding-agent/.changes/ui-chat-output.md +++ b/packages/coding-agent/.changes/ui-chat-output.md @@ -1,3 +1,4 @@ - Changed assistant message body text to a new dimmed `mdBody` theme color, easing the wall of bright default-foreground text while headings, links, and code keep their styling. - Changed the thinking block styling: the "Thinking..." label is no longer bold, and the visible reasoning trace renders one step dimmer while the label and recap keep the thinkingText color. - Changed collapsed tool-call previews to render plain and dim instead of green or syntax-highlighted code while expanded blocks keep full highlighting, and moved prime-theme inline code to a darker neutral so it stays distinct from body text. +- Changed decorative bold text in the conversation surface to normal weight — tool-call labels, bash command headers, section and panel titles, picker headers, startup notices, and the /btw question — keeping bold only where it marks state or a single critical item (selected rows, active tabs, the login verification code). diff --git a/packages/coding-agent/src/core/tools/bash.ts b/packages/coding-agent/src/core/tools/bash.ts index 9b72ffcc24..8baa8f70d0 100644 --- a/packages/coding-agent/src/core/tools/bash.ts +++ b/packages/coding-agent/src/core/tools/bash.ts @@ -188,7 +188,7 @@ function formatBashCall(args: { command?: string; timeout?: number } | undefined } else { commandDisplay = theme.fg("toolOutput", "..."); } - return theme.fg("toolTitle", theme.bold(`$ ${commandDisplay}`)) + timeoutSuffix; + return theme.fg("toolTitle", `$ ${commandDisplay}`) + timeoutSuffix; } function rebuildBashResultRenderComponent( diff --git a/packages/coding-agent/src/core/tools/edit.ts b/packages/coding-agent/src/core/tools/edit.ts index df71f893f4..4eb65f5877 100644 --- a/packages/coding-agent/src/core/tools/edit.ts +++ b/packages/coding-agent/src/core/tools/edit.ts @@ -203,7 +203,7 @@ function formatEditCall( const rawPath = str(args?.file_path ?? args?.path); const path = rawPath !== null ? shortenPath(rawPath) : null; const pathDisplay = path === null ? invalidArg : path ? theme.fg("accent", path) : theme.fg("toolOutput", "..."); - return `${theme.fg("toolTitle", theme.bold("edit"))} ${pathDisplay}`; + return `${theme.fg("toolTitle", "edit")} ${pathDisplay}`; } function formatEditResult( diff --git a/packages/coding-agent/src/modes/interactive/components/bash-execution.ts b/packages/coding-agent/src/modes/interactive/components/bash-execution.ts index 2026e3fec9..4fba3ea458 100644 --- a/packages/coding-agent/src/modes/interactive/components/bash-execution.ts +++ b/packages/coding-agent/src/modes/interactive/components/bash-execution.ts @@ -41,7 +41,7 @@ export class BashExecutionComponent extends Container { this.contentContainer = new Container(); this.addChild(this.contentContainer); - const header = new Text(theme.fg(colorKey, theme.bold(`$ ${command}`)), 1, 0); + const header = new Text(theme.fg(colorKey, `$ ${command}`), 1, 0); this.contentContainer.addChild(header); this.loader = new Loader( @@ -127,7 +127,7 @@ export class BashExecutionComponent extends Container { this.contentContainer.clear(); - const header = new Text(theme.fg("bashMode", theme.bold(`$ ${this.command}`)), 1, 0); + const header = new Text(theme.fg("bashMode", `$ ${this.command}`), 1, 0); this.contentContainer.addChild(header); if (availableLines.length > 0) { diff --git a/packages/coding-agent/src/modes/interactive/components/config-selector.ts b/packages/coding-agent/src/modes/interactive/components/config-selector.ts index 208456f9bd..9fbb7d7f3c 100644 --- a/packages/coding-agent/src/modes/interactive/components/config-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/config-selector.ts @@ -161,7 +161,7 @@ class ConfigSelectorHeader implements Component { invalidate(): void {} render(width: number): string[] { - const title = theme.bold("Resource Configuration"); + const title = "Resource Configuration"; const sep = theme.fg("muted", " · "); const hint = rawKeyHint("space", "toggle") + sep + rawKeyHint("esc", "close"); const hintWidth = visibleWidth(hint); @@ -333,7 +333,7 @@ class ResourceList implements Component, Focusable { if (entry.type === "group") { // Main group header (no cursor) - const groupLine = theme.fg("accent", theme.bold(entry.group.label)); + const groupLine = theme.fg("accent", entry.group.label); lines.push(truncateToWidth(` ${groupLine}`, width, "")); } else if (entry.type === "subgroup") { // Subgroup header (indented, no cursor) diff --git a/packages/coding-agent/src/modes/interactive/components/configuration-menu.ts b/packages/coding-agent/src/modes/interactive/components/configuration-menu.ts index ce269c7634..bb55079f73 100644 --- a/packages/coding-agent/src/modes/interactive/components/configuration-menu.ts +++ b/packages/coding-agent/src/modes/interactive/components/configuration-menu.ts @@ -69,11 +69,7 @@ class ConfigurationMenuTabBar implements Component { const label = `[${tab === activeTab ? "▶" : " "} ${TAB_LABELS[tab]}]`; return tab === activeTab ? theme.bold(theme.fg("accent", label)) : theme.fg("text", label); }); - const lines = this.wrapItems( - [theme.bold(theme.fg("muted", "Tabs:")), ...labels], - theme.fg("muted", " "), - safeWidth, - ); + const lines = this.wrapItems([theme.fg("muted", "Tabs:"), ...labels], theme.fg("muted", " "), safeWidth); const tabKey = keyText("tui.input.tab", { primaryOnly: true }); const shiftTabKey = keyText("app.configuration.previousTab", { primaryOnly: true }); const closeKey = keyText("tui.select.cancel", { primaryOnly: true }); diff --git a/packages/coding-agent/src/modes/interactive/components/context-tree-format.ts b/packages/coding-agent/src/modes/interactive/components/context-tree-format.ts index fd0129cf70..d7c32794d9 100644 --- a/packages/coding-agent/src/modes/interactive/components/context-tree-format.ts +++ b/packages/coding-agent/src/modes/interactive/components/context-tree-format.ts @@ -128,7 +128,7 @@ export function formatContextTree(root: ContextTreeNode, width: number): string ); const lines: string[] = []; - lines.push(theme.bold("Context")); + lines.push("Context"); lines.push(""); if (root.model) { lines.push(`${theme.fg("dim", "Model:")} ${root.model.provider}/${root.model.id}`); @@ -165,7 +165,7 @@ export function formatContextTree(root: ContextTreeNode, width: number): string ); lines.push(""); - lines.push(theme.bold("Tokens")); + lines.push("Tokens"); lines.push(`${theme.fg("dim", "Input:")} ${totals.input.toLocaleString()}`); lines.push(`${theme.fg("dim", "Output:")} ${totals.output.toLocaleString()}`); if (totals.cacheRead > 0) { @@ -178,14 +178,14 @@ export function formatContextTree(root: ContextTreeNode, width: number): string if (totals.cost.total > 0) { lines.push(""); - lines.push(theme.bold("Cost")); + lines.push("Cost"); lines.push(`${theme.fg("dim", "Total:")} $${totals.cost.total.toFixed(4)}`); } const rootContext = root.contextUsage; if (rootContext) { lines.push(""); - lines.push(theme.bold("Context")); + lines.push("Context"); if (rootContext.tokens === null || rootContext.percent === null) { lines.push(`${theme.fg("dim", "Current:")} unknown after compaction`); } else { diff --git a/packages/coding-agent/src/modes/interactive/components/earendil-announcement.ts b/packages/coding-agent/src/modes/interactive/components/earendil-announcement.ts index 6f5a0bde5c..0101c4bcca 100644 --- a/packages/coding-agent/src/modes/interactive/components/earendil-announcement.ts +++ b/packages/coding-agent/src/modes/interactive/components/earendil-announcement.ts @@ -29,7 +29,7 @@ export class EarendilAnnouncementComponent extends Container { super(); this.addChild(new DynamicBorder((text) => theme.fg("accent", text))); - this.addChild(new Text(theme.bold(theme.fg("accent", "pi has joined Earendil")), 1, 0)); + this.addChild(new Text(theme.fg("accent", "pi has joined Earendil"), 1, 0)); this.addChild(new Spacer(1)); this.addChild(new Text(theme.fg("muted", "Read the blog post:"), 1, 0)); this.addChild(new Text(theme.fg("mdLink", BLOG_URL), 1, 0)); diff --git a/packages/coding-agent/src/modes/interactive/components/login-dialog.ts b/packages/coding-agent/src/modes/interactive/components/login-dialog.ts index c3a20aaca8..f03cf8fd0c 100644 --- a/packages/coding-agent/src/modes/interactive/components/login-dialog.ts +++ b/packages/coding-agent/src/modes/interactive/components/login-dialog.ts @@ -57,7 +57,7 @@ class PrimeLoginHeader implements Component { return [ ...logoLines, centeredLine("", safeWidth), - centeredLine(theme.bold(theme.fg("text", "Login to Prime Inference")), safeWidth), + centeredLine(theme.fg("text", "Login to Prime Inference"), safeWidth), centeredLine( theme.fg("muted", "Connect your Prime Intellect account to enable Prime Inference models."), safeWidth, @@ -332,7 +332,7 @@ export class LoginDialogComponent extends Container implements Focusable { } private addSectionTitle(text: string): void { - this.contentContainer.addChild(new Text(theme.bold(theme.fg("text", text)), 0, 0)); + this.contentContainer.addChild(new Text(theme.fg("text", text), 0, 0)); } private addLabel(text: string): void { diff --git a/packages/coding-agent/src/modes/interactive/components/menu-panel.ts b/packages/coding-agent/src/modes/interactive/components/menu-panel.ts index f9e6899d2c..a0863f9b39 100644 --- a/packages/coding-agent/src/modes/interactive/components/menu-panel.ts +++ b/packages/coding-agent/src/modes/interactive/components/menu-panel.ts @@ -247,7 +247,7 @@ export class MenuPanel extends Container { const hasSubtitle = subtitle !== undefined && subtitle.length > 0; const hasHeader = hasTitle || hasSubtitle; if (hasTitle) { - lines.push(surfaceLine(theme.bold(theme.fg("text", this.title)), safeWidth)); + lines.push(surfaceLine(theme.fg("text", this.title), safeWidth)); } if (hasSubtitle) { lines.push(...surfaceWrappedLines(theme.fg("muted", subtitle), safeWidth)); diff --git a/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts b/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts index 0a08f669ab..3bbe670cda 100644 --- a/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/scoped-models-selector.ts @@ -120,7 +120,7 @@ export class ScopedModelsSelectorComponent extends Container implements Focusabl this.addChild(new DynamicBorder()); this.addChild(new Spacer(1)); - this.addChild(new Text(theme.fg("accent", theme.bold("Model Configuration")), 0, 0)); + this.addChild(new Text(theme.fg("accent", "Model Configuration"), 0, 0)); this.addChild( new Text(theme.fg("muted", `Session-only. ${keyText("app.models.save")} to save to settings.`), 0, 0), ); diff --git a/packages/coding-agent/src/modes/interactive/components/settings-selector.ts b/packages/coding-agent/src/modes/interactive/components/settings-selector.ts index 8f2b6bd8f9..adec5529de 100644 --- a/packages/coding-agent/src/modes/interactive/components/settings-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/settings-selector.ts @@ -142,7 +142,7 @@ class SelectSubmenu extends Container { ) { super(); - this.addChild(new Text(theme.bold(theme.fg("accent", title)), 0, 0)); + this.addChild(new Text(theme.fg("accent", title), 0, 0)); if (description) { this.addChild(new Spacer(1)); diff --git a/packages/coding-agent/src/modes/interactive/components/side-question.ts b/packages/coding-agent/src/modes/interactive/components/side-question.ts index 3cf901cb57..62e0f00a34 100644 --- a/packages/coding-agent/src/modes/interactive/components/side-question.ts +++ b/packages/coding-agent/src/modes/interactive/components/side-question.ts @@ -100,7 +100,7 @@ export class SideQuestionComponent implements Component { lines.push(...entry.questionBubble.render(width)); } else { const question = new Text( - `${theme.fg("accent", "/btw")} ${theme.bold(theme.fg("userMessageText", entry.event.question))}`, + `${theme.fg("accent", "/btw")} ${theme.fg("userMessageText", entry.event.question)}`, this.paddingX, 0, ).render(width); diff --git a/packages/coding-agent/src/modes/interactive/components/tool-execution.ts b/packages/coding-agent/src/modes/interactive/components/tool-execution.ts index 3785b4c64d..41564c46ad 100644 --- a/packages/coding-agent/src/modes/interactive/components/tool-execution.ts +++ b/packages/coding-agent/src/modes/interactive/components/tool-execution.ts @@ -205,7 +205,7 @@ export class ToolExecutionComponent extends Container { } private createCallFallback(): Component { - return new Text(theme.fg("toolTitle", theme.bold(this.toolName)), 0, 0); + return new Text(theme.fg("toolTitle", this.toolName), 0, 0); } private createResultFallback(): Component | undefined { diff --git a/packages/coding-agent/src/modes/interactive/components/tree-selector.ts b/packages/coding-agent/src/modes/interactive/components/tree-selector.ts index e01bdfb79d..be93370782 100644 --- a/packages/coding-agent/src/modes/interactive/components/tree-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/tree-selector.ts @@ -1172,7 +1172,7 @@ export class TreeSelectorComponent extends Container implements Focusable { this.addChild(new Spacer(1)); this.addChild(new DynamicBorder()); - this.addChild(new Text(theme.bold(" Session Tree"), 1, 0)); + this.addChild(new Text(" Session Tree", 1, 0)); const filterKeys = [ keyText("app.tree.filter.default"), keyText("app.tree.filter.noTools"), diff --git a/packages/coding-agent/src/modes/interactive/components/user-message-selector.ts b/packages/coding-agent/src/modes/interactive/components/user-message-selector.ts index 999d830344..4b88062e20 100644 --- a/packages/coding-agent/src/modes/interactive/components/user-message-selector.ts +++ b/packages/coding-agent/src/modes/interactive/components/user-message-selector.ts @@ -97,7 +97,7 @@ export class UserMessageSelectorComponent extends Container { super(); this.addChild(new Spacer(1)); - this.addChild(new Text(theme.bold("Fork from Message"), 1, 0)); + this.addChild(new Text("Fork from Message", 1, 0)); this.addChild( new Text( theme.fg("muted", "Select a user message to copy the active path up to that point into a new session"), diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index c1e6dc58b1..d3ebb700f8 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -9278,13 +9278,13 @@ export class InteractiveMode { const stats = await this.agentConnection.getSessionStats(); const sessionName = this.getCurrentSessionName(); - let info = `${theme.bold("Session Info")}\n\n`; + let info = `Session Info\n\n`; if (sessionName) { info += `${theme.fg("dim", "Name:")} ${sessionName}\n`; } info += `${theme.fg("dim", "File:")} ${stats.sessionFile ?? "In-memory"}\n`; info += `${theme.fg("dim", "ID:")} ${stats.sessionId}\n\n`; - info += `${theme.bold("Messages")}\n`; + info += `Messages\n`; info += `${theme.fg("dim", "User:")} ${stats.userMessages}\n`; info += `${theme.fg("dim", "Assistant:")} ${stats.assistantMessages}\n`; info += `${theme.fg("dim", "Tool Calls:")} ${stats.toolCalls}\n`; @@ -9299,7 +9299,7 @@ export class InteractiveMode { private handleLogsCommand(): void { const logsDir = getLogsDir(); - let info = `${theme.bold("Logs")}\n\n`; + let info = `Logs\n\n`; info += `${theme.fg("dim", "Directory:")} ${logsDir}\n\n`; let files: string[] = []; @@ -9332,7 +9332,7 @@ export class InteractiveMode { private async handleSystemPromptCommand(): Promise { const prompt = await this.agentConnection.getSystemPrompt(); - const header = `${theme.bold("System Prompt")} ${theme.fg("dim", `(${prompt.length} chars)`)}`; + const header = `System Prompt ${theme.fg("dim", `(${prompt.length} chars)`)}`; this.chatContainer.addChild(new Spacer(1)); this.chatContainer.addChild(new Text(header, 1, 0)); @@ -9404,7 +9404,7 @@ export class InteractiveMode { private formatTracePreview(result: Extract): string { const lines = [ - theme.bold("Trace Preview"), + "Trace Preview", theme.fg("dim", "Nothing has been uploaded by this command."), "", `${theme.fg("dim", "File:")} ${result.sessionFile}`, @@ -9423,7 +9423,7 @@ export class InteractiveMode { if (result.gitCommit) { lines.push(`${theme.fg("dim", "Git commit:")} ${result.gitCommit}`); } - lines.push("", theme.bold("Raw JSONL payload preview")); + lines.push("", "Raw JSONL payload preview"); if (result.contentPreview) { lines.push(result.contentPreview); if (result.truncated) { @@ -9465,7 +9465,7 @@ export class InteractiveMode { const credential = await getPrimeAgentTraceCredential(this.modelRegistry.authStorage); const state = await this.agentConnection.getState(); const info = [ - theme.bold("Trace Sharing"), + "Trace Sharing", "", `${theme.fg("dim", "Automatic uploads:")} ${this.settingsManager.getAgentTracesEnabled() ? "Enabled" : "Disabled"}`, `${theme.fg("dim", "Credential:")} ${credential?.label ?? "Not configured"}`, @@ -9779,7 +9779,7 @@ export class InteractiveMode { const next = job.nextRunAt ? new Date(job.nextRunAt).toLocaleString() : "-"; const last = job.lastRunAt ? new Date(job.lastRunAt).toLocaleString() : "-"; const lines = [ - theme.bold("Heartbeat"), + "Heartbeat", "", `${theme.fg("dim", "Status:")} ${job.status}`, `${theme.fg("dim", "Every:")} ${job.schedule.expression}`, @@ -9811,7 +9811,7 @@ export class InteractiveMode { this.chatContainer.addChild(new Spacer(1)); this.chatContainer.addChild(new DynamicBorder()); - this.chatContainer.addChild(new Text(theme.bold(theme.fg("accent", "What's New")), 1, 0)); + this.chatContainer.addChild(new Text(theme.fg("accent", "What's New"), 1, 0)); this.chatContainer.addChild(new Spacer(1)); this.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1, this.getMarkdownThemeWithSettings())); this.chatContainer.addChild(new DynamicBorder()); diff --git a/packages/coding-agent/src/modes/shared/startup-notices.ts b/packages/coding-agent/src/modes/shared/startup-notices.ts index 69323d52a9..484848ed48 100644 --- a/packages/coding-agent/src/modes/shared/startup-notices.ts +++ b/packages/coding-agent/src/modes/shared/startup-notices.ts @@ -110,7 +110,7 @@ export async function checkTmuxKeyboardSetup(): Promise { export function formatUpdateAvailableNotice(newVersion: string): string { return ( - `${theme.bold(theme.fg("accent", "Update available:"))} ` + + `${theme.fg("accent", "Update available:")} ` + `${theme.fg("muted", `v${newVersion}. Run `)}${theme.fg("accent", "/update")}` ); } @@ -118,7 +118,7 @@ export function formatUpdateAvailableNotice(newVersion: string): string { export function formatPackageUpdateNotice(packages: string[]): string { const packageList = packages.join(", "); return ( - `${theme.bold(theme.fg("warning", "Package updates available:"))} ` + + `${theme.fg("warning", "Package updates available:")} ` + `${theme.fg("muted", `${packageList}. Run `)}${theme.fg("accent", "/update --extensions")}` ); } diff --git a/packages/coding-agent/test/bash-execution-width.test.ts b/packages/coding-agent/test/bash-execution-width.test.ts index e0f98cfa73..41794bdc8c 100644 --- a/packages/coding-agent/test/bash-execution-width.test.ts +++ b/packages/coding-agent/test/bash-execution-width.test.ts @@ -1,7 +1,7 @@ import { visibleWidth } from "@earendil-works/pi-tui"; import { beforeAll, describe, expect, it } from "vitest"; import { BashExecutionComponent } from "../src/modes/interactive/components/bash-execution.js"; -import { initTheme } from "../src/modes/interactive/theme/theme.js"; +import { initTheme, theme } from "../src/modes/interactive/theme/theme.js"; /** Minimal TUI stub that only exposes terminal.columns */ function createTuiStub(columns: number): { columns: number; stub: any } { @@ -87,4 +87,14 @@ describe("BashExecutionComponent width handling (#2569)", () => { expect(rendered).toContain("failed: spawn failure"); expect(rendered).not.toContain("Running..."); }); + + it("renders the command header at normal weight with its color kept", () => { + const { stub } = createTuiStub(120); + const component = new BashExecutionComponent("npm test", stub); + component.setComplete(0, false); + + const raw = component.render(120).join("\n"); + expect(raw).toContain(theme.fg("bashMode", "$ npm test")); + expect(raw).not.toContain(""); + }); }); From 209b1d7e936bc9fbbc3255cf14b6f5823c0d21ba Mon Sep 17 00:00:00 2001 From: Kevin Thomas Date: Thu, 10 Sep 2026 00:30:21 -0700 Subject: [PATCH 06/24] fix(tui): refine the conversation row text hierarchy --- .../coding-agent/.changes/ui-chat-output.md | 1 + packages/coding-agent/src/core/tools/bash.ts | 10 +- packages/coding-agent/src/core/tools/edit.ts | 2 +- .../interactive/components/agent-message.ts | 4 +- .../components/assistant-message.ts | 15 ++- .../components/conversation-components.ts | 2 +- .../interactive/components/ipython-cell.ts | 6 +- .../src/modes/interactive/interactive-mode.ts | 2 +- .../test/assistant-message.test.ts | 12 +- .../test/event-row-hierarchy.test.ts | 105 ++++++++++++++++++ .../test/ipython-cell-preview.test.ts | 3 + 11 files changed, 138 insertions(+), 24 deletions(-) create mode 100644 packages/coding-agent/test/event-row-hierarchy.test.ts diff --git a/packages/coding-agent/.changes/ui-chat-output.md b/packages/coding-agent/.changes/ui-chat-output.md index c19e5efc69..3aaf7eeb14 100644 --- a/packages/coding-agent/.changes/ui-chat-output.md +++ b/packages/coding-agent/.changes/ui-chat-output.md @@ -2,3 +2,4 @@ - Changed the thinking block styling: the "Thinking..." label is no longer bold, and the visible reasoning trace renders one step dimmer while the label and recap keep the thinkingText color. - Changed collapsed tool-call previews to render plain and dim instead of green or syntax-highlighted code while expanded blocks keep full highlighting, and moved prime-theme inline code to a darker neutral so it stays distinct from body text. - Changed decorative bold text in the conversation surface to normal weight — tool-call labels, bash command headers, section and panel titles, picker headers, startup notices, and the /btw question — keeping bold only where it marks state or a single critical item (selected rows, active tabs, the login verification code). +- Changed the conversation row hierarchy: the thinking label reads "Thinking:" without bold or a separator dot, thinking recaps and traces render in the dim tone, and trailing detail on event rows (agent-message participant and previews, tool command previews, line counts, durations) renders dim while leading labels keep their colors. diff --git a/packages/coding-agent/src/core/tools/bash.ts b/packages/coding-agent/src/core/tools/bash.ts index 8baa8f70d0..6143e6f98c 100644 --- a/packages/coding-agent/src/core/tools/bash.ts +++ b/packages/coding-agent/src/core/tools/bash.ts @@ -177,7 +177,7 @@ function formatDuration(ms: number): string { function formatBashCall(args: { command?: string; timeout?: number } | undefined): string { const command = str(args?.command); const timeout = args?.timeout as number | undefined; - const timeoutSuffix = timeout ? theme.fg("muted", ` (timeout ${timeout}s)`) : ""; + const timeoutSuffix = timeout ? theme.fg("dim", ` (timeout ${timeout}s)`) : ""; let commandDisplay: string; if (command === null) { commandDisplay = invalidArgText(theme); @@ -188,7 +188,7 @@ function formatBashCall(args: { command?: string; timeout?: number } | undefined } else { commandDisplay = theme.fg("toolOutput", "..."); } - return theme.fg("toolTitle", `$ ${commandDisplay}`) + timeoutSuffix; + return theme.fg("dim", `$ ${commandDisplay}`) + timeoutSuffix; } function rebuildBashResultRenderComponent( @@ -228,8 +228,8 @@ function rebuildBashResultRenderComponent( } if (state.cachedSkipped && state.cachedSkipped > 0) { const hint = showExpandHint - ? `${theme.fg("muted", `... ${state.cachedSkipped} earlier lines`)} ${expandCollapseHint("app.tools.expand", false)}` - : theme.fg("muted", `... (${state.cachedSkipped} earlier lines)`); + ? `${theme.fg("dim", `... ${state.cachedSkipped} earlier lines`)} ${expandCollapseHint("app.tools.expand", false)}` + : theme.fg("dim", `... (${state.cachedSkipped} earlier lines)`); return ["", truncateToWidth(hint, width, "..."), ...(state.cachedLines ?? [])]; } return ["", ...(state.cachedLines ?? [])]; @@ -265,7 +265,7 @@ function rebuildBashResultRenderComponent( if (startedAt !== undefined) { const label = options.isPartial ? "Elapsed" : "Took"; const endTime = endedAt ?? Date.now(); - component.addChild(new Text(`\n${theme.fg("muted", `${label} ${formatDuration(endTime - startedAt)}`)}`, 0, 0)); + component.addChild(new Text(`\n${theme.fg("dim", `${label} ${formatDuration(endTime - startedAt)}`)}`, 0, 0)); } } diff --git a/packages/coding-agent/src/core/tools/edit.ts b/packages/coding-agent/src/core/tools/edit.ts index 4eb65f5877..278acac535 100644 --- a/packages/coding-agent/src/core/tools/edit.ts +++ b/packages/coding-agent/src/core/tools/edit.ts @@ -202,7 +202,7 @@ function formatEditCall( const invalidArg = invalidArgText(theme); const rawPath = str(args?.file_path ?? args?.path); const path = rawPath !== null ? shortenPath(rawPath) : null; - const pathDisplay = path === null ? invalidArg : path ? theme.fg("accent", path) : theme.fg("toolOutput", "..."); + const pathDisplay = path === null ? invalidArg : path ? theme.fg("dim", path) : theme.fg("toolOutput", "..."); return `${theme.fg("toolTitle", "edit")} ${pathDisplay}`; } diff --git a/packages/coding-agent/src/modes/interactive/components/agent-message.ts b/packages/coding-agent/src/modes/interactive/components/agent-message.ts index cf6845a495..8e45f8553b 100644 --- a/packages/coding-agent/src/modes/interactive/components/agent-message.ts +++ b/packages/coding-agent/src/modes/interactive/components/agent-message.ts @@ -18,9 +18,9 @@ function collapseText(text: string): string { /** `◆