diff --git a/packages/coding-agent/.changes/error-message-warning-icon.md b/packages/coding-agent/.changes/error-message-warning-icon.md new file mode 100644 index 000000000..d824da2b5 --- /dev/null +++ b/packages/coding-agent/.changes/error-message-warning-icon.md @@ -0,0 +1 @@ +- Added the `⚠` icon prefix to `showError` messages in the interactive TUI, matching the existing `showWarning` treatment ([ENG-6159](https://linear.app/primeintellect/issue/ENG-6159)). diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index c6eb0cf5f..fa5a21313 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -7576,7 +7576,7 @@ export class InteractiveMode { showError(errorMessage: string): void { this.chatContainer.addChild(new Spacer(1)); - this.chatContainer.addChild(new Text(theme.fg("error", `Error: ${errorMessage}`), 1, 0)); + this.chatContainer.addChild(new Text(theme.fg("error", `⚠ Error: ${errorMessage}`), 1, 0)); this.ui.requestRender(); } diff --git a/packages/coding-agent/test/interactive-mode-status.test.ts b/packages/coding-agent/test/interactive-mode-status.test.ts index 610b84b41..1bd0b4e7d 100644 --- a/packages/coding-agent/test/interactive-mode-status.test.ts +++ b/packages/coding-agent/test/interactive-mode-status.test.ts @@ -1301,6 +1301,17 @@ describe("InteractiveMode MCP command", () => { expect(manager.getGlobalMcpServers()).toHaveProperty("fetch"); }); + test("prefixes showError output with the warning icon like showWarning", () => { + const fakeThis = createRenderedMcpHarness(SettingsManager.inMemory({})); + + fakeThis.showError("boom"); + expect(normalizeRenderedOutput(fakeThis.chatContainer)).toContain("⚠ Error: boom"); + + fakeThis.chatContainer.clear(); + fakeThis.showWarning("careful"); + expect(normalizeRenderedOutput(fakeThis.chatContainer)).toContain("⚠ careful"); + }); + test("refreshes MCP providers before deferring a changed command while busy", async () => { const manager = SettingsManager.inMemory({}); const events: string[] = []; diff --git a/packages/tui/.changes/markdown-deep-heading-render.md b/packages/tui/.changes/markdown-deep-heading-render.md new file mode 100644 index 000000000..2849da998 --- /dev/null +++ b/packages/tui/.changes/markdown-deep-heading-render.md @@ -0,0 +1 @@ +- Fixed deep markdown headings to render as formatted headings at all six levels instead of dumping raw `#` prefixes for H3-H6: h1 stays bold+underlined, h2/h3 are bold, h4 is bold+italic, and h5/h6 are italic, all in the heading color ([ENG-6159](https://linear.app/primeintellect/issue/ENG-6159)). diff --git a/packages/tui/src/components/markdown.ts b/packages/tui/src/components/markdown.ts index 704ce10b6..cf6919524 100644 --- a/packages/tui/src/components/markdown.ts +++ b/packages/tui/src/components/markdown.ts @@ -427,7 +427,6 @@ export class Markdown implements Component { switch (token.type) { case "heading": { const headingLevel = token.depth; - const headingPrefix = `${"#".repeat(headingLevel)} `; // Build a heading-specific style context so inline tokens (codespan, bold, etc.) // restore heading styling after their own ANSI resets instead of falling back to @@ -435,6 +434,10 @@ export class Markdown implements Component { let headingStyleFn: (text: string) => string; if (headingLevel === 1) { headingStyleFn = (text: string) => this.theme.heading(this.theme.bold(this.theme.underline(text))); + } else if (headingLevel >= 5) { + headingStyleFn = (text: string) => this.theme.heading(this.theme.italic(text)); + } else if (headingLevel === 4) { + headingStyleFn = (text: string) => this.theme.heading(this.theme.bold(this.theme.italic(text))); } else { headingStyleFn = (text: string) => this.theme.heading(this.theme.bold(text)); } @@ -445,8 +448,7 @@ export class Markdown implements Component { }; const headingText = this.renderInlineTokens(token.tokens || [], headingStyleContext); - const styledHeading = headingLevel >= 3 ? headingStyleFn(headingPrefix) + headingText : headingText; - lines.push(styledHeading); + lines.push(headingText); if (nextTokenType && nextTokenType !== "space") { lines.push(""); // Add spacing after headings (unless space token follows) } diff --git a/packages/tui/test/markdown.test.ts b/packages/tui/test/markdown.test.ts index c2e2b90d3..8c82fb5f3 100644 --- a/packages/tui/test/markdown.test.ts +++ b/packages/tui/test/markdown.test.ts @@ -1032,6 +1032,58 @@ bar`, }); }); + describe("Heading levels", () => { + it("should render every heading level without a raw # prefix", () => { + const markdown = new Markdown("# H1\n## H2\n### H3\n#### H4\n##### H5\n###### H6", 0, 0, defaultMarkdownTheme); + + const plainLines = markdown.render(80).map((line) => line.replace(/\x1b\[[0-9;]*m/g, "").trimEnd()); + + for (const heading of ["H1", "H2", "H3", "H4", "H5", "H6"]) { + assert.ok(plainLines.includes(heading), `Should render "${heading}" as heading content`); + } + for (const line of plainLines) { + assert.ok(!/^#{1,6} /.test(line), `Raw heading prefix should not render: ${JSON.stringify(line)}`); + } + }); + + it("should taper emphasis with heading depth", () => { + const markdown = new Markdown("# h1\n## h2\n### h3\n#### h4\n##### h5\n###### h6", 0, 0, defaultMarkdownTheme); + + const lines = markdown.render(80); + const byContent: Record = {}; + for (const line of lines) { + const plain = line.replace(/\x1b\[[0-9;]*m/g, "").trimEnd(); + byContent[plain] = line; + } + + assert.ok(byContent.h1.includes("\x1b[4m"), "h1 should be underlined"); + assert.ok(byContent.h2.includes("\x1b[1m"), "h2 should be bold"); + assert.ok(byContent.h3.includes("\x1b[1m"), "h3 should be bold"); + assert.ok(!byContent.h3.includes("\x1b[3m"), "h3 should not be italic"); + assert.ok(!byContent.h3.includes("\x1b[4m"), "h3 should not be underlined"); + assert.ok(byContent.h4.includes("\x1b[3m"), "h4 should be italic"); + assert.ok(byContent.h4.includes("\x1b[1m"), "h4 should keep bold"); + assert.ok(byContent.h5.includes("\x1b[3m"), "h5 should be italic"); + assert.ok(byContent.h6.includes("\x1b[3m"), "h6 should be italic"); + }); + + it("should keep deep heading styling across inline code spans", () => { + const markdown = new Markdown("##### Deep heading with `code` inside", 0, 0, defaultMarkdownTheme); + + const lines = markdown.render(80); + const joinedOutput = lines.join("\n"); + + assert.ok(joinedOutput.includes("\x1b[33m"), "Should have yellow for inline code"); + + const afterCodeIndex = joinedOutput.indexOf("inside"); + assert.ok(afterCodeIndex > 0, "Should contain text after inline code"); + + const precedingChunk = joinedOutput.slice(Math.max(0, afterCodeIndex - 40), afterCodeIndex); + assert.ok(precedingChunk.includes("\x1b[3m"), `Should re-apply italic for h5: ${precedingChunk}`); + assert.ok(precedingChunk.includes("\x1b[36m"), `Should re-apply heading color for h5: ${precedingChunk}`); + }); + }); + describe("Strikethrough syntax", () => { it("should render ~~text~~ as strikethrough", () => { const markdown = new Markdown("Use ~~strikethrough~~ here", 0, 0, defaultMarkdownTheme);