Skip to content

feat(web): render terminals with libghostty-vt - #4860

Merged
juliusmarminge merged 54 commits into
pingdotgg:mainfrom
StiensWout:agent/ghostty-web-terminal
Jul 31, 2026
Merged

juliusmarminge merged 54 commits into
pingdotgg:mainfrom
StiensWout:agent/ghostty-web-terminal

Conversation

@StiensWout

@StiensWout StiensWout commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor

Ready for review and feedback.

Closes #1310.

What changed

Replaces the web terminal's xterm.js parser and renderer with a T3-owned browser adapter over the official libghostty-vt C ABI.

The implementation stays deliberately close to mobile:

  • builds web and Android from the same canonical Ghostty revision
  • uses Ghostty terminal, render-state, selection, scrollback, key and mouse encoders, paste, hyperlink, and PTY-reply APIs
  • renders Ghostty cell snapshots into Canvas 2D with style runs and dirty-row updates
  • keeps browser-only behavior (IME textarea, Canvas, clipboard, link activation, and sizing) at the platform boundary
  • shares one WASM runtime per browser tab while retaining and freeing per-terminal handles
  • leaves the existing PTY/WebSocket contracts unchanged for local, remote, relay, and tunnel connections

A 112-byte companion WASM trampoline connects Ghostty's official write_pty callback to JavaScript through Ghostty's exported function table. This preserves terminal-generated replies without depending on Ghostty's unstable Zig API. Captured-session restore temporarily detaches that callback so historical device queries cannot emit replies into the current shell.

The reproducible build script reads mobile's canonical libghostty-vt/VERSION, checks out that exact upstream revision, and vendors the WASM artifact, license, and version. The production build emits separate cacheable assets:

  • ghostty-vt.wasm: 630.9 KB raw / 187.9 KB gzip
  • ghostty-write-pty.wasm: 112 bytes

This is an independent implementation of the worthwhile idea behind #3820, avoiding the lifecycle and input limitations of the third-party ghostty-web wrapper.

Expected benefits

  • one official Ghostty terminal model across Android and web
  • stronger grapheme, Unicode, VT, mouse, selection, hyperlink, and modern keyboard-protocol behavior
  • no dependency on an unmaintained or single-maintainer web wrapper
  • a narrow adapter T3 can profile and evolve without an xterm compatibility facade
  • platform rendering remains replaceable without changing server or transport contracts

Review fixes and reliability pass

All review findings to date are fixed and their threads resolved:

  • application mouse reporting uses Ghostty's official mouse encoder; hover motion is only reported in any-event tracking (DEC 1003), and Shift plus the platform link modifier retain browser behavior
  • attaching a viewport to a session that already exited runs exit handling exactly once, matching the old xterm mount
  • key releases for presses handled locally (keybindings, copy/paste shortcuts, AltGraph text) are swallowed so Kitty report-event-types sessions never see an orphan release

A review-driven polish pass hardened the rest of the adapter:

  • wheel deltas are normalized by deltaMode with fractional accumulation, and wheel motion on the alternate screen becomes arrow keys (honoring DECCKM) so vim/less scroll without mouse tracking
  • drag selections are anchored in Ghostty screen coordinates (streaming output cannot shift the origin) and dragging past the canvas edge autoscrolls into scrollback
  • the canvas refits when devicePixelRatio changes; cell metrics wait for the bundled webfonts; the font stack falls back to locally installed Nerd Font faces for prompt glyphs and devicons
  • the cursor blink restarts from the visible phase on writes, and an unfocused surface shows a steady hollow cursor
  • the IME textarea follows the terminal cursor so candidate windows appear where the user types; Safari's keyCode 229 composition keydown is guarded
  • selection colors come from the app theme with the xterm-parity overlays and no forced foreground; the canvas gets a text cursor and a pointer over link gestures
  • ABI error paths free temporaries on every throwing path, opaque slots are zeroed before first use, and grapheme codepoints are read without an alignment assumption
  • the drawer re-applies the theme after async WASM mount, sends one PTY resize RPC per grid change, and anchors the selection-action menu from Ghostty cell geometry

Validation

  • rebased onto current main
  • focused web tests: 45 passing across the Ghostty ABI, key codes, renderer, surface behavior, and terminal drawer, including vendored-WASM regressions for DEC 1003 mode queries and Kitty release encoding under CSI > 3 u
  • web typecheck, focused lint, and formatting
  • browser QA through real PTYs for mouse SGR bytes, word/line selection actions, OSC 8 activation, scrollback, Unicode/IME, and responsive rendering
  • Electron QA for IME, split lifecycle, and Vim with no page errors
  • production web, desktop, and server builds used for integrated QA
  • reproducible Zig 0.15.2 WASM build from the mobile-pinned Ghostty revision

