Replace xterm.js with Ghostty libghostty-vt in the workspace terminal - #81
Conversation
Keep node-pty sessions, snapshots, and TERM=xterm-256color, and parse VT output with Ghostty's WASM engine plus an Aiden canvas surface. Co-authored-by: Sambit Biswas <sambitcreate@users.noreply.github.com>
Replace the incomplete custom VT canvas with T3's MIT runtime, write-pty trampoline, core snapshots, and surface (IME, scrollback, selection). Load WASM through fetch so packaged Electron file: builds work, and fail the production build if the wasm/font assets are missing. Co-authored-by: Sambit Biswas <sambitcreate@users.noreply.github.com>
T3's adapter uses replaceAll, Array.at, and toSorted, which fail tsc here. Co-authored-by: Sambit Biswas <sambitcreate@users.noreply.github.com>
Vite only copies new URL(..., import.meta.url) when the path is a compile-time literal. The runtime string argument never emitted ghostty-vt.wasm, so production verify and Electron E2E failed. Keep the 112-byte write-pty trampoline as a real file with ?no-inline. Co-authored-by: Sambit Biswas <sambitcreate@users.noreply.github.com>
Vite still needs compile-time new URL() to emit hashed wasm, but tsx rewrites those literals and ?no-inline made npm test fail after the production emit fix. Load vendor files from disk in Node, and keep the Vite URLs in wasm-assets.ts. Co-authored-by: Sambit Biswas <sambitcreate@users.noreply.github.com>
tsx rewrites static new URL(..., import.meta.url) to a cache directory that does not contain vendor/, so GhosttyRuntime.load() failed after the wasm-assets split. Keep Vite literals in wasm-assets.ts only. Co-authored-by: Sambit Biswas <sambitcreate@users.noreply.github.com>
5ffb3f2 to
6aafb7c
Compare
|
Readiness review complete with two independent GPT-5.6 Sol medium passes (merge/base history and adversarial edge cases). The branch was rebased onto current |
There was a problem hiding this comment.
Important
Command-modified keys that are not copy-with-selection or paste now fall through to Ghostty's key encoder and type literal characters into the shell on macOS (verified against the vendored wasm: Super+C encodes to c, Super+A to a; xterm.js sent nothing for Meta+key except Cmd+A select-all). Electron delivers keydown to the page before the application menu, so the Edit menu does not mask this.
Reviewed changes
- Terminal host swap —
renderer/components/terminal-drawer.tsxdynamically imports a newGhosttyTerminalSurface;@xterm/xtermand both addons are removed frompackage.jsonand the lockfile. - Vendored libghostty-vt stack — new
renderer/lib/ghostty-terminal/runtime, core, canvas renderer, input/selection surface, link matcher, theme, and key-code modules, plus the 112-byte PTY trampoline and Symbols Nerd Font. - Build and CSP plumbing —
'wasm-unsafe-eval'is added to the renderer CSP, Vite emits hashed wasm/font assets, and a new build step verifies they ship. - Contracts preserved — PTY ownership stays in
main/services/terminal.ts; browser-link routing, first-open focus, saved-scrollback clear, and a bounded accessible output mirror are wired through the drawer, with the e2e terminal spec extended.
ℹ️ surface.ts and terminal-links.ts ship without focused tests
The PR adds focused tests for core, keyCodes, runtime, and theme, but the two largest new modules — surface.ts (1931 lines of input, IME, selection, clipboard, and scrollbar logic) and terminal-links.ts (290 lines) — have none. The drawer test only greps source text and the e2e exercises a single typed command, so pure helpers like applyTerminalCopyEvent, terminalWheelDeltaRows, resolveTerminalMouseTrackingState, extractTerminalLinks, splitPathAndPosition, and resolvePathLinkTarget are unverified.
Technical details
# Missing focused coverage for new input and link modules
## Affected sites
- `renderer/lib/ghostty-terminal/surface.ts` — 1931 new lines; exported pure helpers have no direct test.
- `renderer/lib/ghostty-terminal/terminal-links.ts` — 290 new lines; no test file and not registered in `test:ghostty-terminal`.
- `renderer/lib/ghostty-terminal/runtime.test.ts:29` — the "parses VT output, and encodes Enter" test only asserts `ghostty_terminal_new` succeeds.
## Required outcome
- Pin the link matcher and the clipboard/scroll helpers with tests that can fail, or state explicitly that the e2e is the intended coverage boundary.
## Suggested approach (optional)
- Add `renderer/lib/ghostty-terminal/terminal-links.test.ts` and register it in the `test:ghostty-terminal` script next to the existing files.ℹ️ Nitpicks
pasteFromClipboard(renderer/lib/ghostty-terminal/surface.ts:920) and theonContextMenuoption (renderer/lib/ghostty-terminal/surface.ts:546) are unreachable here:terminal-drawer.tsxnever passesonContextMenu, so right-click has no menu and the helper is dead code.isTerminalCopyShortcut(renderer/lib/ghostty-terminal/surface.ts:333) dropped the upstream adapter's Ctrl+Insert branch whileisTerminalPasteShortcutstill handles Shift+Insert, so Ctrl+Insert with a selection now falls through to the encoder instead of copying.wasm-assets.tssays the module is loaded "through a dynamic import gated onwindow", butsurface.tsimports it statically; the gated dynamic import lives inruntime.tsand checksdocument.
DeepSeek Flash | 𝕏
|
Addressed all three Pullfrog findings at exact head
Verified locally: production build; real terminal Electron E2E; 14 focused Ghostty tests; renderer and E2E TypeScript; lint; diff check. Fresh exact-head CI, release-consumer, and Pullfrog checks are running. |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Suppressed unhandled macOS Command chords —
onKeyDownnow consultsisTerminalHostReservedShortcutafter the copy/paste paths and records the key code for keyup suppression, soCmd+Cwith no selection no longer encodes a literalcinto the shell. - Gated link affordances on the host navigation policy —
linkAtfilters both OSC 8 hyperlinks and detected matches through a new optionalcanActivateLinkpredicate, whichterminal-drawer.tsxwires tobrowserLinkCommand, removing the dead hover/click affordance on unsupported file-like text. - Added
surface.test.tsand registered it intest:ghostty-terminal; renamed theruntime.test.tscase so its title matches what it actually exercises. - Extended the terminal e2e spec with a
Meta+Cpress before the typed command, so a chord that leaks into the PTY fails the output assertions. - Documented the link-policy, Meta-chord, and Playwright-rebuild findings in
.papercuts/troubleshooting.md.
I verified the new suppression ordering directly: copy-with-selection and paste still run first, the metaKey guard then suppresses everything else and its keyup, and Ctrl chords remain terminal input. The link predicate exactly mirrors the activation handler's null check, so no activatable link loses its affordance. npm run test:ghostty-terminal passes locally (14 tests).
DeepSeek Flash | 𝕏

Summary
Replace the workspace terminal's xterm.js renderer with the official Ghostty
libghostty-vtWebAssembly engine while keeping Aiden's existing PTY ownership inmain/services/terminal.ts.The Ghostty browser surface is adapted from T3 Code's MIT-licensed adapter; vendored revision and notices are recorded in
renderer/lib/ghostty-terminal/VERSIONandTHIRD_PARTY_NOTICES.md.Validation
94c8a1bd: verification and deterministic Electron E2E passed.npm ci, fullnpm test, TypeScript checks, ESLint, 14 focused Ghostty tests, and production build passed locally.Review status
mainwith eight linear commits and no merge commits.