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:
Mario Zechner
2025-09-02 23:59:36 +02:00
parent 004de3c9d0
commit 66cefb236e
29 changed files with 5835 additions and 6225 deletions

View File

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