Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.monolex.ai/llms.txt

Use this file to discover all available pages before exploring further.

Synchronous Rendering

MonoTerm uses synchronous rendering to eliminate the 1-frame flicker that occurs with xterm.js’s default async rendering path.

The Problem: RenderDebouncer Delay

xterm.js uses a RenderDebouncer that schedules all rendering for the next requestAnimationFrame. This creates a 1-frame delay between buffer update and screen display.
╔═══════════════════════════════════════════════════════════════════════════════╗
║  THE ASYNC RENDERING PROBLEM                                                  ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║                                                                               ║
║  Timeline (16.67ms per frame @ 60fps)                                         ║
║  ─────────────────────────────────────                                        ║
║                                                                               ║
║  Frame N                              Frame N+1                               ║
║  ├──────────────────────────────────┼──────────────────────────────────┤      ║
║  0ms                              16.67ms                           33.33ms   ║
║                                                                               ║
║                                                                               ║
║  T=0ms: GridUpdate arrives                                                    ║
║    │                                                                          ║
║    ▼                                                                          ║
║  ┌─────────────────────────┐                                                  ║
║  │ Buffer Injection        │  ← New data written to buffer                    ║
║  │ (cells written)         │                                                  ║
║  └───────────┬─────────────┘                                                  ║
║              │                                                                ║
║              ▼                                                                ║
║  ┌─────────────────────────┐                                                  ║
║  │ term.refresh()          │                                                  ║
║  │   ↓                     │                                                  ║
║  │ RenderDebouncer         │  ← "I'll render on next RAF"                     ║
║  │   ↓                     │                                                  ║
║  │ requestAnimationFrame() │  ← Schedules and returns immediately             ║
║  └───────────┬─────────────┘                                                  ║
║              │                                                                ║
║              │  ┌─────────────────────────────────────┐                       ║
║              │  │                                     │                       ║
║              │  │  What user sees:                    │                       ║
║              │  │                                     │                       ║
║              │  │  ┌───────────────────────────┐     │                       ║
║              │  │  │ OLD DATA (previous frame) │     │                       ║
║              │  │  │                           │     │  FLICKER!              ║
║              │  │  │ Buffer=NEW, GPU=OLD       │     │  Buffer/screen mismatch║
║              │  │  └───────────────────────────┘     │                       ║
║              │  │                                     │                       ║
║              │  └─────────────────────────────────────┘                       ║
║              │                                                                ║
║              │ ─ ─ ─ ─ ─ ─ ─ ─ ─(16.67ms wait)─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ▶  ║
║              │                                        │                       ║
║              │                              Frame N+1 │                       ║
║              │                                        ▼                       ║
║              │                              ┌─────────────────────────┐       ║
║              └─────────────────────────────▶│ RAF callback fires      │       ║
║                                             │   ↓                     │       ║
║                                             │ _renderRows() executes  │       ║
║                                             │   ↓                     │       ║
║                                             │ WebGL Render            │       ║
║                                             └───────────┬─────────────┘       ║
║                                                         │                     ║
║                                                         ▼                     ║
║                                             ┌─────────────────────────┐       ║
║                                             │ Finally shows NEW DATA  │       ║
║                                             └─────────────────────────┘       ║
║                                                                               ║
║  Total delay: ~16.67ms (1 frame)                                              ║
║                                                                               ║
╚═══════════════════════════════════════════════════════════════════════════════╝

The Solution: Direct _renderRows() Call

MonoTerm bypasses the RenderDebouncer by calling _renderRows() directly, achieving immediate rendering.
╔═══════════════════════════════════════════════════════════════════════════════╗
║  SYNC RENDERING - IMMEDIATE DISPLAY                                           ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║                                                                               ║
║  Frame N                              Frame N+1                               ║
║  ├──────────────────────────────────┼──────────────────────────────────┤      ║
║                                                                               ║
║  T=0ms: GridUpdate arrives                                                    ║
║    │                                                                          ║
║    ▼                                                                          ║
║  ┌─────────────────────────┐                                                  ║
║  │ Buffer Injection        │  ← New data written to buffer                    ║
║  │ (cells written)         │                                                  ║
║  └───────────┬─────────────┘                                                  ║
║              │                                                                ║
║              ▼                                                                ║
║  ┌─────────────────────────┐                                                  ║
║  │ _renderRows() direct    │  ← RenderDebouncer BYPASSED                      ║
║  │   ↓                     │                                                  ║
║  │ WebGL Render (immediate)│  ← Executes in same JS tick                      ║
║  └───────────┬─────────────┘                                                  ║
║              │                                                                ║
║              ▼                                                                ║
║  ┌─────────────────────────────────────┐                                      ║
║  │                                     │                                      ║
║  │  What user sees:                    │                                      ║
║  │                                     │                                      ║
║  │  ┌───────────────────────────┐     │                                      ║
║  │  │ NEW DATA (immediately!)   │     │  NO FLICKER!                         ║
║  │  │                           │     │                                      ║
║  │  │ Buffer = Screen = Synced  │     │  Perfect match                       ║
║  │  └───────────────────────────┘     │                                      ║
║  │                                     │                                      ║
║  └─────────────────────────────────────┘                                      ║
║                                                                               ║
║  Total delay: ~0ms (immediate)                                                ║
║                                                                               ║
╚═══════════════════════════════════════════════════════════════════════════════╝

