Skip to content

feat(vscode-ide-companion): adopt WebShell transcript as the default timeline - #9719

Merged
yiliang114 merged 37 commits into
QwenLM:mainfrom
yiliang114:feat/9187-vscode-webshell-transcript
Aug 24, 2026
Merged

feat(vscode-ide-companion): adopt WebShell transcript as the default timeline#9719
yiliang114 merged 37 commits into
QwenLM:mainfrom
yiliang114:feat/9187-vscode-webshell-transcript

Conversation

@yiliang114

@yiliang114 yiliang114 commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Adopts the shared WebShell transcript renderer as the conversation timeline in the VS Code companion. Raw ACP session/update notifications are bridged through the existing shared SDK daemon transcript reducer and rendered by the WebShell transcript component, so the companion timeline gains the same block model (text, tool calls, plans, permissions) the WebShell uses.

Follow-up commits in this branch drop the original qwen-code.experimental.webShellTranscript opt-in setting and remove the legacy companion MessageList renderer: the WebShell transcript is now the default and only timeline.

The WebShell renderer and its heavy transitive dependencies (charting, diagramming, syntax highlighting, math, markdown) are still loaded lazily via code splitting (React.lazy), so the WebShell chunk is fetched when the timeline mounts rather than with the initial webview bundle.

Why it's needed

The companion previously maintained its own transcript timeline, separate from the WebShell's more complete renderer. This closes that gap (#9187) by reusing the existing shared renderer and reducer rather than duplicating the timeline logic again.

Reviewer Test Plan

How to verify

  1. In a VS Code window with this extension build installed, start a conversation.
  2. Observe that the timeline is rendered by the WebShell transcript UI (user turns, assistant text, tool calls, plans).
  3. Reload the window / switch sessions and confirm session boundaries render cleanly with no cross-session bleed.
  4. Confirm normal conversation behavior (submit, cancel, approvals) is unchanged.

Evidence (Before & After)

Before (legacy renderer) After (this PR)
Timeline Legacy companion MessageList timeline WebShell transcript (user turns → tool summary → assistant → composer), now the default
Bundle entry 504KB + 2 static shared chunks ≈ 755KB; no WebShell chunk same entry bundle; + ~4MB WebShell chunk, lazy-loaded on timeline mount
CSP script-src ${cspSource}; (no wasm-unsafe-eval) script-src ${cspSource} 'wasm-unsafe-eval'; (WebShell WASM)

Screenshots: WebShell transcript timeline · legacy timeline (pre-PR)

Unit tests cover the notification-to-blocks reduction (including consecutive-chunk merging) and session-boundary state resets; the companion suite, typecheck, lint, and production bundle build pass.

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

Environment (optional)

node esbuild.js --production, npm run check-types, npm run lint, npx vitest run.

Risk & Scope

  • Main risk or tradeoff: the WebShell renderer pulls in a large dependency graph, so it is code split and lazy-loaded; the timeline renderer swap replaces the legacy MessageList entirely, and the CSP now includes wasm-unsafe-eval.
  • Known parity gaps vs the legacy renderer are being re-wired during review (tracked in the open review threads); anything not re-wired before merge will be listed here explicitly.
  • Deliberate feature removal: the user-message edit/rewind feature (editMessage) was not carried over to the WebShell transcript timeline; with no edit entry point in the timeline, its now-unreachable backend (the handler rewind flow, the editTargetTurnIndex/onSubmitted submit options, and their tests) is removed in this PR instead of being kept as dead code.
  • Not validated / out of scope: stable block identity (segment IDs) and structured prompt-id extraction are deferred to a follow-up; this version relies on the reducer's ordinal ordering and its built-in merge tolerance.
  • Breaking changes / migration notes: none for users; the removed setting was experimental and never defaulted on.

Linked Issues

Reuse WebShell transcript UI in the VS Code companion — #9187.

Note: PR #9641 independently contains an adapter file that overlaps in intent with this change. This PR is a thin, standalone implementation scoped to #9187 and does not include the other changes bundled there.

中文说明

本 PR 做了什么

在 VS Code companion 中采用共享的 WebShell transcript 渲染器作为对话时间线。原始 ACP session/update 通知被桥接进现有的共享 SDK daemon transcript reducer,并由 WebShell transcript 组件渲染,使 companion 时间线获得与 WebShell 相同的块模型(文本、工具调用、计划、权限)。

本分支的后续提交移除了最初的 qwen-code.experimental.webShellTranscript 实验性开关,并删除了旧的 companion MessageList 渲染器:WebShell transcript 现在是默认且唯一的时间线。

WebShell 渲染器及其重型传递依赖(图表、流程图、语法高亮、数学、markdown)仍通过代码分割(React.lazy)懒加载——WebShell chunk 在时间线挂载时才拉取,而不是随初始 webview bundle 一起加载。

为什么需要

companion 此前维护着一条独立的 transcript 时间线,与 WebShell 更完整的渲染器相互分离。本改动通过复用现有的共享渲染器和 reducer 来弥合这一差距(#9187),而不是再次重复实现时间线逻辑。

Reviewer 测试计划

如何验证

  1. 在安装了本扩展构建的 VS Code 窗口中开始一段对话。
  2. 观察时间线由 WebShell transcript UI 渲染(用户消息、助手文本、工具调用、计划)。
  3. 重载窗口 / 切换会话,确认会话边界渲染干净、无跨会话串扰。
  4. 确认正常对话行为(发送、取消、审批)不受影响。

证据(Before & After)

Before(旧渲染器) After(本 PR)
时间线 旧 companion MessageList 时间线 WebShell transcript(用户消息 → 工具摘要 → 助手 → composer),现为默认
Bundle entry 504KB + 2 个静态共享 chunk ≈ 755KB;无 WebShell chunk entry bundle 不变;+ ~4MB WebShell chunk,时间线挂载时懒加载
CSP script-src ${cspSource};(无 wasm-unsafe-eval) script-src ${cspSource} 'wasm-unsafe-eval';(WebShell WASM)

截图:WebShell transcript 时间线 · 旧时间线(PR 前)

单元测试覆盖了通知到块的归约(包括连续 chunk 合并)与会话边界状态重置;companion 测试套件、typecheck、lint 和生产 bundle 构建均通过。

测试环境

操作系统 状态
🍏 macOS ✅ 已测试
🪟 Windows ⚠️ 未测试
🐧 Linux ⚠️ 未测试

环境(可选)

node esbuild.js --productionnpm run check-typesnpm run lintnpx vitest run

风险与范围

  • 主要风险或权衡:WebShell 渲染器引入了庞大的依赖图,因此采用代码分割懒加载;时间线渲染器整体替换了旧 MessageList,CSP 现在包含 wasm-unsafe-eval
  • 与旧渲染器相比已知的功能差距正在 review 过程中逐项补回(见未解决的 review threads);合并前未补回的项会在此处明确列出。
  • 有意的功能移除:用户消息编辑/回退(editMessage)未随 WebShell transcript 时间线保留;时间线中没有编辑入口,其已不可达的后端(handler 中的 rewind 流程、editTargetTurnIndex/onSubmitted 提交选项及其测试)在本 PR 中删除,而不是作为死代码保留。
  • 未验证 / 超出范围:稳定的块身份(segment ID)和结构化的 prompt-id 提取留待后续跟进;此版本依赖 reducer 的顺序排序及其内建的合并容错。
  • 破坏性变更 / 迁移说明:对用户无;被移除的设置是实验性的,且从未默认开启。

关联 Issue

在 VS Code companion 中复用 WebShell transcript UI —— #9187

说明:PR #9641 独立地包含了一个与本改动意图重叠的 adapter 文件。本 PR 是一个仅针对 #9187 的薄、独立实现,不包含该 PR 中捆绑的其他改动。

…imental flag

Bridge ACP session/update notifications into the shared SDK daemon transcript reducer and render the result with the WebShell transcript component, gated on qwen-code.experimental.webShellTranscript (default off).

The WebShell renderer and its heavy transitive dependencies (echarts, mermaid, shiki, codemirror, katex) are lazily loaded via esbuild code splitting, so the default configuration keeps the ~700KB webview bundle unchanged.
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Re-run at the current head, which has moved well past the flag-gated version the last pass reviewed.

Template looks good ✓

Problem: Real and documented. This is the deliberately-narrow first stage of #9187 (open, triaged with an "accept for exploration" direction verdict), part of the WebShell convergence roadmap (#5883 — Desktop already converged in #8092). The companion maintaining a second transcript renderer that drifts from the maintained one is an observed, ongoing cost, not a hypothetical.

Direction: Aligned. With Desktop shipped as a thin shell over WebShell, the VS Code webview was the last heterogeneous live host. Since the last triage pass the PR has taken the follow-up step the issue's roadmap points at: the experimental.webShellTranscript flag is dropped and the WebShell transcript is now the default and only timeline, with the legacy MessageList renderer removed. Claude Code's CHANGELOG has no direct reference to this feature, but IDE-companion surfaces are actively maintained upstream, so the area is relevant.

Size: 17.0k lines changed sounds alarming but decomposes cleanly: ~10.9k is NOTICES.txt (generated license artifact of adding the web-shell workspace dependency, plus generator improvements), ~3.5k is tests, leaving ~2.6k production logic lines. That is above the 1000-line large-PR advisory, noted here for awareness — but the growth since the last pass is the legacy renderer deletion plus re-wiring the features it carried (copy commands, file links, image display), which is the minimal set for a renderer swap without regressions. Splitting it now would be artificial. feat-type, author is a maintainer — no escalation needed.

Approach: Still matches what the issue asked for — reuse the shared SDK daemon transcript reducer and WebShellTranscript behind a thin adapter, lazy-load the heavy renderer via code splitting so the entry bundle is untouched. Every addition since the last pass is a consequence of making that the default: session-boundary resets, cached-history seeding, local-only notice slot, transcript echo for the user's own prompt. Two disclosed tradeoffs worth keeping visible: the user-message edit/rewind feature was deliberately not carried over (its now-unreachable backend is removed rather than kept as dead code), and the CSP gains wasm-unsafe-eval + data: fonts for Shiki/KaTeX.

Risk: No high-risk-path matches from the revert-history signal. The one unconditional change from the earlier pass (IIFE → ESM) remains unconditional — now by design, since the timeline is always WebShell.

Moving on to code review. 🔍

中文说明

按当前 head 重新运行——相比上次审查时的 flag 版本,本分支已大幅推进。

模板完整 ✓

问题:真实且有据。这是 #9187(开放中,已按"接受探索"方向结论完成 triage)刻意收窄的第一阶段,属于 WebShell 收敛路线图(#5883,Desktop 已在 #8092 完成收敛)。companion 维护第二套与主线渐行渐远的 transcript 渲染器是持续发生的实际成本,不是假设性问题。

方向:对齐。Desktop 已作为 WebShell 的薄壳发布,VS Code webview 是最后一个自渲染的 live host。自上次 triage 以来,PR 已迈出 issue 路线图指向的下一步:移除 experimental.webShellTranscript 开关,WebShell transcript 成为默认且唯一的时间线,旧 MessageList 渲染器被删除。

规模:17.0k 行改动听起来吓人,但拆解清晰:约 10.9k 是 NOTICES.txt(新增 web-shell 依赖的生成许可证文件 + 生成器改进),约 3.5k 是测试,生产逻辑约 2.6k 行。超过 1000 行大 PR 提示线,仅在此提示——但上次之后的增量是删除旧渲染器并重接它承载的功能(复制命令、文件链接、图片展示),这是无回归换渲染器的最小集合,此时拆分是人为的。feat 类型、作者是维护者——无需升级。

方案:仍然符合 issue 的要求——薄适配器复用共享 SDK daemon transcript reducer 和 WebShellTranscript,重依赖经代码分割懒加载、初始 bundle 不受影响。上次之后的所有新增都是"成为默认"的后果:会话边界重置、缓存历史回填、本地通知槽、用户消息回显。两个已披露的取舍值得保持可见:用户消息编辑/回退功能有意不迁移(其已不可达的后端被删除而非作为死代码保留);CSP 为 Shiki/KaTeX 增加 wasm-unsafe-evaldata: 字体。

风险:回滚历史信号无高风险路径命中。上一轮的唯一无条件改动(IIFE → ESM)仍是无条件的——现在是设计使然,因为时间线永远是 WebShell。

进入代码审查 🔍

Qwen Code · qwen3.8-max

Reviewed at 82912c5095214f85473f58637297900bc7403430 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Code review

Re-reviewed against an independent proposal (raw ACP notifications forwarded verbatim → shared SDK reducer → lazily rendered WebShellTranscript, with session-boundary resets and the legacy renderer deleted). The implementation matches that shape, and the load-bearing claims check out against the actual sources rather than the diff's framing:

  • Envelope claim holds. getSessionUpdatePayload unwraps .update when present, so passing the whole SessionNotification as data works for both live streaming and rehydration replay; normalizeDaemonEventreduceDaemonTranscriptEvents is exactly the daemon path.
  • The anti-merge marker is real. cachedMessageToNotification stamps cached history rows with qwenDiscreteMessage: true, and the SDK reducer honors it (canMergeTextDelta in the daemon transcript reducer refuses to fold such chunks into the active block), so cached rows render as discrete messages instead of one merged blob.
  • Copy-command plumbing matches the renderer. findBlockByRowKey's msg: / tg- key assumptions match web-shell MessageList's data-message-row-key scheme, and the contributed copyMessage/copyAllMessages/copyLastReply when clauses already exist in package.json — reused, not duplicated.
  • Props contract matches. WebShellTranscriptProps accepts exactly what the companion passes (blocks, theme, style, collapseCompletedTurns), and isResponding is indeed hardcoded false in WebShellTranscript — which is why the companion sets collapseCompletedTurns={false} rather than letting in-progress turns collapse mid-response. The comment at the call site says so honestly.

The previous pass's one real gap is closed, thoroughly. Session switching no longer accumulates transcripts: useAcpTranscript resets on all three boundaries (qwenSessionSwitched / conversationCleared / conversationLoaded), pins the session guard to the id a boundary publishes (liveSessionId winning over the archived id for load-failure fallbacks), drops trailing frames from an abandoned session, and mirrors useWebViewMessages' requestId tracking so a stale/untagged streamEnd can't finalize a live turn early. I also checked the double-render risk on switch: the ACP-load-success path seeds an empty message list and lets history replay through transcriptUpdate alone, while offline/fallback paths seed cached rows with no replay — one render source per path. A 740-line hook test file covers the boundary matrix.

No critical blockers found. The non-blocking observations, all disclosed in the PR body rather than discovered here: edit/rewind is deliberately gone (backend removed with it); collapseCompletedTurns stays off until a live isResponding prop exists; stable block identity is deferred to a follow-up; the CSP's wasm-unsafe-eval and data: font grants plus the ESM bundle are unconditional because the timeline is now always WebShell. The extension-host transcript echo of the user's own prompt (and attached images, with the MAX_IMAGE_SIZE guard and graceful read failures) mirrors the daemon-bridge echo and degrades to text-only on failure — that's the right failure direction.

sequenceDiagram
    participant P1 as ACP connection
    participant P2 as QwenAgentManager
    participant P3 as SessionMessageHandler
    participant P4 as WebViewProvider
    participant P5 as useAcpTranscript hook
    participant P6 as SDK transcript reducer
    participant P7 as WebShellTranscript
    P1->>P2: session update notification
    P2->>P4: onTranscriptUpdate (verbatim)
    P3->>P4: user prompt echo (text and images)
    P4->>P5: transcriptUpdate message
    P5->>P5: session guard - drop foreign session frames
    P5->>P6: reduce via normalizeDaemonEvent
    P6-->>P7: blocks render lazily
    P3->>P5: boundary reset (switch, clear, load)
Loading

Testing

CI evidence for the reviewed commit (fetched via API; PR code never executed here): everything that ran is green — Test (ubuntu-latest, Node 22.x), Desktop Shell (ubuntu-22.04), Desktop Shell (windows-2022), web-shell E2E Smoke (ubuntu-latest, Node 22.x), Capture web-shell visuals (ubuntu-latest, Node 22.x), Dependency CVE audit, Secret scan (TruffleHog), and the Qwen Code CI / Security Checks / Web-shell Visuals runs all completed successfully; 78 path-filtered matrix jobs skipped, one superseded orchestration job cancelled, zero failures.

What CI cannot settle here is the central claim — that the timeline actually renders inside a real VS Code webview. On that: @wenshao ran a deep local verification against exactly this head (34/34 scripted assertions, verdict non-blocking findings) and a host-level pass, and approved; the web-shell visuals job rendered this head against a mock daemon; and a sandboxed @qwen-code /verify run is already in flight on this commit (it was triggered with this triage re-run), so its A/B report will land as a follow-up comment rather than needing anyone to trigger it.

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Desktop Shell (windows-2022) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
Capture web-shell visuals (ubuntu-latest, Node 22.x) ✅ success
Dependency CVE audit ✅ success
Secret scan (TruffleHog) ✅ success
precheck-pr / precheck ✅ success
Qwen Code CI (workflow) ✅ success
Security Checks (workflow) ✅ success
Web-shell Visuals (workflow) ✅ success
中文说明

代码审查

重新对照独立方案审查(原始 ACP 通知原样转发 → 共享 SDK reducer → 懒加载的 WebShellTranscript,含会话边界重置与旧渲染器删除)。实现与该方案同形,且关键论断均对照真实源码验证:

  • 信封论断成立:整个 SessionNotification 作为 data 传入对直播流与重放都有效。
  • 反合并标记是真实机制:缓存历史行带 qwenDiscreteMessage: true,SDK reducer 的 canMergeTextDelta 确实拒绝合并此类块。
  • 复制命令与渲染器对接正确msg: / tg- 行键假设与 web-shell MessageList 的 data-message-row-key 方案一致;三个复制命令的 when 子句复用 package.json 已有配置。
  • 组件 props 契约一致isRespondingWebShellTranscript 中确实硬编码为 false——这正是 companion 传 collapseCompletedTurns={false} 的原因,调用处注释如实说明。

上一轮的唯一真实缺口已被彻底关闭:会话切换不再累积 transcript——三个边界全部重置、按边界发布的会话 id 钉住守卫(加载失败回退时 liveSessionId 优先)、丢弃已放弃会话的尾帧、并按 requestId 丢弃过期 streamEnd。另核实了切换时的双重渲染风险:ACP 加载成功路径只靠 transcriptUpdate 重放,离线/回退路径只回填缓存行——每条路径只有一个渲染来源。740 行 hook 测试覆盖边界矩阵。

未发现阻断性问题。非阻断观察点均已在 PR 正文披露:编辑/回退有意移除(后端一并删除);collapseCompletedTurnsisResponding 贯通后恢复;稳定块 ID 留作后续;CSP 的 wasm-unsafe-evaldata: 字体、ESM bundle 均为无条件改动(时间线现在总是 WebShell)。扩展宿主对用户消息与图片的 transcript 回显有 MAX_IMAGE_SIZE 保护且读取失败优雅降级为纯文本——失败方向正确。

测试

审查提交的 CI 证据(经 API 获取,本地未执行任何 PR 代码):所有运行的检查全绿,零失败;78 个按路径过滤的矩阵任务跳过。CI 无法定论的是"时间线在真实 VS Code webview 中正确渲染"这一核心主张——对此:@wenshao 已在同一 head 上完成深度本地验证(34/34 断言通过,结论为非阻断发现)并在主机层验证后批准;web-shell visuals 任务已渲染该 head;沙箱 @qwen-code /verify 运行已在本次 triage 触发下进行中,其 A/B 报告将作为后续评论跟进。

Qwen Code · qwen3.8-max

Reviewed at 82912c5095214f85473f58637297900bc7403430 · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — both blockers from the last pass are closed with evidence, review found no criticals, and a maintainer verified this exact head locally; approving with two disclosed parity follow-ups.

Stepping back: the two things that held this at 3/5 last time are both settled. The session-switch transcript leak is not just patched but built out properly — three boundary resets, a session-id guard that survives load-failure fallbacks, stale streamEnd protection, and a boundary-matrix test suite to pin it. And "nobody has seen it render" is no longer true: @wenshao ran a deep local verification against exactly this head (34/34 assertions, non-blocking findings only) plus a host-level pass, and approved; the visuals job rendered the head against a mock daemon; the author's WebView and VS Code host E2E reports are in the thread (author's claims, but they now sit next to an independent maintainer verification rather than standing alone).

