Skip to content

feat: OSC 133 shell integration, OSC 22 cursor, focus events, sync output, headless mode, WASM 1.3, Playwright E2E suite#169

Open
diegosouzapw wants to merge 37 commits into
coder:mainfrom
diegosouzapw:main
Open

feat: OSC 133 shell integration, OSC 22 cursor, focus events, sync output, headless mode, WASM 1.3, Playwright E2E suite#169
diegosouzapw wants to merge 37 commits into
coder:mainfrom
diegosouzapw:main

Conversation

@diegosouzapw
Copy link
Copy Markdown

Thank you for ghostty-web 🙏

First and foremost: thank you to everyone at Coder and all contributors to coder/ghostty-web. This library is one of the most exciting open-source terminal projects I've come across — the combination of Ghostty's battle-tested VT engine with a clean TypeScript API is exactly what the web terminal ecosystem needed.


About this PR

I maintain a fork — diegosouzapw/ghostty-web — where I've been building features I need for a project called OmniRoute, an open-source platform that provides free access to LLM models. I plan to embed ghostty-web as the terminal component inside OmniRoute, and when we launch, coder/ghostty-web will be prominently credited on our inspiration/credits page.

Everything we built in this fork was inspired by, or directly builds on top of, the work already done here. We've tried to give back by:

  • Adding Co-authored-by + Inspired-by trailers in every commit that ports upstream work
  • Commenting on the PRs/issues that inspired us
  • Sending this PR with the original contributions back upstream

What's in this PR

New events & terminal modes