images

01-terminal-git-graph-nerd-fonts-dark 02-selection-actions-dark 03-selection-light-theme 04-vim-alt-screen-dark 05-htop-dark
06-ghostty-terminal-demo.webm

Feedback on the adapter boundary, checked-in artifacts, callback trampoline, and Canvas 2D as the initial renderer would be especially useful.

Implemented by GPT-5.6-sol through the Codex harness in T3 Code, with review fixes and the reliability pass by Claude Fable 5 through Claude Code.

Note

Replace xterm.js with libghostty-vt WebAssembly for web terminal rendering

  • Replaces the xterm.js terminal renderer with a Canvas 2D renderer backed by libghostty-vt compiled to WebAssembly, aligning the web terminal with the existing Android implementation.
  • Adds GhosttyRuntime to load and interact with the WASM module, GhosttyTerminalCore to manage terminal state and I/O, and renderGhosttySnapshot to paint dirty rows to a canvas with cursor and selection support.
  • Adds a build script to compile and vendor ghostty-vt.wasm and ghostty-write-pty.wasm from the pinned Ghostty revision.
  • Rewrites TerminalViewport to use GhosttyTerminalSurface for resize, selection, copy, link activation, and exit handling.
  • Risk: CSP on the desktop app now permits 'wasm-unsafe-eval' in script-src to allow WASM execution.

Macroscope summarized 0054869.


Note

High Risk
Large renderer swap with async WASM startup, new CSP for WASM, and deep input/selection/mouse behavior; PTY transport is unchanged but terminal UX regressions are the main risk.

Overview
Replaces xterm.js in the web app with a Canvas 2D stack built on vendored libghostty-vt WebAssembly, aligned with the Android terminal’s Ghostty revision and C ABI.

Adds a reproducible Zig build (build-libghostty-wasm.sh) for ghostty-vt.wasm and a tiny PTY callback trampoline, plus modules for runtime loading, terminal core, rendering, and browser input/selection/IME/links. ThreadTerminalDrawer now mounts GhosttyTerminalSurface asynchronously (theme, resize, selection, exit handling) and drops xterm dependencies and CSS.

Desktop CSP gains wasm-unsafe-eval for WASM. ChatView widens terminal ID allocation so drawer and panel terminals cannot collide on one PTY. Third-party notices document the shared Ghostty pin.

Reviewed by Cursor Bugbot for commit 0b3c1fc. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Replace xterm.js with libghostty-vt WebAssembly terminal renderer in the web app

  • Adds a complete Ghostty WASM terminal stack: runtime.ts wraps the WASM ABI, core.ts manages terminal state, renderer.ts paints frames to a canvas, and surface.ts handles input, fonts, scrollbar, and layout.
  • Refactors ThreadTerminalDrawer.tsx to use GhosttyTerminalSurface instead of xterm.js, with async creation, onResize-driven PTY sizing, and theme updates via setTheme.
  • Removes @xterm/xterm and @xterm/addon-fit dependencies; adds a build script that downloads Zig and compiles ghostty-vt.wasm and ghostty-write-pty.wasm from the same pinned Ghostty revision used by the mobile app.
  • Adds 'wasm-unsafe-eval' to the desktop Electron Content-Security-Policy to allow WASM execution.
  • Fixes terminal ID allocation in ChatView to use the union of all known terminal ID sets when creating or splitting terminals.
  • Risk: WASM execution requires 'wasm-unsafe-eval' in CSP; the custom scrollbar and canvas cursor replace all previous xterm-derived styles.

Macroscope summarized 0b3c1fc.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f5e0dab7-0bb9-4e07-8cf7-c0cd346dbba3

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:XXL 1,000+ changed lines (additions + deletions). labels Jul 29, 2026
Comment thread apps/web/src/terminal/ghostty/core.ts Resolved
Comment thread apps/web/src/terminal/ghostty/core.ts Resolved
Comment thread apps/web/src/terminal/ghostty/core.ts Resolved
Comment thread apps/web/src/terminal/ghostty/core.ts Resolved
@macroscopeapp

ghost commented Jul 29, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

Diff is too large for automated approval analysis. A human reviewer should evaluate this PR.

You can customize Macroscope's approvability policy. Learn more.

@StiensWout
StiensWout force-pushed the agent/ghostty-web-terminal branch from 9a3b61d to 92bd4ec Compare July 29, 2026 21:02
@StiensWout StiensWout changed the title [WIP] feat(web): render terminals with libghostty-vt feat(web): render terminals with libghostty-vt Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Investigate integrating libghostty

2 participants