1dba122c创建于 1 天前历史提交

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

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() coalesces render requests and rate-limits ordinary frames:

  • forced renders (requestRender(true, ...)) reset cached frame/viewport state and run on process.nextTick
  • ordinary renders schedule through #scheduleRender() and respect TUI.#MIN_RENDER_INTERVAL_MS
  • repeated requests while a render is pending collapse into the same scheduled frame

#doRender() pipeline:

  1. Render root component tree to newLines.
  2. Composite visible overlays (if any).
  3. Extract and strip CURSOR_MARKER from the visible viewport.
  4. Normalize non-image lines and append reset/hyperlink terminators.
  5. Classify the frame into a render intent:
    • initial paint / forced viewport repaint
    • explicit session replacement or native scrollback rebuild
    • viewport repaint for width/height/offscreen mutations
    • deferred mutation/shrink when native scrollback is scrolled
    • trailing shrink
    • changed-line diff
    • noop
  6. Emit only the bytes required by the intent and commit cached frame/cursor/viewport state.

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 repaint/rebuild planning 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 repaint or rebuild because wrapping semantics change.
  • Height-only changes repaint the viewport when needed, but skip repaint in Termux and terminal multiplexers where replays are scrollback-hostile.
  • Viewport/top tracking (#viewportTopRow, #maxLinesRendered, scrollback high-water state) avoids invalid relative cursor math and defers destructive native scrollback rewrites while the user is scrolled into history.
  • 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.