Skip to content

v2.7.1 - Terminal glyph & ttyd fixes

Choose a tag to compare

@theyashgupta theyashgupta released this 29 Jul 17:32
· 374 commits to main since this release
v2.7.1
1469d63

If your terminal has ever rendered _ where an arrow, an em dash, or Claude Code's own should be, this release fixes it. The cause had nothing to do with fonts, which is where every previous investigation went, including the one that opened this release.

Underscores instead of glyphs

Dispatch bundles a Nerd Font and loads it from a data URI, so a missing glyph is the obvious suspect. It was not that. The bundled font carries all 11,794 codepoints it claims to, including every character that was rendering wrong. Those characters were already destroyed before the browser ever saw them.

ttyd attaches to your session by running tmux attach, and that tmux client inherits whatever environment the Dispatch backend started with. tmux works out whether a client can render UTF-8 by reading LANG, LC_ALL and LC_CTYPE. When Dispatch runs as the launchd service, launchd hands it a minimal environment with none of them set, the client falls back to ASCII, and it writes a literal _ in place of every non-ASCII cell on the way into the pty. The pane behind it is never wrong. Capture the pane and the text is perfect, byte for byte. Only what the client draws is broken, which is why nothing that inspected the session ever caught this, and why it looked so much like a rendering problem in the browser.

The fix is one flag. The client now starts as tmux -u, which states outright that the terminal is UTF-8 instead of inferring it from a variable that may not exist. Putting a locale into the spawn environment would have worked too, but it would also change what the claude process itself sees, and that is not a decision the terminal layer should be making on your behalf.

This was never specific to v2.7.0. Every version had it, on any machine where the Dispatch process ran without a locale. Starting Dispatch by hand from a normal shell hid it completely, because your shell exports a locale and the terminal inherited it.

ttyd processes that would not die

Chasing that bug turned up a second one underneath it. ttyd rewrites its own argument buffer when it starts, and if an earlier argument is large enough to fill that buffer, the trailing command falls off the end and never appears in ps at all. A ttyd from before v2.7.0 carries exactly such an argument, the multi-kilobyte theme blob that release retired, so it shows no tmux attach anywhere in its process listing.

Dispatch proves a ttyd is its own before sweeping it, and both of its checks missed these: the trailing command was gone, and the revision marker on an old process is by definition not the current one. So they were never swept and never reused. They simply stayed, surviving restarts and upgrades, holding their port and serving their session from the patched page v2.7.0 was supposed to have retired, while a second ttyd got spawned alongside them for the same card. On the machine that prompted this, two of them were days older than the running server and had lived through the upgrade untouched.

Ownership no longer depends on the end of the command line surviving. Dispatch now also recognises its own -b /sessions/<id>/terminal base path, which sits early enough to always survive the rewrite and is specific enough that a ttyd you started yourself is left alone. The process scan also got an explicit buffer limit and a log line when it fails: a single leaked ttyd contributes about 140KB to that listing, enough that the default limit was reachable, and the failure used to be swallowed silently, which disabled sweeping and session reuse together and looked exactly like a clean machine.

The practical effect is that upgrading clears them out. On the first restart after installing this, any stale ttyd gets swept and respawned, and the terminal you get is the one this version actually ships.

Full changelog: v2.7.0...v2.7.1

Requires Node ≥ 22.22, tmux, ttyd, git, and the Claude Code CLI. Reaching the board remotely also needs cloudflared, which Dispatch checks for only when you enable remote access. Setup is in the README.