feat(daemon): add shared UI transcript layer - #4328
Conversation
📋 Review SummaryThis PR adds a shared daemon UI layer for web chat/terminal clients, providing typed daemon event normalization, transcript state management, and React bindings through 🔍 General FeedbackPositive aspects:
Architectural observations:
Recurring themes:
🎯 Specific Feedback🔴 Critical
🟡 High
🟢 Medium
🔵 Low
✅ Highlights
Reviewer Verification Steps: # 1. Verify SDK daemon UI tests pass
cd packages/sdk-typescript && npx vitest run test/unit/daemonUi.test.ts
# 2. Verify type checking passes
cd packages/sdk-typescript && npx tsc --noEmit
# 3. Verify WebUI builds
cd packages/webui && npm run build
# 4. Verify browser-safe export (check for Node imports in daemon UI bundle)
grep -r "require\('fs'\)\|require\('path'\)\|from 'node:" packages/sdk-typescript/src/daemon/ui/
# 5. Smoke test the daemon import
node -e "import('@qwen-code/sdk/daemon').then(m=>console.log('exports:', Object.keys(m).sort().join(', ')))" |
ceb2ae5 to
94d66e0
Compare
|
Generated by GPT-5 model. Follow-up from the review pass:
Review triage / false positives:
Validation run locally after the amend:
|
94d66e0 to
b3e102b
Compare
b3e102b to
fe77066
Compare
chiga0
left a comment
There was a problem hiding this comment.
Multi-round architectural + correctness + performance review on the shared daemon UI transcript layer. Findings as inline comments anchored to specific lines.
Organized by priority:
P0 (blocking)
normalizer.ts:173—_meta.usagepiggyback forassistant.doneis fragilenormalizer.ts:241—toolCallIdfallback toevent.idcreates phantom tool blockstranscript.ts:345—cloneTranscriptStatedoes full deep clone on every event (perf)transcript.ts:152—getBlockByIdlinear scan on streaming hot pathtranscriptAdapter.ts:139—normalizeToolStatusdefault →'failed'breaks forward-compattranscriptAdapter.ts:151—cancelled→'completed'loses informationtranscriptAdapter.ts:115— error block rendered as assistant message with[System Error]prefix is a UX antipatternsdk-typescript/src/index.ts:14— UI re-exports pollute Node-targeted main entry
P1 (strongly recommend)
DaemonSessionProvider.tsx:256— single context value causes unnecessary re-rendersstore.ts:53— dispatch immediately notifies, no microtask batchingtranscript.ts:289— silent fallback to status when permission block was trimmedwebui/package.json:47—"@qwen-code/sdk": "0.1.7"exact-pin causes monorepo skewbuild.js:131—assertBrowserSafeBundleonly string-scans, no bundle size cap
Direction is right — sinking shared UI primitives (normalizer / reducer / store / terminal sanitization) into SDK is exactly what 02-architectural-decisions.md §8 calls for. Concerns are around concrete implementation: reducer performance under streaming, UX antipatterns in the webui adapter, and main-entry pollution.
Not posted as P0 but worth flagging: zero React component tests (DaemonSessionProvider.tsx 341 LOC, 0 tests) and zero transcriptAdapter.ts tests (159 LOC mapping logic). The 5 existing unit tests cover the reducer happy paths but leave the consumer surfaces entirely uncovered. Recommend adding before next PR builds on this.
Deleted DaemonTuiAdapter.{ts,test.ts} (-1874 LOC) is the right call — spike-only, superseded by PR#4202.
Generated with assistance from Claude Opus 4.7 (claude-opus-4-7) — code references verified against PR head fe77066d.
fe77066 to
feddf8e
Compare
99f2952 to
5f48a92
Compare
|
Generated by GPT-5 model. Latest review pass handled in
Local validation run:
|
5f48a92 to
5532d6c
Compare
wenshao
left a comment
There was a problem hiding this comment.
Additional findings (no specific diff line):
- [Critical]
DaemonSessionProvider.tsx(383 lines) has zero test coverage. Handles connection lifecycle, exponential backoff reconnect, event stream loop, error dispatch, and action bindings (sendPrompt, cancel, setModel, respondToPermission). - [Critical] 7 daemon event types in
normalizer.tsuntested:model_switched,model_switch_failed,session_closed,client_evicted,slow_client_warning,stream_error, and unknown-event fallback. - [Critical]
suppressOwnUserEchoandincludeRawEventnormalizer options have zero test coverage.
— DeepSeek/deepseek-v4-pro via Qwen Code /review
PR #4328 作为"统一渲染层"的功能完备度审视跳出单点 issue,从"统一事件 + 状态层是否真的让各 render 端不再感知 provider/模型差异"这个核心目标评估。 结论先放上面:当前 PR 提供的不是"完备的统一层",而是一个 v1 的薄壳。基础事件/状态/reducer/store 都对,但 事件分类只覆盖 daemon 已有事件的 ~50%,时间语义不标准,provider 差异在协议下层已基本抹平但向上还漏 3 处。完成度估算约 55%。 一、事件类型覆盖 vs daemon 实际产出 — 覆盖率约 50%PR 的 已正确归一化
未归一化(全部走
|
| Daemon 事件 | Wave | 后果 |
|---|---|---|
mcp_budget_warning |
W3 PR 14b | 关键 UI 看不到 MCP 预算告警 |
mcp_child_refused_batch |
W3 | 关键 MCP 失败原因不可见 |
mcp_server_restarted / _restart_refused |
W4 PR 17 | UI 不更新 MCP 状态 |
agent_changed |
W4 PR 16 | 切 agent 后 UI 不刷 |
approval_mode_changed |
W4 PR 17 | 关键 mode badge 不更新 |
memory_changed |
W4 PR 16 | memory dialog 不刷新 |
tool_toggled |
W4 PR 17 | 工具开关 UI 不同步 |
workspace_initialized |
W4 PR 17 | 初始化完成无信号 |
session_metadata_updated |
W2.5 PR 11 | session title 不更新 |
auth_device_flow_started/_throttled/_authorized/_failed/_cancelled |
W4 PR 21 | 关键 5 个 OAuth 设备码事件全丢到 debug |
available_commands_update |
Stage 1 | 关键 TUI 命令补全不能动态更新 |
这 12+ 类事件对 UI 当前是不透明 JSON 字符串。 任何渲染层想用都要跳过抽象层去看原始 rawEvent.data,统一层就形同虚设。
二、事件 schema 标准化程度
PR 的归一化做了形状统一,信息维度还差:
| 维度 | 当前 | 缺什么 |
|---|---|---|
tool.update.toolName |
自由 string | 没有 namespace — mcp:<server>:<tool> vs 内置工具 vs subagent 工具混在一起 |
tool.update.toolKind |
自由 string | 没有 closed enum;UI 要 dispatch file.edit/bash/search/web.fetch 全靠 string match |
tool.update.status |
自由 string | wenshao 的 DL8cA / DNHW4 评论指出 cancelled 处理就是这种自由文本带来的问题 |
permission.resolved.outcome |
自由 string selected:allow |
不是 typed 结构 |
error.text + recoverable |
free string + boolean | 丢失 daemon 自带的 7 值 errorKind 闭枚举(missing_binary/blocked_egress/auth_env_error/init_timeout/protocol_error/missing_file/parse_error)— UI 没法分类型显示"重试 auth"按钮 vs "检查路径"按钮 |
assistant.done.reason |
free string | provider 各自的 stopReason(end_turn/max_tokens/cancelled/tool_use)直接透传字符串 |
| Token usage / cost | 完全没有事件 | UI 无法显示 per-turn 成本 |
Progress(长任务进度) |
完全没有 | tool 跑 10 分钟,UI 只有"in_progress",没有进度 |
Citation / file reference |
完全没有 | 模型说"参考 docs/foo.md",UI 没法做 clickable |
Subagent 嵌套 |
完全没有 | agent 调 subagent,事件平铺在 transcript 里,无父子结构 |
三、时间定义是否标准 — 不标准,有具体风险
当前实现(transcript.ts)
createdAt: state.now,
updatedAt: state.now,其中 state.now = opts.now ?? Date.now(),在 cloneTranscriptState 时确定。
三个具体问题
- 客户端时钟 —
Date.now()在浏览器/CLI 上是本地时钟,跨客户端会漂移。同一个 session 两个客户端看,"5 分钟前"可能差几十秒到几分钟。 - 不是 daemon 权威时间 — daemon 那边的 SSE envelope 应该携带 server-side 时间戳,PR 没用。
- 没有 monotonic 保证 —
eventId是 daemon 单调的(Math.max守护),但createdAt不是。lateral reconnect 拿到 replay 事件时,replay 的state.now比原始事件晚几秒,UI 时间轴出现"未来的事件比现在还新",createdAtordering 不可靠。
应该长这样
interface DaemonTranscriptBlockBase {
id: string;
kind: ...;
eventId?: number; // 已有,daemon 单调 cursor — 用这个排序
serverTimestamp?: number; // 缺 — daemon 权威时间,从 SSE envelope 取
clientReceivedAt: number; // = 当前的 createdAt;用于"X 分钟前"展示但不参与排序
updatedAt: number; // 客户端时钟,允许 drift
}修复路径:
- daemon 侧 SSE envelope 加
_meta.serverTimestamp(需要 daemon PR) - SDK normalizer 提取到
DaemonUiEventBase.serverTimestamp - reducer 同时存
serverTimestamp(权威) 和clientReceivedAt(本地);ordering 用eventId优先,fallbackserverTimestamp - UI 时间展示用
serverTimestamp+ 客户端时区调整
当前 PR 的 state.now 模型对单客户端够用,对多客户端协作会出 bug。
四、Provider / 模型兼容性 — 大部分在 daemon 下层抹平,但向上还漏 3 处
已经抹平的(qwen --acp child 内做的)
| 差异点 | 抹平方式 |
|---|---|
OpenAI delta.content 字符串 vs Anthropic content 数组 |
都拆成 ACP agent_message_chunk + text content block |
OpenAI tool_calls 带 JSON-string arguments vs Anthropic tool_use 带对象 |
都转成 ACP tool_call 带结构化 rawInput |
Gemini parts: [Part] 多模态 |
ACP content blocks(text / image / audio / pdf / resource) |
| 各家 stop_reason 名字不同 | ACP stopReason 字符串收敛 |
| Tool result 各家返回形态不同 | ACP tool_call_update.rawOutput |
向上没抹平、UI 需要再处理的 3 处
1. 思考 / reasoning 信息 lossy
- Anthropic thinking blocks:有
thinkingtext +signature(密码学签名)+ 支持redacted_thinking - DeepSeek
reasoning_content:单独字段 - OpenAI o1 reasoning:hidden,只暴露 token count
- Qwen native:类 Anthropic
PR 全部归一化成 thought.text.delta 纯 text 累积。丢失 signature、redacted 标记、多 thinking 块的边界。
后果:做 chat history 上传给后续 turn 时,签名丢了,Anthropic 会拒绝(signature is required)。所以 UI 不能直接用 thought block 反向构造 API 调用 — 必须靠 daemon 内部 ChatRecordingService 保管完整原始。这条强约束需要在文档里写清楚。
2. Tool 命名空间不区分
ACP tool_call.name 是平的 string。UI 拿到:
Bash(内置)Edit(内置)mcp__github__create_issue(MCP server 拼名)Read(内置)但某 MCP 也可能提供同名Read
冲突时 UI 没办法区分。应该 daemon 侧在 tool_call event 里加 provenance: 'builtin' | 'mcp' | 'subagent' + serverId?: string。
3. 多模态 content 只取 text
utils.ts:getTextContent:
const text = value['text'];
return typeof text === 'string' ? text : '';只看 text 字段。如果 daemon emit 的 chunk 是 {type: 'image', source: {...}} 或 {type: 'resource', uri: '...'},直接被 normalizer 丢弃。多模态对话从 UI 视角看就消失了。
修复:getContent 应返回 discriminated union {kind: 'text'|'image'|'audio'|'resource', ...},reducer 把多模态块追加到 assistant block 的 parts: ContentPart[] 而不是 text: string。
五、Reducer / 状态机的设计缺漏
| 维度 | 当前 | 缺什么 |
|---|---|---|
| Tool ↔ shell 关联 | shell output 单独成 block | tool 调了 bash 产生 shell output 这一对应关系在 transcript 上断开 — UI 显示两个独立 block,视觉不连贯 |
| In-flight 状态 | 无 currentTool 字段 |
UI 想做"正在运行 X tool" header,要自己扫 blocks |
| Subagent 嵌套 | 平铺 | 主 agent 派 subagent 跑 5 个工具,5 个工具事件混在主 transcript |
| Plan / 多步任务 | 无 | Anthropic plan mode 的 step 1/2/3,没有 step kind |
| Cancellation propagation | assistant.done.reason = 'cancelled' |
被 cancel 时正在跑的 tool block status 不一定收到 'cancelled' update — 可能永远停在 'in_progress' |
| Token usage | 无 event | 见上文,完全缺失 |
| Progress | 无 event | 长任务无进度反馈 |
| Active client typing(多 client 协作) | 无 | 04-deployment-and-client.md 提到 P1 拓扑的"X is typing"没实现 |
六、Render 侧契约 — 只覆盖 terminal,web/IDE 是空的
PR 只提供:
daemonUiEventToTerminalText(event) // 单事件 → ANSI 文本
transcriptBlockToTerminalText(block) // 单块 → ANSI 文本缺:
daemonBlockToMarkdown(block)— web markdown render 共用daemonBlockToHtml(block, opts)— 直接用于 SSRdaemonBlockToPlainText(block)— copy-paste / 日志daemonToolPreviewToReactElement(preview)— React 组件层共享
后果:transcriptAdapter.ts 159 行的 daemonTranscriptToUnifiedMessages 是 webui 自己写的;TUI 又会单独写一遍;IDE 又写一遍。conformance 没保证。
这正是我前面 review #3803/#4175 时反复提的 adapter conformance test 缺位的根因 — 共享的 render-layer contract 还没下沉。
七、Tool preview taxonomy 极度不足
当前 4 种 preview kind:
type DaemonToolPreview =
| { kind: 'ask_user_question'; questions: ... }
| { kind: 'command'; command: string; cwd?: string }
| { kind: 'key_value'; rows: ... }
| { kind: 'generic'; summary?: string };实际生态需要的至少还有:
file_diff— edit before/after,带 patchfile_read— path + line rangeweb_fetch— url + status + content-typesearch— query + result count + top resultsmcp_invocation— server name + tool + structured argsimage_generation— preview thumbnailcode_block— syntax-highlighted code(为 markdown 渲染省心)tabular— 结构化 table 结果subagent_delegation— 派给哪个 agent + 任务
没有这些,UI 落到 key_value 或 generic 兜底,渲染粗糙;或者 UI 自己解析 rawInput 又重新引入 provider 差异感知 — 跟"统一层"目标矛盾。
八、综合评估
完备度评分
| 维度 | 评分 | 关键缺口 |
|---|---|---|
| 事件 type 覆盖率 | C+ | 12+ daemon 事件未归一化,全落到 debug |
| 事件 schema 信息度 | C | tool/error 没 closed enum;errorKind 闭枚举没穿透到 UI |
| 时间语义 | D | 客户端时钟 + 无 server timestamp;ordering 不稳 |
| Provider 差异隐藏 | B- | 协议下层抹了大头;reasoning signature / 多模态 / tool namespace 三处漏 |
| Reducer 状态机 | B | 基础对,缺 in-flight / subagent 嵌套 / progress / token usage |
| Render 契约 | D | 只 terminal 有 helper,web/IDE 各自写 |
| Tool preview 分类 | D+ | 4 种远不够,缺 diff / search / mcp / 多模态 |
"UI 端不再需要感知 provider 和模型差异" 这个目标 — 当前 PR 完成度 ~55%
需要 daemon 侧补
- SSE envelope 加
_meta.serverTimestamp - Tool call event 加
provenance+serverId - 现有 12+ 事件正式 emit 到 SDK(可能已经 emit,只是 UI 端没归一化)
需要 SDK UI 侧补
- 12+ daemon event 归一化(尤其
available_commands_update/mcp_*/approval_mode_changed/auth_device_flow_*) - 多模态 content 不再丢弃,改 discriminated union
- Tool preview 至少加 5 种(diff / search / mcp / file_read / web_fetch)
- ErrorKind 闭枚举穿透到
DaemonUiErrorEvent - 时间 schema 改
eventId排序 +serverTimestamp显示 daemonBlockToMarkdown/...ToHtml/...ToPlainText三个 helper(render 契约)
需要 reducer 补
- Tool ↔ shell output 关联(同 toolCallId)
currentTool/currentSubagent顶层指针- Cancellation 时 in-flight tool block 强制改
cancelled状态 - 嵌套 transcript(subagent)
九、建议:拆 5 个 follow-up PR
不建议在本 PR 内做完(已经 +2674/-1973 + 21 个 wenshao 评论)。建议拆:
| Follow-up | 范围 | 估算 LOC |
|---|---|---|
| PR-A | 补 12+ event 归一化 + errorKind 穿透 + tool provenance + available_commands_update typed event |
~600 |
| PR-B | 时间 schema 改造(daemon + SDK 配合) | ~200 |
| PR-C | 多模态 content + tool preview taxonomy 扩展 | ~500 |
| PR-D | render 契约下沉(...ToMarkdown / ...ToHtml / ...ToPlainText)+ adapter conformance test |
~800 |
| PR-E | reducer 高级状态机(subagent / progress / current tool / cancellation propagation) | ~600 |
当前 PR 当 v1 ship 是合理的(只要修完 wenshao 的 [Critical]),完整"统一层"的承诺通过 PR-A 到 PR-E 兑现。
Generated with assistance from Claude Opus 4.7 (claude-opus-4-7) — analysis cross-referenced against daemon_mode_b_main HEAD 066cab229 (daemon event taxonomy in events.ts) and PR #4328 HEAD 5f48a92e3.
5532d6c to
ba44e08
Compare
ba44e08 to
f338454
Compare
|
Generated by GPT-5 model. Full comment sweep update for #4328:
Validation after the latest push:
|
Maintainer test report — PR #4328Built and validated locally in a dedicated Heads up on PR base: this PR targets Environment
Results
Browser-safety verification (the PR's central architectural claim)$ node -e "import('@qwen-code/sdk/daemon').then(m => console.log(Object.keys(m).sort().join(', ')))"Resolves to a single-file ESM at Byte-level scan for Native TUI defaults — confirmed untouchedThe PR removes
Lint tooling issue — pre-existing, not a code defect
Root cause: nested Risk assessmentLow-medium risk for the feature-branch merge into
Mergeability statusGitHub reports Not covered locally
Reproducegit fetch origin pull/4328/head:pr-4328 && git checkout pr-4328
npm install # adds @qwen-code/sdk dep + daemon subpath
npm run build && npm run typecheck # all 4 workspaces clean
cd packages/sdk-typescript && npx vitest run --no-coverage # 493/493
cd ../webui && npx vitest run --no-coverage # 53/53
cd ../cli && npx vitest run --no-coverage # 6932 pass / 9 skip / 0 fail
cd ../..
node -e "import('@qwen-code/sdk/daemon').then(m => console.log(typeof m.DaemonClient, typeof m.normalizeDaemonEvent))"
grep -c "node:" packages/sdk-typescript/dist/daemon/index.{js,cjs} # both 0
grep -rc DaemonTuiAdapter dist/cli.js dist/chunks/*.js | grep -v ':0$' # emptyRecommendation: safe to merge into 🇨🇳 中文版本(点击展开)维护者测试报告 — PR #4328在专属 关于 PR base 的注意事项: 这条 PR 的 base 是 环境
结果
浏览器安全验证(PR 的核心架构主张)$ node -e "import('@qwen-code/sdk/daemon').then(m => console.log(Object.keys(m).sort().join(', ')))"解析到一个单文件 ESM 对 Native TUI 默认行为 — 已确认未受影响PR 删了
Lint 工具问题 — 预先存在,不是代码缺陷
根因:嵌套的 风险评估合入
Mergeability 状态GitHub 报 本地未覆盖
复现命令git fetch origin pull/4328/head:pr-4328 && git checkout pr-4328
npm install # 加 @qwen-code/sdk 依赖 + daemon 子路径
npm run build && npm run typecheck # 4 个 workspace 都干净
cd packages/sdk-typescript && npx vitest run --no-coverage # 493/493
cd ../webui && npx vitest run --no-coverage # 53/53
cd ../cli && npx vitest run --no-coverage # 6932 pass / 9 skip / 0 fail
cd ../..
node -e "import('@qwen-code/sdk/daemon').then(m => console.log(typeof m.DaemonClient, typeof m.normalizeDaemonEvent))"
grep -c "node:" packages/sdk-typescript/dist/daemon/index.{js,cjs} # 两个都 0
grep -rc DaemonTuiAdapter dist/cli.js dist/chunks/*.js | grep -v ':0$' # 空结论:合入 |
| ...(rawInput !== undefined | ||
| ? { details: capDetails(stringifyRedactedJson(rawInput)) } | ||
| : rawOutput !== undefined | ||
| ? { details: capDetails(stringifyRedactedJson(rawOutput)) } |
There was a problem hiding this comment.
[Suggestion] The details field for tool-output-only updates changed from getOutputText(rawOutput) (which extracts .text/.stdout/.stderr as human-readable text) to stringifyRedactedJson(rawOutput) (which JSON-stringifies the entire redacted object). For structured tool output like {text: "OK", status: 200}, the old details would show "OK" while the new code produces '{\n "text": "OK",\n "status": 200\n}' — a display quality regression for UI panels rendering the details preview.
Additionally, rawOutput is already the result of redactSensitiveFields(rawOutputSource) at line ~258, so stringifyRedactedJson performs a redundant second redactSensitiveFields traversal on already-redacted data.
| ? { details: capDetails(stringifyRedactedJson(rawOutput)) } | |
| ? { details: capDetails(stringifyJson(rawInput)) } | |
| : rawOutput !== undefined | |
| ? { details: capDetails(getOutputText(rawOutput)) } |
— qwen-latest-series-invite-beta-v34 via Qwen Code /review
| await flushPromises(); | ||
| }); | ||
|
|
||
| // 410 path: SHOULD have retried at least once (so connect succeeded on |
There was a problem hiding this comment.
[Suggestion] This test is now parameterized over [404, 410] (line 776), but the comment still says "410 path". When the test runs with status=404, this comment is misleading. Consider updating to "session-not-found path" or "404/410 path".
| // 410 path: SHOULD have retried at least once (so connect succeeded on | |
| // session-not-found path: SHOULD have retried at least once (so connect succeeded on |
— qwen-latest-series-invite-beta-v34 via Qwen Code /review
| ...(rawInput !== undefined | ||
| ? { details: capDetails(stringifyRedactedJson(rawInput)) } | ||
| : rawOutput !== undefined | ||
| ? { details: capDetails(stringifyRedactedJson(rawOutput)) } |
There was a problem hiding this comment.
[Suggestion] The details field for tool-output-only updates changed from getOutputText(rawOutput) (which extracts .text/.stdout/.stderr as human-readable text) to stringifyRedactedJson(rawOutput) (which JSON-stringifies the entire redacted object). For structured tool output like {text: "OK", status: 200}, the old details would show "OK" while the new code produces '{\n "text": "OK",\n "status": 200\n}' — a display quality regression for UI panels rendering the details preview.
Additionally, rawOutput is already the result of redactSensitiveFields(rawOutputSource) at line ~258, so stringifyRedactedJson performs a redundant second redactSensitiveFields traversal on already-redacted data.
| ? { details: capDetails(stringifyRedactedJson(rawOutput)) } | |
| ? { details: capDetails(stringifyJson(rawInput)) } | |
| : rawOutput !== undefined | |
| ? { details: capDetails(getOutputText(rawOutput)) } |
— qwen-latest-series-invite-beta-v34 via Qwen Code /review
| await flushPromises(); | ||
| }); | ||
|
|
||
| // 410 path: SHOULD have retried at least once (so connect succeeded on |
There was a problem hiding this comment.
[Suggestion] This test is now parameterized over [404, 410] (line 776), but the comment still says "410 path". When the test runs with status=404, this comment is misleading. Consider updating to "session-not-found path" or "404/410 path".
| // 410 path: SHOULD have retried at least once (so connect succeeded on | |
| // session-not-found path: SHOULD have retried at least once (so connect succeeded on |
— qwen-latest-series-invite-beta-v34 via Qwen Code /review
|
ignore [Suggestion] comment to avoid endless update and make pr too big. Can you please approve this pr, thank you. @wenshao @yiliang114 @doudouOUC |
Summary
@qwen-code/webui.@qwen-code/sdk/daemonexport shape, and confirmation that native local TUI / ACP / channel / IDE defaults remain untouched.Validation
npm run buildreports an existing VSCode companion lint warning ineditorGroupUtils.tsbut exits successfully.true true.Scope / Risk
@qwen-code/webuinow has an optional dependency on the SDK daemon subpath; the subpath is built as browser-safe ESM to avoid pulling Node-only SDK query/transport code into browser bundles./webapp or full browser UI parity. It provides the shared UI layer those clients should consume.Testing Matrix
Testing matrix notes:
Linked Issues / Bugs
Related to #3803 and #4175.