My independent proposal and the implementation remain the same shape, and the load-bearing integration points hold up when checked against the SDK and web-shell sources rather than trusted from the diff. The code reads like someone thinking about blast radius: everything that could fail lazily has a recovery path (chunk-load error boundary with reload, image echo degrading to text-only, cached seeding only where no replay will follow), and the deletions — legacy renderer, edit/rewind backend — remove exactly the code the swap makes unreachable, nothing else. In six months this is a thank-them change: one maintained timeline instead of two drifting ones.

Why not 5/5, stated plainly so the follow-ups don't get lost:

  • The renderer-swap claim still rests on local and mock-daemon verification in CI terms. The sandboxed /verify run on this commit is already in flight; if its A/B report surfaces anything, it will land as a follow-up comment and can be acted on before merge.
  • Two disclosed parity gaps remain intentional follow-ups, both named in the PR body: collapseCompletedTurns stays disabled until a live isResponding prop exists in WebShellTranscript, and stable block identity (segment IDs) is deferred — the timeline leans on ordinal ordering plus the reducer's merge tolerance until then.

Approving, pinned to the reviewed commit below.

中文说明

置信度:4/5 —— 上一轮的两个阻断点均已带证据关闭,审查未发现阻断性问题,且维护者已在本地验证该 head;批准,附两个已披露的对等性后续项。

整体来看:上次让它停在 3/5 的两件事都已解决。会话切换的 transcript 泄漏不只是打了补丁,而是被正确建设——三个边界重置、在加载失败回退下仍有效的会话 id 守卫、过期 streamEnd 防护,以及钉住这些行为的边界矩阵测试套件。"没人见过它渲染"也不再成立:@wenshao 在同一 head 上完成深度本地验证(34/34 断言,仅非阻断发现)与主机层验证并已批准;visuals 任务已用 mock daemon 渲染该 head;作者的 WebView 与 VS Code 主机 E2E 报告在帖子中(属作者声明,但现在与独立的维护者验证并列,而非孤证)。

我的独立方案与实现同形,关键集成点对照 SDK 与 web-shell 源码核验成立。代码能看出作者在考虑影响半径:所有可能惰性失败的地方都有恢复路径(chunk 加载错误边界 + 重载、图片回显降级为纯文本、仅在无重放跟随的路径回填缓存),删除的部分——旧渲染器、编辑/回退后端——恰好是本次替换使其不可达的代码,没有多余删除。六个月后这是值得感谢的改动:一条被维护的时间线,而不是两条渐行渐远的。

为什么不是 5/5,直说以免后续项被遗忘:

  • 就 CI 而言,渲染器替换的主张仍依赖本地与 mock daemon 验证。 沙箱 /verify 已在该提交上运行中;若其 A/B 报告发现问题,将作为后续评论跟进,可在合并前处理。
  • 两个已披露的对等性缺口是有意留作后续(均已在 PR 正文写明):collapseCompletedTurnsWebShellTranscript 拥有实时 isResponding prop 前保持关闭;稳定块 ID(segment ID)延期——在此之前时间线依赖序号排序与 reducer 的合并容忍度。

批准,锚定在下方被审查的提交。

Qwen Code · qwen3.8-max

Reviewed at 82912c5095214f85473f58637297900bc7403430 · re-run with @qwen-code /triage

@yiliang114

Copy link
Copy Markdown
Collaborator Author

Visual verification (browser harness)

Verified the feature flag behaves as intended by loading the built dist/webview.js in a plain browser and feeding it synthetic ACP transcriptUpdate messages. Real VS Code runtime verification (shiki oniguruma WASM under the webview CSP) is still pending manual acceptance.

Flag ON (qwen-code.experimental.webShellTranscript: true) — the WebShell transcript UI renders the conversation (user message → tool summary → assistant → composer):

9187-flag-on

Flag OFF (default) — the legacy timeline is unchanged (Past Conversations + empty-state prompt + composer), no WebShell root is mounted:

9187-flag-off

Note: the composer's border/background resolve from VS Code design tokens (--vscode-inlineChatInput-border, --vscode-menu-background) that VS Code always injects into a real webview. The harness injects the same tokens into :root; without them the composer is still present but its border/background compute to transparent — a harness artifact, not a regression.

Comment thread packages/vscode-ide-companion/src/webview/hooks/useAcpTranscript.ts
jifeng
jifeng previously requested changes Aug 22, 2026

@jifeng jifeng 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.

Independent local validation — request changes

Validated exact head 1e9e2e449c72eb9628600889c42453e153699c60 in an isolated worktree on macOS arm64.

Positive evidence

  • Adapter and WebView content tests: 9/9 passed.
  • VS Code Companion typecheck: passed after building the shared SDK, WebUI and Web Shell packages.
  • Production extension build and VSIX packaging: passed; the packaged artifact was 45.02 MB and contained the split WebShell chunks.
  • A real isolated VS Code Extension Development Host activated the PR extension successfully (qwenlm.qwen-code-vscode-ide-companion), registered the readonly file-system provider and started its IDE server.
  • GitHub Ubuntu test, Web Shell E2E smoke, desktop and security checks are green.

Merge blocker

The experimental transcript state never resets on qwenSessionSwitched (or another session boundary). The existing VS Code flow explicitly keeps the webview mounted, clears the legacy messages, then replays the selected session through ACP. useAcpTranscript ignores every message except transcriptUpdate, so the previous session's reducer state remains live.

I reproduced this on the unmodified exact head with a focused React/jsdom probe: emit user text alpha for session A, emit qwenSessionSwitched to session B, then emit beta for B. Expected output contained only beta; actual output was one user block with text alphabeta. This can expose one session's content inside another session and is therefore merge-blocking.

Recommendation: reset both stateRef and rendered blocks on the same qwenSessionSwitched/new-session boundary used by the legacy path, add a regression test that switches between two replayed sessions, and rerun the packaged Extension Development Host scenario.

The root all-package build initially failed because npm ci --ignore-scripts had not applied the repository's Ink patch; after applying the documented postinstall and building the PR's required shared packages, all PR-scoped checks above passed.

中文验证报告

独立本地验证——当前不建议合并

已在 macOS arm64 的隔离 worktree 中验证精确 head 1e9e2e449c72eb9628600889c42453e153699c60

正向证据

  • Adapter 与 WebView Content 测试:9/9 通过
  • 构建共享 SDK、WebUI、Web Shell 后,VS Code Companion 类型检查:通过
  • 生产扩展构建与 VSIX 打包:通过;打包产物 45.02 MB,并包含拆分后的 WebShell Chunks。
  • 真实隔离的 VS Code Extension Development Host 成功激活本 PR 扩展(qwenlm.qwen-code-vscode-ide-companion),注册只读文件系统 Provider 并启动 IDE Server。
  • GitHub 的 Ubuntu 测试、Web Shell E2E Smoke、桌面端及安全检查均为绿色。

合并阻塞项

实验 Transcript 状态不会在 qwenSessionSwitched 或其他 Session 边界上重置。现有 VS Code 流程会保持 Webview 挂载,清空旧版消息,然后通过 ACP Replay 新选择的 Session;但 useAcpTranscript 会忽略 transcriptUpdate 之外的所有消息,因此前一个 Session 的 Reducer 状态仍然存在。

我在未修改的精确 head 上通过定向 React/jsdom Probe 复现:先为 Session A 发送用户文本 alpha,再发送切换到 Session B 的 qwenSessionSwitched,随后为 B 发送 beta。期望结果只包含 beta;实际结果是一个文本为 alphabeta 的用户 Block。这会把一个 Session 的内容显示在另一个 Session 中,属于合并阻塞问题。

建议在旧版路径使用的同一个 qwenSessionSwitched/新 Session 边界上同时重置 stateRef 和渲染 Blocks,增加两个 Replay Session 之间切换的回归测试,并重新运行打包后的 Extension Development Host 场景。

根目录全包构建最初因为 npm ci --ignore-scripts 未应用仓库 Ink Patch 而失败;执行项目约定的 postinstall 并构建本 PR 所需共享包后,上述 PR 范围验证全部通过。

@yiliang114

yiliang114 commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator Author

Test report — before / after

This PR is a flag switch (qwen-code.experimental.webShellTranscript, default off). The hard guarantee is: flag-off is byte-for-byte the legacy companion; only flag-on routes the transcript through the WebShell UI. The table below is the comparison a reviewer should read first, then the evidence.

