WebGPU · macOS · Windows · iOS · v0.4.0 · Electron 35+ · Node 22+ · Rust 1.88+

Use the highest layer that fits.

Start with the complete WebGPU Electron workspace on macOS or Windows. Drop down to terminal surfaces, automation, protocol, or native Metal composition on iOS only when your product needs it.

npm i @vibecook/ghosttea-electron @vibecook/ghosttea-react
#

Choose the highest layer that fits.

Most Electron products should begin with @vibecook/ghosttea-electron and @vibecook/ghosttea-react/workspace.

Goal Package Owns
Complete terminal workspace ghosttea-react/workspace Panes, input, persistence, renderer
Custom terminal UI ghosttea-react Surface and render worker
Electron integration ghosttea-electron Service lifecycle and frame bridge
Headless automation ghosttea-client Control socket only
Native service host ghosttea PTY sessions and service endpoints
Custom native model ghosttea-core Terminal state, effects, frames
One runtime per renderer window.

Share it through GhostteaProvider. Every mounted surface in that window then shares one worker and one GPU device.

#

Electron quickstart

ghosttea crosses Electron's three security boundaries deliberately. The main process owns the backend, preload forwards transferred ports, and the renderer owns the UI.

Start one backend

Managed mode launches ghosttead; external mode attaches to yours.

Transfer two ports

Control and frame traffic reach the renderer without exposing Node.

Mount the workspace

The runtime creates sessions and shares one render worker across panes.

1. Main process

main.ts
import { app, BrowserWindow } from "electron";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { GhostteaElectronBackend } from "@vibecook/ghosttea-electron/main";

await app.whenReady();

const daemonName = process.platform === "win32" ? "ghosttead.exe" : "ghosttead";
const preloadPath = fileURLToPath(new URL("./preload.cjs", import.meta.url));
const backend = new GhostteaElectronBackend({
  mode: "managed",
  daemon: {
    binary: {
      kind: "executable",
      path: join(process.resourcesPath, "bin", daemonName),
    },
  },
});

await backend.start();

const window = new BrowserWindow({
  webPreferences: {
    preload: preloadPath,
    contextIsolation: true,
    nodeIntegration: false,
    sandbox: true,
  },
});

window.webContents.on("did-finish-load", () => {
  backend.attachRenderer(window.webContents);
});

app.on("before-quit", () => backend.stop());

In development, the binary option may use { kind: "cargo", manifestPath, release }. Packaged applications should stage the release executable in their resources.

2. Preload

preload.ts
import { ipcRenderer } from "electron";
import { forwardGhostteaRendererPorts } from "@vibecook/ghosttea-electron/preload";

forwardGhostteaRendererPorts(ipcRenderer);

3. Renderer runtime

terminal.ts
import {
  createGhostteaTerminalRuntime,
  waitForGhostteaRendererPorts,
} from "@vibecook/ghosttea-react";
import "@vibecook/ghosttea-react/styles.css";
import "@vibecook/ghosttea-react/workspace.css";

export const terminalRuntime = createGhostteaTerminalRuntime({
  ports: waitForGhostteaRendererPorts(),
  clientBuild: "my-app",
  platform: {
    writeClipboard: (text) => window.app.writeClipboard(text),
    forceCanvasFallback: () => false,
    setForceCanvasFallback: () => {},
    reload: () => window.location.reload(),
  },
});

window.addEventListener(
  "beforeunload",
  () => terminalRuntime.dispose(),
  { once: true },
);

4. React workspace

App.tsx
import { GhostteaProvider } from "@vibecook/ghosttea-react";
import { GhostteaWorkspace } from "@vibecook/ghosttea-react/workspace";
import { terminalRuntime } from "./terminal";

export function App() {
  return (
    <GhostteaProvider runtime={terminalRuntime}>
      <GhostteaWorkspace
        platform={window.app.terminalPlatform}
        storageKey="my-app:terminal-workspace"
      />
    </GhostteaProvider>
  );
}
The platform adapter is intentionally yours.

It supplies the default shell, clipboard, context menu, window actions, and menu events. See the desktop adapter for a complete reference.

