Skip to content

feat(daemon): add shared UI transcript layer#4328

Open
chiga0 wants to merge 1 commit into
QwenLM:daemon_mode_b_mainfrom
chiga0:feat/daemon-ui-core
Open

feat(daemon): add shared UI transcript layer#4328
chiga0 wants to merge 1 commit into
QwenLM:daemon_mode_b_mainfrom
chiga0:feat/daemon-ui-core

Conversation

@chiga0
Copy link
Copy Markdown
Collaborator

@chiga0 chiga0 commented May 19, 2026

Summary

  • What changed: Adds a shared daemon UI layer for web chat / web terminal clients: typed daemon events are normalized into UI events, reduced into transcript blocks, exposed through a framework-free store, and optionally consumed through new React bindings in @qwen-code/webui.
  • Why it changed: Web clients should not each reimplement streaming merge, tool preview, permission state, shell output, and reconnect-friendly transcript handling on top of raw daemon SSE frames.
  • Reviewer focus: Please focus on the SDK/webui boundary, browser-safe @qwen-code/sdk/daemon export shape, and confirmation that native local TUI / ACP / channel / IDE defaults remain untouched.

Validation

  • Commands run:
    cd packages/sdk-typescript && npx vitest run test/unit/daemonUi.test.ts
    cd /Users/gawain/Documents/codebase/opensource/qwen-code-daemon-ui-core && npm run typecheck
    cd /Users/gawain/Documents/codebase/opensource/qwen-code-daemon-ui-core && npm run build
    cd packages/sdk-typescript && npx eslint src/daemon/ui test/unit/daemonUi.test.ts
    cd packages/webui && npm run lint
    cd /Users/gawain/Documents/codebase/opensource/qwen-code-daemon-ui-core && node -e "import('@qwen-code/sdk/daemon').then(m=>console.log(Boolean(m.DaemonClient), Boolean(m.normalizeDaemonEvent)))"
  • Prompts / inputs used: Unit tests cover daemon stream chunks, AskUserQuestion preview, permission request/resolution, store subscription, and terminal text sanitization.
  • Expected result: SDK UI reducer merges streaming assistant chunks, exposes semantic tool/permission/shell/status blocks, and WebUI can build while keeping the daemon SDK import browser-safe.
  • Observed result: All commands above passed. npm run build reports an existing VSCode companion lint warning in editorGroupUtils.ts but exits successfully.
  • Quickest reviewer verification path: Run the SDK unit test, root typecheck, root build, and import smoke test above.
  • Evidence: Root build completed through WebUI, SDK, and VSCode companion builds; SDK daemon subpath import printed true true.

Scope / Risk

  • Main risk or tradeoff: @qwen-code/webui now 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.
  • Not covered / not validated: This PR does not add the final daemon-served /web app or full browser UI parity. It provides the shared UI layer those clients should consume.
  • Breaking changes / migration notes: Native TUI remains on the current direct path. The deleted daemon TUI adapter was a spike-only path and is not part of the supported native TUI flow.

Testing Matrix

🍏 🪟 🐧
npm run ⚠️ ⚠️
npx ⚠️ ⚠️
Docker N/A N/A N/A
Podman N/A N/A N/A
Seatbelt N/A N/A N/A

Testing matrix notes:

  • Tested locally on macOS. This change is TypeScript SDK/WebUI code and does not add OS-specific runtime behavior.

Linked Issues / Bugs

Related to #3803 and #4175.

@github-actions
Copy link
Copy Markdown
Contributor

📋 Review Summary

This PR adds a shared daemon UI layer for web chat/terminal clients, providing typed daemon event normalization, transcript state management, and React bindings through @qwen-code/webui. The implementation consolidates UI state handling that was previously duplicated across client implementations. Overall assessment: solid architecture with good separation of concerns, but requires attention to browser-safety guarantees and a few API design refinements.

🔍 General Feedback

Positive aspects:

  • Clean separation between the SDK daemon UI layer (@qwen-code/sdk/daemon) and the WebUI React bindings
  • Framework-free store design with useSyncExternalStore compatibility is excellent for React integration
  • Good use of TypeScript types to enforce transcript block structure
  • Comprehensive unit tests covering normalization, permission tracking, and store subscription
  • Proper cleanup of spike code (deleted DaemonTuiAdapter.ts and associated test)

Architectural observations:

  • The @qwen-code/sdk/daemon subpath export is a smart way to keep browser bundles from pulling in Node-only SDK code
  • Transcript reducer pattern follows Redux-like immutability principles
  • The DaemonSessionProvider properly handles SSE stream lifecycle with abort signals

