VibeTunnel Architecture Analysis - Mario’s Technical Deep Dive

This document contains comprehensive technical insights from Mario’s debugging session about VibeTunnel’s architecture, critical performance issues, and detailed solutions.

Executive Summary

Mario identified two critical issues causing performance problems in VibeTunnel:
  1. 850MB Session Bug: External terminal sessions (via fwd.ts) bypass the clear sequence truncation in stream-watcher.ts, sending entire gigabyte files instead of the last 2MB
  2. Resize Loop: Claude terminal app issues full clear sequence (\x1b[2J) and re-renders entire scroll buffer on every resize event, creating exponential data growth
Note: A third issue with Node-PTY’s shared pipe architecture causing Electron crashes has already been resolved with a custom PTY implementation.

System Architecture

Core Components

┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│   Client    │────▶│  Web Server  │────▶│ PTY Process │
└─────────────┘     └──────────────┘     └─────────────┘
      │                    │                     │
      │                    ▼                     ▼
      │              ┌──────────┐         ┌──────────┐
      └─────────────▶│ Terminal │         │ Ascinema │
                     │ Manager  │         │  Files   │
                     └──────────┘         └──────────┘

Detailed Sequence Flow

Key Files and Their Roles

FilePurposeCritical Functions
server.tsMain web serverHTTP endpoints, WebSocket handling
pty-manager.tsPTY lifecycle managementcreateSession(), setupPtyHandlers()
stream-watcher.tsMonitors ascinema filessendExistingContent() - implements clear truncation
fwd.tsExternal terminal forwardingProcess spawning, BYPASSES TRUNCATION
terminal-manager.tsBinary buffer renderingConverts ANSI to binary cells format

Data Flow Paths

Input Path (Keystroke → Terminal)

  1. Browser captures key press
  2. WebSocket sends to /api/sessions/:id/input
  3. Server writes to IPC socket
  4. PTY Manager writes to process stdin
  5. Process executes command

Output Path (Terminal → Browser)

  1. Process writes to stdout
  2. PTY Manager captures via onData handler
  3. Writes to ascinema file (with write queue for backpressure)
  4. Stream watcher monitors file changes
  5. For existing content: scans for last clear sequence
  6. Client receives via:
    • SSE: /api/sessions/:id/stream (text/ascinema format)
    • WebSocket: /buffers (binary cell format)

Binary Cell Buffer Format

The terminal manager pre-renders terminal output into a binary format for efficient transmission:
For each cell at (row, column):
- Character (UTF-8 encoded)
- Foreground color (RGB values)
- Background color (RGB values)
- Attributes (bold, italic, underline, etc.)
Benefits:
  • Server-side ANSI parsing eliminates client CPU usage
  • Efficient binary transmission reduces bandwidth
  • Only last 10,000 lines kept in memory
  • Client simply renders pre-computed cells

Critical Bugs Analysis

1. The 850MB Session Loading Bug

Symptom: Sessions with large output (850MB+) cause infinite loading and browser unresponsiveness. Root Cause: External terminal sessions via fwd.ts bypass the clear sequence truncation logic. Technical Details:
// In stream-watcher.ts - WORKING CORRECTLY
sendExistingContent() {
  // Scans backwards for last clear sequence
  const lastClear = content.lastIndexOf('\x1b[2J');
  // Sends only content after clear
  return content.slice(lastClear); // 2MB instead of 850MB
}
Evidence from Testing:
  • Test file: 980MB containing 2,400 clear sequences
  • Server-created sessions: Correctly send only last 2MB
  • External terminal sessions: Send entire 980MB file
  • Processing time: 2-3 seconds to scan 1GB file
  • Client receives instant replay for 2MB truncated content
The Issue: External terminal path doesn’t trigger sendExistingContent(), sending gigabyte files to clients.

2. Resize Event Performance Catastrophe

Problem: Each resize event causes Claude to re-render the entire terminal history. Claude’s Behavior:
1. Resize event received
2. Claude issues clear sequence: \x1b[2J
3. Re-renders ENTIRE scroll buffer from line 1
4. Rendering causes viewport changes
5. Viewport changes trigger resize event
6. GOTO step 1 (infinite loop)
Technical Evidence:
  • In 850MB session: each resize → full buffer re-render
  • Claude renders from “Welcome to Claude” message every time
  • Mobile UI particularly problematic (frequent resize events)
  • Header button position shifts during rendering indicate viewport instability
  • Session with 39 resize events can generate 850MB+ files
Contributing Factors:
  • React Ink (TUI framework) unnecessarily re-renders entire components
  • Session-detail-view has buggy resize observer
  • Mobile Safari behaves differently than desktop at same viewport size
  • Touch events vs mouse events complicate scrolling behavior

3. Node-PTY Architecture Flaw (ALREADY FIXED)

This issue has been resolved by implementing a custom PTY solution without the shared pipe architecture.

Ascinema Format Details

VibeTunnel uses the ascinema format for recording terminal sessions:
// Format: [timestamp, event_type, data]
[1.234, "o", "Hello World\n"]     // Output event
[1.235, "i", "k"]                 // Input event (keypress)
[1.236, "r", "80x24"]             // Resize event
Clear sequence detection:
const CLEAR_SEQUENCE = '\x1b[2J';  // ANSI clear screen
const CLEAR_WITH_HOME = '\x1b[H\x1b[2J'; // Home + clear

function findLastClearSequence(buffer) {
    // Search from end for efficiency
    let lastClear = buffer.lastIndexOf(CLEAR_SEQUENCE);
    return lastClear === -1 ? 0 : lastClear;
}

Proposed Solutions

Priority 1: Fix External Terminal Clear Truncation (IMMEDIATE)

Problem: External terminal sessions don’t use sendExistingContent() truncation. Investigation Needed:
  1. Trace how fwd.ts connects to client streams
  2. Determine why it bypasses stream-watcher’s truncation
  3. Ensure external terminals use same code path as server sessions
  4. Test with 980MB file to verify fix
Expected Impact: Immediate fix for users experiencing infinite loading with large sessions.

Priority 2: Fix Resize Handling (INVESTIGATION)

Debugging Approach:
  1. Instrument session-detail-view with resize observer logging
  2. Identify what causes viewport expansion
  3. Implement resize event debouncing
  4. Fix mobile-specific issues:
    • Keyboard state affects scrolling
    • Touch vs mouse event handling
    • Scrollbar visibility problems
Code to Add:
// Add to session-detail-view
let resizeCount = 0;
new ResizeObserver((entries) => {
    console.log(`Resize ${++resizeCount}:`, entries[0].contentRect);
    // Debounce resize events
    clearTimeout(this.resizeTimeout);
    this.resizeTimeout = setTimeout(() => {
        this.handleResize();
    }, 100);
}).observe(this.terminalElement);

Implementation Details

Write Queue Implementation

The PTY manager implements backpressure handling:
class WriteQueue {
    constructor(writer) {
        this.queue = [];
        this.writing = false;
        this.writer = writer;
    }
    
    async write(data) {
        this.queue.push(data);
        if (!this.writing) {
            await this.flush();
        }
    }
    
    async flush() {
        this.writing = true;
        while (this.queue.length > 0) {
            const chunk = this.queue.shift();
            await this.writer.write(chunk);
        }
        this.writing = false;
    }
}

Platform-Specific Considerations

macOS:
  • Screen Recording permission required for terminal access
  • Terminal.app specific behaviors and quirks
Mobile Safari:
  • Different behavior than desktop Safari at same viewport
  • Touch events complicate scrolling
  • Keyboard state affects scroll behavior
  • Missing/hidden scrollbars
  • Viewport meta tag issues
Windows (Future):
  • ConPTY vs WinPTY support
  • Different ANSI sequence handling
  • Path normalization requirements

Performance Metrics

MetricCurrentAfter Fix
980MB session initial loadInfinite/Crash2-3 seconds
Data sent to client980MB2MB
Memory per terminal50-100MBTarget: 10MB
Clear sequence scan timeN/A~2 seconds for 1GB
Resize event stormsExponential growthDebounced

Testing and Debugging

Test Large Session Handling

# Create large session file
cd web
npm run dev

# In another terminal, create session
SESSION_ID=$(curl -X POST localhost:3000/api/sessions | jq -r .id)

# Stop server, inject large file
cp /path/to/850mb-test-file ~/.vibetunnel/sessions/$SESSION_ID/stdout

# Restart and verify truncation works
npm run dev

Debug Resize Events

// Add to any component to detect resize loops
window.addEventListener('resize', () => {
    console.count('resize');
    console.trace('Resize triggered from:');
});

Monitor Network Traffic

  • Check /api/sessions/:id/stream response size
  • Verify only sends data after last clear
  • Monitor WebSocket /buffers for binary updates

Architectural Insights

Why Current Architecture Works (When Not Bugged)

  1. Simplicity: “Es ist die todeleinfachste Variante” - It’s the simplest possible approach
  2. Efficiency: 2MB instead of 980MB transmission after clear sequence truncation
  3. Server-side rendering: Binary cell format eliminates client ANSI parsing

Community Contribution Challenges

  • High development velocity makes contribution difficult
  • “Velocity kills” - rapid changes discourage contributors
  • LitElement/Web Components unfamiliar to most developers
  • Large file sizes cause AI tools to refuse processing

Future Architecture Considerations

Go Migration Benefits:
  • Automatic test dependency tracking
  • Only runs tests that changed
  • Pre-allocated buffers minimize GC
  • Better suited for AI-assisted development
Rust Benefits:
  • 2MB static binary
  • 10MB RAM usage
  • Direct C interop for PTY code
  • No garbage collection overhead

Action Plan Summary

  1. Immediate (End of Week): Fix external terminal truncation bug
    • Debug why fwd.ts bypasses sendExistingContent()
    • Deploy fix for immediate user relief
  2. Short Term: Comprehensive resize fix
    • Debug session-detail-view triggers
    • Implement proper debouncing
    • Fix mobile-specific issues
  3. Long Term: Consider architecture migration
    • Evaluate Rust forward binary
    • Consider Go for web server
    • Maintain backwards compatibility

Key Technical Quotes

  • “Wir schicken 2MB statt 980MB” - We send 2MB instead of 980MB
  • “Die haben einen Shared Pipe, wo alle reinschreiben” - They have a shared pipe where everyone writes
  • “Es gibt keinen Grund, warum ich von da weg alles neu rendern muss” - There’s no reason to re-render everything from the beginning
  • “Das ist known good” - Referring to battle-tested implementations
This architecture analysis provides the technical foundation for fixing VibeTunnel’s critical performance issues while maintaining its elegant simplicity.