Code-Level Difference

Async Path (xterm.js Public API)

// Public API - schedules for next frame
term.refresh(0, rows - 1);


RenderService.refresh()


RenderDebouncer.refresh()


this._animationFrame = requestAnimationFrame(() => {
    this._innerRefresh();  // ← Executes on NEXT frame
});

Sync Path (MonoTerm Direct Call)

MonoTerm accesses xterm.js's internal render service directly,
bypassing the RenderDebouncer to execute rendering in the same
JS tick as the buffer update. Falls back to async refresh()
if the internal API is unavailable.

Why This Matters for AI CLI

AI tools produce rapid output streams. With async rendering, every update creates a visible delay.
╔═══════════════════════════════════════════════════════════════════════════════╗
║  AI CLI OUTPUT SCENARIO                                                       ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║                                                                               ║
║  T=0ms    GridUpdate #1 arrives                                               ║
║  T=2ms    GridUpdate #2 arrives                                               ║
║  T=5ms    GridUpdate #3 arrives                                               ║
║  ...                                                                          ║
║  T=16ms   Frame N+1 starts                                                    ║
║                                                                               ║
║                                                                               ║
║  ASYNC:                              SYNC:                                    ║
║  ──────                              ─────                                    ║
║                                                                               ║
║  #1 buffer update → screen OLD       #1 buffer update → screen NEW            ║
║  #2 buffer update → screen OLD       #2 buffer update → screen NEW            ║
║  #3 buffer update → screen OLD       #3 buffer update → screen NEW            ║
║  ...                                 ...                                      ║
║  Frame N+1: finally shows #3         (already displayed)                      ║
║                                                                               ║
║                                                                               ║
║  User Experience:                    User Experience:                         ║
║  ────────────────                    ────────────────                         ║
║  Screen "lags behind"                Real-time response                       ║
║  Visible flicker on fast output      Always smooth                            ║
║                                                                               ║
╚═══════════════════════════════════════════════════════════════════════════════╝

Implementation Sites

MonoTerm applies synchronous rendering at 15 critical points across 5 files:
FileSitesPurpose
atomic-cell-injector.ts6Grid injection render
terminal-instance.ts2Terminal initialization
terminal-scrollbar.ts3Scroll position updates
terminal-renderer-selector.ts1Renderer switching
fonts-terminal.ts3Font change refresh

Why Debouncer Exists (And Why We Bypass It)

The RenderDebouncer serves a purpose in traditional terminals:
╔═══════════════════════════════════════════════════════════════════════════════╗
║  DEBOUNCER PURPOSE vs MONOTERM REALITY                                        ║
╠═══════════════════════════════════════════════════════════════════════════════╣
║                                                                               ║
║  Why xterm.js uses RenderDebouncer:                                           ║
║  ──────────────────────────────────                                           ║
║                                                                               ║
║  1. Coalesce rapid updates into single render                                 ║
║  2. Prevent excessive GPU calls                                               ║
║  3. Sync with browser's natural 60fps refresh                                 ║
║                                                                               ║
║                                                                               ║
║  Why MonoTerm can bypass it:                                                  ║
║  ───────────────────────────                                                  ║
║                                                                               ║
║  1. Rust AtomicState already calculates diff (Full/Partial/None/Skip)         ║
║     → Redundant to debounce what's already optimized                          ║
║                                                                               ║
║  2. ACK flow control limits update rate                                       ║
║     → Backend won't flood frontend                                            ║
║                                                                               ║
║  3. BSU/ESU protocol batches atomic frames                                    ║
║     → Updates arrive pre-batched                                              ║
║                                                                               ║
║  4. Frontend RAF already caps at 60fps                                        ║
║     → Natural rate limiting exists                                            ║
║                                                                               ║
║                                                                               ║
║  Result: Debouncer is REDUNDANT in Atomic Architecture                        ║
║          Bypassing it gives immediate response with no downside               ║
║                                                                               ║
╚═══════════════════════════════════════════════════════════════════════════════╝

Comparison Summary

AspectAsync (refresh)Sync (_renderRows)
Delay1 frame (~16.67ms)0ms (immediate)
FlickerPossibleEliminated
APIPublicPrivate (internal)
Use CaseTraditional terminalsAI-native terminals

Diff Rendering

How AtomicState calculates minimal updates

ACK Flow Control

Backend rate limiting that enables sync render

BSU/ESU Protocol

Atomic frame batching from PTY

Atomic Loop

The complete rendering cycle