Skip to content

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

Closed
PixPMusic wants to merge 15 commits into
pingdotgg:mainfrom
PixPMusic:ghostty-web-terminal
Closed

feat(web): replace xterm.js with ghostty-web#3820
PixPMusic wants to merge 15 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 renderer swap with WASM/CSP changes and new server file reads; terminal I/O protocol unchanged, but regressions in input, links, scrollback, or focus are plausible in a critical UI surface.

Overview
Replaces @xterm/xterm with ghostty-web in the thread terminal drawer. The renderer waits on WASM init and bundled Symbols Nerd Font Mono before mounting; session output that arrives early is replayed once the terminal exists.

Electron CSP adds wasm-unsafe-eval and data: in connect-src for ghostty-web’s WASM and embedded payload.

The drawer adapts ghostty-web vs xterm: inverted custom key-handler semantics, 0-based link row/range conversion, clearing built-in link providers so URLs go through preview/editor, guarding zero-length writes, inverted viewportY for scroll-at-bottom, and writePreservingScrollback so scrolled-back history stays anchored on writes.

Server best-effort reads local Ghostty config (ghosttyStyle.ts) and exposes optional ServerConfig.terminalStyle (fonts, size, light/dark colors). The client overlays that on the app theme and prepends configured font families. Link detection expands to more URI schemes and git+ transport URLs; non-http(s) links open externally.

Focus on style-driven remounts uses shouldFocusTerminalAfterSetup so split views and the composer don’t lose focus.

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

Note

Replace xterm.js with ghostty-web in the web terminal

  • Swaps the terminal renderer from xterm.js to ghostty-web, initializing a WASM module before terminal creation in ThreadTerminalDrawer.tsx.
  • Adds server-side Ghostty config parsing in ghosttyStyle.ts to read fonts, theme colors, and palette entries from the user's local Ghostty config; the result is forwarded to the client via a new terminalStyle field in ServerConfig.
  • Bundles Symbols Nerd Font Mono as a WOFF2 fallback font for icon/glyph rendering in the terminal.
  • Routes http(s) links through the in-app preview while delegating other schemes (e.g. git+ssh://) to the OS; strips leading git+ transport prefixes before activation.
  • Adds wasm-unsafe-eval to script-src and data: to connect-src in the desktop Content Security Policy to support the WASM-based renderer.
  • Risk: ghostty-web snaps the viewport to the bottom on every write; a writePreservingScrollback wrapper restores the previous scroll position, but this is new behavior that may surface edge cases.

Macroscope summarized c405032.

@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 Plus

Run ID: 501ceddd-9005-49f5-9097-690d09e7a356

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

This PR replaces the terminal emulator library (xterm.js → ghostty-web) and adds new server-side Ghostty config parsing for user terminal theming. The scope includes CSP security policy changes, new contracts, and complex WebAssembly initialization — substantial new functionality warranting 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
juliusmarminge added a commit to PixPMusic/t3code that referenced this pull request Jul 20, 2026
Co-authored-by: codex <codex@users.noreply.github.com>
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 feat(web,desktop): replace xterm.js with ghostty-web feat(web): replace xterm.js with ghostty-web Jul 25, 2026
@PixPMusic PixPMusic changed the title feat(web): replace xterm.js with ghostty-web feat(web,desktop): replace xterm.js with ghostty-web Jul 25, 2026
@PixPMusic PixPMusic changed the title feat(web,desktop): replace xterm.js with ghostty-web feat(web): replace xterm.js with ghostty-web Jul 25, 2026
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.
PixPMusic and others added 9 commits July 30, 2026 20:06
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>
@PixPMusic
PixPMusic force-pushed the ghostty-web-terminal branch from 8eed30a to c923244 Compare July 31, 2026 03:16
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 force-pushed the ghostty-web-terminal branch from c923244 to 40382ce Compare July 31, 2026 03:29
Comment thread apps/web/src/terminal-links.ts
Comment thread apps/web/src/terminal-links.ts

@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 1 potential issue.

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 ca084e1. Configure here.

Comment thread apps/web/src/terminal-links.ts Outdated
@juliusmarminge

Copy link
Copy Markdown
Member

#4860. ghosttty-web felt kinda unmaintained and we dont "gained" anything from the added xterm compatibility.

we'll keep the bundled fonts lean and aim to add configurable fonts in a follow up

@PixPMusic PixPMusic closed this Jul 31, 2026
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