diff --git a/.github/scripts/ci/classify-platform-sensitivity.test.mjs b/.github/scripts/ci/classify-platform-sensitivity.test.mjs
index 9bd2f761368..46c0d57f5df 100644
--- a/.github/scripts/ci/classify-platform-sensitivity.test.mjs
+++ b/.github/scripts/ci/classify-platform-sensitivity.test.mjs
@@ -79,7 +79,7 @@ test('the manifests change what each lane executes', () => {
// A workspace manifest is not the root one; it reaches the lanes through
// the subsystem rules or not at all.
assert.equal(
- classifyChangedFiles(['packages/webui/package.json']),
+ classifyChangedFiles(['packages/web-shell/package.json']),
PLATFORM_INSENSITIVE,
);
});
@@ -112,7 +112,6 @@ test('platform-coupled subsystems match on segments, not substrings', () => {
// The substring trap: these contain "shell", "pty", "os" or "platform"
// inside a longer word and must NOT drag both lanes in.
for (const file of [
- 'packages/webui/src/components/Shellfish.tsx',
'packages/core/src/utils/cryptic.ts',
'packages/cli/src/ui/emptyState.ts',
'packages/core/src/telemetry/uploader.ts',
diff --git a/.github/scripts/web-shell-visuals-publish.mjs b/.github/scripts/web-shell-visuals-publish.mjs
index 34ff0e2b66d..3f8d1d5026d 100644
--- a/.github/scripts/web-shell-visuals-publish.mjs
+++ b/.github/scripts/web-shell-visuals-publish.mjs
@@ -122,10 +122,7 @@ export function selectImages(candidates, opts = {}) {
* whether we render at all; this decides whether a "nothing changed" RESULT
* deserves a second look.
*/
-const RENDER_SHAPING_PREFIXES = [
- 'packages/web-shell/client/',
- 'packages/webui/src/',
-];
+const RENDER_SHAPING_PREFIXES = ['packages/web-shell/client/'];
/**
* Extensions that change what a view LOOKS like. Deliberately narrow: a `.ts`
diff --git a/.github/scripts/web-shell-visuals-publish.test.mjs b/.github/scripts/web-shell-visuals-publish.test.mjs
index 302e9ced935..bb7aa8488a2 100644
--- a/.github/scripts/web-shell-visuals-publish.test.mjs
+++ b/.github/scripts/web-shell-visuals-publish.test.mjs
@@ -783,7 +783,6 @@ test('selectRenderShapingFiles keeps rendered .tsx/.css/.svg and drops logic/tes
const { files, total } = selectRenderShapingFiles([
'packages/web-shell/client/components/WelcomeScreen.tsx',
'packages/web-shell/client/components/worktree.module.css',
- 'packages/webui/src/ui/button.tsx',
'packages/web-shell/client/assets/icons/plan.svg',
// Dropped: not a rendered extension...
'packages/web-shell/client/hooks/useWorktree.ts',
@@ -804,9 +803,8 @@ test('selectRenderShapingFiles keeps rendered .tsx/.css/.svg and drops logic/tes
'packages/web-shell/client/assets/icons/plan.svg',
'packages/web-shell/client/components/WelcomeScreen.tsx',
'packages/web-shell/client/components/worktree.module.css',
- 'packages/webui/src/ui/button.tsx',
]);
- assert.equal(total, 4);
+ assert.equal(total, 3);
});
test('selectRenderShapingFiles caps the listed paths but reports the true total', () => {
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f98f81133d1..cb36268de69 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1122,6 +1122,10 @@ jobs:
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"
run: 'npm run check:lockfile'
+ - name: 'Check retired WebUI dependency'
+ if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"
+ run: 'npm run check:no-webui'
+
- name: 'Check desktop workspace isolation'
if: "${{ needs.classify_pr.outputs.skip_ci != 'true' && steps.ci_profile.outputs.ci_profile == 'full' }}"
run: 'npm run check:desktop-isolation'
diff --git a/.github/workflows/qwen-triage.yml b/.github/workflows/qwen-triage.yml
index 73a7114a4fc..cccc7a3c5f6 100644
--- a/.github/workflows/qwen-triage.yml
+++ b/.github/workflows/qwen-triage.yml
@@ -3547,7 +3547,7 @@ jobs:
has_vitest_config() {
# Mirror vitest's own resolution: it accepts vitest.config AND
# vite.config in six extensions. A narrower probe skips runnable
- # packages (packages/webui's only config is vite.config.ts).
+ # packages (some packages only have vite.config.ts).
local n e
for n in vitest.config vite.config; do
for e in ts mts cts js mjs cjs; do
diff --git a/.github/workflows/release-vscode-companion.yml b/.github/workflows/release-vscode-companion.yml
index 7ea43856830..4ae6a6ff521 100644
--- a/.github/workflows/release-vscode-companion.yml
+++ b/.github/workflows/release-vscode-companion.yml
@@ -100,6 +100,16 @@ jobs:
run: |-
npm ci
+ - name: 'Verify published export renderer'
+ run: |-
+ renderer_version=$(node -p "require('./package.json').version")
+ published_renderer=$(mktemp)
+ trap 'rm -f "${published_renderer}"' EXIT
+ curl --fail --location --silent --show-error --retry 3 \
+ "https://unpkg.com/@qwen-code/qwen-code@${renderer_version}/export-transcript-document.js" \
+ --output "${published_renderer}"
+ cmp packages/web-templates/src/export-html/dist/export-transcript-document.js "${published_renderer}"
+
- name: 'Get the version'
id: 'version'
working-directory: 'packages/vscode-ide-companion'
@@ -152,12 +162,6 @@ jobs:
IS_PREVIEW: '${{ steps.vars.outputs.is_preview }}'
MANUAL_VERSION: '${{ inputs.version }}'
- - name: 'Build webui dependency'
- if: |-
- ${{ github.event.inputs.force_skip_tests != 'true' }}
- run: |
- npm run build --workspace=@qwen-code/webui
-
- name: 'Run Tests'
if: |-
${{ github.event.inputs.force_skip_tests != 'true' }}
@@ -231,6 +235,8 @@ jobs:
RELEASE_VERSION: '${{ needs.prepare.outputs.release_version }}'
shell: 'bash'
run: |-
+ # Keep the verified, prebuilt export renderer reference pinned to the
+ # published CLI version from the selected source ref.
npm run release:version -- "${RELEASE_VERSION}"
- name: 'Prepare VSCode Extension'
diff --git a/.github/workflows/web-shell-visuals.yml b/.github/workflows/web-shell-visuals.yml
index 2ed48a24405..1c9b7790f08 100644
--- a/.github/workflows/web-shell-visuals.yml
+++ b/.github/workflows/web-shell-visuals.yml
@@ -25,11 +25,6 @@ on:
- 'packages/web-shell/package.json'
- 'packages/web-shell/vite.config.ts'
- 'packages/web-shell/playwright.visuals.config.ts'
- # The visuals dev server aliases the shared web UI library into the
- # rendered bundle (see the `resolve.alias` block in
- # packages/web-shell/vite.config.ts), so a change to its components/hooks
- # must also refresh the preview.
- - 'packages/webui/src/**'
# NOTE: packages/sdk-typescript/src/** is deliberately NOT a trigger.
# It is aliased in too, but the visuals render against a *mock* daemon, so
# the SDK's transport/client layer is stubbed at the network boundary and
diff --git a/.qwen/skills/find-simplifications/SKILL.md b/.qwen/skills/find-simplifications/SKILL.md
index 7ee5987f684..cc481a5aca3 100644
--- a/.qwen/skills/find-simplifications/SKILL.md
+++ b/.qwen/skills/find-simplifications/SKILL.md
@@ -61,9 +61,8 @@ this repo**. The release workflow npm-publishes `@qwen-code/audio-capture`
and the eight `@qwen-code/channel-*` packages with `--access public`, so a
symbol re-exported by their package entry is reachable the same way. No grep
inside this repo can prove such a symbol has no consumer. The same is true
-of three surfaces whose consumers are not imports at all: `packages/webui` is
-npm-published under its own name (`publishConfig.access: public`, no
-`private`), and `packages/core/vendor/**` and `packages/web-shell` ship inside
+of two surfaces whose consumers are not imports at all:
+`packages/core/vendor/**` and `packages/web-shell` ship inside
the published `@qwen-code/qwen-code` tarball — `packages/core/package.json`
lists `vendor` in `files`, and `scripts/copy_bundle_assets.js` copies both
`vendor/` and `web-shell/dist` into the bundle, where `qwen serve` hands the
@@ -83,14 +82,13 @@ Landable.
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `packages/cli/src` — the whole package (`generated/` stays under the Never-a-target row below; `**/*.sb` stays under the Report-only row below; `i18n/locales/**` and `commands/extensions/examples/**` stay under the Report-only row below; `**/*.test.ts(x)`, `**/*.spec.ts(x)`, `**/__snapshots__/**` are never targets, always searched as consumers) | Landable |
| `scripts/`, `esbuild.config.js`, `eslint.legacy-filenames.mjs`, root manifests | Landable |
-| Whole files or directories nothing consumes by any mechanism named above — no import and no runtime read, loader, manifest, or tool config — anywhere outside `packages/core/src`, `packages/audio-capture`, `packages/channels`, `packages/sdk-*`, `packages/acp-bridge`, `packages/vscode-ide-companion`, `packages/chrome-extension`, `packages/zed-extension`, `packages/webui`, `packages/web-shell`, `packages/core/vendor`, `.github`, and the Never-a-target row below | Landable |
+| Whole files or directories nothing consumes by any mechanism named above — no import and no runtime read, loader, manifest, or tool config — anywhere outside `packages/core/src`, `packages/audio-capture`, `packages/channels`, `packages/sdk-*`, `packages/acp-bridge`, `packages/vscode-ide-companion`, `packages/chrome-extension`, `packages/zed-extension`, `packages/web-shell`, `packages/core/vendor`, `.github`, and the Never-a-target row below | Landable |
| `docs/users/**`, `docs/developers/**`, `docs/index.md`, `docs/_meta.ts`, `packages/cli/src/i18n/locales/**`, `packages/cli/src/commands/extensions/examples/**` — as whole files or directories | **Report-only** — copied into the published tarball (`scripts/prepare-package.js` copies the locales and extension examples; `scripts/copy_bundle_assets.js` copies `docs/users/` for qc-helper) and consumed by the published docs site (`docs-site/scripts/link-public-docs.mjs` symlinks `docs/users/` and `docs/developers/` into the Nextra build per `PUBLIC_DOC_ROOTS` and copies `docs/index.md` and `docs/_meta.ts`; the site discovers pages by walking that tree); consumers are runtime reads — qc-helper's doc paths, the i18n loader's segment-assembled `import()`, `/extensions new` scaffolds — never imports. Individual orphan locale keys stay class-4 candidates: their proof greps the literal key, naming its mechanism |
| `docs-site/` | **Report-only** — standalone published-site app, not a workspace member; route files are consumed by Next.js filesystem routing and an out-of-repo deploy, never imports, and no in-repo CI builds it |
| Tracked `.qwen/skills/**`, `.qwen/agents/**`, `.qwen/e2e-tests/**`, `docs/design/**`, `docs/plans/**` | **Report-only** — consumed by the skill loader, agent definitions, and process readers (including `AGENTS.md` itself), never imports |
| `AGENTS.md`, `CLAUDE.md`, `SECURITY.md`, `CONTRIBUTING.md`, `.prettierrc.json`, `.prettierignore`, `.editorconfig`, `.nvmrc`, `.npmrc`, `.yamllint.yml` | **Report-only** — consumed by external tooling through filename convention (agent harnesses, GitHub's security-policy UI, prettier and yamllint config auto-discovery, nvm, editors); never imports, and an in-repo grep for them measures only prose |
| Anything under `packages/core/src` | **Report-only** — published |
| `packages/audio-capture`, `packages/channels` | **Report-only** — npm-published (`--access public`) |
-| `packages/webui` | **Report-only** — npm-published under its own name; consumers import from the registry |
| `packages/core/vendor/**`, `packages/web-shell` | **Report-only** — shipped inside the published `@qwen-code/qwen-code` tarball / served to browsers by `qwen serve`; consumers are bundled or browser-side, never imports |
| `packages/cli/src/utils/**/*.sb` | **Report-only** — copied into the published bundle by extension glob (`scripts/copy_bundle_assets.js` copies `packages/**/*.sb`, `scripts/prepare-package.js` lists `'*.sb'`) and read at runtime through a segment-assembled path (`resolveSeatbeltProfileFile()` builds `sandbox-macos-${profile}.sb`); consumers are never imports, and a basename grep measures zero |
| Any key in `packages/cli/src/config/settingsSchema.ts` | **Report-only** — see below |
diff --git a/.qwen/skills/repo-hygiene/references/scan.md b/.qwen/skills/repo-hygiene/references/scan.md
index 1b555373b3f..56815b9719d 100644
--- a/.qwen/skills/repo-hygiene/references/scan.md
+++ b/.qwen/skills/repo-hygiene/references/scan.md
@@ -108,13 +108,12 @@ hygiene findings.
- Correct: multi-SDK behavior is consistent, protocol fields match the
TS SDK, bridge error mapping preserves the original error class.
-- **ui-apps** — the three UI apps (`packages/desktop-shell/`,
- `packages/web-shell/`, `packages/webui/`).
+- **ui-apps** — the two UI apps (`packages/desktop-shell/` and
+ `packages/web-shell/`).
- `desktop-shell`: a thin Tauri shell around Web Shell (window
management, process lifecycle, signing, updates).
- `web-shell`: a client React app (`client/`), a Vite build, and a
daemon proxy; ships as an embeddable component.
- - `webui`: a lightweight web client consuming daemon REST endpoints.
- Correct: IPC message shapes match both ends, routes resolve, state
cleans up on unmount, portal roots are scoped.
diff --git a/docs/design/web-shell/chat-transcript-contract-prevalidation.md b/docs/design/web-shell/chat-transcript-contract-prevalidation.md
index 28d6198ddbd..934248ef0be 100644
--- a/docs/design/web-shell/chat-transcript-contract-prevalidation.md
+++ b/docs/design/web-shell/chat-transcript-contract-prevalidation.md
@@ -59,13 +59,13 @@ ChatTranscriptModel
### 2.1 当前生产边界
-| 消费端 | 当前事实 | 本方案处理 |
-| ----------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
-| Web/Qwen Server | daemon state 经 SDK reducer 产生 `DaemonTranscriptBlock[]`,完整 WebShell 渲染 | 保持生产路径不变;作为语义和兼容基线 |
-| Qwen Tauri Desktop | 构建并复制同一 WebShell 产物 | 不增加 Desktop adapter;MR1 不认证安装产物行为 |
-| VS Code | MR2A 继续由现有 legacy `MessageList` 渲染;不注册 transcript update、不引入 `@qwen-code/web-shell`、不发布死 feature flag | 该 render site 是 MR2B 的设计接入 seam,不新增空生产 adapter |
-| HTML Export | CLI、Web API 和 VS Code 导出均把原始 records 交给 document projector,并使用版本绑定的产品模板 | 产品路径已收敛;无 records 的公共调用保留 legacy 兼容 |
-| OpenWork/Craft Electron | 独立聊天实现 | 本方案范围外 |
+| 消费端 | 当前事实 | 本方案处理 |
+| ----------------------- | ------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
+| Web/Qwen Server | daemon state 经 SDK reducer 产生 `DaemonTranscriptBlock[]`,完整 WebShell 渲染 | 保持生产路径不变;作为语义和兼容基线 |
+| Qwen Tauri Desktop | 构建并复制同一 WebShell 产物 | 不增加 Desktop adapter;MR1 不认证安装产物行为 |
+| VS Code | MR2A 继续由现有 legacy `MessageList` 渲染;不注册 transcript update、不引入 `@qwen-code/web-shell`、不发布死 feature flag | 该 render site 是 MR2B 的设计接入 seam,不新增空生产 adapter |
+| HTML Export | CLI、Web API 和 VS Code 导出均把原始 records 交给 document projector,并使用版本绑定的产品模板 | 产品路径已收敛;legacy renderer 已随 `@qwen-code/webui` 退休移除,`toHtml` 要求 records(见 §12.4) |
+| OpenWork/Craft Electron | 独立聊天实现 | 本方案范围外 |
当前 `WebShellTranscript`:
@@ -99,7 +99,7 @@ ChatTranscriptModel
2. 复用现有 SDK reducer 和 `projectChatRecordsToDaemonTranscript()`,不复制 replay 规则;
3. 为 block、renderer item 和宿主动作建立可审计的稳定 identity;
4. 让 VS Code 复用 WebShell 聊天时间线,同时保留其 composer、权限、会话和原生操作;
-5. 让 HTML Export 使用版本化、安全、资源有界且不主动联网的文档输入;
+5. 让 HTML Export 使用版本化、安全、资源有界且不从文档内容主动联网的输入;
6. 保证 Web Shell interactive/readonly 和 Tauri Desktop 不发生功能回归;
7. 通过 fixture、hash、capability matrix 和自动化门禁使每个架构结论可重复验证;
8. 允许 VS Code 与 HTML 两条消费路径通过 MR2A/MR2B 独立评审、灰度、观察和回滚。
@@ -559,6 +559,7 @@ JSON Schema 无法表达 UTF-8 总字节、总文本、总图片和 envelope 预
- Markdown 图片与结构化 images 共用 MIME/来源/字节策略;
- code、diff、shell、command 和普通文本中的 URL 字面量只作为文本,不触发资源加载;
- 外部链接仅在明确用户点击时导航,去除 credential,并按策略处理 query/fragment;
+- credential 去除的实际作用域是 http(s):自由文本中的内嵌 URL 由 `sanitizeEmbeddedUrls` 逐个改写,Markdown link/autolink 由 `normalizeNavigableUrl` 处理,两者都清空 userinfo、query 和 fragment;非 navigable scheme(`ssh user:pw@host`)、裸参数形式的凭据(`-pSECRET`、`--token=...`)以及 `code` / `inlineCode` 节点内的内容按原文导出。document boundary 不是通用 secret scanner,这条边界必须与代码保持一致,不能在文档里给出更强的承诺;
- 超限时在富解析前输出安全占位和 diagnostic,不继续容错解析危险内容。
### 10.8 document mode
@@ -572,11 +573,12 @@ document mode 必须:
- Markdown 远程图片不请求网络,危险 HTML/SVG 不执行;
- Mermaid 限制、超时和 fallback 只在 document mode 启用,不能污染 interactive/readonly 的全局配置或缓存;
- Mermaid、代码高亮、diff 和 chart 失败时保留可复制源码;
-- 不加载需要 `unsafe-eval`、远程 WASM、远程 grammar、字体或动态 renderer 的资源。
+- 除版本绑定的 renderer 外,不加载需要 `unsafe-eval`、远程 WASM、远程 grammar、字体或其他动态资源。
-### 10.9 CSP 与零网络
+### 10.9 CSP 与登记网络
HTML 使用与 CLI build 精确绑定的 renderer。禁止 `latest`、版本范围和运行时远程解析。
+只有已将 renderer asset 发布到 npm 的 CLI 版本才能打开导出;两次发布之间的 source build 按设计 fail closed。
最低安全要求:
@@ -585,7 +587,7 @@ HTML 使用与 CLI build 精确绑定的 renderer。禁止 `latest`、版本范
- `base-uri 'none'`、`form-action 'none'`;
- images 只允许批准的 `data:` 或明确登记的同包资源;
- script/style 使用 nonce/hash 或等价静态策略;若现有 React 需要 style attribute,只允许 `style-src-attr` 的最小例外,DTO 不接受 style 字段;
-- V1 优先内联 renderer 必需资源;打开本地 HTML 后不得产生未登记 subrequest;
+- renderer(包含 React runtime)只允许从 unpkg 的精确 npm 版本 URL 加载,并校验最终发布字节的 SRI;打开本地 HTML 后不得产生其他未登记 subrequest;
- 浏览器测试拦截打开、展开、Markdown/Mermaid、主题和打印期间的全部请求;任何未登记请求或 CSP violation 立即失败。
### 10.10 失败、完整性与 canary
@@ -695,7 +697,7 @@ MR2A 中每项生产代码必须有 HTML 产品消费者。实施顺序:
2. **export builder**:实现 record policy、canonical projection、allowlist、opaque ID、metadata、budget 和 diagnostics;
3. **document mode**:实现非虚拟化/只读/无动作 renderer,Mermaid 限制仅在此 mode;
4. **HTML wiring**:CLI、Web API、VS Code `/export html` 和 integration runner 复用同一产品模板及版本绑定 renderer;
-5. **browser/security gates**:CSP、零网络、canary、最大预算和版本失败测试;
+5. **browser/security gates**:CSP、登记网络、canary、最大预算和版本失败测试;
6. **candidate evidence**:direct-daemon/ACP identity 只保留在 integration helper,不创建 VS Code 生产 adapter;
7. **gate state**:HTML capability 可标 PASS,但 `selectedVscodePath: null`、`overall: "fail"` 保持不变。
@@ -723,6 +725,17 @@ MR2B 从 MR2A 之后开始,并由真实 VS Code consumer 驱动:
MR2A 先独立收敛产品 HTML Export。MR2B 再接入 VS Code live timeline,避免 renderer、transport、host actions、VSIX 与许可证变更挤入同一评审。JSON Schema 无法表达的 credential URL、脱敏 path、总字节和资源预算继续由小型语义安全层负责,不恢复重复的逐字段结构 validator。
+### 12.4 legacy HTML renderer 退休
+
+MR2A 落地后,`@qwen-code/webui` 只剩 HTML Export 的 legacy 回退这一个消费者。该 package 随后被整体退休,legacy renderer 一并移除。删除证据:
+
+1. **无剩余产品消费者**:CLI `/export html`、Web API `session-export` 和 VS Code `/export html` 都无条件传入原始 records,因此 legacy 分支在产品路径上不可达;
+2. **接口收紧**:`toHtml(sessionData, originalRecords)` 的第二参改为必填,`loadHtmlTemplate` 与 `injectDataIntoHtmlTemplate` 连同 UMD/CDN 模板一并删除,document renderer 成为唯一 HTML 导出实现;
+3. **回退被显式拒绝而非静默降级**:integration runner 遇到 legacy exported JSONL 时直接报错,要求提供 source ChatRecord JSONL,避免用一条未经 allowlist 的路径渲染旧文件;
+4. **防回归**:CI 增加 `check:no-webui`,拒绝重新引入该 package 或其依赖。
+
+本节只覆盖 legacy HTML renderer。VS Code legacy `MessageList` 不在此范围内,其移除仍受 §15 的观察期与删除证据约束。
+
## 13. 验证架构与测试矩阵
```mermaid
@@ -764,12 +777,12 @@ MR1 不以源码文本断言认证 Desktop 打包行为。Web/Tauri 的现有构
| VS Code | MR2A 验证 legacy timeline 与 `/export html`;MR2B 验证选定路径、scope/generation、callbacks、feature flag、legacy parity |
| Web Shell | interactive/readonly raw 兼容、document safe-only、render/action identity |
| Export builder | record policy、per-kind allowlist、opaque IDs、metadata、diagnostic、version |
-| Browser | schema failure、zero network、CSP、canary、find/copy/print、最大预算 |
+| Browser | schema failure、登记网络、CSP、canary、find/copy/print、最大预算 |
| Packaging | Web/Tauri regression、VSIX 三平台、CLI renderer 版本绑定、integration runner 收敛 |
Passing test 也必须反向审计:测试是否断言了正确语义、是否加载当前构建产物、是否真的覆盖真实消费者,不能用静态 source assertion 替代浏览器或 VSIX 行为验证。
-当前 MR2A 验证结果:SDK、Core、CLI、Web Shell、VS Code `/export html` 聚焦测试和 direct-daemon/ACP integration candidate gate 已通过;产品 HTML 已完成构建、Node 侧安全断言和真实 Chromium browser gate,concurrent runner 也复用同一产品收集、归一化和 formatter。browser gate 已覆盖最大文档、真实产品入口、零网络、主动 CSP 违规、canary、搜索、复制、打印、远程资源降级和 epoch 时间戳排除。VS Code live timeline、scope/generation/reconnect、宿主动作、VSIX 与 packaged artifact 证据全部属于 MR2B。
+当前 MR2A 验证结果:SDK、Core、CLI、Web Shell、VS Code `/export html` 聚焦测试和 direct-daemon/ACP integration candidate gate 已通过;产品 HTML 已完成构建、Node 侧安全断言和真实 Chromium browser gate,concurrent runner 也复用同一产品收集、归一化和 formatter。browser gate 已覆盖最大文档、真实产品入口、登记网络、主动 CSP 违规、canary、搜索、复制、打印、远程资源降级和 epoch 时间戳排除。VS Code live timeline、scope/generation/reconnect、宿主动作、VSIX 与 packaged artifact 证据全部属于 MR2B。
## 14. 门禁
@@ -866,6 +879,6 @@ Web/Qwen 和 Tauri 不迁移。若 MR2A/MR2B 对共享组件的改动导致默
- HTML 产品路径与 integration runner 不再维护第二套 renderer;
- Web/Qwen Server 和 Tauri Desktop 默认行为无回归;
- security、network、budget、CSP、version、VSIX/CLI packaging 和观察期完成;
-- legacy HTML renderer 与 VS Code timeline 只有在各自有删除证据时才移除;
+- legacy HTML renderer 已移除,删除证据见 §12.4;VS Code legacy timeline 仍只有在有删除证据时才移除;
- 未引入新的跨宿主 ChatPanel 包、通用消息模型或 OpenWork overlay;
- 后续任何公共契约变化继续更新本文档,不创建平行设计来源。
diff --git a/docs/developers/architecture.md b/docs/developers/architecture.md
index f3ba6ee0be6..7c6a2c88786 100644
--- a/docs/developers/architecture.md
+++ b/docs/developers/architecture.md
@@ -83,8 +83,7 @@ an HTTP daemon. See the
| `packages/core` | UI-independent agent orchestration, model-provider integration, prompt and context construction, tool registration and execution, permissions, sessions, memory, telemetry, and shared services. |
| `packages/acp-bridge` | ACP channel lifecycle, session multiplexing, event delivery, permission mediation, process spawning, and the filesystem seam shared by daemon and adapter hosts. |
| `packages/sdk-typescript` | Programmatic process execution through `query()` plus HTTP/SSE clients and transcript projection for `qwen serve`. |
-| `packages/webui` | Shared React components and the daemon React adapter built on the TypeScript SDK. |
-| `packages/web-shell` | The terminal-style browser UI built on `packages/webui` and the daemon SDK. |
+| `packages/web-shell` | The browser UI and daemon React adapter built on the TypeScript SDK. |
| `packages/web-templates` | Web templates packaged as embeddable JavaScript and CSS strings. |
| `packages/audio-capture` | Native microphone capture for voice input. |
| `packages/channels` | The shared channel runtime and platform adapters for messaging services. |
@@ -111,8 +110,8 @@ of the interactive, headless, ACP, daemon, channel, or maintenance flows.
Presentation remains outside the core runtime:
- the Ink TUI renders local interactive sessions;
-- `packages/webui` adapts daemon state to React providers and hooks;
-- `packages/web-shell` provides the browser terminal experience;
+- `packages/web-shell` adapts daemon state to React providers and hooks and
+ provides the browser experience;
- IDE and channel packages translate host-specific events into shared client or
bridge contracts.
@@ -153,8 +152,8 @@ The TypeScript SDK exposes two client styles:
- `query()` starts and controls a Qwen Code process for programmatic local use;
- daemon clients communicate with `qwen serve` over HTTP and SSE.
-`packages/webui` builds a React state layer on the daemon client, and
-`packages/web-shell` builds the browser UI on that state layer. Other clients,
+`packages/web-shell` builds a React state layer and browser UI on the daemon
+client. Other clients,
including IDE integrations and daemon-managed channels, reuse the same SDK and
event contracts instead of importing server implementation code.
diff --git a/docs/developers/daemon-client-adapters/web-ui.md b/docs/developers/daemon-client-adapters/web-shell.md
similarity index 86%
rename from docs/developers/daemon-client-adapters/web-ui.md
rename to docs/developers/daemon-client-adapters/web-shell.md
index 2aa022fa371..c4c23856ccd 100644
--- a/docs/developers/daemon-client-adapters/web-ui.md
+++ b/docs/developers/daemon-client-adapters/web-shell.md
@@ -1,4 +1,4 @@
-# Daemon Web UI Adapter
+# Web Shell Daemon Adapter
## Goal
@@ -26,16 +26,16 @@ The split is:
- `normalizeDaemonEvent()` converts daemon wire events into UI events.
- `createDaemonTranscriptStore()` reduces UI events into transcript blocks.
-React clients can use the optional `@qwen-code/webui` binding:
+React clients can use the binding exported by Web Shell:
```tsx
import {
DaemonSessionProvider,
- useDaemonActions,
- useDaemonConnection,
- useDaemonPendingPermissions,
- useDaemonTranscriptBlocks,
-} from '@qwen-code/webui';
+ useActions,
+ useConnection,
+ usePendingPermissions,
+ useTranscriptBlocks,
+} from '@qwen-code/web-shell/daemon-react-sdk';
```
Minimal React shape:
@@ -51,7 +51,7 @@ function App() {
}
function Transcript() {
- const blocks = useDaemonTranscriptBlocks();
+ const blocks = useTranscriptBlocks();
return blocks.map((block) => );
}
```
@@ -105,13 +105,15 @@ Ink rendering.
- The native `qwen` TUI remains direct and unchanged.
- `--acp`, channel, and IDE paths remain unchanged by default.
- The SDK UI core is additive.
-- The WebUI React binding is optional and only runs in clients that import it.
+- The Web Shell React binding is optional and only runs in clients that import
+ it.
- Removed daemon TUI spike code should not be treated as a product migration.
## Follow-Ups
-- Add a daemon-served local `/web` POC or equivalent same-origin web app.
-- Build first-class chat and terminal renderers on top of transcript blocks.
+- Keep the daemon-served Web Shell and embedded IDE host behavior aligned.
+- Continue building first-class chat and terminal renderers on transcript
+ blocks.
- Add richer typed events only where existing daemon events are too low-level
for stable browser UI behavior.
- Consider a dedicated `@qwen-code/daemon-ui-core` package if non-SDK consumers
diff --git a/docs/developers/daemon/00-index.md b/docs/developers/daemon/00-index.md
index da582cd8077..2483cc1419a 100644
--- a/docs/developers/daemon/00-index.md
+++ b/docs/developers/daemon/00-index.md
@@ -97,7 +97,7 @@ Use these anchors when moving from the docs into the latest `main` code:
| Capabilities | `packages/cli/src/serve/capabilities.ts`, `mcp_server_restart_refused.reason`, `MCP_RESTART_REFUSED_REASONS.has` | [`11`](./11-capabilities-versioning.md) |
| Auth and device flow | `packages/cli/src/serve/auth.ts`, `packages/cli/src/serve/auth/device-flow.ts` | [`12`](./12-auth-security.md) |
| TypeScript SDK daemon client | `packages/sdk-typescript/src/daemon/{DaemonClient,DaemonSessionClient,DaemonAuthFlow,sse,events,types}.ts`, `MCP_RESTART_DEFAULT_TIMEOUT_MS` | [`13`](./13-sdk-daemon-client.md) |
-| Shared UI transcript layer | `DaemonUiEventType`, `DaemonSessionProvider`, `packages/webui/src/daemon/` | [`13`](./13-sdk-daemon-client.md), [`14`](./14-cli-tui-adapter.md), [`../daemon-ui/README.md`](../daemon-ui/README.md) |
+| Shared UI transcript layer | `DaemonUiEventType`, `DaemonSessionProvider`, `packages/web-shell/client/daemon/` | [`13`](./13-sdk-daemon-client.md), [`14`](./14-cli-tui-adapter.md), [`../daemon-ui/README.md`](../daemon-ui/README.md) |
| Channels and IDE adapters | `packages/channels/`, `packages/vscode-ide-companion/src/services/daemonIdeConnection.ts` | [`15`](./15-channel-adapters.md), [`16`](./16-vscode-ide-adapter.md) |
## What is intentionally out of scope
@@ -136,7 +136,7 @@ Use these anchors when moving from the docs into the latest `main` code:
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| TypeScript SDK daemon client | `DaemonClient`, `DaemonSessionClient`, `DaemonAuthFlow`, SSE parser, event reducers, feature preflight, and UI transcript exports are documented. | [`13`](./13-sdk-daemon-client.md) |
| Shared UI transcript layer | SDK `daemon/ui/*` normalizes daemon events into 42 UI semantic event types, reduces them into transcript blocks, and provides renderers/conformance helpers. | [`14`](./14-cli-tui-adapter.md), [`../daemon-ui/README.md`](../daemon-ui/README.md), [`../daemon-ui/MIGRATION.md`](../daemon-ui/MIGRATION.md) |
-| Web UI daemon consumer | `packages/webui/src/daemon/` consumes the SDK transcript store through React providers and adapters. | [`14`](./14-cli-tui-adapter.md), [`../daemon-client-adapters/web-ui.md`](../daemon-client-adapters/web-ui.md) |
+| Web Shell daemon consumer | `packages/web-shell/client/daemon/` consumes the SDK transcript store through React providers and adapters. | [`14`](./14-cli-tui-adapter.md), [`../daemon-client-adapters/web-shell.md`](../daemon-client-adapters/web-shell.md) |
| CLI TUI / channels / VS Code | Legacy paths still exist; migration to shared transcript primitives is documented as follow-up work, not completed behavior. | [`14`](./14-cli-tui-adapter.md), [`15`](./15-channel-adapters.md), [`16`](./16-vscode-ide-adapter.md) |
### Reference and operations
diff --git a/docs/developers/daemon/01-architecture.md b/docs/developers/daemon/01-architecture.md
index 081c0339fdd..d7109b95128 100644
--- a/docs/developers/daemon/01-architecture.md
+++ b/docs/developers/daemon/01-architecture.md
@@ -13,7 +13,7 @@ This doc gives the **system-level picture** that the rest of this documentation
```mermaid
flowchart LR
subgraph clients["Clients"]
- WUI["Web UI (packages/webui/src/daemon)"]
+ WUI["Web Shell (packages/web-shell/client/daemon)"]
TUI["CLI TUI (packages/cli/src/ui/daemon)"]
IDE["VS Code IDE (packages/vscode-ide-companion)"]
CH["Channel bots (DingTalk / WeChat / Telegram / Feishu)"]
@@ -98,7 +98,7 @@ flowchart TB
end
subgraph adapters["Adapters"]
- WUIP["webui/src/daemon/ DaemonSessionProvider.tsx"]
+ WUIP["web-shell/client/daemon/ DaemonSessionProvider.tsx"]
TUIA["cli/src/ui/daemon/ daemon-tui-adapter.ts"]
CHB["channels/base/ DaemonChannelBridge.ts"]
DT["channels/dingtalk"]
diff --git a/docs/developers/daemon/08-session-lifecycle.md b/docs/developers/daemon/08-session-lifecycle.md
index 29d92fc2760..61703c865fa 100644
--- a/docs/developers/daemon/08-session-lifecycle.md
+++ b/docs/developers/daemon/08-session-lifecycle.md
@@ -57,7 +57,7 @@ Under `sessionScope: 'thread'`, each thread can mint a distinct session. The cal
`X-Qwen-Client-Id` is **optional** but **strongly recommended**. The daemon does not generate one on the caller's behalf — clients pick their own and reuse it across requests so the daemon can attribute votes, audit events, and detect reconnects.
-Each independent controller should use a distinct, stable ID. The WebUI generates IDs with a `webui_` prefix by default. A host and an embedded WebShell should share an ID only when they intentionally act as one logical controller; once shared, daemon logs cannot distinguish which one originated a request.
+Each independent controller should use a distinct, stable ID. Web Shell preserves the historical `webui_` prefix for compatibility. A host and an embedded Web Shell should share an ID only when they intentionally act as one logical controller; once shared, daemon logs cannot distinguish which one originated a request.
Validation rules:
diff --git a/docs/developers/daemon/13-sdk-daemon-client.md b/docs/developers/daemon/13-sdk-daemon-client.md
index 0d2e8c43892..3b7d3074cb1 100644
--- a/docs/developers/daemon/13-sdk-daemon-client.md
+++ b/docs/developers/daemon/13-sdk-daemon-client.md
@@ -324,7 +324,7 @@ set of primitives that turn daemon events into transcript blocks:
- Public constants include `DAEMON_PLAN_TOOL_CALL_ID`.
- `conformance.ts` contains the cross-host consistency test suite.
-The first production consumer is `packages/webui/src/daemon/` through React's
+The first production consumer is `packages/web-shell/client/daemon/` through React's
`DaemonSessionProvider`. See [`14-cli-tui-adapter.md`](./14-cli-tui-adapter.md)
for the detailed architecture, glossary, selector table, and relationship to
the legacy `DaemonTuiAdapter`.
diff --git a/docs/developers/daemon/14-cli-tui-adapter.md b/docs/developers/daemon/14-cli-tui-adapter.md
index 7902e22c035..b77d2cc7a87 100644
--- a/docs/developers/daemon/14-cli-tui-adapter.md
+++ b/docs/developers/daemon/14-cli-tui-adapter.md
@@ -11,7 +11,7 @@
- **Renderers** (`render.ts`, `terminal.ts`, `toolPreview.ts`): transcript blocks to HTML, terminal text, and tool preview strings. Hosts can use or replace them.
- **Conformance** (`conformance.ts`): cross-host consistency tests used when channel, TUI, and IDE surfaces migrate to these primitives.
-The first production consumer is **`packages/webui/src/daemon/`** ([#4328](https://github.com/QwenLM/qwen-code/pull/4328)). Its React `DaemonSessionProvider` and transcript adapter let the web UI connect directly to daemon HTTP+SSE instead of only rendering host `postMessage` traffic. CLI TUI, channel base, and VS Code IDE can reuse the same layer later; [`../daemon-ui/MIGRATION.md`](../daemon-ui/MIGRATION.md) documents the v2 incremental migration guide.
+The first production consumer was introduced in [#4328](https://github.com/QwenLM/qwen-code/pull/4328) and now lives in **`packages/web-shell/client/daemon/`**. Its React `DaemonSessionProvider` and transcript adapter let Web Shell connect directly to daemon HTTP+SSE. CLI TUI, channel base, and VS Code IDE can reuse the same layer; [`../daemon-ui/MIGRATION.md`](../daemon-ui/MIGRATION.md) documents the v2 incremental migration guide.
## Responsibilities
@@ -132,17 +132,19 @@ Hosts can stop at `(E)` and implement their own reducer, or consume `(G)` and th
## Consumers
-### `packages/webui/src/daemon/`
+### `packages/web-shell/client/daemon/`
This landed in [#4328](https://github.com/QwenLM/qwen-code/pull/4328).
-| File | Exports |
-| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| `DaemonSessionProvider.tsx` | React ``; `useDaemonSession()`, `useDaemonTranscriptStore()`, `useDaemonTranscriptState()`, `useDaemonTranscriptBlocks()`, `useDaemonPendingPermissions()`, `useDaemonActions()`, `useDaemonConnection()` hooks; `DaemonConnectionStatus`, `DaemonConnectionState`, `DaemonSessionContextValue` types |
-| `transcriptAdapter.ts` | Adapts SDK `DaemonTranscriptBlock` into the web UI's `UnifiedMessage`, including markdown streaming chunk merge and tool call summaries |
-| `index.ts` | Subpackage barrel |
+| File | Exports |
+| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `session/DaemonSessionProvider.tsx` | React ``; `useDaemonSession()`, `useDaemonTranscriptStore()`, `useDaemonTranscriptState()`, `useDaemonTranscriptBlocks()`, `useDaemonPendingPermissions()`, `useDaemonActions()`, `useDaemonConnection()` hooks; `DaemonConnectionStatus`, `DaemonConnectionState`, `DaemonSessionContextValue` types |
+| `session/index.ts` | Session barrel: provider, hooks, and session types |
+| `index.ts` | Subpackage barrel |
-The web UI can now connect directly to daemon HTTP+SSE and render a transcript. The old `ACPAdapter` host `postMessage` path remains available.
+The transcript adapter lives outside this directory: `packages/web-shell/client/adapters/transcriptAdapter.ts` exports only `extractPendingPermission(blocks): PermissionRequest | null`, lifting unresolved SDK permission blocks for host UIs. Transcript blocks themselves flow through the SDK `ui/*` layer and `useDaemonTranscriptBlocks()`; there is no `UnifiedMessage` adapter in Web Shell.
+
+The web UI can now connect directly to daemon HTTP+SSE and render a transcript. The old `ACPAdapter` host `postMessage` path was retired with the legacy WebUI workspace; webviews now embed Web Shell for rendering (see [`16-vscode-ide-adapter.md`](./16-vscode-ide-adapter.md)).
### Later migrations
@@ -163,9 +165,9 @@ The web UI can now connect directly to daemon HTTP+SSE and render a transcript.
## Dependencies
- Upstream wire types: `packages/sdk-typescript/src/daemon/events.ts` (see [`09-event-schema.md`](./09-event-schema.md)).
-- Real downstream consumer: `packages/webui/src/daemon/`.
+- Real downstream consumer: `packages/web-shell/client/daemon/`.
- Later migration targets: `packages/cli/src/ui/`, `packages/channels/base/`, and `packages/vscode-ide-companion/src/services/daemonIdeConnection.ts`.
-- Parallel references: [`../daemon-ui/README.md`](../daemon-ui/README.md), [`../daemon-ui/MIGRATION.md`](../daemon-ui/MIGRATION.md), and [`../daemon-client-adapters/web-ui.md`](../daemon-client-adapters/web-ui.md).
+- Parallel references: [`../daemon-ui/README.md`](../daemon-ui/README.md), [`../daemon-ui/MIGRATION.md`](../daemon-ui/MIGRATION.md), and [`../daemon-client-adapters/web-shell.md`](../daemon-client-adapters/web-shell.md).
## Configuration
@@ -176,7 +178,7 @@ The web UI can now connect directly to daemon HTTP+SSE and render a transcript.
## Caveats and known limits
- **`daemon-tui-adapter.ts` still exists**. It is the CLI package's legacy experimental adapter. New code should prefer SDK `ui/*`: `normalizeDaemonEvent`, `reduceDaemonTranscriptEvents`, and `DaemonTranscriptBlock`.
-- **CLI TUI, channel base, and VS Code IDE are not migrated yet**. They still maintain their own rendering logic. The `docs/developers/daemon-client-adapters/` directory still has `ide.md`, `channel-web.md`, and the historical `tui.md` draft; the newer `web-ui.md` covers the web UI adapter design.
+- **CLI TUI, channel base, and VS Code IDE are not migrated yet**. They still maintain their own rendering logic. The `docs/developers/daemon-client-adapters/` directory still has `ide.md`, `channel-web.md`, and the historical `tui.md` draft; the newer `web-shell.md` covers the web UI adapter design.
- **`eventId` is the primary ordering key**. `createdAt` remains as a deprecated alias (`clientReceivedAt`). New code should use `selectTranscriptBlocksOrderedByEventId(state)`. `MIGRATION.md` shows the code diff for switching from `createdAt` ordering to `eventId` ordering.
- **Unknown wire types normalize to `debug`**. They are no longer dropped as in the old adapter. Renderers do not show `debug` by default; hosts must opt in to display it.
- **Bundle size**: the `ui/*` subpackage is exported as an ESM subpath through `@qwen-code/sdk/daemon` and does not pull in React or DOM dependencies. React integration is only loaded when a web UI consumer uses `DaemonSessionProvider`.
@@ -188,6 +190,6 @@ The web UI can now connect directly to daemon HTTP+SSE and render a transcript.
- `packages/sdk-typescript/src/daemon/ui/normalizer.ts` (wire-to-UI mapping)
- `packages/sdk-typescript/src/daemon/ui/store.ts`, `render.ts`, `terminal.ts`, `toolPreview.ts`, `conformance.ts`
- `packages/sdk-typescript/src/daemon/index.ts` (`ui/*` re-export block)
-- `packages/webui/src/daemon/DaemonSessionProvider.tsx`, `transcriptAdapter.ts`
-- Upstream docs: [`../daemon-ui/README.md`](../daemon-ui/README.md), [`../daemon-ui/MIGRATION.md`](../daemon-ui/MIGRATION.md), [`../daemon-client-adapters/web-ui.md`](../daemon-client-adapters/web-ui.md)
+- `packages/web-shell/client/daemon/session/DaemonSessionProvider.tsx`, `packages/web-shell/client/adapters/transcriptAdapter.ts`
+- Upstream docs: [`../daemon-ui/README.md`](../daemon-ui/README.md), [`../daemon-ui/MIGRATION.md`](../daemon-ui/MIGRATION.md), [`../daemon-client-adapters/web-shell.md`](../daemon-client-adapters/web-shell.md)
- Context PRs: [#4328](https://github.com/QwenLM/qwen-code/pull/4328) (v1 transcript layer and web UI provider), [#4353](https://github.com/QwenLM/qwen-code/pull/4353) (v2 unified completeness follow-up)
diff --git a/docs/developers/daemon/16-vscode-ide-adapter.md b/docs/developers/daemon/16-vscode-ide-adapter.md
index 98b6ff0ee36..3b7c2ebfe29 100644
--- a/docs/developers/daemon/16-vscode-ide-adapter.md
+++ b/docs/developers/daemon/16-vscode-ide-adapter.md
@@ -85,7 +85,7 @@ exception paths call `onEndTurn('error')`.
### Webview bridging
-The connection class is **transport-only**. The actual VS Code integration lives in `packages/vscode-ide-companion/src/webview/providers/ChatWebviewViewProvider.ts` (and friends). The provider subscribes to the connection's callbacks and translates them into webview `postMessage` calls. The webview itself uses the shared `packages/webui/` component library to render — see Adapter Matrix in [`01-architecture.md`](./01-architecture.md).
+The connection class is **transport-only**. The actual VS Code integration lives in `packages/vscode-ide-companion/src/webview/providers/ChatWebviewViewProvider.ts` (and friends). The provider subscribes to the connection's callbacks and translates them into webview `postMessage` calls. The webview embeds Web Shell for rendering.
### Connect serialization
@@ -174,7 +174,7 @@ sequenceDiagram
- `packages/sdk-typescript/src/daemon/` — `DaemonClient`, `DaemonSessionClient` (the actual transport).
- VS Code extension API (`vscode.*`) — host APIs, quick-pick, webview.
-- `packages/webui/src/adapters/ACPAdapter.ts` — webview rendering of ACP-shaped messages relayed via `postMessage`.
+- `packages/web-shell/client/` — embedded webview rendering for daemon session events.
## Configuration
@@ -201,6 +201,6 @@ sequenceDiagram
- `packages/vscode-ide-companion/src/services/daemonIdeConnection.ts` (`createSdkDaemonSessionFactory`)
- `packages/vscode-ide-companion/src/types/connectionTypes.ts` (legacy `AcpConnectionState`)
- `packages/vscode-ide-companion/src/webview/providers/ChatWebviewViewProvider.ts` (webview bridge)
-- `packages/webui/src/adapters/ACPAdapter.ts` (webview ACP-message adapter)
+- `packages/web-shell/client/` (embedded Web Shell renderer)
- Draft design: [`../daemon-client-adapters/ide.md`](../daemon-client-adapters/ide.md)
- SDK reference: [`13-sdk-daemon-client.md`](./13-sdk-daemon-client.md)
diff --git a/docs/developers/examples/daemon-client-quickstart.md b/docs/developers/examples/daemon-client-quickstart.md
index eb365898bcc..444402fd3ac 100644
--- a/docs/developers/examples/daemon-client-quickstart.md
+++ b/docs/developers/examples/daemon-client-quickstart.md
@@ -247,7 +247,7 @@ const client = new DaemonClient({
const client = new DaemonClient({ baseUrl: 'https://your-host:4170' });
```
-The fallback strips leading/trailing whitespace (handy for `export QWEN_SERVER_TOKEN="$(cat token.txt)"` where `cat` adds a newline) and treats empty / whitespace-only values as unset (a stale `export QWEN_SERVER_TOKEN=""` won't accidentally send `Authorization: Bearer ` with no token). The fallback runs once at construction; later `process.env` mutations don't affect already-built clients. Browser bundles (e.g. via `@qwen-code/webui`) get `undefined` cleanly because `globalThis.process` doesn't exist there.
+The fallback strips leading/trailing whitespace (handy for `export QWEN_SERVER_TOKEN="$(cat token.txt)"` where `cat` adds a newline) and treats empty / whitespace-only values as unset (a stale `export QWEN_SERVER_TOKEN=""` won't accidentally send `Authorization: Bearer ` with no token). The fallback runs once at construction; later `process.env` mutations don't affect already-built clients. Browser bundles (e.g. via `@qwen-code/web-shell`) get `undefined` cleanly because `globalThis.process` doesn't exist there.
Wrong / missing tokens return `401` with a uniform body — the SDK throws `DaemonHttpError` on any 4xx/5xx from a route handler.
diff --git a/docs/developers/qwen-serve-protocol.md b/docs/developers/qwen-serve-protocol.md
index b01dae0c682..90544fbf787 100644
--- a/docs/developers/qwen-serve-protocol.md
+++ b/docs/developers/qwen-serve-protocol.md
@@ -22,7 +22,7 @@ Channel webhook ingress (`POST /channels/:channelName/webhooks/:source`) is sepa
When the flag is on, the global `bearerAuth` middleware gates **every normal API route** — including `/health` and `/capabilities`. Channel webhook ingress remains independently shared-secret-authenticated, and Web Shell document and asset routes remain pre-auth. An **unauthenticated** client therefore cannot pre-flight `caps.features` to discover that auth is required: the discovery surface for that case is the **401 response body** itself (uniform across bearer-gated routes per the [Authentication](#authentication) section). The `require_auth` capability tag is a **post-authentication confirmation** — once a client successfully authenticates and reads `/capabilities`, the tag's presence confirms the daemon was started with `--require-auth` (useful for audit / compliance UIs and for SDK clients to surface "this deployment is hardened" in a settings panel). Strict mutation routes accept trusted-loopback primary-listener requests, bearer-authenticated requests, or paired Local Control requests. Non-trusted token-less embeds still receive `401 { code: "token_required", error: "…" }`; with `--require-auth`, global bearer middleware rejects first with the legacy `Unauthorized` body.
-**`--allow-origin ` (T2.4 [#4514](https://github.com/QwenLM/qwen-code/issues/4514)).** Browser webuis hitting the daemon cross-origin are blocked by default — any request carrying an `Origin` header returns `403 {"error":"Request denied by CORS policy"}` because CLI/SDK clients never send `Origin` and the daemon treats its presence as a sign the request came from a browser context the operator has not opted into. Pass `--allow-origin ` (repeatable) at boot to install an allowlist instead of the wall. Each pattern is either:
+**`--allow-origin ` (T2.4 [#4514](https://github.com/QwenLM/qwen-code/issues/4514)).** Browser clients hitting the daemon cross-origin are blocked by default — any request carrying an `Origin` header returns `403 {"error":"Request denied by CORS policy"}` because CLI/SDK clients never send `Origin` and the daemon treats its presence as a sign the request came from a browser context the operator has not opted into. Pass `--allow-origin ` (repeatable) at boot to install an allowlist instead of the wall. Each pattern is either:
- The literal `*` — admit any origin. **Risky**: boot refuses when `*` is configured but no bearer token is set (any source: `--token`, `QWEN_SERVER_TOKEN`, or `--require-auth` which mandates a token at boot). The boot breadcrumb emits a stderr warning when `*` is in the list. **Recommendation**: pair with `--require-auth` on loopback binds so `/health` is also gated by the bearer — it's registered before the bearer middleware on loopback by default (so k8s/Compose probes can reach it without a token), and a `*` allowlist makes it reachable from any cross-origin browser. `--require-auth` still leaves the Web Shell static assets (`/`, `/assets/*`, and `/session/:id` document navigations) pre-auth on loopback by design — they are mounted before the bearer middleware — so under a `*` allowlist they remain readable from any cross-origin browser; `--no-web` removes that surface. On non-loopback binds the bearer is already mandatory at boot and `/health` is registered behind it. Normal API routes are bearer-gated, channel webhook ingress retains its own shared-secret gate, and Web Shell static assets (`/`, `/assets/*`, and `/session/:id` document navigations) remain pre-auth unless `--no-web` removes them.
- A canonical URL origin — `://[:]`. **No trailing slash, no path, no userinfo, no query.** Boot refuses with `InvalidAllowOriginPatternError` if the entry fails the round-trip `new URL(pattern).origin === pattern`; the error message names the bad pattern and the canonical form. Strict-by-intent: silent normalization (e.g. trimming a trailing `/`) would let typos slip through and accept ambiguous input. Without a token, HTTP(S) entries are limited to loopback hosts; a non-loopback browser origin requires a token because it can otherwise drive the full operator API, including code execution as the daemon user. Explicit browser-extension origins keep their existing tokenless local-automation path. Startup logs the authority granted to any tokenless allowed browser origin.
@@ -38,13 +38,13 @@ Access-Control-Max-Age: 86400
Access-Control-Expose-Headers: Retry-After, X-Qwen-Event-Epoch, X-Qwen-SSE-Stream-Id
```
-`Access-Control-Allow-Origin` echoes the request's origin verbatim (lowercase / uppercase as the browser sent it) rather than the literal `*`, even under the `*` pattern — browser caches key responses on it paired with `Vary: Origin`, and echoing leaves room to add `Access-Control-Allow-Credentials` in a later release without a schema change. The exposed headers let browser webuis honor retry hints, retain the SSE epoch, and correlate accepted physical streams. `Access-Control-Allow-Credentials` is **NOT** sent today: configured daemon credentials use bearer-in-`Authorization`, which works cross-origin without `credentials: 'include'`; trusted-loopback authority needs no browser credential.
+`Access-Control-Allow-Origin` echoes the request's origin verbatim (lowercase / uppercase as the browser sent it) rather than the literal `*`, even under the `*` pattern — browser caches key responses on it paired with `Vary: Origin`, and echoing leaves room to add `Access-Control-Allow-Credentials` in a later release without a schema change. The exposed headers let browser clients honor retry hints, retain the SSE epoch, and correlate accepted physical streams. `Access-Control-Allow-Credentials` is **NOT** sent today: configured daemon credentials use bearer-in-`Authorization`, which works cross-origin without `credentials: 'include'`; trusted-loopback authority needs no browser credential.
OPTIONS preflight requests (OPTIONS with `Access-Control-Request-Method` or `Access-Control-Request-Headers`) short-circuit with `204 No Content` plus the headers above. This is the conventional CORS pattern and is safe — the preflight only confirms which methods/headers the daemon will accept; the actual subsequent request still runs the Host gate and then either bearer/listener authority or the channel webhook shared-secret gate before any state is read or mutated. Plain OPTIONS requests from matched origins keep flowing downstream with CORS headers attached.
Origins that don't match the allowlist still get `403 {"error":"Request denied by CORS policy"}` — same envelope as the default wall, so clients that already parsed the wall's response don't have to special-case allowlist-deployed daemons. The reject path **does not** emit any `Access-Control-*` headers (the browser would ignore them, and emitting would indirectly advertise the allowlist size through header presence).
-The configured pattern list is intentionally NOT echoed in `/capabilities` — browser webui already knows its own origin (it called the daemon, after all), and surfacing the list would let an unauthenticated reader of `/capabilities` enumerate every trusted origin (useful recon for a misconfigured deployment). SDK clients gate on the `caps.features.allow_origin` tag for "this daemon honors cross-origin browser hits" without needing to know which specific origins.
+The configured pattern list is intentionally NOT echoed in `/capabilities` — a browser client already knows its own origin (it called the daemon, after all), and surfacing the list would let an unauthenticated reader of `/capabilities` enumerate every trusted origin (useful recon for a misconfigured deployment). SDK clients gate on the `caps.features.allow_origin` tag for "this daemon honors cross-origin browser hits" without needing to know which specific origins.
Loopback self-origin requests (e.g. the Web Shell calling the daemon at the same `127.0.0.1:port`) are handled by a **separate** Origin-strip shim that runs BEFORE the CORS middleware and removes the `Origin` header for `127.0.0.1:port` / `localhost:port` / `[::1]:port` / `host.docker.internal:port` or the exact bound loopback address and port. It also accepts the scheme-matched port-less forms that browsers send for default ports: `http://host` on port 80 and `https://host` on port 443. These requests pass through regardless of `--allow-origin` configuration — operators don't need to list the daemon's own port to make the Web Shell work.
@@ -282,7 +282,7 @@ Runtime status and ensure responses use this shape:
`session_load` and `session_resume` advertise the explicit-restore routes (`POST /session/:id/load` and `POST /session/:id/resume`). Older daemons return `404` for these paths, so SDK clients should pre-flight `caps.features` before calling. `unstable_session_resume` is still advertised as a deprecated alias for compatibility with SDKs that shipped while the underlying ACP method was named `connection.unstable_resumeSession`; new clients should gate on `session_resume`.
-`limits.sessionRestoreTimeoutMs`, when present, is the daemon's wall-clock budget for the underlying ACP `loadSession` / `unstable_resumeSession` request. It is an additive v1 field. The TypeScript SDK gives the daemon 10 seconds of client headroom, and the WebUI watchdog gives it 15 seconds; clients talking to an older daemon should use 70 seconds and 75 seconds respectively.
+`limits.sessionRestoreTimeoutMs`, when present, is the daemon's wall-clock budget for the underlying ACP `loadSession` / `unstable_resumeSession` request. It is an additive v1 field. The TypeScript SDK gives the daemon 10 seconds of client headroom, and the Web Shell watchdog gives it 15 seconds; clients talking to an older daemon should use 70 seconds and 75 seconds respectively.
`session_transcript` advertises `GET /session/:id/transcript`, a read-only paged replay view over the persisted active-session JSONL. It is separate from `/load`: it does not attach a client, seed the live EventBus, create a live session, or change the live replay window. Clients should use it when they need the complete on-disk transcript for a long session, and continue using `/load` only for bounded live replay during cold UI restore.
@@ -556,7 +556,7 @@ operator diagnostic snapshot documented below.
| `mcp_workspace_pool` | the shared MCP transport pool is active. Omitted when `QWEN_SERVE_NO_MCP_POOL=1` disables the pool. |
| `mcp_pool_restart` | the shared MCP transport pool is active; restart responses may include pool-aware multi-entry shapes. |
| `external_tool_guard` | `qwen serve` completed the startup handshake for `--external-tool-guard-mode=required`; every spawned ACP channel must acknowledge the installed callback before Session creation, and every supported top-level managed ACP tool invocation that reaches the final execution boundary must receive one external pre-execution allow. Earlier permission/hook denials make no provider request. Nested AgentCore execution is outside v1 and is rejected while this external provider mode is active. The tag reflects only the external provider: independently of it, every daemon applies the built-in Git relocation guard to the managed tools that carry a shell command line (`run_shell_command` and `monitor`), so the absence of this tag does not mean no pre-execution denials. |
-| `allow_origin` | T2.4 ([#4514](https://github.com/QwenLM/qwen-code/issues/4514)). The daemon was started with at least one `--allow-origin ` (or `allowOrigins: [...]` via the embedded API). Cross-origin requests from matched origins receive proper CORS response headers; unmatched origins still get the default 403. The configured pattern list is intentionally NOT echoed in `/capabilities` to avoid leaking the trusted-origin set to unauthenticated readers — browser webui already knows its own origin. |
+| `allow_origin` | T2.4 ([#4514](https://github.com/QwenLM/qwen-code/issues/4514)). The daemon was started with at least one `--allow-origin ` (or `allowOrigins: [...]` via the embedded API). Cross-origin requests from matched origins receive proper CORS response headers; unmatched origins still get the default 403. The configured pattern list is intentionally NOT echoed in `/capabilities` to avoid leaking the trusted-origin set to unauthenticated readers — a browser client already knows its own origin. |
| `prompt_absolute_deadline` | `--prompt-deadline-ms` / `QWEN_SERVE_PROMPT_DEADLINE_MS` / `ServeOptions.promptDeadlineMs` is set to a positive integer. |
| `writer_idle_timeout` | `--writer-idle-timeout-ms` / `QWEN_SERVE_WRITER_IDLE_TIMEOUT_MS` / `ServeOptions.writerIdleTimeoutMs` is set to a positive integer. |
| `workspace_settings` | the daemon was created with settings persistence available. |
diff --git a/docs/developers/roadmap.md b/docs/developers/roadmap.md
index 86fceed853d..56f57908003 100644
--- a/docs/developers/roadmap.md
+++ b/docs/developers/roadmap.md
@@ -2,13 +2,13 @@
> **Objective**: Catch up with Claude Code's product functionality, continuously refine details, and enhance user experience.
-| Category | Phase 1 | Phase 2 |
-| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| User Experience | ✅ Terminal UI ✅ Support OpenAI Protocol ✅ Settings ✅ OAuth ✅ Cache Control ✅ Memory ✅ Compress ✅ Theme | Better UI OnBoarding LogView ✅ Session Permission 🔄 Cross-platform Compatibility ✅ Coding Plan ✅ Anthropic Provider ✅ Multimodal Input ✅ Unified WebUI |
-| Coding Workflow | ✅ Slash Commands ✅ MCP ✅ PlanMode ✅ TodoWrite ✅ SubAgent ✅ Multi Model ✅ Chat Management ✅ Tools (WebFetch, Bash, TextSearch, FileReadFile, EditFile) | 🔄 Hooks ✅ Skill ✅ Headless Mode ✅ Tools (WebSearch) ✅ LSP Support ✅ Concurrent Runner |
-| Building Open Capabilities | ✅ Custom Commands | ✅ QwenCode SDK ✅ Extension System |
-| Integrating Community Ecosystem | | ✅ VSCode Plugin ✅ ACP/Zed ✅ GHA |
-| Administrative Capabilities | ✅ Stats ✅ Feedback | Costs Dashboard ✅ User Feedback Dialog |
+| Category | Phase 1 | Phase 2 |
+| ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| User Experience | ✅ Terminal UI ✅ Support OpenAI Protocol ✅ Settings ✅ OAuth ✅ Cache Control ✅ Memory ✅ Compress ✅ Theme | Better UI OnBoarding LogView ✅ Session Permission 🔄 Cross-platform Compatibility ✅ Coding Plan ✅ Anthropic Provider ✅ Multimodal Input ✅ Unified Web Shell |
+| Coding Workflow | ✅ Slash Commands ✅ MCP ✅ PlanMode ✅ TodoWrite ✅ SubAgent ✅ Multi Model ✅ Chat Management ✅ Tools (WebFetch, Bash, TextSearch, FileReadFile, EditFile) | 🔄 Hooks ✅ Skill ✅ Headless Mode ✅ Tools (WebSearch) ✅ LSP Support ✅ Concurrent Runner |
+| Building Open Capabilities | ✅ Custom Commands | ✅ QwenCode SDK ✅ Extension System |
+| Integrating Community Ecosystem | | ✅ VSCode Plugin ✅ ACP/Zed ✅ GHA |
+| Administrative Capabilities | ✅ Stats ✅ Feedback | Costs Dashboard ✅ User Feedback Dialog |
> For more details, please see the list below.
@@ -19,7 +19,7 @@
| Feature | Version | Description | Category | Phase |
| ----------------------- | --------- | ------------------------------------------------------- | ------------------------------- | ----- |
| **Coding Plan** | `V0.10.0` | Alibaba Cloud Coding Plan authentication & models | User Experience | 2 |
-| Unified WebUI | `V0.9.0` | Shared WebUI component library for VSCode/CLI | User Experience | 2 |
+| Unified Web Shell | `V0.9.0` | Shared browser UI for VS Code and `qwen serve` | User Experience | 2 |
| Export Chat | `V0.8.0` | Export sessions to Markdown/HTML/JSON/JSONL | User Experience | 2 |
| Extension System | `V0.8.0` | Full extension management with slash commands | Building Open Capabilities | 2 |
| LSP Support | `V0.7.0` | Experimental LSP service (`--experimental-lsp`) | Coding Workflow | 2 |
diff --git a/docs/users/features/commands.md b/docs/users/features/commands.md
index 7b17318ef8c..d2d4063e36d 100644
--- a/docs/users/features/commands.md
+++ b/docs/users/features/commands.md
@@ -34,6 +34,10 @@ These commands help you save, restore, and summarize work progress.
| `/export` | Export session history to file | `/export html`, `/export md`, `/export json`, `/export jsonl` |
| `/rename` | Rename or tag the current session | `/rename My Feature` or `/tag` |
+> [!note]
+>
+> Opening an HTML export loads the renderer for that exact Qwen Code version from `unpkg.com`. If the version has not been published or the renderer cannot be reached, the file shows a load error. Markdown, JSON, and JSONL exports remain self-contained.
+
> [!note]
>
> `/summarize` is an alias for `/compress` (it compresses chat history — a destructive operation). To generate a non-destructive project summary instead, use `/summary`.
diff --git a/docs/users/features/followup-suggestions.md b/docs/users/features/followup-suggestions.md
index 6393ee84ea7..740c43f0d51 100644
--- a/docs/users/features/followup-suggestions.md
+++ b/docs/users/features/followup-suggestions.md
@@ -2,7 +2,7 @@
Qwen Code can predict what you want to type next and show it as placeholder text in the input area. This feature uses an LLM call to analyze the conversation context and generate a natural next step suggestion.
-This feature works end-to-end in the CLI. In the WebUI, the hook and UI plumbing are available, but host applications must trigger suggestion generation and wire the followup state for suggestions to appear.
+This feature works end-to-end in both the CLI and Web Shell. Generation is automatic and server-side: after each completed turn the daemon emits the suggestion on the session stream (on by default; set `ui.enableFollowupSuggestions` to `false` to opt out), and Web Shell's composer already wires the `useDaemonFollowupSuggestion` hook, so suggestions render and accept with no additional host wiring.
## How It Works
diff --git a/docs/users/qwen-serve.md b/docs/users/qwen-serve.md
index 853ab5324f7..809b53acfa7 100644
--- a/docs/users/qwen-serve.md
+++ b/docs/users/qwen-serve.md
@@ -418,43 +418,43 @@ Notes:
## CLI flags
-| Flag | Default | Purpose |
-| --------------------------------------- | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `--port ` | `4170` | TCP port. `0` = OS-assigned ephemeral port. |
-| `--hostname ` | `127.0.0.1` | Bind interface. Anything beyond loopback requires a token. |
-| `--local-control` | `false` | Share the Web Shell on one selected private IPv4 interface with a daemon-owned revocable pairing token, terminal QR code, exact browser origin, and best-effort sleep inhibition. Composes with `--token`, `--allow-origin`, and `--port 0`; conflicts with `--no-web` and non-default `--hostname`. Use `--local-control-address` when multiple LAN candidates are available, and add `--tls-cert` + `--tls-key` for secure-context browser APIs such as voice input. |
-| `--local-control-address ` | — | Which LAN IPv4 address to share when the host has more than one candidate. Only needed if `--local-control` reports an ambiguous choice. |
-| `--token ` | — | Bearer token. Falls back to `QWEN_SERVER_TOKEN` env var (with leading/trailing whitespace stripped — handy for `$(cat token.txt)`). |
-| `--require-auth` | `false` | Refuse to start without a bearer token, even on loopback. Hardens the `127.0.0.1` developer default for shared dev hosts / CI runners / multi-tenant workstations where any local user can hit the listener. Boots only with `--token` or `QWEN_SERVER_TOKEN` set; gates `/health` behind the bearer too. |
-| `--enable-session-shell` | `false` | Enable direct session shell execution. Effective with either a configured bearer token or trusted-loopback mode; calls still require a valid session-bound `X-Qwen-Client-Id`. |
-| `--tls-cert ` | — | Path to a PEM certificate file. Serve over **HTTPS** instead of HTTP. Must be paired with `--tls-key` (boot fails if only one is given). Unlocks secure-context browser APIs — voice input (`getUserMedia`), WebRTC — over a LAN IP, which browsers otherwise block on plain `http://`. TLS termination only; no auto-generation / ACME. See [HTTPS / TLS](#https--tls-for-mobile--cross-device-access) below. |
-| `--tls-key ` | — | Path to a PEM private key file. Must be paired with `--tls-cert`. |
-| `--max-sessions ` | `32` | Cap on concurrent live sessions. New `POST /session` requests that would spawn a fresh child return `503` (with `Retry-After: 5`) when the cap is hit; attaches to existing sessions are NOT counted. Set to `0` to disable. Sized for single-user / small-team usage; raise it if your deployment has the RAM/FD headroom (~30–50 MB per session). |
-| `--max-total-sessions ` | derived | Optional non-negative integer daemon-wide cap on fresh session creation across all registered workspace runtimes. It applies to new child sessions, session restore, and branch/fork-created sessions; attaching to an existing live session does not consume a slot. Set to `0` for unlimited. When omitted with several startup/restored workspaces, the daemon derives a fixed cap from the per-workspace limit and the startup workspace count; later dynamic registration does not recompute it. |
-| `--max-pending-prompts-per-session ` | `5` | Per-session cap on prompts accepted by `POST /session/:id/prompt` but not yet settled, including queued prompts and the active prompt. The bridge rejects overflow synchronously with `503`, `Retry-After: 5`, and `code: "prompt_queue_full"` before returning a `promptId`. Set to `0` to disable. `branchSession` serializes on the same FIFO but does not count against this prompt cap. |
-| `--workspace ` | `process.cwd()` | Absolute workspace directory registered by this daemon. Repeat the flag to host multiple workspaces in one process; the first is primary and remains the default when a request omits `cwd`. Relative values are rejected. Session requests whose canonical `cwd` is not registered return `400 workspace_mismatch`. |
-| `--memory-project-scope ` | `workspace` | Project-memory partitioning mode. `workspace` (default) keys memory by the exact registered workspace directory so each daemon workspace gets its own isolated memory; `git-root` is the legacy compatibility mode shared by workspaces resolved to the same Git root. Overrides `QWEN_CODE_MEMORY_PROJECT_SCOPE` when provided; a blank env value is treated as unset, while an unrecognized non-empty value is ignored with a one-time warning and retains the legacy `git-root` behavior. The new default does not migrate existing git-root project memory — use an explicit `git-root` scope to read those entries during migration. |
-| `--channel ` | — | Experimental daemon-managed channel worker. Repeat the flag to select multiple configured channels, or pass `all` to start every configured channel. `all` cannot be combined with named channels. Selected channel `cwd` values must resolve to a registered workspace; a multi-workspace daemon runs one worker per owning workspace. The worker is owned by `qwen serve`; stop the daemon to stop serve-managed channels. |
-| `--max-connections ` | `256` | Listener-level TCP connection cap (`server.maxConnections`). Bounds raw socket count irrespective of session count — slow / phantom SSE clients get rejected at accept time once full. Raise alongside `--max-sessions` if your deployment expects many SSE subscribers per session. |
-| `--memory-budget-mb ` | 50% of cgroup/host | Total memory budget in MB for the whole daemon process tree. When unset, derived as 50% of the cgroup limit or host memory; either way the effective value is capped at resolved available memory, and both the configured and effective figures are reported. It does not change how any `qwen --acp` child is sized; the one consumer today is adaptive live-journal growth: one daemon-wide growth pool derived as 5% of the effective budget (capped at `1024` MB; on hosts reporting `insufficientMemory` the pool is 0 and adaptive growth is disabled) is shared by every workspace bridge — see `--max-journal-bytes`. Resolved figures appear under `limits.memory` in `GET /daemon/status`, alongside registered and live child counts and advisory per-child shares under `runtime.memory`. A host too small for the minimum reports `insufficientMemory` rather than being clamped upward; because the derived fraction is 50%, any host under ~2 GB trips this. Pass an explicit `--memory-budget-mb 1024` on such a host to override the derived figure (the flag still requires at least 1024 MB of available memory to clear the warning). Must be an integer in `[1024, 1048576]`. |
-| `--memory-pressure-mode ` | `observe` | Whether the daemon turns its own memory reading into a verdict. `observe` (default) reports the pressure level under `runtime.memory.pressure` in `GET /daemon/status` and raises a `daemon_memory_pressure` issue — a `warning`, so the overall `status` leaves `ok` — whenever the level leaves `normal`. `off` still reports every figure, including the level, but raises no issue, so the overall `status` is unchanged; use it while calibrating, or if you alert on the top-level status. The level is the worse of two ratios: RSS against available memory (what the cgroup OOM killer watches) and V8 heap used against this process's heap ceiling. It covers the daemon root process only; compare it against `runtime.memory.children.rssBytes` for the children. Nothing remediates in either mode. One of `off`, `observe`. |
-| `--child-heap-mode ` | `observe` | Whether the daemon models a per-child heap partition of `--memory-budget-mb`. `observe` (default) reports what it would apply — `limits.memory.childHeap.perChildCeilingMb` and `maxConcurrentChildren` — and counts spawns that would have exceeded the limit. **Nothing is applied**: no child is sized from the budget and no spawn is refused. `off` models nothing, and says so on the wire: `maxConcurrentChildren` and `perChildCeilingMb` are both `null` rather than carrying a partition you switched off. A refusal count of 0 does **not** mean the partition would be safe to apply: children still run on the much larger host-derived ceiling, so a workload needing more old space than the modeled ceiling looks perfectly healthy here. Applying the partition ships with the measurement that can answer that. |
-| `--event-ring-size ` | `8000` | Per-session SSE replay ring depth (#3803 §02 target). Sets the backlog available to `GET /session/:id/events` with `Last-Event-ID: N`. Larger = more reconnect headroom at the cost of a few hundred KB extra RAM per session. SDK clients can additionally request a larger per-subscriber backlog cap on a specific subscription via `?maxQueued=N` (range `[16, 2048]`, default 256). Daemons also emit a non-terminal `slow_client_warning` SSE frame at 75% queue fill so clients can drain / reconnect before getting evicted. Pre-flight `caps.features.slow_client_warning`. |
-| `--compacted-replay-max-bytes ` | `4194304` | Per-live-session byte cap for the retained replay events in the bounded snapshot returned by `POST /session/:id/load`. The cap applies to `compactedReplay`; the current in-flight `liveJournal` is separately capped by `--max-journal-events` and `--max-journal-bytes` (baseline caps that adaptive growth can raise — see `--max-journal-bytes`). Values must be positive safe integers; invalid values fail at boot, and the hard ceiling is 256 MiB. When older retained replay is dropped, the snapshot begins with `history_truncated`. This does not limit the on-disk transcript. |
-| `--max-journal-events ` | `10000` | Per-session baseline cap on replay entries retained in the in-flight `liveJournal` for the current unfinished turn. Consecutive compatible text or thought chunks share an entry, with at most 256 source events per entry; other event boundaries are preserved. When exceeded, the daemon first tries adaptive growth (see `--max-journal-bytes`); if no headroom is granted or the grant does not cover the overshoot, the oldest entries are dropped and a `history_truncated` marker is prepended. The marker's `truncatedEvents` and `retainedEvents` counts describe source events. Must be a positive safe integer. Pinning this flag (or `--max-journal-bytes`) disables adaptive growth. |
-| `--max-journal-bytes ` | `8388608` | Per-session baseline byte cap on the in-flight `liveJournal`, accounted from the serialized source events even when compatible chunks share a replay entry. When a turn outgrows the cap, adaptive growth raises the session's caps toward double (up to a per-session hard cap of 256 MiB, limited by the remaining pool headroom) while the growth granted across all of the daemon's live sessions fits in one shared growth pool sized at 5% of the daemon's effective memory budget — the `--memory-budget-mb` value when passed, capped at resolved available memory, otherwise 50% of auto-detected memory (see `--memory-budget-mb`) — capped at `1024` MB; on hosts reporting `insufficientMemory` the pool is 0 and adaptive growth is disabled. Growth happens on demand, and only as far as the pool allows; when it is refused, the pool is exhausted, or a grant does not cover the overshoot, the oldest entries are dropped whole (at least one entry is always kept), so the retained tail can be much smaller than the cap. Pinning this flag (or `--max-journal-events`) disables adaptive growth. Must be a positive safe integer. Defaults to 8 MiB. |
-| `--mcp-client-budget ` | — | Positive integer cap on live MCP clients. When `mcp_workspace_pool` is advertised, the cap and transports are shared per workspace runtime; when the tag is absent, the legacy per-session manager enforces it. Combine with `--mcp-budget-mode`. When unset, no accounting-driven enforcement (but `GET /workspace/mcp` still reports `clientCount`). Distinct from claude-code's `MCP_SERVER_CONNECTION_BATCH_SIZE`, which gates startup concurrency rather than total live clients. Pre-flight `caps.features.mcp_guardrails` and `caps.features.mcp_workspace_pool`. |
-| `--mcp-budget-mode ` | `warn` / `off` | How `--mcp-client-budget` is enforced. `warn` (default when budget set): no refusal, snapshot's `budgets[0].status` flips to `warning` at ≥75% of budget. `enforce`: connects past the cap are refused, per-server cell shows `disabledReason: 'budget'`, deterministic by `mcpServers` declaration order. `off` (default when budget unset): pure observability. Boot rejects `enforce` without a budget. |
-| `--external-tool-guard-mode ` | `off` | Managed ACP external pre-execution policy. `off` makes no provider calls and advertises no capability. `required` fails startup unless a compatible provider completes the v1 handshake, then fails every supported top-level tool invocation closed unless its single prepare request is allowed. |
-| `--external-tool-guard-endpoint ` | — | Origin-only loopback HTTP(S) provider URL used in `required` mode, for example `http://127.0.0.1:8787`. Paths, URL credentials, redirects, non-loopback hosts, and proxy routing are not accepted. |
-| `--external-tool-guard-timeout-ms ` | `3000` | Integer `100..30000`; applies independently to the startup handshake and each prepare request. |
-| `--http-bridge` | `true` | Stage 1 mode: production attempts to preheat one primary `qwen --acp` child for compatibility and retries on first use after failure, while each trusted secondary can start one child on demand. Sessions targeting a runtime multiplex onto its child via ACP `newSession()`; untrusted secondaries cannot start ACP. Stage 2 native in-process becomes available later. |
-| `--initialize-timeout-ms ` | `10000` | ACP child request timeout, including the `initialize` handshake (ms). Must be a positive integer up to `2147483647`. Values above the JS timer ceiling (`2^31-1`) are rejected at boot because Node silently compresses them to 1 ms. Cold-container deployments that need extra headroom for child startup can raise this; the same value governs `newSession`, workspace-status polls, and other ACP ext-method deadlines. |
-| `--session-restore-timeout-ms ` | `60000` | ACP session load/resume deadline in milliseconds. Must be a positive integer up to `2147483647`; `0` is invalid. If omitted, the default is 60 seconds, raised to an explicitly supplied `--initialize-timeout-ms` when that value is larger; a shorter initialize timeout never lowers the restore budget. The SDK and WebUI add 10 and 15 seconds of client headroom. A timeout returns retryable `504 session_restore_timeout`; it does not imply that the daemon itself exited. |
-| `--allow-origin ` | — | T2.4 ([#4514](https://github.com/QwenLM/qwen-code/issues/4514)). Cross-origin allowlist for browser webui clients. Repeatable. Each value is `*` (any origin — boot refuses if no bearer token is configured; `--require-auth` on loopback is recommended so `/health` is also bearer-gated, since it is pre-auth on loopback by default; the Web Shell static assets stay pre-auth in every mode, so pass `--no-web` to remove them) or a canonical URL origin (`://[:]`, no trailing slash / path / userinfo / query). Without a token, HTTP(S) origins must be loopback; remote HTTP(S) origins require a token. Browser extensions retain tokenless access locally. Subdomain wildcards (`https://*.example.com`) are intentionally unsupported — list each subdomain explicitly, or use `*` with a configured token (and `--require-auth` for full hardening). Matched origins receive CORS response headers (`Access-Control-Allow-Origin`, `Vary: Origin`, methods, headers, max-age, and exposed `Retry-After`); unmatched origins still get a 403 with the same envelope as today's wall. `Origin: null` (sandboxed iframes, file:// docs) is always rejected, even under `*`. Pre-flight via `caps.features.allow_origin`. Loopback self-origin hits are unaffected. |
-| `--web` / `--no-web` | `true` | Serve the built Web Shell SPA at the daemon root (`GET /`, `/assets/*`, and `GET /session/` document navigations). These entry points are registered **before** the bearer-auth gate — a browser can't attach a token to a `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-