Skip to content

packages tui

Zachary BENSALEM edited this page Aug 15, 2026 · 1 revision

TUI

Active contributors: Mario Zechner, kt, Sviatoslav Abakumov

Purpose

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.

Directory layout

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

Key abstractions

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

How it works

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.

The render pipeline

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]
Loading

Differential rendering in doRender

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 previousLines vs the new lines to find firstChanged and lastChanged, then repaints only those rows, clearing each with ESC [ 2 K and 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.

Scrollback preservation and the alternate screen

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.

The terminal session

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.

Components

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 handling

Input flows from raw stdin through a series of stages:

  1. StdinBuffer in packages/tui/src/stdin-buffer.ts accumulates 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 dedicated paste event. This prevents partial sequences (a mouse report split across stdin events) from being misread as keystrokes.
  2. keys.ts (packages/tui/src/keys.ts) exposes Key identifiers, parseKey, and matchesKey, covering legacy terminal sequences and the Kitty keyboard protocol. Global setKittyProtocolActive tracks protocol state.
  3. mouse.ts (packages/tui/src/mouse.ts) parses SGR mouse reports into structured MouseEvents with button, wheel, modifier, and motion bits.
  4. TUI.handleInput in packages/tui/src/tui.ts routes the sequence to the focused component's handleInput, filtering out key-release events unless the component opts in via wantsKeyRelease. Fullscreen mouse and viewport keys are handled by handleFullscreenInput.

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.

The editor

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.

Integration points

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.

Entry points for modification

  • 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 in TUI_KEYBINDINGS, registry via setKeybindings).
  • Editor behavior: packages/tui/src/components/editor.ts plus undo-stack.ts, kill-ring.ts, autocomplete.ts.
  • New components: add a file in packages/tui/src/components/, export it from packages/tui/src/index.ts, and follow the Component render contract.

Key source files

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

Related pages

Clone this wiki locally