Skip to content

Fix ESC k title payload rendering#176

Open
ThomasK33 wants to merge 1 commit into
mainfrom
implement-issue-153
Open

Fix ESC k title payload rendering#176
ThomasK33 wants to merge 1 commit into
mainfrom
implement-issue-153

Conversation

@ThomasK33

@ThomasK33 ThomasK33 commented Jun 26, 2026

Copy link
Copy Markdown
Member

Summary

Fixes #153.

  • Routes GNU screen/tmux title sequences (ESC k <text> ESC \ / ESC k <text> BEL) into Ghostty's ignore-string parser state so title payload bytes are consumed instead of rendered.
  • Adds regression tests for the issue sequence through string writes, Uint8Array writes, and split writes across parser boundaries.
  • Keeps existing OSC 0/2 title-change behavior intact and avoids a TypeScript pre-filter.

Validation

  • bun run fmt
  • bun run lint
  • bun run typecheck
  • bun test
  • bun run build
  • GitHub Actions CI for PR Fix ESC k title payload rendering #176: fmt, lint, type check, test, and build all passed on commit cec12d2051.

Dogfooding

  • Browser dogfood injected the issue sequence into a Vite-served terminal and verified that the title payload did not appear in the terminal grid while the following visible text still rendered.
  • Local screenshot/video artifacts were produced during workflow verification; uploading to GitHub user-attachments was blocked in this headless workspace because gh-image requires a GH_SESSION_TOKEN or browser user_session cookie.

📋 Implementation Plan

Implementation Plan: #153

Issue summary

GitHub issue coder/ghostty-web#153 is open and accepted with labels triage:done, bug, and accepted.

Problem: the GNU screen/tmux title sequence ESC k <text> ESC \\ is currently parsed as an unimplemented one-byte ESC k action. The parser returns to ground state, so <text> is printed into the visible terminal grid, while the trailing ESC \\ is consumed as a no-op string terminator.

Expected behavior for the issue reproduction:

write: ESC k /tmp ESC \\ CRLF ESC k ls ESC \\ demo.txt CRLF
line 0: ""
line 1: "demo.txt"

Actual behavior today:

line 0: "/tmp"
line 1: "lsdemo.txt"

Verified context and constraints

  • Live issue ESC k <text> ESC \ (screen/tmux title sequence) leaks payload as visible text #153 was read with gh issue view 153 --repo coder/ghostty-web --comments and --json.
  • The issue only requires preventing visible payload leakage. Routing the payload through onTitleChange is explicitly optional/bonus and should not be added for this minimal fix.
  • Triage reproduced the bug locally and identified the parser path: lib/terminal.ts writes data to this.wasmTerm!.write(data), which enters the Ghostty Zig terminal stream; ghostty/src/terminal/stream.zig warns for unimplemented ESC k after the parser dispatches it as an ordinary ESC action.
  • The Ghostty source is a submodule. Local Ghostty customizations are applied by scripts/build-wasm.sh through patches/ghostty-wasm-api.patch; the implementation should update that patch rather than leaving direct edits in the submodule.
  • ghostty-vt.wasm is currently ignored, not tracked (git ls-files --error-unmatch ghostty-vt.wasm returns no match; git check-ignore ghostty-vt.wasm matches). A local WASM rebuild is required for validation, but the generated root ghostty-vt.wasm should not be committed unless repository policy changes.
  • The referenced fork commit diegosouzapw/ghostty-web@31ed228 fixed the symptom with a TypeScript pre-filter and also included an unrelated renderer change. Do not port the renderer change. Do not use a stateless TypeScript pre-filter: it is fragile for split writes and duplicates parser responsibility in JS.

Recommended approach

Fix this in the Ghostty parser state table by making ESC k enter an ignore string state instead of dispatching as a normal ESC action.

Use State.dcs_ignore as the sink state:

  1. ESC moves the parser from .ground to .escape.
  2. k in .escape moves to .dcs_ignore with .none action.
  3. Title payload bytes remain in .dcs_ignore with no print/dispatch action.
  4. ESC inside the ignore string uses the existing anywhere transition to .escape.
  5. \\ dispatches as ST/no-op and returns to .ground.

This preserves parser state across separate term.write() calls, avoids TypeScript allocations/scanning, and keeps the fix close to the terminal parser behavior that caused the bug.

Files and symbols to touch

