From 0c617aed8d8698f26f0582e40df18da8e690b303 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 11 Jul 2025 08:23:47 +0200 Subject: [PATCH] Fix mobile terminal resize loop (#305) --- docs/architecture-mario.md | 365 ++++++++++++++++++ web/build-native.js | 142 +------ web/{vendored-pty => node-pty}/.gitignore | 1 - web/{vendored-pty => node-pty}/README.md | 0 web/{vendored-pty => node-pty}/binding.gyp | 4 +- .../lib/eventEmitter2.d.ts | 0 .../lib/eventEmitter2.js | 0 web/{vendored-pty => node-pty}/lib/index.d.ts | 0 web/{vendored-pty => node-pty}/lib/index.js | 0 .../lib/interfaces.d.ts | 0 .../lib/interfaces.js | 0 .../lib/terminal.d.ts | 0 .../lib/terminal.js | 0 web/{vendored-pty => node-pty}/lib/types.d.ts | 0 web/{vendored-pty => node-pty}/lib/types.js | 0 .../lib/unixTerminal.d.ts | 0 .../lib/unixTerminal.js | 50 ++- web/{vendored-pty => node-pty}/lib/utils.d.ts | 0 web/{vendored-pty => node-pty}/lib/utils.js | 0 .../lib/windowsTerminal.d.ts | 0 .../lib/windowsTerminal.js | 0 web/{vendored-pty => node-pty}/package.json | 0 web/{vendored-pty => node-pty}/pnpm-lock.yaml | 0 .../src/eventEmitter2.ts | 0 web/{vendored-pty => node-pty}/src/index.ts | 0 .../src/interfaces.ts | 0 .../src/native.d.ts | 0 .../src/terminal.ts | 0 web/{vendored-pty => node-pty}/src/types.ts | 0 .../src/unix/pty.cc | 0 .../src/unix/spawn-helper.cc | 0 .../src/unixTerminal.ts | 57 ++- web/{vendored-pty => node-pty}/src/utils.ts | 0 .../src/win/conpty.cc | 0 .../src/win/conpty.h | 0 .../src/win/conpty_console_list.cc | 0 .../src/win/path_util.cc | 0 .../src/win/path_util.h | 0 .../src/win/winpty.cc | 0 .../src/windowsTerminal.ts | 0 web/{vendored-pty => node-pty}/tsconfig.json | 0 web/package.json | 2 +- web/pnpm-lock.yaml | 18 +- web/src/client/assets/index.html | 19 +- .../client/components/session-view.test.ts | 21 +- web/src/client/components/session-view.ts | 2 +- .../terminal-lifecycle-manager.ts | 10 +- web/src/client/components/terminal.test.ts | 22 +- web/src/client/components/terminal.ts | 208 ++++++++-- web/src/client/utils/resize-coordinator.ts | 119 ------ 50 files changed, 674 insertions(+), 366 deletions(-) create mode 100644 docs/architecture-mario.md rename web/{vendored-pty => node-pty}/.gitignore (87%) rename web/{vendored-pty => node-pty}/README.md (100%) rename web/{vendored-pty => node-pty}/binding.gyp (93%) rename web/{vendored-pty => node-pty}/lib/eventEmitter2.d.ts (100%) rename web/{vendored-pty => node-pty}/lib/eventEmitter2.js (100%) rename web/{vendored-pty => node-pty}/lib/index.d.ts (100%) rename web/{vendored-pty => node-pty}/lib/index.js (100%) rename web/{vendored-pty => node-pty}/lib/interfaces.d.ts (100%) rename web/{vendored-pty => node-pty}/lib/interfaces.js (100%) rename web/{vendored-pty => node-pty}/lib/terminal.d.ts (100%) rename web/{vendored-pty => node-pty}/lib/terminal.js (100%) rename web/{vendored-pty => node-pty}/lib/types.d.ts (100%) rename web/{vendored-pty => node-pty}/lib/types.js (100%) rename web/{vendored-pty => node-pty}/lib/unixTerminal.d.ts (100%) rename web/{vendored-pty => node-pty}/lib/unixTerminal.js (85%) rename web/{vendored-pty => node-pty}/lib/utils.d.ts (100%) rename web/{vendored-pty => node-pty}/lib/utils.js (100%) rename web/{vendored-pty => node-pty}/lib/windowsTerminal.d.ts (100%) rename web/{vendored-pty => node-pty}/lib/windowsTerminal.js (100%) rename web/{vendored-pty => node-pty}/package.json (100%) rename web/{vendored-pty => node-pty}/pnpm-lock.yaml (100%) rename web/{vendored-pty => node-pty}/src/eventEmitter2.ts (100%) rename web/{vendored-pty => node-pty}/src/index.ts (100%) rename web/{vendored-pty => node-pty}/src/interfaces.ts (100%) rename web/{vendored-pty => node-pty}/src/native.d.ts (100%) rename web/{vendored-pty => node-pty}/src/terminal.ts (100%) rename web/{vendored-pty => node-pty}/src/types.ts (100%) rename web/{vendored-pty => node-pty}/src/unix/pty.cc (100%) rename web/{vendored-pty => node-pty}/src/unix/spawn-helper.cc (100%) rename web/{vendored-pty => node-pty}/src/unixTerminal.ts (84%) rename web/{vendored-pty => node-pty}/src/utils.ts (100%) rename web/{vendored-pty => node-pty}/src/win/conpty.cc (100%) rename web/{vendored-pty => node-pty}/src/win/conpty.h (100%) rename web/{vendored-pty => node-pty}/src/win/conpty_console_list.cc (100%) rename web/{vendored-pty => node-pty}/src/win/path_util.cc (100%) rename web/{vendored-pty => node-pty}/src/win/path_util.h (100%) rename web/{vendored-pty => node-pty}/src/win/winpty.cc (100%) rename web/{vendored-pty => node-pty}/src/windowsTerminal.ts (100%) rename web/{vendored-pty => node-pty}/tsconfig.json (100%) delete mode 100644 web/src/client/utils/resize-coordinator.ts diff --git a/docs/architecture-mario.md b/docs/architecture-mario.md new file mode 100644 index 00000000..80b2f3e8 --- /dev/null +++ b/docs/architecture-mario.md @@ -0,0 +1,365 @@ +# 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 + +```mermaid +sequenceDiagram + participant UI as Browser/Electron + participant WS as WebSocket + participant Server as Node Server + participant PTY as PTYManager + participant FWD as fwd.ts + participant Proc as User Process + + UI->>WS: keystrokes + WS->>Server: /api/sessions/:id/input + Server->>PTY: IPC Socket message + PTY->>Proc: write to stdin + Proc-->>PTY: stdout (ANSI sequences) + PTY-->>Server: write to ascinema file + alt External Terminal + FWD-->>TTY: mirror stdout to terminal + end + Server-->>UI: SSE stream (truncated) +``` + +### Key Files and Their Roles + +| File | Purpose | Critical Functions | +|------|---------|-------------------| +| `server.ts` | Main web server | HTTP endpoints, WebSocket handling | +| `pty-manager.ts` | PTY lifecycle management | `createSession()`, `setupPtyHandlers()` | +| `stream-watcher.ts` | Monitors ascinema files | `sendExistingContent()` - implements clear truncation | +| `fwd.ts` | External terminal forwarding | Process spawning, **BYPASSES TRUNCATION** | +| `terminal-manager.ts` | Binary buffer rendering | Converts 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**: +```javascript +// 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: + +```javascript +// 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: +```javascript +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**: +```javascript +// 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: + +```javascript +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 + +| Metric | Current | After Fix | +|--------|---------|-----------| +| 980MB session initial load | Infinite/Crash | 2-3 seconds | +| Data sent to client | 980MB | 2MB | +| Memory per terminal | 50-100MB | Target: 10MB | +| Clear sequence scan time | N/A | ~2 seconds for 1GB | +| Resize event storms | Exponential growth | Debounced | + +## Testing and Debugging + +### Test Large Session Handling +```bash +# 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 +```javascript +// 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. \ No newline at end of file diff --git a/web/build-native.js b/web/build-native.js index ff7c59c6..ccd7af2e 100755 --- a/web/build-native.js +++ b/web/build-native.js @@ -79,148 +79,12 @@ process.on('SIGTERM', () => { process.exit(1); }); -function applyMinimalPatches() { - console.log('Applying minimal SEA patches to node-pty...'); - - // Create sea-loader.js - const seaLoaderPath = path.join(__dirname, 'node_modules/node-pty/lib/sea-loader.js'); - if (!fs.existsSync(seaLoaderPath)) { - const seaLoaderContent = `"use strict"; -/* VIBETUNNEL_SEA_LOADER */ -Object.defineProperty(exports, "__esModule", { value: true }); -var path = require("path"); -var fs = require("fs"); - -// Custom loader for SEA that uses process.dlopen -var pty; - -// Helper function to load native module using dlopen -function loadNativeModule(modulePath) { - const module = { exports: {} }; - process.dlopen(module, modulePath); - return module.exports; -} - -// Determine the path to pty.node -function getPtyPath() { - const execDir = path.dirname(process.execPath); - // Look for pty.node next to the executable first - const ptyPath = path.join(execDir, 'pty.node'); - - if (fs.existsSync(ptyPath)) { - // Add path validation for security - const resolvedPath = path.resolve(ptyPath); - const resolvedExecDir = path.resolve(execDir); - if (!resolvedPath.startsWith(resolvedExecDir)) { - throw new Error('Invalid pty.node path detected'); - } - return ptyPath; - } - - // If not found, throw error with helpful message - throw new Error('Could not find pty.node next to executable at: ' + ptyPath); -} - -try { - const ptyPath = getPtyPath(); - - // Set spawn-helper path for macOS only - // Linux uses forkpty() directly and doesn't need spawn-helper - if (process.platform === 'darwin') { - const execDir = path.dirname(process.execPath); - const spawnHelperPath = path.join(execDir, 'spawn-helper'); - if (fs.existsSync(spawnHelperPath)) { - process.env.NODE_PTY_SPAWN_HELPER_PATH = spawnHelperPath; - } - } - - pty = loadNativeModule(ptyPath); -} catch (error) { - console.error('Failed to load pty.node:', error); - throw error; -} - -exports.default = pty; -`; - fs.writeFileSync(seaLoaderPath, seaLoaderContent); - } - - // Patch index.js - const indexPath = path.join(__dirname, 'node_modules/node-pty/lib/index.js'); - if (fs.existsSync(indexPath)) { - let content = fs.readFileSync(indexPath, 'utf8'); - if (!content.includes('VIBETUNNEL_SEA')) { - content = content.replace( - "exports.native = (process.platform !== 'win32' ? require('../build/Release/pty.node') : null);", - "exports.native = (process.platform !== 'win32' ? (process.env.VIBETUNNEL_SEA ? require('./sea-loader').default : require('../build/Release/pty.node')) : null);" - ); - fs.writeFileSync(indexPath, content); - } - } - - // Patch unixTerminal.js - const unixPath = path.join(__dirname, 'node_modules/node-pty/lib/unixTerminal.js'); - if (fs.existsSync(unixPath)) { - let content = fs.readFileSync(unixPath, 'utf8'); - if (!content.includes('VIBETUNNEL_SEA')) { - // Find and replace the pty loading section - const startMarker = 'var pty;\nvar helperPath;'; - const endMarker = 'var DEFAULT_FILE = \'sh\';'; - const startIdx = content.indexOf(startMarker); - const endIdx = content.indexOf(endMarker); - - if (startIdx !== -1 && endIdx !== -1) { - const newSection = `var pty; -var helperPath; -// For SEA, check environment variables -if (process.env.VIBETUNNEL_SEA) { - pty = require('./sea-loader').default; - // In SEA context, look for spawn-helper on macOS only (Linux doesn't use it) - if (process.platform === 'darwin') { - const execDir = path.dirname(process.execPath); - const spawnHelperPath = path.join(execDir, 'spawn-helper'); - if (require('fs').existsSync(spawnHelperPath)) { - helperPath = spawnHelperPath; - } else if (process.env.NODE_PTY_SPAWN_HELPER_PATH) { - helperPath = process.env.NODE_PTY_SPAWN_HELPER_PATH; - } - } - // On Linux, helperPath remains undefined which is fine -} else { - // Original loading logic - try { - pty = require('../build/Release/pty.node'); - helperPath = '../build/Release/spawn-helper'; - } - catch (outerError) { - try { - pty = require('../build/Debug/pty.node'); - helperPath = '../build/Debug/spawn-helper'; - } - catch (innerError) { - console.error('innerError', innerError); - // Re-throw the exception from the Release require if the Debug require fails as well - throw outerError; - } - } - helperPath = path.resolve(__dirname, helperPath); - helperPath = helperPath.replace('app.asar', 'app.asar.unpacked'); - helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked'); -} -`; - content = content.substring(0, startIdx) + newSection + content.substring(endIdx); - fs.writeFileSync(unixPath, content); - } - } - } - - console.log('SEA patches applied successfully'); -} +// No patching needed - SEA support is built into our vendored node-pty async function main() { try { - // Apply minimal patches to node-pty - applyMinimalPatches(); + // No patching needed - SEA support is built into our vendored node-pty + console.log('Using vendored node-pty with built-in SEA support...'); // Ensure native modules are built (in case postinstall didn't run) const nativePtyDir = 'node_modules/node-pty/build/Release'; diff --git a/web/vendored-pty/.gitignore b/web/node-pty/.gitignore similarity index 87% rename from web/vendored-pty/.gitignore rename to web/node-pty/.gitignore index f8190f2b..62786cf0 100644 --- a/web/vendored-pty/.gitignore +++ b/web/node-pty/.gitignore @@ -1,5 +1,4 @@ node_modules/ build/ -lib/ *.log .DS_Store \ No newline at end of file diff --git a/web/vendored-pty/README.md b/web/node-pty/README.md similarity index 100% rename from web/vendored-pty/README.md rename to web/node-pty/README.md diff --git a/web/vendored-pty/binding.gyp b/web/node-pty/binding.gyp similarity index 93% rename from web/vendored-pty/binding.gyp rename to web/node-pty/binding.gyp index 1fbf94a1..1ec5c1cd 100644 --- a/web/vendored-pty/binding.gyp +++ b/web/node-pty/binding.gyp @@ -11,7 +11,7 @@ 'xcode_settings': { 'GCC_ENABLE_CPP_EXCEPTIONS': 'YES', 'CLANG_CXX_LIBRARY': 'libc++', - 'MACOSX_DEPLOYMENT_TARGET': '10.7', + 'MACOSX_DEPLOYMENT_TARGET': '14.0', }, 'msvs_settings': { 'VCCLCompilerTool': { 'ExceptionHandling': 1 }, @@ -43,7 +43,7 @@ 'conditions': [ ['OS=="mac"', { 'xcode_settings': { - 'MACOSX_DEPLOYMENT_TARGET': '10.12' + 'MACOSX_DEPLOYMENT_TARGET': '14.0' } }] ] diff --git a/web/vendored-pty/lib/eventEmitter2.d.ts b/web/node-pty/lib/eventEmitter2.d.ts similarity index 100% rename from web/vendored-pty/lib/eventEmitter2.d.ts rename to web/node-pty/lib/eventEmitter2.d.ts diff --git a/web/vendored-pty/lib/eventEmitter2.js b/web/node-pty/lib/eventEmitter2.js similarity index 100% rename from web/vendored-pty/lib/eventEmitter2.js rename to web/node-pty/lib/eventEmitter2.js diff --git a/web/vendored-pty/lib/index.d.ts b/web/node-pty/lib/index.d.ts similarity index 100% rename from web/vendored-pty/lib/index.d.ts rename to web/node-pty/lib/index.d.ts diff --git a/web/vendored-pty/lib/index.js b/web/node-pty/lib/index.js similarity index 100% rename from web/vendored-pty/lib/index.js rename to web/node-pty/lib/index.js diff --git a/web/vendored-pty/lib/interfaces.d.ts b/web/node-pty/lib/interfaces.d.ts similarity index 100% rename from web/vendored-pty/lib/interfaces.d.ts rename to web/node-pty/lib/interfaces.d.ts diff --git a/web/vendored-pty/lib/interfaces.js b/web/node-pty/lib/interfaces.js similarity index 100% rename from web/vendored-pty/lib/interfaces.js rename to web/node-pty/lib/interfaces.js diff --git a/web/vendored-pty/lib/terminal.d.ts b/web/node-pty/lib/terminal.d.ts similarity index 100% rename from web/vendored-pty/lib/terminal.d.ts rename to web/node-pty/lib/terminal.d.ts diff --git a/web/vendored-pty/lib/terminal.js b/web/node-pty/lib/terminal.js similarity index 100% rename from web/vendored-pty/lib/terminal.js rename to web/node-pty/lib/terminal.js diff --git a/web/vendored-pty/lib/types.d.ts b/web/node-pty/lib/types.d.ts similarity index 100% rename from web/vendored-pty/lib/types.d.ts rename to web/node-pty/lib/types.d.ts diff --git a/web/vendored-pty/lib/types.js b/web/node-pty/lib/types.js similarity index 100% rename from web/vendored-pty/lib/types.js rename to web/node-pty/lib/types.js diff --git a/web/vendored-pty/lib/unixTerminal.d.ts b/web/node-pty/lib/unixTerminal.d.ts similarity index 100% rename from web/vendored-pty/lib/unixTerminal.d.ts rename to web/node-pty/lib/unixTerminal.d.ts diff --git a/web/vendored-pty/lib/unixTerminal.js b/web/node-pty/lib/unixTerminal.js similarity index 85% rename from web/vendored-pty/lib/unixTerminal.js rename to web/node-pty/lib/unixTerminal.js index 3bcd0813..7629c46d 100644 --- a/web/vendored-pty/lib/unixTerminal.js +++ b/web/node-pty/lib/unixTerminal.js @@ -40,24 +40,48 @@ const terminal_1 = require("./terminal"); const utils_1 = require("./utils"); let pty; let helperPath; -try { - pty = require('../build/Release/pty.node'); - helperPath = '../build/Release/spawn-helper'; +// Check if running in SEA (Single Executable Application) context +if (process.env.VIBETUNNEL_SEA) { + // In SEA mode, load native module using process.dlopen + const fs = require('fs'); + const execDir = path.dirname(process.execPath); + const ptyPath = path.join(execDir, 'pty.node'); + if (fs.existsSync(ptyPath)) { + const module = { exports: {} }; + process.dlopen(module, ptyPath); + pty = module.exports; + } + else { + throw new Error(`Could not find pty.node next to executable at: ${ptyPath}`); + } + // Set spawn-helper path for macOS only (Linux doesn't use it) + if (process.platform === 'darwin') { + helperPath = path.join(execDir, 'spawn-helper'); + if (!fs.existsSync(helperPath)) { + console.warn(`spawn-helper not found at ${helperPath}, PTY operations may fail`); + } + } } -catch (outerError) { +else { + // Standard Node.js loading try { - pty = require('../build/Debug/pty.node'); - helperPath = '../build/Debug/spawn-helper'; + pty = require('../build/Release/pty.node'); + helperPath = '../build/Release/spawn-helper'; } - catch (innerError) { - console.error('innerError', innerError); - // Re-throw the exception from the Release require if the Debug require fails as well - throw outerError; + catch (outerError) { + try { + pty = require('../build/Debug/pty.node'); + helperPath = '../build/Debug/spawn-helper'; + } + catch (innerError) { + console.error('innerError', innerError); + throw outerError; + } } + helperPath = path.resolve(__dirname, helperPath); + helperPath = helperPath.replace('app.asar', 'app.asar.unpacked'); + helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked'); } -helperPath = path.resolve(__dirname, helperPath); -helperPath = helperPath.replace('app.asar', 'app.asar.unpacked'); -helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked'); const DEFAULT_FILE = 'sh'; const DEFAULT_NAME = 'xterm'; const DESTROY_SOCKET_TIMEOUT_MS = 200; diff --git a/web/vendored-pty/lib/utils.d.ts b/web/node-pty/lib/utils.d.ts similarity index 100% rename from web/vendored-pty/lib/utils.d.ts rename to web/node-pty/lib/utils.d.ts diff --git a/web/vendored-pty/lib/utils.js b/web/node-pty/lib/utils.js similarity index 100% rename from web/vendored-pty/lib/utils.js rename to web/node-pty/lib/utils.js diff --git a/web/vendored-pty/lib/windowsTerminal.d.ts b/web/node-pty/lib/windowsTerminal.d.ts similarity index 100% rename from web/vendored-pty/lib/windowsTerminal.d.ts rename to web/node-pty/lib/windowsTerminal.d.ts diff --git a/web/vendored-pty/lib/windowsTerminal.js b/web/node-pty/lib/windowsTerminal.js similarity index 100% rename from web/vendored-pty/lib/windowsTerminal.js rename to web/node-pty/lib/windowsTerminal.js diff --git a/web/vendored-pty/package.json b/web/node-pty/package.json similarity index 100% rename from web/vendored-pty/package.json rename to web/node-pty/package.json diff --git a/web/vendored-pty/pnpm-lock.yaml b/web/node-pty/pnpm-lock.yaml similarity index 100% rename from web/vendored-pty/pnpm-lock.yaml rename to web/node-pty/pnpm-lock.yaml diff --git a/web/vendored-pty/src/eventEmitter2.ts b/web/node-pty/src/eventEmitter2.ts similarity index 100% rename from web/vendored-pty/src/eventEmitter2.ts rename to web/node-pty/src/eventEmitter2.ts diff --git a/web/vendored-pty/src/index.ts b/web/node-pty/src/index.ts similarity index 100% rename from web/vendored-pty/src/index.ts rename to web/node-pty/src/index.ts diff --git a/web/vendored-pty/src/interfaces.ts b/web/node-pty/src/interfaces.ts similarity index 100% rename from web/vendored-pty/src/interfaces.ts rename to web/node-pty/src/interfaces.ts diff --git a/web/vendored-pty/src/native.d.ts b/web/node-pty/src/native.d.ts similarity index 100% rename from web/vendored-pty/src/native.d.ts rename to web/node-pty/src/native.d.ts diff --git a/web/vendored-pty/src/terminal.ts b/web/node-pty/src/terminal.ts similarity index 100% rename from web/vendored-pty/src/terminal.ts rename to web/node-pty/src/terminal.ts diff --git a/web/vendored-pty/src/types.ts b/web/node-pty/src/types.ts similarity index 100% rename from web/vendored-pty/src/types.ts rename to web/node-pty/src/types.ts diff --git a/web/vendored-pty/src/unix/pty.cc b/web/node-pty/src/unix/pty.cc similarity index 100% rename from web/vendored-pty/src/unix/pty.cc rename to web/node-pty/src/unix/pty.cc diff --git a/web/vendored-pty/src/unix/spawn-helper.cc b/web/node-pty/src/unix/spawn-helper.cc similarity index 100% rename from web/vendored-pty/src/unix/spawn-helper.cc rename to web/node-pty/src/unix/spawn-helper.cc diff --git a/web/vendored-pty/src/unixTerminal.ts b/web/node-pty/src/unixTerminal.ts similarity index 84% rename from web/vendored-pty/src/unixTerminal.ts rename to web/node-pty/src/unixTerminal.ts index f10a4118..300c60d1 100644 --- a/web/vendored-pty/src/unixTerminal.ts +++ b/web/node-pty/src/unixTerminal.ts @@ -13,23 +13,48 @@ import { assign } from './utils'; let pty: IUnixNative; let helperPath: string; -try { - pty = require('../build/Release/pty.node'); - helperPath = '../build/Release/spawn-helper'; -} catch (outerError) { - try { - pty = require('../build/Debug/pty.node'); - helperPath = '../build/Debug/spawn-helper'; - } catch (innerError) { - console.error('innerError', innerError); - // Re-throw the exception from the Release require if the Debug require fails as well - throw outerError; - } -} -helperPath = path.resolve(__dirname, helperPath); -helperPath = helperPath.replace('app.asar', 'app.asar.unpacked'); -helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked'); +// Check if running in SEA (Single Executable Application) context +if (process.env.VIBETUNNEL_SEA) { + // In SEA mode, load native module using process.dlopen + const fs = require('fs'); + const execDir = path.dirname(process.execPath); + const ptyPath = path.join(execDir, 'pty.node'); + + if (fs.existsSync(ptyPath)) { + const module = { exports: {} }; + process.dlopen(module, ptyPath); + pty = module.exports as IUnixNative; + } else { + throw new Error(`Could not find pty.node next to executable at: ${ptyPath}`); + } + + // Set spawn-helper path for macOS only (Linux doesn't use it) + if (process.platform === 'darwin') { + helperPath = path.join(execDir, 'spawn-helper'); + if (!fs.existsSync(helperPath)) { + console.warn(`spawn-helper not found at ${helperPath}, PTY operations may fail`); + } + } +} else { + // Standard Node.js loading + try { + pty = require('../build/Release/pty.node'); + helperPath = '../build/Release/spawn-helper'; + } catch (outerError) { + try { + pty = require('../build/Debug/pty.node'); + helperPath = '../build/Debug/spawn-helper'; + } catch (innerError) { + console.error('innerError', innerError); + throw outerError; + } + } + + helperPath = path.resolve(__dirname, helperPath); + helperPath = helperPath.replace('app.asar', 'app.asar.unpacked'); + helperPath = helperPath.replace('node_modules.asar', 'node_modules.asar.unpacked'); +} const DEFAULT_FILE = 'sh'; const DEFAULT_NAME = 'xterm'; diff --git a/web/vendored-pty/src/utils.ts b/web/node-pty/src/utils.ts similarity index 100% rename from web/vendored-pty/src/utils.ts rename to web/node-pty/src/utils.ts diff --git a/web/vendored-pty/src/win/conpty.cc b/web/node-pty/src/win/conpty.cc similarity index 100% rename from web/vendored-pty/src/win/conpty.cc rename to web/node-pty/src/win/conpty.cc diff --git a/web/vendored-pty/src/win/conpty.h b/web/node-pty/src/win/conpty.h similarity index 100% rename from web/vendored-pty/src/win/conpty.h rename to web/node-pty/src/win/conpty.h diff --git a/web/vendored-pty/src/win/conpty_console_list.cc b/web/node-pty/src/win/conpty_console_list.cc similarity index 100% rename from web/vendored-pty/src/win/conpty_console_list.cc rename to web/node-pty/src/win/conpty_console_list.cc diff --git a/web/vendored-pty/src/win/path_util.cc b/web/node-pty/src/win/path_util.cc similarity index 100% rename from web/vendored-pty/src/win/path_util.cc rename to web/node-pty/src/win/path_util.cc diff --git a/web/vendored-pty/src/win/path_util.h b/web/node-pty/src/win/path_util.h similarity index 100% rename from web/vendored-pty/src/win/path_util.h rename to web/node-pty/src/win/path_util.h diff --git a/web/vendored-pty/src/win/winpty.cc b/web/node-pty/src/win/winpty.cc similarity index 100% rename from web/vendored-pty/src/win/winpty.cc rename to web/node-pty/src/win/winpty.cc diff --git a/web/vendored-pty/src/windowsTerminal.ts b/web/node-pty/src/windowsTerminal.ts similarity index 100% rename from web/vendored-pty/src/windowsTerminal.ts rename to web/node-pty/src/windowsTerminal.ts diff --git a/web/vendored-pty/tsconfig.json b/web/node-pty/tsconfig.json similarity index 100% rename from web/vendored-pty/tsconfig.json rename to web/node-pty/tsconfig.json diff --git a/web/package.json b/web/package.json index 7ae50dbe..05e46020 100644 --- a/web/package.json +++ b/web/package.json @@ -75,7 +75,7 @@ "mime-types": "^3.0.1", "monaco-editor": "^0.52.2", "multer": "^2.0.1", - "node-pty": "file:./vendored-pty", + "node-pty": "file:./node-pty", "postject": "^1.0.0-alpha.6", "signal-exit": "^4.1.0", "web-push": "^3.6.7", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 344da5fa..7afbd198 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -72,8 +72,8 @@ importers: specifier: ^2.0.1 version: 2.0.1 node-pty: - specifier: file:./vendored-pty - version: '@vibetunnel/vendored-pty@file:vendored-pty' + specifier: file:./node-pty + version: file:node-pty postject: specifier: ^1.0.0-alpha.6 version: 1.0.0-alpha.6 @@ -970,9 +970,6 @@ packages: '@types/yauzl@2.10.3': resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} - '@vibetunnel/vendored-pty@file:vendored-pty': - resolution: {directory: vendored-pty, type: directory} - '@vitest/coverage-v8@3.2.4': resolution: {integrity: sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==} peerDependencies: @@ -2311,6 +2308,9 @@ packages: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + node-pty@file:node-pty: + resolution: {directory: node-pty, type: directory} + node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} @@ -3934,10 +3934,6 @@ snapshots: '@types/node': 24.0.4 optional: true - '@vibetunnel/vendored-pty@file:vendored-pty': - dependencies: - node-addon-api: 7.1.1 - '@vitest/coverage-v8@3.2.4(vitest@3.2.4)': dependencies: '@ampproject/remapping': 2.3.0 @@ -5398,6 +5394,10 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 + node-pty@file:node-pty: + dependencies: + node-addon-api: 7.1.1 + node-releases@2.0.19: {} normalize-path@3.0.0: {} diff --git a/web/src/client/assets/index.html b/web/src/client/assets/index.html index 845d92a2..439af5c7 100644 --- a/web/src/client/assets/index.html +++ b/web/src/client/assets/index.html @@ -79,7 +79,11 @@