Last updated: 2026-08-03
Branch: refactor/solid-architecture
CanvasTerminal is the sole terminal renderer. xterm.js has been fully removed. The renderer is powered by alacritty_terminal (Rust) sending binary grid frames over a Tauri Channel.
Terminal.tsx (outer shell)
+-- Session lifecycle (create/resume/reconnect PTY)
+-- Parsed event handling (status-line, question, progress, etc.)
+-- Activity tracking, notifications, auto-retry
+-- OSC 0/2 title change handling
+-- Resume/reconnect banners (JSX)
+-- ComposePanel
+-- TerminalSearch
+-- TerminalRef registration with terminalsStore
|
+-- CanvasTerminal (sole renderer)
+-- subscribe_terminal_grid (binary frame push from Rust via Channel/WS)
+-- Base canvas: text cells, backgrounds, block/box-drawing chars
+-- Overlay canvas (pointer-events:none): cursor, selection, search highlights, gutter markers
+-- Custom scrollbar (drag + track click)
+-- Suggest/intent overlay (DOM divs over canvas)
+-- Link detection (hover: file paths, web URLs, OSC 8)
+-- Keyboard input (VT100 + Kitty protocol)
+-- Touch input (tap/swipe/pinch for mobile/tablet via offscreen textarea)
+-- IntersectionObserver flow control (skip paint when hidden)
+-- Plugin raw output forwarding (pluginRegistry.processRawOutput)
+-- OSC 7 CWD + OSC 133 shell integration
+-- Imperative controllers (no reactive frame-path state)
+-- selection + search
+-- cancellable link verification + caches
+-- smooth-scroll position + styled-row cache
+-- keyboard/IME/mouse listener lifecycle
Frame decode, row reconciliation, scheduling, and paint remain colocated in
CanvasTerminal. The extracted controllers own independent state and cleanup;
they do not add Solid signals, effects, or store writes to the render hot path.
Key insight: Terminal.tsx handles parsed events, session lifecycle, banners, and compose panel. CanvasTerminal is purely a renderer + input handler with no session logic.
Each frame: 26-byte header + variable row data. The header ends with a historyBase: u32 (lines evicted from the history top so far); historyBase + (historySize - displayOffset + screenRow) is the eviction-stable absolute index the smooth-scroll row cache keys by, so a cached row never aliases onto a different line after the scrollback cap rotates. keyboard_flags bits 0–4 remain the public keyboard-mode mask; bit 5 carries the active primary/alternate-screen identity and is removed before exposing keyboardFlags to input code. Per cell: 4 bytes codepoint + 3 bytes fg RGB + 3 bytes bg RGB + 1 byte attrs bitmask = 11 bytes. Decoded in decodeBinaryFrame using struct-of-arrays (SoA) typed arrays — zero per-cell object allocation.
Primary and alternate grids can reuse identical numeric row coordinates while representing unrelated content. A bit-5 transition therefore starts a new renderer generation: smooth-scroll animation, delayed row fetches, selection, search, link verification, reconciliation, and absolute-row caches are invalidated as one transaction. Partial transition frames wait for a full replacement instead of merging into the previous grid.
RAF coalescing: All paint triggers (frame arrival, keydown selection clear, mousedown) go through scheduleRepaint() which schedules a single requestAnimationFrame. No synchronous paint calls — prevents double-paint in a single event loop turn.
send_grid_frame clone guard: Frame is only cloned for the grid_watch channel when receiver_count() > 0 (i.e. WS clients connected). Desktop-only path (Tauri Channel) is zero-copy.
screen_text_rows_ref():TerminalGrid exposes a borrowed &[String] view of cached screen rows. Used in process_chunk for chrome cutoff detection to avoid cloning 50 Strings per PTY chunk. Downstream parsers (slash-menu, choice-prompt) share a single owned snapshot computed once per chunk.
No per-chunk parser logs: Slash-menu detection can remain active during a large output burst, so its hot path emits events only when a menu is found and never writes a debug record for every parse.
Trim in-place:read_screen_text() and row_to_text() use String::truncate() instead of .trim_end().to_string(), eliminating one allocation per row.