Skip to content

fix(web-shell): keep live Generated UI cards attached - #15787

Merged
qqqys merged 1 commit into
QwenLM:mainfrom
qqqys:fix/generated-ui-live-attach
Oct 28, 2026
Merged

qqqys merged 1 commit into
QwenLM:mainfrom
qqqys:fix/generated-ui-live-attach

Conversation

@qqqys

@qqqys qqqys commented Oct 28, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

A Generated UI card produced during a live turn now attaches and shows its interactive view as soon as the tool result arrives, instead of rendering as a released card with Reopen interactive view.

Two things kept the card detached. The Web Shell owner source compared the session recovery generation with the value it saw when it last rendered. Sending a prompt advances that generation without changing the capability receipt, so every attach during the turn was refused; the owner snapshot already rejects a recovery that started after it was captured, so the render-time comparison is removed. A refused first attach was also never retried, because the slot attached again only when its key, resource or token changed. A card that has never attached now retries after each render, while a card whose instance was released still requires an explicit reopen.

Why it's needed

Since the retention work in #15611, every card generated in a live Web Shell turn appears released right away: the transcript shows the saved fallback text, the reopen notice and a Reopen interactive view button, and the card only becomes interactive after a page reload, when it is rendered from history. This affects every user who enables experimental.generatedUi, and blocks making the feature default-on (#14019).

Reviewer Test Plan

