diff --git a/package-lock.json b/package-lock.json index a23c4d28..f7594c13 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3195,11 +3195,11 @@ }, "packages/agent": { "name": "@mariozechner/pi-agent", - "version": "0.7.17", + "version": "0.7.18", "license": "MIT", "dependencies": { - "@mariozechner/pi-ai": "^0.7.16", - "@mariozechner/pi-tui": "^0.7.16" + "@mariozechner/pi-ai": "^0.7.17", + "@mariozechner/pi-tui": "^0.7.17" }, "devDependencies": { "@types/node": "^24.3.0", @@ -3225,7 +3225,7 @@ }, "packages/ai": { "name": "@mariozechner/pi-ai", - "version": "0.7.17", + "version": "0.7.18", "license": "MIT", "dependencies": { "@anthropic-ai/sdk": "^0.61.0", @@ -3272,11 +3272,11 @@ }, "packages/coding-agent": { "name": "@mariozechner/pi-coding-agent", - "version": "0.7.17", + "version": "0.7.18", "license": "MIT", "dependencies": { - "@mariozechner/pi-agent": "^0.7.16", - "@mariozechner/pi-ai": "^0.7.16", + "@mariozechner/pi-agent": "^0.7.17", + "@mariozechner/pi-ai": "^0.7.17", "chalk": "^5.5.0", "diff": "^8.0.2", "glob": "^11.0.3" @@ -3319,10 +3319,10 @@ }, "packages/pods": { "name": "@mariozechner/pi", - "version": "0.7.17", + "version": "0.7.18", "license": "MIT", "dependencies": { - "@mariozechner/pi-agent": "^0.7.16", + "@mariozechner/pi-agent": "^0.7.17", "chalk": "^5.5.0" }, "bin": { @@ -3345,7 +3345,7 @@ }, "packages/proxy": { "name": "@mariozechner/pi-proxy", - "version": "0.7.17", + "version": "0.7.18", "dependencies": { "@hono/node-server": "^1.14.0", "hono": "^4.6.16" @@ -3361,7 +3361,7 @@ }, "packages/tui": { "name": "@mariozechner/pi-tui", - "version": "0.7.17", + "version": "0.7.18", "license": "MIT", "dependencies": { "@types/mime-types": "^2.1.4", @@ -3400,12 +3400,12 @@ }, "packages/web-ui": { "name": "@mariozechner/pi-web-ui", - "version": "0.7.17", + "version": "0.7.18", "license": "MIT", "dependencies": { "@lmstudio/sdk": "^1.5.0", - "@mariozechner/pi-ai": "^0.7.16", - "@mariozechner/pi-tui": "^0.7.16", + "@mariozechner/pi-ai": "^0.7.17", + "@mariozechner/pi-tui": "^0.7.17", "docx-preview": "^0.3.7", "jszip": "^3.10.1", "lucide": "^0.544.0", diff --git a/packages/agent/package.json b/packages/agent/package.json index a3cbab51..8b7852ab 100644 --- a/packages/agent/package.json +++ b/packages/agent/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-agent", - "version": "0.7.17", + "version": "0.7.18", "description": "General-purpose agent with transport abstraction, state management, and attachment support", "type": "module", "main": "./dist/index.js", @@ -18,8 +18,8 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@mariozechner/pi-ai": "^0.7.17", - "@mariozechner/pi-tui": "^0.7.17" + "@mariozechner/pi-ai": "^0.7.18", + "@mariozechner/pi-tui": "^0.7.18" }, "keywords": [ "ai", diff --git a/packages/ai/package.json b/packages/ai/package.json index fa530df2..8f09dbe2 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-ai", - "version": "0.7.17", + "version": "0.7.18", "description": "Unified LLM API with automatic model discovery and provider configuration", "type": "module", "main": "./dist/index.js", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index c170a501..4d0547df 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,13 @@ ## [Unreleased] +## [0.7.18] - 2025-11-18 + +### Fixed + +- **Bash Tool Error Handling**: Bash tool now properly throws errors for failed commands (non-zero exit codes), timeouts, and aborted executions. This ensures tool execution components display with red background when bash commands fail. +- **Thinking Traces Styling**: Thinking traces now maintain gray italic styling throughout, even when containing inline code blocks, bold text, or other inline formatting + ## [0.7.17] - 2025-11-18 ### Added diff --git a/packages/coding-agent/package.json b/packages/coding-agent/package.json index bfd7d8d9..80a40df1 100644 --- a/packages/coding-agent/package.json +++ b/packages/coding-agent/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-coding-agent", - "version": "0.7.17", + "version": "0.7.18", "description": "Coding agent CLI with read, bash, edit, write tools and session management", "type": "module", "bin": { @@ -21,8 +21,8 @@ "prepublishOnly": "npm run clean && npm run build" }, "dependencies": { - "@mariozechner/pi-agent": "^0.7.17", - "@mariozechner/pi-ai": "^0.7.17", + "@mariozechner/pi-agent": "^0.7.18", + "@mariozechner/pi-ai": "^0.7.18", "chalk": "^5.5.0", "diff": "^8.0.2", "glob": "^11.0.3" diff --git a/packages/coding-agent/src/tools/bash.ts b/packages/coding-agent/src/tools/bash.ts index 854fedb4..4171f95c 100644 --- a/packages/coding-agent/src/tools/bash.ts +++ b/packages/coding-agent/src/tools/bash.ts @@ -137,7 +137,7 @@ export const bashTool: AgentTool = { } if (output) output += "\n\n"; output += "Command aborted"; - resolve({ content: [{ type: "text", text: `Command failed\n\n${output}` }], details: undefined }); + _reject(new Error(output)); return; } @@ -150,7 +150,7 @@ export const bashTool: AgentTool = { } if (output) output += "\n\n"; output += `Command timed out after ${timeout} seconds`; - resolve({ content: [{ type: "text", text: `Command failed\n\n${output}` }], details: undefined }); + _reject(new Error(output)); return; } @@ -163,10 +163,7 @@ export const bashTool: AgentTool = { if (code !== 0 && code !== null) { if (output) output += "\n\n"; - resolve({ - content: [{ type: "text", text: `Command failed\n\n${output}Command exited with code ${code}` }], - details: undefined, - }); + _reject(new Error(`${output}Command exited with code ${code}`)); } else { resolve({ content: [{ type: "text", text: output || "(no output)" }], details: undefined }); } diff --git a/packages/coding-agent/src/tui/assistant-message.ts b/packages/coding-agent/src/tui/assistant-message.ts index 587a2b92..49f8e1d4 100644 --- a/packages/coding-agent/src/tui/assistant-message.ts +++ b/packages/coding-agent/src/tui/assistant-message.ts @@ -38,12 +38,16 @@ export class AssistantMessageComponent extends Container { if (content.type === "text" && content.text.trim()) { // Assistant text messages with no background - trim the text // Set paddingY=0 to avoid extra spacing before tool executions - this.contentContainer.addChild(new Markdown(content.text.trim(), undefined, undefined, undefined, 1, 0)); + this.contentContainer.addChild(new Markdown(content.text.trim(), 1, 0)); } else if (content.type === "thinking" && content.thinking.trim()) { // Thinking traces in dark gray italic - // Use Markdown component because it preserves ANSI codes across wrapped lines - const thinkingText = chalk.gray.italic(content.thinking); - this.contentContainer.addChild(new Markdown(thinkingText, undefined, undefined, undefined, 1, 0)); + // Use Markdown component with default text style for consistent styling + this.contentContainer.addChild( + new Markdown(content.thinking.trim(), 1, 0, { + color: "gray", + italic: true, + }), + ); this.contentContainer.addChild(new Spacer(1)); } } diff --git a/packages/coding-agent/src/tui/tui-renderer.ts b/packages/coding-agent/src/tui/tui-renderer.ts index 034cf441..12c826a9 100644 --- a/packages/coding-agent/src/tui/tui-renderer.ts +++ b/packages/coding-agent/src/tui/tui-renderer.ts @@ -186,7 +186,7 @@ export class TuiRenderer { this.ui.addChild(new DynamicBorder(chalk.cyan)); this.ui.addChild(new Text(chalk.bold.cyan("What's New"), 1, 0)); this.ui.addChild(new Spacer(1)); - this.ui.addChild(new Markdown(this.changelogMarkdown.trim(), undefined, undefined, undefined, 1, 0)); + this.ui.addChild(new Markdown(this.changelogMarkdown.trim(), 1, 0)); this.ui.addChild(new Spacer(1)); this.ui.addChild(new DynamicBorder(chalk.cyan)); } @@ -989,7 +989,7 @@ export class TuiRenderer { this.chatContainer.addChild(new DynamicBorder(chalk.cyan)); this.ui.addChild(new Text(chalk.bold.cyan("What's New"), 1, 0)); this.ui.addChild(new Spacer(1)); - this.chatContainer.addChild(new Markdown(changelogMarkdown)); + this.chatContainer.addChild(new Markdown(changelogMarkdown, 1, 1)); this.chatContainer.addChild(new DynamicBorder(chalk.cyan)); this.ui.requestRender(); } diff --git a/packages/coding-agent/src/tui/user-message.ts b/packages/coding-agent/src/tui/user-message.ts index 7369c459..a6c5af22 100644 --- a/packages/coding-agent/src/tui/user-message.ts +++ b/packages/coding-agent/src/tui/user-message.ts @@ -15,7 +15,7 @@ export class UserMessageComponent extends Container { } // User messages with dark gray background - this.markdown = new Markdown(text, undefined, undefined, { r: 52, g: 53, b: 65 }); + this.markdown = new Markdown(text, 1, 1, { bgColor: "#343541" }); this.addChild(this.markdown); } } diff --git a/packages/pods/package.json b/packages/pods/package.json index 9a4ccdce..7c1910fb 100644 --- a/packages/pods/package.json +++ b/packages/pods/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi", - "version": "0.7.17", + "version": "0.7.18", "description": "CLI tool for managing vLLM deployments on GPU pods", "type": "module", "bin": { @@ -34,7 +34,7 @@ "node": ">=20.0.0" }, "dependencies": { - "@mariozechner/pi-agent": "^0.7.17", + "@mariozechner/pi-agent": "^0.7.18", "chalk": "^5.5.0" }, "devDependencies": {} diff --git a/packages/proxy/package.json b/packages/proxy/package.json index 83cca1fb..e9b0fa1f 100644 --- a/packages/proxy/package.json +++ b/packages/proxy/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-proxy", - "version": "0.7.17", + "version": "0.7.18", "type": "module", "description": "CORS and authentication proxy for pi-ai", "main": "dist/index.js", diff --git a/packages/tui/package.json b/packages/tui/package.json index cab23920..d281a057 100644 --- a/packages/tui/package.json +++ b/packages/tui/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-tui", - "version": "0.7.17", + "version": "0.7.18", "description": "Terminal User Interface library with differential rendering for efficient text-based applications", "type": "module", "main": "dist/index.js", diff --git a/packages/tui/src/components/markdown.ts b/packages/tui/src/components/markdown.ts index 5795a469..a50cae89 100644 --- a/packages/tui/src/components/markdown.ts +++ b/packages/tui/src/components/markdown.ts @@ -1,55 +1,46 @@ -import chalk from "chalk"; +import { Chalk } from "chalk"; import { marked, type Token } from "marked"; import type { Component } from "../tui.js"; import { visibleWidth } from "../utils.js"; -type Color = - | "black" - | "red" - | "green" - | "yellow" - | "blue" - | "magenta" - | "cyan" - | "white" - | "gray" - | "bgBlack" - | "bgRed" - | "bgGreen" - | "bgYellow" - | "bgBlue" - | "bgMagenta" - | "bgCyan" - | "bgWhite" - | "bgGray"; +// Use a chalk instance with color level 3 for consistent ANSI output +const colorChalk = new Chalk({ level: 3 }); + +/** + * Default text styling for markdown content. + * Applied to all text unless overridden by markdown formatting. + */ +export interface DefaultTextStyle { + /** Foreground color - named color or hex string like "#ff0000" */ + color?: string; + /** Background color - named color or hex string like "#ff0000" */ + bgColor?: string; + /** Bold text */ + bold?: boolean; + /** Italic text */ + italic?: boolean; + /** Strikethrough text */ + strikethrough?: boolean; + /** Underline text */ + underline?: boolean; +} export class Markdown implements Component { private text: string; - private bgColor?: Color; - private fgColor?: Color; - private customBgRgb?: { r: number; g: number; b: number }; private paddingX: number; // Left/right padding private paddingY: number; // Top/bottom padding + private defaultTextStyle?: DefaultTextStyle; // Cache for rendered output private cachedText?: string; private cachedWidth?: number; private cachedLines?: string[]; - constructor( - text: string = "", - bgColor?: Color, - fgColor?: Color, - customBgRgb?: { r: number; g: number; b: number }, - paddingX: number = 1, - paddingY: number = 1, - ) { + constructor(text: string = "", paddingX: number = 1, paddingY: number = 1, defaultTextStyle?: DefaultTextStyle) { this.text = text; - this.bgColor = bgColor; - this.fgColor = fgColor; - this.customBgRgb = customBgRgb; this.paddingX = paddingX; this.paddingY = paddingY; + this.defaultTextStyle = defaultTextStyle; } setText(text: string): void { @@ -60,30 +51,6 @@ export class Markdown implements Component { this.cachedLines = undefined; } - setBgColor(bgColor?: Color): void { - this.bgColor = bgColor; - // Invalidate cache when color changes - this.cachedText = undefined; - this.cachedWidth = undefined; - this.cachedLines = undefined; - } - - setFgColor(fgColor?: Color): void { - this.fgColor = fgColor; - // Invalidate cache when color changes - this.cachedText = undefined; - this.cachedWidth = undefined; - this.cachedLines = undefined; - } - - setCustomBgRgb(customBgRgb?: { r: number; g: number; b: number }): void { - this.customBgRgb = customBgRgb; - // Invalidate cache when color changes - this.cachedText = undefined; - this.cachedWidth = undefined; - this.cachedLines = undefined; - } - render(width: number): string[] { // Check cache if (this.cachedLines && this.cachedText === this.text && this.cachedWidth === width) { @@ -125,7 +92,7 @@ export class Markdown implements Component { wrappedLines.push(...this.wrapLine(line, contentWidth)); } - // Add padding and apply colors + // Add padding and apply background color if specified const leftPad = " ".repeat(this.paddingX); const paddedLines: string[] = []; @@ -139,16 +106,9 @@ export class Markdown implements Component { // Add left padding, content, and right padding let paddedLine = leftPad + line + rightPad; - // Apply foreground color if specified - if (this.fgColor) { - paddedLine = (chalk as any)[this.fgColor](paddedLine); - } - - // Apply background color if specified - if (this.customBgRgb) { - paddedLine = chalk.bgRgb(this.customBgRgb.r, this.customBgRgb.g, this.customBgRgb.b)(paddedLine); - } else if (this.bgColor) { - paddedLine = (chalk as any)[this.bgColor](paddedLine); + // Apply background color to entire line if specified + if (this.defaultTextStyle?.bgColor) { + paddedLine = this.applyBgColor(paddedLine); } paddedLines.push(paddedLine); @@ -158,25 +118,15 @@ export class Markdown implements Component { const emptyLine = " ".repeat(width); const topPadding: string[] = []; for (let i = 0; i < this.paddingY; i++) { - let emptyPaddedLine = emptyLine; - if (this.customBgRgb) { - emptyPaddedLine = chalk.bgRgb(this.customBgRgb.r, this.customBgRgb.g, this.customBgRgb.b)(emptyPaddedLine); - } else if (this.bgColor) { - emptyPaddedLine = (chalk as any)[this.bgColor](emptyPaddedLine); - } - topPadding.push(emptyPaddedLine); + const paddedEmptyLine = this.defaultTextStyle?.bgColor ? this.applyBgColor(emptyLine) : emptyLine; + topPadding.push(paddedEmptyLine); } // Add bottom padding (empty lines) const bottomPadding: string[] = []; for (let i = 0; i < this.paddingY; i++) { - let emptyPaddedLine = emptyLine; - if (this.customBgRgb) { - emptyPaddedLine = chalk.bgRgb(this.customBgRgb.r, this.customBgRgb.g, this.customBgRgb.b)(emptyPaddedLine); - } else if (this.bgColor) { - emptyPaddedLine = (chalk as any)[this.bgColor](emptyPaddedLine); - } - bottomPadding.push(emptyPaddedLine); + const paddedEmptyLine = this.defaultTextStyle?.bgColor ? this.applyBgColor(emptyLine) : emptyLine; + bottomPadding.push(paddedEmptyLine); } // Combine top padding, content, and bottom padding @@ -190,6 +140,85 @@ export class Markdown implements Component { return result.length > 0 ? result : [""]; } + /** + * Apply only background color from default style. + * Used for padding lines that don't have text content. + */ + private applyBgColor(text: string): string { + if (!this.defaultTextStyle?.bgColor) { + return text; + } + + if (this.defaultTextStyle.bgColor.startsWith("#")) { + // Hex color + const hex = this.defaultTextStyle.bgColor.substring(1); + const r = Number.parseInt(hex.substring(0, 2), 16); + const g = Number.parseInt(hex.substring(2, 4), 16); + const b = Number.parseInt(hex.substring(4, 6), 16); + return colorChalk.bgRgb(r, g, b)(text); + } + // Named background color (bgRed, bgBlue, etc.) + return (colorChalk as any)[this.defaultTextStyle.bgColor](text); + } + + /** + * Apply default text style to a string. + * This is the base styling applied to all text content. + */ + private applyDefaultStyle(text: string): string { + if (!this.defaultTextStyle) { + return text; + } + + let styled = text; + + // Apply color + if (this.defaultTextStyle.color) { + if (this.defaultTextStyle.color.startsWith("#")) { + // Hex color + const hex = this.defaultTextStyle.color.substring(1); + const r = Number.parseInt(hex.substring(0, 2), 16); + const g = Number.parseInt(hex.substring(2, 4), 16); + const b = Number.parseInt(hex.substring(4, 6), 16); + styled = colorChalk.rgb(r, g, b)(styled); + } else { + // Named color + styled = (colorChalk as any)[this.defaultTextStyle.color](styled); + } + } + + // Apply background color + if (this.defaultTextStyle.bgColor) { + if (this.defaultTextStyle.bgColor.startsWith("#")) { + // Hex color + const hex = this.defaultTextStyle.bgColor.substring(1); + const r = Number.parseInt(hex.substring(0, 2), 16); + const g = Number.parseInt(hex.substring(2, 4), 16); + const b = Number.parseInt(hex.substring(4, 6), 16); + styled = colorChalk.bgRgb(r, g, b)(styled); + } else { + // Named background color (bgRed, bgBlue, etc.) + styled = (colorChalk as any)[this.defaultTextStyle.bgColor](styled); + } + } + + // Apply text decorations + if (this.defaultTextStyle.bold) { + styled = colorChalk.bold(styled); + } + if (this.defaultTextStyle.italic) { + styled = colorChalk.italic(styled); + } + if (this.defaultTextStyle.strikethrough) { + styled = colorChalk.strikethrough(styled); + } + if (this.defaultTextStyle.underline) { + styled = colorChalk.underline(styled); + } + + return styled; + } + private renderToken(token: Token, width: number, nextTokenType?: string): string[] { const lines: string[] = []; @@ -199,11 +228,11 @@ export class Markdown implements Component { const headingPrefix = "#".repeat(headingLevel) + " "; const headingText = this.renderInlineTokens(token.tokens || []); if (headingLevel === 1) { - lines.push(chalk.bold.underline.yellow(headingText)); + lines.push(colorChalk.bold.underline.yellow(headingText)); } else if (headingLevel === 2) { - lines.push(chalk.bold.yellow(headingText)); + lines.push(colorChalk.bold.yellow(headingText)); } else { - lines.push(chalk.bold(headingPrefix + headingText)); + lines.push(colorChalk.bold(headingPrefix + headingText)); } lines.push(""); // Add spacing after headings break; @@ -220,13 +249,13 @@ export class Markdown implements Component { } case "code": { - lines.push(chalk.gray("```" + (token.lang || ""))); + lines.push(colorChalk.gray("```" + (token.lang || ""))); // Split code by newlines and style each line const codeLines = token.text.split("\n"); for (const codeLine of codeLines) { - lines.push(chalk.dim(" ") + chalk.green(codeLine)); + lines.push(colorChalk.dim(" ") + colorChalk.green(codeLine)); } - lines.push(chalk.gray("```")); + lines.push(colorChalk.gray("```")); lines.push(""); // Add spacing after code blocks break; } @@ -249,14 +278,14 @@ export class Markdown implements Component { const quoteText = this.renderInlineTokens(token.tokens || []); const quoteLines = quoteText.split("\n"); for (const quoteLine of quoteLines) { - lines.push(chalk.gray("│ ") + chalk.italic(quoteLine)); + lines.push(colorChalk.gray("│ ") + colorChalk.italic(quoteLine)); } lines.push(""); // Add spacing after blockquotes break; } case "hr": - lines.push(chalk.gray("─".repeat(Math.min(width, 80)))); + lines.push(colorChalk.gray("─".repeat(Math.min(width, 80)))); lines.push(""); // Add spacing after horizontal rules break; @@ -289,29 +318,44 @@ export class Markdown implements Component { if (token.tokens && token.tokens.length > 0) { result += this.renderInlineTokens(token.tokens); } else { - result += token.text; + // Apply default style to plain text + result += this.applyDefaultStyle(token.text); } break; - case "strong": - result += chalk.bold(this.renderInlineTokens(token.tokens || [])); + case "strong": { + // Apply bold, then reapply default style after + const boldContent = this.renderInlineTokens(token.tokens || []); + result += colorChalk.bold(boldContent) + this.applyDefaultStyle(""); break; + } - case "em": - result += chalk.italic(this.renderInlineTokens(token.tokens || [])); + case "em": { + // Apply italic, then reapply default style after + const italicContent = this.renderInlineTokens(token.tokens || []); + result += colorChalk.italic(italicContent) + this.applyDefaultStyle(""); break; + } case "codespan": - result += chalk.gray("`") + chalk.cyan(token.text) + chalk.gray("`"); + // Apply code styling, then reapply default style after + result += + colorChalk.gray("`") + + colorChalk.cyan(token.text) + + colorChalk.gray("`") + + this.applyDefaultStyle(""); break; case "link": { const linkText = this.renderInlineTokens(token.tokens || []); // If link text matches href, only show the link once if (linkText === token.href) { - result += chalk.underline.blue(linkText); + result += colorChalk.underline.blue(linkText) + this.applyDefaultStyle(""); } else { - result += chalk.underline.blue(linkText) + chalk.gray(` (${token.href})`); + result += + colorChalk.underline.blue(linkText) + + colorChalk.gray(` (${token.href})`) + + this.applyDefaultStyle(""); } break; } @@ -320,14 +364,16 @@ export class Markdown implements Component { result += "\n"; break; - case "del": - result += chalk.strikethrough(this.renderInlineTokens(token.tokens || [])); + case "del": { + const delContent = this.renderInlineTokens(token.tokens || []); + result += colorChalk.strikethrough(delContent) + this.applyDefaultStyle(""); break; + } default: // Handle any other inline token types as plain text if ("text" in token && typeof token.text === "string") { - result += token.text; + result += this.applyDefaultStyle(token.text); } } } @@ -469,7 +515,7 @@ export class Markdown implements Component { lines.push(firstLine); } else { // Regular text content - add indent and bullet - lines.push(indent + chalk.cyan(bullet) + firstLine); + lines.push(indent + colorChalk.cyan(bullet) + firstLine); } // Rest of the lines @@ -486,7 +532,7 @@ export class Markdown implements Component { } } } else { - lines.push(indent + chalk.cyan(bullet)); + lines.push(indent + colorChalk.cyan(bullet)); } } @@ -517,12 +563,12 @@ export class Markdown implements Component { lines.push(text); } else if (token.type === "code") { // Code block in list item - lines.push(chalk.gray("```" + (token.lang || ""))); + lines.push(colorChalk.gray("```" + (token.lang || ""))); const codeLines = token.text.split("\n"); for (const codeLine of codeLines) { - lines.push(chalk.dim(" ") + chalk.green(codeLine)); + lines.push(colorChalk.dim(" ") + colorChalk.green(codeLine)); } - lines.push(chalk.gray("```")); + lines.push(colorChalk.gray("```")); } else { // Other token types - try to render as inline const text = this.renderInlineTokens([token]); @@ -569,7 +615,7 @@ export class Markdown implements Component { // Render header const headerCells = token.header.map((cell, i) => { const text = this.renderInlineTokens(cell.tokens || []); - return chalk.bold(text.padEnd(columnWidths[i])); + return colorChalk.bold(text.padEnd(columnWidths[i])); }); lines.push("│ " + headerCells.join(" │ ") + " │"); diff --git a/packages/tui/test/chat-simple.ts b/packages/tui/test/chat-simple.ts index b7afef75..31baaeee 100644 --- a/packages/tui/test/chat-simple.ts +++ b/packages/tui/test/chat-simple.ts @@ -78,7 +78,7 @@ editor.onSubmit = (value: string) => { isResponding = true; editor.disableSubmit = true; - const userMessage = new Markdown(value, undefined, undefined, { r: 52, g: 53, b: 65 }); + const userMessage = new Markdown(value, 1, 1, { bgColor: "#343541" }); const children = tui.children; children.splice(children.length - 1, 0, userMessage); diff --git a/packages/tui/test/markdown.test.ts b/packages/tui/test/markdown.test.ts index f1aa7854..9ecd71c2 100644 --- a/packages/tui/test/markdown.test.ts +++ b/packages/tui/test/markdown.test.ts @@ -10,9 +10,6 @@ describe("Markdown component", () => { - Nested 1.1 - Nested 1.2 - Item 2`, - undefined, - undefined, - undefined, 0, 0, ); @@ -38,9 +35,6 @@ describe("Markdown component", () => { - Level 2 - Level 3 - Level 4`, - undefined, - undefined, - undefined, 0, 0, ); @@ -61,9 +55,6 @@ describe("Markdown component", () => { 1. Nested first 2. Nested second 2. Second`, - undefined, - undefined, - undefined, 0, 0, ); @@ -84,9 +75,6 @@ describe("Markdown component", () => { - Another nested 2. Second ordered - More nested`, - undefined, - undefined, - undefined, 0, 0, ); @@ -107,9 +95,6 @@ describe("Markdown component", () => { | --- | --- | | Alice | 30 | | Bob | 25 |`, - undefined, - undefined, - undefined, 0, 0, ); @@ -133,9 +118,6 @@ describe("Markdown component", () => { | :--- | :---: | ---: | | A | B | C | | Long text | Middle | End |`, - undefined, - undefined, - undefined, 0, 0, ); @@ -157,9 +139,6 @@ describe("Markdown component", () => { | --- | --- | | A | This is a much longer cell content | | B | Short |`, - undefined, - undefined, - undefined, 0, 0, ); @@ -187,9 +166,6 @@ describe("Markdown component", () => { | Col1 | Col2 | | --- | --- | | A | B |`, - undefined, - undefined, - undefined, 0, 0, ); @@ -207,4 +183,84 @@ describe("Markdown component", () => { assert.ok(plainLines.some((line) => line.includes("│"))); }); }); + + describe("Pre-styled text (thinking traces)", () => { + it("should preserve gray italic styling after inline code", () => { + // This replicates how thinking content is rendered in assistant-message.ts + const markdown = new Markdown("This is thinking with `inline code` and more text after", 1, 0, { + color: "gray", + italic: true, + }); + + const lines = markdown.render(80); + const joinedOutput = lines.join("\n"); + + // Should contain the inline code block + assert.ok(joinedOutput.includes("inline code")); + + // The output should have ANSI codes for gray (90) and italic (3) + assert.ok(joinedOutput.includes("\x1b[90m"), "Should have gray color code"); + assert.ok(joinedOutput.includes("\x1b[3m"), "Should have italic code"); + + // Verify that after the inline code (cyan text), we reapply gray italic + const hasCyan = joinedOutput.includes("\x1b[36m"); // cyan + assert.ok(hasCyan, "Should have cyan for inline code"); + }); + + it("should preserve gray italic styling after bold text", () => { + const markdown = new Markdown("This is thinking with **bold text** and more after", 1, 0, { + color: "gray", + italic: true, + }); + + const lines = markdown.render(80); + const joinedOutput = lines.join("\n"); + + // Should contain bold text + assert.ok(joinedOutput.includes("bold text")); + + // The output should have ANSI codes for gray (90) and italic (3) + assert.ok(joinedOutput.includes("\x1b[90m"), "Should have gray color code"); + assert.ok(joinedOutput.includes("\x1b[3m"), "Should have italic code"); + + // Should have bold codes (1 or 22 for bold on/off) + assert.ok(joinedOutput.includes("\x1b[1m"), "Should have bold code"); + }); + }); + + describe("HTML-like tags in text", () => { + it("should render content with HTML-like tags as text", () => { + // When the model emits something like content in regular text, + // marked might treat it as HTML and hide the content + const markdown = new Markdown( + "This is text with hidden content that should be visible", + 0, + 0, + ); + + const lines = markdown.render(80); + const plainLines = lines.map((line) => line.replace(/\x1b\[[0-9;]*m/g, "")); + const joinedPlain = plainLines.join(" "); + + // The content inside the tags should be visible + assert.ok( + joinedPlain.includes("hidden content") || joinedPlain.includes(""), + "Should render HTML-like tags or their content as text, not hide them", + ); + }); + + it("should render HTML tags in code blocks correctly", () => { + const markdown = new Markdown("```html\n
Some HTML
\n```", 0, 0); + + const lines = markdown.render(80); + const plainLines = lines.map((line) => line.replace(/\x1b\[[0-9;]*m/g, "")); + const joinedPlain = plainLines.join("\n"); + + // HTML in code blocks should be visible + assert.ok( + joinedPlain.includes("
") && joinedPlain.includes("
"), + "Should render HTML in code blocks", + ); + }); + }); }); diff --git a/packages/web-ui/package.json b/packages/web-ui/package.json index 771757d9..d62c8824 100644 --- a/packages/web-ui/package.json +++ b/packages/web-ui/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/pi-web-ui", - "version": "0.7.17", + "version": "0.7.18", "description": "Reusable web UI components for AI chat interfaces powered by @mariozechner/pi-ai", "type": "module", "main": "dist/index.js", @@ -18,8 +18,8 @@ }, "dependencies": { "@lmstudio/sdk": "^1.5.0", - "@mariozechner/pi-ai": "^0.7.17", - "@mariozechner/pi-tui": "^0.7.17", + "@mariozechner/pi-ai": "^0.7.18", + "@mariozechner/pi-tui": "^0.7.18", "docx-preview": "^0.3.7", "jszip": "^3.10.1", "lucide": "^0.544.0",