basant307/AI_Governance_Project
048
1# Migrating to `@qwen-code/sdk/daemon` v22 3PR #4328 shipped the v1 daemon UI layer. PR #4353 (this PR) ships v2 with4seven additive feature commits. This guide walks through the changes for web5chat and web terminal adapter authors first. Native local TUI, channel, and IDE6maintainers can reuse the same primitives later, but those default product paths7are not migrated by this PR.8 9## TL;DR for existing consumers10 11**No breaking changes.** Every commit in this PR is additive:12 13- v1 fields still work (`createdAt` preserved as `@deprecated` alias for14 `clientReceivedAt`)15- v1 normalizer still maps the same 13 event types the same way16- v1 reducer still produces the same blocks for chat events17- New API is opt-in via additional parameters and helpers18 19The PR is safe to merge without any consumer changes. **Adoption of the20new features is incremental.**21 22## Recommended adoption order23 24For each adapter, in order of effort/value ratio:25 26### 1. Ordering: switch sort key from `createdAt` to `eventId`27 28**Before:**29 30```ts31const ordered = [...state.blocks].sort((a, b) => a.createdAt - b.createdAt);32```33 34**After:**35 36```ts37import { selectTranscriptBlocksOrderedByEventId } from '@qwen-code/sdk/daemon';38const ordered = selectTranscriptBlocksOrderedByEventId(state);39```40 41**Why**: `eventId` is daemon-monotonic; survives SSE replay-after-reconnect.42`createdAt` is client clock and shifts under replay.43 44### 2. Display: switch `createdAt` to `serverTimestamp ?? clientReceivedAt`45 46**Before:**47 48```tsx49<TimeLabel ms={block.createdAt} />50```51 52**After:**53 54```tsx55import { formatBlockTimestamp } from '@qwen-code/sdk/daemon';56<TimeLabel text={formatBlockTimestamp(block, { locale })} />;57```58 59**Why**: Multiple clients see consistent "X minutes ago" only when both60read daemon clock. Renderer plus `formatBlockTimestamp` handles tz +61locale.62 63**Note**: Daemon needs to stamp `_meta.serverTimestamp` on envelopes for64this to take effect. SDK forward-compat-ready; falls back to65`clientReceivedAt` until then.66 67### 3. Listen for new event types — pick subset to render68 69The 16 new event types (session-meta, workspace, auth) don't push transcript70blocks. They are sidechannel observations. Each adapter picks which to surface:71 72```ts73// In your SSE consumer74const uiEvents = normalizeDaemonEvent(envelope, {75 clientId,76 suppressOwnUserEcho: true,77});78store.dispatch(uiEvents);79 80// Then in your UI side81for (const event of uiEvents) {82 switch (event.type) {83 case 'session.approval_mode.changed':84 myApprovalModeBadge.update(event.next);85 break;86 case 'workspace.mcp.budget_warning':87 myToast.show(88 `MCP servers approaching budget: ${event.liveCount}/${event.budget}`,89 );90 break;91 case 'auth.device_flow.started':92 myAuthModal.show({93 deviceFlowId: event.deviceFlowId,94 providerId: event.providerId,95 expiresAt: event.expiresAt,96 });97 break;98 // ... etc, opt into what your UI needs99 }100}101```102 103Or use selectors for state-mirrored sidechannels:104 105```ts106import { selectApprovalMode, selectCurrentTool } from '@qwen-code/sdk/daemon';107 108const mode = selectApprovalMode(state); // mirrored from approval_mode.changed109const currentTool = selectCurrentTool(state); // current in-flight tool110```111 112### 4. Render contract: use `daemonBlockToMarkdown` (or HTML / plainText)113 114**Before** (each adapter does its own projection):115 116```ts117function blockToString(block: DaemonTranscriptBlock): string {118 switch (block.kind) {119 case 'user':120 return `You: ${block.text}`;121 case 'assistant':122 return block.text;123 case 'tool':124 return `[${block.title}]\n${block.status}`;125 // ... etc126 }127}128```129 130**After** (delegate to SDK):131 132```ts133import { daemonBlockToMarkdown } from '@qwen-code/sdk/daemon';134const md = daemonBlockToMarkdown(block);135```136 137For HTML SSR:138 139```ts140import MarkdownIt from 'markdown-it';141import DOMPurify from 'dompurify';142const html = DOMPurify.sanitize(md.render(daemonBlockToMarkdown(block)));143```144 145For plain text:146 147```ts148import { daemonBlockToPlainText } from '@qwen-code/sdk/daemon';149const plain = daemonBlockToPlainText(block);150```151 152### 5. Conformance test153 154Add to your adapter's test suite:155 156```ts157import { runAdapterConformanceSuite } from '@qwen-code/sdk/daemon';158 159it('adapter projects daemon UI corpus correctly', () => {160 const result = runAdapterConformanceSuite({161 reduce: (events) => myReduce(events),162 renderToText: (state) => myRender(state),163 });164 expect(result.failed).toEqual([]);165});166```167 168This will run your adapter against 10 fixture scenarios and surface any169projection drift before it reaches users.170 171### 6. Tool icon dispatch via `provenance`172 173**Before** (string match on toolName):174 175```tsx176const isMcp = toolName?.startsWith('mcp__');177const isBuiltin = ['Bash', 'Edit', 'Read'].includes(toolName);178```179 180**After** (typed provenance from PR-A):181 182```tsx183import type { DaemonUiToolUpdateEvent } from '@qwen-code/sdk/daemon';184 185function toolIcon(event: DaemonUiToolUpdateEvent): React.ReactNode {186 switch (event.provenance) {187 case 'mcp':188 return <McpIcon server={event.serverId} />;189 case 'subagent':190 return <SubagentIcon />;191 case 'builtin':192 return <BuiltinIcon name={event.toolName} />;193 case 'unknown':194 default:195 return <GenericIcon />;196 }197}198```199 200SDK has a `mcp__<server>__<tool>` naming heuristic fallback — works today201even when daemon doesn't explicitly stamp provenance.202 203### 7. Error categorization via `errorKind`204 205**Before** (regex on text):206 207```ts208if (error.text.includes('auth')) showAuthRetry();209else if (error.text.includes('file not found')) showFilePicker();210```211 212**After** (closed enum from PR-A):213 214```ts215import type { DaemonErrorKind } from '@qwen-code/sdk/daemon';216 217function errorAction(errorKind?: DaemonErrorKind): React.ReactNode {218 switch (errorKind) {219 case 'auth_env_error': return <RetryAuthButton />;220 case 'missing_file': return <FilePicker />;221 case 'blocked_egress': return <CheckProxyHint />;222 case 'init_timeout': return <RestartDaemonButton />;223 default: return null;224 }225}226```227 228**Note**: Daemon needs to stamp `data.errorKind` on session_died /229stream_error for this to populate. SDK already reads it.230 231### 8. Cancellation handling — already automatic232 233In v1, cancelled prompts left in-flight tool blocks spinning forever.234In v2 (PR-E), `propagateCancellationToInFlightTools` runs automatically235on `assistant.done.reason === 'cancelled'`. Sub-agent children are236cancelled together with their parent.237 238**No adapter changes needed** — your spinners will resolve correctly.239 240### 8a. Sub-agent nesting — opt in to nested rendering (PR-K)241 242Tool blocks invoked inside a sub-agent delegation now carry243`parentToolCallId`, `subagentType`, and (when the parent is in state)244`parentBlockId`. Adapters can opt in to nested rendering:245 246**Before** (flat list, sub-agent calls visually indistinguishable from247top-level):248 249```tsx250state.blocks.map((b) => <ToolBlock block={b} />);251```252 253**After** (recursive nested rendering):254 255```tsx256import {257 selectSubagentChildBlocks,258 isSubagentChildBlock,259} from '@qwen-code/sdk/daemon';260 261function renderTool(block) {262 const children = selectSubagentChildBlocks(state, block.toolCallId);263 return (264 <ToolBlock block={block}>265 {block.subagentType && <SubagentBadge type={block.subagentType} />}266 {children.length > 0 && <Indent>{children.map(renderTool)}</Indent>}267 </ToolBlock>268 );269}270 271const topLevel = state.blocks.filter((b) => !isSubagentChildBlock(b));272return topLevel.map(renderTool);273```274 275**No adapter changes needed if you prefer the flat view** — the new276fields are additive and ignored by code that doesn't read them.277 278### 9. Tool preview taxonomy — pick subset to render with custom components279 280PR-D + PR-F bring 13 preview kinds:281 282- 4 file-shaped: `file_diff`, `file_read`, `web_fetch`, `mcp_invocation`283- 5 content-shaped: `code_block`, `search`, `tabular`, `image_generation`, `subagent_delegation`284- 2 control: `ask_user_question`, `command`285- 2 generic: `key_value`, `generic`286 287Each adapter dispatches on `preview.kind`:288 289```tsx290function ToolPreviewComponent({ preview }: { preview: DaemonToolPreview }) {291 switch (preview.kind) {292 case 'file_diff':293 return (294 <UnifiedDiffView295 path={preview.path}296 old={preview.oldText}297 new={preview.newText}298 />299 );300 case 'mcp_invocation':301 return (302 <McpCard serverId={preview.serverId} toolName={preview.toolName} />303 );304 case 'tabular':305 return <DataTable columns={preview.columns} rows={preview.rows} />;306 case 'image_generation':307 return (308 <ImagePreview309 thumbnailUrl={preview.thumbnailUrl}310 prompt={preview.prompt}311 />312 );313 // ... or fall back to:314 default:315 return <Markdown text={daemonToolPreviewToMarkdown(preview)} />;316 }317}318```319 320Adapters without custom components for all 13 kinds can fall back to the321SDK's `daemonToolPreviewToMarkdown` for any unhandled kind.322 323## Backward-compat checklist324 325| Concern | Status |326| ------------------------------------------------------ | --------------------------------------------- |327| Existing `block.createdAt` reads | ✅ still works (alias for `clientReceivedAt`) |328| Existing reducer event handling | ✅ unchanged for v1 event types |329| `daemonTranscriptToUnifiedMessages(blocks)` call sites | ✅ new options param is optional |330| Existing `selectTranscriptBlocks` consumers | ✅ unchanged |331| New event types in v1 reducer | ✅ no-op, `lastEventId` still advances |332 333## Cross-references334 335- [PR #4353 SUMMARY](https://github.com/QwenLM/qwen-code/pull/4353)336- [Daemon UI README](./README.md) — full API reference337- [PR #4328](https://github.com/QwenLM/qwen-code/pull/4328) — base PR with shared UI transcript layer338 