|
| 1 | +# Daemon Web UI Adapter |
| 2 | + |
| 3 | +## Goal |
| 4 | + |
| 5 | +Web chat and web terminal clients should consume `qwen serve` through the |
| 6 | +daemon HTTP/SSE APIs and render a client-side transcript. Native local TUI, |
| 7 | +channel, and IDE integrations keep their existing default paths for now. |
| 8 | + |
| 9 | +## Shared UI Contract |
| 10 | + |
| 11 | +Use the TypeScript SDK daemon UI exports as the common boundary: |
| 12 | + |
| 13 | +```ts |
| 14 | +import { |
| 15 | + DaemonClient, |
| 16 | + DaemonSessionClient, |
| 17 | + createDaemonTranscriptStore, |
| 18 | + normalizeDaemonEvent, |
| 19 | +} from '@qwen-code/sdk/daemon'; |
| 20 | +``` |
| 21 | + |
| 22 | +The split is: |
| 23 | + |
| 24 | +- `DaemonClient` handles daemon HTTP routes. |
| 25 | +- `DaemonSessionClient` owns session creation/attachment and SSE replay. |
| 26 | +- `normalizeDaemonEvent()` converts daemon wire events into UI events. |
| 27 | +- `createDaemonTranscriptStore()` reduces UI events into transcript blocks. |
| 28 | + |
| 29 | +React clients can use the optional `@qwen-code/webui` binding: |
| 30 | + |
| 31 | +```tsx |
| 32 | +import { |
| 33 | + DaemonSessionProvider, |
| 34 | + useDaemonActions, |
| 35 | + useDaemonConnection, |
| 36 | + useDaemonPendingPermissions, |
| 37 | + useDaemonTranscriptBlocks, |
| 38 | +} from '@qwen-code/webui'; |
| 39 | +``` |
| 40 | + |
| 41 | +Minimal React shape: |
| 42 | + |
| 43 | +```tsx |
| 44 | +function App() { |
| 45 | + return ( |
| 46 | + <DaemonSessionProvider baseUrl="http://127.0.0.1:4170"> |
| 47 | + <Transcript /> |
| 48 | + <PromptBox /> |
| 49 | + </DaemonSessionProvider> |
| 50 | + ); |
| 51 | +} |
| 52 | + |
| 53 | +function Transcript() { |
| 54 | + const blocks = useDaemonTranscriptBlocks(); |
| 55 | + return blocks.map((block) => <RenderBlock key={block.id} block={block} />); |
| 56 | +} |
| 57 | +``` |
| 58 | + |
| 59 | +The provider creates or attaches a daemon session, subscribes to SSE, keeps the |
| 60 | +last event id on `DaemonSessionClient`, and reconnects the stream by default. |
| 61 | +Callers can disable that with `autoReconnect={false}` for tests or custom |
| 62 | +connection management. |
| 63 | + |
| 64 | +## Browser Deployment Shapes |
| 65 | + |
| 66 | +### Same-Origin Local POC |
| 67 | + |
| 68 | +A daemon-served page can call the daemon directly because the page and API share |
| 69 | +one origin. This is the preferred early POC shape for local web chat and web |
| 70 | +terminal validation. |
| 71 | + |
| 72 | +### Remote Web Chat / Web Terminal |
| 73 | + |
| 74 | +A production remote web app should normally talk to a backend-for-frontend. The |
| 75 | +BFF owns daemon URL, token, workspace routing, and session metadata, then |
| 76 | +forwards browser-safe app events to the browser. This keeps bearer tokens out of |
| 77 | +browser storage and lets the deployment decide which daemon/workspace a user is |
| 78 | +allowed to reach. |
| 79 | + |
| 80 | +### Local Browser Against Local Daemon |
| 81 | + |
| 82 | +A separate local dev server is cross-origin from `qwen serve`; it must either |
| 83 | +proxy daemon routes through the same origin or be served by the daemon. The |
| 84 | +daemon intentionally rejects arbitrary browser `Origin` requests. |
| 85 | + |
| 86 | +## Rendering Responsibilities |
| 87 | + |
| 88 | +The shared transcript model is semantic, not visual. UI clients decide how to |
| 89 | +render: |
| 90 | + |
| 91 | +- user and assistant message blocks |
| 92 | +- collapsed thought blocks |
| 93 | +- tool status cards |
| 94 | +- shell output blocks |
| 95 | +- permission request controls |
| 96 | +- status/error/debug blocks |
| 97 | + |
| 98 | +The web terminal is a browser-native semantic renderer. It should look and feel |
| 99 | +terminal-like with monospace layout, scrollback, prompt input, shortcuts, and |
| 100 | +streaming blocks, but it is not a raw PTY proxy and does not require server-side |
| 101 | +Ink rendering. |
| 102 | + |
| 103 | +## Merge Safety |
| 104 | + |
| 105 | +- The native `qwen` TUI remains direct and unchanged. |
| 106 | +- `--acp`, channel, and IDE paths remain unchanged by default. |
| 107 | +- The SDK UI core is additive. |
| 108 | +- The WebUI React binding is optional and only runs in clients that import it. |
| 109 | +- Removed daemon TUI spike code should not be treated as a product migration. |
| 110 | + |
| 111 | +## Follow-Ups |
| 112 | + |
| 113 | +- Add a daemon-served local `/web` POC or equivalent same-origin web app. |
| 114 | +- Build first-class chat and terminal renderers on top of transcript blocks. |
| 115 | +- Add richer typed events only where existing daemon events are too low-level |
| 116 | + for stable browser UI behavior. |
| 117 | +- Consider a dedicated `@qwen-code/daemon-ui-core` package if non-SDK consumers |
| 118 | + need the UI core as an independent dependency. |
0 commit comments