Required

  1. patches/ghostty-wasm-api.patch

    • Add a patch hunk for ghostty/src/terminal/parse_table.zig inside genTable()'s // escape state block.

    • The minimal hunk should override the existing range(&result, 0x60, 0x7E, source, .ground, .esc_dispatch); entry for byte 0x6B ('k') by adding a later single(...) entry:

      diff --git a/src/terminal/parse_table.zig b/src/terminal/parse_table.zig
      --- a/src/terminal/parse_table.zig
      +++ b/src/terminal/parse_table.zig
      @@
               // => dcs_entry
               single(&result, 0x50, source, .dcs_entry, .none);
      +
      +        // GNU screen/tmux title sequence: ESC k <text> ESC \\
      +        // Consume payload so it never reaches the visible grid.
      +        single(&result, 0x6B, source, .dcs_ignore, .none);
       
               // => csi_entry
               single(&result, 0x5B, source, .csi_entry, .none);
    • single() overwrites prior entries in the generated table, so placing this after the broad 0x60...0x7E range is sufficient and keeps the change small. If a reviewer prefers explicit non-overlap, split the broad range into 0x60...0x6A and 0x6C...0x7E instead of relying on overwrite behavior.

  2. lib/terminal.test.ts

    • Add regression coverage using existing terminal test patterns and createIsolatedTerminal from lib/test-helpers.ts.
    • Reuse or add a local helper that extracts a line from term.wasmTerm?.getLine(y) and trimEnd()s it.

Optional only if it materially improves review confidence

  • Add a tiny parser-state unit test to the Ghostty patch for ghostty/src/terminal/Parser.zig, verifying ESC k enters .dcs_ignore and ESC \\ returns to .ground.
  • Prefer not to add this unless the TypeScript regression tests are insufficient, because it increases the patch surface.

Implementation phases and quality gates

Phase 1 — Add parser patch

  1. Update patches/ghostty-wasm-api.patch with the parse_table.zig hunk described above.

  2. Run a patch-only check:

    git -C ghostty apply --check ../patches/ghostty-wasm-api.patch
  3. If the patch does not apply, inspect current ghostty/src/terminal/parse_table.zig context and adjust only the hunk context, not the behavior.

Quality gate: patch applies cleanly to the submodule.

Phase 2 — Rebuild local WASM for validation

  1. Ensure Zig is available. If zig is missing but mise has the expected toolchain, use:

    PATH="$HOME/.local/share/mise/installs/zig/0.15.2/bin:$PATH" bun run build:wasm

    Otherwise:

    bun run build:wasm
  2. Confirm scripts/build-wasm.sh applies the patch, builds ghostty/zig-out/bin/ghostty-vt.wasm, copies ghostty-vt.wasm to the repo root, then reverses the patch and removes generated submodule files.

  3. Check that the submodule is clean after the build:

    git -C ghostty status --short

Quality gate: WASM build succeeds and git -C ghostty status --short is empty.

Phase 3 — Add regression tests

Add focused tests in lib/terminal.test.ts for the visible-grid contract, without requiring title events.

Minimum cases:

  1. Issue reproduction, string write

    • Write:

      '\x1bk/tmp\x1b\\\r\n\x1bkls\x1b\\demo.txt\r\n'
    • Assert row 0 is '' and row 1 is 'demo.txt'.

  2. Issue reproduction, Uint8Array write

    • Use new TextEncoder().encode(...) for the same bytes.
    • Assert row 0 is '' and row 1 is 'demo.txt'.
  3. Split/chunked writes

    • Split across parser boundaries, for example:

      term.write('\x1b');
      term.write('k/tmp');
      term.write('\x1b');
      term.write('\\\r\ndemo.txt\r\n');
    • Assert /tmp does not appear and visible content after the terminator still appears normally.

    • Use the repository's existing write-settling/render-settling pattern if needed before reading rows, so the regression does not become flaky.

  4. Adjacent behavior stays intact

    • Run existing OSC 0/2 onTitleChange tests. Do not add a requirement that ESC k fires onTitleChange; that is outside the accepted minimal scope.

Quality gate: the new regression tests fail before the parser patch and pass after rebuilding WASM with the parser patch.

Phase 4 — Automated validation

Run validation progressively:

bun test lib/terminal.test.ts -t "ESC k"
bun test lib/terminal.test.ts -t "onTitleChange"
bun run fmt && bun run lint && bun run typecheck && bun test && bun run build

If bun test hangs after reporting results, capture the pass/fail summary before terminating, because this repository has a known Bun completion-hang gotcha.

Quality gate: all required checks pass, or any environmental blocker is reported with exact command output and the narrowest passing subset.

Dogfooding and reviewable evidence

Because this is terminal UI/interactive behavior, dogfood with browser evidence in addition to automated tests.

Setup

  1. Start the dev server with Vite, not a plain static server:

    bun run dev
  2. Open http://localhost:8000/demo/ with browser automation. Prefer the agent-browser skill for repeatable interaction.

  3. If the main demo requires the PTY server for input, start it separately:

    cd demo/server && bun install && bun run start

