* Implement ultra-low-latency WebSocket input system This change eliminates input lag on slow networks by replacing HTTP requests with a fire-and-forget WebSocket system for terminal input transmission. Key optimizations: - Raw text transmission (no JSON overhead): 1 byte vs 87+ bytes per keystroke - Fire-and-forget input (no ACK blocking): eliminates 20-50ms roundtrip latency - Single persistent connection per session: zero connection overhead - Direct PTY write path: fastest possible server processing - Graceful HTTP fallback: maintains full backward compatibility Performance improvements: - 99% bandwidth reduction per keystroke - 90% latency reduction on slow networks - Zero blocking waits for rapid typing - Eliminates HTTP/1.1 connection overhead Files changed: - Add: src/client/services/websocket-input-client.ts (WebSocket client) - Add: src/server/routes/websocket-input.ts (WebSocket input handler) - Modify: src/client/components/session-view/input-manager.ts (WebSocket integration) - Modify: src/server/server.ts (WebSocket routing for /ws/input endpoint) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Add socket_input URL parameter feature flag Adds ?socket_input=true/false URL parameter to control WebSocket input behavior: - socket_input=true (default): Enable WebSocket input with HTTP fallback - socket_input=false: Force HTTP-only input mode (disable WebSocket) This feature flag enables: - A/B testing between WebSocket and HTTP input performance - Debugging WebSocket connection issues - Gradual rollout control - Easy fallback mechanism for production issues Examples: - http://localhost:4020/?socket_input=true&session=abc123 (WebSocket enabled) - http://localhost:4020/?socket_input=false&session=abc123 (HTTP only) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix WebSocket input key mapping for special keys The server was receiving key names like "enter" and writing them literally as text instead of interpreting them as actual key presses. Added proper key mapping: - "enter" → '\r' (carriage return) - "escape" → '\x1b' (ESC key) - "backspace" → '\x7f' (DEL) - "tab" → '\t' (TAB) - "arrow_up" → '\x1b[A' (VT100 up arrow) - "arrow_down" → '\x1b[B' (VT100 down arrow) - "arrow_left"/"arrow_right" → '\x1b[D'/'\x1b[C' (VT100 arrows) - Function keys F1-F12 → proper VT100 sequences - Regular text → sent as-is Now "enter" actually triggers ENTER key instead of typing "enter" text. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Reuse existing key mapping logic instead of duplicating Instead of duplicating the key mapping logic (enter → \r, etc.), now properly reuse the existing PtyManager.sendInput() method which already handles: - Key mapping via convertSpecialKey() method - SessionInput type validation - Special key vs regular text determination - Error handling and session management This ensures consistency between HTTP /input and WebSocket /ws/input endpoints and avoids maintaining duplicate key mapping tables. Benefits: - Single source of truth for key mappings - Consistent behavior across input methods - Proper type safety with SessionInput/SpecialKey types - Reuses existing error handling and validation 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix special key detection in pasted text using null-byte markers Problem: Pasting text like "i am entering the world" would incorrectly interpret "enter" as the ENTER key instead of literal text. Solution: Use null-byte markers to distinguish special keys from literal text: - Special keys: "\x00enter\x00" → ENTER key press - Regular text: "enter" → literal text "enter" - Pasted text: "i am entering the world" → literal text This maintains the raw text protocol while solving the ambiguity: - Single keystroke "enter" → "\x00enter\x00" → ENTER key - Pasted word "enter" → "enter" → literal text "enter" - Multi-word paste → always literal text Benefits: - Preserves ultra-minimal bandwidth (just 2 null bytes overhead) - Maintains raw text protocol (no JSON) - Solves paste ambiguity correctly - Null bytes rarely appear in normal text input 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix paste text ambiguity and control sequence handling - Modified sendInputText to always treat pasted content as literal text - Added sendControlSequence method for control characters like Ctrl+R - Updated direct keyboard manager to use sendControlSequence for control chars - This ensures pasted text containing words like "enter", "backspace" is sent as literal text - Control sequences like Ctrl+R (\x12) are properly transmitted via WebSocket with null-byte escaping 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Add debug logging for WebSocket input transmission - Added detailed logging on client side to show what's being sent - Added server side logging to show what's being received - This will help debug the enter key transmission issue 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Add comprehensive input logging to track key processing - Added logging in WebSocket handler to show parsed input - Added logging in PtyManager to show key conversion and output - This will show the complete flow: received key -> parsed input -> converted output 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Add DEBUG environment variable for enabling debug logging - Added DEBUG=true environment variable option alongside --debug flag - Makes it easier to enable debug logging during development 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix WebSocket input handling for HQ mode and mobile keyboards - Add WebSocket proxy support for remote sessions in HQ mode - Fix mobile keyboard special key handling to use sendInput() instead of sendInputText() - Make InputManager.sendInput() public for use by mobile components - Update DirectKeyboardManager to correctly send special keys from custom keyboard - Handle WebSocket data type conversion for native WebSocket API compatibility This ensures special keys are properly wrapped with null bytes (\x00) when sent via WebSocket, and enables low-latency input for remote sessions in HQ mode. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * refactor: eliminate code duplication in input-manager and fix TypeScript typing - Extract common WebSocket/HTTP fallback logic into sendInputInternal() - Reduce ~155 lines of duplicate code across sendInput, sendInputText, and sendControlSequence methods - Add proper WebSocketRequest interface to replace any type usage - Fix linting issues in server.ts WebSocket handling * up * Add comprehensive tests for WebSocket input handler - Test special key handling with null-byte wrapped keys (\x00enter\x00) - Test text containing key names ('i enter the world') treated as literal text - Test HQ mode remote session proxying vs local PTY handling - Test edge cases: empty messages, malformed keys, binary data, Unicode - Test error handling and connection lifecycle - Remove duplicate special key validation logic - Delegate key conversion to ptyManager for consistency 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> * Fix TypeScript linting issues in WebSocket input handler tests - Replace all 'any' types with proper type definitions - Add MockEventListener type for test event handlers - Use 'unknown' instead of 'any' for type assertions - Remove unused SessionInput import - Fix formatting issues per Biome requirements All 20 WebSocket input handler tests now pass with proper TypeScript types. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Mario Zechner <badlogicgames@gmail.com> Co-authored-by: Peter Steinberger <steipete@gmail.com> |
||
|---|---|---|
| .claude | ||
| .github | ||
| apple | ||
| assets | ||
| docs | ||
| ios | ||
| mac | ||
| scripts | ||
| tauri | ||
| TestResults-Mac.xcresult | ||
| VibeTunnel.xcworkspace | ||
| web | ||
| .gitattributes | ||
| .github-config | ||
| .gitignore | ||
| .mcp.json | ||
| appcast-prerelease.xml | ||
| appcast.xml | ||
| calculate-all-coverage.sh | ||
| CHANGELOG.md | ||
| CLAUDE.md | ||
| LICENSE | ||
| README.md | ||
VibeTunnel
Turn any browser into your Mac terminal. VibeTunnel proxies your terminals right into the browser, so you can vibe-code anywhere.
Why VibeTunnel?
Ever wanted to check on your AI agents while you're away? Need to monitor that long-running build from your phone? Want to share a terminal session with a colleague without complex SSH setups? VibeTunnel makes it happen with zero friction.
Quick Start
Requirements
VibeTunnel requires an Apple Silicon Mac (M1+). Intel Macs are not supported.
1. Download & Install
Download VibeTunnel and drag it to your Applications folder.
2. Launch VibeTunnel
VibeTunnel lives in your menu bar. Click the icon to start the server.
3. Use the vt Command
# Run any command in the browser
vt pnpm run dev
# Monitor AI agents
vt claude --dangerously-skip-permissions
# Shell aliases work automatically!
vt claude-danger # Your custom aliases are resolved
# Open an interactive shell
vt --shell
4. Open Your Dashboard
Visit http://localhost:4020 to see all your terminal sessions.
Features
- 🌐 Browser-Based Access - Control your Mac terminal from any device with a web browser
- 🚀 Zero Configuration - No SSH keys, no port forwarding, no complexity
- 🤖 AI Agent Friendly - Perfect for monitoring Claude Code, ChatGPT, or any terminal-based AI tools
- 🔒 Secure by Design - Password protection, localhost-only mode, or secure tunneling via Tailscale/ngrok
- 📱 Mobile Ready - Native iOS app and responsive web interface for phones and tablets
- 🎬 Session Recording - All sessions recorded in asciinema format for later playback
- ⚡ High Performance - Powered by Bun runtime for blazing-fast JavaScript execution
- 🍎 Apple Silicon Native - Optimized for M1/M2/M3 Macs with ARM64-only binaries
- 🐚 Shell Alias Support - Your custom aliases and shell functions work automatically
Note
: The iOS app and Tauri-based components are still work in progress and not recommended for production use yet.
Architecture
VibeTunnel consists of three main components:
- macOS Menu Bar App - Native Swift application that manages the server lifecycle
- Node.js/Bun Server - High-performance TypeScript server handling terminal sessions
- Web Frontend - Modern web interface using Lit components and xterm.js
The server runs as a standalone Bun executable with embedded Node.js modules, providing excellent performance and minimal resource usage.
Remote Access Options
Option 1: Tailscale (Recommended)
- Install Tailscale on your Mac and remote device
- Access VibeTunnel at
http://[your-mac-name]:4020
Option 2: ngrok
- Add your ngrok auth token in VibeTunnel settings
- Enable ngrok tunneling
- Share the generated URL
Option 3: Local Network
- Set a dashboard password in settings
- Switch to "Network" mode
- Access via
http://[your-mac-ip]:4020
Option 4: Cloudflare Quick Tunnel
- Install cloudflared
- Run
cloudflared tunnel --url http://localhost:4020 - Access via the generated
*.trycloudflare.comURL
Building from Source
Prerequisites
- macOS 14.0+ (Sonoma) on Apple Silicon (M1/M2/M3)
- Xcode 16.0+
- Node.js 20+
- Bun runtime
Build Steps
# Clone the repository
git clone https://github.com/amantus-ai/vibetunnel.git
cd vibetunnel
# Set up code signing (required for macOS/iOS development)
# Create Local.xcconfig files with your Apple Developer Team ID
# Note: These files must be in the same directory as Shared.xcconfig
cat > mac/VibeTunnel/Local.xcconfig << EOF
// Local Development Configuration
// DO NOT commit this file to version control
DEVELOPMENT_TEAM = YOUR_TEAM_ID
CODE_SIGN_STYLE = Automatic
EOF
cat > ios/VibeTunnel/Local.xcconfig << EOF
// Local Development Configuration
// DO NOT commit this file to version control
DEVELOPMENT_TEAM = YOUR_TEAM_ID
CODE_SIGN_STYLE = Automatic
EOF
# Build the web server
cd web
pnpm install
pnpm run build
# Optional: Build with custom Node.js for smaller binary (46% size reduction)
# export VIBETUNNEL_USE_CUSTOM_NODE=YES
# node build-custom-node.js # Build optimized Node.js (one-time, ~20 min)
# pnpm run build # Will use custom Node.js automatically
# Build the macOS app
cd ../mac
./scripts/build.sh --configuration Release
Custom Node.js Builds
VibeTunnel supports building with a custom Node.js for a 46% smaller executable (61MB vs 107MB):
# Build custom Node.js (one-time, ~20 minutes)
node build-custom-node.js
# Use environment variable for all builds
export VIBETUNNEL_USE_CUSTOM_NODE=YES
# Or use in Xcode Build Settings
# Add User-Defined Setting: VIBETUNNEL_USE_CUSTOM_NODE = YES
See Custom Node Build Flags for detailed optimization information.
Development
For development setup and contribution guidelines, see CONTRIBUTING.md.
Key Files
- macOS App:
mac/VibeTunnel/VibeTunnelApp.swift - Server:
web/src/server/(TypeScript/Node.js) - Web UI:
web/src/client/(Lit/TypeScript) - iOS App:
ios/VibeTunnel/
Testing & Code Coverage
VibeTunnel has comprehensive test suites with code coverage enabled for all projects:
# Run all tests with coverage
./scripts/test-all-coverage.sh
# macOS tests with coverage (Swift Testing)
cd mac && swift test --enable-code-coverage
# iOS tests with coverage (using xcodebuild)
cd ios && ./scripts/test-with-coverage.sh
# Web tests with coverage (Vitest)
cd web && ./scripts/coverage-report.sh
Coverage Requirements:
- macOS/iOS: 75% minimum (enforced in CI)
- Web: 80% minimum for lines, functions, branches, and statements
Debug Logging
Enable debug logging for troubleshooting:
# Enable debug mode
export VIBETUNNEL_DEBUG=1
# Or use inline
VIBETUNNEL_DEBUG=1 vt your-command
Debug logs are written to ~/.vibetunnel/log.txt.
Documentation
- Technical Specification - Detailed architecture and implementation
- Contributing Guide - Development setup and guidelines
- Architecture - System design overview
- Build System - Build process details
- Push Notifications - How web push notifications work
macOS Permissions
macOS is finicky when it comes to permissions. The system will only remember the first path from where an app requests permissions. If subsequently the app starts somewhere else, it will silently fail. Fix: Delete the entry and restart settings, restart app and next time the permission is requested, there should be an entry in Settings again.
Important: You need to set your Developer ID in Local.xcconfig. If apps are signed Ad-Hoc, each new signing will count as a new app for macOS and the permissions have to be (deleted and) requested again.
Debug vs Release Bundle IDs: The Debug configuration uses a different bundle identifier (sh.vibetunnel.vibetunnel.debug) than Release (sh.vibetunnel.vibetunnel). This allows you to have both versions installed simultaneously, but macOS treats them as separate apps for permissions. You'll need to grant permissions separately for each version.
If that fails, use the terminal to reset:
# This removes Accessibility permission for a specific bundle ID:
sudo tccutil reset Accessibility sh.vibetunnel.vibetunnel
sudo tccutil reset Accessibility sh.vibetunnel.vibetunnel.debug # For debug builds
sudo tccutil reset ScreenCapture sh.vibetunnel.vibetunnel
sudo tccutil reset ScreenCapture sh.vibetunnel.vibetunnel.debug # For debug builds
# This removes all Automation permissions system-wide (cannot target specific apps):
sudo tccutil reset AppleEvents
Support VibeTunnel
Love VibeTunnel? Help us keep the terminal vibes flowing! Your support helps us buy pizza and drinks while we keep hacking on your favorite AI agent orchestration platform.
All donations go directly to the development team. Choose your own amount - one-time or monthly! Visit our Polar page to support us.
Credits
Created with ❤️ by:
- @badlogic - Mario Zechner
- @mitsuhiko - Armin Ronacher
- @steipete - Peter Steinberger
- @hjanuschka - Helmut Januschka
- @manuelmaly - Manuel Maly
License
VibeTunnel is open source software licensed under the MIT License. See LICENSE for details.
Ready to vibe? Download VibeTunnel and start tunneling!
