feat(sdk/daemon-ui): unified completeness follow-up to #4328#4353
Conversation
📋 Review SummaryThis PR delivers a comprehensive follow-up to #4328, implementing a unified daemon UI layer across 5 coordinated commits (PR-A through PR-E). The changes introduce typed event schemas, server-side timestamps, state machine tracking, tool preview taxonomy, and render contract helpers. Test coverage is strong (77/77 passing), and the implementation demonstrates solid architectural thinking around forward-compatibility and cross-client consistency. 🔍 General FeedbackPositive aspects:
Architectural decisions:
Potential concerns:
🎯 Specific Feedback🟡 High
🟢 Medium
🔵 Low
✅ Highlights
|
wenshao
left a comment
There was a problem hiding this comment.
A deterministic typecheck also reports TS4111 in packages/webui/src/components/toolcalls/ShellToolCall.tsx for existing Record<string, unknown> dot-property accesses (description / command). Those lines are not part of the PR diff, so I am not posting them as inline comments, but the changed-file typecheck will still need to be clean before merge.
— gpt-5.5 via Qwen Code /review
…ete (PR-F) Closes the "5 additional preview kinds" item in PR QwenLM#4353's TODO §A (SDK-only work). ## New preview kinds (8 → 13) - `code_block` — `{ language?, code, origin? }` — REPL / formatter / generator output, fenced as `\`\`\`<language>` in markdown - `search` — `{ query, resultCount?, top? }` — grep / ripgrep / find / glob results with up to 5 top hits - `tabular` — `{ columns, rows, totalRows? }` — structured table output (50-row cap with `totalRows` truncation indicator); supports both `columns: string[] + rows: unknown[][]` explicit shape and legacy `data: Array<Record<>>` shape (auto-infers columns from first row) - `image_generation` — `{ prompt, thumbnailUrl?, model? }` — dall-e / diffusion / imagen / flux / sora style tools - `subagent_delegation` — `{ agentName, task, parentDelegationId? }` — Anthropic-style Task tool and similar sub-agent dispatchers ## Detector priority Order matters — most specific wins. New detectors slot in between `mcp_invocation` and `file_diff`: ``` mcp_invocation > subagent_delegation > search > image_generation > file_diff > file_read > web_fetch > code_block > tabular > command > key_value > generic ``` Rationale: subagent / search / image generation are most discriminable (distinct toolName patterns); file ops next; code_block / tabular last because their shapes (`code:`, `columns:`) can appear in other tools. ## Render projections Both `daemonToolPreviewToMarkdown` and the plain-text rendering paths extended with cases for all 5 new kinds: - code_block: fenced markdown code block with language tag - search: bold header + GFM bullet list of top results - tabular: GFM pipe table with header / separator / body / truncation hint - image_generation: bold header + blockquoted prompt + embedded markdown image (URL sanitization respected via `sanitizeUrls` opt) - subagent_delegation: bold delegate-arrow header + blockquoted task + optional parent delegation reference ## Test coverage (91/91 pass, +14 new) - Each detector with positive case - Detector priority verified: subagent_delegation wins over file_diff when toolName='Task' has both subagent + file-edit fields - Tabular row cap (50) + totalRows stamping for truncated data - Legacy data: Array<Record<>> auto-column inference - Each render projection with structural assertions (markdown table format, image embed, bullet lists) ## Roadmap PR-F of the unified follow-up to PR QwenLM#4328. Brings the preview taxonomy to 13 kinds covering: file ops (3), web (1), code/data (2), media (1), agent control (2 — ask_user_question + subagent_delegation), MCP (1), search (1), generic fallbacks (2). Generated with AI Co-authored-by: Claude Opus 4.7 <[email protected]>
…PR-G) Closes the "Adapter conformance test framework" item in PR QwenLM#4353's TODO §A. Lets any daemon-ui adapter (TUI / web / IDE / channel / mobile) validate that it projects a fixed corpus of daemon SSE event streams to the same semantic shape — catches projection drift before it reaches users. ## API surface ```ts interface DaemonUiAdapterUnderTest { reduce(events: readonly DaemonUiEvent[]): unknown; renderToText(state: unknown): string; } interface DaemonUiConformanceFixture { name: string; description: string; envelopes: DaemonEvent[]; // raw daemon envelopes expectedContains: string[]; // phrases the rendered text MUST contain expectedAbsent?: string[]; // phrases that MUST NOT appear normalizeOptions?: { ... }; // forward-compat normalize opts } runAdapterConformanceSuite(adapter, opts?): ConformanceSuiteResult DAEMON_UI_CONFORMANCE_FIXTURES: ReadonlyArray<DaemonUiConformanceFixture> ``` ## Design **Format-agnostic assertion**: adapters can render to ANSI / HTML / markdown / JSX — the framework only inspects plain text via `renderToText`. Catches semantic divergence (missing user message, wrong tool status, leaked secret) without forcing identical formatting. **Embedded fixture corpus** (no fs reads — works in browser bundle): - `simple-chat` — user/assistant streaming flow - `tool-call-lifecycle` — running → completed transition - `file-edit-diff` — file_diff preview surfacing - `mcp-invocation` — MCP serverId/toolName extraction via heuristic - `permission-lifecycle` — request + resolved with outcome - `mcp-budget-warning` — Wave 3 event (adapter must observe but rendering is its choice) - `cancellation-propagates` — tool block status flows - `malformed-payload-redaction` — uses `includeRawEvent: true` to verify even a debug-mode adapter doesn't leak `token: secret-do-not-leak` - `auth-device-flow-success` — Wave 4 OAuth events - `available-commands-typed-event` — PR-A upgrade from status text Per-fixture `expectedContains` and `expectedAbsent` describe the content contract independently of format. ## Suite result ```ts { passed: number, failed: ConformanceFailure[], // each carries missing + leaked + excerpt total: number, } ``` **Does not throw** — caller asserts on `result.failed` so adapter test suites can produce per-fixture diagnostics rather than a single opaque exception. ## Filter options `only` / `skip` allow targeted runs during adapter development: ```ts runAdapterConformanceSuite(myAdapter, { only: ['simple-chat'] }); runAdapterConformanceSuite(myAdapter, { skip: ['cancellation-propagates'] }); ``` ## Test coverage (97/97 pass, +6 new) - SDK reference adapter (reducer + markdown render) passes all fixtures - SDK reference adapter (reducer + plainText render) also passes - Buggy adapter (empty string output) fails every fixture with non-empty `expectedContains` - Buggy adapter (raw event dump via JSON.stringify) caught by redaction fixture's `expectedAbsent` - `only` filter narrows to a single fixture - `skip` filter excludes named fixtures from the corpus ## Usage from adapter authors ```ts // In your adapter's test file import { runAdapterConformanceSuite } from '@qwen-code/sdk/daemon'; import { reduceForTui, renderTuiState } from './my-tui-adapter'; it('TUI adapter conforms to daemon UI corpus', () => { const result = runAdapterConformanceSuite({ reduce: reduceForTui, renderToText: renderTuiState, }); expect(result.failed).toEqual([]); }); ``` ## Roadmap PR-G of the unified follow-up to PR QwenLM#4328. The corpus is intentionally small (10 fixtures) but extensible — adapter authors can submit new fixtures via additions to `DAEMON_UI_CONFORMANCE_FIXTURES` to lock in regression coverage for edge cases their adapter encountered. Generated with AI Co-authored-by: Claude Opus 4.7 <[email protected]>
…act (PR-H) Closes the "WebUI transcriptAdapter migration" item in PR QwenLM#4353's TODO §A. Validates the PR-D render contract end-to-end on the real WebUI consumer. ## Migration approach — additive opt-in `daemonTranscriptToUnifiedMessages(blocks, options?)` gains a new options parameter: ```ts interface DaemonTranscriptAdapterOptions { useMarkdown?: boolean; // default: false enrichToolDetailsWithPreview?: boolean; // default: false } ``` Defaults preserve legacy behavior — existing callers see no change. ## What `useMarkdown: true` does For `user` / `assistant` / `thought` blocks, content is projected via SDK's `daemonBlockToMarkdown` instead of raw sanitized text. The WebUI's markdown renderer (markdown-it) then gets: - `**You**\n\n<content>` for user blocks (bold "You" label) - Raw text for assistant blocks (markdown formatting in agent output passes through cleanly) - `> *thought:* <text>` blockquote for thought blocks ## What `enrichToolDetailsWithPreview: true` does For `tool` blocks, `rawOutput` is replaced with `daemonToolPreviewToMarkdown(block.preview)`. This lets WebUI surfaces without per-preview-kind React components still display: - `file_diff` as a fenced unified diff - `mcp_invocation` as `server::tool` with args summary - `tabular` as GFM pipe table - `search` as bullet list with match count - `image_generation` as embedded markdown image - `subagent_delegation` as delegate arrow + task quote Renderers with per-kind components should leave this opt-out. ## SDK daemon root index.ts re-exports `packages/sdk-typescript/src/daemon/index.ts` was missing exports for PR-D / PR-F / PR-G / PR-B / PR-E surface — WebUI's `@qwen-code/sdk/daemon` import path uses the daemon root, not the ui/ sub-index. Added 15+ re-exports so consumers don't need to use the longer `@qwen-code/sdk/daemon/ui/index.js` path. Now exported from `@qwen-code/sdk/daemon` root: - `daemonBlockToMarkdown` / `daemonBlockToHtml` / `daemonBlockToPlainText` - `daemonToolPreviewToMarkdown` - `extractContentPart` + `DaemonUiContentPart` type - `formatBlockTimestamp` + `selectTranscriptBlocksOrderedByEventId` - `selectCurrentTool` / `selectApprovalMode` / `selectToolProgress` - `runAdapterConformanceSuite` + `DAEMON_UI_CONFORMANCE_FIXTURES` - All associated types ## Test fixture migration `webui/src/daemon/transcriptAdapter.test.ts` mock blocks updated to include `clientReceivedAt` (required field added in PR-B). Mechanical change — every `createdAt: N` test fixture gets a matching `clientReceivedAt: N`. ## Validation - WebUI `npm run typecheck` — clean - SDK `npm run typecheck` — clean - SDK `vitest run test/unit/daemonUi.test.ts` — 97/97 pass - WebUI transcriptAdapter test fixtures typecheck against updated DaemonTranscriptBlockBase schema ## Roadmap PR-H of the unified follow-up to PR QwenLM#4328. Closes the WebUI migration gap in TODO §A. Generated with AI Co-authored-by: Claude Opus 4.7 <[email protected]>
Closes the final "Documentation" item in PR QwenLM#4353's TODO §A. Brings the unified daemon UI surface to ~95% SDK-side completion. ## Files added - `docs/developers/daemon-ui/README.md` — full API reference - Three-layer model (normalizer → reducer → render helpers) - Quick start with idiomatic event-loop pattern - Event taxonomy (28+ types categorized: chat-stream / session-meta / workspace / auth device-flow) - Render contract cookbook (markdown / HTML / plainText) - Tool preview taxonomy (13 kinds with use cases) - State selectors (currentTool / approvalMode / toolProgress / ordering) - Cancellation propagation explanation - Time semantics (eventId > serverTimestamp > clientReceivedAt precedence) - Adapter conformance usage - ErrorKind dispatch pattern - Tool provenance dispatch pattern - Forward-compat principles - `docs/developers/daemon-ui/MIGRATION.md` — adapter author migration cookbook - Step-by-step recommended adoption order (9 steps, value-ranked) - Before/after code examples for each step - Backward-compat checklist (everything is additive — no breaking changes) - Cross-references to PR-A through PR-H commits ## Roadmap PR-I of the unified follow-up to PR QwenLM#4328. Documentation-only — no code changes; no tests affected. Generated with AI Co-authored-by: Claude Opus 4.7 <[email protected]>
Summary
Unified follow-up to #4328 — closes every SDK-only gap from the unified-renderer-layer review so library-embedder consumers (web chat, web terminal, and any third-party host built on
@qwen-code/sdk/daemon+@qwen-code/webui) all render the same transcript the same way. Native TUI, channel adapters (DingTalk / Telegram / WeChat), and IDE companions stay on their existing direct ACP paths and are NOT in this PR's adoption scope (seedocs/developers/daemon-ui/README.md).#4328 shipped the v1 transcript-layer skeleton (~55%). This PR brings the daemon UI surface to ~95% completeness — the remaining 5% is daemon/Core work outside the SDK package, declared in TODO §B / §D below.
What this PR delivers
1. Full daemon event coverage (was 13 types → now 28+)
The normalizer used to fall back to
debugfor 16 of the daemon's emitted event types (session-meta, workspace Wave 3/4, auth device-flow, etc.). Adapters had no way to dispatch on them without greppingdebug.text. This PR types every one —session.metadata.changed,session.approval_mode.changed,workspace.mcp.budget_warning,auth.device_flow.failed, and so on — with closed-enum fields where the daemon protocol defines them (errorKind,provenance,serverId).Benefit: adapters get a typed discriminated union to switch on. Forward-compat for new daemon events still routes through
debugcleanly; no exhaustiveness failures.2. Cross-client time consistency
DaemonUiEventBase.serverTimestamp?+DaemonTranscriptBlockBase.clientReceivedAt, plusselectTranscriptBlocksOrderedByEventId(daemon-monotonic ordering) andformatBlockTimestamp(Intl-based locale-aware formatter).Benefit: when multiple clients attach to the same session, "X minutes ago" labels and block ordering stay consistent regardless of each client's local clock drift. Survives SSE replay-after-reconnect because the daemon's
eventIdis the primary sort key.3. Reducer state machine — currentTool / approvalMode / cancellation
DaemonTranscriptStatenow tracks sidechannel state alongside the block list:currentToolCallId— which tool is in-flight right now (auto-maintained on tool lifecycle transitions)approvalMode— mirrored fromsession.approval_mode.changedtoolProgress— ready for the (still-pending)tool.progresseventassistant.done.reason === 'cancelled', every in-flight tool's status flips to'cancelled'automatically — daemon doesn't guarantee a terminaltool_call_updatefor every in-flight tool when the parent prompt is cancelledBenefit: UIs stop showing "tool spinning forever" after cancel. Renderers can read
selectCurrentTool(state)instead of scanning blocks. NewselectSubagentChildBlocksexposes sub-agent delegation as a queryable tree (via the daemon's_meta.parentToolCallIdstamp).4. Render contract — markdown / HTML / plain text
daemonBlockToMarkdown/daemonBlockToHtml/daemonBlockToPlainText/daemonToolPreviewToMarkdown— four projection helpers that take a block and return a renderable string. Conservative HTML sanitizer (ANSI strip → HTML escape;role="alert"for errors).sanitizeUrlsstrips token-shaped query params from CDN/auth URLs.maxFieldLengthtruncation caps any single field at 8192 chars by default.Benefit: web chat, web terminal, IDE extension, and any future adapter all render identically by default. Adapters opt into custom rendering per
block.kind/preview.kindonly where they need it. No more per-adapter projection drift.5. Tool preview taxonomy — 4 → 13 kinds
file_diff·file_read·web_fetch·mcp_invocation·code_block·search·tabular·image_generation·subagent_delegation·ask_user_question·command·key_value·generic. Each detected from tool input shape; each with a markdown + plain-text projection.Benefit: tool calls render with appropriate per-kind affordances (unified diff for edits, MCP server badge for MCP calls, image thumbnail for generation tools, etc.) without each adapter writing its own switch.
6. Adapter conformance framework
runAdapterConformanceSuite(adapter)+ an embedded fixture corpus (11 fixtures including subagent nesting, redaction, cancellation, mcp-budget, auth-device-flow). Adapters run this in their own test suite and surface projection drift before users see it.Benefit: when a new daemon event or preview kind lands, every adapter that runs conformance sees a failing fixture instead of silently displaying nothing — projection drift is caught in CI, not in user reports.
7. WebUI migration
packages/webui'stranscriptAdapternow bridges through the SDK render contract. Opt-in flags (useMarkdown,enrichToolDetailsWithPreview) preserve legacy default behavior for incremental rollout.Benefit: web chat starts consuming the shared render layer immediately; rich previews (file diffs, MCP, tabular) surface without webui adding kind-specific components.
8. Sensitive-field redaction at the normalizer boundary
redactSensitiveFieldswalks tool input/output/content/locations and redacts values forapiKey/token/secret/password/authorization/cookie/bearertoken/accesstokenetc. (closed list, normalized for case/separators) before they reach transcript blocks.Benefit: a buggy debug panel or naive
JSON.stringify(block)can't leak credentials. Tests verify end-to-end (Bearer secret-do-not-leaknever appears in any serialized event).9. Sub-agent nesting
When the daemon stamps
_meta.parentToolCallId+_meta.subagentTypeon a tool call (theTask-equivalent delegation pattern), the reducer correlates child tool blocks under their parent (parentBlockId). Out-of-order arrival (child before parent) is handled — back-fill happens when the parent appears, or when a later child update arrives.Benefit: renderers can draw nested sub-agent activity (folder-header + indented children) without re-correlating on every render.
selectSubagentChildBlocks(state, parentId)returns direct children in O(1) after first build.10. Performance & correctness hardening
Lazy copy-on-write in the reducer (
state.blocksreference preserved across sidechannel-only dispatches → WeakMap caches for sort + children-index actually hit). Cancellation iterates only non-trimmed entries. Tool progress + permission block index pruned post-trim to bound memory in long sessions.Benefit:
useSyncExternalStoreconsumers don't pay an O(n log n) re-sort on every dispatch when only metadata changed.11. Adapter author documentation
docs/developers/daemon-ui/README.md— full API reference with cookbook (markdown / HTML / plain-text rendering, sub-agent nested rendering, sensitive-field handling, time formatting).docs/developers/daemon-ui/MIGRATION.md— 9-step before/after guide for adapter authors.Benefit: lowers cost of bringing a new adapter (channel plugin, IDE extension, dashboard) onto the shared contract from "read 600 LOC of normalizer source" to "run
runAdapterConformanceSuite+ read the cookbook".Daemon-side dependency status (verified against
daemon_mode_b_main@57d04786d)After landing #4360 (daemon protocol completion), 5 of 7 declared dependencies are now satisfied on the wire — meaning the forward-compat code paths in this PR activate automatically once merged:
_meta.serverTimestampenvelope stampingserver.ts:2670(cites issue #19 P0)formatBlockTimestampprovenance+serverIdon tool_callToolCallEmitter.emitStarterrorKindonstream_errorserver.ts:2046DaemonUiErrorEvent.errorKindtypederrorKindonsession_diedreasonfieldreason_meta.parentToolCallId)SubAgentTracker.getSubagentMeta()selectSubagentChildBlockstool.progresseventMessageEmitter.emitUserContent)extractContentPartreadyValidation
Reference adapter conformance:
Remaining (deferred to follow-up PRs, not blockers for this one)
tool.progress— new SSE event type (~50 LOC daemon). SDK state shape already ready.MessageEmitter.emitUserContent(parts)+HistoryReplayerinlineData/fileDatabranches (~80 LOC Core) + reducer wiring (~80 LOC SDK). SDK'sextractContentParthelper already shipped, awaiting Core.Both unblock specific UX features (long-task progress display + image/audio attachment echo); neither blocks this PR's render-contract delivery.
Scope / Risk
daemon_mode_b_main(21 files). All changes are additive to the public API; no existing export removed or renamed.createdAtpreserved as@deprecatedalias forclientReceivedAt.debug.@qwen-code/sdk/daemonsubpath has zero React / zero Node-only deps (asserted inassertBrowserSafeBundle). Web-terminal / web-chat bundles include only the helpers they import (tree-shake friendly).Dependencies
daemon_mode_b_main@57d04786d(post-feat(daemon): add shared UI transcript layer #4328 merge, post-feat(serve+sdk): F4 prereq — daemon protocol completion (serverTimestamp / provenance / errorKind / state_resync_required) #4360 F4 prereq, post-perf(core): F2 cleanup PR A — R9/W11/W12/R10 (post-merge follow-ups) #4411 F2 cleanup, post-refactor(acp-bridge): F1 test split — lift bridge.test.ts (6861 LOC) to acp-bridge #4445 F1 test split).Linked
feat(daemon): add shared UI transcript layer(base, merged)feat(serve+sdk): F4 prereq — daemon protocol completion(merged; satisfies §C1/§C2/§C3 dependencies)cc @wenshao @doudouOUC
Generated with assistance from Claude Opus 4.7. Full SDK + WebUI typecheck + 153 unit tests pass against the rebased branch HEAD.