mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-17 08:00:59 +00:00
Massive refactor of API
- Switch to function based API - Anthropic SDK style async generator - Fully typed with escape hatches for custom models
This commit is contained in:
parent
004de3c9d0
commit
66cefb236e
29 changed files with 5835 additions and 6225 deletions
|
|
@ -1,15 +1,6 @@
|
|||
#!/usr/bin/env npx tsx
|
||||
import {
|
||||
Container,
|
||||
LoadingAnimation,
|
||||
TextComponent,
|
||||
TextEditor,
|
||||
TUI,
|
||||
WhitespaceComponent,
|
||||
} from "../src/index.js";
|
||||
import chalk from "chalk";
|
||||
|
||||
|
||||
import { Container, LoadingAnimation, TextComponent, TextEditor, TUI, WhitespaceComponent } from "../src/index.js";
|
||||
|
||||
/**
|
||||
* Test the new smart double-buffered TUI implementation
|
||||
|
|
@ -24,7 +15,7 @@ async function main() {
|
|||
|
||||
// Monkey-patch requestRender to measure performance
|
||||
const originalRequestRender = ui.requestRender.bind(ui);
|
||||
ui.requestRender = function() {
|
||||
ui.requestRender = () => {
|
||||
const startTime = process.hrtime.bigint();
|
||||
originalRequestRender();
|
||||
process.nextTick(() => {
|
||||
|
|
@ -38,10 +29,12 @@ async function main() {
|
|||
|
||||
// Add header
|
||||
const header = new TextComponent(
|
||||
chalk.bold.green("Smart Double Buffer TUI Test") + "\n" +
|
||||
chalk.dim("Testing new implementation with component-level caching and smart diffing") + "\n" +
|
||||
chalk.dim("Press CTRL+C to exit"),
|
||||
{ bottom: 1 }
|
||||
chalk.bold.green("Smart Double Buffer TUI Test") +
|
||||
"\n" +
|
||||
chalk.dim("Testing new implementation with component-level caching and smart diffing") +
|
||||
"\n" +
|
||||
chalk.dim("Press CTRL+C to exit"),
|
||||
{ bottom: 1 },
|
||||
);
|
||||
ui.addChild(header);
|
||||
|
||||
|
|
@ -57,7 +50,9 @@ async function main() {
|
|||
|
||||
// Add text editor
|
||||
const editor = new TextEditor();
|
||||
editor.setText("Type here to test the text editor.\n\nWith smart diffing, only changed lines are redrawn!\n\nThe animation above updates every 80ms but the editor stays perfectly still.");
|
||||
editor.setText(
|
||||
"Type here to test the text editor.\n\nWith smart diffing, only changed lines are redrawn!\n\nThe animation above updates every 80ms but the editor stays perfectly still.",
|
||||
);
|
||||
container.addChild(editor);
|
||||
|
||||
// Add the container to UI
|
||||
|
|
@ -71,15 +66,20 @@ async function main() {
|
|||
const statsInterval = setInterval(() => {
|
||||
if (renderCount > 0) {
|
||||
const avgRenderTime = Number(totalRenderTime / BigInt(renderCount)) / 1_000_000; // Convert to ms
|
||||
const lastRenderTime = renderTimings.length > 0
|
||||
? Number(renderTimings[renderTimings.length - 1]) / 1_000_000
|
||||
: 0;
|
||||
const lastRenderTime =
|
||||
renderTimings.length > 0 ? Number(renderTimings[renderTimings.length - 1]) / 1_000_000 : 0;
|
||||
const avgLinesRedrawn = ui.getAverageLinesRedrawn();
|
||||
|
||||
statsComponent.setText(
|
||||
chalk.yellow(`Performance Stats:`) + "\n" +
|
||||
chalk.dim(`Renders: ${renderCount} | Avg Time: ${avgRenderTime.toFixed(2)}ms | Last: ${lastRenderTime.toFixed(2)}ms`) + "\n" +
|
||||
chalk.dim(`Lines Redrawn: ${ui.getLinesRedrawn()} total | Avg per render: ${avgLinesRedrawn.toFixed(1)}`)
|
||||
chalk.yellow(`Performance Stats:`) +
|
||||
"\n" +
|
||||
chalk.dim(
|
||||
`Renders: ${renderCount} | Avg Time: ${avgRenderTime.toFixed(2)}ms | Last: ${lastRenderTime.toFixed(2)}ms`,
|
||||
) +
|
||||
"\n" +
|
||||
chalk.dim(
|
||||
`Lines Redrawn: ${ui.getLinesRedrawn()} total | Avg per render: ${avgLinesRedrawn.toFixed(1)}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
}, 1000);
|
||||
|
|
@ -96,7 +96,11 @@ async function main() {
|
|||
ui.stop();
|
||||
console.log("\n" + chalk.green("Exited double-buffer test"));
|
||||
console.log(chalk.dim(`Total renders: ${renderCount}`));
|
||||
console.log(chalk.dim(`Average render time: ${renderCount > 0 ? (Number(totalRenderTime / BigInt(renderCount)) / 1_000_000).toFixed(2) : 0}ms`));
|
||||
console.log(
|
||||
chalk.dim(
|
||||
`Average render time: ${renderCount > 0 ? (Number(totalRenderTime / BigInt(renderCount)) / 1_000_000).toFixed(2) : 0}ms`,
|
||||
),
|
||||
);
|
||||
console.log(chalk.dim(`Total lines redrawn: ${ui.getLinesRedrawn()}`));
|
||||
console.log(chalk.dim(`Average lines redrawn per render: ${ui.getAverageLinesRedrawn().toFixed(1)}`));
|
||||
process.exit(0);
|
||||
|
|
@ -112,4 +116,4 @@ async function main() {
|
|||
main().catch((error) => {
|
||||
console.error("Error:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
#!/usr/bin/env npx tsx
|
||||
import { TUI, Container, TextEditor, TextComponent, MarkdownComponent, CombinedAutocompleteProvider } from "../src/index.js";
|
||||
import {
|
||||
CombinedAutocompleteProvider,
|
||||
Container,
|
||||
MarkdownComponent,
|
||||
TextComponent,
|
||||
TextEditor,
|
||||
TUI,
|
||||
} from "../src/index.js";
|
||||
|
||||
/**
|
||||
* Chat Application with Autocomplete
|
||||
*
|
||||
*
|
||||
* Demonstrates:
|
||||
* - Slash command system with autocomplete
|
||||
* - Dynamic message history
|
||||
|
|
@ -16,7 +23,7 @@ const ui = new TUI();
|
|||
// Add header with instructions
|
||||
const header = new TextComponent(
|
||||
"💬 Chat Demo | Type '/' for commands | Start typing a filename + Tab to autocomplete | Ctrl+C to exit",
|
||||
{ bottom: 1 }
|
||||
{ bottom: 1 },
|
||||
);
|
||||
|
||||
const chatHistory = new Container();
|
||||
|
|
@ -82,7 +89,8 @@ ui.onGlobalKeyPress = (data: string) => {
|
|||
};
|
||||
|
||||
// Add initial welcome message to chat history
|
||||
chatHistory.addChild(new MarkdownComponent(`
|
||||
chatHistory.addChild(
|
||||
new MarkdownComponent(`
|
||||
## Welcome to the Chat Demo!
|
||||
|
||||
**Available slash commands:**
|
||||
|
|
@ -96,10 +104,11 @@ chatHistory.addChild(new MarkdownComponent(`
|
|||
- Works with home directory (\`~/\`)
|
||||
|
||||
Try it out! Type a message or command below.
|
||||
`));
|
||||
`),
|
||||
);
|
||||
|
||||
ui.addChild(header);
|
||||
ui.addChild(chatHistory);
|
||||
ui.addChild(editor);
|
||||
ui.setFocus(editor);
|
||||
ui.start();
|
||||
ui.start();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { test, describe } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { describe, test } from "node:test";
|
||||
import { Container, TextComponent, TextEditor, TUI } from "../src/index.js";
|
||||
import { VirtualTerminal } from "./virtual-terminal.js";
|
||||
import { TUI, Container, TextComponent, TextEditor } from "../src/index.js";
|
||||
|
||||
describe("Differential Rendering - Dynamic Content", () => {
|
||||
test("handles static text, dynamic container, and text editor correctly", async () => {
|
||||
|
|
@ -23,7 +23,7 @@ describe("Differential Rendering - Dynamic Content", () => {
|
|||
ui.setFocus(editor);
|
||||
|
||||
// Wait for next tick to complete and flush virtual terminal
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
await terminal.flush();
|
||||
|
||||
// Step 4: Check initial output in scrollbuffer
|
||||
|
|
@ -33,14 +33,16 @@ describe("Differential Rendering - Dynamic Content", () => {
|
|||
console.log("Initial render:");
|
||||
console.log("Viewport lines:", viewport.length);
|
||||
console.log("ScrollBuffer lines:", scrollBuffer.length);
|
||||
|
||||
|
||||
// Count non-empty lines in scrollbuffer
|
||||
let nonEmptyInBuffer = scrollBuffer.filter(line => line.trim() !== "").length;
|
||||
const nonEmptyInBuffer = scrollBuffer.filter((line) => line.trim() !== "").length;
|
||||
console.log("Non-empty lines in scrollbuffer:", nonEmptyInBuffer);
|
||||
|
||||
// Verify initial render has static text in scrollbuffer
|
||||
assert.ok(scrollBuffer.some(line => line.includes("Static Header Text")),
|
||||
`Expected static text in scrollbuffer`);
|
||||
assert.ok(
|
||||
scrollBuffer.some((line) => line.includes("Static Header Text")),
|
||||
`Expected static text in scrollbuffer`,
|
||||
);
|
||||
|
||||
// Step 5: Add 100 text components to container
|
||||
console.log("\nAdding 100 components to container...");
|
||||
|
|
@ -52,7 +54,7 @@ describe("Differential Rendering - Dynamic Content", () => {
|
|||
ui.requestRender();
|
||||
|
||||
// Wait for next tick to complete and flush
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
await terminal.flush();
|
||||
|
||||
// Step 6: Check output after adding 100 components
|
||||
|
|
@ -62,10 +64,10 @@ describe("Differential Rendering - Dynamic Content", () => {
|
|||
console.log("\nAfter adding 100 items:");
|
||||
console.log("Viewport lines:", viewport.length);
|
||||
console.log("ScrollBuffer lines:", scrollBuffer.length);
|
||||
|
||||
|
||||
// Count all dynamic items in scrollbuffer
|
||||
let dynamicItemsInBuffer = 0;
|
||||
let allItemNumbers = new Set<number>();
|
||||
const allItemNumbers = new Set<number>();
|
||||
for (const line of scrollBuffer) {
|
||||
const match = line.match(/Dynamic Item (\d+)/);
|
||||
if (match) {
|
||||
|
|
@ -73,31 +75,39 @@ describe("Differential Rendering - Dynamic Content", () => {
|
|||
allItemNumbers.add(parseInt(match[1]));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
console.log("Dynamic items found in scrollbuffer:", dynamicItemsInBuffer);
|
||||
console.log("Unique item numbers:", allItemNumbers.size);
|
||||
console.log("Item range:", Math.min(...allItemNumbers), "-", Math.max(...allItemNumbers));
|
||||
|
||||
|
||||
// CRITICAL TEST: The scrollbuffer should contain ALL 100 items
|
||||
// This is what the differential render should preserve!
|
||||
assert.strictEqual(allItemNumbers.size, 100,
|
||||
`Expected all 100 unique items in scrollbuffer, but found ${allItemNumbers.size}`);
|
||||
|
||||
assert.strictEqual(
|
||||
allItemNumbers.size,
|
||||
100,
|
||||
`Expected all 100 unique items in scrollbuffer, but found ${allItemNumbers.size}`,
|
||||
);
|
||||
|
||||
// Verify items are 1-100
|
||||
for (let i = 1; i <= 100; i++) {
|
||||
assert.ok(allItemNumbers.has(i), `Missing Dynamic Item ${i} in scrollbuffer`);
|
||||
}
|
||||
|
||||
// Also verify the static header is still in scrollbuffer
|
||||
assert.ok(scrollBuffer.some(line => line.includes("Static Header Text")),
|
||||
"Static header should still be in scrollbuffer");
|
||||
|
||||
// And the editor should be there too
|
||||
assert.ok(scrollBuffer.some(line => line.includes("╭") && line.includes("╮")),
|
||||
"Editor top border should be in scrollbuffer");
|
||||
assert.ok(scrollBuffer.some(line => line.includes("╰") && line.includes("╯")),
|
||||
"Editor bottom border should be in scrollbuffer");
|
||||
|
||||
// Also verify the static header is still in scrollbuffer
|
||||
assert.ok(
|
||||
scrollBuffer.some((line) => line.includes("Static Header Text")),
|
||||
"Static header should still be in scrollbuffer",
|
||||
);
|
||||
|
||||
// And the editor should be there too
|
||||
assert.ok(
|
||||
scrollBuffer.some((line) => line.includes("╭") && line.includes("╮")),
|
||||
"Editor top border should be in scrollbuffer",
|
||||
);
|
||||
assert.ok(
|
||||
scrollBuffer.some((line) => line.includes("╰") && line.includes("╯")),
|
||||
"Editor bottom border should be in scrollbuffer",
|
||||
);
|
||||
|
||||
ui.stop();
|
||||
});
|
||||
|
|
@ -124,7 +134,7 @@ describe("Differential Rendering - Dynamic Content", () => {
|
|||
contentContainer.addChild(new TextComponent("Content Line 2"));
|
||||
|
||||
// Initial render
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
await terminal.flush();
|
||||
|
||||
let viewport = terminal.getViewport();
|
||||
|
|
@ -142,7 +152,7 @@ describe("Differential Rendering - Dynamic Content", () => {
|
|||
statusContainer.addChild(new TextComponent("Status: Processing..."));
|
||||
ui.requestRender();
|
||||
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
await terminal.flush();
|
||||
|
||||
viewport = terminal.getViewport();
|
||||
|
|
@ -162,7 +172,7 @@ describe("Differential Rendering - Dynamic Content", () => {
|
|||
}
|
||||
ui.requestRender();
|
||||
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
await terminal.flush();
|
||||
|
||||
viewport = terminal.getViewport();
|
||||
|
|
@ -180,7 +190,7 @@ describe("Differential Rendering - Dynamic Content", () => {
|
|||
contentLine10.setText("Content Line 10 - MODIFIED");
|
||||
ui.requestRender();
|
||||
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
await terminal.flush();
|
||||
|
||||
viewport = terminal.getViewport();
|
||||
|
|
@ -190,4 +200,4 @@ describe("Differential Rendering - Dynamic Content", () => {
|
|||
|
||||
ui.stop();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { TUI, SelectList } from "../src/index.js";
|
||||
import { readdirSync, statSync } from "fs";
|
||||
import { join } from "path";
|
||||
import { SelectList, TUI } from "../src/index.js";
|
||||
|
||||
const ui = new TUI();
|
||||
ui.start();
|
||||
|
|
@ -52,4 +52,4 @@ function showDirectory(path: string) {
|
|||
ui.setFocus(fileList);
|
||||
}
|
||||
|
||||
showDirectory(currentPath);
|
||||
showDirectory(currentPath);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, test } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { TextEditor, TextComponent, Container, TUI } from "../src/index.js";
|
||||
import { describe, test } from "node:test";
|
||||
import { Container, TextComponent, TextEditor, TUI } from "../src/index.js";
|
||||
import { VirtualTerminal } from "./virtual-terminal.js";
|
||||
|
||||
describe("Layout shift artifacts", () => {
|
||||
|
|
@ -27,7 +27,7 @@ describe("Layout shift artifacts", () => {
|
|||
|
||||
// Initial render
|
||||
ui.start();
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
await term.flush();
|
||||
|
||||
// Capture initial state
|
||||
|
|
@ -40,7 +40,7 @@ describe("Layout shift artifacts", () => {
|
|||
ui.requestRender();
|
||||
|
||||
// Wait for render
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
await term.flush();
|
||||
|
||||
// Capture state with status message
|
||||
|
|
@ -51,7 +51,7 @@ describe("Layout shift artifacts", () => {
|
|||
ui.requestRender();
|
||||
|
||||
// Wait for render
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
await term.flush();
|
||||
|
||||
// Capture final state
|
||||
|
|
@ -64,8 +64,12 @@ describe("Layout shift artifacts", () => {
|
|||
const nextLine = finalViewport[i + 1];
|
||||
|
||||
// Check if we have duplicate bottom borders (the artifact)
|
||||
if (currentLine.includes("╰") && currentLine.includes("╯") &&
|
||||
nextLine.includes("╰") && nextLine.includes("╯")) {
|
||||
if (
|
||||
currentLine.includes("╰") &&
|
||||
currentLine.includes("╯") &&
|
||||
nextLine.includes("╰") &&
|
||||
nextLine.includes("╯")
|
||||
) {
|
||||
foundDuplicateBorder = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -74,18 +78,12 @@ describe("Layout shift artifacts", () => {
|
|||
assert.strictEqual(foundDuplicateBorder, false, "Found duplicate bottom borders - rendering artifact detected!");
|
||||
|
||||
// Also check that there's only one bottom border total
|
||||
const bottomBorderCount = finalViewport.filter((line) =>
|
||||
line.includes("╰")
|
||||
).length;
|
||||
const bottomBorderCount = finalViewport.filter((line) => line.includes("╰")).length;
|
||||
assert.strictEqual(bottomBorderCount, 1, `Expected 1 bottom border, found ${bottomBorderCount}`);
|
||||
|
||||
// Verify the editor is back in its original position
|
||||
const finalEditorStartLine = finalViewport.findIndex((line) =>
|
||||
line.includes("╭")
|
||||
);
|
||||
const initialEditorStartLine = initialViewport.findIndex((line) =>
|
||||
line.includes("╭")
|
||||
);
|
||||
const finalEditorStartLine = finalViewport.findIndex((line) => line.includes("╭"));
|
||||
const initialEditorStartLine = initialViewport.findIndex((line) => line.includes("╭"));
|
||||
assert.strictEqual(finalEditorStartLine, initialEditorStartLine);
|
||||
|
||||
ui.stop();
|
||||
|
|
@ -103,7 +101,7 @@ describe("Layout shift artifacts", () => {
|
|||
|
||||
// Initial render
|
||||
ui.start();
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
await term.flush();
|
||||
|
||||
// Rapidly add and remove a status message
|
||||
|
|
@ -112,25 +110,21 @@ describe("Layout shift artifacts", () => {
|
|||
// Add status
|
||||
ui.children.splice(1, 0, status);
|
||||
ui.requestRender();
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
await term.flush();
|
||||
|
||||
// Remove status immediately
|
||||
ui.children.splice(1, 1);
|
||||
ui.requestRender();
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
await term.flush();
|
||||
|
||||
// Final output check
|
||||
const finalViewport = term.getViewport();
|
||||
|
||||
// Should only have one set of borders for the editor
|
||||
const topBorderCount = finalViewport.filter((line) =>
|
||||
line.includes("╭") && line.includes("╮")
|
||||
).length;
|
||||
const bottomBorderCount = finalViewport.filter((line) =>
|
||||
line.includes("╰") && line.includes("╯")
|
||||
).length;
|
||||
const topBorderCount = finalViewport.filter((line) => line.includes("╭") && line.includes("╮")).length;
|
||||
const bottomBorderCount = finalViewport.filter((line) => line.includes("╰") && line.includes("╯")).length;
|
||||
|
||||
assert.strictEqual(topBorderCount, 1);
|
||||
assert.strictEqual(bottomBorderCount, 1);
|
||||
|
|
@ -148,4 +142,4 @@ describe("Layout shift artifacts", () => {
|
|||
|
||||
ui.stop();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/usr/bin/env npx tsx
|
||||
import { TUI, Container, TextComponent, TextEditor, MarkdownComponent } from "../src/index.js";
|
||||
import { Container, MarkdownComponent, TextComponent, TextEditor, TUI } from "../src/index.js";
|
||||
|
||||
/**
|
||||
* Multi-Component Layout Demo
|
||||
|
|
@ -75,4 +75,4 @@ ui.onGlobalKeyPress = (data: string) => {
|
|||
return true;
|
||||
};
|
||||
|
||||
ui.start();
|
||||
ui.start();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { test, describe } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { describe, test } from "node:test";
|
||||
import { Container, LoadingAnimation, MarkdownComponent, TextComponent, TextEditor, TUI } from "../src/index.js";
|
||||
import { VirtualTerminal } from "./virtual-terminal.js";
|
||||
import { TUI, Container, TextComponent, MarkdownComponent, TextEditor, LoadingAnimation } from "../src/index.js";
|
||||
|
||||
describe("Multi-Message Garbled Output Reproduction", () => {
|
||||
test("handles rapid message additions with large content without garbling", async () => {
|
||||
|
|
@ -20,7 +20,7 @@ describe("Multi-Message Garbled Output Reproduction", () => {
|
|||
ui.setFocus(editor);
|
||||
|
||||
// Initial render
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
await terminal.flush();
|
||||
|
||||
// Step 1: Simulate user message
|
||||
|
|
@ -32,7 +32,7 @@ describe("Multi-Message Garbled Output Reproduction", () => {
|
|||
statusContainer.addChild(loadingAnim);
|
||||
|
||||
ui.requestRender();
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
await terminal.flush();
|
||||
|
||||
// Step 3: Simulate rapid tool calls with large outputs
|
||||
|
|
@ -54,7 +54,7 @@ node_modules/get-tsconfig/README.md
|
|||
chatContainer.addChild(new TextComponent(globResult));
|
||||
|
||||
ui.requestRender();
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
await terminal.flush();
|
||||
|
||||
// Simulate multiple read tool calls with long content
|
||||
|
|
@ -74,7 +74,7 @@ A collection of tools for managing LLM deployments and building AI agents.
|
|||
chatContainer.addChild(new MarkdownComponent(readmeContent));
|
||||
|
||||
ui.requestRender();
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
await terminal.flush();
|
||||
|
||||
// Second read with even more content
|
||||
|
|
@ -94,7 +94,7 @@ Terminal UI framework with surgical differential rendering for building flicker-
|
|||
chatContainer.addChild(new MarkdownComponent(tuiReadmeContent));
|
||||
|
||||
ui.requestRender();
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
await terminal.flush();
|
||||
|
||||
// Step 4: Stop loading animation and add assistant response
|
||||
|
|
@ -114,7 +114,7 @@ The TUI library features surgical differential rendering that minimizes screen u
|
|||
chatContainer.addChild(new MarkdownComponent(assistantResponse));
|
||||
|
||||
ui.requestRender();
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
await terminal.flush();
|
||||
|
||||
// Step 5: CRITICAL - Send a new message while previous content is displayed
|
||||
|
|
@ -126,7 +126,7 @@ The TUI library features surgical differential rendering that minimizes screen u
|
|||
statusContainer.addChild(loadingAnim2);
|
||||
|
||||
ui.requestRender();
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
await terminal.flush();
|
||||
|
||||
// Add assistant response
|
||||
|
|
@ -144,7 +144,7 @@ Key aspects:
|
|||
chatContainer.addChild(new MarkdownComponent(secondResponse));
|
||||
|
||||
ui.requestRender();
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
await terminal.flush();
|
||||
|
||||
// Debug: Show the garbled output after the problematic step
|
||||
|
|
@ -153,19 +153,25 @@ Key aspects:
|
|||
debugOutput.forEach((line, i) => {
|
||||
if (line.trim()) console.log(`${i}: "${line}"`);
|
||||
});
|
||||
|
||||
|
||||
// Step 6: Check final output
|
||||
const finalOutput = terminal.getScrollBuffer();
|
||||
|
||||
// Check that first user message is NOT garbled
|
||||
const userLine1 = finalOutput.find(line => line.includes("read all README.md files"));
|
||||
assert.strictEqual(userLine1, "read all README.md files except in node_modules",
|
||||
`First user message is garbled: "${userLine1}"`);
|
||||
const userLine1 = finalOutput.find((line) => line.includes("read all README.md files"));
|
||||
assert.strictEqual(
|
||||
userLine1,
|
||||
"read all README.md files except in node_modules",
|
||||
`First user message is garbled: "${userLine1}"`,
|
||||
);
|
||||
|
||||
// Check that second user message is clean
|
||||
const userLine2 = finalOutput.find(line => line.includes("What is the main purpose"));
|
||||
assert.strictEqual(userLine2, "What is the main purpose of the TUI library?",
|
||||
`Second user message is garbled: "${userLine2}"`);
|
||||
const userLine2 = finalOutput.find((line) => line.includes("What is the main purpose"));
|
||||
assert.strictEqual(
|
||||
userLine2,
|
||||
"What is the main purpose of the TUI library?",
|
||||
`Second user message is garbled: "${userLine2}"`,
|
||||
);
|
||||
|
||||
// Check for common garbling patterns
|
||||
const garbledPatterns = [
|
||||
|
|
@ -173,14 +179,14 @@ Key aspects:
|
|||
"README.mdectly",
|
||||
"modulesl rendering",
|
||||
"[assistant]ns.",
|
||||
"node_modules/@esbuild/darwin-arm64/README.mdategy"
|
||||
"node_modules/@esbuild/darwin-arm64/README.mdategy",
|
||||
];
|
||||
|
||||
for (const pattern of garbledPatterns) {
|
||||
const hasGarbled = finalOutput.some(line => line.includes(pattern));
|
||||
const hasGarbled = finalOutput.some((line) => line.includes(pattern));
|
||||
assert.ok(!hasGarbled, `Found garbled pattern "${pattern}" in output`);
|
||||
}
|
||||
|
||||
ui.stop();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
import { test, describe } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { VirtualTerminal } from "./virtual-terminal.js";
|
||||
import { describe, test } from "node:test";
|
||||
import {
|
||||
TUI,
|
||||
Container,
|
||||
TextComponent,
|
||||
TextEditor,
|
||||
WhitespaceComponent,
|
||||
MarkdownComponent,
|
||||
SelectList,
|
||||
TextComponent,
|
||||
TextEditor,
|
||||
TUI,
|
||||
WhitespaceComponent,
|
||||
} from "../src/index.js";
|
||||
import { VirtualTerminal } from "./virtual-terminal.js";
|
||||
|
||||
describe("TUI Rendering", () => {
|
||||
|
||||
test("renders single text component", async () => {
|
||||
const terminal = new VirtualTerminal(80, 24);
|
||||
const ui = new TUI(terminal);
|
||||
|
|
@ -22,7 +21,7 @@ describe("TUI Rendering", () => {
|
|||
ui.addChild(text);
|
||||
|
||||
// Wait for next tick for render to complete
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
|
||||
// Wait for writes to complete and get the rendered output
|
||||
const output = await terminal.flushAndGetViewport();
|
||||
|
|
@ -48,7 +47,7 @@ describe("TUI Rendering", () => {
|
|||
ui.addChild(new TextComponent("Line 3"));
|
||||
|
||||
// Wait for next tick for render to complete
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
|
||||
const output = await terminal.flushAndGetViewport();
|
||||
assert.strictEqual(output[0], "Line 1");
|
||||
|
|
@ -68,7 +67,7 @@ describe("TUI Rendering", () => {
|
|||
ui.addChild(new TextComponent("Bottom text"));
|
||||
|
||||
// Wait for next tick for render to complete
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
|
||||
const output = await terminal.flushAndGetViewport();
|
||||
assert.strictEqual(output[0], "Top text");
|
||||
|
|
@ -96,7 +95,7 @@ describe("TUI Rendering", () => {
|
|||
ui.addChild(new TextComponent("After container"));
|
||||
|
||||
// Wait for next tick for render to complete
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
|
||||
const output = await terminal.flushAndGetViewport();
|
||||
assert.strictEqual(output[0], "Before container");
|
||||
|
|
@ -117,11 +116,11 @@ describe("TUI Rendering", () => {
|
|||
ui.setFocus(editor);
|
||||
|
||||
// Wait for next tick for render to complete
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
|
||||
// Initial state - empty editor with cursor
|
||||
const output = await terminal.flushAndGetViewport();
|
||||
|
||||
|
||||
// Check that we have the border characters
|
||||
assert.ok(output[0].includes("╭"));
|
||||
assert.ok(output[0].includes("╮"));
|
||||
|
|
@ -142,7 +141,7 @@ describe("TUI Rendering", () => {
|
|||
ui.addChild(dynamicText);
|
||||
|
||||
// Wait for initial render
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
await terminal.flush();
|
||||
|
||||
// Save initial state
|
||||
|
|
@ -153,8 +152,8 @@ describe("TUI Rendering", () => {
|
|||
ui.requestRender();
|
||||
|
||||
// Wait for render
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
|
||||
// Flush terminal buffer
|
||||
await terminal.flush();
|
||||
|
||||
|
|
@ -180,7 +179,7 @@ describe("TUI Rendering", () => {
|
|||
ui.addChild(text3);
|
||||
|
||||
// Wait for initial render
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
|
||||
let output = await terminal.flushAndGetViewport();
|
||||
assert.strictEqual(output[0], "Line 1");
|
||||
|
|
@ -191,7 +190,7 @@ describe("TUI Rendering", () => {
|
|||
ui.removeChild(text2);
|
||||
ui.requestRender();
|
||||
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
await new Promise((resolve) => setImmediate(resolve));
|
||||
|
||||
output = await terminal.flushAndGetViewport();
|
||||
assert.strictEqual(output[0], "Line 1");
|
||||
|
|
@ -212,7 +211,7 @@ describe("TUI Rendering", () => {
|
|||
}
|
||||
|
||||
// Wait for next tick for render to complete
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
|
||||
const output = await terminal.flushAndGetViewport();
|
||||
|
||||
|
|
@ -241,7 +240,7 @@ describe("TUI Rendering", () => {
|
|||
ui.addChild(new TextComponent("After"));
|
||||
|
||||
// Wait for next tick for render to complete
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
|
||||
const output = await terminal.flushAndGetViewport();
|
||||
assert.strictEqual(output[0], "Before");
|
||||
|
|
@ -262,7 +261,7 @@ describe("TUI Rendering", () => {
|
|||
ui.addChild(markdown);
|
||||
|
||||
// Wait for next tick for render to complete
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
|
||||
const output = await terminal.flushAndGetViewport();
|
||||
// Should have formatted markdown
|
||||
|
|
@ -289,7 +288,7 @@ describe("TUI Rendering", () => {
|
|||
ui.setFocus(selectList);
|
||||
|
||||
// Wait for next tick for render to complete
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
|
||||
const output = await terminal.flushAndGetViewport();
|
||||
// First option should be selected (has → indicator)
|
||||
|
|
@ -303,28 +302,28 @@ describe("TUI Rendering", () => {
|
|||
|
||||
test("preserves existing terminal content when rendering", async () => {
|
||||
const terminal = new VirtualTerminal(80, 24);
|
||||
|
||||
|
||||
// Write some content to the terminal before starting TUI
|
||||
// This simulates having existing content in the scrollback buffer
|
||||
terminal.write("Previous command output line 1\r\n");
|
||||
terminal.write("Previous command output line 2\r\n");
|
||||
terminal.write("Some important information\r\n");
|
||||
terminal.write("Last line before TUI starts\r\n");
|
||||
|
||||
|
||||
// Flush to ensure writes are complete
|
||||
await terminal.flush();
|
||||
|
||||
|
||||
// Get the initial state with existing content
|
||||
const initialOutput = [...terminal.getViewport()];
|
||||
assert.strictEqual(initialOutput[0], "Previous command output line 1");
|
||||
assert.strictEqual(initialOutput[1], "Previous command output line 2");
|
||||
assert.strictEqual(initialOutput[2], "Some important information");
|
||||
assert.strictEqual(initialOutput[3], "Last line before TUI starts");
|
||||
|
||||
|
||||
// Now start the TUI with a text editor
|
||||
const ui = new TUI(terminal);
|
||||
ui.start();
|
||||
|
||||
|
||||
const editor = new TextEditor();
|
||||
let submittedText = "";
|
||||
editor.onSubmit = (text) => {
|
||||
|
|
@ -332,87 +331,87 @@ describe("TUI Rendering", () => {
|
|||
};
|
||||
ui.addChild(editor);
|
||||
ui.setFocus(editor);
|
||||
|
||||
|
||||
// Wait for initial render
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
await terminal.flush();
|
||||
|
||||
|
||||
// Check that the editor is rendered after the existing content
|
||||
const afterTuiStart = terminal.getViewport();
|
||||
|
||||
|
||||
// The existing content should still be visible above the editor
|
||||
assert.strictEqual(afterTuiStart[0], "Previous command output line 1");
|
||||
assert.strictEqual(afterTuiStart[1], "Previous command output line 2");
|
||||
assert.strictEqual(afterTuiStart[2], "Some important information");
|
||||
assert.strictEqual(afterTuiStart[3], "Last line before TUI starts");
|
||||
|
||||
|
||||
// The editor should appear after the existing content
|
||||
// The editor is 3 lines tall (top border, content line, bottom border)
|
||||
// Top border with box drawing characters filling the width (80 chars)
|
||||
assert.strictEqual(afterTuiStart[4][0], "╭");
|
||||
assert.strictEqual(afterTuiStart[4][78], "╮");
|
||||
|
||||
|
||||
// Content line should have the prompt
|
||||
assert.strictEqual(afterTuiStart[5].substring(0, 4), "│ > ");
|
||||
// And should end with vertical bar
|
||||
assert.strictEqual(afterTuiStart[5][78], "│");
|
||||
|
||||
|
||||
// Bottom border
|
||||
assert.strictEqual(afterTuiStart[6][0], "╰");
|
||||
assert.strictEqual(afterTuiStart[6][78], "╯");
|
||||
|
||||
|
||||
// Type some text into the editor
|
||||
terminal.sendInput("Hello World");
|
||||
|
||||
|
||||
// Wait for the input to be processed
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
await terminal.flush();
|
||||
|
||||
|
||||
// Check that text appears in the editor
|
||||
const afterTyping = terminal.getViewport();
|
||||
assert.strictEqual(afterTyping[0], "Previous command output line 1");
|
||||
assert.strictEqual(afterTyping[1], "Previous command output line 2");
|
||||
assert.strictEqual(afterTyping[2], "Some important information");
|
||||
assert.strictEqual(afterTyping[3], "Last line before TUI starts");
|
||||
|
||||
|
||||
// The editor content should show the typed text with the prompt ">"
|
||||
assert.strictEqual(afterTyping[5].substring(0, 15), "│ > Hello World");
|
||||
|
||||
|
||||
// Send SHIFT+ENTER to the editor (adds a new line)
|
||||
// According to text-editor.ts line 251, SHIFT+ENTER is detected as "\n" which calls addNewLine()
|
||||
terminal.sendInput("\n");
|
||||
|
||||
|
||||
// Wait for the input to be processed
|
||||
await new Promise(resolve => process.nextTick(resolve));
|
||||
await new Promise((resolve) => process.nextTick(resolve));
|
||||
await terminal.flush();
|
||||
|
||||
|
||||
// Check that existing content is still preserved after adding new line
|
||||
const afterNewLine = terminal.getViewport();
|
||||
assert.strictEqual(afterNewLine[0], "Previous command output line 1");
|
||||
assert.strictEqual(afterNewLine[1], "Previous command output line 2");
|
||||
assert.strictEqual(afterNewLine[2], "Some important information");
|
||||
assert.strictEqual(afterNewLine[3], "Last line before TUI starts");
|
||||
|
||||
|
||||
// Editor should now be 4 lines tall (top border, first line, second line, bottom border)
|
||||
// Top border at line 4
|
||||
assert.strictEqual(afterNewLine[4][0], "╭");
|
||||
assert.strictEqual(afterNewLine[4][78], "╮");
|
||||
|
||||
|
||||
// First line with text at line 5
|
||||
assert.strictEqual(afterNewLine[5].substring(0, 15), "│ > Hello World");
|
||||
assert.strictEqual(afterNewLine[5][78], "│");
|
||||
|
||||
|
||||
// Second line (empty, with continuation prompt " ") at line 6
|
||||
assert.strictEqual(afterNewLine[6].substring(0, 4), "│ ");
|
||||
assert.strictEqual(afterNewLine[6][78], "│");
|
||||
|
||||
|
||||
// Bottom border at line 7
|
||||
assert.strictEqual(afterNewLine[7][0], "╰");
|
||||
assert.strictEqual(afterNewLine[7][78], "╯");
|
||||
|
||||
|
||||
// Verify that onSubmit was NOT called (since we pressed SHIFT+ENTER, not plain ENTER)
|
||||
assert.strictEqual(submittedText, "");
|
||||
|
||||
|
||||
ui.stop();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { test, describe } from "node:test";
|
||||
import assert from "node:assert";
|
||||
import { describe, test } from "node:test";
|
||||
import { VirtualTerminal } from "./virtual-terminal.js";
|
||||
|
||||
describe("VirtualTerminal", () => {
|
||||
|
|
@ -86,13 +86,13 @@ describe("VirtualTerminal", () => {
|
|||
assert.strictEqual(viewport.length, 10);
|
||||
assert.strictEqual(viewport[0], "Line 7");
|
||||
assert.strictEqual(viewport[8], "Line 15");
|
||||
assert.strictEqual(viewport[9], ""); // Last line is empty after the final \r\n
|
||||
assert.strictEqual(viewport[9], ""); // Last line is empty after the final \r\n
|
||||
|
||||
// Scroll buffer should have all lines
|
||||
assert.ok(scrollBuffer.length >= 15);
|
||||
// Check specific lines exist in the buffer
|
||||
const hasLine1 = scrollBuffer.some(line => line === "Line 1");
|
||||
const hasLine15 = scrollBuffer.some(line => line === "Line 15");
|
||||
const hasLine1 = scrollBuffer.some((line) => line === "Line 1");
|
||||
const hasLine15 = scrollBuffer.some((line) => line === "Line 15");
|
||||
assert.ok(hasLine1, "Buffer should contain 'Line 1'");
|
||||
assert.ok(hasLine15, "Buffer should contain 'Line 15'");
|
||||
});
|
||||
|
|
@ -129,9 +129,12 @@ describe("VirtualTerminal", () => {
|
|||
const terminal = new VirtualTerminal(80, 24);
|
||||
|
||||
let received = "";
|
||||
terminal.start((data) => {
|
||||
received = data;
|
||||
}, () => {});
|
||||
terminal.start(
|
||||
(data) => {
|
||||
received = data;
|
||||
},
|
||||
() => {},
|
||||
);
|
||||
|
||||
terminal.sendInput("a");
|
||||
assert.strictEqual(received, "a");
|
||||
|
|
@ -146,13 +149,16 @@ describe("VirtualTerminal", () => {
|
|||
const terminal = new VirtualTerminal(80, 24);
|
||||
|
||||
let resized = false;
|
||||
terminal.start(() => {}, () => {
|
||||
resized = true;
|
||||
});
|
||||
terminal.start(
|
||||
() => {},
|
||||
() => {
|
||||
resized = true;
|
||||
},
|
||||
);
|
||||
|
||||
terminal.resize(100, 30);
|
||||
assert.strictEqual(resized, true);
|
||||
|
||||
terminal.stop();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import xterm from '@xterm/headless';
|
||||
import type { Terminal as XtermTerminalType } from '@xterm/headless';
|
||||
import { Terminal } from '../src/terminal.js';
|
||||
import type { Terminal as XtermTerminalType } from "@xterm/headless";
|
||||
import xterm from "@xterm/headless";
|
||||
import type { Terminal } from "../src/terminal.js";
|
||||
|
||||
// Extract Terminal class from the module
|
||||
const XtermTerminal = xterm.Terminal;
|
||||
|
|
@ -81,7 +81,7 @@ export class VirtualTerminal implements Terminal {
|
|||
async flush(): Promise<void> {
|
||||
// Write an empty string to ensure all previous writes are flushed
|
||||
return new Promise<void>((resolve) => {
|
||||
this.xterm.write('', () => resolve());
|
||||
this.xterm.write("", () => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -107,7 +107,7 @@ export class VirtualTerminal implements Terminal {
|
|||
if (line) {
|
||||
lines.push(line.translateToString(true));
|
||||
} else {
|
||||
lines.push('');
|
||||
lines.push("");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -127,7 +127,7 @@ export class VirtualTerminal implements Terminal {
|
|||
if (line) {
|
||||
lines.push(line.translateToString(true));
|
||||
} else {
|
||||
lines.push('');
|
||||
lines.push("");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -155,7 +155,7 @@ export class VirtualTerminal implements Terminal {
|
|||
const buffer = this.xterm.buffer.active;
|
||||
return {
|
||||
x: buffer.cursorX,
|
||||
y: buffer.cursorY
|
||||
y: buffer.cursorY,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue