co-mono/packages/tui
Mario Zechner e7097d911a Custom tools with session lifecycle, examples for hooks and tools
- Custom tools: TypeScript modules that extend pi with new tools
  - Custom TUI rendering via renderCall/renderResult
  - User interaction via pi.ui (select, confirm, input, notify)
  - Session lifecycle via onSession callback for state reconstruction
  - Examples: todo.ts, question.ts, hello.ts

- Hook examples: permission-gate, git-checkpoint, protected-paths

- Session lifecycle centralized in AgentSession
  - Works across all modes (interactive, print, RPC)
  - Unified session event for hooks (replaces session_start/session_switch)

- Box component added to pi-tui

- Examples bundled in npm and binary releases

Fixes #190
2025-12-17 16:03:23 +01:00
..
src Custom tools with session lifecycle, examples for hooks and tools 2025-12-17 16:03:23 +01:00
test add new getCursor and getLines methods to editor (#201) 2025-12-16 20:13:43 +01:00
package.json Release v0.22.5 2025-12-17 01:22:13 +01:00
README.md Custom tools with session lifecycle, examples for hooks and tools 2025-12-17 16:03:23 +01:00
tsconfig.build.json Initial monorepo setup with npm workspaces and dual TypeScript configuration 2025-08-09 17:18:38 +02:00
vitest.config.ts fix: escape codes not properly wrapped during line wrapping 2025-12-05 21:49:44 +01:00

@mariozechner/pi-tui

Minimal terminal UI framework with differential rendering and synchronized output for flicker-free interactive CLI applications.

Features

  • Differential Rendering: Three-strategy rendering system that only updates what changed
  • Synchronized Output: Uses CSI 2026 for atomic screen updates (no flicker)
  • Bracketed Paste Mode: Handles large pastes correctly with markers for >10 line pastes
  • Component-based: Simple Component interface with render() method
  • Built-in Components: Text, Input, Editor, Markdown, Loader, SelectList, Spacer, Image, Box, Container
  • Inline Images: Renders images in terminals that support Kitty or iTerm2 graphics protocols
  • Autocomplete Support: File paths and slash commands

Quick Start

import { TUI, Text, Editor, ProcessTerminal } from "@mariozechner/pi-tui";

// Create terminal
const terminal = new ProcessTerminal();

// Create TUI
const tui = new TUI(terminal);

// Add components
tui.addChild(new Text("Welcome to my app!"));

const editor = new Editor();
editor.onSubmit = (text) => {
	console.log("Submitted:", text);
	tui.addChild(new Text(`You said: ${text}`));
};
tui.addChild(editor);

// Start
tui.start();

Core API

TUI

Main container that manages components and rendering.

const tui = new TUI(terminal);
tui.addChild(component);
tui.removeChild(component);
tui.start();
tui.stop();
tui.requestRender(); // Request a re-render

Component Interface

All components implement:

interface Component {
	render(width: number): string[];
	handleInput?(data: string): void;
}

Built-in Components

Container

Groups child components.

const container = new Container();
container.addChild(component);
container.removeChild(component);

Box

Container that applies padding and background color to all children.

const box = new Box(
	1,                  // paddingX (default: 1)
	1,                  // paddingY (default: 1)
	(text) => chalk.bgGray(text)  // optional background function
);
box.addChild(new Text("Content", 0, 0));
box.setBgFn((text) => chalk.bgBlue(text));  // Change background dynamically

Text

Displays multi-line text with word wrapping and padding.

const text = new Text("Hello World", paddingX, paddingY); // defaults: 1, 1
text.setText("Updated text");

Input

Single-line text input with horizontal scrolling.

const input = new Input();
input.onSubmit = (value) => console.log(value);
input.setValue("initial");

Key Bindings:

  • Enter - Submit
  • Ctrl+A / Ctrl+E - Line start/end
  • Ctrl+W or Option+Backspace - Delete word backwards
  • Ctrl+U - Delete to start of line
  • Ctrl+K - Delete to end of line
  • Arrow keys, Backspace, Delete work as expected

Editor

Multi-line text editor with autocomplete, file completion, and paste handling.

const editor = new Editor();
editor.onSubmit = (text) => console.log(text);
editor.onChange = (text) => console.log("Changed:", text);
editor.disableSubmit = true; // Disable submit temporarily
editor.setAutocompleteProvider(provider);

Features:

  • Multi-line editing with word wrap
  • Slash command autocomplete (type /)
  • File path autocomplete (press Tab)
  • Large paste handling (>10 lines creates [paste #1 +50 lines] marker)
  • Horizontal lines above/below editor
  • Fake cursor rendering (hidden real cursor)

Key Bindings:

  • Enter - Submit
  • Shift+Enter, Ctrl+Enter, or Alt+Enter - New line (terminal-dependent, Alt+Enter most reliable)
  • Tab - Autocomplete
  • Ctrl+K - Delete line
  • Ctrl+A / Ctrl+E - Line start/end
  • Arrow keys, Backspace, Delete work as expected

Markdown

Renders markdown with syntax highlighting and optional background colors.

const md = new Markdown(
	"# Hello\n\nSome **bold** text",
	bgColor,           // optional: "bgRed", "bgBlue", etc.
	fgColor,           // optional: "white", "cyan", etc.
	customBgRgb,       // optional: { r: 52, g: 53, b: 65 }
	paddingX,          // optional: default 1
	paddingY           // optional: default 1
);
md.setText("Updated markdown");

Features:

  • Headings, bold, italic, code blocks, lists, links, blockquotes
  • Syntax highlighting with chalk
  • Optional background colors (including custom RGB)
  • Padding support
  • Render caching for performance

Loader

Animated loading spinner.

const loader = new Loader(tui, "Loading...");
loader.start();
loader.stop();

SelectList

Interactive selection list with keyboard navigation.

const list = new SelectList([
	{ value: "opt1", label: "Option 1", description: "First option" },
	{ value: "opt2", label: "Option 2", description: "Second option" },
], 5); // maxVisible

list.onSelect = (item) => console.log("Selected:", item);
list.onCancel = () => console.log("Cancelled");
list.setFilter("opt"); // Filter items

Controls:

  • Arrow keys: Navigate
  • Enter or Tab: Select
  • Escape: Cancel

Spacer

Empty lines for vertical spacing.

const spacer = new Spacer(2); // 2 empty lines (default: 1)

Image

Renders images inline for terminals that support the Kitty graphics protocol (Kitty, Ghostty, WezTerm) or iTerm2 inline images. Falls back to a text placeholder on unsupported terminals.

import { Image } from "@mariozechner/pi-tui";

const image = new Image(
	base64Data,        // base64-encoded image data
	"image/png",       // MIME type
	{ fallbackColor: (s) => s },  // theme for fallback text
	{ maxWidthCells: 60 }         // optional: limit width
);
tui.addChild(image);

Supported formats: PNG, JPEG, GIF, WebP. Dimensions are parsed from the image headers automatically.

Autocomplete

CombinedAutocompleteProvider

Supports both slash commands and file paths.

import { CombinedAutocompleteProvider } from "@mariozechner/pi-tui";

const provider = new CombinedAutocompleteProvider(
	[
		{ name: "help", description: "Show help" },
		{ name: "clear", description: "Clear screen" },
		{ name: "delete", description: "Delete last message" },
	],
	process.cwd() // base path for file completion
);

editor.setAutocompleteProvider(provider);

Features:

  • Type / to see slash commands
  • Press Tab for file path completion
  • Works with ~/, ./, ../, and @ prefix
  • Filters to attachable files for @ prefix

Differential Rendering

The TUI uses three rendering strategies:

  1. First Render: Output all lines without clearing scrollback
  2. Width Changed or Change Above Viewport: Clear screen and full re-render
  3. Normal Update: Move cursor to first changed line, clear to end, render changed lines

All updates are wrapped in synchronized output (\x1b[?2026h ... \x1b[?2026l) for atomic, flicker-free rendering.

Terminal Interface

The TUI works with any object implementing the Terminal interface:

interface Terminal {
	start(onInput: (data: string) => void, onResize: () => void): void;
	stop(): void;
	write(data: string): void;
	get columns(): number;
	get rows(): number;
	moveBy(lines: number): void;
	hideCursor(): void;
	showCursor(): void;
	clearLine(): void;
	clearFromCursor(): void;
	clearScreen(): void;
}

Built-in implementations:

  • ProcessTerminal - Uses process.stdin/stdout
  • VirtualTerminal - For testing (uses @xterm/headless)

Example

See test/chat-simple.ts for a complete chat interface example with:

  • Markdown messages with custom background colors
  • Loading spinner during responses
  • Editor with autocomplete and slash commands
  • Spacers between messages

Run it:

npx tsx test/chat-simple.ts

Development

# Install dependencies (from monorepo root)
npm install

# Run type checking
npm run check

# Run the demo
npx tsx test/chat-simple.ts