Recurring themes:

  • Several places could benefit from more defensive type guards
  • Browser-safety of the daemon subpath needs explicit verification in build config
  • Documentation updates are minimal for the amount of new API surface

🎯 Specific Feedback

🔴 Critical

  • File: packages/sdk-typescript/src/daemon/ui/normalizer.ts:140-160 - The normalizeSessionUpdate function handles agent_message_chunk but emits both assistant.text.delta AND assistant.done events when _meta.usage is present. This could cause race conditions if the UI processes these synchronously. Recommendation: Either emit a single combined event or document the ordering guarantee explicitly.

  • File: packages/webui/src/daemon/DaemonSessionProvider.tsx:88-95 - The SSE event loop doesn't handle reconnection logic. If the SSE stream disconnects (network blip, server restart), the provider enters disconnected state but doesn't attempt to resume with lastEventId. Recommendation: Add optional auto-reconnect with exponential backoff, or at minimum document this limitation prominently.

  • File: packages/sdk-typescript/src/daemon/index.ts - The re-exported types from ./ui/index.js create a circular dependency risk if the UI module ever needs to import from the parent daemon module. Recommendation: Consider keeping the UI module completely independent or use explicit barrel imports with care.

🟡 High

  • File: packages/sdk-typescript/scripts/build.js - The build script modification adds the daemon subpath but doesn't include explicit browser-safe validation. Recommendation: Add a build-time check that verifies no Node.js-specific imports (e.g., fs, path, http) leak into the daemon UI bundle. This is critical for the stated goal of browser-safe ESM.

  • File: packages/sdk-typescript/src/daemon/ui/transcript.ts:45-60 - The reduceDaemonTranscriptEvent function uses mutable state cloning (cloneTranscriptState then direct mutation). While this is internal, it's error-prone and makes time-travel debugging harder. Recommendation: Consider using Immer or a more functional update pattern for complex state transitions.

  • File: packages/webui/src/daemon/transcriptAdapter.ts:40-50 - The daemonTranscriptToUnifiedMessages adapter assumes all tool blocks have valid toolCallId and title fields. If the daemon sends malformed data, this will crash the UI. Recommendation: Add defensive null checks or use a schema validator like Zod at the adapter boundary.

  • File: packages/sdk-typescript/src/daemon/ui/types.ts:100-120 - The NormalizeDaemonEventOptions interface has suppressOwnUserEcho which relies on clientId matching. This is subtle behavior that could cause confusing bugs if the clientId is undefined or changes. Recommendation: Add runtime warnings when suppression is enabled but clientId is missing.

🟢 Medium

  • File: packages/sdk-typescript/src/daemon/ui/utils.ts - The sanitizeTerminalText function strips OSC sequences but the regex pattern should be documented with examples of what it catches. Recommendation: Add JSDoc comments with before/after examples.

  • File: packages/sdk-typescript/src/daemon/ui/store.ts:45-55 - The dispatch method accepts single events or arrays but doesn't validate input. Recommendation: Add runtime type guards to catch developer errors early (e.g., dispatching undefined or null).

  • File: packages/webui/src/daemon/DaemonSessionProvider.tsx:145-165 - The actions object is recreated on every render due to the useMemo dependency on store. Since store is stable, this is fine, but the requireSession helper throws generically. Recommendation: Use more specific error types or error codes for better error handling upstream.

  • File: packages/sdk-typescript/test/unit/daemonUi.test.ts - Test coverage is good but doesn't include edge cases like: rapid event firing, event ID gaps, or malformed daemon events. Recommendation: Add stress tests for the reducer under high-frequency updates.

🔵 Low

  • File: docs/developers/daemon-client-adapters/web-ui.md - The new documentation is helpful but doesn't include a quickstart example showing the minimal DaemonSessionProvider wrapper. Recommendation: Add a "Getting Started" code snippet.

  • File: packages/sdk-typescript/src/daemon/ui/types.ts - The type definitions are comprehensive but lack inline JSDoc for most interfaces. Recommendation: Add brief descriptions for each event type explaining when it's emitted.

  • File: packages/webui/package.json - The @qwen-code/sdk dependency is pinned to 0.1.7. Recommendation: Consider using a range like ^0.1.7 to allow minor updates, or document the upgrade process when SDK changes.

  • File: packages/sdk-typescript/src/daemon/ui/terminal.ts - The daemonUiEventToTerminalText function is exported but doesn't have clear usage examples. Recommendation: Add a code comment showing typical terminal renderer integration.

