@bhd/react-web-terminal
v0.3.9
Published
React + xterm.js + node-pty terminal package with tmux backing, binary WS framing, scrollback replay
Downloads
1,248
Readme
@bhd/react-web-terminal
React + xterm.js + node-pty terminal package with tmux backing, binary WS
framing, scrollback replay, and a persistent terminal pool. Extracted
from goal-dashboard into a standalone private repo so any React app can
embed a real terminal that survives server restarts.
Private. Consumed via pinned git URL:
"@bhd/react-web-terminal": "git+https://github.com/buihongduc132/react-web-terminal.git#v0.1.0"
Entrypoints (LD2)
| Import | Runtime | Format | Use |
|---|---|---|---|
| @bhd/react-web-terminal/client | browser | ESM, "use client" banner (LD13) | React components (<TerminalDock/>) |
| @bhd/react-web-terminal/client/styles.css | browser | CSS | .rwt- namespaced styles (LD26) |
| @bhd/react-web-terminal/server | node | dual ESM + CJS (LD27), browser:false (LD12) | protocol, PTY session store, WS server |
| @bhd/react-web-terminal/next | node | ESM | registerTerminalServer() for instrumentation.ts |
Setup (consumer)
Install (npm ≥ 7 runs
prepareautomatically; pnpm needsenable-pre-post-scripts=truein.npmrc):npm install git+https://github.com/buihongduc132/react-web-terminal.git#v0.1.0 npm install @radix-ui/react-tabs # peer dep — LD25, you provide itBoot the WS server —
instrumentation.ts:import { registerTerminalServer } from "@bhd/react-web-terminal/next"; export async function register() { registerTerminalServer({ consumerSlug: "goal-dashboard", persistence: "tmux", // LD4 — exact-state survival allowedOrigins: [ // LD22 — hardcoded, never from Host "http://localhost:3001", "http://127.0.0.1:3001", ], }); }Render the dock —
app/layout.tsx:import { TerminalDock } from "@bhd/react-web-terminal/client"; import "@bhd/react-web-terminal/client/styles.css"; // AFTER your Tailwind import (LD26)
Cross-boundary import guard (LD12)
@bhd/react-web-terminal/server pulls in @homebridge/node-pty-prebuilt-multiarch,
a native .node binary. Importing it from the browser will crash the
Next.js/Turbopack build. Three layers of defense:
browser: falseinpackage.jsonexports map for./server— tells bundlers to never resolve the server entry in a browser build."use client"banner on./client(LD13) — keeps the client entry as a client component so xterm.js DOM access works under SSR.eslint-plugin-importno-restricted-paths(this package'seslint.config.js) — belt-and-suspenders: lints that client surfaces (src/components/**,src/client.ts) never import the native-dep server modules.
Which src/lib modules are off-limits to client surfaces:
| Module | Why |
|---|---|
| src/lib/pty-sessions.ts | require() of @homebridge/node-pty-prebuilt-multiarch |
| src/lib/terminal-server.ts | ws + pty-sessions |
| src/lib/tmux-backing.ts | spawns tmux child processes |
| src/lib/scrollback.ts | @xterm/headless + @xterm/addon-serialize (node-side) |
Which src/lib modules are explicitly shared (pure logic, no native deps):
| Module | Why it's safe |
|---|---|
| src/lib/protocol.ts | binary framing — pure byte math |
| src/lib/reconnect.ts | backoff + jitter math |
| src/lib/backpressure.ts | threshold math |
| src/lib/origin-guard.ts | origin string matching |
| src/lib/state-file.ts | path resolution + JSON |
NEVER import
/serverfrom a client component. The eslint rule above enforces this at lint time; thebrowser: falseguard enforces it at build time. Both are load-bearing — removing either will crash the consumer's browser build.
Locked decisions (LD1–LD31)
All 31 locked decisions from flow/findings/2026-07-21-terminal-refactor-explore/
are baked in. See the goal-dashboard AGENTS.md ## Source of truth section for
the full LD table. Headlines:
- LD5 — binary framing (1-byte type prefix:
0x00=data,0x01=control). No\x01sentinel. - LD4/LD6/LD10 — tmux backing:
gd_<id>session name,setsiddetach, SIGTERM handler that does not signal tmux children. - LD9 — WS backpressure via
socket.bufferedAmount(1MB threshold, pause PTY on overflow). - LD17 —
WebglAddonactive-pane only (DOM renderer for pooled inactive). - LD19/LD20 —
serialize({includeAltBuffer:true})scrollback replay; PTY paused during snapshot,{type:caught-up}marker. - LD22 — hardcoded Origin allowlist + per-page-load shared-secret token (
verifyClient). - LD31 — state file at
${XDG_STATE_HOME:-$HOME/.local/state}/<consumer-slug>/terminals.json, atomic write.
Build
npm install
npm run build # tsup (client ESM + server dual + next ESM) + copy assets
npm run typecheck # tsc --noEmit
npm test # vitest
npm run subset-font # regenerate public/fonts/meslolgs-nf-icons.woff2 (LD15; needs python fonttools)Native dependency (LD23)
Depends on @homebridge/node-pty-prebuilt-multiarch (NOT plain node-pty).
Prebuilts ship for linux-{x64,arm64}. macOS/Windows consumers need a build
toolchain (node-gyp + python3 + make + clang).
License
Private. Internal use only.
