fix(session): give the PTY a UTF-8 locale so tmux stops mangling CJK

A launchd/Finder-launched server has no LANG/LC_* at all, so tmux — which
decides per client whether the terminal is UTF-8 capable purely from those
variables — fell back to its non-UTF-8 mode and rewrote the byte stream
server-side, before it ever reached xterm:

  - every wide character became `_`  (中文测试 → ________)
  - `✓` / `⏺` / emoji became `_`
  - `═║╔╗` was downgraded to DEC Special Graphics (ESC ( 0), so rounded
    corners vanished and boxes rendered as loose horizontal lines

The `-l` login shell cannot fix this: tmux is the PTY's root process and has
already made its decision by the time the shell sources ~/.zprofile.

withUtf8Locale() fills a locale in on the spawn env, and only when needed —
an existing UTF-8 LC_ALL / LC_CTYPE / LANG (zh_CN.UTF-8, ja_JP.UTF-8, …) is
left untouched; only a missing or non-UTF-8 charset is replaced.

Existing tmux sessions do not need recreating: re-attaching with a UTF-8
client restores correct output (already-scrolled `_` stays mangled).
This commit is contained in:
Yaojia Wang
2026-07-29 08:09:48 +02:00
parent 1dbed54581
commit b1bc50ccd1
4 changed files with 155 additions and 1 deletions

View File

@@ -24,6 +24,22 @@
> 新会话读到的第一块。保持准确,只描述"此刻"。
### 🐛 [x] Mac 版中文全变 `_` + 框线渲染错乱 —— PTY 缺 UTF-8 locale(2026-07-28)
- **现象**(用户截图,Mac 版):Claude Code TUI 里所有中文变成 `_`;banner/输入框只剩零散横线,圆角边框消失。
- **根因**(实测确认,不是字体也不是前端):`src/session/session.ts` spawn PTY 时用 `env: {...process.env}`,而**服务端由 launchd/Finder 启动,环境里根本没有 `LANG`/`LC_*`**(`~/Library/LaunchAgents/com.web-terminal.base-app.plist``EnvironmentVariables` 只有 PATH/PORT/BIND_HOST)。tmux 逐 client 从 `LC_ALL`/`LC_CTYPE`/`LANG` 判断 UTF-8 能力,全空 → 进入**非 UTF-8 模式**,在**服务端就把字节改写掉**:宽字符→`_``✓ ⏺` 等→`_``═║╔╗` 降级成 DEC Special Graphics(`ESC ( 0` + `qxlk`)。所以前端 xterm 收到的已经是残骸,改字体/renderer 救不回来。
- 用户 `~/.zshrc` 里其实**有** `LANG=sv_SE.UTF-8`,但没用:tmux 是 PTY 的根进程,它做完 UTF-8 判定之后 shell 才 source profile。登录 shell(`-l`)救不了 tmux。
- node-pty 探针实证:无 LANG → `MARK ________ ==BOX== \e(0qxlk`;有 LANG → `MARK 中文测试 ==BOX== ═║╔╗ ✓`
- **修复**: 新增 `src/session/locale.ts``withUtf8Locale(env)`,在 `createSession()` 的 spawn env 上先过一道。POSIX 优先级 `LC_ALL > LC_CTYPE > LANG`;**已是 UTF-8 就原样不动**(用户的 `zh_CN.UTF-8`/`ja_JP.UTF-8` 保留),只在缺失或非 UTF-8(`C`/`POSIX`/`ISO8859-1`)时补 `en_US.UTF-8`(并清掉会盖过它的非 UTF-8 `LC_ALL`/`LC_CTYPE`)。
- **验证**:
- `test/locale.test.ts` 9 例(TDD:先 RED 再 GREEN);`tsc --noEmit` 干净。
- 端到端:用 `env -u LANG -u LC_ALL -u LC_CTYPE` 复现 launchd 环境跑 server → 新建 session → `中文测试:你好,世界!`/`🎉 ✅`/`╭─╮ ═║╔╗ █▓▒░ ⏺ ⎿` **DOM 里逐字符核对全部正确**(修复前同一条命令得到 `________`)。
- 之后 `npm run build` + `launchctl kickstart` 让真正的 launchd 单元(仍无 LANG)接管,再复验一次通过。
- 回归:`test/integration/server.test.ts`(含 ⑥ CJK ring-buffer replay、H1 tmux 重启存活)+ git-ops + worktree + projects 共 **107 例全绿**。(全量 79 文件并跑时有 4 例超时 flake,单跑全过,与本改动无关。)
- 存量 tmux session **无需重建**:探针证明用带 LANG 的新 client 重新 attach 一个"非 UTF-8 模式下创建"的旧 session,新输出即恢复正常(只有已滚进 scrollback 的旧 `_` 救不回)。
- **遗留**: `/Applications/Web Terminal.app/Contents/Resources/dist/` 里打包的是 7/18 的旧代码,**仍未修**——桌面 Electron 版要 `cd desktop && npm run dist:mac` 重打包(或就地补 `dist/session/*.js`,注意 adhoc 签名会失效)。仓库内 `dist/` 与 launchd base-app 已修好。
- **文件**: `src/session/locale.ts`(新)· `src/session/session.ts`(spawn env)· `test/locale.test.ts`(新)。
### 🔐 零接触隧道 Enrollment 产品化 + Control Panel(2026-07-18~23,当前活跃)
- **需求**: 用户要"本机 app 一跑就自动连服务器、自动管证书;手机连接时自动搞定证书"——即**产品级零接触**(引导时一次动作,之后自动签发+续期,永不再管)。诚实边界:第一次签发无法零人类动作(否则谁碰端点谁拿证书),所以"引导一次,之后免管"。