✅ Highlights

  • Excellent React integration: The useSyncExternalStore pattern in DaemonSessionProvider is the modern best practice for framework-free stores
  • Clean event normalization: The normalizeDaemonEvent function handles the complex daemon event schema translation elegantly
  • Good test coverage: Unit tests cover the critical paths including permission tracking, streaming chunks, and store subscription
  • Proper cleanup: Removing the spike DaemonTuiAdapter code shows good discipline in not accumulating dead code
  • Type-safe transcript blocks: The discriminated union types for DaemonTranscriptBlock variants will catch many bugs at compile time

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(', ')))"

@chiga0 chiga0 requested review from wenshao and yiliang114 May 19, 2026 14:18
@chiga0 chiga0 force-pushed the feat/daemon-ui-core branch from ceb2ae5 to 94d66e0 Compare May 19, 2026 14:30
@chiga0
Copy link
Copy Markdown
Collaborator Author

chiga0 commented May 19, 2026

Generated by GPT-5 model.

Follow-up from the review pass:

  • Addressed: added default SSE auto-reconnect in DaemonSessionProvider; it keeps the same DaemonSessionClient, so reconnect resumes with the client cursor / last event id.
  • Addressed: stabilized createSessionRequest dependencies so inline React props do not accidentally tear down and recreate the session every render.
  • Addressed: added CJS support for @qwen-code/sdk/daemon and smoke-tested both import() and require().
  • Addressed: SDK build now checks the browser daemon ESM bundle for Node-only tokens, and the generated bundle scan is clean.
  • Addressed: added a minimal WebUI quickstart note covering provider usage and reconnect behavior.

Review triage / false positives:

  • assistant.text.delta followed by assistant.done is intentional ordered reducer input: the delta is applied first, then done only marks the active assistant block non-streaming. No combined event is needed for this reducer.
  • The daemon barrel re-export does not create a circular runtime dependency: UI modules import daemon wire types/utilities directly, and the parent daemon/index.ts only re-exports them.
  • Tool transcript fields are required by the normalized transcript type; malformed daemon tool events get fallback ids/titles or degrade to debug/status events before WebUI mapping.
  • The internal reducer clones state before mutation; callers only receive the replaced snapshot, so this keeps the framework-free store simple without adding Immer to a browser-facing SDK surface.

Validation run locally after the amend:

  • cd packages/sdk-typescript && npx vitest run test/unit/daemonUi.test.ts
  • cd packages/sdk-typescript && ../../node_modules/.bin/eslint src/daemon/ui test/unit/daemonUi.test.ts
  • cd packages/webui && npm run lint
  • cd packages/webui && npm run typecheck
  • cd packages/webui && npm run build
  • cd packages/sdk-typescript && npm run build
  • root npm run typecheck
  • root npm run build (completed with the existing vscode-ide-companion curly warning, no errors)
  • ESM/CJS smoke: import('@qwen-code/sdk/daemon') and require('@qwen-code/sdk/daemon') both returned the expected daemon exports.

Comment thread packages/sdk-typescript/src/daemon/ui/utils.ts
Comment thread packages/webui/src/daemon/DaemonSessionProvider.tsx
Comment thread packages/webui/src/daemon/DaemonSessionProvider.tsx
Comment thread packages/webui/src/daemon/DaemonSessionProvider.tsx
Comment thread packages/sdk-typescript/src/daemon/ui/transcript.ts
Comment thread packages/sdk-typescript/src/daemon/ui/normalizer.ts Outdated
Comment thread packages/webui/src/daemon/transcriptAdapter.ts
Comment thread packages/webui/src/daemon/transcriptAdapter.ts Outdated
Comment thread packages/webui/src/daemon/transcriptAdapter.ts
Comment thread packages/sdk-typescript/src/daemon/ui/toolPreview.ts
@chiga0 chiga0 force-pushed the feat/daemon-ui-core branch from 94d66e0 to b3e102b Compare May 19, 2026 14:41
@chiga0 chiga0 requested a review from wenshao May 19, 2026 14:43
@chiga0 chiga0 force-pushed the feat/daemon-ui-core branch from b3e102b to fe77066 Compare May 19, 2026 14:44
Copy link
Copy Markdown
Collaborator Author

