Documentation

Everything you need to know about VibeGhost, from installation to multi-session orchestration.

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

terminal
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.

On mobile, the input component includes a quick-keys toolbar with arrow keys, Ctrl+C, Tab, Esc, and other common terminal shortcuts. There's also a focus terminal button that opens the native keyboard for direct terminal input.

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 tab
Cmd+W Close tab
Cmd+N New window
Cmd+Q Quit browser
Cmd+1-9 Switch tabs
Cmd+C/V/X Copy/paste/cut
Cmd+Shift+] Next tab
Cmd+Shift+[ Previous tab
On Windows/Linux, replace Cmd with Ctrl. The shortcut detection is platform-aware.

Terminal shortcuts

Everything else goes to the terminal. Common ones include:

Ctrl+C Interrupt (SIGINT)
Ctrl+D EOF
Ctrl+Z Suspend (SIGTSTP)
Ctrl+L Clear screen
Ctrl+R Reverse search
Ctrl+A Start of line
Ctrl+E End of line
Ctrl+W Delete word

Cross-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"

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 other pi sessions and other sessions working on openfront.
  • 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:

Orange Modified
Green Added / Untracked
Red Deleted
Default Unchanged

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:

Ghost 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.