mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-20 09:01:49 +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.
132 lines
No EOL
4.3 KiB
Markdown
132 lines
No EOL
4.3 KiB
Markdown
# 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
|
|
```typescript
|
|
interface ComponentRenderResult {
|
|
lines: string[];
|
|
changed: boolean;
|
|
}
|
|
|
|
interface ContainerRenderResult extends ComponentRenderResult {
|
|
keepLines: number; // Lines from top that are unchanged
|
|
}
|
|
```
|
|
|
|
### The Flicker Problem
|
|
|
|
**Root Cause:**
|
|
1. LoadingAnimation (`packages/agent/src/renderers/tui-renderer.ts`) updates every 80ms
|
|
2. Calls `ui.requestRender()` on each frame, marking itself as changed
|
|
3. Container's `keepLines` logic stops accumulating once any child changes
|
|
4. All components below animation must re-render completely
|
|
5. TextEditor always returns `changed: true` for cursor updates
|
|
|
|
**Current Differential Rendering:**
|
|
- Moves cursor up by `(totalLines - keepLines)` lines
|
|
- Clears everything from cursor down with `\x1b[0J`
|
|
- Writes all lines after `keepLines` position
|
|
- Creates visible flicker when large portions re-render
|
|
|
|
### Performance Bottlenecks
|
|
|
|
1. **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
|
|
|
|
2. **Animation Cascade Effect:**
|
|
- Single animated component forces all components below to re-render
|
|
- Container stops accumulating `keepLines` after first change
|
|
- No isolation between independent component updates
|
|
|
|
3. **Terminal I/O:**
|
|
- Single large `writeSync()` call for all changing content
|
|
- Clears and redraws entire sections even for minor changes
|
|
|
|
### Existing Optimizations
|
|
|
|
**Component Caching:**
|
|
- TextComponent: Stores `lastRenderedLines[]`, compares arrays
|
|
- MarkdownComponent: Uses `previousLines[]` comparison
|
|
- WhitespaceComponent: `firstRender` flag
|
|
- Components properly detect and report changes
|
|
|
|
**Render Batching:**
|
|
- `requestRender()` uses `process.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:**
|
|
1. Extend current TUI class
|
|
2. Maintain front buffer (last rendered lines) and back buffer (new render)
|
|
3. Override `renderToScreen()` with line-by-line diffing algorithm
|
|
4. Batch consecutive changed lines to minimize writeSync() calls
|
|
5. Position cursor only at changed lines, not entire sections
|
|
|
|
**Line-Level Diffing Algorithm:**
|
|
```typescript
|
|
// 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
|
|
|
|
1. **Reduced Flicker:**
|
|
- Only changed lines are redrawn
|
|
- Animation updates don't affect static content below
|
|
- TextEditor cursor updates don't require full redraw
|
|
|
|
2. **Better Performance:**
|
|
- Fewer terminal control sequences
|
|
- Smaller writeSync() payloads
|
|
- Components can cache aggressively
|
|
|
|
3. **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:
|
|
1. `packages/tui/test/single-buffer.ts` - Current implementation
|
|
2. `packages/tui/test/double-buffer.ts` - New implementation
|
|
3. Both with LoadingAnimation above TextEditor
|
|
4. 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 implementation
|
|
- `packages/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) |