Before (flag OFF, default) vs After (flag ON)

Dimension Before — flag OFF (unchanged) After — flag ON (WebShell transcript)
Rendered UI Legacy timeline: Past Conversations + empty-state prompt + composer WebShell transcript: user → tool summary → assistant → composer
Message path Legacy message handler (untouched) Thin ACP adapter reduceSessionNotificationuseAcpTranscript hook, its own message listener (does not touch the legacy handler)
CSP script-src ${cspSource}; — no wasm-unsafe-eval script-src ${cspSource} 'wasm-unsafe-eval'; (Shiki Oniguruma WASM)
Bundle entry 504KB + 2 static shared chunks ≈ 755KB; WebShell chunk not fetched + ~4MB WebShell chunk, lazy via React.lazy + esbuild splitting, fetched only when enabled
Code blocks N/A (no highlight path) Shiki highlights (after CSP fix below)

No ACP protocol change: SessionNotification.update already matches the daemon envelope's data.update, so the adapter just wraps it and feeds the SDK reducer.

CSP fix before / after (d9cc2140114b)

Before the fix After the fix
flag-on code highlight Shiki WASM blocked by CSP → degrades to plain text (try/catch fallback, no white-screen) wasm-unsafe-eval granted → WASM loads → code blocks highlight
flag-off CSP script-src ${cspSource}; script-src ${cspSource}; — byte-for-byte identical, no widened permission for legacy users

