Skip to content

Keep non-ASCII startup input as UTF-8 - #15081

Merged
austinywang merged 4 commits into
mainfrom
12915-korean-osc-title-mojibake
Sep 28, 2026
Merged

austinywang merged 4 commits into
mainfrom
12915-korean-osc-title-mojibake

Conversation

@austinywang

@austinywang austinywang commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #12915

Summary

cmux workspace create --command "printf '\033]0;라마바OSC테스트\007'; sleep 90" gave the workspace a mojibake title, while --name with the same text worked. The OSC decode was fine: the shell never received UTF-8 in the first place. --command (and the other --command creation paths) passes its text to Ghostty as startup input. The embedded apprt escapes startup input with std.zig.stringEscape, which writes each non-ASCII byte as \xNN, and termio parses it back with config/string.zig, which encoded each \xNN as a UTF-8 codepoint. So 라 (EB 9D BC) reached printf as C3 AB C2 9D C2 BC, and the title it printed was UTF-8 read as Latin-1. The workspace label, sidebar, tab title, cmux workspace list, notifications and session restore all show the title Ghostty parsed, so every one of them was wrong.

The Ghostty fork now carries upstream's fix, 29b82dd80c46 ("config: preserve bytes in hex escapes"), cherry-picked in manaflow-ai/ghostty#239, so \xNN is one byte again and startup input reaches the shell unchanged. Any non-ASCII text passed with --command is fixed, not just OSC titles.

Regression window. The issue points at 0.64.25, but nothing between v0.64.24 and v0.64.25 touches this path. The Ghostty escape/parse mismatch is older; it became user-visible when --command shipped in #9614 (v0.64.23). Titles set with --name, and titles printed by commands typed into a shell, never went through startup input, which is why they looked fine.

Trade-offs

  • A text: keybind with \xNN at or above 0x80 now sends that raw byte rather than the codepoint's UTF-8. That matches upstream and Zig's own string syntax; \u{...} still sends a codepoint.
  • The fork branch starts from the current pointer edefce778, so this PR doesn't pull in the newer, unrelated commits on the fork's main. The fork PR is left open for a maintainer.
  • An OSC title that isn't valid UTF-8 is dropped by Ghostty, keeping the previous title, rather than shown with replacement characters. That's existing behavior; the new test pins it so such bytes can't come back as a Latin-1 title.

Testing

Red commit 992bf8e adds the test; fix commit 3ec1a57 bumps Ghostty; 566794a pins the GhosttyKit checksum.

  • cmuxTests/GhosttyStartupInputUTF8Tests drives the real startup-input path: it creates a surface with the exact \033]0;라마바OSC테스트\007 payload as initial input, checks the bytes the shell would read, echoes them back and asserts the title is 라마바OSC테스트. It repeats this for Japanese, emoji with a ZWJ sequence, and combining marks, and checks that an invalid-UTF-8 title never shows up as Latin-1 and doesn't block the next title.
    • Red on 992bf8e: pending, run 36359955943.
    • Green on 566794a: pending, run 36361490263 (with GhosttyCopyModeScrollbackTests, which shares the test terminal).
  • Ghostty tests parse: hex escapes are bytes and cloneParsed restores Zig-escaped bytes passed in GhosttyKit run 36360076795 at e168fd31c (75 tests), which also published the pinned xcframework.
  • Dogfood tour on red 992bf8e (run 36360118764) sent the verbatim repro through workspace.create and read workspace.list: the title came back as printf '\033]0;ë¼ë§ë°OSCíì¤í¸\007'; sleep 90, and the window title, sidebar row and tab showed the same mojibake. The after-fix tour on 566794a is pending.
  • python3 scripts/verify-local.py: 15/15 static checks passed. python3 scripts/swift_file_length_budget.py passed.
  • Tagged app repro on the final SHA: pending.

Not verified: no local native build or test run on this machine (per the machine-load rule); iOS is untouched.

Changelog

Fixed: Non-ASCII text passed with --command, such as a Korean terminal title it prints, no longer arrives in the shell as mojibake

Checklist

  • Behavior changes have added or updated tests, or Testing says why not
  • UI, settings, menu, schema, help-text or user-facing docs change: localization audited — no user-facing strings changed
  • User-facing docs updated if needed — docs/ghostty-fork.md records the fork change

🤖 Generated with Claude Code


Summary by cubic