#

Electron API

@vibecook/ghosttea-electron owns service lifecycle, the utility-process bridge, and a control-only automation client.

GhostteaElectronBackend

new GhostteaElectronBackend(options: GhostteaElectronBackendOptions)
Member Purpose
start(): Promise<void> Starts or connects the daemon, automation client, and frame bridge.
attachRenderer(webContents) Transfers fresh control and frame ports into one renderer.
automation Returns the backend's control-only automation client.
connection Authenticated endpoint information for this backend.
running True when the service and bridge are ready.
stop() Stops owned clients and, in managed mode, the service process.

Managed vs. external

ownership modes
// ghosttea owns the process.
{ mode: "managed", daemon: { binary: { kind: "executable", path } } }

// Your application owns the Rust service.
{ mode: "external", connection: { controlSocket, frameSocket, authToken } }

External mode is the right choice when one Rust process shares a Truffle node or other infrastructure across several application services.

#

React API

@vibecook/ghosttea-react owns the renderer runtime, terminal surfaces, input routing, and worker-owned WebGPU renderer.

Runtime

createGhostteaTerminalRuntime(options: GhostteaTerminalRuntimeOptions): GhostteaTerminalRuntime

Use waitForGhostteaRendererPorts() for the ports transferred by the Electron preload. The runtime connects lazily and can be shared by any number of surfaces in one window.

Common methods

Method What it does
connect() Authenticates both renderer channels and negotiates protocol support.
createSession(options) Creates a PTY-backed local session.
listSessions() Returns the sessions visible to this service.
openRemoteSession(...) Creates a local replica of an advertised shared session.
setTheme(handle, theme) Updates native terminal colors and redraws affected surfaces.
terminate(id, source?) Requests classified session termination.
dispose() Closes ports, timers, subscriptions, and the render worker.

GhostteaProvider

<GhostteaProvider runtime={runtime}>…</GhostteaProvider>

Makes the shared runtime available to TerminalSurface and GhostteaWorkspace.

TerminalSurface

<TerminalSurface session={session} theme={theme} active visible />

Mounts one accessible canvas and invisible text input. The component handles keyboard, IME, pointer, selection, scrollbar, focus, and grid resize behavior.

GhostteaWorkspace

import { GhostteaWorkspace } from "@vibecook/ghosttea-react/workspace"

Adds persisted split panes, zoom, themes, terminal focus, Ghostty-style shortcuts, remote-session browsing, and an optional application sidebar.

Persist identity, not live state.

Workspace documents contain stable layout and session IDs. Resolve them against live sessions at startup; never put credentials or transport state in the document.

#

Node automation client

@vibecook/ghosttea-client controls sessions without Electron or frame rendering.

automation.mts
import { GhostteaAutomationClient } from "@vibecook/ghosttea-client";

const client = new GhostteaAutomationClient({
  controlSocket,
  authToken,
});

const session = await client.createSession({
  executable: "/bin/zsh",
  args: [],
  environment: {
    mode: "clean",
    variables: { PATH: "/usr/bin:/bin" },
  },
  cols: 120,
  rows: 40,
  persistence: "terminate-with-app",
});

const result = await client.pasteAndSubmit(
  session.id,
  "printf 'hello from ghosttea\\n'",
);

if (!result.accepted) {
  console.log("A human typed first; automation yielded.");
}

High-level methods

Lifecycle Input
createSession, listSessions pasteAndSubmit, paste
getSession, waitForExit sendText, interrupt
terminateAndWait, closeSessionOwner humanInputEpoch, input

The client never opens the frame socket, attaches a view, or claims resize authority.

#

Browser control client

@vibecook/ghosttea exports ControlClient, a typed request, notification, and event layer over a transferred MessagePort. It is used by the React runtime and is available for custom renderers.

new ControlClient(port: MessagePort)

Call request(command, timeoutMs?) for a response, notify(command) for fire-and-forget input, and dispose() when the renderer closes.

#

Protocol and frames

