Skip to content

fix(web-shell): prevent terminal query freezes and clean up protocol rejection - #11748

Merged
wenshao merged 1 commit into
mainfrom
codex/web-terminal-followup
Sep 13, 2026
Merged

wenshao merged 1 commit into
mainfrom
codex/web-terminal-followup

Conversation

@yiliang114

@yiliang114 yiliang114 commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Fixes the remaining Web Shell terminal follow-ups from #11643: preserves mode-query handling in minified output, releases the PTY when rejecting an older daemon, and localizes the restart notice. Adds regression coverage for failed-spawn responder cleanup and the six published node-pty pins, and documents that reconnect does not answer queries emitted while the browser was offline.

Why it's needed

The maintainer's round-2 verification found that esbuild 0.25.6 can miscompile xterm's logical assignments when lowering to ES2020, freezing the terminal after a DECRQM query. The production target is now ES2021. Separately, protocol rejection used to leave the older daemon's shell alive until idle reclamation because the client closed without requesting release.

Reviewer Test Plan

How to verify

  • Send a DECRQM query followed by text to the production terminal. It should answer the query and continue displaying output.
  • Connect the new client to an older daemon. It should send release before closing with code 4002 and display a localized restart notice. Closing the tab afterward should not send release again.
  • When both Windows backend spawn attempts fail, the responder should be disposed and the terminal ID reusable. Package preparation should preserve all six native PTY pins from the core manifest.

Evidence (Before & After)

Baseline real-daemon/browser evidence: maintainer round 2. This follow-up verifies parser behavior and control ordering with focused tests; it does not repeat that full E2E run.

Local: TerminalPanel 22/22, web-terminal-registry 43/43, package-assets 38/38; changed-file ESLint passed. The real xterm module minified with locked esbuild 0.25.6 passes at ES2021; the same test at ES2020 fails with ReferenceError: i is not defined in requestMode.

Tested on

OS Status
🍏 macOS Focused tests and lint passed
🪟 Windows Not rerun
🐧 Linux Not run locally

Environment (optional)

Node 22.22.0. Existing dependencies were copied into an isolated worktree. Registry tests use the changed source with existing core build artifacts for package-entry prerequisites.

Risk & Scope

  • The standalone Web Shell now targets ES2021. Native ConPTY behavior and responder ownership are unchanged.
  • Web Shell typecheck could not pass in this worktree: SDK daemon build declarations are missing. Full repository build/typecheck and real-daemon browser E2E are not claimed; CI remains the full build gate. The local review-gate and pr-ui-verify skills were unavailable; the focused code review and parser/component checks are the local evidence.
  • No transport migration is added. The existing protocol mismatch path now explicitly releases its PTY.
  • Updated designs: English / 简体中文.

Linked Issues

Follow-up to #11643. Refs #11734; its replay-suppression implementation already merged in #11643, and this PR documents the remaining offline-query boundary.

中文说明

改动内容

处理 #11643 剩余的 Web Shell 终端后续问题:避免压缩产物中的模式查询冻结,在拒绝旧 daemon 时释放 PTY,并本地化重启提示。补上 spawn 失败释放应答器、发布包六个 node-pty 版本声明的回归测试,并说明浏览器离线期间的查询不会在重连时补答。

修改原因

维护者第 2 轮验证发现 esbuild 0.25.6 向 ES2020 降级时可能错误编译 xterm 的逻辑赋值,导致 DECRQM 查询后终端冻结。生产构建目标现改为 ES2021。另外,旧的协议拒绝路径只断开连接而不请求释放,旧 daemon 的 shell 会残留至空闲回收。

验证计划

发送 DECRQM 查询及后续文字,应得到应答且继续显示输出。新客户端连接旧 daemon 时,应在以 4002 关闭之前发送 release 并显示本地化重启提示;随后关闭页签不应重复释放。Windows 两个后端都 spawn 失败时,应释放应答器并允许重用终端 ID;发布包应完整继承 core manifest 的六个 PTY 版本声明。

前后证据

基线真实 daemon/浏览器证据见上方维护者第 2 轮报告。本轮通过定向测试验证解析行为和控制帧顺序,没有重跑完整 E2E。TerminalPanel 22/22、registry 43/43、package-assets 38/38 通过;改动文件 ESLint 通过。真实 xterm 模块经锁定的 esbuild 0.25.6 压缩后,在 ES2021 下通过;同一测试改为 ES2020 时在 requestModeReferenceError: i is not defined

测试平台与环境

macOS 定向测试和 lint 通过;Windows 未重跑,Linux 未在本地测试。Node 22.22.0,在独立工作树中使用现有依赖副本。registry 测试加载改动源码,包入口前置条件使用已有 core 构建产物。

风险与范围

独立 Web Shell 的构建目标升为 ES2021;原生 ConPTY 行为和应答归属不变。工作树缺少 SDK daemon 构建声明,Web Shell 类型检查未通过;不宣称全仓 build/typecheck 或真实 daemon 浏览器 E2E 通过,由 CI 承担完整构建验证。本地 review-gate 和 pr-ui-verify skills 不可用,本轮证据是定向代码检查及解析器/组件测试。不新增协议迁移,仅在现有不匹配路径显式释放 PTY。英文和中文设计文档同步更新,链接见上方。

关联

#11643 的后续修复;关联 #11734,其回放抑制已由 #11643 合入,本 PR 补充离线查询边界说明。

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Focused verification on macOS / Node 22.22.0:

  • TerminalPanel: 22 passed, including release-before-close on an old-daemon snapshot and no duplicate release on tab close.
  • WebTerminalRegistry: 43 passed, including responder disposal after both spawn attempts fail.
  • Package assets: 38 passed, including all six derived node-pty pins with a changed fixture version.
  • Minified real xterm: 1 passed with the configured ES2021 target and repository esbuild 0.25.6. The same test with ES2020 fails with ReferenceError: i is not defined in requestMode; the temporary negative-control test was removed.
  • Changed-file ESLint and formatting passed. Full diff reviewed locally.

The xterm test exercises the real compiled module and parser under jsdom, not a full Vite build or real-browser E2E. Existing copied dependencies included a Vite-private esbuild 0.28.2 override, so the regression deliberately uses the repository's locked root compiler (0.25.6). Web Shell typecheck remains unverified because SDK daemon build declarations are missing in this worktree. Windows native behavior was not rerun.

中文

macOS / Node 22.22.0:TerminalPanel 22 项、registry 43 项、package assets 38 项、真实 xterm 压缩回归 1 项通过,共 104 项。ES2020 负向对照在 requestMode 触发 ReferenceError: i is not defined,ES2021 正常应答并继续解析输出。临时负向测试已删除。改动文件 ESLint 和格式检查通过,已做本地完整差异检查。

xterm 测试覆盖真实编译模块及 jsdom 中的 parser,不是完整 Vite 构建或真实浏览器 E2E。依赖副本中的 Vite 私有 esbuild 曾升级到 0.28.2,因此回归明确使用仓库锁定的根版本 0.25.6。新工作树缺 SDK daemon 构建声明,Web Shell 类型检查尚未验证通过;Windows 原生行为未重跑。

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

⚠️ Qwen Triage ended earlyview run. It stopped before finishing; check the run log.

