Skip to content

Replace xterm.js with Ghostty libghostty-vt in the workspace terminal - #81

Merged
sambitcreate merged 8 commits into
mainfrom
cursor/libghostty-terminal-4228
Sep 11, 2026
Merged

Replace xterm.js with Ghostty libghostty-vt in the workspace terminal#81
sambitcreate merged 8 commits into
mainfrom
cursor/libghostty-terminal-4228

Conversation

@sambitcreate

@sambitcreate sambitcreate commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Summary

Replace the workspace terminal's xterm.js renderer with the official Ghostty libghostty-vt WebAssembly engine while keeping Aiden's existing PTY ownership in main/services/terminal.ts.

  • Vendors the pinned Ghostty ABI, PTY callback trampoline, Canvas 2D renderer, IME/selection/scrollback surface, and Symbols Nerd Font Mono.
  • Loads emitted WASM assets in the sandboxed Electron renderer and verifies the WASM/font artifacts during production builds.
  • Preserves current Aiden Browser routing for validated terminal links, semantic terminal selection styling, and all current release/test contracts.
  • Recovers from import/font/WASM/canvas initialization failures by cleaning the partial surface and closing the unusable PTY session.
  • Focuses newly created terminals after asynchronous initialization, clears saved scrollback securely, applies Aiden's complete 256-color terminal palette, and exposes a bounded VoiceOver output mirror.
  • Keeps host-reserved Meta/Command chords out of the PTY after terminal copy/paste handling, and shares Browser command eligibility with terminal hover/activation so unsupported source-like text has no dead link affordance.

The Ghostty browser surface is adapted from T3 Code's MIT-licensed adapter; vendored revision and notices are recorded in renderer/lib/ghostty-terminal/VERSION and THIRD_PARTY_NOTICES.md.

Validation

  • Exact-head CI is green at 94c8a1bd: verification and deterministic Electron E2E passed.
  • Exact-head release-consumer contract passed; Pullfrog is green.
  • Lockfile-exact npm ci, full npm test, TypeScript checks, ESLint, 14 focused Ghostty tests, and production build passed locally.
  • Real Electron PTY coverage verifies automatic first-open focus, shell input/output persistence, Cmd+C with no selection does not type into the shell, accessible output, saved-scrollback clearing, and drawer lifecycle.
  • Android correctly skipped because this PR changes neither native clients nor shared server/transcript contracts.

Review status

  • Rebased onto current main with eight linear commits and no merge commits.
  • GitHub reports CLEAN and MERGEABLE.
  • Zero unresolved review threads.

cursoragent and others added 7 commits September 10, 2026 23:05
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>
@sambitcreate
sambitcreate force-pushed the cursor/libghostty-terminal-4228 branch from 5ffb3f2 to 6aafb7c Compare September 11, 2026 03:24
@sambitcreate

Copy link
Copy Markdown
Owner Author

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 main without merge commits and hardened at 6aafb7c038a52ad381ff5eadd1ff624fe90accc0: preserved validated Browser link routing; added initialization-failure cleanup/session closure; fixed first-open async focus; made Clear erase saved scrollback; applied Aiden’s full 256-color palette; added a bounded VoiceOver output mirror; and reconciled current semantic selection/radius contracts. Local lockfile install, full tests, type checks, lint, build, and focused real-PTY Electron coverage pass. Exact-head CI https://github.com/sambitcreate/aiden-agent/actions/runs/34558316908 and release-consumer validation https://github.com/sambitcreate/aiden-agent/actions/runs/34558316825 are green. GitHub reports CLEAN/MERGEABLE with zero unresolved review threads.

@sambitcreate
sambitcreate marked this pull request as ready for review September 11, 2026 03:36

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 swaprenderer/components/terminal-drawer.tsx dynamically imports a new GhosttyTerminalSurface; @xterm/xterm and both addons are removed from package.json and 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 the onContextMenu option (renderer/lib/ghostty-terminal/surface.ts:546) are unreachable here: terminal-drawer.tsx never passes onContextMenu, 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 while isTerminalPasteShortcut still handles Shift+Insert, so Ctrl+Insert with a selection now falls through to the encoder instead of copying.
  • wasm-assets.ts says the module is loaded "through a dynamic import gated on window", but surface.ts imports it statically; the gated dynamic import lives in runtime.ts and checks document.

Pullfrog  | Fix all ➔Fix 👍s ➔View workflow run | Using DeepSeek Flash𝕏

Comment thread renderer/lib/ghostty-terminal/surface.ts
Comment thread renderer/lib/ghostty-terminal/runtime.test.ts Outdated
Comment thread renderer/components/terminal-drawer.tsx
@sambitcreate

Copy link
Copy Markdown
Owner Author

Addressed all three Pullfrog findings at exact head 94c8a1bd4add2db5f64c81b820d914a10a4f441f:

  • Unhandled Meta/Command chords are now suppressed after terminal-specific copy/paste handling, so Cmd+C without a selection cannot type c into the shell; Ctrl chords remain terminal input. A real macOS Electron PTY regression sends Cmd+C before a shell command and passes.
  • Hover/underline and activation now share the same browserLinkCommand eligibility predicate, so unsupported source-like text such as .ts:12 is no longer advertised as clickable.
  • The runtime test title now accurately states its asserted C-ABI coverage, and focused surface policy tests are registered in test:ghostty-terminal.

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.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

  • Suppressed unhandled macOS Command chords — onKeyDown now consults isTerminalHostReservedShortcut after the copy/paste paths and records the key code for keyup suppression, so Cmd+C with no selection no longer encodes a literal c into the shell.
  • Gated link affordances on the host navigation policy — linkAt filters both OSC 8 hyperlinks and detected matches through a new optional canActivateLink predicate, which terminal-drawer.tsx wires to browserLinkCommand, removing the dead hover/click affordance on unsupported file-like text.
  • Added surface.test.ts and registered it in test:ghostty-terminal; renamed the runtime.test.ts case so its title matches what it actually exercises.
  • Extended the terminal e2e spec with a Meta+C press 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).

Pullfrog  | View workflow run | Using DeepSeek Flash𝕏

@sambitcreate
sambitcreate merged commit a2725be into main Sep 11, 2026
6 checks passed
@sambitcreate
sambitcreate deleted the cursor/libghostty-terminal-4228 branch September 11, 2026 16:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants