From c75f53f6f220e9c736126f823cc9cd2b46be92f7 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 12 Nov 2025 20:09:11 +0100 Subject: [PATCH] Improve edit tool diff display with context-aware rendering - Add generateDiffString() function in edit tool to create unified diffs with line numbers and 4 lines of context - Store only the formatted diff string in tool result details instead of full file contents - Update tool-execution renderer to parse and colorize the diff string - Filter out message_update events from session saving to prevent verbose session files - Add markdown nested list and table rendering tests --- packages/coding-agent/src/main.ts | 6 +- packages/coding-agent/src/tools/edit.ts | 101 ++++++++- .../coding-agent/src/tui/tool-execution.ts | 127 +++++++++-- packages/coding-agent/src/tui/tui-renderer.ts | 22 +- packages/coding-agent/test/tools.test.ts | 5 +- packages/tui/src/components/markdown.ts | 177 +++++++++++++-- packages/tui/test/markdown.test.ts | 210 ++++++++++++++++++ 7 files changed, 584 insertions(+), 64 deletions(-) create mode 100644 packages/tui/test/markdown.test.ts diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index fb4066c8..5b8e4a91 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -337,8 +337,10 @@ export async function main(args: string[]) { sessionManager.saveMessage(event.message); } - // Log all events - sessionManager.saveEvent(event); + // Log all events except message_update (too verbose) + if (event.type !== "message_update") { + sessionManager.saveEvent(event); + } }); // Determine mode: interactive if no messages provided diff --git a/packages/coding-agent/src/tools/edit.ts b/packages/coding-agent/src/tools/edit.ts index ead1a8dd..19cf05c8 100644 --- a/packages/coding-agent/src/tools/edit.ts +++ b/packages/coding-agent/src/tools/edit.ts @@ -1,6 +1,7 @@ import * as os from "node:os"; import type { AgentTool } from "@mariozechner/pi-ai"; import { Type } from "@sinclair/typebox"; +import * as Diff from "diff"; import { constants } from "fs"; import { access, readFile, writeFile } from "fs/promises"; import { resolve as resolvePath } from "path"; @@ -18,6 +19,99 @@ function expandPath(filePath: string): string { return filePath; } +/** + * Generate a unified diff string with line numbers and context + */ +function generateDiffString(oldContent: string, newContent: string, contextLines = 4): string { + const parts = Diff.diffLines(oldContent, newContent); + const output: string[] = []; + + const oldLines = oldContent.split("\n"); + const newLines = newContent.split("\n"); + const maxLineNum = Math.max(oldLines.length, newLines.length); + const lineNumWidth = String(maxLineNum).length; + + let oldLineNum = 1; + let newLineNum = 1; + let lastWasChange = false; + + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + const raw = part.value.split("\n"); + if (raw[raw.length - 1] === "") { + raw.pop(); + } + + if (part.added || part.removed) { + // Show the change + for (const line of raw) { + if (part.added) { + const lineNum = String(newLineNum).padStart(lineNumWidth, " "); + output.push(`+${lineNum} ${line}`); + newLineNum++; + } else { + // removed + const lineNum = String(oldLineNum).padStart(lineNumWidth, " "); + output.push(`-${lineNum} ${line}`); + oldLineNum++; + } + } + lastWasChange = true; + } else { + // Context lines - only show a few before/after changes + const nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed); + + if (lastWasChange || nextPartIsChange) { + // Show context + let linesToShow = raw; + let skipStart = 0; + let skipEnd = 0; + + if (!lastWasChange) { + // Show only last N lines as leading context + skipStart = Math.max(0, raw.length - contextLines); + linesToShow = raw.slice(skipStart); + } + + if (!nextPartIsChange && linesToShow.length > contextLines) { + // Show only first N lines as trailing context + skipEnd = linesToShow.length - contextLines; + linesToShow = linesToShow.slice(0, contextLines); + } + + // Add ellipsis if we skipped lines at start + if (skipStart > 0) { + output.push(` ${"".padStart(lineNumWidth, " ")} ...`); + } + + for (const line of linesToShow) { + const lineNum = String(oldLineNum).padStart(lineNumWidth, " "); + output.push(` ${lineNum} ${line}`); + oldLineNum++; + newLineNum++; + } + + // Add ellipsis if we skipped lines at end + if (skipEnd > 0) { + output.push(` ${"".padStart(lineNumWidth, " ")} ...`); + } + + // Update line numbers for skipped lines + oldLineNum += skipStart + skipEnd; + newLineNum += skipStart + skipEnd; + } else { + // Skip these context lines entirely + oldLineNum += raw.length; + newLineNum += raw.length; + } + + lastWasChange = false; + } + } + + return output.join("\n"); +} + const editSchema = Type.Object({ path: Type.String({ description: "Path to the file to edit (relative or absolute)" }), oldText: Type.String({ description: "Exact text to find and replace (must match exactly)" }), @@ -37,7 +131,10 @@ export const editTool: AgentTool = { ) => { const absolutePath = resolvePath(expandPath(path)); - return new Promise<{ content: Array<{ type: "text"; text: string }>; details: undefined }>((resolve, reject) => { + return new Promise<{ + content: Array<{ type: "text"; text: string }>; + details: { diff: string } | undefined; + }>((resolve, reject) => { // Check if already aborted if (signal?.aborted) { reject(new Error("Operation aborted")); @@ -148,7 +245,7 @@ export const editTool: AgentTool = { text: `Successfully replaced text in ${path}. Changed ${oldText.length} characters to ${newText.length} characters.`, }, ], - details: undefined, + details: { diff: generateDiffString(content, newContent) }, }); } catch (error: any) { // Clean up abort handler diff --git a/packages/coding-agent/src/tui/tool-execution.ts b/packages/coding-agent/src/tui/tool-execution.ts index 5f1b33a5..232c9cdb 100644 --- a/packages/coding-agent/src/tui/tool-execution.ts +++ b/packages/coding-agent/src/tui/tool-execution.ts @@ -1,6 +1,7 @@ import * as os from "node:os"; import { Container, Spacer, Text } from "@mariozechner/pi-tui"; import chalk from "chalk"; +import * as Diff from "diff"; /** * Convert absolute path to tilde notation if it's in home directory @@ -21,36 +22,101 @@ function replaceTabs(text: string): string { } /** - * Generate a unified diff between old and new strings with line numbers + * Generate a unified diff with line numbers and context */ function generateDiff(oldStr: string, newStr: string): string { - // Split into lines + const parts = Diff.diffLines(oldStr, newStr); + const output: string[] = []; + + // Calculate max line number for padding const oldLines = oldStr.split("\n"); const newLines = newStr.split("\n"); - - const diff: string[] = []; - - // Calculate line number padding (for alignment) const maxLineNum = Math.max(oldLines.length, newLines.length); const lineNumWidth = String(maxLineNum).length; - // Show old lines with line numbers - diff.push(chalk.red("- old:")); - for (let i = 0; i < oldLines.length; i++) { - const lineNum = String(i + 1).padStart(lineNumWidth, " "); - diff.push(chalk.red(`- ${chalk.dim(lineNum)} ${oldLines[i]}`)); + const CONTEXT_LINES = 2; // Show 2 lines of context around changes + + let oldLineNum = 1; + let newLineNum = 1; + let lastWasChange = false; + + for (let i = 0; i < parts.length; i++) { + const part = parts[i]; + const raw = part.value.split("\n"); + if (raw[raw.length - 1] === "") { + raw.pop(); + } + + if (part.added || part.removed) { + // Show the change + for (const line of raw) { + if (part.added) { + const lineNum = String(newLineNum).padStart(lineNumWidth, " "); + output.push(chalk.green(`${lineNum} ${line}`)); + newLineNum++; + } else { + // removed + const lineNum = String(oldLineNum).padStart(lineNumWidth, " "); + output.push(chalk.red(`${lineNum} ${line}`)); + oldLineNum++; + } + } + lastWasChange = true; + } else { + // Context lines - only show a few before/after changes + const isFirstPart = i === 0; + const isLastPart = i === parts.length - 1; + const nextPartIsChange = i < parts.length - 1 && (parts[i + 1].added || parts[i + 1].removed); + + if (lastWasChange || nextPartIsChange || isFirstPart || isLastPart) { + // Show context + let linesToShow = raw; + let skipStart = 0; + let skipEnd = 0; + + if (!isFirstPart && !lastWasChange) { + // Show only last N lines as leading context + skipStart = Math.max(0, raw.length - CONTEXT_LINES); + linesToShow = raw.slice(skipStart); + } + + if (!isLastPart && !nextPartIsChange && linesToShow.length > CONTEXT_LINES) { + // Show only first N lines as trailing context + skipEnd = linesToShow.length - CONTEXT_LINES; + linesToShow = linesToShow.slice(0, CONTEXT_LINES); + } + + // Add ellipsis if we skipped lines at start + if (skipStart > 0) { + output.push(chalk.dim(`${"".padStart(lineNumWidth, " ")} ...`)); + } + + for (const line of linesToShow) { + const lineNum = String(oldLineNum).padStart(lineNumWidth, " "); + output.push(chalk.dim(`${lineNum} ${line}`)); + oldLineNum++; + newLineNum++; + } + + // Add ellipsis if we skipped lines at end + if (skipEnd > 0) { + output.push(chalk.dim(`${"".padStart(lineNumWidth, " ")} ...`)); + } + + // Update line numbers for skipped lines + oldLineNum += skipStart + skipEnd; + newLineNum += skipStart + skipEnd; + } else { + // Skip these context lines entirely + oldLineNum += raw.length; + newLineNum += raw.length; + } + + lastWasChange = false; + } } - diff.push(""); - - // Show new lines with line numbers - diff.push(chalk.green("+ new:")); - for (let i = 0; i < newLines.length; i++) { - const lineNum = String(i + 1).padStart(lineNumWidth, " "); - diff.push(chalk.green(`+ ${chalk.dim(lineNum)} ${newLines[i]}`)); - } - - return diff.join("\n"); + return output.join("\n"); } /** @@ -63,6 +129,7 @@ export class ToolExecutionComponent extends Container { private result?: { content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>; isError: boolean; + details?: any; }; constructor(toolName: string, args: any) { @@ -83,6 +150,7 @@ export class ToolExecutionComponent extends Container { updateResult(result: { content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>; + details?: any; isError: boolean; }): void { this.result = result; @@ -183,9 +251,20 @@ export class ToolExecutionComponent extends Container { const path = shortenPath(this.args?.file_path || this.args?.path || ""); text = chalk.bold("edit") + " " + (path ? chalk.cyan(path) : chalk.dim("...")); - // Show diff if we have old_string and new_string - if (this.args?.old_string && this.args?.new_string) { - text += "\n\n" + generateDiff(this.args.old_string, this.args.new_string); + // Show diff if available + if (this.result?.details?.diff) { + // Parse the diff string and apply colors + const diffLines = this.result.details.diff.split("\n"); + const coloredLines = diffLines.map((line: string) => { + if (line.startsWith("+")) { + return chalk.green(line); + } else if (line.startsWith("-")) { + return chalk.red(line); + } else { + return chalk.dim(line); + } + }); + text += "\n\n" + coloredLines.join("\n"); } } else { // Generic tool diff --git a/packages/coding-agent/src/tui/tui-renderer.ts b/packages/coding-agent/src/tui/tui-renderer.ts index 5d23dcd3..0269b6a4 100644 --- a/packages/coding-agent/src/tui/tui-renderer.ts +++ b/packages/coding-agent/src/tui/tui-renderer.ts @@ -288,15 +288,7 @@ export class TuiRenderer { // Update the existing tool component with the result const component = this.pendingTools.get(event.toolCallId); if (component) { - // Update the component with the result - const content = - typeof event.result === "string" - ? [{ type: "text" as const, text: event.result }] - : event.result.content; - component.updateResult({ - content, - isError: event.isError, - }); + component.updateResult(event.result); this.pendingTools.delete(event.toolCallId); this.ui.requestRender(); } @@ -388,16 +380,16 @@ export class TuiRenderer { } } } else if (message.role === "toolResult") { - // Update existing tool execution component with results - const toolResultMsg = message as any; - const component = this.pendingTools.get(toolResultMsg.toolCallId); + // Update existing tool execution component with results ; + const component = this.pendingTools.get(message.toolCallId); if (component) { component.updateResult({ - content: toolResultMsg.content, - isError: toolResultMsg.isError, + content: message.content, + details: message.details, + isError: message.isError, }); // Remove from pending map since it's complete - this.pendingTools.delete(toolResultMsg.toolCallId); + this.pendingTools.delete(message.toolCallId); } } } diff --git a/packages/coding-agent/test/tools.test.ts b/packages/coding-agent/test/tools.test.ts index e9ee655c..a516478b 100644 --- a/packages/coding-agent/test/tools.test.ts +++ b/packages/coding-agent/test/tools.test.ts @@ -192,7 +192,10 @@ describe("Coding Agent Tools", () => { }); expect(getTextOutput(result)).toContain("Successfully replaced"); - expect(result.details).toBeUndefined(); + expect(result.details).toBeDefined(); + expect(result.details.diff).toBeDefined(); + expect(typeof result.details.diff).toBe("string"); + expect(result.details.diff).toContain("testing"); }); it("should fail if text not found", async () => { diff --git a/packages/tui/src/components/markdown.ts b/packages/tui/src/components/markdown.ts index 4436d73d..10baee9c 100644 --- a/packages/tui/src/components/markdown.ts +++ b/packages/tui/src/components/markdown.ts @@ -231,29 +231,19 @@ export class Markdown implements Component { break; } - case "list": - for (let i = 0; i < token.items.length; i++) { - const item = token.items[i]; - const bullet = token.ordered ? `${i + 1}. ` : "- "; - const itemText = this.renderInlineTokens(item.tokens || []); - - // Check if the item text contains multiple lines (embedded content) - const itemLines = itemText.split("\n").filter((line) => line.trim()); - if (itemLines.length > 1) { - // First line is the list item - lines.push(chalk.cyan(bullet) + itemLines[0]); - // Rest are treated as separate content - for (let j = 1; j < itemLines.length; j++) { - lines.push(""); // Add spacing - lines.push(itemLines[j]); - } - } else { - lines.push(chalk.cyan(bullet) + itemText); - } - } + case "list": { + const listLines = this.renderList(token as any, 0); + lines.push(...listLines); // Don't add spacing after lists if a space token follows // (the space token will handle it) break; + } + + case "table": { + const tableLines = this.renderTable(token as any); + lines.push(...tableLines); + break; + } case "blockquote": { const quoteText = this.renderInlineTokens(token.tokens || []); @@ -448,4 +438,151 @@ export class Markdown implements Component { return wrapped.length > 0 ? wrapped : [""]; } + + /** + * Render a list with proper nesting support + */ + private renderList(token: Token & { items: any[]; ordered: boolean }, depth: number): string[] { + const lines: string[] = []; + const indent = " ".repeat(depth); + + for (let i = 0; i < token.items.length; i++) { + const item = token.items[i]; + const bullet = token.ordered ? `${i + 1}. ` : "- "; + + // Process item tokens to handle nested lists + const itemLines = this.renderListItem(item.tokens || [], depth); + + if (itemLines.length > 0) { + // First line - check if it's a nested list (contains cyan ANSI code for bullets) + const firstLine = itemLines[0]; + const isNestedList = firstLine.includes("\x1b[36m"); // cyan color code + + if (isNestedList) { + // This is a nested list, just add it as-is (already has full indent) + lines.push(firstLine); + } else { + // Regular text content - add indent and bullet + lines.push(indent + chalk.cyan(bullet) + firstLine); + } + + // Rest of the lines + for (let j = 1; j < itemLines.length; j++) { + const line = itemLines[j]; + const isNestedListLine = line.includes("\x1b[36m"); // cyan bullet color + + if (isNestedListLine) { + // Nested list line - already has full indent + lines.push(line); + } else { + // Regular content - add parent indent + 2 spaces for continuation + lines.push(indent + " " + line); + } + } + } else { + lines.push(indent + chalk.cyan(bullet)); + } + } + + return lines; + } + + /** + * Render list item tokens, handling nested lists + * Returns lines WITHOUT the parent indent (renderList will add it) + */ + private renderListItem(tokens: Token[], parentDepth: number): string[] { + const lines: string[] = []; + + for (const token of tokens) { + if (token.type === "list") { + // Nested list - render with one additional indent level + // These lines will have their own indent, so we just add them as-is + const nestedLines = this.renderList(token as any, parentDepth + 1); + lines.push(...nestedLines); + } else if (token.type === "text") { + // Text content (may have inline tokens) + const text = + token.tokens && token.tokens.length > 0 ? this.renderInlineTokens(token.tokens) : token.text || ""; + lines.push(text); + } else if (token.type === "paragraph") { + // Paragraph in list item + const text = this.renderInlineTokens(token.tokens || []); + lines.push(text); + } else if (token.type === "code") { + // Code block in list item + lines.push(chalk.gray("```" + (token.lang || ""))); + const codeLines = token.text.split("\n"); + for (const codeLine of codeLines) { + lines.push(chalk.dim(" ") + chalk.green(codeLine)); + } + lines.push(chalk.gray("```")); + } else { + // Other token types - try to render as inline + const text = this.renderInlineTokens([token]); + if (text) { + lines.push(text); + } + } + } + + return lines; + } + + /** + * Render a table + */ + private renderTable(token: Token & { header: any[]; rows: any[][] }): string[] { + const lines: string[] = []; + + // Calculate column widths + const columnWidths: number[] = []; + + // Check header + for (let i = 0; i < token.header.length; i++) { + const headerText = this.renderInlineTokens(token.header[i].tokens || []); + const width = visibleWidth(headerText); + columnWidths[i] = Math.max(columnWidths[i] || 0, width); + } + + // Check rows + for (const row of token.rows) { + for (let i = 0; i < row.length; i++) { + const cellText = this.renderInlineTokens(row[i].tokens || []); + const width = visibleWidth(cellText); + columnWidths[i] = Math.max(columnWidths[i] || 0, width); + } + } + + // Limit column widths to reasonable max + const maxColWidth = 40; + for (let i = 0; i < columnWidths.length; i++) { + columnWidths[i] = Math.min(columnWidths[i], maxColWidth); + } + + // Render header + const headerCells = token.header.map((cell, i) => { + const text = this.renderInlineTokens(cell.tokens || []); + return chalk.bold(text.padEnd(columnWidths[i])); + }); + lines.push("│ " + headerCells.join(" │ ") + " │"); + + // Render separator + const separatorCells = columnWidths.map((width) => "─".repeat(width)); + lines.push("├─" + separatorCells.join("─┼─") + "─┤"); + + // Render rows + for (const row of token.rows) { + const rowCells = row.map((cell, i) => { + const text = this.renderInlineTokens(cell.tokens || []); + const visWidth = visibleWidth(text); + const padding = " ".repeat(Math.max(0, columnWidths[i] - visWidth)); + return text + padding; + }); + lines.push("│ " + rowCells.join(" │ ") + " │"); + } + + lines.push(""); // Add spacing after table + return lines; + } } diff --git a/packages/tui/test/markdown.test.ts b/packages/tui/test/markdown.test.ts new file mode 100644 index 00000000..f1aa7854 --- /dev/null +++ b/packages/tui/test/markdown.test.ts @@ -0,0 +1,210 @@ +import assert from "node:assert"; +import { describe, it } from "node:test"; +import { Markdown } from "../src/components/markdown.js"; + +describe("Markdown component", () => { + describe("Nested lists", () => { + it("should render simple nested list", () => { + const markdown = new Markdown( + `- Item 1 + - Nested 1.1 + - Nested 1.2 +- Item 2`, + undefined, + undefined, + undefined, + 0, + 0, + ); + + const lines = markdown.render(80); + + // Check that we have content + assert.ok(lines.length > 0); + + // Strip ANSI codes for checking + const plainLines = lines.map((line) => line.replace(/\x1b\[[0-9;]*m/g, "")); + + // Check structure + assert.ok(plainLines.some((line) => line.includes("- Item 1"))); + assert.ok(plainLines.some((line) => line.includes(" - Nested 1.1"))); + assert.ok(plainLines.some((line) => line.includes(" - Nested 1.2"))); + assert.ok(plainLines.some((line) => line.includes("- Item 2"))); + }); + + it("should render deeply nested list", () => { + const markdown = new Markdown( + `- Level 1 + - Level 2 + - Level 3 + - Level 4`, + undefined, + undefined, + undefined, + 0, + 0, + ); + + const lines = markdown.render(80); + const plainLines = lines.map((line) => line.replace(/\x1b\[[0-9;]*m/g, "")); + + // Check proper indentation + assert.ok(plainLines.some((line) => line.includes("- Level 1"))); + assert.ok(plainLines.some((line) => line.includes(" - Level 2"))); + assert.ok(plainLines.some((line) => line.includes(" - Level 3"))); + assert.ok(plainLines.some((line) => line.includes(" - Level 4"))); + }); + + it("should render ordered nested list", () => { + const markdown = new Markdown( + `1. First + 1. Nested first + 2. Nested second +2. Second`, + undefined, + undefined, + undefined, + 0, + 0, + ); + + const lines = markdown.render(80); + const plainLines = lines.map((line) => line.replace(/\x1b\[[0-9;]*m/g, "")); + + assert.ok(plainLines.some((line) => line.includes("1. First"))); + assert.ok(plainLines.some((line) => line.includes(" 1. Nested first"))); + assert.ok(plainLines.some((line) => line.includes(" 2. Nested second"))); + assert.ok(plainLines.some((line) => line.includes("2. Second"))); + }); + + it("should render mixed ordered and unordered nested lists", () => { + const markdown = new Markdown( + `1. Ordered item + - Unordered nested + - Another nested +2. Second ordered + - More nested`, + undefined, + undefined, + undefined, + 0, + 0, + ); + + const lines = markdown.render(80); + const plainLines = lines.map((line) => line.replace(/\x1b\[[0-9;]*m/g, "")); + + assert.ok(plainLines.some((line) => line.includes("1. Ordered item"))); + assert.ok(plainLines.some((line) => line.includes(" - Unordered nested"))); + assert.ok(plainLines.some((line) => line.includes("2. Second ordered"))); + }); + }); + + describe("Tables", () => { + it("should render simple table", () => { + const markdown = new Markdown( + `| Name | Age | +| --- | --- | +| Alice | 30 | +| Bob | 25 |`, + undefined, + undefined, + undefined, + 0, + 0, + ); + + const lines = markdown.render(80); + const plainLines = lines.map((line) => line.replace(/\x1b\[[0-9;]*m/g, "")); + + // Check table structure + assert.ok(plainLines.some((line) => line.includes("Name"))); + assert.ok(plainLines.some((line) => line.includes("Age"))); + assert.ok(plainLines.some((line) => line.includes("Alice"))); + assert.ok(plainLines.some((line) => line.includes("Bob"))); + // Check for table borders + assert.ok(plainLines.some((line) => line.includes("│"))); + assert.ok(plainLines.some((line) => line.includes("─"))); + }); + + it("should render table with alignment", () => { + const markdown = new Markdown( + `| Left | Center | Right | +| :--- | :---: | ---: | +| A | B | C | +| Long text | Middle | End |`, + undefined, + undefined, + undefined, + 0, + 0, + ); + + const lines = markdown.render(80); + const plainLines = lines.map((line) => line.replace(/\x1b\[[0-9;]*m/g, "")); + + // Check headers + assert.ok(plainLines.some((line) => line.includes("Left"))); + assert.ok(plainLines.some((line) => line.includes("Center"))); + assert.ok(plainLines.some((line) => line.includes("Right"))); + // Check content + assert.ok(plainLines.some((line) => line.includes("Long text"))); + }); + + it("should handle tables with varying column widths", () => { + const markdown = new Markdown( + `| Short | Very long column header | +| --- | --- | +| A | This is a much longer cell content | +| B | Short |`, + undefined, + undefined, + undefined, + 0, + 0, + ); + + const lines = markdown.render(80); + + // Should render without errors + assert.ok(lines.length > 0); + + const plainLines = lines.map((line) => line.replace(/\x1b\[[0-9;]*m/g, "")); + assert.ok(plainLines.some((line) => line.includes("Very long column header"))); + assert.ok(plainLines.some((line) => line.includes("This is a much longer cell content"))); + }); + }); + + describe("Combined features", () => { + it("should render lists and tables together", () => { + const markdown = new Markdown( + `# Test Document + +- Item 1 + - Nested item +- Item 2 + +| Col1 | Col2 | +| --- | --- | +| A | B |`, + undefined, + undefined, + undefined, + 0, + 0, + ); + + const lines = markdown.render(80); + const plainLines = lines.map((line) => line.replace(/\x1b\[[0-9;]*m/g, "")); + + // Check heading + assert.ok(plainLines.some((line) => line.includes("Test Document"))); + // Check list + assert.ok(plainLines.some((line) => line.includes("- Item 1"))); + assert.ok(plainLines.some((line) => line.includes(" - Nested item"))); + // Check table + assert.ok(plainLines.some((line) => line.includes("Col1"))); + assert.ok(plainLines.some((line) => line.includes("│"))); + }); + }); +});