Building with @pi/tui
Relevant source files
The following files were used as context for generating this wiki page:
The @mariozechner/pi-tui package is a minimal terminal UI framework designed for creating flicker-free, interactive command-line applications. Its core features include a component-based architecture, a powerful differential rendering system, and synchronized output to ensure atomic screen updates. This makes it suitable for building complex interfaces like chat applications and interactive agents directly in the terminal.
This library is a foundational element within the pi-mono repository, providing the user interface for the @mariozechner/pi-coding-agent. It abstracts away the complexities of terminal manipulation, allowing developers to focus on application logic by composing simple, state-driven components.
Sources: packages/tui/README.md, packages/tui/package.json, packages/coding-agent/src/tui/tui-renderer.ts
Core Architecture
The framework is built around three main concepts: the TUI class, the Component interface, and the Terminal interface. These elements work together to manage the application state, render the UI, and handle user input.
Sources: packages/tui/README.md:19-25, packages/tui/src/tui.ts:16-25
Key Abstractions
The following class diagram illustrates the relationship between the core abstractions.
This diagram shows that the TUI class orchestrates the application, using a Terminal implementation for I/O and managing a tree of Component objects that define the UI.
Sources: packages/tui/README.md:22-25,32-35, packages/tui/src/tui.ts:16-25, packages/tui/src/components/editor.ts:24, packages/tui/src/components/markdown.ts:21
The TUI Class
The TUI class is the central controller of the application. It initializes the terminal, manages the component tree, and orchestrates the rendering and input handling loop.
- Component Management: It maintains a list of child components. Methods like
addChild()andremoveChild()allow for a dynamic UI. - Focus Management: The
setFocus()method directs keyboard input to a specific component. - Render Loop: The
start()method initiates the main loop that listens for terminal events (input, resize) and triggers re-renders. - Rendering: It calls the
render()method on its child components to get the lines to be drawn and then uses its differential rendering algorithm to efficiently update the screen.
Sources: packages/tui/src/tui.ts:16-173
The Terminal Interface
This interface abstracts terminal operations, allowing the TUI to work with different terminal backends. The library provides two implementations:
ProcessTerminal: Usesprocess.stdinandprocess.stdoutfor live applications.VirtualTerminal: Uses@xterm/headlessfor testing purposes.
Sources: packages/tui/README.md:144-162
The Component Interface
Everything that can be rendered on the screen is a Component. It's a simple interface with two optional methods:
render(width: number): string[]: Returns an array of strings, where each string is a line to be displayed.handleInput?(data: string): void: Processes user input if the component is in focus.
Sources: packages/tui/README.md:27-31
Differential Rendering
A key feature of @pi/tui is its flicker-free differential rendering, which minimizes screen updates. All updates are wrapped in synchronized output escape codes (CSI ?2026h and CSI ?2026l) to ensure atomic updates.
The rendering logic follows three strategies:
- First Render: Writes all lines directly to the output without clearing the screen.
- Full Re-render: If the terminal width changes or a change occurs above the visible area, the entire screen is cleared and redrawn.
- Partial Update: For normal changes, the TUI calculates the first line that differs from the previous render. It then moves the cursor to that line, clears everything below it, and writes only the new and changed lines.
Sources: packages/tui/README.md:133-142, packages/tui/src/tui.ts:175-253
The following flowchart illustrates the rendering decision process within the TUI.render() method.
Sources: packages/tui/src/tui.ts:175-253
Built-in Components
The library includes several pre-built components for common UI patterns.
| Component | Description | Source File |
|---|---|---|
Container | Groups child components vertically. | packages/tui/README.md:32-35 |
Text | Displays multi-line, word-wrapped text with padding. | packages/tui/README.md:37-40 |
Input | A single-line text input field. | packages/tui/README.md:42-45 |
Editor | A multi-line text editor with autocomplete and paste handling. | packages/tui/src/components/editor.ts |
Markdown | Renders Markdown text with syntax highlighting. | packages/tui/src/components/markdown.ts |
Loader | An animated loading spinner. | packages/tui/README.md:99-102 |
SelectList | An interactive list for selection, with keyboard navigation. | packages/tui/README.md:104-111 |
Spacer | Adds a specified number of empty lines for vertical spacing. | packages/tui/README.md:126-128 |
Editor Component
The Editor is one of the most powerful components, providing a rich multi-line editing experience.
Features:
- Multi-line text editing with word wrap.
- Autocomplete for slash commands and file paths, managed via an
AutocompleteProvider. - Smart paste handling that marks large pastes.
- Callbacks for
onSubmitandonChangeevents. - A fake cursor is rendered, while the real terminal cursor is hidden for a smoother experience.
Key Bindings:
Enter: Submit the content.Shift+Enter/Alt+Enter: Insert a new line.Tab: Trigger autocomplete.Ctrl+K: Delete the current line.
Sources: packages/tui/src/components/editor.ts, packages/tui/README.md:55-75
Markdown Component
The Markdown component renders Markdown-formatted strings to the terminal. It uses the marked library for parsing and chalk for syntax highlighting.
Features:
- Supports standard Markdown syntax like headings, lists, code blocks, and bold/italic text.
- Renders tables and nested lists.
- Caches rendered output for performance, re-rendering only when text or width changes.
- Customizable theming for colors and styles.
- Can be pre-styled with a base color and italicization, which is used in the coding agent to display "thinking" traces.
Sources: packages/tui/src/components/markdown.ts:21-179, packages/tui/test/markdown.test.ts, packages/tui/package.json:30-31
Real-World Implementation: TuiRenderer
The @mariozechner/pi-coding-agent uses @pi/tui extensively in its TuiRenderer class. This class serves as an excellent example of how to build a complex, dynamic TUI application.
Sources: packages/coding-agent/src/tui/tui-renderer.ts
The TuiRenderer composes multiple components to create the agent's chat interface:
- A main
chatContainerholds the history of user and assistant messages. - An
editorContainerdynamically switches between theCustomEditorand various selector components (ModelSelectorComponent,ThemeSelectorComponent, etc.). - A
FooterComponentdisplays status information. Loadercomponents are shown during asynchronous operations.Markdownand custom components likeAssistantMessageComponentare used to render chat history.
The following diagram shows the high-level component structure of the TuiRenderer.
This demonstrates how TuiRenderer dynamically adds and removes components to the ChatContainer and swaps components within the EditorContainer to create an interactive experience.
Sources: packages/coding-agent/src/tui/tui-renderer.ts:70-165, 340-360
Event Handling and UI Updates
The TuiRenderer subscribes to events from the Agent and updates the UI accordingly. This pattern decouples the UI from the application logic.
The sequence diagram below shows the flow when a user submits a message.
This flow highlights how TuiRenderer listens to agent events to add user messages, show loading indicators, and stream the assistant's response into the chat view.
Sources: packages/coding-agent/src/tui/tui-renderer.ts:289-338, 485-619, packages/tui/test/chat-simple.ts:70-137
Conclusion
The @pi/tui package provides a robust and efficient foundation for building sophisticated terminal user interfaces in TypeScript. Its component-based design, combined with a smart differential rendering engine, enables developers to create responsive and flicker-free applications with minimal boilerplate. The architecture is flexible, allowing for simple static layouts or complex, dynamic interfaces like the one seen in the pi-coding-agent. By abstracting terminal I/O and state management, it empowers developers to build powerful CLI tools.