How to verify

  1. Enable experimental.generatedUi, start qwen serve and open Web Shell.
  2. Ask for an interactive view, for example: 请使用 generated-ui 技能和 generated_ui_show 工具,在对话里给我一个交互卡片:聚餐分账,菜品总价 924 元,6 个人平分,可以调整人数和服务费比例。
  3. While the turn is still running, the card should render its interactive view when the tool result arrives, without a Reopen interactive view button, and it should stay attached after the turn ends.
  4. Releasing a retained card (for example by deleting the session's messages or letting the page track more than 128 cards) should still require an explicit reopen.

Unit coverage: a new GeneratedUiRuntimeProvider case renders a slot whose owner is not current, then makes it current and expects the card to attach without a reopen button; it fails without this change.

Evidence (Before & After)

Same daemon build, model (qwen3.8-max) and prompt; a Playwright probe sampled the transcript once per second:

Before (9ef52e1536) After
Card while the turn runs released at 56 s: no frame, Reopen interactive view shown attached at 88 s when the tool result arrived
After the turn ends still released until a page reload still attached

Instrumenting the owner check on the baseline showed the only failing term during the turn was the render-time recovery generation (210 refusals in one turn).

Tests: packages/web-shell client/generated-ui, DaemonSessionProvider and McpApp suites, 617 passed; generated-ui.retention.spec.ts and generated-ui.continue.spec.ts in Chromium passed, with three timing-sensitive cases that also fail intermittently on main under full-suite load passing when rerun.

Tested on

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

Environment (optional)

Local bundle of main with and without this change, qwen serve with an isolated HOME, Chromium via Playwright.

Risk & Scope

  • Main risk or tradeoff: an attach is no longer refused merely because a prompt advanced the recovery generation since the last render. Recoveries that start after the owner is captured, capability receipt changes, session and client changes, manual clears and confirmed revocations still invalidate it.
  • Not validated / out of scope: other Generated UI presentation changes in the feat(web-shell): complete experimental intelligent UI with stateful interactions #14019 series.
  • Breaking changes / migration notes: none.

Linked Issues

Follow-up to #15611. Part of #14019.

中文说明

这个 PR 做了什么

在实时回合中生成的 Generated UI 卡片,现在会在工具结果到达时直接挂载并显示交互视图,不再一出现就变成带 重新打开交互视图 按钮的"已释放"卡片。

导致卡片挂不上的原因有两个。Web Shell 的 owner source 用"上次渲染时看到的"会话恢复代数做比对;发送消息会推进这个代数,但能力回执并没有变化,于是整个回合内的每次 attach 都被拒绝。owner 快照本身已经会拒绝在捕获之后才开始的恢复,因此去掉了这项按渲染时取值的比对。另外,首次 attach 被拒后不会重试,因为卡片槽位只在 key、resource 或 token 变化时才重新 attach。现在从未挂上过的卡片会在每次渲染后重试;已经被释放的卡片仍需用户显式重新打开。

为什么需要

自 #15611 引入保留机制以来,实时 Web Shell 回合中生成的每张卡片都会立刻显示为已释放:transcript 中显示保存的回退文本、重新打开说明和 重新打开交互视图 按钮,只有刷新页面、从历史记录渲染时卡片才能交互。所有开启 experimental.generatedUi 的用户都会遇到,也阻碍了把该功能默认开启(#14019)。

审阅者测试计划

如何验证

  1. 开启 experimental.generatedUi,启动 qwen serve 并打开 Web Shell。
  2. 请求一个交互视图,例如:请使用 generated-ui 技能和 generated_ui_show 工具,在对话里给我一个交互卡片:聚餐分账,菜品总价 924 元,6 个人平分,可以调整人数和服务费比例。
  3. 在回合仍在进行时,工具结果一到达,卡片就应显示交互视图,不出现 重新打开交互视图 按钮;回合结束后卡片保持挂载。
  4. 释放一张已保留的卡片(例如删除会话消息,或让页面跟踪超过 128 张卡片)后,仍应要求显式重新打开。

单元测试:新增一个 GeneratedUiRuntimeProvider 用例,先以 owner 非当前状态渲染槽位,再让它变为当前,预期卡片自动挂载且没有重新打开按钮;没有本改动时该用例失败。

证据(修改前后)

相同的 daemon 构建、模型(qwen3.8-max)和提示词;Playwright 探针每秒采样一次 transcript:

修改前(9ef52e1536) 修改后
回合进行中的卡片 56 秒时已释放:没有 frame,显示 重新打开交互视图 88 秒工具结果到达时挂载
回合结束后 仍为已释放,需刷新页面 保持挂载

在基线上对 owner 检查加日志后发现,回合期间唯一不成立的就是按渲染时取值的恢复代数(一个回合内被拒 210 次)。

测试:packages/web-shell 中 client/generated-ui、DaemonSessionProvider 和 McpApp 测试共 617 个通过;Chromium 下 generated-ui.retention.spec.ts 与 generated-ui.continue.spec.ts 通过,其中三个对时序敏感、在 main 上整套运行时同样会偶发失败的用例,重跑后通过。

测试平台

OS 状态
🍏 macOS ⚠️
🪟 Windows ⚠️
🐧 Linux ✅

环境(可选)

main 在有无本改动两种情况下的本地 bundle,隔离 HOME 的 qwen serve,通过 Playwright 驱动 Chromium。

风险与范围

  • 主要风险或取舍:attach 不再仅仅因为"自上次渲染后发送了消息、推进了恢复代数"而被拒绝。在 owner 捕获之后开始的恢复、能力回执变化、会话和客户端变化、手动清空以及已确认的撤销,仍会使其失效。
  • 未验证 / 不在范围内:feat(web-shell): complete experimental intelligent UI with stateful interactions #14019 系列中其他 Generated UI 展示改动。
  • 破坏性变更 / 迁移说明:无。

关联 Issue

#15611 的后续修复。属于 #14019 的一部分。

A card produced during a live turn showed Reopen interactive view with
its fallback text as soon as it rendered; it only attached after the
page was reloaded from history.

The owner source compared the session recovery generation with the
value seen at render. Sending a prompt advances that generation without
changing the capability receipt, so every attach during the turn was
refused. The owner snapshot already rejects a recovery that started
after it was captured, so the render-time comparison is removed.

A refused first attach was also never retried, because the slot only
attached again when its key, resource or token changed. A card that has
never attached now retries after each render; a card whose instance was
released still needs an explicit reopen.

@yiliang114 yiliang114 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 — APPROVE ✅

Scope: 3 files, +33/−9. Fix for Generated UI cards being detached during live turns.

Root cause: During a live turn, the sessionRecoveryGenerationRef advances when a prompt is sent, but the owner capability receipt does not change. The generatedUiRecoveryGeneration captured at render time would never match the ref's current value during the turn, causing every isCurrent() to fail and every card attach to be rejected.

Fix analysis:

  1. DaemonSessionProvider.tsx: Removes the generatedUiRecoveryGeneration capture and comparison. The comment correctly notes that owner.isCurrent() already handles the "recovery started after capture" race; the generation check was redundant and over-blocking.

  2. GeneratedUiRuntimeProvider.tsx: Adds a retry path — when a card has never been successfully attached (!store.getRecord(slot.key)) and is not already active (!ordinaryActive), it calls store.attach() instead of store.update(). This recovers from a stale-first-attach without re-attaching cards that were intentionally released.

  3. Test: attaches a live card once a refused first attach becomes current — directly exercises the stale→current transition, verifying no iframe on first render and successful attach on re-render.

Verdict: No P0 or P1 findings. The fix is minimal, correct, and well-tested. CI is green on all relevant lanes.

@qqqys
qqqys enabled auto-merge (squash) October 28, 2026 11:30

@qwen-code-dev-bot qwen-code-dev-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.

APPROVE — 审查版本 head c3135c75(提交前后两次读取一致,单 commit)。

  • 历史 Review:唯一一条正式 Review(yiliang114,APPROVE)无 Critical/必要 Suggestion,无未解决的 review thread;结论与本次独立检查一致。
  • CI:head 上唯一 required check Lint & Static (ubuntu-latest, Node 22.x) success,Test (ubuntu-latest, Node 22.x)、web-shell E2E Smoke、Integration Tests (no-AK)、TUI parity、OpenTUI no-flicker、Managed Agent gate 均 success。非 required 的 Capture web-shell visuals 仍在运行、precheck-pr / precheck 自 09:17Z 起 pending(自动化上下文,非代码 lane),不构成门禁阻塞;若 visuals lane 后续变红请再触发一次 review。
  • 独立检查未发现新 Critical:
    • 去掉 render 期 recovery generation 比较后,失效保护没有变弱:captureOwner() 内的 ownerGuardValue.capture({ includeRecovery: true }) 在消费方捕获时记录 generation,任何此后的 advanceSessionRecoveryGeneration(prompt 开始、turn_complete/turn_error、rewind、policy 变更)都会让 isCurrent() 变假;session 对象、clientId、eventEpoch、scope、connection、generatedUi receipt 身份仍逐项实时校验。
    • 重试路径不会把「已释放」的卡片自动拉起:attach() 被拒绝且原本无 record 时不写入 record(getRecord 仍为 undefined,故可重试);而 release/dispose(含 128 条 record 上限、MAX_PARKED_INSTANCES 驱逐、deleted/reconnected/replay_complete/cancelled/stream_ended 等 invalidation)都保留 record(entry 置空),此时 effect 走 update() 分支且因 entry?.token !== slot.token 直接 no-op,仍需显式 reopen。
    • 唯二会删除 record 的路径是 scope-changed invalidation(scope 变更后 generatedUiScopeRef.current === generatedUiScope 使 isCurrent() 为假)与 provider 卸载时的 store.clear(),都不会造成误重挂。
    • 无渲染风暴:attach 成功才 changed(),重试被拒时不触发 store 版本变更;update() 不 changed()。

@qqqys
qqqys merged commit e93baf0 into QwenLM:main Oct 28, 2026
63 of 64 checks passed
@qwen-code-review-bot

qwen-code-review-bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

✅ Qwen Triage finished — view run. See the stage comments in this thread for the result.

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

@qwen-code-review-bot

Copy link
Copy Markdown
Collaborator

Released in v0.26.3.

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.

4 participants