-
Notifications
You must be signed in to change notification settings - Fork 0
packages tui
Active contributors: Mario Zechner, kt, Sviatoslav Abakumov
packages/tui is @earendil-works/pi-tui, the terminal rendering engine and component library that powers Prime Agent's interactive mode. It is a standalone TUI framework: it owns the terminal session (raw mode, alternate screen, mouse and keyboard protocols), renders a component tree into a screen buffer using differential rendering, and ships the interactive widgets the chat UI is built from (editor, markdown, select list, images, loaders). The interactive mode in packages/coding-agent/src/modes/interactive/ is the primary consumer; the package is also used by demo apps and does not depend on the coding agent.
packages/tui/
├── src/
│ ├── index.ts Public exports
│ ├── tui.ts TUI engine, Container, Component, overlays, diffing
│ ├── terminal.ts Terminal interface + ProcessTerminal
│ ├── render-cache.ts VersionedRenderCache
│ ├── fullscreen.ts FullscreenViewport (alternate-screen transcript window)
│ ├── terminal-colors.ts Default color probing and ANSI color helpers
│ ├── terminal-image.ts Kitty / iTerm2 image encoding and capability detection
│ ├── keys.ts Keyboard sequence parsing and key identifiers
│ ├── keybindings.ts Configurable keybinding registry
│ ├── mouse.ts SGR mouse event parsing
│ ├── stdin-buffer.ts Batched stdin sequence splitting
│ ├── editor-component.ts EditorComponent interface for custom editors
│ ├── autocomplete.ts Autocomplete providers and suggestion logic
│ ├── latex.ts LaTeX math to Unicode conversion
│ ├── fuzzy.ts Fuzzy matching helpers
│ ├── selection-metadata.ts Table cell selection regions
│ ├── slash-command-context.ts Slash command context for the editor
│ ├── undo-stack.ts Generic clone-on-push undo stack
│ ├── kill-ring.ts Emacs-style kill/yank ring buffer
│ ├── utils.ts Text, ANSI, and width utilities
│ └── components/
│ ├── box.ts Bordered box
│ ├── cancellable-loader.ts Cancellable loading indicator
│ ├── editor.ts Multi-line text editor (86 KB, the largest component)
│ ├── image.ts Terminal image renderer
│ ├── input.ts Single-line text input
│ ├── loader.ts Indeterminate loading indicator
│ ├── markdown.ts Markdown renderer
│ ├── select-list.ts Scrollable list / dropdown
│ ├── settings-list.ts Key-value settings list
│ ├── spacer.ts Layout spacer
│ ├── text.ts Static styled text
│ └── truncated-text.ts Width-truncated text
├── test/ Vitest suites (29 files)
└── package.json
| Type | Path | Description |
|---|---|---|
TUI |
packages/tui/src/tui.ts |
Main engine: owns the terminal, runs the render loop, holds the component tree and focused component |
Container |
packages/tui/src/tui.ts |
Composes child components, stacking their rendered lines |
Component |
packages/tui/src/tui.ts |
Render contract (render(width) returns string[], optional handleInput) |
Terminal |
packages/tui/src/terminal.ts |
Abstract terminal interface: input, resize, cursor, alt screen, mouse tracking |
ProcessTerminal |
packages/tui/src/terminal.ts |
Real implementation over process.stdin/stdout, negotiates the Kitty keyboard protocol |
VersionedRenderCache |
packages/tui/src/render-cache.ts |
Cache that returns previously rendered lines for a (width, version) key |
FullscreenViewport |
packages/tui/src/fullscreen.ts |
Scrollable window over the transcript with a dock pinned to the bottom |
KeybindingsManager |
packages/tui/src/keybindings.ts |
Resolves configurable keybinding definitions to concrete keys |
StdinBuffer |
packages/tui/src/stdin-buffer.ts |
Splits batched stdin into complete escape sequences |
Editor |
packages/tui/src/components/editor.ts |
Full-featured multi-line editor with undo, kill ring, autocomplete, paste markers |
EditorComponent |
packages/tui/src/editor-component.ts |
Interface allowing custom editor implementations |
UndoStack |
packages/tui/src/undo-stack.ts |
Clone-on-push state snapshot stack used by the editor |
KillRing |
packages/tui/src/kill-ring.ts |
Emacs-style kill/yank ring used by the editor |
Key, parseKey, matchesKey
|
packages/tui/src/keys.ts |
Type-safe key identifiers and sequence matching |
parseSgrMouseEvent |
packages/tui/src/mouse.ts |
Parses SGR mouse reports into structured events |
The engine's job is to convert a component tree into a minimal stream of terminal escape sequences each frame. It is organized around a double-buffer differential render: it keeps the previous frame's lines, renders the current frame, diffs them, and emits only the changed rows wrapped in synchronized output.
TUI (a Container) renders its children top to bottom. Each child's render(width) returns a string[], which Container.render in packages/tui/src/tui.ts concatenates into the full frame. Before diffing, overlays are composited into the frame and the CURSOR_MARKER (an APC escape sequence components emit where the hardware cursor should sit) is extracted. applyLineResets appends a style-reset tail to every non-image line so style from one row cannot bleed into the next.
flowchart TD
A[Component tree] --> B["Container.render(width)"]
B --> C[Full frame string[]]
C --> D[Composite overlays]
D --> E[Extract CURSOR_MARKER]
E --> F["applyLineResets"]
F --> G[First/last changed line scan vs previousLines]
G --> H{Changed rows?}
H -- No --> I[Only reposition hardware cursor]
H -- Yes --> J[Build diff buffer: clear + repaint changed rows]
J --> K[Wrap in synchronized output ESC [ ? 2026 h / l]
K --> L["terminal.write(buffer)"]
L --> M[Store newLines as previousLines]
M --> N[Position hardware cursor at marker]
doRender in packages/tui/src/tui.ts decides between several strategies:
- First render: emit everything assuming a clean screen, no clearing.
- Width changed: always full re-render, because line wrapping changes. Height changed: full re-render, except in Termux where the software keyboard changes height constantly.
-
clearOnShrink: if content shrank below the working area (with no overlays), re-render to clear empty rows. - Otherwise it scans
previousLinesvs the new lines to findfirstChangedandlastChanged, then repaints only those rows, clearing each withESC [ 2 Kand rewriting it. Changed rows are wrapped in synchronized output (ESC [?2026h/ESC [?2026l) so the terminal renders the batch atomically and avoids flicker. - When the first changed line is above the previous viewport top, the on-screen rows no longer correspond to the new frame. While the transcript is growing, the engine repaints only the visible window in place, preserving terminal scrollback and avoiding the whole-transcript replay that causes flicker. A shrink still triggers a full redraw.
Line width is a hard invariant. If a rendered line exceeds terminal.columns, doRender writes a crash log to ~/.prime/agent/pi-crash.log and throws with guidance to use visibleWidth() and truncateToWidth() from packages/tui/src/utils.ts.
Inline rendering leaves the primary screen's scrollback untouched: cleared rows use ESC [ 2 K (erase line) rather than scrolling, and fullRender(true) uses ESC [2J ESC [H, which clears the visible screen without destroying scrollback above it. Full-screen (alternate screen) rendering via enterFullscreen in packages/tui/src/tui.ts uses FullscreenViewport (packages/tui/src/fullscreen.ts): a fixed grid painted with absolute addressing and diffed row by row, with a dock pinned to the bottom rows. Scroll position there is application state, not terminal scrollback. On exit, the inline differ resumes from the entry snapshot so content produced while fullscreen flows into native scrollback.
ProcessTerminal in packages/tui/src/terminal.ts sets raw mode, enables bracketed paste, and negotiates the Kitty keyboard protocol by sending CSI ? u and watching for a CSI ? <flags> u response in the StdinBuffer. If none arrives within 150 ms it falls back to xterm modifyOtherKeys mode 2 (needed under tmux). It probes default terminal colors via OSC, queries cell dimensions for image rendering (CSI 16 t), and supports an alternate-screen handoff so a worker-backed session attach can preserve the fullscreen frame across an in-process restart.
| Component | Path | Purpose |
|---|---|---|
Box |
packages/tui/src/components/box.ts |
Draws a bordered box around child content |
Text |
packages/tui/src/components/text.ts |
Static, styled text block |
Input |
packages/tui/src/components/input.ts |
Single-line text input with prompt handling |
Editor |
packages/tui/src/components/editor.ts |
Multi-line editor with cursor navigation, undo, kill ring, autocomplete, paste and image markers |
Markdown |
packages/tui/src/components/markdown.ts |
Renders markdown (headings, lists, code, links) to styled lines |
SelectList |
packages/tui/src/components/select-list.ts |
Scrollable single-select list used for dropdowns and pickers |
SettingsList |
packages/tui/src/components/settings-list.ts |
Key-value list for settings/options UI |
Loader |
packages/tui/src/components/loader.ts |
Indeterminate spinner/progress indicator |
CancellableLoader |
packages/tui/src/components/cancellable-loader.ts |
Loader with a cancel affordance |
Image |
packages/tui/src/components/image.ts |
Renders terminal images (Kitty / iTerm2), with fallback for unsupported terminals |
Spacer |
packages/tui/src/components/spacer.ts |
Consumes vertical/horizontal space in a layout |
TruncatedText |
packages/tui/src/components/truncated-text.ts |
Text truncated to a width, with an ellipsis |
Input flows from raw stdin through a series of stages:
-
StdinBufferinpackages/tui/src/stdin-buffer.tsaccumulates raw data and emits complete escape sequences. It detects CSI, OSC, DCS, and APC termination, handles old-style and SGR mouse sequences, and unwraps bracketed paste into a dedicatedpasteevent. This prevents partial sequences (a mouse report split across stdin events) from being misread as keystrokes. -
keys.ts(packages/tui/src/keys.ts) exposesKeyidentifiers,parseKey, andmatchesKey, covering legacy terminal sequences and the Kitty keyboard protocol. GlobalsetKittyProtocolActivetracks protocol state. -
mouse.ts(packages/tui/src/mouse.ts) parses SGR mouse reports into structuredMouseEvents with button, wheel, modifier, and motion bits. -
TUI.handleInputinpackages/tui/src/tui.tsroutes the sequence to the focused component'shandleInput, filtering out key-release events unless the component opts in viawantsKeyRelease. Fullscreen mouse and viewport keys are handled byhandleFullscreenInput.
Keybindings are configurable through KeybindingsManager in packages/tui/src/keybindings.ts. TUI_KEYBINDINGS maps semantic actions (tui.editor.cursorLeft, tui.input.submit, tui.viewport.pageUp, and so on) to default KeyIds; downstream code replaces the global manager via setKeybindings. The manager detects conflicts where two actions claim the same key and can report them via getConflicts. The interactive mode registers a larger binding set on top of this base, so all key handling goes through the registry rather than hardcoded matchesKey checks.
Editor (packages/tui/src/components/editor.ts) is the largest component. It is a multi-line editor built on Intl.Segmenter grapheme segmentation with word-aware cursor movement, deletion, and wrapping. It uses UndoStack (packages/tui/src/undo-stack.ts, a clone-on-push snapshot stack) for undo and KillRing (packages/tui/src/kill-ring.ts, an Emacs-style ring with yank and yank-pop) for cut/paste. Paste content and image references are rendered as atomic [paste #N] and [image #N] markers so cursor movement and deletion treat them as single units. It supports autocomplete via the providers in packages/tui/src/autocomplete.ts, slash-command context from packages/tui/src/slash-command-context.ts, and emits CURSOR_MARKER so the engine places the hardware cursor for IME input.
packages/tui/src/editor-component.ts defines the EditorComponent interface that any custom editor (for example a vim-mode editor) can implement to plug into the same container layout and input plumbing while exposing text, history, autocomplete, and paste snapshot hooks.
The interactive mode in packages/coding-agent/src/modes/interactive/interactive-mode.ts drives the TUI directly. It constructs new TUI(new ProcessTerminal(), ...), builds the chat screen from many Containers (header, chat, status, prompt dock, editor, footer), registers the full keybinding set, calls ui.start(), and uses ui.enterFullscreen(...) for the transcript view with a docked editor. Custom editors (extension-editor.ts) and dialogs (login-dialog.ts) are built on the same Editor, SelectList, and Container primitives. See interactive mode for the full chat UI.
The Terminal abstraction and TerminalStopOptions (with preserveAltScreen) exist so a worker-backed session attach can stop one ProcessTerminal and hand the alternate screen and raw-mode stdin state off to the next in-process TUI without flicker or leaked input.
- Render behavior, diffing strategy, overlays, focus:
packages/tui/src/tui.ts. - Terminal session setup, protocol negotiation, input buffering wiring:
packages/tui/src/terminal.ts. - Full-screen transcript window and dock:
packages/tui/src/fullscreen.ts. - Keyboard mapping:
packages/tui/src/keybindings.ts(defaults inTUI_KEYBINDINGS, registry viasetKeybindings). - Editor behavior:
packages/tui/src/components/editor.tsplusundo-stack.ts,kill-ring.ts,autocomplete.ts. - New components: add a file in
packages/tui/src/components/, export it frompackages/tui/src/index.ts, and follow theComponentrender contract.
| File | Role |
|---|---|
packages/tui/src/tui.ts |
Engine: render loop, diffing, overlays, focus, fullscreen |
packages/tui/src/terminal.ts |
Terminal abstraction and ProcessTerminal implementation |
packages/tui/src/render-cache.ts |
Width/version render cache |
packages/tui/src/fullscreen.ts |
Alternate-screen transcript viewport |
packages/tui/src/keys.ts |
Key parsing and matching |
packages/tui/src/keybindings.ts |
Configurable keybinding registry |
packages/tui/src/stdin-buffer.ts |
Batched stdin sequence splitting |
packages/tui/src/components/editor.ts |
Multi-line editor with undo, kill ring, autocomplete |
packages/tui/src/components/markdown.ts |
Markdown renderer |
packages/tui/src/components/select-list.ts |
Selection list / dropdown |
packages/tui/src/editor-component.ts |
Custom editor interface |
packages/tui/src/terminal-image.ts |
Terminal image encoding and capabilities |
- Interactive mode, the terminal chat UI driving the TUI
- CLI, CLI entry points that start interactive mode
- Architecture, system overview
- Fun facts, Doom in the terminal and the butterfly logo