Feature API Mode
Shell integration onPromptStart, onCommandStart, onCommandEnd OSC 133 A/C/D
Cursor shape onMouseCursorChange OSC 22
Focus tracking \x1b[I / \x1b[O on focus/blur DEC mode 1004
Synchronized output Deferred canvas render, 500ms force-flush DEC mode 2026

Headless mode

TerminalCore base class + ghostty-web/headless entry point: full VT parsing, buffer access, all events — no DOM or canvas required. Mirrors the @xterm/headless API.

Ghostty 1.3 WASM upgrade

Replaces the 1 738-line custom shim with a 133-line patch. New structured C API, row/cell iterators, WAT-based callback trampolines, kitty graphics support.

Playwright E2E test suite

81 tests across 9 spec files covering every public API method, event, and user interaction. Runs against the live demo page via Chromium headless.

01-rendering.spec.ts   — canvas, ANSI, cursor, wide chars, alt screen (13 tests)
02-keyboard.spec.ts    — input(), onData, disableStdin, onKey (5 tests)
03-scroll.spec.ts      — scrollToTop/Bottom/Lines/Pages, onScroll, wheel (8 tests)
04-selection.spec.ts   — select, selectAll, clearSelection, drag (7 tests)
05-resize.spec.ts      — resize(), onResize, FitAddon (6 tests)
06-events.spec.ts      — all 14 event types including OSC 133/22/mode 1004 (14 tests)
07-theme-options.spec.ts — theme, fontSize, cursorBlink, clear/reset (9 tests)
08-addons.spec.ts      — loadAddon, FitAddon lifecycle (5 tests)
09-lifecycle.spec.ts   — write/writeln, buffer, getCell, markers, unicode (14 tests)

Bug fixes & security

  • IME textarea repositioned to cursor coordinates on every render frame (fixes [DEMO]CJK Input position issue in DEMO #97)
  • Demo RCE closed: unauthenticated cross-origin WebSocket + /dist/ path traversal
  • WASM page buffers zero-initialized (prevents memory corruption)
  • Viewport corruption from page memory reuse
  • Ghost cursor at (0,0) on init + ESC k title leak
  • Font metrics DPR alignment, URL parentheses detection, wide-char copy, and more

Docs

  • CHANGELOG.md from v0.1.0 → v0.4.1 with full contributor attribution
  • README rewritten with full API reference, headless examples, and events table

A note on conflicts

Some commits in this PR port features that may already be in your main (e.g., WASM 1.3, powerline rendering). Feel free to cherry-pick only the original contributions if a full merge isn't clean — the goal is to give back, not to create work for you.

Thank you again for building ghostty-web. We're excited to use it in OmniRoute and will make sure the credit is where it belongs. 🚀

diegosouzapw and others added 30 commits May 23, 2026 03:24
Reserves .agents/, .claude/, .worktrees/, and _tasks/ for local-only
artefacts used by the /port-upstream-prs and /resolve-upstream-issues
slash commands. None of these paths should ever be tracked.
scrollback_limit is passed to Ghostty's Terminal.max_scrollback, which is
in bytes. The low-level GhosttyTerminalConfig / TerminalConfig docs
described it as "number of scrollback lines", which is misleading — a
caller passing 10,000 expecting lines gets 10,000 bytes and falls below
the 2-page PageList floor.

Only the low-level docstrings are corrected here. The xterm.js-compat
ITerminalOptions.scrollback field still inherits xterm.js-compat framing
and a misleadingly xterm.js-shaped default (1000) despite plumbing
directly to a bytes-valued field; fixing that properly requires a
lines-to-bytes conversion and a separate PR.


Inspired-by: coder#151

Co-authored-by: Sauyon Lee <git@sjle.co>
URLs containing parentheses — such as Wikipedia links like
https://en.wikipedia.org/wiki/Rust_(programming_language) — were
incorrectly truncated. The URL regex character class excluded `(` and
`)`, so the match stopped at the first parenthesis. Additionally,
TRAILING_PUNCTUATION unconditionally stripped `)`, breaking URLs where
parentheses are part of the path.

Fix:
- Add `()` to the URL regex character class so parentheses are captured
- Remove `)` from TRAILING_PUNCTUATION to preserve balanced parens
- Add a balanced-paren stripping pass: only strip trailing `)` when the
  URL has more closes than opens (e.g. URL wrapped in surrounding parens)

Adds four unit tests covering Wikipedia paths, wrapped URLs, multiple
parenthesized path segments, and nested parentheses.


Inspired-by: coder#152

Co-authored-by: eric-jy-park <2019147551@yonsei.ac.kr>
…ive (#4)

When a TUI application enables mouse tracking (modes 1000/1002/1003),
wheel events were being intercepted by the Terminal-level capture
handler and converted to arrow key sequences, losing the mouse position.
This meant applications like tmux or vim with split panes could not
determine which zone the cursor was over, causing the wrong pane to
scroll.

Now, when mouse tracking is active, Terminal forwards wheel events to
InputHandler.handleWheelEvent() which sends proper SGR/X10 mouse
sequences with cell coordinates (button 64/65 for scroll up/down).


Inspired-by: coder#136

Co-authored-by: David Gageot <david.gageot@docker.com>
…h traversal in /dist/ (#15)

The demo PTY server (demo/bin/demo.js) had three combined issues that
chained into Remote Code Execution against any user running
\`npx @ghostty-web/demo@next\`:

1. **No Origin / Host check on /ws upgrade.** Any web page the user
   visited could open the WebSocket and pipe arbitrary input to the
   shell. The code even acknowledged this with a TODO: "In production,
   consider validating req.headers.origin". A reporter published a
   full PoC that scanned 127.0.0.1:1-10000 from the browser in <30s,
   found the demo, and ran a payload.
2. **Bound to 0.0.0.0 by default.** \`httpServer.listen(HTTP_PORT)\`
   without a host argument means all interfaces — so the PTY was also
   reachable from the LAN, not just from local browsers.
3. **Pre-existing path traversal in /dist/.** \`/dist/<x>\` did
   \`path.join(distPath, pathname.slice(6))\`, letting a request like
   \`/dist/../../etc/passwd\` escape distPath and read any file the
   server process could.

Fixes:

- Bind explicitly to \`127.0.0.1\` by default (\`LISTEN_HOST\` env can
  opt back into \`0.0.0.0\` for users who genuinely want remote
  access). Both production mode and Vite dev mode are updated.
- Allowlist WebSocket Origins: only \`http(s)://localhost:PORT\`,
  \`http(s)://127.0.0.1:PORT\`, \`http(s)://[::1]:PORT\`. When the user
  opted into a non-loopback bind, the actual bound hostname is also
  accepted. Missing/empty Origin is rejected (curl-style direct
  clients are not a demo use case and would bypass the CSRF defense).
  Rejected upgrades are answered with HTTP 403 and a console.warn
  pointing operators at the HOST escape hatch.
- For /dist/ static serving, resolve the joined path with
  \`path.resolve\` and require the result to stay inside distPath
  (\`startsWith(distRoot)\`). Returns HTTP 403 on escape attempts.
  Semgrep continues to flag the call site as user-input-into-resolve
  — false positive, the startsWith guard validates the result.

Smoke test (PORT=8099 node demo/bin/demo.js):

- ss confirms bind on 127.0.0.1 only (no 0.0.0.0)
- Origin: http://localhost:8099   → HTTP 101 (upgrade succeeds)
- Origin: http://evil.example     → HTTP 403 (rejected)
- (no Origin header)              → HTTP 403 (rejected)
- GET /dist/../../etc/passwd      → blocked (403 or normalized away)
- GET / and /dist/ghostty-web.js  → HTTP 200 (legitimate flows still work)

Reported-by: therealcoiffeur (coder#160)
…nt seams (#6)

When devicePixelRatio is non-integer (e.g. 1.25, 1.5, 1.75 from browser
zoom or HiDPI displays), rounding cell width/height to the nearest CSS
pixel with Math.ceil() produces fractional *physical* pixel coordinates
at cell edges.

The canvas rasterizer antialiases clearRect/fillRect calls at those
sub-pixel boundaries. With alpha:true on the canvas (enabled in coder#93 for
transparent backgrounds), the resulting partially-transparent edge
pixels composite against the page background and appear as thin black
seams between rows and columns.

Fix: round up to the nearest *device* pixel instead of CSS pixel. The
+2/+1 paddings for glyph overflow stay in CSS units before the DPR
multiplication so they scale correctly.

Ports only the font-metrics subset of upstream PR coder#146 — the rest of
that PR bundles a substantial render-loop refactor (startRenderLoop →
scheduleRender) and several perf caches whose risk/benefit needs
separate evaluation against our current architecture.


Inspired-by: coder#146

Co-authored-by: tommyme <chris.b.you@qq.com>
Add a focusOnOpen boolean option (default: true) that controls whether
the terminal automatically focuses itself when open() is called.
Setting it to false lets embedders open a terminal in the background
without stealing keyboard focus from another element.

Resolves coder#100.


Inspired-by: coder#149

Co-authored-by: Sauyon Lee <git@sjle.co>
…#5)

When the user has scrolled into the scrollback and new output arrives,
the legacy xterm.js-style behaviour auto-scrolls back to the bottom
(losing the user's reading position). Modern terminals (kitty, alacritty)
instead lock the viewport on the same content so the user keeps reading
where they were.

This commit ports the upstream fix as a new opt-in option:

- Add `preserveScrollOnWrite?: boolean` to `ITerminalOptions`
  (default: `false`, preserves the current xterm.js-compat behaviour).
- When `true`, save `viewportY` and `getScrollbackLength()` before the
  WASM write, compute the scrollback delta after, and shift `viewportY`
  by that delta — clamped to the new scrollback length in case old lines
  were dropped by the scrollback limit. Re-fires `scrollEmitter` and
  briefly shows the scrollbar when the viewport actually shifts.
- Add two regression tests covering both behaviours.

This is an adaptation of upstream PR coder#150 (which made the new behaviour
unconditional). Resolves coder#127 (request to make
auto-scroll configurable).


Inspired-by: coder#150

Co-authored-by: Sauyon Lee <git@sjle.co>
When the selection range covered text containing wide characters
(CJK, fullwidth Latin, etc.), copying the selection inserted a stray
space between every wide glyph — e.g. "안녕하" came out as "안 녕 하 ".

Root cause: wide characters occupy two terminal cells. The first cell
has the codepoint and width=2; the second cell is a continuation
marker with codepoint=0 and width=0. SelectionManager.getSelection's
empty-cell branch treated both empty cells AND continuation cells the
same way and appended a space.

Fix: skip continuation cells (cell exists with width===0) in the
empty-cell branch. Only truly empty cells (no cell, or cell.width!==0
with codepoint===0) get a space.

Ports only the selection-manager subset of upstream PR coder#120 — the rest
of that PR (IME composition routing, textarea-focus refactor, removal
of contenteditable) needs more analysis around regressions with browser
extensions and is deferred to a separate port.

Adds one regression test asserting that selecting "안녕하" copies as
"안녕하", not "안 녕 하".


Inspired-by: coder#120

Co-authored-by: Seungwoo Hong <ai.baryon.ai@gmail.com>
Adds ImagePasteAddon, a new addon following the ITerminalAddon pattern
(same as FitAddon), that detects image data in clipboard paste events
and emits them as base64-encoded payloads via an onImagePaste event.

Also updates InputHandler.handlePaste to only claim paste events that
contain text. Paste events without text (e.g. image-only) are no longer
consumed by the default handler, allowing them to bubble through to
addons like ImagePasteAddon.

The core Terminal API stays strictly xterm.js-conformant — no custom
events on the Terminal class.

Public API additions in lib/index.ts:
- ImagePasteAddon class
- IImagePasteData type

Usage:

  import { Terminal, ImagePasteAddon } from 'ghostty-web';
  const term = new Terminal();
  const addon = new ImagePasteAddon();
  term.loadAddon(addon);
  addon.onImagePaste((data) => { /* data.name, data.dataBase64 */ });

Adapts the import order to satisfy our Biome organizeImports rule
(value imports before type-only imports).


Inspired-by: coder#143

Co-authored-by: Brian Egan <brian.egan@verygood.ventures>
…eplies (#7)

Some embedders answer terminal queries (DSR cursor position, device
attributes, etc.) at the PTY boundary or server-side mux layer. In those
integrations, renderer-generated replies race with or duplicate the
server-side replies and — because they are emitted through onData — are
indistinguishable from real user input to the PTY.

This commit adds an opt-in option:

- emitTerminalResponses?: boolean on ITerminalOptions (default: true).
  Preserves the current behaviour — parser-generated replies flow
  through onData as before.
- When false, the terminal still parses and renders queries normally,
  but processTerminalResponses() is skipped on each write so onData
  carries only user-keyboard input.

Includes two regression tests covering both option values via \\x1b[5n
(DSR "are you there?").


Inspired-by: coder#165

Co-authored-by: assim <hello@assim.me>
… (#16)

Bundles two renderer/parser fixes that don't share scope:

**coder#122 — ghost cursor at (0,0) on init.** The renderer skipped redrawing
the previous cursor row when the new cursor stayed on the SAME row as
the previous frame. The cursor-line redraw at the top of the cursor-
moved block only fires for the NEW cursor row; the symmetric branch
for the OLD cursor row had a `lastCursorPosition.y !== cursor.y` guard
that skipped same-row moves and an `!isRowDirty` guard that skipped any
move where the regular dirty pass was already going to redraw the row.
The combination left a stale cursor glyph at the initial (0,0) position
whenever later content moved the cursor on the same row via positional
sequences (no cell content changing on row 0). Always redrawing the
previous cursor row on cursorMoved is a trivial extra-render cost and
guarantees the ghost is erased.

**coder#153 — ESC k title sequence leaks onto the grid.** Ghostty WASM
(commit 5714ed07) does not consume `ESC k <text> ESC \` — the GNU
screen / tmux title-setting extension. The parser logs `unimplemented
ESC action: ESC k` and then prints `<text>` onto the grid, also
consuming the trailing `ESC \`. Same for the BEL-terminated variant.
We pre-filter input in `Terminal.write` to strip ESC k sequences before
they reach WASM. Implemented for both `string` and `Uint8Array` (the
Uint8Array path does a single-pass byte scan and only allocates when a
sequence is actually found). OSC 0/1/2 title-setting (`ESC ] …`) is
untouched and continues to be consumed by the WASM parser as before.

Adds four regression tests for the title-set behaviour (string input
with ST, BEL terminator, OSC 0 untouched, Uint8Array equivalence).
The cursor-ghost fix is structural and cannot be asserted in a
headless render context; manual smoke test pending in bun run dev.

Reported-by: mats16 (coder#122)
Reported-by: Fisher-Wang (coder#153)
…tion (#12)

Ghostty allocates page buffers via the wasm allocator (which calls
wasm_alloc -> memory.grow). New memory pages on grow are zero on most
runtimes but the cell layout depends on this being EXPLICITLY true:
some cell fields are inspected before the first write, and a stray
non-zero byte can corrupt the screen state in ways that surface much
later (wrong colors, stuck cursor, dropped grapheme clusters).

Patches/ghostty-wasm-api.patch is updated so the underlying buffer
arrays are explicitly memset-to-zero at construction. Adds two TS-side
regression tests that exercise the corrupted-render shape.


Inspired-by: coder#142

Co-authored-by: Sauyon Lee <git@sjle.co>
…alt screen (#13)

Three independent PTY-input gaps wrapped in a single port from upstream
coder#147 because they share the same WASM-side patch surface.

1. **DECSCUSR (cursor shape)** — apps that send the `CSI Ps SP q` DECSCUSR
   sequence to change cursor shape (block/bar/underline) and blink state
   used to be silently ignored on the JS side. WASM now exports
   `render_state_get_cursor_style` and `render_state_get_cursor_blinking`;
   the renderer queries them each frame so vim/tmux insert-mode cursor
   styles take effect.

2. **Ctrl+V forwarding** — Ctrl+V used to be intercepted and dropped so
   the browser paste event could handle it. That broke apps that read
   raw \\x16 from the PTY (e.g. opencode triggering osascript image
   paste). Ctrl+V now emits \\x16 via the Ghostty key encoder AND still
   lets the paste event fire for text content. Cmd+V on macOS behaves
   as before (no byte emitted, paste event handles it).

3. **Mouse scroll in alt screen** — wheel events while in the alt screen
   buffer (vim, less, htop) used to bypass mouse-tracking. Now they go
   through the same mouse-tracking path as the main screen, so apps that
   subscribe to wheel events receive them in alt screen too.

WASM-API patch updates:

- New exports for cursor_style / cursor_blinking
- Hunk headers in patches/ghostty-wasm-api.patch recounted to reflect
  the added lines (the original patch upstream had stale @@ headers
  that prevented `git apply` from succeeding)

The two pre-existing "Ctrl+V/Cmd+V should not emit onData" tests were
documenting the old (now-incorrect) behaviour and have been rewritten
to assert the new contract: Ctrl+V → \\x16, Cmd+V → empty (encoder
returns no bytes for Super modifier).


Inspired-by: coder#147

Co-authored-by: Jesse Peng <jesse23@gmail.com>
IME composition events (compositionstart / compositionupdate /
compositionend) fire on the focused element. ghostty-web focuses a
hidden textarea for keyboard input, but composition listeners were
attached to the container element — so every Korean / Chinese / Japanese
input event was missed.

This commit:

- Moves composition listeners from `container` to `inputElement`
  (textarea) when the input element is available. Detach is also
  retargeted to the same element so disposal is symmetric.
- Adds a state machine to handle the "terminating key" of an IME
  composition (space, period, etc.). The key is queued during
  composition and replayed after compositionend so the composed text
  appears before the terminator.
- Removes `contenteditable="true"` from the parent container. Having
  contenteditable on the container caused IME text to be inserted as
  text nodes in the container, bypassing the textarea entirely. The
  textarea is itself a real input element, so most browser extensions
  (Vimium, etc.) leave it alone — this should not regress the
  motivation behind coder#78, but needs verification in real browsers.
- Sets `tabindex="-1"` on the parent so it is no longer click/tab
  focusable. Redirects parent mousedown and focus events to the
  textarea so any focus eventually lands on the input element.
- Updates `Terminal.focus()` to target the textarea instead of the
  container, with the same delayed-focus backup behaviour.

Differences from upstream PR coder#120 (deliberate):

- The composition-preview overlay (a div with hardcoded Korean text
  "조합중:" and `#ffcc00` on dark background) is intentionally NOT
  ported. Native browsers already render IME composition feedback,
  and the upstream overlay was both untranslated and theme-hostile.
- The selection-manager wide-char fix from that PR was already
  shipped separately as #120a.


Inspired-by: coder#120

Co-authored-by: Seungwoo Hong <ai.baryon.ai@gmail.com>
…routing

PR #11 routes focus to the hidden textarea inside the container rather than
to the container element itself. Accept either as valid.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Deletes the "printable character" and "simple special keys" fast paths
from InputHandler.handleKeyDown and routes every keydown through the
Ghostty WASM key encoder.

The old fast paths were a simplified model that diverged from both
xterm.js and Ghostty's encoder for several keys:

- Home and End ignored DECCKM (application cursor mode). xterm.js emits
  \\x1b[H in normal mode and \\x1bOH in application mode; the fast path
  emitted \\x1b[H always.
- Shift+Home / Shift+End / Shift+PageUp / Shift+PageDown / Shift+F1..F12
  dropped the Shift modifier. xterm.js encodes it into the CSI sequence
  (e.g. \\x1b[1;2H for Shift+Home); the fast path emitted the plain
  unmodified sequence.

Going through the encoder also unlocks Kitty keyboard protocol and xterm
modifyOtherKeys state 2 when applications enable them.

Two intentional differences from xterm.js are documented in README.md:

- Shift+Enter is distinguishable from Enter (\\x1b[27;2;13~ via fixterms
  rather than bare \\r), so modern line editors and REPLs can treat
  Shift+Enter as newline-without-submit.
- Kitty keyboard protocol and xterm modifyOtherKeys state 2 are
  supported when apps enable them.

If embedders need byte-for-byte xterm.js behaviour for a specific key,
they can intercept via attachCustomKeyEventHandler and emit the bytes
they want with term.input(bytes, true).


Inspired-by: coder#159

Co-authored-by: Sauyon Lee <git@sjle.co>
…heme (#14)

Today, the theme passed to the Terminal constructor is captured at open()
time and never changes. Apps that need to switch themes at runtime (light/
dark toggle, accessibility preference change, multi-window state) had to
dispose the Terminal and recreate it — which destroys scrollback,
selection, and focus.

This commit adds a runtime theme change path:

- Public API: `Terminal.setTheme(theme)` updates the theme atomically and
  triggers a single render. Equivalent to assigning `options.theme = ...`
  via the existing options Proxy (also supported).
- WASM bridge: new exports `terminal_set_theme` (full theme update) and
  the renderer is invalidated so the next frame redraws every cell with
  the new palette / background.
- The renderer's color cache (introduced in older PRs) is cleared on
  theme change so old `rgb(...)` strings don't outlive their palette.

Adds 12 new tests covering: full-theme update mid-session, ANSI palette
update, default-color fallback when theme omits ansi colors, no-op on
identical theme, render scheduling, options-proxy compatibility.

Excludes the binary `ghostty-vt.wasm` from the upstream diff (CI / local
`bun run build:wasm` rebuilds it from the updated patch).


Inspired-by: coder#144

Co-authored-by: Brian Egan <brian.egan@verygood.ventures>
The terminal renders via a permanent requestAnimationFrame loop. When a
browser tab is partially backgrounded, when the frame budget is tight,
or when rAF is otherwise deferred, the gap between sending a keystroke
and seeing the echo can grow to a full frame longer than the PTY
round-trip — typing feels sluggish.

Track an `awaitingEcho` flag that is set whenever input is emitted via
`onData` (keyboard input handler callback, paste, public `input(data,
true)` API). When the next `write()` arrives, presumed to carry the
echo bytes from the PTY, the terminal does a synchronous render right
after the WASM write instead of waiting for the next rAF tick.

After the synchronous render the dirty rows are cleared, so the rAF
loop has nothing to redraw on its next tick — no double-paint cost.
The flag only fires once per user input → no impact on bulk stdout.

Adds a regression test that monkey-patches `renderer.render` to count
synchronous calls. `term.input('x', true)` followed by `term.write('x')`
must increment the counter (synchronous render fired); a subsequent
`term.write(...)` without prior input must NOT (flag was cleared).

Reported-by: ruoso (coder#161)
…rebase

Regenerated with git diff --recount after dynamic theme rebase introduced
stale @@ counts from the merge conflict resolution.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…18)

Fixes two keyboard issues from upstream coder#109:

1. Shift+Tab now produces the standard backtab sequence (CSI Z / \x1b[Z)
   via the Ghostty encoder with SHIFT modifier on Key.TAB.

2. Alt+letter on macOS: Alt transforms event.key to Unicode (Alt+T → '†').
   The encoder now receives the correct letter by deriving utf8 from
   event.code (KeyT → 't') when altKey is set and event.key is non-ASCII.

3. Enable ALT_ESC_PREFIX (DEC mode 1036) by default so the encoder emits
   ESC+letter for Alt-modified keys, matching xterm metaSendsEscape
   default behavior.

Inspired by: coder#109

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
)

Ports fixes for upstream issues coder#138 and coder#139:

Issue coder#139 (viewport corruption when viewport spans multiple pages):
- renderStateGetViewport: replace per-row pages.pin(.active) calls with
  cached row pins from RenderState.row_data, matching the native renderer.
  Independent per-row pin resolution produced inconsistent results across
  page boundaries.
- terminal_new_with_config: convert scrollback_limit from line count to
  bytes using page layout calculation (Terminal.init expects bytes, not
  lines). This makes the page-spanning condition much less frequent.

Issue coder#138 (stale cell data visible after scroll with default cursor style):
- cursorDownScroll in Screen.zig: make row clearing unconditional. The old
  check `if (bg_color != .none)` skipped clearing when cursor style was
  default (after ESC[0m), leaving stale cells from reused page memory
  visible on empty lines.

Inspired by: coder#133
Inspired by: coder#134

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
- Upgrade @happy-dom/global-registrator from ^15.11.0 to 20.9.0
- Pin rollup to 3.30.0 and postcss to 8.5.10 via overrides
- Add { url: 'http://localhost/' } to GlobalRegistrator.register()
  (required by happy-dom v20 API)
- Add .devcontainer/ to .gitignore
- Fix pre-existing biome lint issues in viewport/iris test files
  (import order, Number.parseInt)


Inspired-by: coder#167

Co-authored-by: Brent Rockwood <brent@brentrockwood.com>
Before the first call to write() (or after reset()), render a blank
canvas filled with the theme background colour instead of showing
a transparent/black frame. This eliminates the flash of unstyled
content on open().

- Add parseCssColorToRgb() for hex and rgb() CSS colour parsing
- Add createBlankBootstrapCells() to build a dummy blank frame
- Add bootstrapBuffer proxy IRenderable that delegates to WASM once
  bootstrapCells is null
- armBootstrapBlank(): sets bootstrapCells from current theme on open()/reset()
- disarmBootstrapBlank(): clears bootstrapCells on first write()
- All render paths now use bootstrapBuffer instead of wasmTerm directly
- Fix pre-existing biome lint issues in viewport/iris test files


Inspired-by: coder#154

Co-authored-by: alice <aliceisjustplaying@gmail.com>
- Add renderBlockChar() for U+2580-U+259F (block elements) as filled
  rectangles, eliminating inter-character gaps in ASCII art/progress bars
- Add renderPowerlineGlyph() for U+E0B0-U+E0B7 as canvas vector paths,
  ensuring glyphs span exactly the cell height regardless of font
- Switch measureFont() to fontBoundingBox metrics so cell height
  accommodates the full font cap-height (required by powerline chars)
- Merge DPR-aware rounding from PR coder#146 with fontBoundingBox: cells
  stay pixel-perfect at non-integer device pixel ratios
- Pass cursor.style from IRenderable.getCursor() through to renderCursor()
  so callers can override the cursor shape
- buildFontString() helper quotes font family names that contain spaces
- Add demo/bin/render-test.ts and demo/render-test.html for visual
  regression testing (puppeteer auto-installed on demand)
- Fix pre-existing biome lint issues in viewport/iris test files


Inspired-by: coder#128

Co-authored-by: Stuart Lang <stuart.b.lang@gmail.com>
Ports the Ghostty 1.3 VT engine into ghostty-web. The 1.3 public C API
replaces the 1738-line custom shim with a compact 133-line patch covering
only wasm-specific adaptations (WASM-safe Timestamp, kitty medium guard,
stale-cell fix). Key changes:

- New structured terminal_new/free/vt_write/resize API
- Render state: row iterator + per-row cells iterator replaces flat buffer
- Callback trampolines for write_pty, size, decodePng (WAT-based)
- Kitty graphics support (decodePng trampoline + image storage limit)
- Block element / Powerline glyph pixel-perfect rendering in renderer.ts
- Dynamic theme changes via ghostty_terminal_set(COLOR_*)
- bootstrapCells rendered via blank IRenderable until first VT output

Fixes carried forward from our fork:
- Screen.zig: always clear new rows in cursorDownScroll (stale-cell fix)
- scrollback line count → bytes conversion (×1000, clamped to u32 max)
- ghostty_terminal_free double-free guard (handle zeroed after free)
- getViewport resolves default fg/bg using terminal's current palette,
  matching pre-1.3 behaviour where all cells returned fully-resolved RGB

Co-authored-by: Evan Wies <evan@neomantra.net>
Inspired-by: coder#162
feat(wasm): upgrade Ghostty 1.2 → 1.3 with new C API and kitty graphics
Extracts shared VT parsing logic into TerminalCore, enabling DOM-free
usage via the new ghostty-web/headless entry point. Mirrors the
@xterm/headless API: write, buffer access, events (onData, onResize,
onBell, onTitleChange, onLineFeed, onWriteParsed), scrolling, addons,
and full lifecycle management.

The WASM terminal is created in the TerminalCore constructor so headless
consumers never need to call open(). The browser Terminal class preserves
all existing behaviour by overriding write, reset, resize, and the
response-draining loop; open() now only mounts the canvas renderer.

Co-authored-by: Kyle Carberry <kyle@carberry.com>
Inspired-by: coder#95
feat(terminal): add headless mode via TerminalCore base class
…ment

The hidden input textarea was pinned to position 0,0 causing CJK IME
composition windows to appear at the top-left of the terminal instead
of near the cursor, and triggering visual canvas displacement on some
browsers.

syncTextareaToCursor() moves the textarea to the cursor's cell coordinates
on every render frame so the OS IME window anchors to the correct position.

Fixes: coder#97
diegosouzapw and others added 7 commits May 24, 2026 09:22
fix(terminal): sync textarea position to cursor for correct IME placement
…(mode 2026)

Focus events: when DEC mode 1004 is active, emit \x1b[I on focus and
\x1b[O on blur. Required by vim, neovim, emacs and other editors that
use focus tracking to trigger :checktime and similar hooks.

Synchronized output: defer canvas renders while DEC mode 2026 is active.
Applications (tmux, vim) set this mode before batching screen updates to
prevent visible flicker mid-redraw. A 500ms timeout force-flushes any
sync window that is never closed by the application.

Both modes were parsed by the Ghostty WASM already; only the JS-side
responses were missing.
…utput

feat(terminal): add focus events (mode 1004) and synchronized output (mode 2026)
…r shape (#27)

Add onPromptStart, onCommandStart, and onCommandEnd events to TerminalCore
by intercepting OSC 133 markers (A/C/D) in the write() path via pure JS
regex scanning — no WASM rebuild required.

Add onMouseCursorChange event to Terminal and apply the requested CSS cursor
to the canvas/container element when OSC 22 is received from the application.

XTGETTCAP already works transparently via WASM readResponse() — no changes needed.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
… ports (v0.4.0)

## Features
- Headless mode via TerminalCore base class (ghostty-web/headless entry point)
- Ghostty 1.3 WASM upgrade: new C API, kitty graphics, structured iterators
- OSC 133 shell integration events (onPromptStart, onCommandStart, onCommandEnd)
- OSC 22 cursor shape (onMouseCursorChange)
- Focus events mode 1004 (FocusIn/FocusOut sequences)
- Synchronized output mode 2026 (defer canvas render, 500ms force-flush)
- Dynamic theme changes via Terminal.setTheme() and options.theme setter
- Powerline + block element pixel-perfect rendering
- Bootstrap blank state before first write()
- emitTerminalResponses option to suppress parser-generated replies
- ImagePasteAddon for clipboard image handling
- preserveScrollOnWrite option to lock viewport on new output
- focusOnOpen option to focus canvas on open()

## Fixes
- IME textarea repositioned to cursor coordinates on every render frame
- WASM page buffers zero-initialized to prevent memory corruption
- Viewport corruption from page memory reuse (RenderState row pins)
- Stale cell data after scroll (unconditional row clear in cursorDownScroll)
- Ghost cursor at (0,0) on init and ESC k title sequence leak
- Cursor shape (DECSCUSR), Ctrl+V forwarding, alt screen mouse scroll
- IME composition events routed to hidden textarea
- Keydown routing through Ghostty encoder (Alt→ESC prefix, macOS Alt keys)
- Font metrics aligned to device pixel boundaries (no sub-pixel seams)
- Wheel events include cursor coordinates when mouse tracking is active
- URL detection handles balanced parentheses
- Wide-character continuation cells skipped during selection copy
- Demo RCE: unauthenticated cross-origin WebSocket + /dist/ path traversal
- Dependency CVEs: happy-dom v20, rollup 3.30.0, postcss 8.5.10

## Tests
- Playwright E2E suite: 81 tests across 9 spec files (01-rendering → 09-lifecycle)
- playwright.config.ts: Chromium, serial, 15s timeout, Vite dev server
- tests/e2e/helpers/terminal.ts: shared helpers for all specs

## Docs
- CHANGELOG.md from v0.1.0 to v0.4.0 with full attribution
- README.md rewritten: headless, shell integration, events reference table, etc.
- .gitignore: exclude *.tgz, tests/e2e/report/, tests/e2e/results/
- .prettierignore + bunfig.toml: exclude Playwright generated artifacts

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

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.

[DEMO]CJK Input position issue in DEMO

1 participant