@chiga0 chiga0 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.usage piggyback for assistant.done is fragile
  • normalizer.ts:241toolCallId fallback to event.id creates phantom tool blocks
  • transcript.ts:345cloneTranscriptState does full deep clone on every event (perf)
  • transcript.ts:152getBlockById linear scan on streaming hot path
  • transcriptAdapter.ts:139normalizeToolStatus default → 'failed' breaks forward-compat
  • transcriptAdapter.ts:151cancelled'completed' loses information
  • transcriptAdapter.ts:115 — error block rendered as assistant message with [System Error] prefix is a UX antipattern
  • sdk-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-renders
  • store.ts:53 — dispatch immediately notifies, no microtask batching
  • transcript.ts:289 — silent fallback to status when permission block was trimmed
  • webui/package.json:47"@qwen-code/sdk": "0.1.7" exact-pin causes monorepo skew
  • build.js:131assertBrowserSafeBundle only 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.

Comment thread packages/sdk-typescript/src/daemon/ui/normalizer.ts Outdated
Comment thread packages/sdk-typescript/src/daemon/ui/normalizer.ts Outdated
Comment thread packages/sdk-typescript/src/daemon/ui/transcript.ts
Comment thread packages/sdk-typescript/src/daemon/ui/transcript.ts Outdated
Comment thread packages/sdk-typescript/src/daemon/ui/transcript.ts
Comment thread packages/sdk-typescript/src/index.ts Outdated
Comment thread packages/webui/src/daemon/DaemonSessionProvider.tsx Outdated
Comment thread packages/sdk-typescript/src/daemon/ui/store.ts Outdated
Comment thread packages/webui/package.json Outdated
Comment thread packages/sdk-typescript/scripts/build.js
@chiga0 chiga0 force-pushed the feat/daemon-ui-core branch from fe77066 to feddf8e Compare May 19, 2026 15:28
Comment thread packages/sdk-typescript/src/daemon/ui/transcript.ts
Comment thread packages/sdk-typescript/src/daemon/ui/transcript.ts Outdated
Comment thread packages/sdk-typescript/src/daemon/ui/transcript.ts
Comment thread packages/sdk-typescript/src/daemon/ui/transcript.ts
Comment thread packages/sdk-typescript/src/daemon/ui/utils.ts
Comment thread packages/sdk-typescript/src/daemon/ui/normalizer.ts
Comment thread packages/webui/src/daemon/DaemonSessionProvider.tsx
Comment thread packages/webui/src/daemon/DaemonSessionProvider.tsx Outdated
Comment thread packages/webui/src/daemon/DaemonSessionProvider.tsx
Comment thread packages/webui/src/daemon/transcriptAdapter.ts
@chiga0 chiga0 force-pushed the feat/daemon-ui-core branch 2 times, most recently from 99f2952 to 5f48a92 Compare May 19, 2026 15:37
@chiga0 chiga0 requested a review from wenshao May 19, 2026 15:40
@chiga0
Copy link
Copy Markdown
Collaborator Author

chiga0 commented May 19, 2026

Generated by GPT-5 model.

Latest review pass handled in 5f48a92e3:

  • Removed fragile _meta.usage completion and missing-tool-id synthesis.
  • Reworked transcript reducer hot paths with block indexing, structural sharing, batched reduction, and batched store notifications.
  • Preserved tool preview/status on partial updates, fixed thought block lifecycle, permission orphan visibility, output recursion depth, and redacted fallback error text.
  • Kept UI-only daemon transcript APIs scoped to @qwen-code/sdk/daemon; top-level SDK smoke checks confirm they are not exported from @qwen-code/sdk.
  • Split WebUI daemon provider contexts and added adapter/reducer coverage for the reviewed edge cases.

Local validation run:

  • cd packages/sdk-typescript && npx vitest run test/unit/daemonUi.test.ts test/unit/daemon-public-surface.test.ts
  • cd packages/webui && npx vitest run src/daemon/transcriptAdapter.test.ts
  • cd packages/webui && npm run lint
  • cd packages/webui && npm run typecheck
  • cd packages/sdk-typescript && npm run build
  • cd packages/webui && npm run build
  • npm run typecheck
  • SDK daemon ESM/CJS smoke checks

Comment thread packages/webui/src/daemon/DaemonSessionProvider.tsx
Comment thread packages/webui/src/daemon/DaemonSessionProvider.tsx Outdated
Comment thread packages/sdk-typescript/src/daemon/ui/normalizer.ts
Comment thread packages/sdk-typescript/src/daemon/ui/transcript.ts
@chiga0 chiga0 force-pushed the feat/daemon-ui-core branch from 5f48a92 to 5532d6c Compare May 20, 2026 02:57
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.

2 participants