# TUI runtime internals This document maps the non-theme runtime path from terminal input to rendered output in interactive mode. It focuses on behavior in `packages/tui` and its integration from `packages/coding-agent` controllers. ## Runtime layers and ownership - **`packages/tui` engine**: terminal lifecycle, stdin normalization, focus routing, render scheduling, differential painting, overlay composition, hardware cursor placement. - **`packages/coding-agent` interactive mode**: builds component tree, binds editor callbacks and keymaps, reacts to agent/session events, and translates domain state (streaming, tool execution, retries, plan mode) into UI components. Boundary rule: the TUI engine is message-agnostic. It only knows `Component.render(width)`, `handleInput(data)`, focus, and overlays. Agent semantics stay in interactive controllers. ## Implementation files - [`../src/modes/interactive-mode.ts`](../packages/coding-agent/src/modes/interactive-mode.ts) - [`../src/modes/controllers/event-controller.ts`](../packages/coding-agent/src/modes/controllers/event-controller.ts) - [`../src/modes/controllers/input-controller.ts`](../packages/coding-agent/src/modes/controllers/input-controller.ts) - [`../src/modes/components/custom-editor.ts`](../packages/coding-agent/src/modes/components/custom-editor.ts) - [`../../tui/src/tui.ts`](../packages/tui/src/tui.ts) - [`../../tui/src/terminal.ts`](../packages/tui/src/terminal.ts) - [`../../tui/src/editor-component.ts`](../packages/tui/src/editor-component.ts) - [`../../tui/src/stdin-buffer.ts`](../packages/tui/src/stdin-buffer.ts) - [`../../tui/src/components/loader.ts`](../packages/tui/src/components/loader.ts) ## Boot and component tree assembly `InteractiveMode` constructs `TUI(new ProcessTerminal(), settings.get("showHardwareCursor"))`, applies `settings.get("clearOnShrink")`, and creates persistent containers: - `chatContainer` - `pendingMessagesContainer` - `statusContainer` - `todoContainer` - `btwContainer` - `statusLine` - `hookWidgetContainerAbove` - `editorContainer` (holds `CustomEditor`) - `hookWidgetContainerBelow` `init()` wires the tree in that order, focuses the editor, registers input handlers via `InputController`, subscribes terminal appearance changes into theme auto-detection, starts TUI, and requests a forced render. A forced render (`requestRender(true)`) resets previous-line caches and cursor bookkeeping before repainting. ## Terminal lifecycle and stdin normalization `ProcessTerminal.start()`: 1. Enables raw mode and bracketed paste. 2. Attaches resize handler. 3. Creates a `StdinBuffer` to split partial escape chunks into complete sequences. 4. Queries Kitty keyboard protocol support (`CSI ? u`), then enables protocol flags if supported; otherwise enables modifyOtherKeys fallback after a short timeout. 5. Queries OSC 11 background color and enables Mode 2031 appearance notifications for dark/light theme detection. 6. On Windows, attempts VT input enablement via `kernel32` mode flags. `StdinBuffer` behavior: - Buffers fragmented escape sequences (CSI/OSC/DCS/APC/SS3). - Emits `data` only when a sequence is complete or timeout-flushed. - Detects bracketed paste and emits a `paste` event with raw pasted text. This prevents partial escape chunks from being misinterpreted as normal keypresses. ## Input routing and focus model Input path: `stdin -> ProcessTerminal -> StdinBuffer -> TUI.#handleInput -> focusedComponent.handleInput` Routing details: 1. TUI runs registered input listeners first (`addInputListener`), allowing consume/transform behavior. 2. TUI handles global debug shortcut (`shift+ctrl+d`) before component dispatch. 3. If focused component belongs to an overlay that is now hidden/invisible, TUI reassigns focus to next visible overlay or saved pre-overlay focus. 4. Key release events are filtered unless focused component sets `wantsKeyRelease = true`. 5. After dispatch, TUI schedules render. `setFocus()` also toggles `Focusable.focused`, which controls whether components emit `CURSOR_MARKER` for hardware cursor placement. ## Key handling split: editor vs controller `CustomEditor` intercepts high-priority combos first (escape, ctrl-c/d/z, ctrl-v, ctrl-p variants, ctrl-t, alt-up, extension custom keys) and delegates the rest to base `Editor` behavior (text editing, history, autocomplete, cursor movement). `InputController.setupKeyHandlers()` then binds editor callbacks to mode actions: - cancellation / mode exits on `Escape` - shutdown on double `Ctrl+C` or empty-editor `Ctrl+D` - suspend/resume on `Ctrl+Z` - slash-command and selector hotkeys - follow-up/dequeue toggles and expansion toggles This keeps key parsing/editor mechanics in `packages/tui` and mode semantics in coding-agent controllers. ## Render loop and diffing strategy `TUI.requestRender()` is debounced to one render per tick using `process.nextTick`. Multiple state changes in the same turn coalesce. `#doRender()` pipeline: 1. Render root component tree to `newLines`. 2. Composite visible overlays (if any). 3. Extract and strip `CURSOR_MARKER` from visible viewport lines. 4. Append segment reset suffixes for non-image lines. 5. Choose full repaint vs differential patch: - first frame - width change - shrink with `clearOnShrink` enabled and no overlays - edits above previous viewport 6. For differential updates, patch only changed line range and clear stale trailing lines when needed. 7. Reposition hardware cursor for IME support. Render writes use synchronized output mode (`CSI ? 2026 h/l`) to reduce flicker/tearing. ## Render safety constraints Critical safety checks in `TUI`: - Non-image rendered lines are expected to fit terminal width; the differential path truncates overwide lines as a last-resort guard and can write debug diagnostics when redraw debugging is enabled. - Overlay compositing includes defensive truncation and post-composite width guarding. - Width changes force full redraw because wrapping semantics change. - Cursor position is clamped before movement. These constraints are runtime guards plus component conventions; renderers should still return width-safe lines rather than rely on truncation. ## Resize handling Resize events are event-driven from `ProcessTerminal` to `TUI.requestRender()`. Effects: - Width changes trigger full redraw. - Height changes trigger full redraw except in Termux and terminal multiplexers, where the renderer avoids scrollback-hostile full replays. - Viewport/top tracking (`#previousViewportTop`, `#maxLinesRendered`) avoids invalid relative cursor math when content or terminal size changes. - Overlay visibility can depend on terminal dimensions (`OverlayOptions.visible`); focus is corrected when overlays become non-visible after resize. ## Streaming and incremental UI updates `EventController` subscribes to `AgentSessionEvent` and updates UI incrementally: - `agent_start`: starts loader in `statusContainer`. - `message_start` assistant: creates `streamingComponent` and mounts it. - `message_update`: updates streaming assistant content; creates/updates tool execution components as tool calls appear. - `tool_execution_update/end`: updates tool result components and completion state. - `message_end`: finalizes assistant stream, handles aborted/error annotations, marks pending tool args complete on normal stop. - `agent_end`: stops loaders, clears transient stream state, flushes deferred model switch, issues completion notification if backgrounded. Read-tool grouping is intentionally stateful (`#lastReadGroup`) to coalesce consecutive read tool calls into one visual block until a non-read break occurs. ## Status and loader orchestration Status lane ownership: - `statusContainer` holds transient loaders (`loadingAnimation`, `autoCompactionLoader`, `retryLoader`). - `statusLine` renders persistent status/hooks/plan indicators and drives editor top border updates. Loader behavior: - `Loader` updates every 80ms via interval and requests render each frame. - Escape handlers are temporarily overridden during auto-compaction and auto-retry to cancel those operations. - On end/cancel paths, controllers restore prior escape handlers and stop/clear loader components. ## Mode transitions and backgrounding ### Bash/Python input modes Input text prefixes toggle editor border mode flags: - `!` -> bash mode - `$` (non-template literal prefix) -> python mode Escape exits inactive mode by clearing editor text and restoring border color; when execution is active, escape aborts the running task instead. ### Plan mode `InteractiveMode` tracks plan mode flags, status-line state, active tools, and model switching. Enter/exit updates session mode entries and status/UI state, including deferred model switch if streaming is active. ### Suspend/resume (`Ctrl+Z`) `InputController.handleCtrlZ()`: 1. Registers one-shot `SIGCONT` handler to restart TUI and force render. 2. Stops TUI before suspend. 3. Sends `SIGTSTP` to process group. ### Background mode (`/background` or `/bg`) `handleBackgroundCommand()`: - Rejects when idle. - Switches tool UI context to non-interactive (`hasUI=false`) so interactive UI tools fail fast. - Stops loaders/status line and unsubscribes foreground event handler. - Subscribes background event handler (primarily waits for `agent_end`). - Stops TUI and sends `SIGTSTP` (POSIX job control path). On `agent_end` in background with no queued work, controller sends completion notification and shuts down. ## Cancellation paths Primary cancellation inputs: - `Escape` during active stream loader: restores queued messages to editor and aborts agent. - `Escape` during bash/python execution: aborts running command. - `Escape` during auto-compaction/retry: invokes dedicated abort methods through temporary escape handlers. - `Ctrl+C` single press: clear editor; double press within 500ms: shutdown. Cancellation is state-conditional; same key can mean abort, mode-exit, selector trigger, or no-op depending on runtime state. ## Event-driven vs throttled behavior Event-driven updates: - Agent session events (`EventController`) - Key input callbacks (`InputController`) - terminal resize callback - terminal appearance callbacks, SIGWINCH theme reevaluation, and git branch watchers in `InteractiveMode` Throttled/debounced paths: - TUI rendering is tick-debounced (`requestRender` coalescing). - Loader animation is fixed-interval (80ms), each frame requesting render. - Editor autocomplete updates (inside `Editor`) use debounce timers, reducing recompute churn during typing. The runtime therefore mixes event-driven state transitions with bounded render cadence to keep interactivity responsive without repaint storms.