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 Ghost orchestration 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)
Pasting Images
VibeGhost supports direct image pasting into terminal sessions. This is particularly useful when working with multimodal AI agents that can "see" your UI or debug visual issues.
How it works
To add an image to your session, simply drag and drop the image directly into the terminal window or the input area. On desktop, you can also use standard Cmd/Ctrl+V to paste from your clipboard.
Just like VibeTunnel, VibeGhost saves images to a control directory at:
The absolute path to the saved image is then sent to the terminal so the agent can access it.
Mobile Support
If you're running VibeGhost on your phone, you can still paste images directly using the Paste button in the quick-keys toolbar (above the agent input).
Tapping this button opens a dialog where you can paste your image directly. Once pasted, the image is processed and its path is sent to the agent just like on desktop.
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 Orchestrator
The Ghost orchestrator enables multi-session control. A single Ghost terminal session can read other sessions, send them work, and coordinate progress across your workspace.
How it works
When you invoke a Ghost (via the InvokeGhostDialog), you select which sessions it should control. VibeGhost creates a local workspace in ~/.vibeghost/ghost, writes Ghost's local instruction and memory files, starts a shell in that workspace, and sends pi @BOOTSTRAP_PROMPT.md to start the orchestrator.
Ghost now relies on local files such as GHOST.md, SOUL.md, MEMORY.md, SESSION_CONTROL.md, CONTROLLED_SESSIONS.md, and BOOTSTRAP_PROMPT.md. It can interact with controlled sessions through the local Ghost API:
# List sessions
curl -s 'http://127.0.0.1:4021/api/ghost?action=list'
# Read terminal output
curl -s 'http://127.0.0.1:4021/api/ghost?action=read&sessionId=SESSION_ID'
# Send text or a command to a session
curl -s -X POST 'http://127.0.0.1:4021/api/ghost' \
-H 'Content-Type: application/json' \
-d '{"action":"send","sessionId":"SESSION_ID","text":"your message or command\n"}'
# Wait for a session to become idle
curl -s 'http://127.0.0.1:4021/api/ghost?action=wait&sessionId=SESSION_ID'A trailing newline in the text payload acts like pressing Enter. That lets Ghost submit commands or messages the same way the dedicated input box does in the session UI.
Heartbeat
Heartbeat is the lightweight supervision loop around Ghost. In a nutshell, Ghost is one terminal controlling many others, and heartbeat is the simple cron that periodically asks Ghost how those controlled terminals are doing.
Ghost is the real orchestrator. It owns memory, judgment, supervision policy, and session interventions. Heartbeat is only a lightweight supervision trigger that asks Ghost to do another normal pass; heartbeat does not supervise sessions directly, does not define supervision policy, and is not intended to become a second orchestrator.
When heartbeat is enabled, VibeGhost only sends the supervision message if the Ghost orchestrator is idle, meaning nothing is currently streaming in that terminal. If Ghost is already active, the heartbeat tick is skipped.
Every heartbeat run also creates a file under ~/.vibeghost/ghost/heartbeats. The prompt sent to Ghost tells it to update that file with blockers, worker questions it answered, sessions that look complete and ready for review, or the fact that heartbeat nudged the workspace forward and kept the terminals running.
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, so Ghost can tell whether a session is ready for input or already busy:
- 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.