Use these packages only when building a custom client or renderer:

  • @vibecook/ghosttea-protocol — control commands, events, session types, and strict runtime guards.
  • @vibecook/ghosttea-frame — bounded decoder for binary TRF1 terminal frames and sections.
Current protocol versions

Control protocol 1.7; TRF1 frame protocol 1. Control packets are capped at 1 MiB and frame packets at 16 MiB.

Frame headers carry session, layout, sequence, and terminal revisions. Treat gaps as a request for an authoritative full snapshot rather than trying to merge around them.

#

Rust embedding

ghosttea is the transport-neutral service; ghosttea-core is the platform-neutral terminal model.

service.rs
use ghosttea::{TerminalService, TerminalServiceConfig};

TerminalService::new(TerminalServiceConfig {
    control_socket,
    frame_socket,
    auth_token,
})
.with_text_engine(text_engine)
.run()
.await?;

Applications that own endpoint permissions and startup order can bind listeners and call serve(TerminalServiceListeners). A host-owned TerminalMesh adds remote discovery and sessions without coupling the service to Truffle.

Published crates

Crate Role
ghosttea PTY sessions, endpoints, frame fanout, mesh contract
ghosttea-core Terminal model, effects, authority, logical replica, TRF1
ghosttea-vt Safe wrapper over the pinned Ghostty VT shim
ghosttea-text Font discovery, shaping, fallback, rasterization
ghosttea-truffle Application-owned Truffle transport adapter
ghosttea-vt-sys Verified native Ghostty artifact resolution
#

Native Apple stack

GhostteaKit targets iOS 18.1+ and macOS 14+. It composes the shared Rust core with Swift actors, Metal rendering, direct SSH, Truffle shared sessions, and a platform-neutral workspace model.

Module Use
GhostteaCore Safe Swift ownership over the versioned Rust C ABI
GhostteaSession Ordered transport, terminal, input, resize, and lifecycle actor
GhostteaTerminal Retained TRF1 state, Metal renderer, UIKit terminal view
GhostteaSSH Nonblocking libssh2 transport and security policy
GhostteaTruffle Discovery, shared-session attachment, local replica rendering
GhostteaWorkspace Secret-free tabs, pane reducers, restoration, and memory policy

Start with the GhostteaKit package guide. The production app is implemented and device-tested; external distribution qualification remains in progress.

#

Sessions

A local session owns one PTY, one Ghostty terminal model, one ordered input actor, and zero or more attached views. A remote session is a local logical replica of a terminal whose PTY remains authoritative on another host.

Environment policy

  • { mode: "inherit", overrides } — normal shell behavior with private ghosttea and transport variables removed.
  • { mode: "clean", variables } — an explicit environment for agents and isolated tools.

Every session summary includes identity, grid, executable, metadata, activity, and classified exit information.

#

Input and resize authority

Each attached view receives an attachment epoch. Input sequences deduplicate retries; control epochs reject stale resize owners; layout epochs keep frames aligned with the canonical PTY size.

Simple control rule

The most recently processed focus claim from a writable view controls terminal size. Writable views may still send input, which the session serializes in arrival order.

Automation does not attach a view or claim size. It commits only if no human input was accepted after the automation client observed the human-input epoch.

#

Security defaults

  • Renderer windows remain sandboxed with context isolation and no Node access.
  • Control and frame endpoints require the same random bearer token.
  • Unix sockets live under a private runtime directory.
  • Windows named pipes use a current-user-only protected DACL.
  • Private service and transport variables are removed from inherited PTYs.
  • Remote peers are read-only unless the host grants write access deliberately.
  • Apple credentials persist only as opaque, device-only Keychain references.

Local authentication is service-wide, not command-scoped. Applications that require per-client authorization should place a policy boundary in front of the service.

#

Platform matrix

Platform Status Renderer
Apple Silicon macOS Release-gated desktop target WebGPU
x64 Windows Release-gated desktop target WebGPU
iOS 18.1+ Production target; release hardening in progress Metal
Linux Service support; no published Ghostty VT bundle Host-dependent

Electron 35+ and React 19+ are supported peer ranges. Node clients require Node 22+; Rust crates declare Rust 1.88 as their minimum.