feat(web): replace xterm.js with ghostty-web - #3820
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
ApprovabilityVerdict: 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. |
b4665fe to
bb28ac9
Compare
9291c4c to
069f1dd
Compare
Co-authored-by: codex <codex@users.noreply.github.com>
069f1dd to
34c0fb2
Compare
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>
8eed30a to
c923244
Compare
c923244 to
40382ce
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ 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.
|
#4860. we'll keep the bundled fonts lean and aim to add configurable fonts in a follow up |

Working on #1310.
Swaps the thread terminal drawer from
@xterm/xtermto 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:
init(); a session buffer that arrives first is replayed once the terminal mounts.truereturn as "consumed, skip terminal input" — the opposite of xterm.js. Without this, all typing is swallowed.activate()callswindow.opendirectly, bypassing the app's preview/editor flow.alloc(0)returns an out-of-bounds pointer, sowrite("")throws aRangeErrordeep in WASM. Status-only session updates produced exactly that and took the app down; writes are now guarded. (Worth an upstream report.)viewportYsemantics are inverted (0 = bottom), so the at-bottom checks used for resize scroll restoration are adapted.@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 — includinglight:/dark:variants, resolved from the user themes dir or the Ghostty.app bundle) into an optionalServerConfig.terminalStylefield. 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
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-evalanddata:inconnect-srcfor 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
viewportYfor scroll-at-bottom, andwritePreservingScrollbackso scrolled-back history stays anchored on writes.Server best-effort reads local Ghostty config (
ghosttyStyle.ts) and exposes optionalServerConfig.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 andgit+transport URLs; non-http(s) links open externally.Focus on style-driven remounts uses
shouldFocusTerminalAfterSetupso 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
ThreadTerminalDrawer.tsx.ghosttyStyle.tsto read fonts, theme colors, and palette entries from the user's local Ghostty config; the result is forwarded to the client via a newterminalStylefield inServerConfig.git+ssh://) to the OS; strips leadinggit+transport prefixes before activation.wasm-unsafe-evaltoscript-srcanddata:toconnect-srcin the desktop Content Security Policy to support the WASM-based renderer.writePreservingScrollbackwrapper restores the previous scroll position, but this is new behavior that may surface edge cases.Macroscope summarized c405032.