diff --git a/packages/tui/src/components-new/markdown.ts b/packages/tui/src/components-new/markdown.ts index 2a46578e..6e79629c 100644 --- a/packages/tui/src/components-new/markdown.ts +++ b/packages/tui/src/components-new/markdown.ts @@ -111,8 +111,8 @@ export class Markdown implements Component { for (const line of wrappedLines) { // Calculate visible length (strip ANSI codes) const visibleLength = stripVTControlCharacters(line).length; - // Right padding to fill to width (accounting for left padding) - const rightPadLength = Math.max(0, width - visibleLength - this.paddingX * 2); + // Right padding to fill to width (accounting for left padding and content) + const rightPadLength = Math.max(0, width - this.paddingX - visibleLength); const rightPad = " ".repeat(rightPadLength); // Add left padding, content, and right padding diff --git a/packages/tui/src/tui-new.ts b/packages/tui/src/tui-new.ts index d1fbf58a..4b09bc8e 100644 --- a/packages/tui/src/tui-new.ts +++ b/packages/tui/src/tui-new.ts @@ -56,13 +56,26 @@ export class Container implements Component { * Text component - displays multi-line text with word wrapping */ export class Text implements Component { - constructor(private text: string = "") {} + private paddingX: number; // Left/right padding + private paddingY: number; // Top/bottom padding + + constructor( + private text: string = "", + paddingX: number = 1, + paddingY: number = 1, + ) { + this.paddingX = paddingX; + this.paddingY = paddingY; + } setText(text: string): void { this.text = text; } render(width: number): string[] { + // Calculate available width for content (subtract horizontal padding) + const contentWidth = Math.max(1, width - this.paddingX * 2); + if (!this.text) { return [""]; } @@ -71,7 +84,7 @@ export class Text implements Component { const textLines = this.text.split("\n"); for (const line of textLines) { - if (line.length <= width) { + if (line.length <= contentWidth) { lines.push(line); } else { // Word wrap @@ -81,7 +94,7 @@ export class Text implements Component { for (const word of words) { if (currentLine.length === 0) { currentLine = word; - } else if (currentLine.length + 1 + word.length <= width) { + } else if (currentLine.length + 1 + word.length <= contentWidth) { currentLine += " " + word; } else { lines.push(currentLine); @@ -95,7 +108,33 @@ export class Text implements Component { } } - return lines.length > 0 ? lines : [""]; + // Add padding to each line + const leftPad = " ".repeat(this.paddingX); + const paddedLines: string[] = []; + + for (const line of lines) { + const rightPadLength = Math.max(0, width - this.paddingX - line.length); + const rightPad = " ".repeat(rightPadLength); + paddedLines.push(leftPad + line + rightPad); + } + + // Add top padding (empty lines) + const emptyLine = " ".repeat(width); + const topPadding: string[] = []; + for (let i = 0; i < this.paddingY; i++) { + topPadding.push(emptyLine); + } + + // Add bottom padding (empty lines) + const bottomPadding: string[] = []; + for (let i = 0; i < this.paddingY; i++) { + bottomPadding.push(emptyLine); + } + + // Combine top padding, content, and bottom padding + const result = [...topPadding, ...paddedLines, ...bottomPadding]; + + return result.length > 0 ? result : [""]; } } diff --git a/packages/tui/test/chat-simple.ts b/packages/tui/test/chat-simple.ts index 3fdec7e9..555bd681 100644 --- a/packages/tui/test/chat-simple.ts +++ b/packages/tui/test/chat-simple.ts @@ -6,7 +6,6 @@ import { CombinedAutocompleteProvider } from "../src/autocomplete.js"; import { Editor } from "../src/components-new/editor.js"; import { Loader } from "../src/components-new/loader.js"; import { Markdown } from "../src/components-new/markdown.js"; -import { Spacer } from "../src/components-new/spacer.js"; import { ProcessTerminal } from "../src/terminal.js"; import { Text, TUI } from "../src/tui-new.js"; @@ -74,31 +73,21 @@ editor.onSubmit = (value: string) => { } if (trimmed) { - // Mark as responding and disable submit isResponding = true; editor.disableSubmit = true; - // Add user message with custom gray background (similar to Claude.ai) const userMessage = new Markdown(value, undefined, undefined, { r: 52, g: 53, b: 65 }); - // Insert before the editor (which is last) const children = tui.children; children.splice(children.length - 1, 0, userMessage); - children.splice(children.length - 1, 0, new Spacer()); - // Add loader const loader = new Loader(tui, "Thinking..."); - const loaderSpacer = new Spacer(); children.splice(children.length - 1, 0, loader); - children.splice(children.length - 1, 0, loaderSpacer); tui.requestRender(); - // Simulate a 1 second delay setTimeout(() => { - // Remove loader and its spacer tui.removeChild(loader); - tui.removeChild(loaderSpacer); // Simulate a response const responses = [ @@ -116,7 +105,6 @@ editor.onSubmit = (value: string) => { // Add assistant message with no background (transparent) const botMessage = new Markdown(randomResponse); children.splice(children.length - 1, 0, botMessage); - children.splice(children.length - 1, 0, new Spacer()); // Re-enable submit isResponding = false;