Evidence

  • acpTranscriptAdapter.test.ts — 2 tests: user_message_chunk wraps→reduces to a user text block; consecutive chunks merge into one block.
  • WebViewContent.test.ts — 4 flag tests: attribute omitted when off; data-web-shell-transcript="true" when on; script-src without wasm-unsafe-eval when off; with it when on.
  • Full companion suite npx vitest run — 55 files / 506 passed / 1 skipped (was 504 before the CSP fix).
  • npm run check-types and npm run lint — pass.
  • npm run generate:notices --workspace=qwen-code-vscode-ide-companion — NOTICES.txt updated (642 deps; web-shell's transitive heavy deps echarts/mermaid/shiki/katex/codemirror/react-markdown recorded).
  • Production bundle audit — entry 504KB; dist/chunks/ = 426 files / ~19MB, all lazy-reachable.
  • Browser harness — before (flag-off) renders the legacy empty state unchanged; after (flag-on) renders the WebShell transcript (screenshots in the previous comment).

Remaining gaps (out of scope, tracked separately)

Manual regression (real VS Code — the part a browser harness cannot cover)

  1. flag-off regression — full session with the flag off; legacy timeline must behave exactly as the "before" column (zero change).
  2. flag-on end-to-end — multi-step task + tool calls + thinking + a code block; no dropped/duplicated blocks, streaming appends incrementally (the "after" column).
  3. Composer → transcript — submit from the composer; a new user message appears in the transcript.
  4. Code-block highlight — emit a ```ts block; confirm it highlights with no CSP violation in the console (verifies the CSP fix).
  5. Artifact preview — trigger a PDF/image artifact; expected blocked (tracked in VS Code companion WebShell transcript: artifact blob CSP #9727).
  6. Permission request — trigger an edit permission under the flag; the legacy dialog still opens and accepts/rejects.
  7. Session switch — create/switch sessions under the flag; the transcript resets correctly.
  8. Empty-state branding — flag on + empty session: middle area blank (legacy EmptyState replaced by the blank WebShell empty list). Known UX gap.

@yiliang114

Copy link
Copy Markdown
Collaborator Author

CSP gap #1 resolved (Shiki WASM)

Fixed in d9cc2140114bWebViewContent.ts now grants wasm-unsafe-eval to script-src only when the WebShell transcript flag is on; the flag-off CSP string is byte-for-byte unchanged.

  • Flag-off: script-src ${cspSource}; (unchanged — no widened permission for legacy users).
  • Flag-on: script-src ${cspSource} 'wasm-unsafe-eval'; so Shiki's Oniguruma WASM can instantiate.
  • Regression: two WebViewContent.test.ts cases pin the flag-off and flag-on CSP strings.

Full suite after the fix: 506 passed / 1 skipped; check-types and lint pass.

Gaps #2 (artifact blob) and #3 (local-control fetch) remain out of scope — blob tracked in #9727; local-control is N/A until the companion grows a settings surface.

…line

Drop the experimental flag and the legacy MessageList renderer. The companion timeline now always renders through the shared WebShell transcript component, fed by ACP session/update notifications via the SDK daemon transcript reducer (lazy loaded through esbuild code splitting).

The flag-gated wiring is removed: the qwen-code.experimental.webShellTranscript setting, the conditional CSP/body attribute in WebViewContent, and the legacy MessageList path in App.tsx (~850 lines). The webview CSP now grants wasm-unsafe-eval unconditionally for Shiki's Oniguruma WASM.
@yiliang114 yiliang114 changed the title feat(vscode-ide-companion): reuse WebShell transcript UI behind experimental flag feat(vscode-ide-companion): adopt WebShell transcript as the default timeline Aug 22, 2026
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Visual verification (direct replacement)

Rendered through the web-shell visual pipeline (mock daemon, no auth) — the companion timeline is now the WebShell transcript component, captured dark and light.

Scenario dark light
Conversation + Shiki code highlight session-transcript-dark session-transcript-light
Core flow · collapsed core-flow-collapsed-dark core-flow-collapsed-light
Core flow · expanded core-flow-expanded-dark core-flow-expanded-light

The expanded scenario walks a full agent turn end to end: user message → collapsed thinking → Read tool (running → completed) → assistant markdown with a Shiki-highlighted code block and copy button.

… switch

The experimental useAcpTranscript hook only consumed transcriptUpdate
messages, so its reducer state survived session boundaries. When the
extension switched sessions it kept the webview mounted and replayed the
newly-selected session through ACP, causing the previous session's blocks
to merge with the new replay (e.g. user text "alpha" from session A leaked
into session B as "alphabeta").

Reset both the reducer state and the rendered blocks on the same
boundaries the legacy message flow uses: qwenSessionSwitched (sent before
the ACP replay of the selected session) and conversationCleared (new
session). Adds a regression test that replays two sessions with a switch
between them.
@yiliang114
yiliang114 requested a review from jifeng August 22, 2026 15:22

@doudouOUC doudouOUC 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.

Review of PR #9719 — adopt WebShell transcript as the default timeline

Summary

This PR replaces the legacy companion timeline with the shared WebShell transcript renderer, bridged through ACP session/update notifications via the SDK daemon transcript reducer. The WebShell renderer and its heavy transitive dependencies are loaded lazily through esbuild code splitting.

Prior review: jifeng reviewed the initial commit and found a Critical session-switch state leak, which was fixed in commit 7fc35c1. The fix is verified by the useAcpTranscript.test.ts regression tests.

Verification: All 12 PR-scoped tests pass (3 test files, 12 tests). The session boundary reset, adapter wrapping, and CSP changes are correctly tested.

Findings

No Criticals or Suggestions. The changes are clean, well-structured, and the prior round's blocker has been addressed. The following observations are for awareness:

  1. CSP hardening'wasm-unsafe-eval' is now unconditional in the webview CSP. This is required for Shiki's Oniguruma WASM engine used by the WebShell transcript. The trade-off is documented in the PR description.

  2. onTranscriptUpdate ordering — The callback fires before the rehydration branch in qwenAgentManager.ts. This is intentional (documented in code) and harmless since the hook is idempotent to duplicate notifications.

  3. Test coverage — The old getLastUserTurnIndex and MessageList tests were removed with the legacy component. The new tests cover the adapter (2 tests), the hook with session boundary reset (3 tests), and the CSP changes (2 tests). This is proportionate to the change scope.

Test Results

Test Files  3 passed (3)
     Tests  12 passed (12)

Reviewed at commit 7fc35c16f286dc90311a898524841ebaf18cf2f5.

@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.

Not reviewed: reverse audit — an auditor ran and opened its brief, but no agent was launched with the prompt the CLI built — the launch was written by hand, and what the agent was actually asked is not what this skill certifies.

中文说明

仅完成部分审查,审查缺口已披露。

未审查:反向审计——有审计 agent 运行并打开了自己的 brief,但没有 agent 是用 CLI 构建的 prompt 启动的——启动 prompt 是手写的,agent 实际被要求做的并不是本 skill 所认证的内容。

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

Comment thread packages/vscode-ide-companion/src/webview/providers/WebViewContent.ts Outdated
Comment thread packages/vscode-ide-companion/src/webview/hooks/useAcpTranscript.ts
Comment thread packages/vscode-ide-companion/src/webview/hooks/useAcpTranscript.ts Outdated
Comment thread packages/vscode-ide-companion/src/webview/App.tsx
Comment thread packages/vscode-ide-companion/src/webview/App.tsx Outdated
Comment thread packages/vscode-ide-companion/NOTICES.txt
Comment thread packages/vscode-ide-companion/src/webview/App.tsx Outdated
Comment thread packages/vscode-ide-companion/src/webview/providers/WebViewContent.ts Outdated
Comment thread packages/vscode-ide-companion/src/webview/App.tsx
Comment thread packages/vscode-ide-companion/NOTICES.txt
- reset the transcript on `conversationLoaded` too, closing the same
  cross-session leak the previous commit fixed for `qwenSessionSwitched`
  and `conversationCleared` (agent reconnect posts only this boundary)
- track the active session id and drop late `transcriptUpdate` frames
  whose `sessionId` no longer matches, so a previous session's trailing
  frames cannot contaminate the next session's timeline
- seed the transcript from cached messages carried by
  `qwenSessionSwitched` so offline restores and load-failure fallbacks
  render their history instead of a blank timeline
- dispatch `assistant.done` on `streamEnd`/`sessionLoadComplete` so the
  final assistant/thought block of a turn (or history replay) does not
  stay `streaming: true` forever
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Closeout — round cap-4 of the R1 bot review, plus the R1-1 body fix, pushed to the fork as 8dc018b9bf.

Body fix (orchestrator, no code):

  • R1-1: PR body rewritten to match the current head — the WebShell transcript is now the default and only timeline (the experimental setting and the legacy MessageList were removed in 78c07b4686); lazy code-splitting and the CSP change are described as unconditional. EN + 中文 sections updated, read-back verified.

Fixed in 8dc018b9bf (3 files, +298/-8):

  • R1-3: conversationLoaded added to the reset boundaries in useAcpTranscript — the reconnect path (WebViewProvider's initializeEmptyConversation) posts it as the third boundary the legacy flow honours; the same cross-session leak class the head commit closed for qwenSessionSwitched/conversationCleared is now closed here too.
  • R1-9: the hook now tracks the active sessionId (adopted from qwenSessionSwitched; first frame adopts when the boundary carries none) and drops transcript frames whose sessionId no longer matches — late frames from a previous session can no longer contaminate the next session. (The reviewer's "ideally cancel the previous session's turn" half needs extension-side loadSession/cancel changes — noted in the thread as follow-up.)
  • R1-2: cached-history restore paths now render — new cachedMessageToNotification adapter maps user/assistant/thinking cached messages to ACP chunk notifications reduced through the same shared reducer; the hook seeds state from non-empty qwenSessionSwitched.messages.
  • R1-11: the hook now dispatches assistant.done through the shared reducer on streamEnd (with reason mapping incl. cancel) and on sessionLoadComplete — the final block of a turn no longer stays streaming: true. Reuses existing authoritative signals; no new message type or provider changes.

Verification: useAcpTranscript.test.ts + acpTranscriptAdapter.test.ts 10/10, package check-types clean, all four fixes mutation-checked (each revert fails its witness). One non-force push 7fc35c16f2..8dc018b9bf to the fork.

Remaining open (21): R1-4/R1-5/R1-6/R1-7/R1-8/R1-10/R1-12 Criticals and R1-13..R1-22 Suggestions — next cap-4 rounds. Note: R1-2/R1-11 are rendering-visible; validated via the jsdom harness + mutation checks (no live VS Code webview harness on this box — bot headless probe remains the pre-merge spot check).

@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.

1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R2-9 (Onboarding/empty-state centering broken by the block-level replacement container at App.tsx:835) — dropped as an overlap: that line already carries round-1 comments (R1-5 comment 3836781122, R1-10 comment 3836781132); the finding rema…

[Critical] R1-4 (carried from round 1, still stands): Locally generated messages never render. Connection/auth errors, generic error messages, and the local "Interrupted" cancel marker are added via messageHandling.addMessage (making hasContent true), but the only rendered surface is now fed exclusively by ACP transcriptUpdate notifications, so these messages appear nowhere; a failed "New Conversation" is completely invisible.

[Critical] R1-5 (carried from round 1, still stands): The three contributed copy commands (qwen-code.copyMessage/copyAllMessages/copyLastReply) are now dead — the diff deleted the data-vscode-context attribute, the copyCommand handler, and the contextMenuTriggered emitter, while package.json still contributes the commands under webviewSection == 'chat-messages' and sendCopyCommand still posts into the void.

[Critical] R1-6 (carried from round 1, still stands): File-link clicks can no longer open files in the editor — handleFileClick (which posted openFile) and every onFileClick consumer were removed, and WebShellTranscript exposes no file-open prop, so FileMessageHandler.handleOpenFile is unreachable from transcript content.

[Critical] R1-7 (carried from round 1, still stands): The /insight feedback path is orphaned — WebViewProvider still parses insight notifications and posts insightProgress/insightReportReady, returning before the stream-text path, but the diff removed the setters so the handlers are optional-chained no-ops, and the openInsightReport handler is unreachable dead code.

[Critical] R1-8 (carried from round 1, still stands): The user-message edit/rewind feature is removed from the UI with no replacement, leaving dead switches — useMessageSubmit still declares and branches on editTargetTurnIndex/onSubmitted and SessionMessageHandler still implements the full rewind flow, but no code path can send editMessage.

[Critical] R1-10 (carried from round 1, still stands): The composer occludes the transcript tail — the deleted scroll container reserved pb-[140px] for the floating InputForm, but the replacement scroll area's bottom padding is only calc(8px + var(--web-shell-bottom-panel-inset, 0px)) and nothing in this package sets --web-shell-bottom-panel-inset.

[Critical] R1-12 (carried from round 1, still stands): WebShellTranscript hardcodes isResponding={false} (and pendingApproval={null}) into web-shell's MessageList, and no prop lets the companion report that a turn is in flight, so the in-progress turn is treated as completed and auto-collapses mid-response.

中文说明

本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

[Critical] R1-4 (carried from round 1, still stands): Locally generated messages never render. Connection/auth errors, generic error messages, and the local "Interrupted" cancel marker are added via messageHandling.addMessage (making hasContent true), but the only rendered surface is now fed exclusively by ACP transcriptUpdate notifications, so these messages appear nowhere; a failed "New Conversation" is completely invisible.

[Critical] R1-5 (carried from round 1, still stands): The three contributed copy commands (qwen-code.copyMessage/copyAllMessages/copyLastReply) are now dead — the diff deleted the data-vscode-context attribute, the copyCommand handler, and the contextMenuTriggered emitter, while package.json still contributes the commands under webviewSection == 'chat-messages' and sendCopyCommand still posts into the void.

[Critical] R1-6 (carried from round 1, still stands): File-link clicks can no longer open files in the editor — handleFileClick (which posted openFile) and every onFileClick consumer were removed, and WebShellTranscript exposes no file-open prop, so FileMessageHandler.handleOpenFile is unreachable from transcript content.

[Critical] R1-7 (carried from round 1, still stands): The /insight feedback path is orphaned — WebViewProvider still parses insight notifications and posts insightProgress/insightReportReady, returning before the stream-text path, but the diff removed the setters so the handlers are optional-chained no-ops, and the openInsightReport handler is unreachable dead code.

[Critical] R1-8 (carried from round 1, still stands): The user-message edit/rewind feature is removed from the UI with no replacement, leaving dead switches — useMessageSubmit still declares and branches on editTargetTurnIndex/onSubmitted and SessionMessageHandler still implements the full rewind flow, but no code path can send editMessage.

[Critical] R1-10 (carried from round 1, still stands): The composer occludes the transcript tail — the deleted scroll container reserved pb-[140px] for the floating InputForm, but the replacement scroll area's bottom padding is only calc(8px + var(--web-shell-bottom-panel-inset, 0px)) and nothing in this package sets --web-shell-bottom-panel-inset.

[Critical] R1-12 (carried from round 1, still stands): WebShellTranscript hardcodes isResponding={false} (and pendingApproval={null}) into web-shell's MessageList, and no prop lets the companion report that a turn is in flight, so the in-progress turn is treated as completed and auto-collapses mid-response.

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

Comment thread packages/vscode-ide-companion/src/webview/hooks/useAcpTranscript.ts
Comment thread packages/vscode-ide-companion/src/services/qwenAgentManager.ts
Comment thread packages/vscode-ide-companion/src/webview/hooks/useAcpTranscript.ts
Comment thread packages/vscode-ide-companion/src/webview/App.tsx
Comment thread packages/vscode-ide-companion/src/webview/hooks/useAcpTranscript.ts
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Closeout — cap-4 round (4 of 9 Criticals fixed, push 8dc018b9bf..195c53f240, one non-force push to the fork)

  • sessionId guard dropping live frames (newest finding): fixed in c2cffbb7b7 — the load-failure fallback boundary now publishes the fresh ACP session id as liveSessionId; useAcpTranscript adopts it over the archived id and seeds the cached history under the same id, so live frames survive the fallback again while foreign-session frames are still dropped. useAcpTranscript.test.ts 9/9, mutation-checked.
  • user's live prompt never renders (newest finding): fixed in bd09e19d86SessionMessageHandler.handleSendMessage now synthesizes the missing user_message_chunk echo (display text + live ACP session id) into the transcript before agentManager.sendMessage. SessionMessageHandler.test.ts 26/26, mutation-checked (payload + ordering).
  • R1-12 hardcoded isResponding={false} collapsing in-progress turns: fixed in 195c53f240 via the interim option suggested in the thread — the companion passes the already-existing collapseCompletedTurns={false} prop (no web-shell API change), restoring the pre-PR always-expanded timeline. Plumbing a live isResponding needs a web-shell public-API addition, out of scope here; noted in the thread reply.
  • R1-10 composer occluding the transcript tail: fixed in the same commit — the wrapper sets --web-shell-bottom-panel-inset: 140px through WebShellTranscript's existing style prop, restoring exactly the pb-[140px] clearance the deleted scroll container reserved.

Verification: 64/64 across the 5 targeted suites (App, SessionMessageHandler, useAcpTranscript, neighboring useWebViewMessages), check-types clean, eslint clean on all 6 changed files. All four fixes are webview-visible; evidence is deterministic component/hook tests (live VS Code webview screenshots are infeasible in this headless environment) — stated in each thread reply.

Disclosures: R1-12 + R1-10 share one commit (same JSX element); the producer-side payload assertion for the first fix landed with the second commit's test file due to the ≤3-file-per-finding ceiling (consumer half fully covered in its own commit).

Process note: the first reply pass posted the R1-12/R1-10 evidence into the R1-6/R1-5 threads (same file+line collision); the strays were deleted and the evidence re-posted into the correct threads before this comment — the resolved states now carry the right replies.

Remaining 5 Criticals (R1-4 local messages, R1-5 copy commands, R1-6 file links, R1-7 /insight, R1-8 edit/rewind) stay unresolved for the next round.

@chiga0 chiga0 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.

No new blockers found at this head.

Scope: vscode-ide-companion source only (NOTICES.txt excluded). Read SessionMessageHandler.ts, useAcpTranscript.ts, acpTranscriptAdapter.ts, App.tsx (key sections), WebViewContent.ts, WebViewProvider.ts, copyTranscript.ts, fileLinks.ts, chatTypes.ts, qwenAgentManager.ts (transcript dispatch path). Static cross-file trace; no execution (no local toolchain).

CI at time of review: Desktop Shell (ubuntu + windows) ✅ · Test (ubuntu-latest, Node 22.x) ⏳ in-progress · Test (macos/windows) and Integration Tests ⏸ SKIPPED. The Test job was still running; macOS and Windows test jobs are skipped by branch/path filter. I am reviewing at in-progress test state.


What I checked and found clean

Session boundary guards (useAcpTranscript.ts)

  • All three boundary message types (qwenSessionSwitched, conversationCleared, conversationLoaded) trigger resetTranscript().
  • activeSessionIdRef is pinned on the boundary; late frames from the abandoned session are dropped.
  • liveSessionId wins over sessionId for load-failure fallback paths (so the fresh ACP session's live frames are not dropped).

Turn finalization

  • finishTurn called on streamEnd (with requestId correlation via activeRequestIdRef) and on sessionLoadComplete.
  • Foreign streamEnd events (mismatched requestId) are dropped; background source='background_notification' events are also correctly gated.

User message echo

  • transcriptEchoSessionId guard sends the user's own turn as transcriptUpdate so it renders in the WebShell timeline (the companion's stdio ACP channel does not receive a user_message_chunk for interactive prompts).
  • Image attachments are echoed via per-image transcriptUpdate frames.

Local-only message rendering

  • All type:'message' sends in SessionMessageHandler.ts carry localOnly: true.
  • App.tsx filters them into localNotices rendered above the composer; they never go to WebShellTranscript.

File-link clicks

  • handleTranscriptClick at the container level intercepts anchor clicks and calls resolveFileLinkFromAnchor.
  • normalizeExplicitFileLink handles file:// URIs specially (percent-decode then strip scheme; no fragment split on # since that would be a literal path character). Non-file:// paths split on # correctly.

Chunk-load recovery

  • TranscriptErrorBoundary catches the rejected import() that Suspense re-throws (stale content-hashed chunk after an extension auto-update) and offers a reload button. This is the right failure mode.

CSP widening

  • wasm-unsafe-eval in WebViewContent.ts is required for Shiki/Oniguruma WASM and is documented in the PR description. Acceptable tradeoff.

Copy-all and copy-last-reply

  • formatBlocksForCopyAll and findLastAssistantText in copyTranscript.ts handle the block kinds the WebShell reducer produces.

MIT_FALLBACK_TEXT

  • Previously flagged typo ("THE USE OF OTHER DEALINGS") confirmed fixed — reads "THE USE OR OTHER DEALINGS IN THE SOFTWARE" at this head.

Outstanding items (not new findings — pre-existing open threads)

R1 round outstanding: jifeng has a CHANGES_REQUESTED review still open (inline comment on useAcpTranscript.ts:173 — body referenced a local temp file I could not read). Worth confirming that concern was addressed before merging.

Cross-check — cannot confirm or refute: A prior reviewer (round 2) claimed web-shell's MessageList keys tool-group rows as msg:tg-<blockId>, which would make findBlockByRowKey return null for every "Copy Message" on a tool row. findBlockByRowKey currently matches on msg:<blockId> and msg:<blockId>-<suffix> but not msg:tg-<blockId>. I cannot verify web-shell's internal key format here; whoever has access to @qwen-code/web-shell source should confirm the key shape used for tool blocks.


Reviewed with AI assistance.

…ecoding

normalizeExplicitFileLink decoded the whole value before splitting on #, so an encoded %23 in a filename was treated as a fragment delimiter and truncated the path. Split on the raw # first and decode the path and fragment parts separately; the file:// branch decodes only the path component. resolveFileLinkFromAnchor also no longer runs URL decoding/fragment logic over the anchor-text fallback: the text is a literal path, which keeps the /export 'export (#1).html' links (whose file: href the sanitizer strips) clickable.
…script messages

Cached-history seeding emitted each row as a bare *_message_chunk with no promptId/sourceRecordIds/_meta, so the shared reducer merged runs of consecutive same-role cached rows (Tool Result / telemetry / Plan rows per turn) into one plain-concatenated block, and a dropped whitespace-only user row let different turns fuse. Stamp every synthesized cached row with the reducer's existing anti-merge marker (_meta.qwenDiscreteMessage) so offline restores render the same discrete blocks as live replays.
… content parts

Copy Message silently failed on every tool row: web-shell keys tool_group rows as msg:tg-<block id>, which findBlockByRowKey never matched. Strip the tg- prefix before the existing exact/longest-prefix matching; merged groups share the first block's key, so a group row resolves to the group's first tool block (documented). The tool case of getBlockCopyText also serialized only title + details (the input summary), dropping the output text and diffs the timeline renders; walk block.content and append text parts and ---/+++ diff renderings like the pre-PR formatToolCallForCopy did, restoring Copy Message / Copy All parity for tool rows.
@yiliang114 yiliang114 removed the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Aug 24, 2026
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Cap-4 round on the Aug-23 18:25Z Criticals, all verified real at head dedb0b87b8 before fixing.

  • bhOSf (Copy Message fails on every tool-group row) + bhOTE (tool copy ignores block.content) — fixed in 1926329: findBlockByRowKey strips the web-shell tg- group prefix before the exact/longest-prefix match (merged groups resolve to the group's first tool block — per-tool disambiguation is impossible, merged rows share one key); getBlockCopyText's tool case now walks block.content, rendering output text and ---/+++ diffs like the timeline does (Copy All benefits via the same helper).
  • bhOSi (cached-history rows merged by the SDK reducer) — fixed in c830bc6: every synthesized cached row is stamped _meta: { qwenDiscreteMessage: true }, the reducer's existing anti-merge marker; live streaming merge behavior untouched.
  • bhOSl (file-link # semantics) — fixed in d1ad62b: split on the raw # first, percent-decode path and fragment separately (file:// branch decodes the path part only); anchor-text fallback is treated as a literal path, no URL-fragment logic.

Verification at pushed head: copyTranscript 19, App 24, fileLinks 16, acpTranscriptAdapter 8 (67/67); npm run check-types + eslint clean on all 7 touched files; every fix mutation-checked (red on revert). One non-force push dedb0b87b8..19263295f2; 4/4 threads replied with SHA evidence and resolved.

Untouched this round: R1-20 (escalated KaTeX-fonts design call) + the remaining Suggestions (queued for a future cap round).

@yiliang114

Copy link
Copy Markdown
Collaborator Author

Closeout update for 82912c509521:

Fixed in the final two additive commits:

  • made embedded WebShellTranscript math rendering self-contained (scoped KaTeX CSS + inlined WOFF2 fonts) and allowed only data: fonts in the VS Code CSP;
  • bounded transcript image echo reads with MAX_IMAGE_SIZE;
  • restored error / debug rows in Copy All Messages;
  • removed internal clipboard/filesystem image references from both live user echoes and cached-history seeds while preserving live inline image rendering.

Local verification:

  • VS Code companion focused regression matrix: 53/53 passed;
  • image/restore matrix after the final commit: 42/42 passed;
  • WebShell artifact + transcript DOM/contract matrix: 26/26 passed;
  • packages/web-shell build passed;
  • packages/vscode-ide-companion typecheck, lint, and bundle build passed;
  • built artifacts contain the scoped .katex-mathml rules and inlined WOFF2 data, with no runtime font-file dependency.

There are no unresolved Critical threads at this head. The remaining Suggestions are intentionally deferred after the review-round cap: notices-generator wording/test probes; local-notice lifecycle/copy behavior and dead rewind/waiting-contract cleanup (better handled by the full VS Code WebShell cutover / WebUI retirement follow-ups); and test-only mutation cases with no demonstrated production defect.

Final GitHub CI, security, visual capture, and automatic review are running on this head.

@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 82912c5. Only screenshots that changed are shown (flows below, if any, are head-only) — refreshes on every push.

Screenshots · before / after

⚠️ No preview: one or more scenarios failed to render on this head — see the workflow run. This is not "no visual change" — a scenario that times out or throws produces no image. Fix the failing scenario (or a genuine regression it caught) and the preview returns on the next push.

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

Qwen Code · web-shell visuals

@yiliang114

Copy link
Copy Markdown
Collaborator Author

Visual workflow attribution for run 32689856799: the missing preview is caused by the same pre-existing harness failure on both PR head and merge-base. In dark and light workspace sidebar, getByText('Run auth migration') is strict against two matching session rows; head: 35 passed / 2 failed, base: 30 passed / 2 failed, with the same locator and line. This is not a regression from #9719, so I am not widening this already mature PR to change the repository-wide visual fixture. The previously posted real VS Code before/after host evidence remains the PR-specific visual record.

@wenshao

wenshao commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Local verification report for PR #9719

I checked out feat/9187-vscode-webshell-transcript at 82912c5095 and built the full workspace plus the VS Code companion extension on macOS arm64 (Node v22.22.2). Below are the results.

What passed

Step Command Result
Workspace install + build npm install (triggers prepare → full npm run build) ✅ passed
VS Code companion production build cd packages/vscode-ide-companion && npm run build:prod ✅ passed
VS Code companion packaging npm run package (produces .vsix) ✅ passed
VS Code companion typecheck npm run check-types ✅ passed (see note below)
VS Code companion lint npm run lint ✅ passed
VS Code companion unit tests npm test 606 passed, 1 skipped
Web Shell unit tests cd packages/web-shell && npm test 4191 passed
Webview bundle syntax check node --check dist/webview.js ✅ passed
Headless browser smoke test Playwright loads the built Web Shell app without page errors ✅ passed

Build artifacts

  • packages/vscode-ide-companion/dist/webview.js500 KB (initial webview bundle, unchanged from pre-PR baseline)
  • packages/vscode-ide-companion/dist/chunks/61 MB of lazy-loaded chunks (WebShell renderer, Shiki, Mermaid, KaTeX, echarts, etc.)
  • packages/vscode-ide-companion/qwen-code-vscode-ide-companion-0.22.0.vsix61.32 MB
  • dist/webview.js contains React.lazy and WebShell references, confirming the renderer is still code-split and lazy-loaded.
  • The webview CSP now includes 'wasm-unsafe-eval' as expected for Shiki's Oniguruma WASM.

Note on check-types

npm run check-types fails only after npm run package copies the bundled CLI into dist/qwen-cli/. The copied CLI artifacts contain relative imports that do not resolve from inside the extension package. Removing dist/qwen-cli/ makes check-types pass again. This is a pre-existing packaging artifact issue, not caused by the PR's source changes.

Screenshots

Web Shell app loads cleanly from the production build (headless Chromium, no daemon connected):

Web Shell loading screen

PR author-provided before/after of the VS Code companion timeline:

Before (legacy MessageList) After (WebShell transcript)
legacy timeline WebShell transcript timeline

Limitations

VS Code desktop is not installed in this verification environment, so I could not activate the packaged .vsix in a real Extension Development Host. The runtime evidence above is limited to:

  • successful build/packaging,
  • passing unit tests,
  • clean bundle parse, and
  • the shared Web Shell renderer loading without errors in a headless browser.

A maintainer who has VS Code locally should still do a final smoke test by installing the .vsix and starting a conversation to confirm the timeline renders and session switching does not bleed content.

Conclusion

From a build, type, lint, and unit-test perspective the PR is green. The bundle shape matches the PR description (small initial webview bundle + large lazy WebShell chunk), and the latest review at dedb0b87b8df found no new blockers. I would be comfortable merging once a VS Code runtime smoke test confirms the visual behavior shown in the screenshots above.


📝 中文版验证报告(点击展开)

PR #9719 本地验证报告

我在 macOS arm64(Node v22.22.2)上检出 feat/9187-vscode-webshell-transcript 分支的 82912c5095 提交,完整构建了工作区以及 VS Code companion 扩展。结果如下。

通过的验证项

步骤 命令 结果
工作区安装与构建 npm install(会触发 prepare → 完整 npm run build ✅ 通过
VS Code companion 生产构建 cd packages/vscode-ide-companion && npm run build:prod ✅ 通过
VS Code companion 打包 npm run package(生成 .vsix ✅ 通过
VS Code companion 类型检查 npm run check-types ✅ 通过(见下方说明)
VS Code companion 代码检查 npm run lint ✅ 通过
VS Code companion 单元测试 npm test 606 通过,1 跳过
Web Shell 单元测试 cd packages/web-shell && npm test 4191 通过
Webview 包语法检查 node --check dist/webview.js ✅ 通过
无头浏览器冒烟测试 Playwright 加载构建后的 Web Shell 应用,无页面错误 ✅ 通过

构建产物

  • packages/vscode-ide-companion/dist/webview.js500 KB(初始 webview bundle,与 PR 前基线一致)
  • packages/vscode-ide-companion/dist/chunks/61 MB 懒加载 chunk(WebShell 渲染器、Shiki、Mermaid、KaTeX、echarts 等)
  • packages/vscode-ide-companion/qwen-code-vscode-ide-companion-0.22.0.vsix61.32 MB
  • dist/webview.js 包含 React.lazyWebShell 引用,确认渲染器仍为代码分割懒加载。
  • webview CSP 已按预期包含 'wasm-unsafe-eval',以支持 Shiki 的 Oniguruma WASM。

关于 check-types 的说明

npm run check-types 仅在 npm run package 将 bundled CLI 复制到 dist/qwen-cli/ 后失败。复制的 CLI 产物包含在扩展包内部无法解析的相对导入。删除 dist/qwen-cli/check-types 恢复通过。这是预先存在的打包产物问题,不是 PR 源码改动导致的。

截图

Web Shell 应用从生产构建正常加载(无头 Chromium,未连接 daemon):

Web Shell 加载界面

PR 作者提供的 VS Code companion 时间线 Before & After:

Before(旧 MessageList) After(WebShell transcript)
旧时间线 WebShell transcript 时间线

局限性

本次验证环境未安装 VS Code 桌面版,因此无法在真实 Extension Development Host 中激活打包后的 .vsix。运行时证据限于:

  • 成功构建/打包,
  • 单元测试通过,
  • bundle 语法检查通过,
  • 共享 Web Shell 渲染器在无头浏览器中无错误加载。

建议本地有 VS Code 的维护者再做一个最终冒烟测试:安装 .vsix 并开始一段对话,确认时间线正常渲染且切换会话不会串内容。

结论

从构建、类型、代码规范和单元测试角度看,该 PR 为绿色。bundle 形态与 PR 描述一致(较小的初始 webview bundle + 较大的懒加载 WebShell chunk),且最新一次对 dedb0b87b8df 的 review 未发现新的阻塞项。建议在完成 VS Code 运行时冒烟测试、确认上图视觉行为后即可合并。

@wenshao

wenshao commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Deep verification report — local maintainer round (2026-08-24)

Verdict: findings (non-blocking) — 34/34 scripted assertions passed, 0 unexpected failures. The central claim is proven load-bearing end to end. Verified head 82912c5095214f85473f58637297900bc7403430 against base b2edb80a57bf640ccb933d7e90d5b066f854f014.

Central claim — A/B

The companion timeline renders through the WebShell transcript fed by raw ACP session/update notifications bridged through the shared SDK daemon reducer. A mock-free harness (real useAcpTranscript hook + real @qwen-code/sdk/daemon reducer, real postMessage frames in jsdom, no vi.mock) drove the full conversation shape and returned 13/13: user turn, thought, tool-call lifecycle, consecutive-chunk merge, turn finalization, session-boundary isolation (the "alphabeta" cross-session leak), cached-history seeding, late-frame drop, and timeout force-finalization.

cell oracle result
head 82912c5 (real hook + shared reducer) block model for a scripted conversation 13/13
head + pipe disabled (scratch mutation) timeline content must vanish 8/13 flip red — the transcriptUpdate pipe is the sole content carrier
base b2edb80 (production build + suite) legacy baseline 710,013 B single IIFE bundle, legacy renderer markers present, no wasm-unsafe-eval; suite 501/0
head production build lazy renderer 511,684 B ESM entry (−28%, zero katex/web-shell-root markers — renderer fully lazy), <script type="module">, CSP adds wasm-unsafe-eval + font-src data:

Mutation matrix on the author's own test file: boundary-reset removal kills 6/20, strict seeding-guard forcing kills 1/20 (the author's ceuN pin holds), timeout-mapping removal kills exactly the 2 timeout tests (positive control). The graceful guard-true variant survives and is adjudicated behavior-preserving (empty body never runs), not a coverage gap.

Findings (both non-blocking)

  1. [Description accuracy — merge reference] The lazy payload is measured 49.4 MB across 852 chunks; the two WebShell core chunks alone are ~14.2 MB and fetch at timeline mount. The PR description says "+ ~4MB WebShell chunk" (and the App.tsx:69 comment says "~17MB") — both disagree with the measurement by ≥3×. Not a code defect (lazy loading works, entry is clean), but the numbers a reviewer merges against should be corrected.
  2. [Suggestion — cleanup follow-up] Dead-code removal is incomplete vs the stated scope: qwenAgentManager.rewindSession() (qwenAgentManager.ts:406) → acpConnection.rewindSession() (acpConnection.ts:493) now have zero production callers, and WebViewProvider.ts:1936 still handles the editMessage message type the new webview can never send. Harmless at runtime; fine as a follow-up commit.

No Critical or blocking finding. Session-boundary isolation — the bug class this PR's hardest commits closed — was re-proven live in this round.

Gates

Head companion suite 606 passed / 0 failed (base 501/0 — no pre-existing failures; delta +4 files / +105 tests, all green) · tsc --noEmit exit 0 · eslint exit 0 on both changed workspaces (gate liveness proven with a planted violation) · web-shell build-artifact.test.ts 13/13 (incl. the new self-contained KaTeX CSS/fonts test) · NOTICES.txt regenerates byte-identical to the committed +10,879-line file.

Evidence

head harness 13/13

load-bearing mutation flip

mutation matrix

bundle A/B

Not covered

Real VS Code window rendering (the WebShell component itself is prop-asserted, not pixel-rendered, here), live ACP agent traffic (frames scripted per the daemon reducer's notification shape), Windows/Linux, .vsix packaging, and cold-mount latency of the 49.4 MB chunk set. Boundary note: the PR moves the shaping boundary (base consumed pre-shaped stream events; head consumes raw notifications), so each A/B cell is driven at its own boundary against the same scripted conversation — content parity, not a shared input wire.

中文摘要

结论:findings(非阻塞) — 34/34 脚本断言全部通过,0 意外失败;核心改动被端到端证明是载荷性的。验证 head 82912c5,base b2edb80

A/B 结论:mock-free harness 驱动真实 useAcpTranscript + 共享 SDK daemon reducer,13/13 通过(用户轮次、思考块、工具块生命周期、连续 chunk 合并、收尾、会话边界隔离、缓存回填、迟到帧丢弃、timeout 强制终结)。禁用 transcriptUpdate 管线的对照突变使 8/13 断言翻红 — 新管线是时间线内容的唯一载体。作者测试套件的突变矩阵:移除边界重置杀死 6/20,严格版 seeding guard 强制杀死 1/20(作者的 ceuN 钉住有效),timeout 映射移除恰好杀死 2 个对应测试(阳性对照)。

发现(均非阻塞):① 懒加载负载实测 49.4MB / 852 个 chunk(WebShell 核心 chunk 约 14.2MB,时间线挂载时拉取),PR 描述写 "+ ~4MB"、App.tsx 注释写 "~17MB",均与实测不符(低估 3 倍以上)—— 建议合并前修正描述;② 死代码清理不彻底:rewindSession 调用链零调用方,WebViewProvider 仍保留 editMessage 输入分支 — 可作为后续清理提交。

门禁:head 套件 606/0(base 501/0,无预存失败)、tsc 0、eslint 0(活性已证明)、web-shell build-artifact 13/13、NOTICES.txt 再生成字节级一致。

未覆盖:真实 VS Code 窗口渲染、真实 ACP 流量、Windows/Linux、.vsix 打包、冷挂载延迟。测试为 props 级证明,非像素级渲染证明。

@wenshao

wenshao commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (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: 4872 passed · 0 failed · 4872 total

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

中文 — 判定:✅ 通过 · 可合入(agent 判定)

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

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

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

Verification report

PR 9719 — Deep Verification

Verdict: merge-ready — 4872/4872 scripted assertions passed (0 unexpected failures), verified head 82912c5095214f85473f58637297900bc7403430. The central claim (companion timeline rendered by the shared WebShell transcript via the SDK daemon reducer, with clean session boundaries and a lazy code-split chunk) is proven load-bearing by A/B; all 8 boundary/wiring guards are pinned by their named tests; the committed NOTICES.txt is byte-identical to a fresh regeneration. Non-blocking nits only (description numbers, dead-code residue) — see Findings.

中文摘要
  • 结论: merge-ready。4872 条脚本化断言全部通过,0 条意外失败;验证的 head 为 82912c5095
  • A/B 结论:
    • 行为核心(跨会话泄漏):head 干净;移除边界 reset 的对照单元精确复现 "alpha 泄漏"(expected length +0 but got 1),见 01-leak-ab-head-vs-control.png
    • 打包:base 为单文件 iife 693.4 KB;head 入口 499.7 KB(更小)+ 懒加载 WebShell chunk 5.7 MB(挂载闭包 5 个 chunk 共 6.5 MB),CSP 增加 wasm-unsafe-evalfont-src data:,脚本标签改为 type="module",见 02-bundle-ab-base-vs-head.png
  • 桥接正确性: 真实 adapter + 真实 SDK reducer 的 harness 37/37(块模型、chunk 合并、图片折叠、缓存行离散性、空白行拒绝、图片引用隐藏、解析器阶梯),见 03-bridge-harness-blocks.png
  • 变异矩阵: 8/8 守卫均被其命名测试杀死(边界 reset、sessionId 守卫、liveSessionId 优先级、requestId 关联、离散标记、空白拒绝、转发、blocks 接线),见 04-mutation-matrix-kills.png
  • 门控: companion 601 过(+5 个 ide-server 环境性失败,base 上逐字节相同,与本 PR 无关);web-shell 4210/4210;两个包 typecheck 干净;NOTICES.txt 与重新生成逐字节一致。见 05-targeted-gates.png
  • Findings(均不阻塞): 描述中的打包数字与实测不符(见 Corrections);editMessage/conversationRewound 移除后仍有两处死代码残留;setWaitingForResponse 调用方仍传被丢弃的字符串。
  • 未覆盖: 每提交归因(shallow checkout,仅 3 个提交可达);真实 VS Code 内渲染(容器无 VS Code,作者已在 macOS 手测);Windows/Linux。

Scope

  • Central claim: the companion timeline is now the shared WebShell transcript — ACP session/update notifications are bridged through @qwen-code/sdk/daemon's transcript reducer (useAcpTranscript + acpTranscriptAdapter) and rendered by the lazily code-split WebShellTranscript, with session-boundary resets and a session-id guard preventing cross-session bleed.
  • Secondary 1: the WebShell renderer is lazy-loaded (entry bundle not inflated); CSP grants wasm-unsafe-eval + font-src data:; webview script is ESM.
  • Secondary 2: cached-history seeding renders discrete rows without merging, hides internal image references, and rejects blank rows.

Per-commit attribution was out of reach (shallow checkout: 37 commits in the snapshot, 1 reachable locally); the aggregate HEAD^1..HEAD diff is what was verified.

A/B: session-leak behavior (central claim)

Oracle: the PR's own regression scenario — session A user text "alpha", qwenSessionSwitched, session B "beta"; blocks must be exactly beta after the boundary. Control = single-hunk revert of the boundary reset in useAcpTranscript.ts. Witness: 01-leak-ab-head-vs-control.png.

cell change resets … qwenSessionSwitched … test leak
head (82912c5095) none passes (1 passed) none
control resetTranscript() removed fails: expected [ Array(1) ] to have a length of +0 but got 1 reproduced

The base arm has no hook at all (feature absent), so the meaningful control for this claim is the hunk revert at head; the base arm is exercised in the bundle A/B below.

A/B: bundle / CSP (secondary 1)

Both arms built with node esbuild.js --production (head in the main tree, base in a tmp/base-tree worktree at HEAD^1 with the main tree's nested node_modules symlinked in — the PR leaves package-lock.json untouched, so dependency state is identical; the only package.json change is the workspace dep declaration). Witness: 02-bundle-ab-base-vs-head.png.

metric base head Δ
entry webview.js 693.4 KB (gzip 235.2 KB), single iife 499.7 KB (gzip 174.2 KB), ESM −27.9%
entry static chunks none 2 (250.1 KB + 0.8 KB) initial load 693.4 → 750.6 KB (+8.2%)
lazy WebShell chunk chunks/dist-LZ3UGE2R.js 5741.3 KB (gzip 1844.1 KB) new
mount-time fetch set 5 chunks, 6545.4 KB (static closure of the lazy import) new, on mount
runtime-reachable lazy (shiki grammars/themes, echarts, mermaid) 317 chunks, 14.9 MB new, on demand
all emitted chunks 0 426, 19.7 MB new
CSP script-src ${cspSource}; script-src ${cspSource} 'wasm-unsafe-eval'; font-src data: grants added
script tag classic type="module" ESM

Load-bearing property verified: the entry did not grow; the heavy payload is behind a dynamic import() (asserted in the harness). The description's Before/After numbers do not match the measured reality in either direction — see Corrections.

Bridge harness (mock-free, real reducer)

harness/bridge.ts bundles the PR's real acpTranscriptAdapter.ts + imageSupport.ts against the compiled @qwen-code/sdk/daemon dist and drives real ACP frame shapes lifted from the SDK's own fixtures. 37/37 (witness 03-bridge-harness-blocks.png):

  • live turn: user echo text + inline image part fold into one user block (image accepted in the daemon-echo {type:'image', data, mimeType} shape, mime sniffed image/png); thought/assistant deltas merge; a following tool_call finalizes the text block; plan renders as an updated_plan tool block carrying both entries; assistant.done clears streaming on a pure-text turn (the semantic the hook's streamEnd/sessionLoadComplete finalization relies on).
  • cached seeding: 5 rows → 5 discrete blocks; control arm: the same notifications with _meta.qwenDiscreteMessage stripped merge (fewer blocks) — the marker is the load-bearing part of the seeding.
  • image-reference hiding: @/tmp/x.png stripped from user text; escaped \@ kept; non-image extensions kept; path stops at punctuation.
  • edge cases: empty / whitespace-only / unknown-role / image-ref-only rows rejected.
  • parser ladder: 2k/3k/5k/20k hostile chars all complete in <2s (linear scanner, no ReDoS shape).

Mutation matrix (vacuity of the new tests)

Baseline green (20/20 hook tests) before mutation. Each mutant = one point-revert, applied in place, target test file(s) run, restored; witness 04-mutation-matrix-kills.png. 8/8 killed, each by exactly the test named for the guard:

mutant killed by
boundary reset removed the 3 reset tests + 3 drop/seed tests (leak reproduced)
session-id guard removed the 4 late/abandoned-frame drop tests
liveSessionId precedence swapped only the load-failure-fallback test
streamEnd request correlation removed the 2 stale-streamEnd tests
qwenDiscreteMessage marker removed the 4 discrete-stamping tests
post-split empty-text reject removed strips persisted image references from restored user text
onTranscriptUpdate forwarding removed both verbatim-forwarding tests
blocks={transcriptBlocks} unwired in App feeds reduced transcript blocks … into the WebShell transcript

Single-test kills (M3, M6) are the positive controls proving harness resolution; no survivors, so no combination rows were needed.

Targeted gates

Witness 05-targeted-gates.png; raw logs in logs/.

  • companion vitest run: 601 passed, 1 skipped, 5 failed — all 5 in src/ide-server.test.ts, a HOME-path mismatch (/home/test mocked vs container $HOME=/__w/_temp/verify-agent-home). A/A control at base fails the same 5 tests byte-identically (logs/gate-ide-server-base-AA.log); the PR touches no ide-server code. Pre-existing environmental — excluded from the tally with the A/A proof, not counted as passes.
  • web-shell vitest run: 4210/4210 (includes the new KaTeX self-containment build-artifact test).
  • tsc --noEmit: companion exit 0 (zero diagnostics), web-shell exit 0.
  • NOTICES.txt: regenerated via node ./scripts/generate-notices.js (652 deps) — byte-identical to the committed file.

Corrections (description vs measured)

The PR body's Evidence table and one code comment describe the bundle change with numbers that do not reproduce at this merge commit; the mechanism itself is verified, only the figures are off:

  1. Body: "entry 504KB + 2 static shared chunks ≈ 755KB" before. Measured base at HEAD^1: a single iife file, 693.4 KB, no chunks at all.
  2. Body: "same entry bundle" after. Measured: entry shrinks 27.9% (693.4 → 499.7 KB) and gains 2 static chunks (250.9 KB), i.e. initial load 750.6 KB (+8.2%), not "same".
  3. Body: "+ ~4MB WebShell chunk". Measured: the chunk carrying the renderer is 5.7 MB (gzip 1.8 MB); the mount-time static closure is 6.5 MB over 5 chunks.
  4. App.tsx comment: "the ~17MB web-shell chunk". No such chunk exists; the whole lazy payload is 19.7 MB over 426 chunks, of which 14.9 MB is runtime-on-demand (shiki grammars/themes, echarts, mermaid). The comment overstates the single-chunk cost ~3×.

Findings (non-blocking)

  1. Dead-code residue from the edit/rewind removal (the PR's stated intent was to remove dead code, not keep it):
    • src/webview/providers/WebViewProvider.ts:1936 still branches on message.type === 'editMessage' (timer-reset condition); nothing sends editMessage anymore.
    • src/webview/hooks/useWebViewMessages.ts:686-724 still handles conversationRewound; the extension host no longer emits it.
      Both are inert (no behavioral effect) — cleanup candidates for a follow-up.
  2. Discarded-string callers: useMessageSubmit.ts:158,165 and useWebViewMessages.ts:1099 still pass strings into setWaitingForResponse, whose implementation now takes no argument (legal via TS parameter bivariance; the interfaces at useMessageSubmit.ts:35 / useWebViewMessages.ts:90 still declare (message: string) => void). The strings are computed and dropped. Cosmetic; the pin test documents the intent.

Not covered

  • Per-commit attribution (shallow checkout; aggregate diff verified instead).
  • Real VS Code rendering of the ESM webview (no VS Code in this container; author tested on macOS; unit gates + CSP/module assertions cover the wiring). Windows/Linux untested here.
  • The 5 ide-server.test.ts failures are excluded from the tally as pre-existing environmental (A/A-proven); they were not counted as passes.
  • vsce package packaging and the extension marketplace manifest were not exercised.
  • The web-shell KaTeX/wasm-unsafe-eval runtime behavior in a real webview is covered only by the build-artifact test + CSP assertions, not by a browser run.

Methodology

Environment: node:22-bookworm CI container, merge-ref checkout (HEAD merge, HEAD^1 base aac9606f78, HEAD^2 = verified head; snapshot baseRefOid b2edb80a predates the checkout — the merge ref's base is what the A/B used). Harnesses in harness/ drive compiled real code (esbuild-bundled PR source + SDK dist; production esbuild builds per arm); base arm reuses the root dependency tree via symlinks (lockfile unchanged — stated as the control's basis). Raw logs in logs/, captures in evidence/, per-run JSON in logs/mutation-results.json. Assertion counts: bridge 37 + bundle 10 + leak 2 + matrix 9 + companion 601 + web-shell 4210 + typechecks 2 + notices 1 = 4872 pass, 0 fail.

Flakiness gate log

rounds=5 files=16 skipped=0
file packages/vscode-ide-companion/scripts/generate-notices.test.js: (cd packages/vscode-ide-companion) npx --no-install vitest run ./scripts/generate-notices.test.js
file packages/vscode-ide-companion/src/services/qwenAgentManager.test.ts: (cd packages/vscode-ide-companion) npx --no-install vitest run ./src/services/qwenAgentManager.test.ts
file packages/vscode-ide-companion/src/utils/imageSupport.test.ts: (cd packages/vscode-ide-companion) npx --no-install vitest run ./src/utils/imageSupport.test.ts
file packages/vscode-ide-companion/src/webview/App.test.tsx: (cd packages/vscode-ide-companion) npx --no-install vitest run ./src/webview/App.test.tsx
file packages/vscode-ide-companion/src/webview/adapters/acpTranscriptAdapter.test.ts: (cd packages/vscode-ide-companion) npx --no-install vitest run ./src/webview/adapters/acpTranscriptAdapter.test.ts
file packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.test.ts: (cd packages/vscode-ide-companion) npx --no-install vitest run ./src/webview/handlers/SessionMessageHandler.test.ts
file packages/vscode-ide-companion/src/webview/hooks/message/useMessageHandling.test.tsx: (cd packages/vscode-ide-companion) npx --no-install vitest run ./src/webview/hooks/message/useMessageHandling.test.tsx
file packages/vscode-ide-companion/src/webview/hooks/useAcpTranscript.test.ts: (cd packages/vscode-ide-companion) npx --no-install vitest run ./src/webview/hooks/useAcpTranscript.test.ts
file packages/vscode-ide-companion/src/webview/hooks/useImage.test.ts: (cd packages/vscode-ide-companion) npx --no-install vitest run ./src/webview/hooks/useImage.test.ts
file packages/vscode-ide-companion/src/webview/hooks/useMessageSubmit.test.ts: (cd packages/vscode-ide-companion) npx --no-install vitest run ./src/webview/hooks/useMessageSubmit.test.ts
file packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.test.tsx: (cd packages/vscode-ide-companion) npx --no-install vitest run ./src/webview/hooks/useWebViewMessages.test.tsx
file packages/vscode-ide-companion/src/webview/providers/WebViewContent.test.ts: (cd packages/vscode-ide-companion) npx --no-install vitest run ./src/webview/providers/WebViewContent.test.ts
file packages/vscode-ide-companion/src/webview/providers/WebViewProvider.test.ts: (cd packages/vscode-ide-companion) npx --no-install vitest run ./src/webview/providers/WebViewProvider.test.ts
file packages/vscode-ide-companion/src/webview/utils/copyTranscript.test.ts: (cd packages/vscode-ide-companion) npx --no-install vitest run ./src/webview/utils/copyTranscript.test.ts
file packages/vscode-ide-companion/src/webview/utils/fileLinks.test.ts: (cd packages/vscode-ide-companion) npx --no-install vitest run ./src/webview/utils/fileLinks.test.ts
file packages/web-shell/client/build-artifact.test.ts: (cd packages/web-shell) npx --no-install vitest run ./client/build-artifact.test.ts


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/vscode-ide-companion/scripts/generate-notices.test.js: PPPPP
  packages/vscode-ide-companion/src/services/qwenAgentManager.test.ts: PPPPP
  packages/vscode-ide-companion/src/utils/imageSupport.test.ts: PPPPP
  packages/vscode-ide-companion/src/webview/App.test.tsx: PPPPP
  packages/vscode-ide-companion/src/webview/adapters/acpTranscriptAdapter.test.ts: PPPPP
  packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.test.ts: PPPPP
  packages/vscode-ide-companion/src/webview/hooks/message/useMessageHandling.test.tsx: PPPPP
  packages/vscode-ide-companion/src/webview/hooks/useAcpTranscript.test.ts: PPPPP
  packages/vscode-ide-companion/src/webview/hooks/useImage.test.ts: PPPPP
  packages/vscode-ide-companion/src/webview/hooks/useMessageSubmit.test.ts: PPPPP
  packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.test.tsx: PPPPP
  packages/vscode-ide-companion/src/webview/providers/WebViewContent.test.ts: PPPPP
  packages/vscode-ide-companion/src/webview/providers/WebViewProvider.test.ts: PPPPP
  packages/vscode-ide-companion/src/webview/utils/copyTranscript.test.ts: PPPPP
  packages/vscode-ide-companion/src/webview/utils/fileLinks.test.ts: PPPPP
  packages/web-shell/client/build-artifact.test.ts: PPPPP

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

--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/vscode-ide-companion/scripts/generate-notices.test.js: P (exit 0)
round 1 · packages/vscode-ide-companion/src/services/qwenAgentManager.test.ts: P (exit 0)
round 1 · packages/vscode-ide-companion/src/utils/imageSupport.test.ts: P (exit 0)
round 1 · packages/vscode-ide-companion/src/webview/App.test.tsx: P (exit 0)
round 1 · packages/vscode-ide-companion/src/webview/adapters/acpTranscriptAdapter.test.ts: P (exit 0)
round 1 · packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.test.ts: P (exit 0)
round 1 · packages/vscode-ide-companion/src/webview/hooks/message/useMessageHandling.test.tsx: P (exit 0)
round 1 · packages/vscode-ide-companion/src/webview/hooks/useAcpTranscript.test.ts: P (exit 0)
round 1 · packages/vscode-ide-companion/src/webview/hooks/useImage.test.ts: P (exit 0)
round 1 · packages/vscode-ide-companion/src/webview/hooks/useMessageSubmit.test.ts: P (exit 0)
round 1 · packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.test.tsx: P (exit 0)
round 1 · packages/vscode-ide-companion/src/webview/providers/WebViewContent.test.ts: P (exit 0)
round 1 · packages/vscode-ide-companion/src/webview/providers/WebViewProvider.test.ts: P (exit 0)
round 1 · packages/vscode-ide-companion/src/webview/utils/copyTranscript.test.ts: P (exit 0)
round 1 · packages/vscode-ide-companion/src/webview/utils/fileLinks.test.ts: P (exit 0)
round 1 · packages/web-shell/client/build-artifact.test.ts: P (exit 0)
round 2 · packages/vscode-ide-companion/scripts/generate-notices.test.js: P (exit 0)
round 2 · packages/vscode-ide-companion/src/services/qwenAgentManager.test.ts: P (exit 0)
round 2 · packages/vscode-ide-companion/src/utils/imageSupport.test.ts: P (exit 0)
round 2 · packages/vscode-ide-companion/src/webview/App.test.tsx: P (exit 0)
round 2 · packages/vscode-ide-companion/src/webview/adapters/acpTranscriptAdapter.test.ts: P (exit 0)
round 2 · packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.test.ts: P (exit 0)
round 2 · packages/vscode-ide-companion/src/webview/hooks/message/useMessageHandling.test.tsx: P (exit 0)
round 2 · packages/vscode-ide-companion/src/webview/hooks/useAcpTranscript.test.ts: P (exit 0)
round 2 · packages/vscode-ide-companion/src/webview/hooks/useImage.test.ts: P (exit 0)
round 2 · packages/vscode-ide-companion/src/webview/hooks/useMessageSubmit.test.ts: P (exit 0)
round 2 · packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.test.tsx: P (exit 0)
round 2 · packages/vscode-ide-companion/src/webview/providers/WebViewContent.test.ts: P (exit 0)
round 2 · packages/vscode-ide-companion/src/webview/providers/WebViewProvider.test.ts: P (exit 0)
round 2 · packages/vscode-ide-companion/src/webview/utils/copyTranscript.test.ts: P (exit 0)
round 2 · packages/vscode-ide-companion/src/webview/utils/fileLinks.test.ts: P (exit 0)
round 2 · packages/web-shell/client/build-artifact.test.ts: P (exit 0)
round 3 · packages/vscode-ide-companion/scripts/generate-notices.test.js: P (exit 0)
round 3 · packages/vscode-ide-companion/src/services/qwenAgentManager.test.ts: P (exit 0)
round 3 · packages/vscode-ide-companion/src/utils/imageSupport.test.ts: P (exit 0)
round 3 · packages/vscode-ide-companion/src/webview/App.test.tsx: P (exit 0)
round 3 · packages/vscode-ide-companion/src/webview/adapters/acpTranscriptAdapter.test.ts: P (exit 0)
round 3 · packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.test.ts: P (exit 0)
round 3 · packages/vscode-ide-companion/src/webview/hooks/message/useMessageHandling.test.tsx: P (exit 0)
round 3 · packages/vscode-ide-companion/src/webview/hooks/useAcpTranscript.test.ts: P (exit 0)
round 3 · packages/vscode-ide-companion/src/webview/hooks/useImage.test.ts: P (exit 0)
round 3 · packages/vscode-ide-companion/src/webview/hooks/useMessageSubmit.test.ts: P (exit 0)
round 3 · packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.test.tsx: P (exit 0)
round 3 · packages/vscode-ide-companion/src/webview/providers/WebViewContent.test.ts: P (exit 0)
round 3 · packages/vscode-ide-companion/src/webview/providers/WebViewProvider.test.ts: P (exit 0)
round 3 · packages/vscode-ide-companion/src/webview/utils/copyTranscript.test.ts: P (exit 0)
round 3 · packages/vscode-ide-companion/src/webview/utils/fileLinks.test.ts: P (exit 0)
round 3 · packages/web-shell/client/build-artifact.test.ts: P (exit 0)
round 4 · packages/vscode-ide-companion/scripts/generate-notices.test.js: P (exit 0)
round 4 · packages/vscode-ide-companion/src/services/qwenAgentManager.test.ts: P (exit 0)
round 4 · packages/vscode-ide-companion/src/utils/imageSupport.test.ts: P (exit 0)
round 4 · packages/vscode-ide-companion/src/webview/App.test.tsx: P (exit 0)
round 4 · packages/vscode-ide-companion/src/webview/adapters/acpTranscriptAdapter.test.ts: P (exit 0)
round 4 · packages/vscode-ide-companion/src/webview/handlers/SessionMessageHandler.test.ts: P (exit 0)
round 4 · packages/vscode-ide-companion/src/webview/hooks/message/useMessageHandling.test.tsx: P (exit 0)
round 4 · packages/vscode-ide-companion/src/webview/hooks/useAcpTranscript.test.ts: P (exit 0)
round 4 · packages/vscode-ide-companion/src/webview/hooks/useImage.test.ts: P (exit 0)
round 4 · packages/vscode-ide-companion/src/webview/hooks/useMessageSubmit.test.ts: P (exit 0)
round 4 · packages/vscode-ide-companion/src/webview/hooks/useWebViewMessages.test.tsx: P (exit 0)
round 4 · packages/vscode-ide-companion/src/webview/providers/WebViewContent.test.ts: P (exit 0)
round 4 · packages/vscode-ide

...truncated -- full content in the run artifacts.

Evidence images

01-leak-ab-head-vs-control

02-bundle-ab-base-vs-head

03-bridge-harness-blocks

04-mutation-matrix-kills

05-targeted-gates

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.

LGTM, looks ready to ship. ✅

@chiga0 chiga0 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.

Re-review at head 82912c509521 — 5 new commits since dedb0b87b8df.

Prior ledger

R1 item Status at this head
Outstanding: findBlockByRowKey did not strip tg- prefix from web-shell tool-group row keys Fixed — commit 19263295f2d5 strips tg- before matching

New commits reviewed

  • d1ad62bd36ca — fix: split file links on raw # before percent-decoding
  • c830bc6c7a38 — fix: stamp cached history rows as discrete transcript messages
  • 19263295f2d5 — fix: resolve tool-group copy rows and copy tool content parts
  • 01cc92ea8117 — fix: close remaining transcript regressions
  • 82912c509521 — fix: hide internal image references

What I checked and found clean

fileLinks.ts# splitting fix: normalizeExplicitFileLink now splits on the raw (unencoded) # before calling safeDecodePath, so %23 in a filename is never mistaken for a fragment delimiter. resolveFileLinkFromAnchor now bypasses URL decoding for anchor text (which is a literal filesystem path, not a URL). Both halves of the fix are correct.

copyTranscript.ts — tool-group copy: findBlockByRowKey strips the tg- prefix that web-shell stamps on tool-group row keys. getToolContentCopyParts walks block.content entries to copy tool output text and ---/+++ diffs, matching pre-PR behaviour. error and debug block kinds are included in formatBlocksForCopyAll. All three address Round 3 [Critical] and [Suggestion] findings.

acpTranscriptAdapter.ts — discrete-message marker: Cached history rows now carry _meta: { qwenDiscreteMessage: true }, preventing the shared reducer from folding consecutive same-role rows into one concatenated block. Empty-after-strip user messages (pure @/path image references) are dropped via the new text.trim().length === 0 guard; safe because the existing !message.content guard above ensures message.content is truthy on entry.

imageSupport.ts / useImage.ts — function move: splitMessageContentForImages and its helpers relocated from useImage.ts to imageSupport.ts. Implementations are byte-for-byte equivalent. Both new callers (acpTranscriptAdapter.ts, SessionMessageHandler.ts) import from the canonical location.

SessionMessageHandler.ts — oversized image guard: toBase64 now calls fsp.stat before fsp.readFile and returns null for images exceeding MAX_IMAGE_SIZE, matching the existing pasted-attachment cap. Test added and pinned to the skip path.

WebViewContent.ts / vite.lib.config.ts / WebShellTranscript.tsx: font-src data: added to the webview CSP for KaTeX inline fonts. katex/dist/katex.min.css removed from the external list so it is bundled into the web-shell artifact. Build artifact test verifies @font-face contains data:font/woff2;base64, inline.

Cross-check against prior reviews

Finding Reviewer Status
Session state not reset on qwenSessionSwitched jifeng — CHANGES_REQUESTED (id 5000171614) Fixed in 7fc35c16f286; confirmed clean at R1. Review not dismissed but blocker no longer present at this head.
cachedMessageToNotification emits bare chunks with no qwenDiscreteMessage qwen-code-ci-bot R3 [Critical] Fixed in c830bc6c7a38 (this round)
Stored @/path image refs visible in cached transcript qwen-code-ci-bot R3 [Suggestion] Fixed in 82912c509521 (this round)
Copy Message silently fails on every tool-group row (msg:tg-<blockId>) qwen-code-ci-bot R3 [Critical] Fixed in 19263295f2d5 (this round)
Copy All drops error/debug blocks qwen-code-ci-bot R3 [Suggestion] Fixed in 19263295f2d5 (this round)
fileLinks.ts decodes before # split qwen-code-ci-bot R3 [Critical] Fixed in d1ad62bd36ca (this round)
stale streamEnd finalizes wrong session / conversationCleared blind-adopt qwen-code-ci-bot R3 [Critical] Fixed in 5eea55b75da2/9e261235c704 (before R1); confirmed clean at R1
qwen-code-ci-bot latest verdict qwen-code-ci-bot APPROVED at 2026-08-24T07:22Z (id 5005390576)

CI at time of review

Check Result
Test (ubuntu-latest, Node 22.x) ✅ PASS (25m 30s)
Desktop Shell (ubuntu / windows) ✅ PASS
web-shell E2E Smoke ✅ PASS
Test (macos / windows) ⏸ SKIPPED — branch/path filter (same as R1)
Integration Tests ⏸ SKIPPED — branch/path filter (same as R1)

Scope: vscode-ide-companion and web-shell source only. NOTICES.txt excluded. Static read + cross-file trace. No local toolchain available — execution rungs 1–3 not run.

Reviewed with AI assistance.

@yiliang114
yiliang114 added this pull request to the merge queue Aug 24, 2026
Merged via the queue into QwenLM:main with commit 5b3830b Aug 24, 2026
123 of 125 checks passed
@wenshao

wenshao commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Runtime validation report — PR #9719

Verified head 82912c5095 (37 commits) against merge-base b2edb80a57, on Linux, in a real browser with the real production bundles. Not a code read: both arms were built with the package's own node esbuild.js --production, the page HTML came from the real WebViewContent.generate() (real CSP), and the extension-host side was played by the extension's own compiled QwenSessionUpdateHandler plus the same transcriptUpdate forwarding QwenAgentManager.onSessionUpdate performs. The 27 replayed session/update frames were captured from a real qwen --acp run (read → edit → shell) against a mock OpenAI server, so both arms see byte-identical input.

Verdict: the timeline swap works and its guards are real. Three parity regressions against the legacy renderer are worth resolving before merge; two are on the cached-history restore path, which the Reviewer Test Plan exercises (step 3) but the current test suite locks in as intended.

timeline A/B


Findings

# Severity What Where
1 High Restored history drops image attachments; an image-only user turn vanishes entirely acpTranscriptAdapter.ts cachedMessageToNotification
2 Medium Consecutive cached assistant rows render run-together in one paragraph web-shell/client/adapters/transcriptToMessages.ts:608
3 Medium First-paint payload is ~11× the pre-PR bundle — larger than the PR body states esbuild.js splitting + katex CSS commit
4 Low Local notices render on screen but are dropped from Copy all messages copyTranscript.ts
5 Nit Dead code the PR body says was removed still ships qwenAgentManager.ts, WebViewProvider.ts:1936

1 — Restored history loses images, and image-only turns disappear (High)

Same cached ChatMessage[] replayed through qwenSessionSwitched into both builds:

image restore regression

Cached history BEFORE AFTER
user: "what is wrong in this screenshot? @/…/shot.png" [Image #1] + text, image decoded 120×60 text only, no image
user: "@/…/shot.png" (image only) [Image #2] renders whole turn dropped
the two assistant replies around it two separate rows merged into The button label is clipped.Same issue in the second shot.

The legacy path is intact and still reachable: the webview still posts resolveImagePaths on restore, and WebViewProvider.handleResolveImagePaths still answers with Array<{path, src}>expandUserMessageWithImages was simply not carried over to the adapter. cachedMessageToNotification strips the reference and then returns null when nothing is left, so the user turn is gone and the neighbouring assistant blocks collapse into each other.

acpTranscriptAdapter.test.ts asserts this toBeNull(), so it is deliberate — but it is a real regression on the “attach a screenshot → ask → reload the window” path, and it is not among the parity gaps listed in the PR body. Suggested fix: emit an image content part for cached refs the same way the live echo already does (readPromptImageAsBase64), or at minimum keep a placeholder row so turns don't vanish and replies don't merge.

Live sends are not affected — an attached image echoes correctly as an inline data: part and decodes at natural size (verified separately).

2 — Consecutive cached assistant rows are concatenated with no separator (Medium)

row merge

The CACHED_ROW_META anti-merge marker works: driving the compiled adapter directly, three cached rows produce three discrete blocks, each carrying meta.qwenDiscreteMessage: true. The collapse happens one layer up — transcriptToMessages.ts:608 does content: target.content + textBlock.text for a consecutive assistant block, with no separator. The rendered DOM is a single <p>Tool Result ATool Result B</p>.

That concatenation is correct for the web-shell's own streaming deltas; it is wrong for the discrete rows the companion now seeds. Since the companion persists tool-result / telemetry / plan rows as separate assistant messages, this is exactly the case the marker was added for. Fix either side: honour meta.qwenDiscreteMessage in the web-shell adapter, or join with \n\n in the companion.

3 — First-paint payload and package size (Medium)

Measured from the actual network log of a real turn, plus the built artifacts:

BEFORE AFTER
entry webview.js 709,373 B 511,044 B
lazy chunks actually fetched on timeline mount 10 files, 7,597,115 B
JS before the first message paints 0.68 MiB 7.73 MiB (11.4×)
largest chunk (dist-*.js, the WebShell lib) 5,913,487 B
dist/ on disk 17.68 MB 37.75 MB (426 chunk files)
dist/ zipped (≈ VSIX payload) 4.7 MB 9.7 MB

Of that, the hide internal image references commit's katex change accounts for a clean +1,538,583 B (+35.9%) on the web-shell lib alone — measured by rebuilding the same worktree with only WebShellTranscript.tsx + vite.lib.config.ts reverted: 4,284,252 B → 5,822,835 B. KaTeX ships its font faces base64-inlined, so every session pays ~1.5 MiB whether or not it renders math. Worth considering a separate lazy import for the math CSS. The PR body's “~4MB WebShell chunk / entry 504KB + 2 static chunks ≈ 755KB” is now out of date either way.

4 — Local notices are excluded from Copy all messages (Low)

An error webview message renders in [data-testid="local-message-notices"] in both arms, but only the legacy build copies it:

on screen in copy-all
BEFORE yes yes — **Qwen Code:** Tool failed: EACCES on /etc/hosts (1,281 chars)
AFTER yes no (2,197 chars, notice absent)

Related: the new error / debug cases in formatBlocksForCopyAll are unreachable on this surface — the adapter only ever wraps session_update events, and those block kinds are produced solely by daemon error/debug events. Harmless if it is forward-compat for the daemon path, but it is not what covers the case above.

5 — Dead code the PR body says was removed (Nit)

QwenAgentManager.rewindSessionAcpConnection.rewindSession now has no caller outside tests, and WebViewProvider.ts:1936 still branches on message.type === 'editMessage', which the webview no longer sends.


What was verified working

Behaviour matrix — 15 scenarios, both arms, real Chromium (click to expand)
Scenario Result
Timeline render WebShell block model: 6 discrete rows, 32 Shiki-highlighted tokens, markdown table, 2 code blocks, 0 console errors, 0 failed requests
wasm-unsafe-eval Load-bearing. Served with the pre-PR CSP the same bundle logs 2 WebAssembly.instantiate() … violates CSP warnings and drops to 0 highlighted tokens; the 751-char transcript still renders (graceful degradation)
font-src data: Load-bearing. With it: 2 KaTeX faces loaded, math box 101 px. Without it: 0 faces, 86 px fallback metrics
Live VS Code theme switch data-vscode-theme-kind → light flips the root class and background live (oklch(0.145 0 0)rgb(255,255,255))
Cross-session bleed Late frames from an abandoned session A are dropped; session B's answer stays clean
Permission drawer Renders mid-turn with the command text and 3 options; clicking Allow once posts permissionResponse {optionId:"allow-once"}
Cancel mid-turn Posts cancelStreaming, transcript shows Interrupted, composer stays alive
File links greeting.ts anchor click posts openFile with the correct absolute path (identical in both arms)
Per-row / all copy Row-scoped copy now returns tool content (ReadFile: greeting.ts + args + output); copy-all grew 1,226 → 2,197 chars and gained the tool sections
Live image attachment Inline data:image/png part decodes at natural 120×60
zh-CN locale 思考片刻 / 读取文件 / 已编辑 1 个文件 已运行 1 条命令 render correctly
Chunk-load failure Recoverable error state, see teeth below
Cached restore (no images) Renders correctly
Image-reference parsing splitMessageContentForImages is behaviour-identical to the pre-PR parser (function body diffed; 18-case adversarial matrix identical), so its edge cases — a @…/x.png inside a fenced block or a URL gets stripped — are carried over, not introduced

Mutation teeth

Each PR guard was reverted in source, the webview rebuilt, and the same scenario re-driven:

teeth

Guard removed PR behaviour Mutant behaviour
TranscriptErrorBoundary "The conversation timeline failed to load / Reload panel", composer alive entire panel blank (root.innerHTML = 0 bytes) on the same chunk 404
stale-session frame guard (useAcpTranscript.ts) 677-char clean answer 780 chars — trailing session-A frames appended and the active answer corrupted mid-sentence
live-theme MutationObserver dark → light applies theme frozen at the mount-time snapshot
--web-shell-bottom-panel-inset: 140px last row bottom 752 px vs composer top 807 px → 0 px occluded last row bottom 869 px → 63 px hidden behind the composer
font-src data: in the CSP 2 KaTeX faces loaded 0 faces, fallback glyph metrics

theme + katex

Build & tests

  • node esbuild.js --productionpass (after building @qwen-code/web-shell's lib from this branch).
  • vitest run (companion) — 607 passed, 1 skipped, 58 files, 0 failures. Twelve files initially failed to load and one timed out under parallel load; re-run serially with a longer timeout they all pass, so no real failures.
  • tsc --noEmit could not be run faithfully here: my worktree uses a node_modules link farm in which the sibling workspaces' dist/types are not built, so every error is a missing sibling declaration (@qwen-code/webui, @qwen-code/sdk/daemon) and reproduces identically on the base arm. Not attributed to this PR — CI's ordered build is the authority.
  • postcss.config.js uses module.exports inside a "type": "module" package, which breaks vitest in this environment; it is identical on base, so it is pre-existing, not a PR issue.
Harness detail

Both arms served over HTTPS from an origin that plays the role of webview.cspSource, so the real generated CSP is enforced verbatim. boot.js stubs acquireVsCodeApi and reproduces WebViewProvider.setupAgentCallbacks(), SessionMessageHandler.handleSendMessage's user echo, and handleResolveImagePaths's Array<{path, src}> reply. Chromium 1228 headless, 520×900 viewport at DPR 2 (sidebar-like width).

One methodology note worth recording: an early run showed the whole webview crashing with TypeError: m is not iterable on any cached history containing an image reference — that reproduced in both arms and turned out to be my harness replying to resolveImagePaths with an object instead of the array the webview expects. Corrected before any of the numbers above were taken.

中文版报告

PR #9719 运行时验证报告

在真实浏览器中用真实生产构建产物验证了 head 82912c5095(37 个 commit)与 merge-base b2edb80a57 的对比。不是代码走查:两个 arm 都用包自带的 node esbuild.js --production 构建,页面 HTML 来自真实的 WebViewContent.generate()(真实 CSP),扩展宿主侧由扩展自己编译出的 QwenSessionUpdateHandler 以及与 QwenAgentManager.onSessionUpdate 相同的 transcriptUpdate 转发逻辑扮演。回放的 27 条 session/update 帧来自一次真实 qwen --acp 运行(read → edit → shell)对接 mock OpenAI 服务的抓取,因此两个 arm 的输入完全一致。

结论:时间线替换本身可用,其防护逻辑经得起变异测试。 有三处相对旧渲染器的行为回退建议合并前处理,其中两处位于缓存历史恢复路径上——正是 Reviewer Test Plan 第 3 步覆盖的场景,但当前测试用例把它固化成了“预期行为”。

问题清单

# 严重度 问题 位置
1 恢复历史时图片附件丢失;纯图片的用户消息整条消失 acpTranscriptAdapter.ts cachedMessageToNotification
2 缓存的连续 assistant 行被拼成同一段、且无分隔符 web-shell/client/adapters/transcriptToMessages.ts:608
3 首屏加载体积约为 PR 前的 11 倍,比 PR 描述中的数字更大 esbuild.js 代码分割 + katex CSS commit
4 本地通知在界面上可见,但被“复制全部消息”丢弃 copyTranscript.ts
5 提示 PR 描述称已删除的死代码仍在仓库中 qwenAgentManager.tsWebViewProvider.ts:1936

问题 1(高):把同一份缓存 ChatMessage[] 通过 qwenSessionSwitched 灌入两个构建:旧渲染器渲染出 [Image #1]/[Image #2](图片实际解码为 120×60);本 PR 一张都不渲染,且纯图片的那条用户消息被整体丢弃,导致它前后的两条 assistant 回复被合并成 The button label is clipped.Same issue in the second shot.

旧链路仍然完好可用:webview 在恢复时依然会发出 resolveImagePathsWebViewProvider.handleResolveImagePaths 依然返回 Array<{path, src}>——只是 expandUserMessageWithImages 没有被迁移到新的 adapter。cachedMessageToNotification 先剥离图片引用,剩余文本为空时返回 null,于是整条消息消失、相邻的 assistant 块随之粘连。

acpTranscriptAdapter.test.tstoBeNull() 固化了这一行为,说明是有意为之——但在“贴图 → 提问 → 重载窗口”这条真实路径上它确实是回退,且并未列入 PR 描述的已知差距清单。建议:像实时回显那样(readPromptImageAsBase64)为缓存引用也补发 image content part;至少保留一个占位行,避免消息消失和回复粘连。

实时发送不受影响:附带的图片会以内联 data: part 正确回显并按原始尺寸解码(已单独验证)。

问题 2(中)CACHED_ROW_META 反合并标记本身是生效的——直接驱动编译后的 adapter,三条缓存行确实产出三个独立 block,且都带 meta.qwenDiscreteMessage: true。粘连发生在上一层:transcriptToMessages.ts:608 对连续 assistant block 执行 content: target.content + textBlock.text,没有任何分隔符,最终 DOM 是单个 <p>Tool Result ATool Result B</p>。这种拼接对 web-shell 自身的流式增量是正确的,但对 companion 新引入的离散行是错的。两侧修其一即可:web-shell adapter 尊重 meta.qwenDiscreteMessage,或 companion 侧用 \n\n 连接。

问题 3(中):真实网络日志与构建产物实测——入口 webview.js 从 709,373 B 降到 511,044 B,但时间线挂载时会真实拉取 10 个 chunk 共 7,597,115 B,首屏 JS 从 0.68 MiB 变为 7.73 MiB(11.4 倍);dist/ 从 17.68 MB 增至 37.75 MB(426 个 chunk 文件),压缩后(约等于 VSIX 载荷)从 4.7 MB 增至 9.7 MB。其中 hide internal image references 这个 commit 的 katex 改动单独贡献 +1,538,583 B(+35.9%)——在同一 worktree 中只回退 WebShellTranscript.tsx + vite.lib.config.ts 重新构建对比得出:4,284,252 B → 5,822,835 B。KaTeX 的字体是 base64 内联的,因此无论是否渲染公式,每个会话都要付这 ~1.5 MiB。建议考虑把数学 CSS 拆成独立的懒加载。PR 描述中的 “~4MB WebShell chunk / entry 504KB + 2 static chunks ≈ 755KB” 已经过时。

问题 4(低)error 类型的 webview 消息在两个 arm 中都会渲染进 [data-testid="local-message-notices"],但只有旧构建会把它写进复制内容(1,281 字符,含 **Qwen Code:** Tool failed: EACCES on /etc/hosts);本 PR 的复制结果 2,197 字符中不含该通知。另外,formatBlocksForCopyAll 新增的 error / debug 分支在这个界面上不可达——adapter 只包装 session_update 事件,而这两类 block 仅由 daemon 的 error/debug 事件产生。若是为 daemon 路径预留则无害,但它并不覆盖上面这个场景。

问题 5(提示)QwenAgentManager.rewindSessionAcpConnection.rewindSession 除测试外已无调用方;WebViewProvider.ts:1936 仍在判断 webview 不再发送的 message.type === 'editMessage'

已验证正常的行为

时间线渲染(6 个离散行、32 个 Shiki 高亮 token、markdown 表格、2 个代码块、0 控制台错误、0 失败请求);wasm-unsafe-eval 确实必需(用 PR 前的 CSP 跑同一 bundle 会出现 2 条 WebAssembly.instantiate() … violates CSP 警告并退化为 0 个高亮 token,751 字符正文仍正常渲染);font-src data: 确实必需(有则 2 个 KaTeX 字体 loaded、公式宽 101px,无则 0 个、退化为 86px);VS Code 主题实时切换;跨会话串扰防护;权限抽屉(显示命令 + 3 个选项,点击 Allow once 回发 permissionResponse {optionId:"allow-once"});中途取消(发出 cancelStreaming,显示 Interrupted,输入框存活);文件链接点击回发 openFile(两个 arm 路径一致);按行/全量复制(复制全部从 1,226 字符增至 2,197 字符并新增工具段落);实时图片附件按 120×60 解码;zh-CN 文案正确;splitMessageContentForImages 与 PR 前的解析器行为完全一致(函数体逐行 diff + 18 个对抗用例结果相同),因此其边界情况(代码块或 URL 中的 @….png 会被剥离)属于沿用而非新引入。

变异测试

逐个把 PR 的防护改回,重新构建 webview 并重跑同一场景:移除 TranscriptErrorBoundary 后,同样的 chunk 404 会让整个面板空白root.innerHTML = 0 字节),而 PR 版本显示“时间线加载失败 / 重新加载面板”且输入框存活;移除会话陈旧帧防护后正文从 677 字符变成 780 字符并出现句中错乱;移除主题 MutationObserver 后主题冻结在挂载瞬间的快照;移除 --web-shell-bottom-panel-inset: 140px 后最后一行有 63px 被输入框遮挡(PR 版本为 0px);移除 CSP 中的 font-src data: 后 KaTeX 字体加载数从 2 降为 0。

构建与测试

node esbuild.js --production 通过(需先从本分支构建 @qwen-code/web-shell 的 lib)。companion 的 vitest run607 通过、1 跳过、58 个文件、0 失败(并行压力下有 12 个文件加载失败、1 个超时,串行并放宽超时后全部通过,非真实失败)。tsc --noEmit 在本环境无法忠实执行:我的 worktree 用的是 node_modules link farm,兄弟工作区的 dist/类型未构建,所有报错都是缺失兄弟包声明(@qwen-code/webui@qwen-code/sdk/daemon),且在 base arm 上完全复现,故不归因于本 PR,以 CI 的有序构建为准。另外 postcss.config.js"type": "module" 包中使用 module.exports,会让 vitest 在本环境启动失败——base 上完全相同,属既有问题。

方法学备注:早期有一次跑出“任何含图片引用的缓存历史都会让 webview 整体崩溃(TypeError: m is not iterable)”,但该现象在两个 arm 上都复现,最终查明是我的 harness 对 resolveImagePaths 回复了对象而非 webview 期望的数组。已在采集上述任何数据之前修正。

yiliang114 added a commit to water-in-stone/qwen-code that referenced this pull request Aug 24, 2026
…o main

Resolves conflicts between PR QwenLM#9641 (water-in-stone: WebShell transcript
identity, VS Code adapter, and HTML export) and main (QwenLM#9719 already merged).

Strategy: main (QwenLM#9719) is authoritative for the adapter plumbing; preserve
QwenLM#9641's identity projection and HTML export (renderMode/documentMode) while
dropping the duplicate adapter implementation.

Also fixes merge remnants: orphaned data-web-shell-transcript flag, duplicate
onTranscriptUpdate declaration, stale useAcpTranscript.test.tsx rename, and
duplicate @qwen-code/web-shell dependency.
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.22.2.

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.

7 participants