A terminal emulator for the web.
wterm ("dub-term") renders to the DOM — native text selection, copy/paste, find, and accessibility work directly on the mounted rows. The core is written in Zig and compiled to WASM for near-native performance.
| Package | Description |
|---|---|
@wterm/core |
Headless WASM bridge, TerminalCore interface, WebSocket transport |
@wterm/dom |
DOM renderer, input handler — vanilla JS terminal |
@wterm/react |
React component + useTerminal hook (TypeScript) |
@wterm/vue |
Vue 3 component + template ref API |
@wterm/svelte |
Svelte component + callback API |
@wterm/ghostty |
Full-featured VT emulation core powered by libghostty |
@wterm/just-bash |
In-browser Bash shell powered by just-bash |
@wterm/markdown |
Render Markdown in the terminal |
- Pluggable cores — built-in lightweight Zig core (~12 KB) or opt-in libghostty backend (~400 KB) for full VT compliance
- Zig + WASM core — VT100/VT220/xterm escape sequence parser compiled to a ~12 KB
.wasmbinary (release build) - DOM rendering — native text selection, clipboard, browser find, and screen reader support for mounted rows
- Native hyperlinks — OSC 8 links remain attached to their exact cells through viewport and scrollback, with safe HTTP(S) anchors
- Dirty-row tracking — only touched rows are re-rendered each frame via
requestAnimationFrame - Frame-direct scheduling — writes queue their render on the next animation frame without an extra timer hop
- Synchronized output — mode 2026 blocks paint atomically with a bounded recovery deadline
- Themes — CSS custom properties with built-in Default, Solarized Dark, Monokai, and Light themes
- Alternate screen buffer —
vim,less,htop, and similar apps work correctly - Windowed scrollback history — configurable ring buffer with a bounded visible DOM window
- Wide Unicode cells — CJK, fullwidth, and emoji codepoints keep cursor-addressed redraws aligned
- Grapheme strings — the Ghostty core preserves combining marks and ZWJ emoji through the DOM renderer and scrollback
- Kitty terminal images — Ghostty-backed terminals render direct PNG/RGB/RGBA graphics in a scroll-aware canvas overlay with configurable display bounds; implicit image placements keep following prompts visually below the image
- 24-bit color — full RGB SGR support
- Auto-resize —
ResizeObserver-based terminal resizing - Framework bindings — React, Vue 3, and Svelte components
- WebSocket transport — connect to a PTY backend with binary framing and reconnection
- Mouse and focus reporting — DOM input for SGR mouse tracking and terminal focus events
- Kitty keyboard protocol: negotiated key disambiguation, event types, alternate keys, all-key reporting, and associated text
pnpm installzig buildFor a release build:
zig build -Doptimize=ReleaseSmallThe built binary is committed at packages/@wterm/core/wasm/wterm.wasm and CI fails if it does not match the Zig sources, so rebuild and commit it with any change under src/.
src/unicode_width_table.zig holds the East Asian Width ranges the core uses to decide cell width. It is generated, not hand-edited. Run this when Unicode publishes a new version, after bumping UNICODE_VERSION in the script:
node scripts/gen-unicode-width.mjspnpm buildThe docs use Geistdocs with content in apps/docs/content/docs. Existing URLs stay at the site root, including /get-started, /react, and /api-reference. The homepage keeps the interactive terminal, and Ask AI keeps the wterm chat interface.
pnpm exec turbo run build --filter='@wterm/docs^...'
pnpm --filter @wterm/docs devPortless prints the local URL for docs.wterm.localhost. Documentation search, per-page Markdown (/react.md), /llms.txt, and /sitemap.md share the same content source. Compatible browsers expose the read-only WebMCP tools search_docs and read_current_page; ordinary browsers need no experimental features. Configuration lives in apps/docs/src/lib/geistdocs/config.tsx.
With the server running, verify the public route contract with Node.js, using the exact URL printed by Portless:
NODE_EXTRA_CA_CERTS="$HOME/.portless/ca.pem" DOCS_TEST_URL=https://docs.wterm.localhost:1355 node --test apps/docs/tests/docs-routes.test.mjsCI also checks the production build with pnpm --filter @wterm/docs test:routes. This starts an isolated loopback server on an available port, runs the route suite, and shuts the server down. Build the docs first with pnpm --filter @wterm/docs build. Running the suite without a URL fails rather than silently skipping it.
For responsive browser checks, install agent-browser separately and run DOCS_TEST_URL=https://docs.wterm.localhost:1355 pnpm --filter @wterm/docs test:responsive. It checks narrow, intermediate, and desktop widths in both themes, top/middle/bottom scroll positions, and chat opening/closing without making model requests. Screenshots and measurements go to apps/docs/test-results/docs-responsive, or DOCS_ARTIFACT_DIR when set. This optional browser check is separate from the dependency-free Node route suite in CI.
The header uses the shared Geistdocs OSS navbar, including its product menus. The custom wterm Ask AI launcher uses the primary button style and floats centered below 640px, or at the lower right on wider screens. On mobile it aligns vertically with the footer theme controls and respects the device safe area. On wider screens its base right and bottom margins are 80% of the footer theme control's right inset. As that control approaches the viewport, the launcher gradually recovers its full right inset so their right edges align at the footer. The bottom margin increases when needed to leave at least 16px above the theme control. It returns to its base position when scrolling away and disappears while chat is open. It does not replace or modify the shared header. The mobile docs menu and sticky table-of-contents button remain separate. The existing chat modes remain: a full-screen sheet below 640px and a resizable side panel on wider screens. The side panel leaves at least 320px for the documentation and header. Responsive checks close chat with its explicit Close panel button; Escape in the mobile terminal remains a known issue.
To test WebMCP with agent-browser, use a separate browser session and the local URL printed by Portless:
export AGENT_BROWSER_SESSION=wterm-webmcp-test
agent-browser --headed open https://docs.wterm.localhost:1355
agent-browser webmcp list
agent-browser --json webmcp invoke search_docs --params '{"query":"WebSocketTransport"}'
agent-browser open https://docs.wterm.localhost:1355/react
agent-browser --json webmcp invoke read_current_page --params '{}'Both invocations should report data.status: "completed". Reading after navigation should return the React documentation, not the homepage. The tools are read-only; they do not navigate automatically, run terminal commands, or send data to a model.
The full search payload includes highlighting and page/heading/text matches for agents and the search UI. For a compact manual view with jq:
agent-browser --json webmcp invoke search_docs --params '{"query":"WebSocketTransport"}' |
jq -er 'if .success and .data.status == "completed" then .data.output[] | select(.type == "page" or .type == "heading") | [.content, .url] | @tsv else error("WebMCP search failed") end'When finished, run agent-browser close to close only the test session.
Ask AI still requires the KV rate-limit configuration and model access; it returns 503 when KV is not configured. Search and WebMCP do not require model credentials.
Serve the web/ directory with any static file server:
cd web && python3 -m http.server 8000For Kitty image support, use the Ghostty example instead. It loads
@wterm/ghostty, which provides the graphics-capable core; the built-in core
consumes unsupported Kitty APC payloads safely but does not decode images.
pnpm --filter ghostty-example devAll terminal graphics are transient browser/WASM memory. Direct media is
accepted only within the Ghostty image budget (32 MiB by default), with a
32 MiB hard cap per image and 4,096 resident image/placement records per
screen; the DOM overlay separately caps visible canvas backing stores at 32 MiB
and bounds each canvas to the terminal pixel area. Set imageStorageLimit: 0
to disable Ghostty graphics. File paths, shared memory, URLs, Sixel,
iTerm2, animation, and virtual placements are not loaded.
Kitty graphics clients can use the Ghostty example's browser terminal
directly; WTerm reports the viewport and cell pixel sizes required by
commands such as kitten icat --transfer-mode=stream image.png.
The local shell example also forwards the browser viewport dimensions to its
PTY, which lets kitten icat --detect-support work from that embedded shell.
All dev servers use portless to avoid hardcoded ports. Each app is served at a .localhost URL (e.g. nextjs-example.wterm.localhost).
cp web/wterm.wasm examples/nextjs/public/
pnpm --filter nextjs devThe Svelte example uses @wterm/svelte with an in-browser just-bash shell,
theme switching, and imperative terminal controls:
pnpm --filter svelte-example devIt opens at svelte-example.wterm.localhost through Portless.
zig build testOn macOS or Linux, test both cores against a real /bin/sh PTY and recorded
Neovim/tmux output in Chromium, Firefox, and WebKit:
pnpm exec playwright install chromium firefox webkit
pnpm test:ptyThe runner builds the terminal packages and owns an isolated server on an
available loopback port. Tests cover browser keyboard input, shell execution,
resize, and exit. Replay cases check Unicode, styles, alternate screens,
history, and synchronized output. JSON timing reports, a combined baseline.json,
and failure traces are saved under e2e/test-results/pty/. CI runs the suite
and uploads those artifacts. Playback uses checked-in bytes; Neovim and tmux
are only needed to regenerate the recordings.
Linux requires a C++/Python toolchain for the permitted node-pty native build.
For interactive checks, run pnpm --filter @internal/pty-harness dev after the
package build above. Portless prints the URL for pty-harness.wterm.localhost.
The harness includes core switching and a round-trip probe. Timing callbacks
measure frame opportunities, not physical display latency. See the
harness README for setup and measurement details.
With Zig 0.16.0 and the Playwright browsers installed, build a pinned, unpatched upstream WASM artifact and exercise its public C API in Node and all three browser engines:
pnpm test:libghosttyThe isolated libghostty experiment covers
render state, terminal effects, retained history, snapshots, and application
recordings. It documents the compatibility gaps that keep the shipped adapter
on v1.3.1. Generated binaries and reports stay in its ignored dist/ directory.
Apache-2.0