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 aRenderDebouncer 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:| File | Sites | Purpose |
|---|---|---|
atomic-cell-injector.ts | 6 | Grid injection render |
terminal-instance.ts | 2 | Terminal initialization |
terminal-scrollbar.ts | 3 | Scroll position updates |
terminal-renderer-selector.ts | 1 | Renderer switching |
fonts-terminal.ts | 3 | Font 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
| Aspect | Async (refresh) | Sync (_renderRows) |
|---|---|---|
| Delay | 1 frame (~16.67ms) | 0ms (immediate) |
| Flicker | Possible | Eliminated |
| API | Public | Private (internal) |
| Use Case | Traditional terminals | AI-native terminals |
Related Documentation
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