Skip to content

feat(web,desktop): replace xterm.js with ghostty-web#3820

Open
PixPMusic wants to merge 10 commits into
pingdotgg:mainfrom
PixPMusic:ghostty-web-terminal
Open

feat(web,desktop): replace xterm.js with ghostty-web#3820
PixPMusic wants to merge 10 commits into
pingdotgg:mainfrom
PixPMusic:ghostty-web-terminal

Conversation

@PixPMusic

@PixPMusic PixPMusic commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Working on #1310.

Swaps the thread terminal drawer from @xterm/xterm to ghostty-web, the libghostty-vt based drop-in mentioned in the issue. The server terminal protocol is a raw byte stream, so the change is contained to the client renderer — no server terminal changes.

Renderer swap

ghostty-web is close to API-compatible with xterm.js, but a few behaviors differ and are adapted here:

  • WASM init: terminal creation waits on init(); a session buffer that arrives first is replayed once the terminal mounts.
  • Custom key handler semantics are inverted: ghostty-web treats a true return as "consumed, skip terminal input" — the opposite of xterm.js. Without this, all typing is swallowed.
  • Link providers: rows/ranges are converted between the app's 1-based link helpers and ghostty-web's 0-based coordinates. The built-in OSC8/URL providers are removed since their activate() calls window.open directly, bypassing the app's preview/editor flow.
  • Zero-length write crash: ghostty-web 0.4.0's alloc(0) returns an out-of-bounds pointer, so write("") throws a RangeError deep in WASM. Status-only session updates produced exactly that and took the app down; writes are now guarded. (Worth an upstream report.)
  • viewportY semantics are inverted (0 = bottom), so the at-bottom checks used for resize scroll restoration are adapted.
  • Scrollbar/selection: ghostty-web renders its own canvas scrollbar and selection; the xterm-specific scrollbar theme keys and CSS import are gone, and @xterm/* dependencies are removed.

Ghostty config passthrough

Since the terminal engine is now Ghostty's, the drawer can match the user's actual Ghostty setup: the server parses ~/.config/ghostty/config (font-family, font-size, theme — including light:/dark: variants, resolved from the user themes dir or the Ghostty.app bundle) into an optional ServerConfig.terminalStyle field. The drawer overlays those colors on the app-derived theme and prepends the configured fonts. Everything is best-effort: no Ghostty config means the previous defaults exactly.

Nerd Font families are appended to the font fallback stack so powerline/devicon glyphs resolve from locally installed patched fonts (Ghostty embeds its own symbols fallback; browser canvas only walks the CSS font list), and the terminal container is painted with the theme background so partial-cell fringes match the canvas during drawer resizing.

I like this approach, but I'll remove it if asked--I could also split it out into a separate PR.

Known limitations

  • Runtime theme switching only updates default fg/bg/cursor — cell colors are baked in WASM at write time (ghostty-web limitation).
  • Ghostty config changes are picked up on client reload, not watched live.

Note

Medium Risk
Large client terminal refactor with WASM/CSP requirements and behavioral differences from xterm; mitigated by tests and fallbacks when Ghostty config is missing.

Overview
Replaces the thread terminal drawer renderer from @xterm/xterm with ghostty-web (WASM), including async init(), inverted custom-key-handler semantics, 0-based link coordinates, and guards against empty writes that crash 0.4.0. writePreservingScrollback keeps scroll position when output arrives while scrolled back; shouldFocusTerminalAfterSetup avoids focus theft on style-driven remounts and in split view.

The server best-effort loads local Ghostty config (ghosttyStyle.ts) and exposes optional ServerConfig.terminalStyle (fonts, sizes, light/dark palettes); the drawer merges that with app theme, Nerd Font fallbacks, and expanded link schemes (mailto, ssh, etc.). Desktop CSP adds wasm-unsafe-eval and data: for WASM; xterm CSS/deps are removed.

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

Note

Replace xterm.js with ghostty-web as the web terminal renderer

  • Swaps the @xterm/xterm and @xterm/addon-fit dependencies for ghostty-web (v0.4.0), which uses a WASM module initialized once on first terminal mount.
  • Adds ghosttyStyle utilities on the server to parse the user's local Ghostty config (fonts, colors, palette) and expose a terminalStyle field in the ServerConfig payload so the web terminal can adopt the user's Ghostty appearance.
  • Implements writePreservingScrollback so terminal output no longer snaps the viewport to the bottom when the user has scrolled up.
  • Disables ghostty-web's built-in link providers and installs the app's own link routing in their place.
  • Risk: wasm-unsafe-eval and data: must be added to the Electron Content Security Policy to allow WASM compilation and ghostty-web's embedded WASM payload fetch.

Macroscope summarized 8eed30a.

@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

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

Run ID: edf6ffbd-620f-49a6-a137-ee916cde2236

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
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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 size:XL 500-999 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list. labels Jul 9, 2026
Comment thread apps/web/src/components/ThreadTerminalDrawer.tsx
Comment thread apps/web/src/components/ThreadTerminalDrawer.tsx Outdated
Comment thread apps/server/src/terminal/ghosttyStyle.ts Outdated
Comment thread apps/web/src/components/ThreadTerminalDrawer.tsx
Comment thread apps/web/src/components/ThreadTerminalDrawer.tsx
@PixPMusic PixPMusic changed the title Replace xterm.js with ghostty-web in the web terminal Replace xterm.js with ghostty-web in the web/desktop terminal Jul 9, 2026
@macroscopeapp

macroscopeapp Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

Replaces the core terminal emulator library (xterm.js → ghostty-web), introduces security-relevant CSP changes for WebAssembly, and adds new functionality to load user's local Ghostty theme/font configuration. This major infrastructure change warrants human review.

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

Comment thread apps/server/src/terminal/ghosttyStyle.ts Outdated
Comment thread apps/web/src/components/ThreadTerminalDrawer.tsx Outdated
@PixPMusic PixPMusic changed the title Replace xterm.js with ghostty-web in the web/desktop terminal feat(web,desktop): replace xterm.js with ghostty-web in the terminal Jul 9, 2026
Comment thread apps/web/src/components/ThreadTerminalDrawer.tsx Outdated
Comment thread apps/web/src/components/ThreadTerminalDrawer.tsx
Comment thread apps/web/src/components/ThreadTerminalDrawer.tsx
Comment thread apps/server/src/terminal/ghosttyStyle.ts Outdated
@PixPMusic PixPMusic changed the title feat(web,desktop): replace xterm.js with ghostty-web in the terminal feat(web,desktop): replace xterm.js with ghostty-web Jul 9, 2026
@PixPMusic
PixPMusic force-pushed the ghostty-web-terminal branch from b4665fe to bb28ac9 Compare July 9, 2026 06:18
Comment thread apps/web/src/components/ThreadTerminalDrawer.tsx
Comment thread apps/server/src/terminal/ghosttyStyle.ts
Comment thread apps/server/src/terminal/ghosttyStyle.ts Outdated
Comment thread apps/web/src/components/ThreadTerminalDrawer.tsx
Comment thread apps/web/src/components/ThreadTerminalDrawer.tsx Outdated
Comment thread apps/web/src/components/ThreadTerminalDrawer.tsx Outdated
Comment thread apps/server/src/terminal/ghosttyStyle.ts Outdated
@juliusmarminge
juliusmarminge self-requested a review July 20, 2026 08:54
Comment thread apps/web/src/components/ThreadTerminalDrawer.tsx Outdated
Comment thread apps/server/src/terminal/ghosttyStyle.ts
Comment thread apps/server/src/terminal/ghosttyStyle.ts
Comment thread apps/server/src/terminal/ghosttyStyle.ts
Comment thread apps/server/src/terminal/ghosttyStyle.ts
Comment thread apps/web/src/components/ThreadTerminalDrawer.tsx
PixPMusic and others added 9 commits July 20, 2026 18:05
Swap the thread terminal drawer from @xterm/xterm to ghostty-web, the
libghostty-vt based drop-in. The server terminal protocol is unchanged;
the swap is contained to the client renderer:

- Gate terminal creation on the ghostty-web WASM init and replay the
  session buffer that arrives before it resolves
- Invert custom key handler returns (ghostty-web treats true as
  "consumed", the opposite of xterm.js)
- Convert link provider rows/ranges between the 1-based terminal-links
  helpers and ghostty-web's 0-based coordinates, and drop the built-in
  providers whose activate() bypasses the app's preview flow
- Guard zero-length writes: ghostty-web 0.4.0 alloc(0) returns an
  out-of-bounds pointer and crashes on status-only session updates
- Adapt at-bottom checks to ghostty-web's inverted viewportY semantics

Also style the terminal from the user's local Ghostty config when one
exists: the server parses font-family/font-size/theme (resolving named
themes from the user themes dir or the Ghostty.app bundle) into an
optional ServerConfig.terminalStyle field, and the drawer applies it
over the app-derived defaults. Nerd Font fallbacks cover powerline and
devicon glyphs, and the mount is painted with the theme background so
partial-cell fringes match the canvas.
The desktop protocol's Content-Security-Policy blocked the ghostty-web
terminal from initializing: script-src had no 'wasm-unsafe-eval', so
WebAssembly compilation was rejected, and connect-src had no data:, so
the library's embedded WASM payload fetch failed. Add both sources.
- Retry WASM init on a later mount instead of caching a rejected
  promise until page reload
- Blur non-active terminals after open(): ghostty-web focuses
  unconditionally, letting the last-mounted split steal focus
- Seed the session snapshot with version 0 and a closed status at mount
  so an already-exited session still surfaces its exit and closes the
  viewport
- Keep CSS generic font keywords (monospace, system-ui, ...) unquoted
  when building the terminal font stack
- Resolve named Ghostty themes from the Linux system dirs
  (/usr/share/ghostty/themes, /usr/local/share/ghostty/themes) too
- Attach a rejection handler to the terminal WASM init so a load
  failure logs, shows a message in the viewport, and clears on retry
  instead of leaving a blank pane and an unhandled rejection
- Treat an empty XDG_CONFIG_HOME as unset per the XDG spec instead of
  resolving the Ghostty config relative to the server cwd
- Bump a terminal epoch when the deferred WASM setup finishes so the
  session sync effect re-runs against the live terminal even when no
  session field changed during the load, completing the exited-at-mount
  handling
- Read config.ghostty as an alternate config file name and merge the
  XDG and macOS Application Support configs in Ghostty's load order
  instead of stopping at the first existing file
- Preserve a scrolled-back viewport when session output arrives:
  ghostty-web snaps to the bottom on every write, so re-anchor the
  viewport by the number of lines the write pushed into scrollback
- Read both config and config.ghostty when they coexist in a Ghostty
  config directory instead of letting the first shadow the second
- Linkify mailto/ftp/ssh/git/tel/magnet/gemini/gopher/news URLs in the
  terminal and open non-http schemes with the OS handler, keeping the
  in-app preview flow for http(s)
- Once scrollback is full its length stops growing while writes keep
  evicting lines, so the preserved viewport drifted toward the tail;
  fall back to counting written newlines at the cap
- Parse config.ghostty before config so config wins conflicts, matching
  Ghostty's own load order
Co-authored-by: codex <codex@users.noreply.github.com>
Co-authored-by: codex <codex@users.noreply.github.com>
Comment thread apps/server/src/terminal/ghosttyStyle.ts Outdated

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 34c0fb2. Configure here.

Comment thread apps/web/src/components/ThreadTerminalDrawer.tsx
Comment thread apps/web/src/components/ThreadTerminalDrawer.tsx
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL 500-999 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.

2 participants