Overview
VibeGhost is a custom Next.js frontend for terminal-based AI coding sessions. It started as a way to improve VibeTunnel by replacing its Lit-based frontend and xterm.js terminal with a modern React stack.
Along the way, we rebuilt nearly everything and eventually brought the VibeTunnel server code directly into the application. The result is a single, self-contained app that runs your terminal sessions in the browser with full cross-browser support, session organization, file preview, diff visualization, and two orchestration harnesses for multi-session automation.
Installation
git clone https://github.com/openshiporg/vibeghost.git cd vibeghost npm install # Start on the default port (4021) npm run dev # Or run on a custom port PORT=4022 npm run dev
The application will be available at http://localhost:4021 (or whichever port you set with the PORT environment variable).
Keep it running
Run the setup script to install VibeGhost as a persistent service that survives reboots and terminal closures:
./setup.sh
Mobile Access
VibeGhost is fully responsive and works on mobile browsers (iOS Safari, Chrome on Android, etc.). The easiest way to access it from your phone is through Tailscale.
Setup
Install Tailscale on the computer running VibeGhost and on your phone. Sign in with the same account on both devices.
Find your computer's Tailscale IP. It will look something like 100.x.x.x. You can find it in the Tailscale app or by running:
tailscale ip -4
On your phone's browser, navigate to:
http://100.x.x.x:4021
Replace 100.x.x.x with your actual Tailscale IP and 4021 with whatever port VibeGhost is running on.
Ghostty Web
VibeTunnel originally used xterm.js for terminal rendering. We replaced it with Ghostty Web, which is faster, handles mobile rendering properly, and supports proper font rendering.
The terminal component loads the Ghostty WASM binary (ghostty-vt.wasm) once and reuses it across sessions. Each session gets its own terminal instance that connects to the underlying PTY via the WebSocket v3 protocol.
How it works
The GhosttyTerminalComponent in components/terminal/ghostty-terminal.tsx wraps the Ghostty library in a React component. It handles:
- WASM loading and initialization (lazy, once per page)
- Terminal resize with debounced PTY resize messages
- Scroll position tracking with a "scroll to bottom" button
- Mobile detection and keyboard visibility handling
- WebSocket connection lifecycle (connect, reconnect, cleanup)
Input Component
VibeTunnel required typing directly into the terminal. This was error-prone on mobile, had poor paste support, and made it hard to compose multi-line prompts. VibeGhost adds a dedicated input component (components/agent-input.tsx) that sits below the terminal.
Features
- Multi-line editing (Shift+Enter for new line, Enter to send)
- Safe paste handling with an explicit paste button and paste preview
- Hold-to-repeat scroll buttons for terminal scrollback
- Abort button that sends Ctrl+C (SIGINT) to the terminal
- Connection status indicator (connected/connecting/disconnected)
Mobile toolbar
On mobile, a horizontally scrollable quick-keys toolbar appears above the input. It includes arrow keys, Tab, Esc, Ctrl+C, Ctrl+D, Ctrl+Z, Ctrl+L, Ctrl+R, page navigation, and common special characters. Each button uses touch events with hold-to-repeat support.
Keybindings
VibeGhost intercepts keyboard events to route them correctly between the browser and the terminal. The logic lives in lib/browser-shortcuts.ts.
Browser shortcuts (never captured)
These always go to the browser, not the terminal:
Cmd+T New tabCmd+W Close tabCmd+N New windowCmd+Q Quit browserCmd+1-9 Switch tabsCmd+C/V/X Copy/paste/cutCmd+Shift+] Next tabCmd+Shift+[ Previous tabTerminal shortcuts
Everything else goes to the terminal. Common ones include:
Ctrl+C Interrupt (SIGINT)Ctrl+D EOFCtrl+Z Suspend (SIGTSTP)Ctrl+L Clear screenCtrl+R Reverse searchCtrl+A Start of lineCtrl+E End of lineCtrl+W Delete wordCross-Browser Support
VibeTunnel's SSE (Server-Sent Events) implementation only worked reliably in Chrome. Firefox, Safari, Arc, and mobile browsers would fail to connect or drop connections.
VibeGhost works across all modern browsers. This was achieved by:
- Migrating from SSE to WebSockets (no 6-connection limit)
- Using Ghostty Web instead of xterm.js for terminal rendering
- Building the UI with React + shadcn/ui instead of Lit web components
WebSocket v3 Protocol
VibeTunnel used Server-Sent Events (SSE) for terminal output streaming. SSE has a hard limit of 6 concurrent connections per browser, which meant running more than 6 sessions would throttle everything. It also only worked in Chrome.
We migrated to WebSockets using VibeTunnel's internal v3 binary protocol. This is the same protocol that VibeTunnel Beta 16 implements, but that version hasn't been published yet.
How the client works
The TerminalSocketClient in lib/terminal-socket-client.ts is a singleton that manages a single WebSocket connection for all sessions. It handles:
- Session multiplexing over one connection (subscribe/unsubscribe per session)
- Binary frame encoding/decoding for stdout, resize, input, snapshots, and events
- Automatic reconnection with exponential backoff
- Session activity tracking (progress indicators, idle detection)
- Connection state broadcasting to all components
Message types
The protocol supports stdout data, input text, resize commands, buffer snapshots, session events, and subscribe/unsubscribe frames. Each frame starts with a message type byte followed by the session ID and payload.
Integrated Server
VibeGhost originally depended on a separate VibeTunnel installation. The frontend would proxy API calls to a running VibeTunnel instance. We eventually brought the entire VibeTunnel server codebase directly into the app.
Architecture
The server.ts file boots a single HTTP server that handles both Next.js page requests and Express API routes. The stack:
- Next.js for the React frontend (SSR + static pages)
- Express for the VibeTunnel REST API (
/api/*routes) - node-pty for pseudo-terminal management
- WsV3Hub for WebSocket v3 protocol handling
- PtyManager for session creation, resize, and lifecycle
- SessionMonitor for activity detection and status tracking
- HeartbeatService for autonomous session monitoring
All services run on a single port (default 4021) via a custom server.ts entrypoint. Session state is stored in a control directory at ~/.vibeghost/control.
Session Grouping
VibeTunnel showed all sessions in a flat list. VibeGhost adds smart session grouping based on session names. The logic lives in components/sessions/sessions-shell.tsx.
Bracket mode (default)
Sessions are grouped by their bracket prefix. For example:
[pi] openfront → group "pi" [pi] vibeghostty → group "pi" [dev] openfront → group "dev" [Ghost] orchestrator → group "Ghost"
Ghost sessions ([Ghost] prefix) are always sorted to the top. Exited sessions are grouped separately at the bottom.
Name mode
Switch to name mode to group sessions by project name instead (stripping the bracket prefix). This lets you see all sessions working on the same project across different agents:
[pi] openfront → group "openfront" [dev] openfront → group "openfront" [pi] vibeghostty → group "vibeghostty"
Sidebar Cards
When you're in a session, the sidebar (components/sessions/sidebar-cards.tsx) shows contextual information organized into tabs:
- Sessions tab: Related and sibling sessions. If you're in
[pi] openfront, it shows otherpisessions and other sessions working onopenfront. - Files tab: File explorer for the session's working directory with git status indicators.
- Changes tab: Working changes (modified, added, deleted files) with inline diff viewing.
On mobile, the sidebar becomes a bottom drawer (using Vaul) that can be swiped up from the bottom of the screen.
File Explorer
The file explorer (components/sessions/file-explorer.tsx) shows a tree view of the session's working directory. Directories are lazily loaded as you expand them.
Files are color-coded by git status:
Clicking a file opens it in the Monaco editor preview. Files with changes also have a diff button to view inline diffs.
Monaco Editor
Click any file in the sidebar to open it in a Monaco Editor preview. This is the same editor that powers VS Code, so you get full syntax highlighting, bracket matching, code folding, and minimap directly in the browser.
The editor is read-only and automatically detects the language from the file extension. It respects the current theme (light/dark).
Diff Visualization
The working changes panel uses Pierre Computer's @pierre/diffs library to render inline diff views. The PatchDiff component from @pierre/diffs/react takes a unified diff string and renders it with syntax-highlighted additions and deletions.
Diffs are fetched from the server's git routes (/api/git/diff) and displayed in a drawer on mobile or inline on desktop.
Ghost Harness
The Ghost harness enables multi-session orchestration. A "Ghost" terminal session becomes a controller that can send commands to other terminal sessions.
How it works
When you invoke a Ghost (via the InvokeGhostDialog), you select which sessions it should control and optionally choose a harness type from the registry. The Ghost session is created with a [Ghost] bracket prefix and a pipe-separated name encoding the controlled session IDs and harness configuration.
The Ghost gets a system prompt (generated by lib/ghost-utils.ts) describing the available sessions and their current state. It can interact with controlled sessions through VibeTunnel's API:
# List sessions
curl http://localhost:4021/api/ghost?action=list
# Read terminal output
curl "http://localhost:4021/api/ghost?action=read&sessionId=SESSION_ID"
# Send command to a session
curl -X POST http://localhost:4021/api/ghost \
-d '{"action":"send","sessionId":"ID","text":"your command\n"}'
# Wait for a session to become idle
curl "http://localhost:4021/api/ghost?action=wait&sessionId=SESSION_ID"Orchestrator UI
The Ghost orchestrator layout (components/ghost/ghost-orchestrator-layout.tsx) shows the Ghost terminal alongside all controlled sessions. Each controlled session has a real-time activity indicator:
- Green dot = Ready (idle, waiting for input)
- Red pulsing dot = Busy (actively running)
On mobile, controlled sessions open in a sheet overlay. On desktop, clicking a session shows its terminal in a side panel.
Heartbeat Harness
Inspired by OpenClaw's heartbeat, the Heartbeat harness brings autonomous session monitoring directly into VibeGhost. It periodically checks on your running sessions, detects when AI agents are stuck or idle, and can wake them back up automatically.
Configuration
The heartbeat dialog (components/heartbeat/heartbeat-dialog.tsx) lets you configure:
- Command: The command to send to idle sessions (default:
pi) - Interval: How often to check, in minutes (default: 30)
- OK Token: A string to look for in the response (default:
HEARTBEAT_OK) - Session scope: Monitor all sessions or select specific ones
- Active hours: Only run during certain time windows (e.g. 8 AM to midnight)
How ticks work
Each tick, the heartbeat service inspects every monitored session. If a session appears idle (no recent activity), the heartbeat sends the configured command and checks for the OK token in the response. The tick result includes how many sessions were inspected, prompted, and skipped (because they were already busy).
If a session doesn't respond with the OK token, the heartbeat flags it as needing attention. You can view the run history and last alert message in the heartbeat status panel.
Harness Registry
The Ghost harness system is extensible through a harness registry (lib/harnesses/harness-registry.ts). Each harness defines:
- id and name for identification
- description shown in the invoke dialog
- configFields: Form fields (text, password, select) for harness-specific configuration
- generatePrompt: A function that takes the controlled sessions and config, and returns the system prompt for the Ghost
The default harness is the "Openfront Builder," which orchestrates building multiple Openfront verticals in parallel. You can add your own harnesses by adding entries to the HARNESS_REGISTRY array.