Skip to content

feat: pin Ghostty v1.4.0 so DECSTR soft resets apply - #43

Merged
ThomasK33 merged 5 commits into
mainfrom
feat/ghostty-v1.4.0
Oct 30, 2026
Merged

ThomasK33 merged 5 commits into
mainfrom
feat/ghostty-v1.4.0

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Closes #37

Why

Terminals ignored the DECSTR soft reset (ESC[!p), so a program that reset with it left the cursor hidden and the pen styled in every later snapshot. This moves the Ghostty pin to the v1.4.0 release, which includes upstream DECSTR support.

What changed

  • The build pins Ghostty v1.4.0 and Zig 0.17.0 (mise.toml, mise.lock, the build script and its docs).
  • native/terminal.cc follows two C API changes. Terminal creation now sets scrollback with ghostty_terminal_set, and the UTF-8 mouse mode read uses ghostty_terminal_get.
  • The DECSTR test now asserts the reset, and the README drops the soft-reset limit.
  • resize does a narrower and taller resize in two steps, rows first. Ghostty v1.4.0 skips the scrollback pull when one call does both, which left the prompt up with blank rows below it.

Scope

Tradeoffs

  • scrollbackLimit maps to the byte limit, not the new line limit. The old Ghostty counted max_scrollback in bytes, and only the byte setter keeps 0 as "no history". The unit was never documented, so changing it would be a silent behavior change.
  • I pinned the v1.4.0 release instead of f473f31, the first commit with DECSTR. It is a supported release, and it needs Zig 0.17.0 instead of 0.16.0.

Blast Radius

Every terminal now runs on Ghostty v1.4.0 (2,472 upstream commits). An independent verifier diffed main against this branch through the packed package. The differences it traced to upstream fixes include reflow cursor placement, C1 and C0 control characters no longer printing as cells, Unicode 18 width tables, and formatHtml no longer adding a newline at each page. Building from source now needs Zig 0.17.0.

Upstream changes consumers can see

These come from Ghostty v1.4.0, not from this binding:

  • Reflow keeps the cursor at the last write position, and a pending wrap is repaired after a resize.
  • C0 (0x10 to 0x1F) and C1 (0x80 to 0x9F) control characters no longer print as cells. Unicode 18 width tables change the width of 1,021 newly assigned codepoints.
  • CHA, VPA, HPR and VPR are origin-relative, BS stops at the left margin, and TBC changed.
  • With OSC 133, ED2 at a prompt moves the screen into scrollback. A scrollbackLimit of 0 no longer keeps rows from a scroll region at the top.
  • In mouse mode 1005, buttons eight and nine now send bytes (c2a0, c2a1).
  • formatHtml no longer adds a newline at each page.

Verification

  • The full local gate passes: libghostty build, native build, typecheck, 73 tests, smoke, npm pack, the UBSan build and tests, and the TLA+ lifecycle model.
  • Scrollback, old build against new build, for 20,000 lines: an unset limit and 0 keep no history on both. A limit of 1 to 100,000 keeps 969 lines on the old build and 903 on the new one. Both are Ghostty's page minimum. A new test fails when the code uses the line limit instead.
  • In the verifier's 400-scenario seeded fuzz, the resize fix brings 16 more scenarios back to main's exact state, and no scenario diverges earlier than before.
  • The Zig 0.17.0 checksums in mise.lock match ziglang.org's index.json for linux-x64, linux-arm64 and macos-arm64.

Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: high

Ghostty v1.4.0 needs Zig 0.17.0 and replaces two C calls. Terminal
creation now takes cols and rows and sets scrollback through
ghostty_terminal_set. The byte limit keeps the old scrollbackLimit
behavior, including 0 for no history. Mode reads go through
ghostty_terminal_get.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_

Change-Id: I04864bff70e3982b1b76e833037e9afed8dfd574
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33 ThomasK33 self-assigned this Oct 30, 2026
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-30T02:00:12.193406Z f267d86 Manual request
🔒 Security Review ✅ Completed 2026-10-30T02:00:46.713943Z f267d86 Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 90c38d0254

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .agents/skills/verify-libghostty-vt-node/SKILL.md Outdated
Comment thread README.md
@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 90c38d0254

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

The verify skill's fallback PATH named Zig 0.15.2, which the new pin
rejects. dogfood/regenerate.sh reruns commands.sh and rewrites the
bundle, which now records a passing build on the new pin.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_

Change-Id: Ib17d40e43d656e1d2e6185959c2af87b31501e8c
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: aa49c04447

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aa49c04447

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread dogfood/regenerate.sh Outdated
Comment thread dogfood/bootstrap/build-native-output.txt Outdated
@ThomasK33

Copy link
Copy Markdown
Member Author

FAIL

I am an independent verifier. I did not write this change.

  • Verified commit: 90c38d02546062228a7dab421384280f5ba33a68. Ghostty 83fb95624b0439ee660c2679b43b38c8b63769cd (v1.4.0), Zig 0.17.0.
  • Current PR head: aa49c04447111e03456c6e022adf0bae5a6634f5. It differs from the verified commit only in dev-only files that are not in the package and do not change the build: .agents/skills/verify-libghostty-vt-node/SKILL.md, dogfood/README.md, dogfood/regenerate.sh, and dogfood/bootstrap/* (12 files in git diff --stat).
  • Parent: origin/main at 74f3bc6488c34fa249ee71474186225f902608ce. Ghostty 48ccec1, Zig 0.15.2.

I packed each commit with .agents/skills/verify-libghostty-vt-node (launch.sh) and installed each tarball in its own scratch consumer. Then I ran the same journeys against @coder/libghostty-vt-node by name at both commits (Node v26.12.1, linux-x64). Tarball sha256: parent 3af7fd4a6019352ded0fc42b4ebf61c1733d0caa8d8e1c47c6c2d34799633f13, head 824a85a000b41dd82e11ad397524eae8d373573db98c83af4a602904a71b4ad0.

Why FAIL: one resize regression

A single resize(cols, rows) that makes the terminal narrower and taller at the same time no longer pulls history back from scrollback when the cursor is on the bottom row. On main the prompt stays on the bottom row. On head it stays where it was, and blank rows fill the screen below it. The text is not lost: it stays in scrollbackLines. Two other cases still behave as on main: a rows-only grow, and a wider+taller resize. The same resize done as two calls also behaves as on main, in either order.

A consumer that forwards window resizes to resize() sees this. Dragging a corner often changes both dimensions at once. The bug affects terminals that keep scrollback, including agent-tty's default scrollbackLimit: 10_000.

Repro journey (regression-repro). It exits 0 on main and 1 on head:

import assert from "node:assert/strict";
import { createTerminal } from "@coder/libghostty-vt-node";
const t = createTerminal({ cols: 80, rows: 24, scrollbackLimit: 10_000 });
t.feed(Array.from({ length: 40 }, (_, i) => `out ${i}\r\n`).join("") + "$ ");
t.resize(80, 10);
t.resize(60, 30); // narrower and taller in one call
const s = t.snapshot({ includeScrollback: true });
assert.equal(s.cursorRow, 29); // main: 29, head: 9
Case (80x24, 40 lines, then 80x10) main cursorRow / top line / scrollback head cursorRow / top line / scrollback
one call to 60x30 29 / out 11 / 11 9 / out 31 / 31, 20 blank rows below the prompt
80x30, then 60x30 29 / out 11 / 11 29 / out 11 / 11
60x10, then 60x30 29 / out 11 / 11 29 / out 11 / 11
rows only, 80x30 29 / out 11 / 11 29 / out 11 / 11
wider+taller, 100x30 29 / out 11 / 11 29 / out 11 / 11

A seeded fuzz of 400 random sessions shows the same pattern in 30 scenarios. In 25 of them, doing the resize at head as two calls (rows first) reproduces main exactly.

Likely cause. I inferred this from the Ghostty source and did not bisect. Ghostty c44afa625 ("preserve active cursor position during reflow") now passes the cursor pin to PageList.resize. When cols shrink, PageList.resize grows the rows first, which pulls history down. Then resizeCols computes remaining_rows from the cursor's old y (src/terminal/PageList.zig), so the reflow adds rows below the cursor again and pushes the pulled rows back into scrollback.

Claims

Claim Parent (74f3bc6) Head (90c38d0) Result
1. DECSTR after ESC[?25l ESC[7m A, then X cursorVisible false, X inverse true, line 0 AX cursorVisible true, X has no inverse, A keeps inverse, line 0 AX holds
2. scrollbackLimit keeps byte semantics unset and 0 keep 0 lines. Limits 1 to 1,000,000 keep 945 lines (80x24, 5,000 lines fed) and 715 (120x40). 10,000,000 keeps 4977 and 4961. unset and 0 keep 0 lines. Limits 1 to 1,000,000 keep 931 and 704. 10,000,000 keeps 4977 and 4961. holds
3. Other behavior unchanged or explained baseline 1 regression (resize, above). Every other difference is expected or an upstream fix (list below). fails
4a. A non-empty feed() resets motion dedup, geometry changes reset it, SGR-pixels reports in-cell motion holds holds, identical bytes in 7 dedup sequences holds
4b. Buttons ten and eleven give an empty press. A legacy release still encodes. holds press empty in all 16 modes. Under modes 1000, 1002 and 1003, the release equals the left release in the default, UTF-8 and URXVT encodings, and is empty in the SGR encodings. Identical to parent. holds
5. getNativeInfo() reports the new Ghostty ghosttyVersion 0.1.0-dev, no ghosttyCommit same output pre-existing note (see Notes)

Claim 2 details for 80x24 with 200 lines fed: every nonzero limit keeps 177 lines at both commits. For 20x3 with 2,000 lines fed: every nonzero limit keeps 1998 at both.

Difference list

My compare script found 90 differences in 1,960 observe-all observations (70 expected, 20 upstream fix). It found 20 in 58 upstream-probes observations (1 expected, 19 upstream fix). upstream-probes targets upstream commits whose subjects describe a terminal-state change. A rule table classifies every difference, and none is left unclassified.

Expected

  1. DECSTR now applies (Ghostty f473f3109, softReset). It shows the cursor, resets SGR and colors, resets the OSC 4 palette, origin mode, wraparound, insert mode, left and right margins, the scroll region and the DEC graphics charset, and makes DECRC restore home. 0df8c4868 also turns off ISO protection. Screen content stays. Mouse modes stay set, and the bytes are identical. This covers 55 decstr.* observations, resize.after-decstr, workers.result, and u.0df8c4868.spa-erase-after-decstr.
  2. Scrollback page counts: 945 becomes 931 (80x24) and 715 becomes 704 (120x40) for limits 1 to 1,000,000 with 5,000 lines fed. This covers 12 observations.
  3. Packed files that the PR changes: README.md, native/README.md, native/terminal.cc, scripts/README.md, scripts/build-libghostty-vt.sh, and the rebuilt prebuild .node.

Upstream fix (Ghostty commits between 48ccec1 and v1.4.0)

  1. c44afa625: after a reflow resize, the cursor follows the write position. Example: "$ " plus 20 CJK glyphs at 80 cols, resized to 41. Main puts the cursor at (0,40), on top of the prompt text. Head puts it at (1,2), after the last glyph. This covers resize.reflow, resize.cursor, resize.wide-boundary, resize.max, u.a55850c98.*, u.alt-screen-resize-back, and 87 fuzz scenarios whose only difference is the cursor. Side effect: when the cursor sat past the new width, its wrapped row adds rows. With scrollbackLimit 0 or unset, that row can push one more top row off the screen (u.hyperlink-reflow: 15 chars on main, 10 on head). With scrollback, it leaves more rows in scrollbackLines (9 narrower+taller fuzz scenarios, where head's one-call result equals its rows-first two-call result).
  2. f9ab34f10: a pending wrap is repaired after a resize. In a 10-col terminal, fill the row, widen to 20, and print X: X lands at col 10 of the same row. Main wrapped it to the next row.
  3. e6db5b633: narrowing without reflow (in the alt screen, or with DECAWM off) drops a wide char that the new width cuts. Main kept an orphaned wide head past the right edge. Together with the c44afa625 side effect, this covers 17 fuzz scenarios where head has fewer printable chars after a resize. I traced 6 of them (fuzz.6, 62, 66, 252, 264, 300).
  4. f4c68d65e: scrollbackLimit 0 or unset now never keeps history. Main kept rows that scrolled out of a top-anchored scroll region (CSI 1;2r, then LF) in scrollbackLines. This covers 6 fuzz scenarios.
  5. ff779564c: ED2 at an OSC 133 prompt scrolls the screen into scrollback (Ghostty #905). This now also happens when the prompt is not on the last row. Effect for consumers: a shell clear with OSC 133 integration keeps the prior output in scrollbackLines.
  6. 40a40f848: UTF-8 C1 controls U+0080 to U+009F no longer print as cells.
  7. 0f35043c9: C0 controls 0x10 to 0x1F no longer print as cells. Main put them into visibleLines text.
  8. c20fcfa1b, 3f11e695d and b10ba7ea2: combining marks and VS16 at a pending wrap or at the last column attach to the correct cell, and graphemes survive widening. This covers 28 fuzz scenarios.
  9. 157c44f64: in UTF-8 mouse mode (1005), a press of button eight or nine now UTF-8 encodes the button byte (c2a0 and c2a1 instead of a raw a0 and a1), as xterm does. This covers 10 observations. The other 1,208 mouse.fresh pairs are identical, and so are the 28 pairs from the earlier verifier.
  10. f024d21fc: formatHtml no longer adds a newline at each per-page <div>. 200 full rows now render as 200 lines (main 202), and 5,000 lines render as 5,000 (main 5,008). formatPlain is identical.
  11. Control-sequence fixes: CHA, VPA, HPR and VPR are origin-relative (82df0be2c). CUB and BS stop at the left margin (53fb05c6e). TBC with no parameter clears the tab stop at the cursor (d0c0d4008). Codepoints above 0xFF print unmapped in DEC graphics, where main printed blanks (6220a3617). RIS resets the palette (bb20f8e45).
  12. 12542b392, Unicode 18 width tables: 1,021 codepoints change width. 990 go from 1 to 2 and 31 go from 1 to 0. All are in newly assigned ranges, such as U+18E00 to U+19191 and U+1FAF9 to U+1FAFA. I inferred "newly assigned" from the ranges and did not check it against the UCD. No ASCII, Latin, CJK or established emoji width changed.

Regression

  1. A narrower+taller resize in one call skips the scrollback pull (above). This covers 30 fuzz scenarios. Two more scenarios show the same pattern when the alt screen exits after such a resize (fuzz.186, fuzz.358). I inferred that those two are the same bug.

Other differences that I did not trace one by one: 22 fuzz scenarios differ in layout after a resize, 2 differ after a DECRC that follows resizes, and 1 (fuzz.50) keeps a row on head that main lost when it narrowed in the alt screen. They fit c44afa625 and afded91df, but that is an inference. Fuzz total: 204 of 400 scenarios diverge, and 196 match after every op.

Notes

  • Claim 5 is a pre-existing limit and not a reason for this FAIL. getNativeInfo() returns the same object on both commits: {"napiVersion":10,"ghosttyVersion":"0.1.0-dev","packageVersion":"0.1.0-beta.1","platform":"linux","arch":"x64"}. ghosttyVersion is libghostty-vt's own lib_version (build.zig:11 at v1.4.0), not the app version. Ghostty sets no build string for the lib artifact, so ghosttyCommit is absent. .agents/skills/verify-libghostty-vt-node/features/native-info.md:76-77 already documents this.
  • formatHtml edge case after f024d21fc: when a whole page of rows is blank, head renders one line too few. On a 500x200 screen with A on row 0 and Z on row 199, head renders 199 lines, main 201, and the correct count is 200.
  • No change on either commit: errors (43 cases with the same RangeError, TypeError and AssertionError names and messages, plus 3 boundary cases that succeed), use after dispose (9 cases), worker_threads apart from DECSTR, 475 screen-corpus observations apart from C1 and formatHtml, and the cursor and visibleLines bounds that agent-tty asserts (cursorRow < rows, cursorCol < cols) in all 400 fuzz scenarios.
  • .agents/skills/verify-libghostty-vt-node/SKILL.md at 90c38d0 says Zig 0.17.0 but its example path still names zig/0.15.2. The new head commit edits that line.
  • A possible fix in the binding, which I did not test as a patch: when cols shrink and rows grow, TerminalWrap::Resize could call ghostty_terminal_resize twice, rows first. In the fuzz, rows first matched main exactly in 25 of the 39 narrower+taller cases, against 24 for cols first. Of the 9 cases that are c44afa625 side effects, head's one-call result equals rows first in all 9 and differs from cols first in 6.

Review count: this is one verifier pass on this PR. I did not review the code beyond the C API port, and I made no edits.


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: xhigh

Ghostty v1.4.0 skips the scrollback pull when one resize both narrows
the grid and adds rows, so the prompt stayed up with blank rows below
it. Resizing the rows first, at the old width, restores the earlier
result. regenerate.sh now fails when a step fails or when npm scripts
run a Node other than the mise.toml pin.

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_

Change-Id: I6b009462abc8865ad9052dd655bc3c8d3107e642
Signed-off-by: Thomas Kosiewski <tk@coder.com>
_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_

Change-Id: I6b79e14cd14acdb142433ed6f7588cd17e8a629f
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: 7ff47a56ba

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7ff47a56ba

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread dogfood/regenerate.sh
@ThomasK33

Copy link
Copy Markdown
Member Author

PASS+NOTES

I am an independent verifier. I did not write this change. The resize regression from the earlier FAIL (#43 (comment)) is gone, and I found no new regression. The notes below are pre-existing limits or upstream Ghostty behavior, and none of them blocks the merge.

  • PR head: 7ff47a56ba2566a213e6ca2e95cf9965ddbe0820. Ghostty 83fb95624b0439ee660c2679b43b38c8b63769cd (v1.4.0), Zig 0.17.0.
  • Parent: origin/main at 74f3bc6488c34fa249ee71474186225f902608ce. Ghostty 48ccec182a932c2ec04c344d45a5fc553861cb13, Zig 0.15.2. Main has not moved since the earlier verifier ran.
  • Earlier head for the comparison: the 90c38d0 build from the FAIL run. Between 90c38d0 and 7ff47a5, only native/terminal.cc and native/README.md change under native/, src/, scripts/, binding.gyp and package.json.

I packed each commit with .agents/skills/verify-libghostty-vt-node (launch.sh, exit 0 on both) and installed each tarball in its own scratch consumer. Node v26.12.1 ran the npm scripts on both sides (npm exec -c 'node -p process.version'). Tarball sha256: parent ac8036db02f8755faa723d6a879a662b76306176696529bbe9058fa39cae7caa, head c87361c93e33be4ee047665ad916f1f740152e27af7a6164421f7858b909abca. CI run 40987163324 passed all four ci rows on 7ff47a5.

Checks

# Check Result
1 Repro from the FAIL comment (80x24, 40 lines, 80x10, then 60x30 in one call) Head: cursorRow 29, top line out 11, 11 scrollback lines. Main: the same. The regression-repro journey exits 0 on both (it exited 1 on the 90c38d0 build). All 5 rows of its table are identical between parent and head. holds
2 Earlier journey set and 400-scenario seeded fuzz, parent and head 16 journeys, exit 0 on both sides. The parent output is unchanged from the earlier parent run, apart from the prebuild hash in info.packageFiles. Every head change traces to a narrower and taller resize (details below). Fuzz: 212 of 400 scenarios match main after every op, and 188 diverge (earlier: 196 and 204). Cursor and visibleLines invariant violations: 0 on parent, 0 on head. holds
3 DECSTR, scrollbackLimit byte semantics, encodeMouse parity, the two README mouse claims claims.out is byte-identical to the earlier run. DECSTR: cursorVisible false on main, true on head, and X has no inverse on head. Unset and 0 keep 0 lines on both. Limits 1 to 1,000,000 keep 945/931 lines (80x24) and 715/704 (120x40). 10,000,000 keeps 4977 and 4961 on both. The 7 dedup sequences and the ten/eleven buttons are identical to main. holds
4 dogfood/bootstrap/ and dogfood/regenerate.sh environment.txt: node=v26.12.1, zig=0.17.0, ghostty_commit=83fb95624b0439ee660c2679b43b38c8b63769cd. build-native-output.txt: gyp info using node@26.12.1. session.log records 7 steps, each exit=0, and test-output.txt shows 73 passed (73). In a throwaway clone, regenerate.sh exits 1 with a stale Path (error: npm scripts run node v22.23.4, but mise.toml pins v26.12.1). It also exits 1 after I added a type error to src/index.ts (error: 'npm run build' exited 2), and its session.log records exit=2 for that step and exit=0 for the other 6. holds
5 The new test fails without the two-step block In a throwaway clone I removed only the 4-line if (cols < old_cols && rows > old_rows) { ... } block and rebuilt the addon. The test "pulls history back when one resize makes the terminal narrower and taller" fails with AssertionError: expected 9 to be 29. With the block in place it passes. In a clean clone at head, npm test gives 73 passed (73) and npm run typecheck exits 0. holds

How the head changes trace to the fix

Compared with the earlier head run, head output changed in these journeys, and nowhere else:

  • fuzz2 and fuzz: the same 40 scenarios. In 37 of them, the first op where the head state changed is a resize that makes the grid narrower and taller at once. In the other 3 (fuzz.186, fuzz.252, fuzz.358), it is an alt-screen exit that follows a narrower and taller resize done in the alt screen (ops 14 and 26, op 22, and ops 27 and 37).
  • replay-resize and split-resize: 30 rows, all in that set of 40. replay and replay2: fuzz.186 and fuzz.358.
  • pull-matrix: 4 rows, all narrower and taller (10x2>8x6, 12x2>8x6, 10x2>9x6, 80x24-shell>100x10>60x30). They now equal main.
  • alt-resize2: alt1049.shrink-grow-cols-exit and noalt.shrink-grow-cols (12x2 to 8x6, one in the alt screen). They now equal main.
  • observe-all: only info.packageFiles, whose hashes for native/README.md, native/terminal.cc and the prebuild .node changed.
  • regression-repro: the one-call row, as in check 1.

I also proved the trace mechanically. I removed the block in a throwaway clone and replayed all 400 fuzz scenarios on that build. Plain replay reproduces the earlier head hashes in 400 of 400 scenarios. When the replay splits every narrower and taller resize into rows first, then cols, from JS, it reproduces the new head hashes in 400 of 400. So the fix changes nothing else.

Diff against the earlier FAIL classification

observe-all keeps 90 differences in 1,960 observations (70 expected, 20 upstream fix), and upstream-probes keeps 20 in 58 (19 upstream fix, 1 expected). Neither has an unclassified difference. Both compare outputs match the earlier ones except the info.packageFiles hashes. The fuzz grouping by first diverging op changed as follows:

First diverging op Earlier Now Class
resize, cursor only 87 95 upstream fix c44afa625 / f9ab34f10
resize, narrower+taller, no scrollback pull 30 0 was the regression, now fixed
resize, narrower+taller, cursor's wrapped row leaves more rows in scrollback 9 10 upstream fix, c44afa625 side effect
resize, head has fewer printable chars 17 19 upstream fix e6db5b633 (or the c44afa625 side effect)
resize, other layout differences 22 24 c44afa625 (I traced the 2 new ones)
text, combining mark or VS16 28 29 upstream fix c20fcfa1b / 3f11e695d
text, lines or edit with scrollbackLimit 0 or unset 6 6 upstream fix f4c68d65e
DECRC after resizes 2 3 inferred afded91df
alt-screen exit after a narrower+taller resize in the alt screen 2 0 was the regression, now fixed
alt-screen exit after other resizes in the alt screen 1 2 inferred c44afa625
Total diverging 204 188

Of the 32 regression scenarios, 16 now match main after every op. The other 16 now show a different first difference, and I classified each one by replaying both packed builds through that op:

  1. Five still differ at the fixed resize, as the earlier rows-first prediction said. fuzz.132, fuzz.230, fuzz.374 and fuzz.381 differ only in the cursor, and lines and scrollback equal main (c44afa625). In fuzz.155 (38x3 to 1x10), main puts the cursor at [7,0] on the ア row. Head puts it at [9,0], at the write position, and keeps 17 scrollback rows against 16. Printable chars are equal. This is the c44afa625 side effect.
  2. Four now match main through the fixed resize and differ only in the cursor after a later resize: fuzz.51 (10x9 to 8x8), fuzz.271 (39x12 to 18x6), fuzz.285 (40x12 to 24x4) and fuzz.379 (21x12 to 31x12). Class: c44afa625 / f9ab34f10.
  3. fuzz.144 (33x9 to 11x9) and fuzz.151 (34x6 to 12x6) differ after a later reflow. Main pulls rows down from scrollback and leaves the cursor off the write position, at [7,8] on the x row and at [4,9] on a blank row. Head keeps the cursor after the last written cell, at [1,7] and [1,6], and keeps 7 and 4 more rows in scrollback. Printable chars are equal. Class: c44afa625.
  4. fuzz.75 (9x10 to 5x8) and fuzz.147 (25x3 to 1x3) narrow with DECAWM off. Main keeps a wide glyph that the new width cuts (👨‍ and 😀), and head drops it. Class: e6db5b633.
  5. In fuzz.130, ❤ plus VS16 is printed in the last column of a 3-column alt screen. Main stores the cell as ❤ and drops the VS16. Head stores ❤️. Both keep width 1. Class: 3f11e695d.
  6. In fuzz.110, the alt screen exits after the in-alt resizes 21x9, 1x4, 2x4, 11x3 and 32x12. None of these is narrower and taller. Main leaves 8 rows in scrollback and shows hello world on row 1. Head restores the primary screen text as it was before ?47h, with abc on row 6, hello world on row 9 and no scrollback. A fresh-terminal repro of the same ops, with no narrower and taller resize anywhere, gives the same main and head results. So the fix is not the cause. Same class as fuzz.50, inferred c44afa625.
  7. In fuzz.172, DECRC follows the resizes 19x12, 18x1 and 16x9, and DECSC had saved [11,8]. Main restores the cursor to [8,5], and head restores it to [8,8]. A fresh-terminal repro that sends the 16x9 resize as two calls (rows, then cols), so the new block never runs, gives the same [8,5] and [8,8]. So the fix is not the cause. Same class as fuzz.315 and fuzz.349, inferred afded91df.

Eight more scenarios (fuzz.39, 89, 148, 218, 252, 281, 343, 350) changed only after their first difference from main. Their first diverging op and its class did not change.

Notes

  • getNativeInfo() still returns {"napiVersion":10,"ghosttyVersion":"0.1.0-dev","packageVersion":"0.1.0-beta.1","platform":"linux","arch":"x64"} on both commits, with no ghosttyCommit. This limit predates the PR, as the FAIL comment explained.
  • The formatHtml blank-page edge case from the FAIL comment is unchanged. On a 500x200 screen with A on row 0 and Z on row 199, head renders 199 lines and main 201, and the correct count is 200.
  • environment.txt names git_head=834fc2d9d467a5af04f196a8d9491e2b77544b33, not the PR head. This is expected. 7ff47a5 changes only 5 files under dogfood/bootstrap/ (git diff --stat 834fc2d 7ff47a5), so the bundle describes the same code. build-native-output.txt names Node but not Zig or Ghostty. Ghostty 83fb956 appears in build-libghostty-output.txt, and Zig appears in environment.txt.
  • I read the new TerminalWrap::Resize code. GHOSTTY_TERMINAL_DATA_COLS and GHOSTTY_TERMINAL_DATA_ROWS write a uint16_t (terminal.h at v1.4.0), which matches the locals. ThrowResult throws a C++ exception (NAPI_CPP_EXCEPTIONS), so each failed call returns at once. I did not test the case where the first resize succeeds and the second fails, because I had no way to make ghostty_terminal_resize fail. In that case the terminal stays at the old width with the new rows.
  • Two builds of the same commit are not byte-identical. The parent static library and tarball differ from the earlier parent run (96f99834... against ff2032dc..., same build-input hash). All 16 parent journeys still give the same stdout, apart from the prebuild hash in info.packageFiles.

Review count: this is the second verifier pass on this PR. The first one posted FAIL. I made no edits to the repo.


Generated with xum • Model: anthropic:claude-opus-5-5 • Thinking: xhigh

_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_

Change-Id: I05348d97a66fea2f4619268072c74aee96e29342
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex security review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: f267d866a4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@chatgpt-codex-connector

Copy link
Copy Markdown

🛡️ Codex Security Review

Security review completed. No security issues were found in this pull request.

Reviewed commit: f267d866a4

View security finding report

Only the user who started this review can view the report in Codex.

ℹ️ About Codex security reviews in GitHub

This is an experimental Codex feature. Security reviews are triggered when:

  • You comment "@codex security review"
  • A regular code review gets triggered (for example, "@codex review" or when a PR is opened), and you’re opted in so security review runs alongside code review

Once complete, Codex will leave suggestions, or a comment if no findings are found.

@ThomasK33
ThomasK33 merged commit 83d79f7 into main Oct 30, 2026
5 checks passed
ThomasK33 added a commit that referenced this pull request Oct 30, 2026
## Why
Ship the changes merged since `0.1.0-beta.1` to npm. They include mode-aware mouse encoding (#12), the new snapshot attributes and `cursorVisible` (#17, #38), and Ghostty `v1.4.0` with DECSTR support (#43).

## What changed
- `package.json` and `package-lock.json` go from `0.1.0-beta.1` to `0.1.0-beta.2`. The branch name matches the version, so `publish.yml` creates `v0.1.0-beta.2` after merge.
- The `release-changelog` workflow adds the CHANGELOG entry to this branch.
- `communique.toml` raises `max_tokens` to 16384. With Communique 1.0.3's default of 4096, generating these notes failed twice with "submit_release_notes was malformed 3 times". The next run succeeded.
- `prepare-release-changelog.mjs` now detects Communique's `## [vX](url) - date` heading. Before, every push regenerated the entry over hand edits.
- I corrected the generated entry. `cursorVisible` and `encodeMouse` first ship in this release, so they are listed as Added, not as changes or fixes.

## Blast Radius
The merge publishes `0.1.0-beta.2` to npm under the `beta` dist-tag. `latest` stays on `0.1.0-beta.0`. Consumers that build a `TerminalSnapshot` themselves must now set `cursorVisible` (#38).

## Verification
- `RELEASE_BRANCH_NAME=release/v0.1.0-beta.2 node .github/scripts/prepare-release-changelog.mjs origin/main` prints `should_generate=true` and `tag_name=v0.1.0-beta.2`. `resolve-npm-publish-tag.mjs` prints `beta`.
- `npm run typecheck` and `npm test` (73 passed).
- Three independent verifier passes, the last PASS+NOTES on the head. They check every CHANGELOG bullet against the `v0.1.0-beta.1` tag and confirm that the packed tarball loads its addon from `prebuilds/`.
- The local linux-x64 prebuild steps from `mise run release-prebuilds` pass: `build:libghostty`, `build`, `build:prebuild`, `verify:prebuilds` and `npm pack --dry-run`.

---
_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `high`_
ThomasK33 added a commit that referenced this pull request Oct 30, 2026
## Why
The `verify-native-lifecycle` skill keeps a sync commit. At that commit, its TLA+ model describes the covered native code. The sync commit was still `7366b75` (#12). Since then, #40 to #43, #46, and #49 changed covered code. #46 promised this follow-up.

## What changed
- The sync commit moves to `c54051e`, the current main. It includes #48 and #49.
- The coverage rows now name `ghostty_terminal_set` (scrollback byte limit) in the constructor and its failure path, `Resize`'s second `ghostty_terminal_resize` call, and the `GHOSTTY_TERMINAL_DATA_MODE` read in `EncodeMouse`. Ghostty v1.4.0 (#43) brought these.
- The "no JS inside native methods" assumption now says how each method reads its arguments:
  1. `Snapshot` reads its options before `RequireTerminal`, and it builds its result with `DefineValue` and `DefineIndex`.
  2. `Feed` and `Resize` call `RequireTerminal` first. After that, they only test types and read strings, bytes, and numbers, which runs no JS.
  3. `GetVisibleText`, `FormatPlain`, and `FormatHtml` read no arguments.
  4. The assumption was false for `snapshot` until #46.
- The bullet that lists where caller JS can run now names public `feed`: `assertFeedData` runs `data instanceof Uint8Array`, which runs a Proxy's `getPrototypeOf` trap after `#assertUsable`. The native null check covers it, as it does for `snapshot`.
- `AddonEnvLifecycle.tla` changes in comments only. The `CreateFails` comment now names the `ghostty_terminal_set` failure too.

The model itself does not change. #48 changes only a test. #49 changes only `Feed`'s argument checks, so the `Use(t)` step still describes `feed`.

## Verification
- I checked each row against `native/terminal.cc` and `src/index.ts` at `c54051e`.
- `git diff 7366b75 c54051e` on the covered paths adds no new static state, `Napi::Reference`, or new place that frees `terminal_` or `mouse_encoder_`.
- `run.sh`: all 35 rows match EXPECT (`OK: every result matches EXPECT.`). The current config holds all 5 rules. Each of the 6 mutants breaks the rule it guards.

The verifier's two notes on `0ff0941` (the `feed` window and the `CreateFails` comment) are fixed in `584c561`. `run.sh` still passes all 35 rows.

Workspace 36beaed175 drafted the first version of this commit. I corrected the assumption bullet: it said that every method reads its arguments before `RequireTerminal`, but `Feed` and `Resize` do not.

---
_Generated with [`xum`](https://github.com/coder/xum) • Model: `anthropic:claude-opus-5-5` • Thinking: `xhigh`_
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.

Upgrade the pinned Ghostty past f473f31 to support DECSTR (needs Zig 0.16)

1 participant