Fixes #12915: non-ASCII text passed with cmux workspace create --command reached the shell as mojibake, so a Korean OSC title printed by the command showed mojibake in the workspace label, sidebar, tab, list, and notifications. Ghostty escaped each non-ASCII byte of startup input as \xNN and termio decoded each escape as a UTF-8 codepoint; the fork now preserves bytes in hex escapes so startup input reaches the shell unchanged. The issue became user-visible when --command shipped (v0.64.23); --name titles and titles printed by commands typed into a shell never went through startup input.

Side effects

  • A text: keybind that uses \xNN at or above 0x80 now sends that raw byte; \u{...} still sends a codepoint.
  • An OSC title that isn't valid UTF-8 is dropped, keeping the previous title — existing behavior now pinned by a test.

Testing

  • New GhosttyStartupInputUTF8Tests drives the real startup-input path with the issue's exact payload, covering Korean, Japanese, emoji with a ZWJ sequence, and combining marks, plus invalid UTF-8.
  • The Ghostty parse and round-trip tests now run in build-ghosttykit.yml before packaging.
  • Branch merged with main (a616a2b); the only conflict was the pbxproj file, resolved by unioning the added test entries then re-normalizing.

Written for commit 7029467. Summary will update on new commits.

Review in cubic

austinywang and others added 3 commits September 27, 2026 16:48
`cmux workspace create --command` types its command into the new shell
as Ghostty startup input. When that command prints an OSC 0 title, the
title comes from the bytes Ghostty wrote to the child. Drive the real
startup-input path with the issue's exact payload, echo the captured
bytes back as program output, and assert the surface title, covering
Korean, Japanese, emoji and combining marks. A second test checks that
an OSC title with invalid UTF-8 never becomes a Latin-1 title and does
not block the next valid one.

Fails today: every non-ASCII byte arrives re-encoded as a Latin-1
codepoint, so the title is mojibake.

Refs #12915

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bump the Ghostty fork to e168fd31c. `cmux workspace create --command`
passes its command as Ghostty startup input, which the embedded apprt
escaped with std.zig.stringEscape (`\xNN` per non-ASCII byte) and termio
parsed back as one UTF-8 codepoint per escape. Korean, Japanese, emoji
and any other non-ASCII command text reached the shell as mojibake, so an
OSC 0/2 title the command printed showed as mojibake in the workspace
label, sidebar and `cmux workspace list`. The fork cherry-picks upstream
29b82dd80c46 ("config: preserve bytes in hex escapes") and adds a
round-trip test; build-ghosttykit.yml runs both tests before packaging.

Fixes #12915

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

Catch-up merge by scripts/ci/catch_up_pr.py (RFC #14631).
Merged by scripts/merge-main.sh: origin/main at a616a2b.

Resolved conflicts:
- cmux.xcodeproj/project.pbxproj: union of added entries, then normalize-pbxproj.py

Catch-up-previous-head: 566794a
Catch-up-base: a616a2b
@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

Warning

Review limit reached

Next included review available in 18 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Repository: manaflow-ai/cmux/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 836bc032-67b0-47bb-8bb4-7d4aacb4e39d

📥 Commits

Reviewing files that changed from the base of the PR and between a616a2b and 7029467.

📒 Files selected for processing (7)
  • .github/workflows/build-ghosttykit.yml
  • cmux.xcodeproj/project.pbxproj
  • cmuxTests/GhosttyStartupInputUTF8Tests.swift
  • cmuxTests/ScrollbackTestTerminal.swift
  • docs/ghostty-fork.md
  • ghostty
  • scripts/ghosttykit-checksums.txt
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@austinywang

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 28, 2026 •

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@austinywang
austinywang merged commit 847c919 into main Sep 28, 2026
61 checks passed
@austinywang
austinywang deleted the 12915-korean-osc-title-mojibake branch September 28, 2026 00:50
@github-actions

Copy link
Copy Markdown
Contributor

Merge receipt for 7029467d1a: every check was green at merge (16 verified; 15 skipped by policy). Full suite runs on main after merge.

rustybret pushed a commit to rustybret/bmux that referenced this pull request Sep 28, 2026
2604935 coderouter: wait for sticky credential refreshes on a lease-completion signal (manaflow-ai#15087)
369cd16 coderouter: scope org API keys to team-shared accounts (manaflow-ai#15086)
847c919 Keep non-ASCII startup input as UTF-8 (manaflow-ai#15081)
a616a2b Remove obsolete bash PR watcher loops (manaflow-ai#15075)

# Conflicts:
#	.github/workflows/build-ghosttykit.yml
#	.github/workflows/ci-guards.yml
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.

0.64.25 Korean/non-ASCII OSC terminal titles render as mojibake

1 participant