Repository navigation
fix(generated-ui): offer Generated UI only to clients that render MCP Apps - #15720
Conversation
… Apps Generated UI activated in every trusted local session once the setting was on, including the interactive TUI, qwen -p and other headless runs, editor ACP clients and channels. None of them can show a card, yet the model still called generated_ui_show and told the user to adjust the sliders in a card above. The daemon's ACP bridge, whose sessions Web Shell and Desktop display, now declares qwen.mcpAppHost in its client capabilities, and acpAgent marks those session configs as MCP App hosts before initialization. Every other session reports the new unsupported-client reason and does not receive the tool or skill. That reason is an expected state, so it no longer produces the startup warning the bootstrap config used to log as unsupported-runtime.
|
Verdict: merge-ready — 35/35 scripted assertions at head 中文摘要结论:merge-ready(35/35 条脚本断言全部通过)。 验证对象:head 中心主张 A/B(真实构建 + 真实
trial merge(main tip 变异矩阵: M1(客户端门禁)、M2(告警抑制)、M3(acpAgent setter)、M4(bridge 能力声明)、M7(同文件对照)均被杀;M5(状态白名单新增 CI: 实质代码 lane(ubuntu 单测、lint、无 AK 集成、Web Shell smoke、OpenTUI)全绿;其余红点是 未覆盖: 真实模型下的自主行为(模型是否真去用卡片)、Web Shell 的 DOM 渲染、managed/hosted 会话、只通过 SDK 查看的 daemon 会话、Windows/macOS 渲染,以及全仓测试。 What the PR claimsGenerated UI (the bundled MCP App provider) becomes conditional on the client being able to render MCP Apps. The daemon ACP bridge declares A/B proof of the central claimBoth arms are separate worktrees built from this repository, driven as real processes. The model side is a local OpenAI-compatible endpoint, so the observable is the actual wire payload the product sends; the probe scripts the model to call
The headless row is the defect the PR fixes: at base the tool and the skill are offered to a terminal that cannot render a card; at head they are not. The daemon rows are the positive control that the gate did not simply disable the feature: the capability plumbing reaches the child, the session still reports Capture: FindingsF1 —
|
| observable | head | trial merge |
|---|---|---|
| headless skill / tool | withheld / not found | withheld / not found |
daemon generatedUi |
enabled: true |
enabled: true |
| daemon skill / tool | present / discoverable | present / discoverable |
| daemon startup warning | 0 | 0 |
core/src/config/config.test.ts |
975 passed | 975 passed |
acp-bridge/src/bridge.test.ts |
1023 passed | 1023 passed |
acp-bridge/src/generated-ui.test.ts |
17 passed | 17 passed |
cli/acpAgent.test.ts -t 'declares an MCP App host' |
3 passed | 3 passed |
Capture: 04-trial-merge.png.
Mutation matrix
| mutation | selection | baseline | outcome |
|---|---|---|---|
M1 delete the mcpAppHostSupported gate |
config.test.ts -t 'clients that do not render MCP Apps' |
1 passed | KILLED |
M2 re-enable the warning for unsupported-client |
same | 1 passed | KILLED |
M3 drop the setMcpAppHostSupported(true) plumbing |
acpAgent.test.ts -t 'declares an MCP App host' |
3 passed | KILLED |
M4 drop 'qwen.mcpAppHost': true from the bridge handshake |
bridge.test.ts -t 'negotiates the capability…' |
1 passed | KILLED |
M7 control: drop policy-disabled from the same allow-list |
generated-ui.test.ts (17 tests) |
17 passed | KILLED |
M5 drop unsupported-client from the allow-list |
generated-ui.test.ts |
17 passed | SURVIVED (F1) |
| M6 restore the pre-PR reason expression | config.test.ts -t … |
1 passed | SURVIVED (F2) |
Both survivors are same-file controls or covered by one (M7), so they are coverage gaps, not harness failures; neither changes runtime behaviour of the shipped code. Capture: 02-mutation-matrix.png.
CI state at the verified head
The substantive lanes ran and passed on f6a2297a: Test (ubuntu-latest, Node 22.x), Lint & Static, Integration Tests (no-AK, No Sandbox), web-shell E2E Smoke, OpenTUI no-flicker, Managed Agent gate, Check TUI impact. The red checks are not test failures: verify, tmux-testing, triage, authorize, review-pr, Installed npm global and npx and the Node 26/Bun acceptance job are cancelled; precheck-pr and fallback-comment fail on gh: Bad credentials (HTTP 401). Test (macos-latest) is a skipped matrix entry.
Not covered
- Real-model behaviour. The model side is a fake OpenAI endpoint. Tool/skill availability is measured on the wire; whether a real model would have used the card, and whether its prose still mentions one, is not re-run here (the PR body has that evidence for qwen3.8-max).
- Web Shell DOM rendering. Only the session receipt and tool availability are measured; the MCP App card UI itself is not rendered in a browser this round.
- Managed / hosted sessions. Only the trusted local daemon path is driven.
- SDK-only daemon clients. The PR states these still receive the tool (the capability belongs to the connection that creates the session). That tradeoff was not exercised.
- Interactive TUI and editor ACP clients. They share the same code path as headless (no
qwen.mcpAppHost), but onlyqwen -pwas driven end to end. - Windows/macOS rendering differences, the full repository suite, and
typecheck(the PR's own CI ran lint and the ubuntu unit lane). - Mixed-version bridge/CLI pairs. Relevant to F1: an older bridge dropping the new reason from a newer child's status was reasoned about, not executed.
Methodology
Two detached worktrees: base at the merge-base d60124ad, head at f6a2297a; head installed with corepack pnpm install --frozen-lockfile (lockfile identical at base, head and main tip), base seeded by hard-linking that node_modules, with readlink -f node_modules/@qwen-code/qwen-code-core asserted into each tree. Both built with Node 22.23.4 and an 8 GB heap. A third worktree at main tip 57f4bd14 received the PR's effective patch via git apply --3way and was built the same way. Each arm ran real node packages/cli/dist/index.js -p … --output-format stream-json and qwen serve processes with an isolated HOME/QWEN_HOME; the fake endpoint recorded every model request, and the harness read the tool_search tool result from the follow-up request. Raw logs are in obs/, the harness and mutation specs in harness/ and mutations.json.
…mon creates A daemon-managed channel creates its sessions through the daemon bridge, which declares an MCP App host, but its users read replies in the IM client and cannot see a card. Treat channel-sourced sessions as unsupported clients.
yiliang114
left a comment
There was a problem hiding this comment.
APPROVE — zero P0/P1 findings
The gate is the right one: rendersMcpApps() chains mcpAppHostSupported (declared by the ACP bridge at handshake) with sessionSourceType !== 'channel' (excluding IM-bot sessions that travel through the same daemon bridge but whose users never see a card). Every other session — TUI, headless, editor ACP, channels — receives unsupported-client, which is an expected state and produces no startup warning.
Mechanisms verified at head f05832ee
-
Call-ordering guard.
acpAgent.tsreadsclientCapabilities._meta['qwen.mcpAppHost']and callsconfig.setMcpAppHostSupported(true)beforeconfig.initialize(). The test assertsinvocationCallOrderto catch any reordering that would leave the flag unset wheninitializeBundledGeneratedUireads it. -
Reason priority.
generatedUiUnavailableReason()checkssetting-disabledfirst, thenrendersMcpApps()(→unsupported-client), then the runtime/workspace/provided gates. A disabled setting correctly shadows the client check (unit-tested). -
Warning suppression. Both
setting-disabledandunsupported-clientskip the warning, so the daemon no longer prints the misleading "Bundled Generated UI is unavailable (unsupported-runtime)" line at every start. -
unsupported-runtimenarrowing.initializeBundledGeneratedUinow only returnsunsupported-runtimewhen the setting is enabled AND the client does render MCP Apps AND the options disable it — the old ternary would fire it unconditionally whendisableBundledGeneratedUi || skipSkillManagerwas set. The real reason for a non-MCP-app client was always the client, not the runtime. -
Session control plane.
session-control-plane.tsaddsunsupported-clientto the filter list alongside the other inactive reasons, so the feature flag reports correctly.
Test coverage
config.test.ts: unsupported-client for non-host clients, channel sessions, setting-disabled priority.acpAgent.test.ts: three-wayit.each(true→called, false→not-called, undefined→not-called) with ordering check.bridge.test.ts&channel-handshake.ts: capabilities includeqwen.mcpAppHost.mcp-client-manager.test.ts: fixture declares host support beforeinitializeBundledGeneratedUi.
No P2/P3 findings.
中文
门控正确:rendersMcpApps() 将 ACP bridge 握手时声明的 mcpAppHostSupported 与 sessionSourceType !== 'channel' 串联,排除了走同一 daemon bridge 但用户在 IM 客户端读回复的频道会话。其他会话(TUI、headless、编辑器 ACP、频道)统一拿 unsupported-client,此状态不产生启动警告。
已核机制:
- acpAgent 在
config.initialize()前调用setMcpAppHostSupported(true),测试用invocationCallOrder守序。 - 原因优先级:
setting-disabled→unsupported-client→ runtime/workspace 等,有单元测试验证 setting-disabled 优先。 unsupported-client和setting-disabled均不产生警告,daemon 不再误打 log。unsupported-runtime收窄为「设置启用 + 客户端能渲染 MCP App + 选项禁用」三个条件同时成立才触发。- session-control-plane 过滤器补上了
unsupported-client。
零 P0/P1/P2,测试覆盖充分。
qwen-code-dev-bot
left a comment
There was a problem hiding this comment.
APPROVE — 审查版本 head f05832ee(提交前后两次读取一致)。
- 历史 Review:本 PR 没有正式 GitHub Review 记录;仅有 verify 报告(针对
f6a2297a,结论 merge-ready),其 F1(unsupported-client进入 bridge 状态 allow-list 未加用例)与 F2(reason 优先级重排未加用例)均被报告方明确标注为非阻塞覆盖缺口,不影响正确性、安全或兼容性,按 Suggestion 处理。最新提交f05832ee已为频道会话门禁补了对应用例。 - CI:head 上唯一 required check
Lint & Static (ubuntu-latest, Node 22.x)success;ubuntu 单测、no-AK 集成、web-shell E2E smoke、OpenTUI no-flicker、TUI parity、Managed Agent gate、linux x64/arm64 npm 安装、Node26/Bun 验收均 success。两个非 required 的Installed npm global and npx (darwin-x64/arm64)自 08:01Z 起排队未启动(同仓Test (macos-latest)也被 skip),属 macOS runner 基础设施问题,且本 PR 未触及打包路径。 - 独立检查未发现新 Critical:客户端门禁是唯一决策点,
bundledGeneratedUiActive/getBundledGeneratedUiStatus()的全部消费方(merged MCP servers、bundled skill 过滤、isBundledGeneratedUiServer、mcp-tool 可见性)都读同一状态;setMcpAppHostSupported位于共享的newSessionConfigInRuntimeContext,在setSessionSource之后、initialize()之前执行,new/load/resume 路径一致;daemon 频道会话由packages/cli/src/commands/channel/daemon-worker.ts传sourceType: 'channel',被rendersMcpApps()正确排除;新 reason 已同时加入BundledGeneratedUiStatus类型联合与session-control-plane.ts的 allow-list,仓库内不存在遗漏该枚举值的 reason→文案映射;告警抑制仅限setting-disabled与unsupported-client,setting-disabled仍优先于客户端判定。
|
Released in v0.26.3. |




What this PR does
Bundled Generated UI is now offered only to sessions whose client can render MCP Apps. The daemon's ACP bridge, whose sessions Web Shell and Desktop display, declares an MCP App host capability when it connects to the agent, and the agent marks those session configs as MCP App hosts before initialization. Every other session, including the interactive TUI,
qwen -pand other headless runs, editor ACP clients and channels, reports a newunsupported-clientreason and receives neither thegenerated_ui_showtool nor the writer skill. This includes channels the daemon runs: their sessions come through the daemon bridge, but carry thechannelsource, and their users read replies in the IM client.unsupported-clientis an expected state, so it does not produce a startup warning. This also removes the misleading "Bundled Generated UI is unavailable (unsupported-runtime)" line the daemon printed at every start even though its sessions had Generated UI enabled.Why it's needed
With
experimental.generatedUion, every trusted local session received the tool and skill. In clients that cannot show a card, the model still used them: a realqwen -prun (qwen3.8-max) loaded the skill, calledgenerated_ui_show, and finished with "the interactive splitter is ready, drag the sliders in the card above", although the terminal has no card. This blocks turning the experimental setting on by default, and #14019 requires that CLI/headless clients not receive a Web Shell-only experience.Reviewer Test Plan
How to verify
experimental.generatedUiin user settings in a trusted workspace.qwen -p "<a request to adjust a bill split interactively>" --output-format stream-json. The stream should contain nogenerated-uiskill load and nomcp__generated-ui__generated_ui_showcall, and the answer should not refer to a card.qwen serve, open Web Shell and make the same request. The session status reportsgeneratedUi: { version: 1, enabled: true }and the model can show the card.Unit coverage: core config (unsupported-client status for non-host clients and daemon-created channel sessions, no warning, setting-disabled still reported first, provider withheld), acpAgent (the setter is called before
initializeonly when the client advertisesqwen.mcpAppHost), acp-bridge handshake capabilities, and MCP client manager fixtures that now declare an MCP App host.Evidence (Before & After)
Same settings, model and prompt (
聚餐分账:菜品总价 924 元,6 个人平分。我想在对话里直接拖动/输入人数、优惠金额和服务费比例,实时看到总价和每人应付。不需要写文件。),qwen -p --output-format stream-json:e67cb4a8b4)skill generated-ui,tool_search,mcp__generated-ui__generated_ui_showWeb Shell sessions on the same daemon still report
generatedUi: { version: 1, enabled: true }and render the card.Targeted tests:
packages/coreconfig/skills/MCP suites (1480 passed),packages/acp-bridgebridge.test.ts(1023 passed),packages/cliacpAgent MCP App host and Goal proposal cases (9 passed).Tested on
Environment (optional)
Local bundle (
npm run build && npm run bundle), isolatedHOME,qwen servewith Web Shell andqwen -p, model qwen3.8-max.Risk & Scope
Linked Issues
Part of #14019.
中文说明
这个 PR 做了什么
内置 Generated UI 现在只提供给能渲染 MCP App 的客户端所在的会话。daemon 的 ACP bridge(Web Shell 和 Desktop 显示的就是它的会话)在连接 agent 时声明自己是 MCP App 宿主,agent 在初始化前把这些会话的配置标记为 MCP App 宿主。其他会话,包括交互式 TUI、
qwen -p及其他 headless 运行、编辑器 ACP 客户端和频道,都会报告新的unsupported-client原因,既拿不到generated_ui_show工具,也拿不到写作技能。unsupported-client是预期状态,因此不会产生启动警告。这也去掉了 daemon 每次启动都会打印的误导性日志 "Bundled Generated UI is unavailable (unsupported-runtime)"——实际上它的会话是启用了 Generated UI 的。为什么需要
开启
experimental.generatedUi后,每个受信任的本地会话都会拿到工具和技能。在无法显示卡片的客户端里,模型照样会用:一次真实的qwen -p运行(qwen3.8-max)加载了技能、调用了generated_ui_show,最后回复"已生成交互式分账计算器,可以直接在上方卡片里拖动滑块",而终端里根本没有卡片。这会阻碍把这个实验设置默认开启;#14019 也要求 CLI/headless 客户端不能收到只在 Web Shell 可用的体验。审阅者测试计划
如何验证
experimental.generatedUi。qwen -p "<一个想交互式调整分账的请求>" --output-format stream-json。输出中不应有generated-ui技能加载,也不应有mcp__generated-ui__generated_ui_show调用,回答也不应提到卡片。qwen serve,在 Web Shell 中提同样的请求。会话状态报告generatedUi: { version: 1, enabled: true },模型可以展示卡片。单元测试覆盖:core 配置(unsupported-client 状态、无警告、setting-disabled 仍优先报告、不注入 provider)、acpAgent(只有客户端声明
qwen.mcpAppHost时才在initialize之前调用 setter)、acp-bridge 握手能力,以及 MCP client manager 中现在需要声明 MCP App 宿主的测试夹具。证据(修改前后)
相同设置、模型和提示词(
聚餐分账:菜品总价 924 元,6 个人平分。我想在对话里直接拖动/输入人数、优惠金额和服务费比例,实时看到总价和每人应付。不需要写文件。),qwen -p --output-format stream-json:e67cb4a8b4)skill generated-ui、tool_search、mcp__generated-ui__generated_ui_show同一个 daemon 上的 Web Shell 会话仍报告
generatedUi: { version: 1, enabled: true }并能渲染卡片。定向测试:
packages/core配置/技能/MCP 相关测试(1480 通过)、packages/acp-bridgebridge.test.ts(1023 通过)、packages/cliacpAgent 中 MCP App 宿主与 Goal proposal 用例(9 通过)。测试平台
环境(可选)
本地 bundle(
npm run build && npm run bundle)、隔离的HOME、qwen serve配合 Web Shell,以及qwen -p,模型 qwen3.8-max。风险与范围
关联 Issue
属于 #14019 的一部分。