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 |
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.
Managed mode launches ghosttead; external mode attaches to yours.
Control and frame traffic reach the renderer without exposing Node.
The runtime creates sessions and shares one render worker across panes.
1. Main process
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
import { ipcRenderer } from "electron";
import { forwardGhostteaRendererPorts } from "@vibecook/ghosttea-electron/preload";
forwardGhostteaRendererPorts(ipcRenderer);
3. Renderer runtime
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
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>
);
}
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
| 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
// 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
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
Makes the shared runtime available to TerminalSurface and GhostteaWorkspace.
TerminalSurface
Mounts one accessible canvas and invisible text input. The component handles keyboard, IME, pointer, selection, scrollbar, focus, and grid resize behavior.
GhostteaWorkspace
Adds persisted split panes, zoom, themes, terminal focus, Ghostty-style shortcuts, remote-session browsing, and an optional application sidebar.
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.
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.
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.
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.
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.
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.