⚠️ Qwen Triage 提前结束 —— 查看运行。未跑完,请查看运行日志。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓ — every required section is present with real content. (The opening heading reads ## What does this PR do? where the template says ## What this PR does; the substance is complete, so not worth a round-trip.)

Problem: two things are bundled here and they don't have the same evidentiary standing.

  • The terminal freeze is an observed bug. It came out of the maintainer's round-2 verification on fix(core): run web terminal PTYs on the bundled ConPTY backend #11643 (linked), with a concrete failure signature (ReferenceError: i is not defined in requestMode) and a named mechanism — esbuild lowering xterm's ES2021 logical assignments when the target list includes ES2020. That is exactly the evidence this gate asks for.
  • The PTY surviving a protocol rejection has no reproduction. The mechanism is real on reading — the client closed with 4002 without ever sending release, so the daemon held the shell until idle reclamation. But idle reclamation already cleans it up, and that branch only fires when client and daemon are out of version sync, which the design doc says shouldn't happen. So it reads as "deterministic instead of eventual" cleanup rather than a user-visible bug. Fine to ride along at three lines in a follow-up to your own just-merged PR; I'd have pushed back on it as a standalone.

Direction: aligned. This is the tail of #11643, which landed as b5567bb7 (current main HEAD), and it closes out items that review raised rather than opening new surface. Worth saying explicitly, because it is a shipped-artifact property and not just an internal one: build.target sets the syntax floor of the published standalone Web Shell bundle. Vite 5's default ('modules') is roughly es2020 / chrome87 / firefox78 / safari14, and es2021 needs logical-assignment support (Chrome 85, Firefox 79, Safari 14) — so the effective floor barely moves and there is no meaningful compat regression. CHANGELOG: no direct reference, but the area is clearly relevant.

Size: 12 production lines — TerminalPanel.tsx 6, i18n.tsx 4, vite.config.ts 2. 150 test lines across four files, 11 doc lines. The only core-path file is packages/core/src/services/web-terminal-registry.test.ts, and it is a test — no core production change, so the Stage 0 core bar isn't engaged. Well under every advisory threshold.

Approach: scope is right, with one honest question. The five items are a grab-bag, but they all trace back to #11643, so bundling them is defensible. The heavy part is the new 97-line scripts/tests/web-terminal-build.test.ts — it pulls the real xterm.mjs through esbuild, evaluates it in a JSDOM context, and pokes _core._inputHandler.parse() for a synchronous oracle. Reaching into private xterm internals is brittle across upgrades, but for a compiler regression I don't see a cheaper way to get a behavioural oracle, and the file says why. My reservation is about fidelity rather than size and I've written it up in Stage 2 — short version: the test compiles xterm with a different esbuild than the one that builds the shipped bundle.

Risk: no elevated risk signals — none of the changed non-test files match the revert-correlated path list.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓ —— 所有必需章节都有实质内容。(开头标题写的是 ## What does this PR do?,模板里是 ## What this PR does;内容齐全,不值得为此来回一轮。)

问题: 这里捆了两件事,证据强度并不相同。

  • 终端冻结是已观测到的 bug。它来自维护者在 fix(core): run web terminal PTYs on the bundled ConPTY backend #11643 的第 2 轮验证(已附链接),有具体失败特征(requestMode 中的 ReferenceError: i is not defined)和明确机制——当 target 列表包含 ES2020 时,esbuild 会对 xterm 的 ES2021 逻辑赋值做降级并产生错误代码。这正是本 gate 要求的证据。
  • 协议拒绝后 PTY 残留没有复现。从代码看机制是真实的——客户端以 4002 关闭却从未发送 release,daemon 因此持有该 shell 直到空闲回收。但空闲回收本来就会清理,而且这个分支只在客户端与 daemon 版本不一致时触发,设计文档明确说两端应同步升级。所以它更像是"确定性清理取代最终清理",而不是用户可见的 bug。作为你自己刚合入 PR 的后续、只占三行,顺带做掉没问题;如果是独立 PR 我会反对。

方向: 对齐。这是 #11643 的收尾,该 PR 已作为 b5567bb7(当前 main HEAD)合入;本次处理的是 review 提出的遗留项,没有开辟新面。有一点需要明说,因为它是发布产物的属性而非纯内部实现:build.target 决定了独立 Web Shell 产物的语法下限。Vite 5 的默认值('modules')大致是 es2020 / chrome87 / firefox78 / safari14,而 es2021 只额外要求逻辑赋值支持(Chrome 85、Firefox 79、Safari 14),所以实际下限几乎没有变化,不存在有意义的兼容性回退。CHANGELOG:无直接对应条目,但该领域显然相关。

规模: 生产代码 12 行——TerminalPanel.tsx 6 行、i18n.tsx 4 行、vite.config.ts 2 行。测试 150 行(四个文件),文档 11 行。唯一触及核心路径的文件是 packages/core/src/services/web-terminal-registry.test.ts,且是测试文件——核心生产代码未改动,因此不涉及 Stage 0 的核心门槛。远低于所有提示性阈值。

方案: 范围合理,但有一个坦率的疑问。五项改动是个组合包,不过都能追溯到 #11643,捆在一起说得通。最重的是新增的 97 行 scripts/tests/web-terminal-build.test.ts——它把真实的 xterm.mjs 过一遍 esbuild,在 JSDOM 上下文中执行,并通过 _core._inputHandler.parse() 拿到同步断言点。依赖 xterm 私有内部结构在升级时容易脆断,但对于编译器回归,我想不到更便宜的行为级验证方式,文件里也说明了原因。我的保留意见在于保真度而非体量,已写在 Stage 2——简版:该测试编译 xterm 用的 esbuild,和构建实际发布产物用的 esbuild 不是同一个。

风险: 无升级风险信号——改动的非测试文件均未命中与 revert 相关的路径列表。

进入代码审查 🔍

Qwen Code · qwen3.8-max-2026-09-02

Reviewed at 538ae5cc1690b3640b18371c970fe9b78708308f · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

Code review

Static review only — this run does not execute any PR-derived code (see Testing evidence below for what that means for the claims).

TerminalPanel.tsx — release before reject is correct. Setting releaseRequested = true before the send is what makes it safe, and it's doing two jobs at once: the callback registered in releaseCallbacks early-returns on that flag, so the later tab-close path can't emit a second release, and ws.onclose early-returns too, so no reconnect gets scheduled. 4002 is already in NON_RETRYABLE_CLOSE_CODES, so that half is belt-and-braces rather than load-bearing. Send-then-close on an OPEN socket preserves frame order. The new assertions pin exactly the two properties worth pinning — ordering via invocationCallOrder, and no-duplicate-release by calling releaseWebTerminal afterwards and asserting the send count didn't move.

Considered and dismissed — injecting a control frame into an old daemon's shell. This branch fires precisely when the daemon is too old to have sent a snapshot control frame, so I checked whether \x00{"type":"release"} could land as PTY input there instead of being parsed. It isn't a new hazard: the base code already sends \x00{"type":"resize",…} unconditionally in ws.onopen, before any mismatch can be detected, so a daemon naive enough to treat NUL frames as input was already exposed. The current daemon also drops unrecognised NUL-prefixed text frames rather than writing them through (packages/cli/src/serve/routes/terminal.ts:276). No regression, and the cleanup is a strict improvement on daemons that do understand the frame.

i18nEN and ZH are the only two Messages tables in i18n.tsx, and both received terminal.notice.protocolMismatch. The string moved verbatim out of the hardcoded literal. ✓

Registry testloadXtermHeadless() is awaited exactly once per create, and only on the win32 bundled-ConPTY branch, so mockResolvedValueOnce is sufficient rather than leaking into a second call. The stub constructor returns responder, which makes queryTerminal === responder, and the spawn-failure catch calls queryTerminal?.dispose() before finishCreating. So toHaveBeenCalledOnce() genuinely pins cleanup that previously had no coverage. ✓

package-assets testpackages/core/package.json declares exactly six @lydell/node-pty* optional dependencies, so toHaveLength(6) matches reality instead of being a magic number that drifts silently. Deriving the expected pins from the real manifest rather than restating them is the right shape. ✓

Docs — the EN and zh-CN additions correspond one-to-one (the offline-query boundary, and release-before-reject with a localized notice), both updated in the same change. ✓

One finding worth your attention (non-blocking)

The new build test doesn't exercise the compiler that builds the shipped bundle.

packages/web-shell builds with its own nested Vite — packages/web-shell/node_modules/vite resolves to 5.4.21 — and Vite 5.4.21 depends on esbuild@^0.21.3, which the lockfile resolves to packages/web-shell/node_modules/esbuild0.21.5. Neither the root nor the web-shell manifest carries an esbuild override, so 0.21.5 is what lowers and minifies the production bundle.

The new test sits in scripts/tests/, so its bare import { transform } from 'esbuild' resolves to the root esbuild@0.25.6, and its import { resolveConfig } from 'vite' resolves to the root vite@7.3.6 — not the 5.4.21 that actually loads this config file during a build.

So the test proves esbuild@0.25.6 at es2021 handles real xterm correctly. It doesn't prove the shipped bundle is free of the miscompilation, and it wouldn't notice a regression in the compiler that actually ships. The in-file comment — "a locally overridden Vite dependency may already fix the logical-assignment lowering bug and mask this regression" — reads as though the nested 0.21.5 were an accidental local override; per the lockfile it's simply Vite 5.4.21's own dependency, i.e. the production compiler.

None of this makes the fix wrong: target: 'es2021' is read by the real build and does lift the floor above the es2020 entry in Vite 5's default 'modules' target (es2020 / edge88 / firefox78 / chrome87 / safari14), which is the right direction. But it leaves a question the diff can't answer — if 0.21.5 doesn't share 0.25.6's lowering bug, then es2021 may not be what actually cured the observed freeze. If you want the test to be load-bearing, resolve both modules from the package instead of from the test's own directory, e.g. build a createRequire off packages/web-shell/package.json and require.resolve('esbuild') / require.resolve('vite') through it, so the harness compiles with the same toolchain as npm -w packages/web-shell run build.

Minor, and not worth a change on its own: config.build.target || 'esnext' is effectively dead, since Vite always populates a default. If the explicit target were ever removed, Vite 7's default (baseline-widely-available) isn't a string esbuild accepts as a target, so the test would fail with a confusing esbuild error instead of a clear one. It still fails, so the guard holds — it just fails ungracefully.

Testing evidence

What this comment carries: static review of the diff plus the PR's own CI check results read through the API. This is an unattended pull_request_target run, so nothing was built, executed, or driven in a browser by the gate — no PR-derived code runs here, because the agent environment holds a write PAT.

Nothing has failed so far, but the four jobs that would actually exercise this diff were still in flight when I read them, so there is no green signal to report yet. I don't poll; the finalize workflow rewrites the table below once CI settles.

Test (ubuntu-latest, Node 22.x) is the one to watch for the new harness: npm run test:ci chains into test:scripts, which runs scripts/tests/vitest.config.ts — and its include is scripts/tests/**/*.test.{js,ts}, so the new .ts file is collected. That job is the arbiter of whether resolveConfig can load a Vite-5-era config (with @vitejs/plugin-react@4.7.0 and @tailwindcss/vite@4.3.2) through root Vite 7.3.6 without throwing.

Final CI results for 538ae5c (auto-updated by the triage finalize job after CI completed):

Check Conclusion
Capture web-shell visuals (ubuntu-latest, Node 22.x) ✅ success
Classify PR ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
Integration Tests (no-AK, No Sandbox) ✅ success
Lint & Static (ubuntu-latest, Node 22.x) ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

Skipped entries above are the platform matrix legs this PR didn't trigger plus bot-orchestration jobs (ack-review-request, review-pr, precheck-pr, resolve-pr, verify, tmux-testing and friends) that don't apply to this run; I've left the meaningful ones and dropped the rest of the 71 checks rather than padding the table.

Not verified, explicitly:

  • The ES2020 failure the fix is premised on — ReferenceError: i is not defined in requestMode. That is the author's local run at esbuild 0.25.6, quoted as their claim, not re-executed here.
  • The author's local counts (TerminalPanel 22/22, web-terminal-registry 43/43, package-assets 38/38, changed-file ESLint) — author's claim, macOS / Node 22.22.0 only. Windows not rerun, Linux not run locally.
  • Real-daemon / browser E2E. The author does not claim it either; the baseline is the maintainer's round-2 report on fix(core): run web terminal PTYs on the bundled ConPTY backend #11643.
  • Whether the shipped bundle is or was miscompiled — see the finding above. No test in this PR touches esbuild 0.21.5.
  • Full-repo build and typecheck: the description says the Web Shell typecheck could not pass in the author's worktree (missing SDK daemon build declarations). CI is the gate for that, and it hasn't reported yet.

Sandboxed verification would settle this: @qwen-code /verify — specifically, whether the production web-shell bundle (nested Vite 5.4.21 + esbuild 0.21.5) actually miscompiles xterm's logical assignments at the pre-PR target and actually answers DECRQM at es2021. That is the load-bearing claim behind the whole fix, and the new regression test compiles with root esbuild 0.25.6, so it structurally cannot answer it; an A/B against the base build would. /tmux is the wrong lane here — this is a browser Web Shell surface, not the TUI. The in-progress Capture web-shell visuals job renders the real client and is the closest native signal, though it won't diff ES2020 against ES2021.

Real-scenario testing (2c): N/A — unattended CI run, so no tmux capture was driven and none is claimed.

中文说明

代码审查

仅静态审查——本轮不执行任何来自 PR 的代码(这对各项论断意味着什么,见下方"测试证据")。

TerminalPanel.tsx——先释放再拒绝,写法正确。 关键在于发送之前就把 releaseRequested = true,它同时承担了两件事:注册在 releaseCallbacks 里的回调会因该标志提前返回,所以后续关闭页签的路径不会重复发送 releasews.onclose 同样提前返回,因此不会安排重连。4002 本来就在 NON_RETRYABLE_CLOSE_CODES 里,所以那一半属于双保险而非承重结构。在 OPEN 状态的 socket 上先 send 后 close 能保证帧顺序。新增断言恰好固定了两个值得固定的性质——通过 invocationCallOrder 验证顺序,以及随后调用 releaseWebTerminal 并断言 send 次数没有增加,验证不会重复释放。

已考虑并排除——把控制帧当成输入注入旧 daemon 的 shell。 这个分支恰好在 daemon 旧到没发快照控制帧时触发,所以我确认了 \x00{"type":"release"} 是否可能被当成 PTY 输入写入而非被解析。这不是新增风险:基线代码在 ws.onopen 里本来就无条件发送 \x00{"type":"resize",…},而这发生在任何不匹配被检测到之前,所以一个会把 NUL 帧当输入的 daemon 早就暴露了。当前 daemon 也会丢弃无法识别的 NUL 前缀文本帧而不写入 PTY(packages/cli/src/serve/routes/terminal.ts:276)。没有回归,而在能理解该帧的 daemon 上,这次清理是严格改进。

i18n——i18n.tsx 中只有 ENZH 两个 Messages 表,两者都加上了 terminal.notice.protocolMismatch,字符串从硬编码字面量原样迁移。✓

registry 测试——loadXtermHeadless() 每次 create 只 await 一次,且仅在 win32 bundled-ConPTY 分支执行,因此 mockResolvedValueOnce 足够,不会泄漏到第二次调用。桩构造函数返回 responder,使 queryTerminal === responder;spawn 失败的 catch 会在 finishCreating 之前调用 queryTerminal?.dispose()。所以 toHaveBeenCalledOnce() 确实固定了此前没有覆盖的清理逻辑。✓

package-assets 测试——packages/core/package.json 恰好声明六个 @lydell/node-pty* 可选依赖,所以 toHaveLength(6) 与事实一致,而不是一个会悄悄漂移的魔法数字。从真实 manifest 推导期望值而不是复述它,方向是对的。✓

文档——英文与中文的新增内容一一对应(离线期间查询的边界、以及拒绝前释放并显示本地化提示),在同一次改动中同步更新。✓

一个值得注意的问题(非阻塞)

新增的构建测试并没有使用真正构建发布产物的那个编译器。

packages/web-shell 用的是它自己嵌套的 Vite——packages/web-shell/node_modules/vite 解析为 5.4.21——而 Vite 5.4.21 依赖 esbuild@^0.21.3,lockfile 将其解析为 packages/web-shell/node_modules/esbuild0.21.5。根 manifest 和 web-shell manifest 都没有 esbuild override,所以生产产物的降级与压缩用的是 0.21.5。

新测试位于 scripts/tests/,因此它的裸导入 import { transform } from 'esbuild' 解析到 esbuild@0.25.6import { resolveConfig } from 'vite' 解析到 vite@7.3.6——而不是构建时真正加载这个配置文件的 5.4.21。

所以该测试证明的是:esbuild@0.25.6es2021 下能正确处理真实 xterm。它并没有证明发布产物不存在错误编译,也不会察觉真正参与发布的编译器发生回归。文件内的注释——"本地被覆盖的 Vite 依赖可能已修复逻辑赋值降级 bug,从而掩盖此回归"——读起来像是把嵌套的 0.21.5 当成了意外的本地覆盖;但按 lockfile,它就是 Vite 5.4.21 自己的依赖,也就是生产编译器。

这并不说明修复本身有问题:target: 'es2021' 会被真实构建读取,并且确实把下限提到了 Vite 5 默认 'modules' target(es2020 / edge88 / firefox78 / chrome87 / safari14)中 es2020 之上,方向正确。但它留下一个 diff 无法回答的问题——如果 0.21.5 并不存在 0.25.6 的降级 bug,那么 es2021 可能并不是真正治好所观测冻结的原因。如果希望这个测试真正承重,应让两个模块从该 package 解析而非从测试自身目录解析,例如基于 packages/web-shell/package.json 构造 createRequire,再通过它 require.resolve('esbuild') / require.resolve('vite'),使验证工具链与 npm -w packages/web-shell run build 一致。

次要问题,单独看不值得改:config.build.target || 'esnext' 实际上是死代码,因为 Vite 总会填一个默认值。若显式 target 被移除,Vite 7 的默认值(baseline-widely-available)不是 esbuild 接受的 target 字符串,测试会以一个令人困惑的 esbuild 错误失败,而不是清晰的失败。它仍会失败,所以防护有效——只是失败得不够优雅。

测试证据

本评论承载的证据: 对 diff 的静态审查,加上通过 API 读取的 PR 自身 CI 检查结果。这是一次无人值守的 pull_request_target 运行,因此 gate 没有构建、执行任何代码,也没有在浏览器中操作——这里不运行 PR 代码,因为 agent 环境持有写入 PAT。

目前没有任何失败,但真正会验证这个 diff 的四个 job 在我读取时仍在运行,所以还没有可报告的绿灯信号。我不做轮询;CI 结束后 finalize workflow 会就地重写下方表格。

Test (ubuntu-latest, Node 22.x) 是新验证工具最需要关注的一项:npm run test:ci 会接续 test:scripts,后者运行 scripts/tests/vitest.config.ts,其 includescripts/tests/**/*.test.{js,ts},所以新增的 .ts 文件会被收集。该 job 是判定 resolveConfig 能否用根 Vite 7.3.6 加载一份 Vite 5 时代的配置(含 @vitejs/plugin-react@4.7.0@tailwindcss/vite@4.3.2)而不抛错的裁判。

上方表格中被 skip 的条目,是本 PR 未触发的平台矩阵分支,以及本次运行不适用的 bot 编排 job(ack-review-requestreview-prprecheck-prresolve-prverifytmux-testing 等);我保留了有意义的条目,没有把全部 71 个 check 塞进表格充数。

明确未验证的部分:

  • 修复所依据的 ES2020 失败——requestMode 中的 ReferenceError: i is not defined。这是作者在 esbuild 0.25.6 下的本地运行结果,此处作为其主张引用,未重新执行。
  • 作者的本地测试数(TerminalPanel 22/22、web-terminal-registry 43/43、package-assets 38/38、改动文件 ESLint)——作者主张,仅 macOS / Node 22.22.0。Windows 未重跑,Linux 未本地运行。
  • 真实 daemon / 浏览器 E2E。作者也未宣称;基线是 fix(core): run web terminal PTYs on the bundled ConPTY backend #11643 上维护者的第 2 轮报告。
  • 发布产物是否曾经或现在被错误编译——见上述发现。本 PR 没有任何测试触及 esbuild 0.21.5。
  • 全仓 build 与 typecheck:描述中说明作者工作树里 Web Shell typecheck 无法通过(缺少 SDK daemon 构建声明)。这由 CI 把关,而 CI 尚未出结果。

沙箱验证可以解决这一点:@qwen-code /verify——具体而言,验证生产 web-shell 产物(嵌套 Vite 5.4.21 + esbuild 0.21.5)在 PR 之前的 target 下是否真的会错误编译 xterm 的逻辑赋值,以及在 es2021 下是否真的能应答 DECRQM。这是整个修复背后承重的论断,而新增回归测试用的是根 esbuild 0.25.6,结构上无法回答它;与 base 构建做 A/B 才可以。/tmux 在这里不是合适的通道——这是浏览器端 Web Shell 界面,不是 TUI。正在进行中的 Capture web-shell visuals job 会渲染真实客户端,是最接近的原生信号,但它不会对比 ES2020 与 ES2021。

真实场景测试(2c):N/A——无人值守 CI 运行,因此没有驱动 tmux 抓取,也不作此宣称。

Qwen Code · qwen3.8-max-2026-09-02

Reviewed at 538ae5cc1690b3640b18371c970fe9b78708308f · re-run with @qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

Rendered against a mock daemon (no real backend): the PR base vs this PR head 538ae5c. Only screenshots that changed are shown (flows below, if any, are head-only) — refreshes on every push.

Screenshots · before / after

ℹ️ No screenshot changed against the PR base — but this PR edits 2 render-shaping files:

  • packages/web-shell/client/components/terminal/TerminalPanel.tsx
  • packages/web-shell/client/i18n.tsx

Either the change has no visual effect (logic, plumbing, a state the scenarios never reach), or no scenario renders this UI — in which case the preview cannot see it, and an empty result is a coverage gap rather than a clean bill of health. To make it visible, add a scenario to packages/web-shell/client/e2e/visuals/screenshots.spec.ts that seeds whatever state the UI is gated on; it then appears here as a head-only (NEW) capture.

Full-resolution recordings (.webm) are attached to the workflow run.

Qwen Code · web-shell visuals

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — the code is clean and safe; what I can't settle is whether the headline fix actually cures the observed freeze in the bundle we ship.

Going back to what I'd have written myself: for the freeze I'd have raised the build target so the buggy lowering never runs, plus a regression test that compiles the real dependency rather than asserting a config string — which is what this does, and its behavioural oracle is better than the expect(target).toBe('es2021') I'd probably have settled for. For the PTY leak I'd have written the same three lines, including setting releaseRequested first so the tab-close path can't double-release. So the approach isn't my reservation. I didn't find a simpler path it missed.

The reservation is that the PR's own verification stops short of the thing it claims to fix. Production builds with nested Vite 5.4.21 and its own esbuild 0.21.5; the new harness compiles with root Vite 7.3.6 and root esbuild 0.25.6. The freeze was observed pre-fix by the maintainer in a real browser against a real daemon, and nobody has observed the post-fix production bundle — the description is upfront about that ("does not repeat that full E2E run", "real-daemon browser E2E are not claimed"). So the causal chain is: a real bug was seen, a plausible mechanism was named, a fix was applied that addresses that mechanism in a different compiler than the one that produced the symptom, and the evidence offered is from that different compiler.

Two reasons I'm not treating that as a blocker. The change is monotonic — lifting the floor above the es2020 entry in Vite 5's default target can only reduce lowering, it cannot introduce the bug, and the compat cost is effectively nil (Firefox 78→79 for logical assignment; Chrome 87 and Safari 14 already clear the bar). And the rest of the PR stands on its own: the protocol-rejection fix is verifiable by reading, correctly ordered, and its test pins the two properties that matter; the registry and package-assets tests close real coverage gaps; both design docs are updated in lockstep.

Two reasons I'm not approving either. Green CI won't close the gap — Test (ubuntu-latest) running the new harness proves it loads and passes under root Vite 7 with esbuild 0.25.6, not that the shipped bundle answers DECRQM. And if I'm honest about the six-month view: I'd thank them for the TerminalPanel and registry changes, and mildly curse the build test. Ninety-seven lines reaching into _core._inputHandler, guarding a compiler we don't ship with, will break on the first xterm or Vite upgrade for reasons unrelated to the bug it exists to catch — and when it breaks someone will "fix" it by relaxing it, having never learned whether it was protecting anything.

Not volume fatigue and not vagueness — this is one PR and my hesitation is specific and nameable.

What would move me to a 4: either @qwen-code /verify A/B against the base build showing that the production bundle (nested Vite 5.4.21 + esbuild 0.21.5) freezes at the old target and answers DECRQM at es2021; or the harness resolving esbuild and vite from packages/web-shell instead of from scripts/tests, so that its pass is actually evidence about what we ship. The second is a small change and makes the first much cheaper to trust.

⏸️ Deferring to @BenGuanRan — needs a human call on whether the unverified production-toolchain link above is acceptable to merge as-is, or whether the harness should be repointed first. Two pull_request workflow runs (Qwen Code CI, Web-shell Visuals) were still in flight at review time; because a green run wouldn't resolve the concern, I have deliberately not emitted the deferred-approval marker, so nothing will auto-approve this on CI completion. The bot's deterministic owner resolver found no area label and no prior human reviewer here, so I'm routing to the PR's existing repo-assigned owner rather than guessing a login.

Nothing in this PR is broken and I'd merge it after the harness repoint. Flagging rather than blocking because the author is a maintainer and the call about verification depth on their own follow-up is theirs to make.

中文说明

置信度:3/5 —— 代码本身干净且安全;我无法确定的是,这个核心修复是否真的治好了发布产物中所观测到的冻结。

回到我自己会怎么写:针对冻结问题,我会抬高构建 target 让有 bug 的降级根本不发生,再加一个编译真实依赖的回归测试,而不是断言一个配置字符串——这正是本 PR 的做法,而且它的行为级验证点比我很可能会将就的 expect(target).toBe('es2021') 更好。针对 PTY 残留,我会写同样的三行,包括先设置 releaseRequested 以避免关闭页签路径重复释放。所以方案不是我的保留意见所在,我也没找到它遗漏的更简路径。

我的保留意见是:PR 自身的验证止步于它所声称修复的对象之前。生产构建用的是嵌套的 Vite 5.4.21 及其自带的 esbuild 0.21.5;新增的验证工具用的是根 Vite 7.3.6 和根 esbuild 0.25.6。冻结是维护者在修复之前、在真实浏览器对真实 daemon 的场景下观测到的,而修复之后的生产产物无人观测过——描述对此很坦诚("没有重跑完整 E2E"、"不宣称真实 daemon 浏览器 E2E")。所以这条因果链是:确实看到了 bug,指出了合理的机制,应用了一个针对该机制的修复,但修复所作用的是与产生症状时不同的编译器,而提供的证据也来自那个不同的编译器。

我不把它当作阻塞项的两个理由。其一,这个改动是单调的——把下限提到 Vite 5 默认 target 中 es2020 之上只会减少降级,不可能引入该 bug,且兼容代价几乎为零(逻辑赋值使 Firefox 从 78 抬到 79;Chrome 87 与 Safari 14 本就达标)。其二,PR 的其余部分能独立成立:协议拒绝的修复可以通过阅读验证,顺序正确,其测试固定了两个关键性质;registry 与 package-assets 测试补上了真实的覆盖缺口;两份设计文档同步更新。

我也不批准的两个理由。CI 变绿并不能弥合这个缺口——Test (ubuntu-latest) 跑通新增验证工具,只证明它能在根 Vite 7 + esbuild 0.25.6 下加载并通过,并不证明发布产物能应答 DECRQM。而且坦率地说六个月后的维护感受:TerminalPanel 和 registry 的改动我会感谢作者,而这个构建测试我会略微抱怨。九十七行代码伸进 _core._inputHandler,守护的却是我们并不用于发布的编译器;第一次 xterm 或 Vite 升级时它就会因为与该 bug 无关的原因挂掉——届时会有人通过放宽断言来"修好"它,而从未弄清它到底在保护什么。

这不是数量疲劳,也不是含糊其辞——只有一个 PR,我的迟疑是具体且可指名道姓的。

什么能让我给到 4 分: 要么 @qwen-code /verify 与 base 构建做 A/B,证明生产产物(嵌套 Vite 5.4.21 + esbuild 0.21.5)在旧 target 下会冻结、在 es2021 下能应答 DECRQM;要么让验证工具从 packages/web-shell 而非 scripts/tests 解析 esbuildvite,使它通过时真正成为关于我们发布内容的证据。第二项改动很小,且能让第一项更可信。

⏸️ 转交 @BenGuanRan —— 需要人来判断:上述"生产工具链未经验证"这一环是否可以就这样合入,还是应当先把验证工具重新指向正确的编译器。审查时还有两个 pull_request workflow 运行中(Qwen Code CIWeb-shell Visuals);由于变绿并不能解决该疑虑,我刻意没有写入延迟批准标记,因此 CI 完成后不会有任何自动批准。机器人的确定性 owner 解析器在此没有找到 area 标签,也没有此前的人工 reviewer,所以我转给该 PR 已有的仓库指派 owner,而不是猜一个账号。

这个 PR 没有任何东西是坏的,把验证工具重新指向之后我就会合。之所以是提示而非阻塞,是因为作者本身就是维护者,对其自己后续 PR 的验证深度应当由他来定。

Qwen Code · qwen3.8-max-2026-09-02

Reviewed at 538ae5cc1690b3640b18371c970fe9b78708308f · re-run with @qwen-code /triage

@wenshao

wenshao commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification — 538ae5cc: the shipped bundle on the real stack, and whether the new build test guards it

This is a follow-up to my round 2 on #11643. This PR closes every item that round left open (§3.1–§3.4 and §5). The triage review left one question open: does es2021 cure the freeze in the compiler we actually ship (nested Vite 5.4.21 → esbuild 0.21.5)? I measured that directly instead of reasoning about it.

Verdict: the three product changes work end to end at 538ae5cc. One test issue should be fixed:

  • Freeze fix: the production Web Shell bundle freezes on DECRQM at the merge-base, and answers and keeps rendering at this head (§1). It runs on a real qwen serve, a real bash PTY and Chromium.
  • Protocol rejection: against a pre-fix(core): run web terminal PTYs on the bundled ConPTY backend #11643 daemon, the PTY is released before the reject, closing the tab does not release twice, and the notice is localized (§2).
  • Registry and package-assets tests: both are load-bearing, and mutation A/B shows each one catches a regression the merge-base suite misses (§4).
  • Should-fix — the new web-terminal-build.test.ts does not catch a revert under CI's lockfile toolchain. I ran it with the fix reverted and it passes (§3). Resolving vite and esbuild from packages/web-shell fixes that; the change is verified below. The product fix does not depend on the test, so landing it first and repointing in a follow-up is also reasonable.

1 · The freeze fix, on the toolchain that builds the shipped bundle

DECRQM real-stack A/B

Setup

  • Both arms run the same daemon, built from this head.
  • Each Web Shell is a real vite build inside packages/web-shell, so it uses Vite 5.4.21 and esbuild 0.21.5 per the lockfile.
  • The base arm differs only in the three product files, taken from b5567bb7: vite.config.ts, TerminalPanel.tsx and i18n.tsx.
  • Oracle: a program inside the terminal sends one query and logs the reply bytes that actually reach the PTY's stdin.

Built bundle

  • Base: requestMode(e,t){(v=>(…))(void 0||(n={}));…, with the let n declaration dropped. That is 1 broken site.
  • Head: requestMode(e,t){let n;(v=>(…))(n||={});…, with 0 broken sites.
  • The published @qwen-code/qwen-code@0.23.3 Web Shell bundle contains the same (void 0||(n={})) site, so the current release is affected.

Behaviour

base head
DA1 control 7 reply bytes 7 reply bytes
DECRQM ?2026 / 4 / ?2004 0 / 0 / 0; nothing after the first query renders 11 / 7 / 11; everything renders
Page errors 2 × ReferenceError: n is not defined 0
After a page reload blank pane; new output never renders history restored; new output renders

Compiler matrix (real xterm 6.0.0, minified and parsed; bottom table of the §3 figure):

  • esbuild 0.21.5 has the same bug as 0.25.6. Both break at es2020 and at Vite 5's default modules target, and both are fine at es2021.
  • esbuild 0.28.2 is fine even at es2020.
  • So on the shipped compiler, es2021 is what removes the broken lowering.

2 · New Web Shell → daemon that predates the replay protocol

Old daemon A/B

Setup

Base Web Shell

  • It sends only resize, then closes with 4002.
  • 1 bash is left stranded. It goes away only when the user closes the tab, which opens a release=1 socket.

Head Web Shell

  • It sends resize and then release, and 0 shells remain.
  • Closing the tab afterwards opens no socket, sends 0 frames and leaves 0 shells.
  • After /language ui zh-CN, the notice renders as 终端协议已更新,请重启 daemon 并刷新页面。. The base bundle has no terminal.notice.protocolMismatch key at all.

Observation, non-blocking: at head, the browser's close event is 4004 Terminal released, not 4002.

  • The old daemon handles the release first and closes the socket itself, before the client's own 4002 close takes effect.
  • onclose returns early on releaseRequested, so nothing misbehaves.
  • Just don't expect a 4002 on the wire or in daemon-side logs for this path. The test plan's "closing with code 4002" describes what the client requests, not what arrives.

3 · Does scripts/tests/web-terminal-build.test.ts catch a revert? Not in CI (should-fix)

Build test guard

Why the test misses the revert

  • The test imports bare vite and esbuild, so it gets the root copies. In package-lock.json (this head and current main) those are Vite 7.3.6 and esbuild 0.25.6.
  • A revert deletes the target line. Vite 7 then resolves the default baseline-widely-available to ["chrome107","edge107","firefox104","safari16"].
  • esbuild accepts that list and never lowers ||=, so the test passes with the fix reverted, even though the real build still ships the broken site.
  • It only fails where root node_modules has drifted to Vite 5. That is how this box first "caught" it.

Correction to the triage stage-2 note: it said a removed target "would fail with a confusing esbuild error … the guard holds". It does not fail; resolveConfig expands the keyword into a browser list. The author's negative control set es2020 explicitly, which proves ES2020 breaks but is not the revert case.

Repointed test, verified in all four arms: it fails on the merge-base config under both toolchains and passes at head under both.

-import { resolveConfig } from 'vite';
 import { JSDOM } from 'jsdom';
-// Use the repository's compiler; a locally overridden Vite dependency may
-// already fix the logical-assignment lowering bug and mask this regression.
-import { transform } from 'esbuild';

 const require = createRequire(import.meta.url);
 const webShellRoot = resolve(import.meta.dirname, '../../packages/web-shell');
+// Resolve the toolchain `packages/web-shell` builds with. Root Vite 7 resolves
+// a missing target to modern browsers and would pass with the fix reverted.
+const webShellRequire = createRequire(resolve(webShellRoot, 'package.json'));
+const { resolveConfig } = webShellRequire('vite') as typeof import('vite');
+const { transform } = webShellRequire('esbuild') as typeof import('esbuild');

The existing comment about "a locally overridden Vite dependency" is also misleading: esbuild 0.21.5 is simply Vite 5.4.21's own dependency, i.e. the production compiler.

4 · The other new tests are load-bearing (mutation A/B)

  • web-terminal-registry:
    • Head passes 43/43.
    • Removing queryTerminal?.dispose() from the spawn-failure catch fails exactly reports a spawn failure and frees the id when both backends fail, at toHaveBeenCalledOnce.
    • The merge-base test file passes against that mutant (43/43).
  • package-assets:
    • Mutant M1 drops the win32-arm64 pin; mutant M2 adds a stale hard-coded linux-x64 pin.
    • The new test fails on both mutants.
    • The merge-base test file passes on both (36 passed, 1 skipped).
  • TerminalPanel: the PR's tests against the merge-base component give 21/22. The one failure is the new release assertion: the component sent only resize.

Gates at 538ae5cc

  • PR's tests: TerminalPanel 22/22, web-terminal-registry 43/43, package-assets 37 passed / 1 skipped, web-terminal-build 1/1.
  • Web Shell tsc --noEmit: identical to a merge-base overlay of the three files (1 error on this box, the same one in both), so the PR adds no type errors.
  • Formatting: prettier --check is clean on every changed file.
  • ESLint: it crashes on this box with an ajv missingRefs error, identically on an untouched main checkout, so I did not run it locally. The CI Lint & Static job is green.
  • CI: all checks green at this head. Test (ubuntu-latest) ran ✓ scripts/tests/web-terminal-build.test.ts (1 test).
  • Not rerun: Windows. This PR changes no ConPTY or registry source.
Environment, arms and how to reproduce

Environment

  • Debian 13 (kernel 6.12), Node 22.22.2, Chromium via Playwright 1.58.2, @xterm/xterm 6.0.0.
  • Web Shell build: Vite 5.4.21 + esbuild 0.21.5, the nested copies per the lockfile.
  • The CI-lockfile toolchain arms use root Vite 7.3.6 + esbuild 0.25.6, installed separately and aliased into the scripts Vitest run.

Trees

  • One worktree at 538ae5cc, with core, acp-bridge, sdk-typescript, web-shell, web-templates and cli built in it.
  • The base Web Shell arm is a second vite build of that tree after git show b5567bb7a9: of the three product files. The tree was restored afterwards and verified clean.
  • Each daemon serves one arm, linked in as packages/web-shell/dist. Every bundle was checked by entry chunk and broken-site count.

Daemons

  • New: packages/cli/dist/index.js serve from this head.
  • Old: the pre-fix(core): run web terminal PTYs on the bundled ConPTY backend #11643 build a651427f, whose /terminal route has no snapshot control frame.
  • Both run on loopback with an isolated HOME, folder trust off, and a local fake OpenAI stub. Like round 2, the driver sends one prompt before opening the terminal.

Harness sources: pr11748/harness/ on my fork.

  • e2e11748.cjs + query-emit.cjs: the DECRQM and old-daemon scenarios.
  • run-arms.sh + start-daemon.sh: arm orchestration.
  • zh-probe2.cjs: the zh-CN notice.
  • matrix.mjs: esbuild × target.
  • target.mjs: resolveConfig under Vite 5 and 7.
  • web-terminal-build.ws-toolchain.test.ts: the repointed test.
中文版

维护者验证 —— 538ae5cc:真实栈上的发布产物,以及新增构建测试能否守住修复

本文是我在 #11643 第 2 轮 报告的后续。本 PR 关闭了那一轮留下的全部事项(§3.1–§3.4 与 §5)。triage 审查留下一个未决问题:es2021 能否在我们实际发布所用的编译器(嵌套的 Vite 5.4.21 → esbuild 0.21.5)上治好冻结?这次我直接实测,而不是推理。

结论:三处产品改动在 538ae5cc 上端到端成立。 有一处测试问题应当修:

  • 冻结修复: 生产 Web Shell 产物在 merge-base 上遇到 DECRQM 会冻结,在本 head 上能应答并继续渲染(§1)。验证跑在真实 qwen serve、真实 bash PTY 和 Chromium 上。
  • 协议拒绝: 连接 fix(core): run web terminal PTYs on the bundled ConPTY backend #11643 之前的 daemon 时,拒绝前会先释放 PTY,关闭页签不会重复释放,提示已本地化(§2)。
  • registry 与 package-assets 测试: 两者都真正起作用,变异 A/B 显示它们各自能捕获 merge-base 测试集漏掉的回归(§4)。
  • 应修 —— 新增的 web-terminal-build.test.ts 在 CI 的 lockfile 工具链下发现不了回退。 我把修复回退后实际跑了,它依然通过(§3)。改为从 packages/web-shell 解析 viteesbuild 即可修正,改法已验证,见下文。产品修复本身不依赖这个测试,所以先合入、再用后续 PR 修正测试也合理。

1 · 在构建发布产物的工具链上验证冻结修复

环境

  • 两臂使用同一个由本 head 构建的 daemon。
  • 每个 Web Shell 都是在 packages/web-shell 内真实执行 vite build 得到的,按 lockfile 即 Vite 5.4.21 + esbuild 0.21.5。
  • base 臂只有三个产品文件不同,取自 b5567bb7vite.config.tsTerminalPanel.tsxi18n.tsx
  • 判定依据: 终端内的程序发送一个查询,并记录真正到达 PTY stdin 的应答字节。

构建产物

  • base:requestMode(e,t){(v=>(…))(void 0||(n={}));…let n 声明被丢掉,即 1 处错误编译。
  • head:requestMode(e,t){let n;(v=>(…))(n||={});…,0 处。
  • 已发布的 @qwen-code/qwen-code@0.23.3 Web Shell 产物含有同样的 (void 0||(n={})),所以当前发布版本受影响。

行为

base head
DA1 对照 7 字节应答 7 字节应答
DECRQM ?2026 / 4 / ?2004 0 / 0 / 0;第一个查询之后不再渲染任何内容 11 / 7 / 11;全部正常渲染
页面错误 2 次 ReferenceError: n is not defined 0
刷新页面后 空白面板,新输出永不渲染 历史恢复,新输出正常渲染

编译器矩阵(真实 xterm 6.0.0 压缩后解析;即 §3 图中底部表格):

  • esbuild 0.21.5 与 0.25.6 有同样的 bug。 两者在 es2020 和 Vite 5 默认 modules target 下都会出错,在 es2021 下都正常。
  • esbuild 0.28.2 即便在 es2020 下也正常。
  • 所以在发布所用的编译器上,正是 es2021 消除了错误降级。

2 · 新 Web Shell → 早于回放协议的 daemon

环境

base Web Shell

  • 只发送 resize,随后以 4002 关闭。
  • 残留 1 个 bash。 只有用户关闭页签(打开一个 release=1 的 socket)后才会释放。

head Web Shell

  • 先发 resize 再发 release残留 0 个 shell
  • 之后关闭页签,不开新 socket、发送 0 帧、shell 仍为 0。
  • 执行 /language ui zh-CN 后,提示显示为 终端协议已更新,请重启 daemon 并刷新页面。。base 产物里根本没有 terminal.notice.protocolMismatch 这个 key。

观察(非阻塞): head 下浏览器收到的关闭事件是 4004 Terminal released,而不是 4002。

  • 旧 daemon 先处理 release 并自己关闭 socket,早于客户端自己的 4002 关闭生效。
  • onclosereleaseRequested 提前返回,行为没有问题。
  • 只是这条路径上线路和 daemon 日志里不会出现 4002。测试计划中"以 4002 关闭"描述的是客户端请求的关闭码,不是实际收到的。

3 · scripts/tests/web-terminal-build.test.ts 能发现回退吗?在 CI 上不能(应修)

为什么测试发现不了回退

  • 测试 import 的是裸 viteesbuild,得到的是根目录的副本。按 package-lock.json(本 head 和当前 main)它们是 Vite 7.3.6 和 esbuild 0.25.6。
  • 回退就是删掉 target 那一行。这时 Vite 7 把默认的 baseline-widely-available 解析为 ["chrome107","edge107","firefox104","safari16"]
  • esbuild 接受这个列表,并且不会降级 ||=,于是修复被回退后测试依然通过,而真实构建仍在发布错误代码。
  • 只有根 node_modules 漂移成 Vite 5 的环境才会失败,本机最初"抓到"它就是这个原因。

更正 triage 第 2 阶段的说法: 它说去掉 target"会以难懂的 esbuild 报错失败……守护依然成立"。实际上不会失败,因为 resolveConfig 会把该关键字展开为浏览器列表。作者的负向对照是显式设置 es2020,这证明了 ES2020 会出错,但不是回退场景。

改为从 packages/web-shell 解析后的测试,四臂均已验证: merge-base 配置在两种工具链下都失败,head 在两种工具链下都通过。改法见英文部分的 diff。另外,现有注释说"本地覆盖的 Vite 依赖"也有误导:esbuild 0.21.5 就是 Vite 5.4.21 自己的依赖,也就是生产编译器。

4 · 其余新增测试确实起作用(变异 A/B)

  • web-terminal-registry
    • head 43/43 通过。
    • 删除 spawn 失败 catch 中的 queryTerminal?.dispose() 后,恰好 reports a spawn failure and frees the id when both backends failtoHaveBeenCalledOnce 处失败。
    • merge-base 的测试文件对该变异体全部通过(43/43)。
  • package-assets
    • 变异 M1 去掉 win32-arm64 的版本声明;变异 M2 加入一个过期的硬编码 linux-x64 版本声明。
    • 新测试对两者都失败。
    • merge-base 测试文件对两者都通过(36 通过、1 跳过)。
  • TerminalPanel 用 PR 的测试跑 merge-base 组件,结果 21/22。唯一的失败是新的 release 断言:组件只发了 resize

538ae5cc 上的检查

  • PR 的测试: TerminalPanel 22/22,web-terminal-registry 43/43,package-assets 37 通过 / 1 跳过,web-terminal-build 1/1。
  • Web Shell tsc --noEmit 与三个文件回到 merge-base 的对照完全一致(本机 1 个错误,两边是同一个),所以 PR 没有新增类型错误。
  • 格式: 所有改动文件 prettier --check 通过。
  • ESLint: 本机因 ajv missingRefs 报错崩溃,在未改动的 main 上也完全一样,所以没有在本地跑。CI 的 Lint & Static 任务为绿色。
  • CI: 本 head 全部检查为绿色。Test (ubuntu-latest) 实际跑了 ✓ scripts/tests/web-terminal-build.test.ts (1 test)
  • 未重跑: Windows。本 PR 没有改动 ConPTY 或 registry 源码。

复现环境与脚本见上方英文的折叠部分。脚本在我 fork 的 pr11748/harness/

@wenshao

wenshao commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@wenshao
wenshao enabled auto-merge September 13, 2026 03:04
@qwen-code-ci-bot

qwen-code-ci-bot commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ❌ not passed — findings reported (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 15 passed · 0 failed · 15 total

Flakiness gate: ✅ 4 changed test file(s) x 5 identical rounds, no divergence

中文 — 判定:❌ 不通过 · 报告了发现(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:15 通过 · 0 失败 · 15 总计

抖动门:✅ 4 changed test file(s) x 5 identical rounds, no divergence

Verification report

PR #11748 deep verification — fix(web-shell): prevent terminal query freezes and clean up protocol rejection

Verdict: findings — the central claim is proven load-bearing on the real production artifacts, but the PR fixes only one of the two bundles that ship the defect, and the regression test it adds for that fix cannot go red when the fix is removed.

  • Scripted assertions: 15 pass / 0 fail / 15 total (assertions.json; every number traces to a printed ASSERTIONS pass=N fail=M line in logs/).
  • Verified head: 538ae5cc1690b3640b18371c970fe9b78708308f (git rev-parse HEAD^2), base tip b5567bb7a992047a8720511b74777bef842da17b (HEAD^1).
  • Commit reachability: git rev-list HEAD^1..HEAD^2 = 1 commit, snapshot commits array = 1 commit — they match, so per-commit attribution was in reach (this PR is a single commit).
  • Working tree restored clean after every mutation (git status --porcelain | wc -l = 0).
中文摘要

结论:findings(有发现,非阻塞回归)。 核心主张成立:vite.config.tstarget: 'es2021' 确实是必需的,我在真实产物上验证了 base 与 head 的差异(见下方「Central claim + A/B」表与图 01-esbuild-compiler-target-matrix.png02-lib-bundle-sibling-defect.png)。

两个主要发现:

  1. 同类缺陷仍存在于已发布的库产物中。 本 PR 只给 vite.config.ts(独立应用)加了 target,而 vite.lib.config.ts(构建已发布的 @qwen-code/web-shell 主入口 dist/index.js,被 web-templatesvscode-ide-companion 消费)完全没有 target。我在 HEAD 上重新构建该库,requestMode 里仍是 )(void 0 || (n = {})) —— 声明被丢弃,与 base 的独立包一模一样;加上 target: 'es2021' 后变回 let n; (...)(n ||= {})。这是既有缺陷(该文件本 PR 未改动),不是本 PR 引入的回归,但它属于 PR 标题声称要修的同一类问题。

  2. 新增的回归测试是空转的(vacuous)。target: 'es2021'vite.config.ts 删掉(即 base 状态),scripts/tests/web-terminal-build.test.ts 仍然 1/1 通过(图 03-vacuity-test-green-with-fix-removed.png)。原因是该测试用根目录的 vite 7.3.6 读 build.target,其默认值解析为 ['chrome107','edge107','firefox104','safari16'](不降级逻辑赋值);而真正构建 Web Shell 的是 packages/web-shell/node_modules 下的 vite 5.4.21,其默认值解析为 ['es2020','edge88','firefox78','chrome87','safari14'](会降级)。同理,测试用根 esbuild 0.25.6 做 transform,而生产编译器是 vite 5 自带的 esbuild 0.21.5。作为正向对照,把 target 显式改成 es2020 时测试确实变红并报出预期断言 ReferenceError: i is not defined,说明测试本身有效,只是覆盖不到「删除该配置项」这一最现实的回归路径。

其余次要发现见 Findings:releaseRequested = true 是有实际作用但未被测试钉住的覆盖缺口(我已给出可钉住它的 fixture);target: 'es2021' 使 Firefox 78 失去支持(产物中未降级的逻辑赋值从 8 处增至 62 处)。

未覆盖范围:真实浏览器/daemon E2E、Windows ConPTY 原生路径、全仓 build/typecheck/lint、--mode transcript 库构建变体、docs 中「离线查询不补答」的行为验证。详见 Not covered。

Central claim + A/B

Central claim: setting build.target: 'es2021' in packages/web-shell/vite.config.ts prevents esbuild from miscompiling xterm's requestMode logical assignment, so a DECRQM query (ESC [ ? 2004 $ p) is answered and the terminal keeps rendering subsequent output instead of freezing.

The defect is a single construct in real @xterm/xterm@6.0.0 (lib/xterm.mjs, 344,970 bytes, 17 ||=, 2 ??=, 2 &&=):

requestMode(e,i){let r;(P=>(P[P.NOT_RECOGNIZED=0]="NOT_RECOGNIZED", ))(r||={}); }

esbuild 0.21.5 lowers r||={} to r||(r={}), substitutes void 0 for the provably-undefined read, and then drops the let r; declaration entirely, leaving a write to an identifier that is not declared.

Table A — real build artifacts (the decisive oracle)

Five artifacts, one oracle: inside the requestMode body, is the enum-init let X; …(X ||= {}) (healthy) or …(void 0 || (X = {})) (declaration dropped)? Harness lib-sibling.mjs, log logs/19-lib-sibling-ab.log, witness 02-lib-bundle-sibling-defect.png.

# artifact built by build.target requestMode enum-init verdict
1 dist-base/assets/index-C8Yx7BVy.js (4,715,209 B) vite 5.4.21 + esbuild 0.21.5 none['es2020','edge88','firefox78','chrome87','safari14'] (void 0||(n={}))declaration dropped DEFECT
2 dist-head/assets/index-CRYSdxJW.js (4,623,893 B) same 'es2021' let n;(…)(n||={}) FIXED
3 packages/web-shell/dist/index.js (7,272,167 B, CI-built at HEAD) vite.lib.config.ts, vite 5.4.21 + esbuild 0.21.5 none (void 0 || (n = {})) DEFECT
4 lib-head-asis/index.js (fresh rebuild at HEAD) same none (void 0 || (n = {})) DEFECT
5 lib-head-fixed/index.js (candidate fix) same 'es2021' added let n; ((w) => …)(n ||= {}) FIXED

Artifact 3 contains the PR's own new i18n key (terminal.notice.protocolMismatch ×3), which proves it was built at HEAD, not stale.

A/B result: 2/2 standalone-app cells flip DEFECT → FIXED; 0/2 library cells flip. 5/5 scripted assertions passed.

Table B — behavioural A/B on the production compiler

Harness matrix.mjs, log logs/01-matrix.log, witness 01-esbuild-compiler-target-matrix.png. Oracle: load the transformed real xterm.mjs in a JSDOM vm context, synchronously parse('\x1b[?2004$pafter-query'), assert the reply is exactly \x1b[?2004;2$y and line 0 reads after-query.

compiler (realpath-asserted) target result
prod packages/web-shell/node_modules/esbuild 0.21.5the one vite 5.4.21 uses to build the Web Shell ['es2020','edge88','firefox78','chrome87','safari14'] (= vite 5 default) THROW ReferenceError: i is not defined
prod 0.21.5 es2020 THROW ReferenceError: i is not defined
prod 0.21.5 es2021 (= HEAD) OK replies=["ESC[?2004;2$y"] output="after-query"
prod 0.21.5 esnext OK
root node_modules/esbuild 0.25.6the one the PR's new test imports vite5-default / es2020 THROW ×2
root 0.25.6 es2021 / esnext OK ×2
vite7 node_modules/vite/node_modules/esbuild 0.28.2 es2020 / es2021 / esnext OK ×3 (bug already fixed upstream)
vite7 0.28.2 vite5-default list TRANSFORM-ERROR (cannot lower destructuring to that target set)

7/12 cells OK. The central claim holds on the production compiler: base target → freeze, es2021 → correct answer plus continued output. 5/5 scripted assertions passed.

Strict-mode characterization (logs/10-strict-mode-semantics.log, 1/1 passed): evaluating the exact base-emitted expression void 0||(n={}) under "use strict" yields ReferenceError: n is not defined; in sloppy mode it silently creates a global instead. Both shipped artifacts are strict, so both fail loud at the query rather than leaking a global: the standalone bundle is loaded via <script type="module" crossorigin src="/assets/index-CRYSdxJW.js"> in the built index.html, and the library bundle is an ES module by construction (exports["."].import, formats: ['es']).

Corrections to the PR description

These are corrections of stated fact, not requests to change code.

  1. "esbuild 0.25.6 can miscompile xterm's logical assignments" — 0.25.6 does miscompile (measured, Table B), but it is not the compiler that builds the Web Shell. packages/web-shell declares vite: ^5.0.0 and resolves vite 5.4.21, which resolves its own nested esbuild 0.21.5 at packages/web-shell/node_modules/esbuild. Root esbuild@^0.25.0 → 0.25.6 is a separate, unrelated dependency (used by esbuild.config.js for the CLI bundle at target: 'node22'). The version that matters here is 0.21.5, and it reproduces the identical defect.
  2. "The production target is now ES2021" — true for the standalone app only. The published library entry keeps vite 5's default target (Table A, rows 3–4).
  3. "The real xterm module minified with locked esbuild 0.25.6 passes at ES2021; the same test at ES2020 fails" — reproduced exactly (ReferenceError: i is not defined in requestMode). But this is a statement about 0.25.6, and the test's config read goes through root vite 7.3.6, not the vite 5.4.21 that builds the artifact. See Finding 2.

Findings

1. Major — the published @qwen-code/web-shell library still ships the same miscompile (pre-existing, not a regression)

packages/web-shell/vite.lib.config.ts sets no build.target (grep -n target packages/web-shell/vite.lib.config.ts → no match), so it builds dist/index.js with vite 5's default target list and esbuild 0.21.5 — the exact combination that drops the declaration. The published entry point is "." → ./dist/index.js, consumed by packages/web-templates/package.json and packages/vscode-ide-companion/package.json, and it does contain xterm (requestMode ×3, _inputHandler ×21, xterm-accessibility ×6, allowProposedApi ×4) and the PR's own TerminalPanel (terminal.notice.protocolMismatch ×3).

At HEAD, that bundle reads:

requestMode(e, t) {
    ((_) => (_[_.NOT_RECOGNIZED = 0] = "NOT_RECOGNIZED", ))(void 0 || (n = {}));

Reproduce (≈15 s):

cd packages/web-shell && npx vite build --config vite.lib.config.ts --outDir /tmp/lib-asis
node -e "const s=require('fs').readFileSync('/tmp/lib-asis/index.js','utf8');const i=s.indexOf('requestMode(e, t) {');console.log(s.slice(i,i+330))"

Blast radius and bounds. I proved the emitted defect is byte-identical in shape to the standalone base bundle, and that adding target: 'es2021' to vite.lib.config.ts clears it (Table A row 5 — a measured candidate fix, not an eyeballed one). What I did not settle behaviourally is the runtime consequence inside the 7 MB library bundle: it is an ES module, hence strict, so an undeclared write should throw ReferenceError — but the bundle does contain one module-scope function n, so I cannot rule out the quieter variant where (n = {}) clobbers that binding and the failure surfaces later as a TypeError in unrelated code. Per the observability ranking, the quiet variant would be the worse one. Either way the terminal is broken for library consumers on a DECRQM query.

Not a merge blocker for this PR: vite.lib.config.ts is untouched by the diff, so rows 3–4 are equally broken at base. This is an unfixed sibling of the same bug class, and the PR's title ("prevent terminal query freezes") and its new test's name ("Web Shell production terminal") both read as broader than what is fixed. The description's Risk section does say "The standalone Web Shell now targets ES2021", so the narrower scope was declared.

Measured candidate fix
--- a/packages/web-shell/vite.lib.config.ts
+++ b/packages/web-shell/vite.lib.config.ts
@@
   build: {
+    target: 'es2021',
     emptyOutDir: false,

Verified: with this applied, lib-head-fixed/index.js contains let n; ((w) => …)(n ||= {}) and the DEFECT pattern is absent; index.js goes 7,272.17 kB → 7,167.20 kB and toolNames 364.18 kB → 346.18 kB (fewer lowering helpers emitted). Both library builds completed clean (6.9 s / 6.4 s). I did not run the library's consumers against the patched bundle.

2. Major — the new regression test cannot fail when the fix is removed

scripts/tests/web-terminal-build.test.ts is the only guard for the central claim. Removing target: 'es2021' from vite.config.ts (i.e. restoring base) leaves it green:

 ✓ scripts/tests/web-terminal-build.test.ts (1 test) 261ms
 Test Files  1 passed (1)
      Tests  1 passed (1)

Reproduce — witness 03-vacuity-test-green-with-fix-removed.png, log logs/05-vacuity-base-test-run.log:

# delete the `target: 'es2021',` line from packages/web-shell/vite.config.ts, then:
npx vitest run --config ./scripts/tests/vitest.config.ts scripts/tests/web-terminal-build.test.ts
git checkout -- packages/web-shell/vite.config.ts

Root cause — the test reads and compiles through a different toolchain than production. Measured with the fix removed (logs/04-vacuity-target-resolution.log):

resolveConfig(...).build.target
vite 7.3.6 at node_modules/vite — what the test imports ['chrome107','edge107','firefox104','safari16'] → no logical-assignment lowering → no bug
vite 5.4.21 at packages/web-shell/node_modules/vite — what builds the Web Shell ['es2020','edge88','firefox78','chrome87','safari14'] → lowers → bug

So the test's control arm structurally cannot reproduce production's base behaviour. Independently, its transform import resolves to root esbuild 0.25.6, while production uses 0.21.5. Today both miscompile, so the test is not wrong — but it is coincidentally correct: esbuild 0.28.2 already has the fix (Table B), so any future bump of the root esbuild@^0.25.0 range past 0.27 would leave this test permanently green while the production compiler stays at 0.21.5.

Positive control (the harness is live). The assertion can fail, and fails with the intended behavioural mismatch — setting target: 'es2020' explicitly turns it red (logs/06-positive-control-es2020.log):

 × Web Shell production terminal > answers DECRQM and keeps processing output after minification
   → i is not defined
ReferenceError: i is not defined
 ❯ ja.requestMode evalmachine.<anonymous>:44:103624
 ❯ queryAndPrint scripts/tests/web-terminal-build.test.ts:71:32

So the gap is specifically the removal path — which is the realistic regression, since a future cleanup would read target: 'es2021' as redundant against vite's default.

Suggested direction

Resolve the compiler and the config through the web-shell workspace rather than the repo root — e.g. createRequire(resolve(webShellRoot, 'x.js')).resolve('esbuild') and the same for vite — so the test exercises vite 5.4.21 / esbuild 0.21.5. Then assert the base condition directly as a second case: transform at vite 5's default target and assert it throws, which pins the mechanism rather than only the configured value. I did not implement or measure this variant.

3. Minor — coverage gap: releaseRequested = true is load-bearing but unpinned

Mutation matrix for the guards this PR adds/relies on. logs/20-vacuity-mutations.log, logs/21-fine-mutations.log, logs/22-m5-adjudication.log.

# mutation suite expected observed classification
M1 revert the whole TerminalPanel.tsx mismatch hunk (no release, hardcoded English notice) TerminalPanel 22 red red 1/22expected "spy" to be called with [ '\u0000{"type":"release"}' ] pinned
M4 keep the release send, move it after ws.close(4002) TerminalPanel 22 red red 1/22expected 204 to be less than 203 (ordering assertion) pinned
M5 keep the release send, drop releaseRequested = true TerminalPanel 22 red green 22/22 coverage gap
M2 drop queryTerminal?.dispose() from the spawn-failure catch web-terminal-registry 43 red red 1/43expected "dispose" to be called once, but got 0 times pinned
M3 stop spreading ...nodePtyPins into optionalDependencies package-assets (filtered) red red — expected 6 pins, received {} pinned
M6 remove target: 'es2021' from vite.config.ts web-terminal-build 1 red green 1/1 Finding 2

M5 is a genuine gap, not dead code and not redundant defence — adjudicated with a positive control:

arm probe result
A HEAD + expect(FakeWebSocket.instances.length).toBe(socketsBefore) 22/22 green
B M5 + same probe red — expected 2 to be 1
C M5, PR's own test unmodified 22/22 green

Mechanism: FakeWebSocket.close() sets readyState = CLOSED, so with the flag unset release() skips the OPEN branch and falls into else if (!ws || ws.readyState !== WebSocket.CONNECTING) → connect(true), opening a brand-new release-only WebSocket (with up to 3 retries via releaseAttempts). The PR's assertion counts ws.send on the original socket, so it cannot observe the second one. The guard therefore prevents a spurious reconnect against an already-rejected old daemon — real behaviour, unasserted.

The fixture that would pin it, appended to the existing case:

const socketsBefore = FakeWebSocket.instances.length;
act(() => releaseWebTerminal('terminal:one'));
expect(FakeWebSocket.instances.length).toBe(socketsBefore);

4. Minor — target: 'es2021' narrows browser support to exclude Firefox 78

'es2021' replaces vite 5's whole default list, dropping the browser-specific entries. Measured over every .js in each standalone bundle (logs/16-strictness-and-browser-floor.log):

total JS bytes un-lowered ||= / ??= / &&= private-field-ish static blocks
dist-base 18,086,783 8 65 0
dist-head 17,970,719 62 65 0
Δ −116,064 +54 0 0

The entire syntax delta is logical assignment (ES2021); ES2022 constructs are unchanged. Logical assignment shipped in Chrome/Edge 85, Safari 14.0, Firefox 79 — so of vite's default list (es2020, edge88, firefox78, chrome87, safari14) only Firefox 78 regresses, from working to a parse-time SyntaxError (blank page, not a broken terminal). Every residual is accounted for: the +54 sites are exactly the lowering the fix exists to suppress, and the −116 kB is the lowering helpers no longer emitted.

The trade is clearly worth it — a hard freeze on every browser beats a parse failure on a 2020 ESR release. If keeping the floor matters, the equivalent fix that preserves it is a list rather than a bare spec:

target: ['es2021', 'edge88', 'firefox79', 'chrome87', 'safari14'],

I did not build or test that variant; it is a suggestion, not a measured fix.

Also verified (no finding)

  • No control-byte leak into the shell. The client now sends \x00{"type":"release"} to a daemon that by definition does not mark its snapshot. packages/cli/src/serve/routes/terminal.ts:276 drops any NUL-prefixed frame that fails to parse (if (!isBinary && text.startsWith(CONTROL_FRAME_PREFIX) && !control) return;) instead of falling through to registry.write(), so an old daemon cannot receive the frame as typed input. Current daemons handle control?.type === 'release' explicitly.
  • The headless sibling is safe. @xterm/headless reaches production only through the CLI bundle (esbuild.config.js, target: 'node22' ×3 sites). Node 22 supports logical assignment natively, so no lowering occurs and the bug class cannot reach it.
  • @xterm/xterm@6.0.0 really does contain the construct — 17 ||=, 2 ??=, 2 &&= — so this is not a synthetic repro.
  • The 6-pin assertion matches reality. packages/core/package.json declares exactly 6 @lydell/node-pty* optional dependencies; package-assets.test.js full file 38/38 pass at HEAD (logs/24-package-assets-full.log), matching the PR's claim.
  • Suite counts match the PR's claims: TerminalPanel 22/22 (logs/11-terminalpanel-head.log), web-terminal-registry 43/43 (logs/12-registry-head.log), package-assets 38/38.
  • Localization is complete on both axes. terminal.notice.protocolMismatch is present in EN (i18n.tsx:1472) and ZH (i18n.tsx:5169), and consumed at TerminalPanel.tsx:310; t is in scope via useI18n() (line 117).
  • Reviewer Test Plan, step by step. Step 1 (DECRQM answers and output continues) — verified at the parser level on real artifacts (Tables A/B); not verified in a real browser. Step 2 (release before 4002, localized notice, no re-release on tab close) — the ordering half is pinned (M4) and the notice is localized; the "closing the tab afterward should not send release again" half is asserted in a way that passes with the guard removed (Finding 3), so that step is not actually verified by the PR's own test. Step 3 (responder disposed, id reusable; six pins preserved) — both pinned (M2, M3).

Not covered

  • No real browser or real daemon E2E. No tmux/Playwright run; the PR cites the maintainer's round-2 baseline for that and does not claim it. My DECRQM evidence is at the parser/artifact level. This reproduces the mechanism and the emitted defect, not a user watching a browser freeze.
  • Windows ConPTY native paths not exercised — Linux container, no conpty.dll/OpenConsole.exe. The double-spawn-failure path was verified only through the registry's mocked spawn.
  • The library bundle's runtime failure mode is not settled (Finding 1 bounds): ReferenceError vs silent clobber of the module-scope function n. Settling it needs the 7 MB ESM bundle evaluated with its externals stubbed.
  • No repo-wide gates run. Not executed: npm run build, npm run typecheck, npm run lint, prettier --check, the full scripts/tests suite (only package-assets.test.js and web-terminal-build.test.ts), and every other workspace suite. The PR itself states web-shell typecheck did not pass in the author's worktree; I did not attempt it.
  • vite build --config vite.lib.config.ts --mode transcript variant not built — only the default (index + daemon-react-sdk) entry.
  • No trial merge into current main. The checkout is depth 2 (git rev-parse --is-shallow-repository → true), so main is not reachable and merge-conflict status against it is unknown.
  • The docs change is not behaviourally tested. The new claim that "queries emitted while no browser is attached remain unanswered on reconnect" is a prose boundary statement; I did not drive a reconnect to confirm replay suppresses replies, and I did not diff the EN/ZH design docs beyond reading the changed hunks (they are consistent with each other).
  • esbuild versions outside this lockfile. I tested 0.21.5 / 0.25.6 / 0.28.2 only; I did not bisect which release fixed the lowering bug.

Methodology

Environment: the CI verify container (node:22-bookworm), Node v22.23.2, npm 10.9.8, tree at the merge ref c67f83165d with npm ci + npm run build already completed; no zstd binary present; $RUNNER_TEMP unset in my shell. I asserted every toolchain identity by realpath rather than assuming it (createRequire(...).resolve + realpathSync, printed at the top of logs/01-matrix.log), because the monorepo carries three different esbuilds and two different vites and a naive harness silently measures the wrong one. Four harnesses drove real code with no stubs: matrix.mjs transformed the genuine @xterm/xterm@6.0.0 lib/xterm.mjs with each compiler and loaded the output in a JSDOM vm context to parse a real DECRQM sequence; probe-dist.mjs and lib-sibling.mjs classified requestMode in five artifacts produced by real vite build invocations (two standalone arms from mutating only packages/web-shell/vite.config.ts, three library arms from vite.lib.config.ts as-is and with the candidate fix); the mutation runs drove the real vitest suites against single-point edits of production source, each reverted with git checkout -- and confirmed by git status --porcelain | wc -l = 0. All scratch output went to tmp/pr11748-verify-20260913-031940/; the four vite build output trees (dist-base, dist-head, lib-head-asis, lib-head-fixed, ≈50 MB of minified JS) were deleted after evidence extraction to keep the published artifact small — their decisive excerpts are quoted verbatim in Tables A/B and in logs/09, logs/15, logs/19, and each is regenerable with the commands named in Finding 1 and in Table A's built by column. No git worktree was needed, so none was left behind. Raw per-run logs are logs/01logs/24; harness sources are matrix.mjs, probe-dist.mjs, lib-sibling.mjs, vacuity.sh, fine-mutations.sh, m5-adjudicate.sh, all rerunnable as-is. Images were produced with node scripts/verify-capture.mjs; captures 01 and 02 pipe the saved logs from the real executions, capture 03 is a live run.

Flakiness gate log

rounds=5 files=4 skipped=0
file packages/core/src/services/web-terminal-registry.test.ts: (cd packages/core) npx --no-install vitest run ./src/services/web-terminal-registry.test.ts
file packages/web-shell/client/components/terminal/TerminalPanel.test.tsx: (cd packages/web-shell) npx --no-install vitest run ./client/components/terminal/TerminalPanel.test.tsx
file scripts/tests/package-assets.test.js: (cd .) npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/package-assets.test.js
file scripts/tests/web-terminal-build.test.ts: (cd .) npx --no-install vitest run --config ./scripts/tests/vitest.config.ts ./scripts/tests/web-terminal-build.test.ts


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/core/src/services/web-terminal-registry.test.ts: PPPPP
  packages/web-shell/client/components/terminal/TerminalPanel.test.tsx: PPPPP
  scripts/tests/package-assets.test.js: PPPPP
  scripts/tests/web-terminal-build.test.ts: PPPPP

verdict: pass
summary: 4 changed test file(s) x 5 identical rounds, no divergence

--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/core/src/services/web-terminal-registry.test.ts: P (exit 0)
round 1 · packages/web-shell/client/components/terminal/TerminalPanel.test.tsx: P (exit 0)
round 1 · scripts/tests/package-assets.test.js: P (exit 0)
round 1 · scripts/tests/web-terminal-build.test.ts: P (exit 0)
round 2 · packages/core/src/services/web-terminal-registry.test.ts: P (exit 0)
round 2 · packages/web-shell/client/components/terminal/TerminalPanel.test.tsx: P (exit 0)
round 2 · scripts/tests/package-assets.test.js: P (exit 0)
round 2 · scripts/tests/web-terminal-build.test.ts: P (exit 0)
round 3 · packages/core/src/services/web-terminal-registry.test.ts: P (exit 0)
round 3 · packages/web-shell/client/components/terminal/TerminalPanel.test.tsx: P (exit 0)
round 3 · scripts/tests/package-assets.test.js: P (exit 0)
round 3 · scripts/tests/web-terminal-build.test.ts: P (exit 0)
round 4 · packages/core/src/services/web-terminal-registry.test.ts: P (exit 0)
round 4 · packages/web-shell/client/components/terminal/TerminalPanel.test.tsx: P (exit 0)
round 4 · scripts/tests/package-assets.test.js: P (exit 0)
round 4 · scripts/tests/web-terminal-build.test.ts: P (exit 0)
round 5 · packages/core/src/services/web-terminal-registry.test.ts: P (exit 0)
round 5 · packages/web-shell/client/components/terminal/TerminalPanel.test.tsx: P (exit 0)
round 5 · scripts/tests/package-assets.test.js: P (exit 0)
round 5 · scripts/tests/web-terminal-build.test.ts: P (exit 0)

Evidence images

01-esbuild-compiler-target-matrix

02-lib-bundle-sibling-defect

03-vacuity-test-green-with-fix-removed

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Partially reviewed — gaps disclosed. Suggestions are inline.

Not reviewed: build-and-test — the "Test (macos-latest, Node 22.x)" and "Test (windows-latest, Node 22.x)" CI legs were skipped at this commit, so the two new scripts tests were exercised only on Linux (locally by the build/test agent and on the ubuntu leg); their cross-platform behaviour is unmeasured.

Not explored to full depth (tool budget reached): "agent 6b": did not re-run vite build for packages/web-shell to confirm from a fresh build (rather than the tree's existing dist/ ) that the app chunk and lib bundle d…; "agent 3c": running vite build --config vite.lib.config.ts and executing the emitted bundle to confirm the lib-build breakage end to end (finding 1 rests on the resolved …; "agent 6c": I did not enumerate the .github/workflows jobs that invoke test:scripts / test:ci (which OSes, and whether any are merge_group - or schedule -gated), so…; "agent 6c": I did not check git log on packages/cli/src/serve/routes/terminal.ts to establish whether a pre-marking daemon parses the release control frame or would t…; "agent 3a": did not run packages/web-shell 's vite.lib.config.ts builds to confirm whether @xterm/xterm survives tree-shaking into dist/index.js (left as the stated …, and 1 more.

Not reviewed: reverse audit — stopped before round 3 by the review time budget.

中文说明

仅完成部分审查,审查缺口已披露。 建议见行内评论。

未审查(原文为英文):build-and-test — the "Test (macos-latest, Node 22.x)" and "Test (windows-latest, Node 22.x)" CI legs were skipped at this commit, so the two new scripts tests were exercised only on Linux (locally by the build/test agent and on the ubuntu leg); their cross-platform behaviour is unmeasured.

未探索到全部深度(达到工具调用预算):"agent 6b"did not re-run vite build for packages/web-shell to confirm from a fresh build (rather than the tree's existing dist/ ) that the app chunk and lib bundle d…"agent 3c"running vite build --config vite.lib.config.ts and executing the emitted bundle to confirm the lib-build breakage end to end (finding 1 rests on the resolved …"agent 6c"I did not enumerate the .github/workflows jobs that invoke test:scripts / test:ci (which OSes, and whether any are merge_group - or schedule -gated), so…"agent 6c"I did not check git log on packages/cli/src/serve/routes/terminal.ts to establish whether a pre-marking daemon parses the release control frame or would t…"agent 3a"did not run packages/web-shell 's vite.lib.config.ts builds to confirm whether @xterm/xterm survives tree-shaking into dist/index.js (left as the stated …,另有 1 条。

未审查:反向审计——评审时间预算不足,未能开始第 3 轮。

— qwen3.8-max via Qwen Code /review (v0.23.3)

Comment thread scripts/tests/web-terminal-build.test.ts
Comment thread packages/web-shell/vite.config.ts
Comment thread scripts/tests/package-assets.test.js

@qqqys qqqys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE

核对基线:head 538ae5cc1690b3640b18371c970fe9b78708308f

历史阻塞问题:无

本 PR 只有一条 review(qwen-code-ci-bot 于 2026-09-13T03:46:40Z,针对当前 head,状态 COMMENTED),从未出现 CHANGES_REQUESTED。5 条线程全部未解决,但逐条读过,R1-1 至 R1-5 全是 [Suggestion] 级,Critical 级未解决数为 0。按本渠道策略 Suggestion 不作为合入门禁。

本轮独立扫描:未发现 Critical

生产改动只有三处共 +9/-3,我全部读完并核对了调用链:

  1. TerminalPanel.tsx:305-310 的协议不匹配分支是实质修复,不只是文案本地化。 改动前该分支只 term.writeln(硬编码英文) 然后 ws.close(4002, ...);改动后变成 releaseRequested = true → 发送 CONTROL_FRAME_PREFIX + {type:'release'} → 写本地化提示 → 关闭。两点都对:
    • releaseRequested 确实是承重状态,不是死代码。它在 :200 声明,被 :329ws.onopen:若已请求释放则立即发 release 并关闭,避免一个迟到的 socket 反而开起会话)、:370onclose 重连路径:if (disposed || releaseRequested) return;)、:388release() 的幂等门)、:431(卸载清理)四处读取。其中 :370 是关键:不设这个标志,协议不匹配关闭后客户端仍可能走重连,而这种不匹配永远不可能靠重连恢复。
    • 补发 release 控制帧使该分支与 :387-395release() 契约一致(标记已释放 → 通知服务端释放 PTY → 关闭)。改动前直接 close 而不发 release,服务端那一侧的 PTY 可能被继续持有。
  2. i18n.tsx 的两个字典同步添加。 terminal.notice.protocolMismatch 在 EN(:1472-1473)与 ZH(:5169-5170)两侧都加了,文案与原硬编码字符串语义一致,Messages 类型由 TS 约束,缺一侧会编译不过;当前 head 的 Lint & Static(含类型检查)为 pass。
  3. vite.config.ts:91-92target: 'es2021' 不会造成兼容性回归。 Vite 默认的 build.target: 'modules' 本身就以支持原生 ESM 的浏览器为底(含 safari14),而 ES2021 的逻辑赋值运算符正是 Safari 14 / Chrome 85 / Firefox 79 起支持,因此浏览器下限实际没有抬高;换来的是不再让 esbuild 去降级 xterm 里的逻辑赋值。
  4. 其余 6 个文件是设计文档(中英双份同步)、web-terminal-registry.test.tsTerminalPanel.test.tsxscripts/tests/package-assets.test.jsscripts/tests/web-terminal-build.test.ts,均为文档或测试,不含生产逻辑。

CI:当前 head 上 Lint & StaticTest (ubuntu-latest, Node 22.x)Integration Tests (no-AK, No Sandbox)web-shell E2E Smokereview-pr 全部 pass,无 fail、无 pending,没有由本 PR 引入的失败。

两条值得后续处理的 Suggestion(不阻塞本次合入)

我复核后认为这两条观察成立,建议跟进,但都不是本 diff 的缺陷:

  • R1-2 的范围问题确实存在。 packages/web-shell/package.jsonbuildvite build && vite build --config vite.lib.config.ts && vite build --config vite.lib.config.ts --mode transcript && tsc -p tsconfig.lib.json,本次的 target 只加在 vite.config.ts,因此只覆盖第一个构建。而 vite.lib.config.tsbuild 块没有 target,其 rollupOptions.external 列表里也没有 xterm;同时 lib 入口 client/index.tsx 会引入 AppApp 又会渲染 TerminalPanel,所以 xterm 是被打进 lib 产物的,两个 lib 构建仍按默认 target 让 esbuild 降级逻辑赋值。也就是说宿主以库形式嵌入 @qwen-code/web-shell 时,可能仍带着这次要修的那个产物。这不是本 PR 引入的回归(lib 构建的行为在改动前后一致),本 PR 也没有让任何一侧变差,因此我按 Suggestion 处理;但既然根因是「esbuild 降级 xterm 的逻辑赋值会产出无效代码」,建议把同样的 target 加到 vite.lib.config.ts,或抽成共享的 build 片段,让三个构建口径一致。
  • R1-1 的守卫测试可能无法失败。 该条指出 scripts/tests/web-terminal-build.test.tsimport { resolveConfig } from 'vite'scripts/tests/ 解析到根 hoist 的 vite,因此这条回归守卫对它要抓的回归不敏感。测试有效性问题按策略不构成 Critical,但如果守卫抓不到回归,上面那条 target 覆盖范围的问题将来更容易悄悄退回去,建议一并修。

R1-3(断言观察不到 releaseRequested)、R1-4(本地化文案缺测试见证)、R1-5(硬编码了另一个套件已拥有的计数)同样按 Suggestion 处理,不影响本次结论。

结论:无历史阻塞问题,本轮未发现可证明的 Critical,提交 APPROVE。

@wenshao
wenshao added this pull request to the merge queue Sep 13, 2026
Merged via the queue into main with commit bc7a186 Sep 13, 2026
186 checks passed
@chiga0

chiga0 commented Sep 13, 2026

Copy link
Copy Markdown
Collaborator

Post-merge review — findings are for follow-up tracking.

Scope

Source files and all changed tests reviewed. No working tree available for execution rungs; PR provides its own evidence (ES2020 throws ReferenceError, unit suites pass).

Cross-check against existing review (qwen-code-ci-bot R1)

All five findings from the prior review confirmed independently. None refuted. No additional findings.

R1-1 — ineffective regression guard (web-terminal-build.test.ts:87): Confirmed.
resolveConfig in the test script resolves vite from the root, landing on Vite 7.3.6 whose default target is ["chrome107","edge107",...] — a browser list that does not lower logical assignment. Removing target: "es2021" from vite.config.ts hands the test a target that passes, while vite build in the web-shell still runs under Vite 5.4.21 with its ES2020 default and ships broken xterm. The || "esnext" fallback also masks the bug. Fix: assert expect(config.build.target).toBe("es2021") before the runtime probe so the test goes red when the pin is dropped.

R1-2 — ES2021 fix misses lib build (vite.config.ts:92): Confirmed, pre-existing scope.
vite.lib.config.ts bundles @xterm/xterm into dist/index.js at Vite 5's default ES2020 target. TerminalPanel is in the published bundle; a consumer opting "terminal" into WebShellRightPanelItem gets a broken terminal. This PR strictly improves things (fixes the app build) and leaves the lib side as it was at the merge base. One-line fix in the sibling config; worth a follow-up.

R1-3 — releaseRequested mutation not caught (TerminalPanel.test.tsx:321): Confirmed.
Without releaseRequested = true, release() opens a second FakeWebSocket with release=1 URL; that socket is never .open()-ed so its send mock is never invoked and ws.send.mock.calls.length is unchanged. The preceding ordering assertion is effective. Fix: add expect(FakeWebSocket.instances).toHaveLength(1) after the releaseWebTerminal call.

R1-4 — protocolMismatch key unasserted (TerminalPanel.test.tsx:315): Confirmed.
The mock i18n dictionary does not include terminal.notice.protocolMismatch and falls through to ?? key, so the terminal is written the raw key string. No type enforcement on Messages; check-i18n does not cover web-shell. A misspelling in either locale stays green repo-wide.

R1-5 — hardcoded pin count (package-assets.test.js:952): Confirmed.
expect(Object.keys(pins)).toHaveLength(6) fires at fixture construction (line 951) before preparePackage is called. Adding a seventh @lydell/node-pty platform pin fails this test with a misleading message pointing at the count rather than the packaging script. conpty-host.test.ts:64 already owns this count. Fix: expect(Object.keys(pins).length).toBeGreaterThan(0).

Production code — no blocking defect

The three substantive production changes are correct:

  • vite.config.ts ES2021 target: fixes esbuild logical-assignment lowering; PR provides mutation witness (ES2020 throws ReferenceError in requestMode).
  • TerminalPanel.tsx release-before-reject: releaseRequested = true is set before the send; the send executes on an OPEN socket (called from onmessage); ws.onclose returns early on releaseRequested, preventing reconnect; releaseCallbacks entry returns early on the flag, preventing double-send. Ordering verified by test.
  • i18n.tsx key extraction: correct EN and ZH entries, consistent with sibling keys.

Reviewed with AI assistance.

@yiliang114

Copy link
Copy Markdown
Collaborator Author

Closeout for head 314b1ac (round-1 findings R1-1..R1-5).

Changed

  • The build-config probe now resolves through web-shell's own Vite 5 (createRequire anchored at the package), is parameterized over vite.config.ts and vite.lib.config.ts, and pins build.target === 'es2021' — so a dropped target goes red on the toolchain the build actually uses. (R1-1, R1-2)
  • WEB_SHELL_BUILD_TARGET is shared by both configs; the published lib bundle (dist/index.js, both lib invocations) now builds at es2021 like the app. Verified locally: lib build succeeds and the emitted bundle keeps logical assignments intact instead of throwing in requestMode. (R1-2)
  • The mismatch test now also pins the localized restart notice (terminal.notice.protocolMismatch added to the mock dictionary) and asserts no second release-only socket is opened after the mismatch release (FakeWebSocket.instances stays length 1). (R1-3, R1-4)
  • The duplicated node-pty pin count is replaced with a non-empty guard; conpty-host.test.ts keeps owning the count. (R1-5)
  • PR body heading aligned with the template ("What this PR does").

Verified

  • scripts/tests/web-terminal-build.test.ts + package-assets.test.js via --config scripts/tests/vitest.config.ts: 39 passed, 1 skipped.
  • TerminalPanel.test.tsx: 22/22 pass.

Pending: CI + automated re-review on the new head.

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.23.4.

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.

6 participants