mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-17 09:02:08 +00:00
- Create Terminal interface abstracting stdin/stdout operations for dependency injection - Implement ProcessTerminal for production use with process.stdin/stdout - Implement VirtualTerminal using @xterm/headless for accurate terminal emulation in tests - Fix TypeScript imports for @xterm/headless module - Move all component files to src/components/ directory for better organization - Add comprehensive test suite with async/await patterns for proper render timing - Fix critical TUI differential rendering bug when components grow in height - Issue: Old content wasn't properly cleared when component line count increased - Solution: Clear each old line individually before redrawing, ensure cursor at line start - Add test verifying terminal content preservation and text editor growth behavior - Update tsconfig.json to include test files in type checking - Add benchmark test comparing single vs double buffer performance The implementation successfully reduces flicker by only updating changed lines rather than clearing entire sections. Both TUI implementations maintain the same interface for backward compatibility.
4.3 KiB
4.3 KiB
TUI Double Buffer Implementation Analysis
Current Architecture
Core TUI Rendering System
- Location:
/Users/badlogic/workspaces/pi-mono/packages/tui/src/tui.ts - render() method (lines 107-150): Traverses components, calculates keepLines
- renderToScreen() method (lines 354-429): Outputs to terminal with differential rendering
- Terminal output: Single
writeSync()call at line 422
Component Interface
interface ComponentRenderResult {
lines: string[];
changed: boolean;
}
interface ContainerRenderResult extends ComponentRenderResult {
keepLines: number; // Lines from top that are unchanged
}
The Flicker Problem
Root Cause:
- LoadingAnimation (
packages/agent/src/renderers/tui-renderer.ts) updates every 80ms - Calls
ui.requestRender()on each frame, marking itself as changed - Container's
keepLineslogic stops accumulating once any child changes - All components below animation must re-render completely
- TextEditor always returns
changed: truefor cursor updates
Current Differential Rendering:
- Moves cursor up by
(totalLines - keepLines)lines - Clears everything from cursor down with
\x1b[0J - Writes all lines after
keepLinesposition - Creates visible flicker when large portions re-render
Performance Bottlenecks
-
TextEditor (
packages/tui/src/text-editor.ts):- Always returns
changed: true(lines 122-125) - Complex
layoutText()recalculates wrapping every render - Heavy computation for cursor positioning and highlighting
- Always returns
-
Animation Cascade Effect:
- Single animated component forces all components below to re-render
- Container stops accumulating
keepLinesafter first change - No isolation between independent component updates
-
Terminal I/O:
- Single large
writeSync()call for all changing content - Clears and redraws entire sections even for minor changes
- Single large
Existing Optimizations
Component Caching:
- TextComponent: Stores
lastRenderedLines[], compares arrays - MarkdownComponent: Uses
previousLines[]comparison - WhitespaceComponent:
firstRenderflag - Components properly detect and report changes
Render Batching:
requestRender()usesprocess.nextTick()to batch updates- Prevents multiple renders in same tick
Double Buffer Solution
Architecture Benefits
- Components already return
{lines, changed}- no interface changes needed - Clean separation between rendering (back buffer) and output (terminal)
- Single
writeSync()location makes implementation straightforward - Existing component caching remains useful
Implementation Strategy
TuiDoubleBuffer Class:
- Extend current TUI class
- Maintain front buffer (last rendered lines) and back buffer (new render)
- Override
renderToScreen()with line-by-line diffing algorithm - Batch consecutive changed lines to minimize writeSync() calls
- Position cursor only at changed lines, not entire sections
Line-Level Diffing Algorithm:
// Pseudocode
for (let i = 0; i < maxLines; i++) {
if (frontBuffer[i] !== backBuffer[i]) {
// Position cursor at line i
// Clear line
// Write new content
// Or batch with adjacent changes
}
}
Expected Benefits
-
Reduced Flicker:
- Only changed lines are redrawn
- Animation updates don't affect static content below
- TextEditor cursor updates don't require full redraw
-
Better Performance:
- Fewer terminal control sequences
- Smaller writeSync() payloads
- Components can cache aggressively
-
Preserved Functionality:
- No changes to existing components
- Backward compatible with current TUI class
- Can switch between single/double buffer modes
Test Plan
Create comparison tests:
packages/tui/test/single-buffer.ts- Current implementationpackages/tui/test/double-buffer.ts- New implementation- Both with LoadingAnimation above TextEditor
- Measure render() timing and visual flicker
Files to Modify
New Files:
packages/tui/src/tui-double-buffer.ts- New TuiDoubleBuffer class
Test Files:
packages/tui/test/single-buffer.ts- Test current implementationpackages/tui/test/double-buffer.ts- Test new implementation
No Changes Needed:
- Component implementations (already support caching and change detection)
- Component interfaces (already return required data)