Exercise the bug sequence

Use whichever path is least invasive and does not require committing demo changes. Prefer direct terminal writes over typing shell commands, because shell echo can make screenshots ambiguous:

  • Best: from browser DevTools/agent-browser console, write directly to the terminal instance if it is exposed.

  • If the terminal instance is not exposed, use a throwaway local page/script or temporary console hook during dogfooding only; do not commit demo-only exposure changes.

  • Fallback: use the PTY shell only if direct writes are unavailable:

    printf '\033k/tmp\033\\\r\n\033kls\033\\demo.txt\r\n'

Expected visible result: no /tmp or ls title payload appears in the terminal grid; only intended visible text such as demo.txt appears after the title sequence terminator.

Evidence to capture

  • Screenshot after injecting the sequence showing the clean terminal grid.
  • Short video recording or browser automation recording showing the sequence being injected and the grid staying clean.
  • Console/log capture showing no unimplemented ESC action: ESC k warning during the dogfood run, if logs are available.
  • Attach screenshots/recordings to the implementation summary or PR using attach_file/GitHub upload tooling as appropriate.

Acceptance criteria

  • ESC k <text> ESC \\ payload is consumed and never rendered as visible grid text.
  • The exact issue reproduction produces row 0 '' and row 1 'demo.txt' for both string and Uint8Array writes.
  • A split-write regression proves parser state survives separate term.write() calls without leaking title payload.
  • The browser console no longer logs unimplemented ESC action: ESC k for the fixed sequence.
  • Existing OSC 0/2 title behavior and onTitleChange tests continue to pass.
  • No TypeScript pre-filter is added to lib/terminal.ts.
  • The Ghostty submodule is clean after WASM build validation.
  • Full quality gate passes: bun run fmt && bun run lint && bun run typecheck && bun test && bun run build.
  • Dogfooding evidence includes at least one screenshot and one recording or trace demonstrating the interactive sequence.

Explicit non-goals

  • Do not implement onTitleChange propagation for ESC k in this issue.
  • Do not port unrelated changes from the referenced fork commit, especially renderer/cursor behavior.
  • Do not broaden this into a general parser rewrite or TypeScript stream sanitizer.
  • Do not file or fix out-of-scope issues unless implementation uncovers a separate root problem and the required duplicate search confirms no existing issue covers it.

Out-of-scope issue protocol during implementation

If a separate bug, flaky behavior, missing feature, or nice-to-have is discovered while implementing:

  1. Stop expanding the ESC k <text> ESC \ (screen/tmux title sequence) leaks payload as visible text #153 patch.
  2. Search existing open and closed issues for the same root problem, including recently opened issues.
  3. If a match exists, comment with new evidence and link back to ESC k <text> ESC \ (screen/tmux title sequence) leaks payload as visible text #153 or the current PR when relevant.
  4. If no match exists, create a new issue with reproduction steps, expected behavior, actual behavior, why it is out of scope for ESC k <text> ESC \ (screen/tmux title sequence) leaks payload as visible text #153, and label it needs-triage.
  5. If the label is missing and permissions allow, create it; otherwise note that labeling failed.
  6. Return to the ESC k <text> ESC \ (screen/tmux title sequence) leaks payload as visible text #153 implementation scope.

Advisor review status

Approved by advisor. Non-blocking advisor refinements were incorporated: use existing test settling patterns to avoid flake, prefer direct term.write(...) dogfooding over shell echo for clear screenshots, and note the optional explicit range-split alternative if reviewers dislike relying on table overwrite behavior.


Generated with mux • Model: openai:gpt-5.5 • Thinking: xhigh

@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

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

Reviewed commit: cec12d2051

ℹ️ 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".

@ThomasK33 ThomasK33 force-pushed the implement-issue-153 branch from cec12d2 to 793c828 Compare June 26, 2026 16:28
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@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: 793c828e24

ℹ️ 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 patches/ghostty-wasm-api.patch Outdated
@ThomasK33 ThomasK33 force-pushed the implement-issue-153 branch from 793c828 to 2d6d616 Compare June 26, 2026 16:52
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@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: 2d6d616414

ℹ️ 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 patches/ghostty-wasm-api.patch
Change-Id: Ifcd1e3a10786423d8d08284f44d11433440b68b1
Signed-off-by: Thomas Kosiewski <tk@coder.com>
@ThomasK33

Copy link
Copy Markdown
Member Author

@codex review

@ThomasK33 ThomasK33 force-pushed the implement-issue-153 branch from 2d6d616 to 8f2d24d Compare June 26, 2026 17:00
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit: 8f2d24d721

ℹ️ 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".

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.

ESC k <text> ESC \ (screen/tmux title sequence) leaks payload as visible text

2 participants