Release v0.7.18

This commit is contained in:
Mario Zechner 2025-11-18 22:08:44 +01:00
parent a11c1aa4ff
commit 22d8a0ae4a
16 changed files with 284 additions and 174 deletions

28
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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

View file

@ -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"

View file

@ -137,7 +137,7 @@ export const bashTool: AgentTool<typeof bashSchema> = {
}
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<typeof bashSchema> = {
}
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<typeof bashSchema> = {
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 });
}

View file

@ -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));
}
}

View file

@ -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();
}

View file

@ -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);
}
}

View file

@ -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": {}

View file

@ -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",

View file

@ -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",

View file

@ -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(" │ ") + " │");

View file

@ -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);

View file

@ -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 <thinking>content</thinking> in regular text,
// marked might treat it as HTML and hide the content
const markdown = new Markdown(
"This is text with <thinking>hidden content</thinking> 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("<thinking>"),
"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<div>Some HTML</div>\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("<div>") && joinedPlain.includes("</div>"),
"Should render HTML in code blocks",
);
});
});
});

View file

@ -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",