qwen-code/packages/web-shell
易良 9dde2f657d
fix(web-shell): surface Managed session list failures (#16082)
* fix(web-shell): surface Managed session list failures

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* test(web-shell): match the list-failure sentences, not the whole alert

The list-failure alert wraps its message and its Retry button in the same
role=alert div, so textContent reads 'Managed sessions could not be loaded.Retry
loading sessions'. Two assertions compared that concatenation against the
sentence alone, which made four cases fail with a message that looked identical
on both sides.

Assert the sentence is present, the way the sibling HTTP-500 case already does,
and keep the alert count so the 'once' property is still pinned.

Not verified locally: this checkout's core dist predates
managed-runtime/managed-permission-rules.js, so the suite cannot load here. CI
is the first real run.

---------

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Co-authored-by: yiliang114 <yiliang114@users.noreply.github.com>
2026-11-02 10:41:58 +08:00
..
client fix(web-shell): surface Managed session list failures (#16082) 2026-11-02 10:41:58 +08:00
scripts refactor(managed-agent): Split server into Maven modules (#15871) 2026-10-30 14:56:35 +00:00
components.json feat(web-shell): add shadcn UI foundation (#6760) 2026-07-12 13:51:52 +00:00
NOTICES.txt feat(web-shell): preview DOCX files across file entry points (#14868) 2026-10-20 22:40:46 +08:00
package.json chore(release): v0.26.5 (#16035) 2026-11-01 12:40:25 +08:00
pierre-build.ts fix(web-shell): polish files and trim preview resources (#14143) 2026-10-16 16:45:56 +08:00
playwright.config.ts feat(web-shell): add session Workflow mode controls (DW-04B) (#15779) 2026-10-31 18:24:29 +08:00
playwright.visuals.config.ts fix(web-shell): make the cockpit visual capture deterministic (#11786) 2026-09-14 01:55:50 +00:00
README.md feat(web-shell): add Russian locale for the Goal card and Goals dialog (#13685) 2026-10-29 12:49:34 +00:00
tsconfig.build.json feat(daemon): merge daemon-mode feature batch into main (#4490) 2026-06-12 00:34:49 +08:00
tsconfig.json fix(export): shrink the exported transcript renderer to what a read-only transcript needs (#11038) 2026-09-06 06:15:42 +00:00
tsconfig.lib.json fix(web-shell): polish files and trim preview resources (#14143) 2026-10-16 16:45:56 +08:00
vite.config.ts fix(web-shell): proxy the PR watches route in dev (#16004) 2026-10-31 11:15:30 +00:00
vite.lib.config.ts feat(web-shell): add Markdown copy and refine reply actions (#15345) 2026-10-26 14:18:51 +08:00
vitest.config.ts fix(web-shell): polish files and trim preview resources (#14143) 2026-10-16 16:45:56 +08:00

@qwen-code/web-shell

Qwen Code Web Shell 是面向浏览器的 daemon 会话终端 UI,可以作为 React 组件嵌入到其他项目中。

Managed 工作区与模型

完整 Web Shell 在配置 managedAgentProvider 后,在同一个工作区列表中同时显示本地和托管工作区,不再使用本地/托管 tab。名称旁的标志显示所属服务,工作区的新建会话使用统一的书写图标;顶部“新建会话”继承当前会话或草稿的服务。新会话的工作区菜单可双向选择两类服务已有的工作区,保留原身份和独立草稿,提交前不创建工作区或会话。添加工作区时可明确选择本地(daemon)或托管(Managed Agent),切换表单或取消不会改变当前会话。托管模式复用标准欢迎界面、输入框、对话和工具卡片、审批、停止按钮及侧栏,后台任务继续运行。未配置 provider 时保持原首页。Java Vite 演示入口 ?managedProvider=java 默认托管,managed=0 可明确选择本地,managed=1&managedSession=... 深链继续有效;直接打开本地会话链接时保持本地。完整行为与边界见统一工作区导航设计。

已有 Managed 会话的工作区、相对目录、Agent 预设和环境状态在会话上下文中查看。模型和工作区仍由 Java 授权,界面复用不改变后端工具范围或审批策略。导出的 ManagedAgentWebShell 独立入口保持兼容。

ManagedAgentWebShell 可通过 enableWorkspaceBinding 启用 Java 授权工作区列表,并通过 modelPresets={[{ agentId: 'agent-model-a', label: 'Model A' }]} 提供新建会话的模型预设。每个 agentId 必须由宿主预先发布为模型专属 AgentDefinition;服务端模型允许列表和定义中的工具、审批设置决定实际准入。浏览器不发布定义或传入模型凭证。模型预设名称是宿主展示数据;已有会话的初始 AgentDefinition 及工具、审批配置保留,模型选择由独立的 Java 会话接口管理。

Java 新建页直接复用标准 Web Shell 的欢迎标题、输入框、原生模型菜单和工作区底栏。选择工作区、相对目录和模型后输入任务并发送,一次请求创建会话与首个 Turn;响应丢失保留冻结请求,已创建后的读回重试不会再次发送输入。旧 provider 的空创建能力保持兼容。更换工作区需新建会话;支持模型切换的会话可在空闲时选择服务端允许的模型,绑定工作区内切换目录仍使用已有修订校验流程。对话复用标准消息列表及输入框布局,保留 Managed 审批、取消、结果与未确认请求重试;不提供 daemon 附件、文件引用或斜杠命令。

工作区查询返回 capabilities.approvalModeSelection: true 时,新会话按所选 Agent 查询审批策略,并复用输入框的策略菜单。Shell profile 支持 Ask Approval 与 Auto Edit,纯文件 profile 还可提供 Full Access;托管模式不提供 Classifier Approval 或 Plan。已保存的 AgentDefinition 只提供自身固定策略。选项加载失败或无效时暂停创建;更换模型会重新读取选项并保留草稿和工作区。明确选择的策略与会话创建请求一起固定保存,刷新后的重试沿用原策略;旧待确认请求继续省略策略。已有会话显示服务端保存的策略并禁用修改,需要新建会话才能更换。未宣告能力的旧服务端保留原行为。

已有工作区托管会话的输入框优先显示服务端已确认的模型选择,其次显示最近成功主模型请求的实际模型;没有这些数据时只显示 Agent 身份。会话摘要返回 capabilities.modelChange: true 时,模型菜单从 /session-models/query 读取服务端允许列表,空闲时通过 /session-models/change 在同一会话内切换,后续消息使用新模型。活跃任务、审批、目录切换或未确认请求期间暂停切换。请求发送前保存会话、目标模型、修订和幂等键;响应不确定或刷新后继续确认原请求,读回后才更新选择,对话与草稿保留。旧服务端和不支持的会话保持只读模型控件。上下文浮层复用原生环形控件,显示最近成功请求的 prompt token 数、同一模型配置的窗口及剩余容量;切换后等待新模型观测,缺少有效计数时明确显示未知,不能用累计计费量推测。该浮层仅提供当前占用,不显示尚无托管接口支持的手动压缩或分类明细操作。本地模式保持原有模型切换与上下文功能。

Java 工作区查询返回 capabilities.workspaceCreation: true 时,工作区菜单提供“新建工作区 → 从空目录开始”。输入名称后,服务端创建并登记独立的空目录、授权给当前用户,页面自动选中它并保留任务草稿与模型。创建请求在发送前保存在当前标签页;响应不确定时,关闭弹窗或刷新页面后仍可重试同一请求。该能力默认关闭,需要服务端配置 qwen.managed-agent.runtime-broker.workspace-creation-root 及相应的已验证 Workspace 执行依赖;浏览器不选择服务器路径。

本地守护进程通告 git_workspace_clone 时,“添加工作区”对话框额外提供“Git 仓库”来源:输入 clone 地址和必须尚不存在的绝对目标路径后,POST /workspaces { kind: "git", url, target } 返回 202 与任务 id,对话框通过 /workspace-git-clone-jobs/:id 轮询进度并可取消。clone 成功后对话框复用现有 POST /workspaces { cwd } 注册该目录,因此容量、嵌套、持久化和信任策略与手动添加已有目录一致;clone 本身不授予信任。该来源只对当前连接的守护进程开放;私有仓库可用 SSH agent 认证,或把 token 放进粘贴的 URL(由守护进程转交 Git,不写入克隆仓库的配置)。clone 子进程不读取宿主上已配置的 Git 凭据(credential.helper、url.*.insteadOf、http.* 均不生效)。失败、取消或超时会删除守护进程创建的目标目录。

本地 Vite Java 演示可添加 managedWorkspace=1,或设置 VITE_MANAGED_AGENT_WORKSPACE_BINDING=true。VITE_MANAGED_AGENT_MODEL_PRESETS 为上述预设数组的 JSON;VITE_MANAGED_AGENT_ACTOR_ID 可通过 X-Qwen-Actor-Id 提供本地用户身份,同时进入存储隔离 scope。Java 列出当前用户已有授权的工作区,创建能力按上述服务端配置开启;本地演示需显式配置 QWEN_MANAGED_AGENT_TRUSTED_ACTOR_HEADER=X-Qwen-Actor-Id,该配置仅用于可信 loopback 环境。文件工具执行还需要 Java 文档中的 Workspace files、Session Store、Broker mounts 和 Harness Broker 连接配置。

Managed 工具结果

ManagedAgentWebShell 与 createJavaManagedAgentProvider 支持 Java O3 工具结果。 只有 Session 返回 capabilities.artifacts: true 时才提供输出入口;它不启用 Shell 执行。卡片区分执行、捕获与交付状态,面板按固定版本分页读取原始 bytes, 当前账号的内容读取权由 metadata 响应独立决定。此路径不使用 daemon 文件 API。

宿主应提供包含租户及账号身份的 productScope,身份切换时同步更新它;刷新同一 账号的短期 token 则继续使用动态 getHeaders。所有 metadata、range 和下载请求 都经过注入的 fetch、getHeaders 与 credentials。跨源 gateway 需要允许带 Range/If-Match 的鉴权 GET,并向浏览器暴露 ETag、Content-Range 和 Content-Length;不要对 bytes 响应重新压缩或变换。

支持 showSaveFilePicker 的浏览器默认把下载流直接写入用户选择的文件。其他宿主 可提供 saveArtifact(artifact, { signal, openStream }) 回调,在用户操作中取得 可写目标后调用 openStream(),再用带 signal 的 pipeTo 保存。导出的 ManagedArtifactSave 定义这一接口。回调必须传播取消和失败,并保持背压;不要用 blob()/arrayBuffer() 聚合完整下载,也不要把 token 放进 URL。没有流式保存能力 时只提供有界读取,并明确显示下载不可用。

更新

支持更新的服务会在后台检查并下载新版本,准备好后才在侧栏显示(双栏在「更多」中,单栏在版本号旁) 「更新」按钮。点击后应用更新并重启服务,连接恢复后自动刷新当前任务页面。 重启会结束正在运行的工作。更新目标是连接的服务,远程连接时也是远程机器上的安装。

支持具备进程替换能力的 macOS/Linux CLI 服务,以及独立安装或受管理的全局 npm 安装。关闭 general.enableAutoUpdate、旧服务、桌面 shell、不支持的安装及嵌入式 服务不显示按钮。临时配对连接也不提供更新入口。使用自定义 sidebar.footer.items 时,通过 update 项控制此功能。

工作区文件浏览

独立页面右侧拓展区的「文件」入口打开只读浏览器:未选择文件时仅展示 Pierre 目录树,点击文件后左侧显示内容、右侧保留目录树。Markdown、HTML 和图片沿用现有预览,文本源码由 Pierre File 高亮展示;Markdown 和 HTML 可以切换预览与源码。目录按需加载,筛选仅覆盖 已加载路径,不因筛选而读取目录;输入保留原文,零匹配时显示明确提示。刷新重新加载根目录和当前文件,并收起目录。窄面板可隐藏文件树。 中间分隔线可拖动调整两列宽度,也支持键盘方向键;文件树底部保留留白。 深层目录和长文件名可通过整棵树的横向滚动查看,不压缩名称。 目录列表最多显示前 2,000 个条目,截断时有提示。符号链接不作为目录递归展开。 切换页签保留目录展开、选中文件、源码模式、筛选及滚动位置。文件页签保存 工作区归属,展开与选中状态不会跨页面刷新保存。不接入 Git, 不编辑文件,也不自动把打开的文件加入对话上下文。文件右键菜单支持复制相对 或绝对路径、下载、在独立页签预览 Markdown/HTML/图片,以及通过「引用」插入 与输入框 @ 选择文件相同的标签。仅当前工作区文件可引用,不自动发送消息。

嵌入式宿主通过 rightPanel={{ items: ['files', 'review', 'sideTask'] }} 启用。 此入口需要有效、可信且无歧义的 daemon 工作区归属。

新建会话页的内置 header 只提供工作区级入口:右侧面板开关、启用时的 Local Control,以及多主机位置。面板开关遵循 header.items,并在工作区确定后 才显示;rightPanel.items 此时仅保留 files、terminal 和 webPreview, review、sideTask、trajectory 要等首条消息创建会话后才出现。 自定义 renderChatHeader 的宿主在新建会话页仍不调用该渲染器。 输入框没有显示工作区选择器时,精简 header 保留只读工作区图标。 面板跟随输入框选择的工作区;各工作区草稿独立保存,刷新和会话切换后可恢复, 首条消息创建会话时迁移面板并删除草稿。草稿与会话各保留最近 20 份面板存档, 草稿不会挤占会话的额度。

源码高亮支持 33 个常用语言键及 GitHub 浅色/深色主题,其他语言显示原文。 语言清单维护于 pierre-resources.ts, 选择策略与补丁移除条件见后续设计。 文件树移除名称中的控制字符,保留普通空格;筛选同时支持显示名称和真实路径。 修改 Pierre 补丁并重新应用后,开发时执行 npm run dev -- --force 清除预构建缓存。

Excel(.xlsx)支持内联预览,也可通过右键「预览」在独立页签打开;重复打开同一文件复用页签,其他 Excel 格式仍仅支持下载。 文件树源码预览在读取前限制为 4 MiB,更大的文件请通过右键菜单下载。 导入失败仅在凭据可恢复的独立页面中提供重载;嵌入宿主或仅内存凭据时提供关闭文件,保留对话和宿主页面。

开发网页预览

独立 Web Shell 的右侧面板提供「网页预览」。先通过终端启动开发服务器, 再打开面板并输入当前浏览器可访问的 HTTP/HTTPS 地址。预览支持热更新、 刷新、390px 手机宽度与桌面宽度切换,以及外部打开。

面板内切换标签会保留页面运行状态。URL 和宽度模式按工作区与会话保存; 重新打开整个面板、切换会话或刷新 Web Shell 后会从保存的入口 URL 加载。 预览中的跨域页面导航不会同步到地址栏,刷新也会返回入口 URL。 关闭预览不会停止开发服务器。

Artifact 发布的网页链接会在对应轮次留下产物卡片;开启网页预览后, 点击卡片「打开」可在右栏查看。普通 record_artifact 链接显示详情, 可从详情外部打开。关闭页签不删除卡片,之后仍能从历史消息重新打开。手动输入的地址只保存为面板状态,不会新增对话记录。 实时链接始终显示当前内容。

需要保留当时的网页时,使用 Artifact 发布自包含 HTML:每次发布会额外 保存一份独立的历史版本,包含内联样式、脚本、数据和内嵌资源。原消息的 卡片打开当次保存的内容,关闭面板、刷新或重启 daemon 后仍可回看和交互, 不依赖源文件、最新发布地址或开发服务器。重新打开会从交付时的初始状态 开始,不保存用户在预览中的临时操作状态。

历史版本文件保存在所属运行目录的 artifacts/snapshots/ 中;原发布 URL 仍更新为最新版。记录沿用会话产物的保留策略(默认最多 200 条,超过后 可能淘汰旧记录),并非无限存档。淘汰、删除记录、回退历史或删除会话时, 会释放该会话的快照引用;分支会话仍在使用的内容会保留,最后一个引用 释放后才回收文件。持久化失败时不会提前删除历史文件。旧版没有引用信息 的快照,以及落盘前崩溃或清理失败留下的文件会保守保留。仅开启会话记录的受管 ACP 会话保存快照,普通 CLI 发布 不额外写入历史文件。删除运行目录、移动会话而未复制快照, 或修改快照文件后会提示不可用,不会退回最新版。普通实时链接和过去未保存 内容的记录不能自动还原;任意开发网站也不会自动打包成离线网页。

嵌入式接入方通过 rightPanel={{ items: ['review', 'sideTask', 'webPreview'] }} 开启入口。新建会话页也可使用此入口;网页预览的草稿按输入框已选工作区保存, 首条消息创建会话后随面板迁移。宿主 CSP 需要允许预览来源的 frame-src,同时用 frame-ancestors 限制宿主页面只能被受信任的宿主嵌入,不能允许开发页面 反过来嵌入宿主。预览容器会限制直接子页面跳转,但开发应用自己的后代 iframe 仍需宿主的防嵌入策略保护。

地址必须使用主机名或 IPv4,不能包含登录凭据,也不能指向 Web Shell 或 daemon 自身。页面的防嵌入策略可能要求使用外部打开。远程开发目前需要 浏览器可访问的地址或已有端口转发;预览不会自动把浏览器的 localhost 转成远程 daemon 地址,也不会转发 daemon 凭据。

宿主接管产物与代码高亮

onRightPanelOpen 同步返回 false 时继续 Web Shell 原生打开逻辑;返回 true 或 undefined 时由宿主接管,保持旧版无返回值回调的行为。 未提供回调时仍使用原生行为,onFileReviewOpen 保持更高优先级。 该回调处理右侧面板请求;原生直接外部打开的记录链接仍走外部链接能力。

filterArtifact(artifact, { turnId, sourceSessionId }) 返回是否展示消息末尾的 产物卡片。过滤先于折叠数量计算,并应用于主会话、分屏和嵌套会话。 它不删除产物记录、不改变会话产物同步结果,也不隐藏文件变更卡片。

<WebShell
  {...connectionProps}
  onRightPanelOpen={(request) => {
    if (request.kind !== 'artifact') return false;
    openHostPreview(request);
    return true;
  }}
  filterArtifact={(artifact) => artifact.id !== hiddenArtifactId}
/>

预览组件可从独立入口复用高亮服务,无需导入聊天 UI 或样式:

import { highlightCode } from '@qwen-code/web-shell/code-highlighter';

const html = await highlightCode({
  code: 'SELECT id FROM orders',
  language: 'sql',
  theme: 'dark', // 或 'light'
});

返回高亮 HTML;未知语言、纯文本、超出已有大小限制或高亮失败返回 null, 宿主应回退为转义的纯文本。服务复用同一模块实例的 Shiki、语言加载和缓存, 不暴露可变的高亮器实例。独立 JavaScript realm 或重复打包的模块不共享实例。 样式和 HTML 的安全渲染由宿主负责。

实时语音中的屏幕共享

无需启动原生 Live Host。在 Web Shell 设置中启用 Live Voice 并配置支持图像输入的 实时模型,点击 Live Voice 后会直接接入新的语音会话,再点击「共享屏幕」。 浏览器需要支持屏幕共享, 并通过 HTTPS 或 localhost 等安全上下文访问;共享范围由浏览器选择器决定。

共享后画面自动作为当前语音对话的持续上下文,无需填写目标或额外开始观察。 可直接问「这个是什么意思」「下一步怎么做」。画面与麦克风进入同一条模型连接, 画面到达本身不会请求模型回复,也不会启动单独的目标监控模型。 画面中的文字仅作观察证据;用户要求执行操作时仍由执行 Agent 处理。

默认每秒采样一帧,包括内容未变的画面;单帧最多 190 KiB,网络拥塞时丢弃过期帧, 不会累积截图队列。每张图像紧随新的音频帧发送;麦克风静音或音频暂停期间仅保留最新画面,恢复音频后继续。 这是近实时画面上下文,不是逐帧视频分析。模型需要支持所选实时接口的图像输入, 图像输入会产生对应的模型用量。截图不会由 Live Feed 保存为图片文件。

停止共享会立即停止后续图像输入,语音可以继续。挂断会关闭面板并释放浏览器麦克风; 再次点击 Live Voice 会创建新会话。关闭页面或断开连接也会 停止共享。请保持页面和通话开启;浏览器后台节流或休眠可能中断采样,连续 15 秒 没有收到画面会停止 Live Feed 并提示重新共享。旧 daemon 继续支持按需截图, 界面会明确提示不支持实时画面。停止共享后的历史画面仍可能属于对话上下文, 不能当作当前屏幕。默认不会自动解说或主动提醒。

环境要求

  • React:^18.0.0 || ^19.0.0
  • React DOM:^18.0.0 || ^19.0.0
  • @qwen-code/sdk:>=0.1.8
  • 浏览器环境需要能访问 Qwen Code daemon serve 的 HTTP 接口。

组件包会自动注入自身的 CSS(包括 Tailwind 编译产物),接入方不需要配置 Tailwind 或额外引入全局 CSS。

Browser Support Matrix

  • Chrome / Edge 111+
  • Firefox 128+
  • Safari / iOS 16.4+
  • Android System WebView 111+

最低版本覆盖 Tailwind v4 的生成 CSS;JavaScript 构建目标单独设置为 ES2021。 独立页面在不支持的浏览器显示升级提示,嵌入式组件由宿主保证该支持约定。 此矩阵不是所有最低版本真机均已验证的声明。

独立生产页面在 HTTPS 或可信 loopback origin 下注册 service worker。 仅带内容哈希的构建资源使用 worker 缓存;manifest、公开图标、API、令牌和 事件流不缓存。HTML 连接失败时显示 503 重试页,不支持离线会话。 安装入口由浏览器决定,不保证自动弹出安装提示。

浏览器任务通知

通过 qwen serve 打开的独立 Web Shell 可在 Settings → UI → 任务通知 管理提醒。内置 main.tsx 显式设置默认开启;用户已保存的关闭选择优先。可在设置或审批/提问面板右上角的通知图标中授权;偏好保存在当前浏览器站点, 不写入 daemon 或 workspace 设置,同源标签页之间同步。

页面在后台或窗口失焦时,当前聊天及 Split View 中仍挂载的聊天在回合结束、失败、需要工具审批或等待 AskUserQuestion 回答时可以发送系统通知。取消回合、初次加载历史和前台已处理的回合或请求保持静默。工具审批和提问提醒只显示会话标题与待处理状态,不展示命令、路径、问题、选项或部分回复;同一请求只提醒一次,不影响后续回合结束提醒。快照重放不补发权限或提问提醒,包括断线期间错过的请求。回合结束通知显示会话标题(最多 60 个 Unicode 码点)、回合状态,以及带“提问 / 回复”标签的本轮提问(最多 80 个码点)和回复(最多 120 个码点)纯文本摘录。 标题显示 QwenCode · 会话名;回合结束通知在会话名尚未生成时使用本轮问题首行,两者均缺失时显示 QwenCode。审批或提问提醒缺少会话名时只显示 QwenCode。业务文案不额外添加地址。通知携带 Qwen Code 图标,实际显示位置由浏览器和操作系统决定,不能替换 macOS 上 Chrome 自身的标志。摘要直接截取文本,不额外调用模型;提问或回复缺失时省略对应行,两者均缺失时只显示状态。失败时可显示本轮提问,不展示错误详情或部分回复。系统可能进一步截短通知正文。 通知内容可能显示在系统通知中心或锁屏上,遵循系统的预览设置;点击通知尝试聚焦原窗口并在主聊天中打开对应会话,退出设置页或分屏;已在目标会话时不会重新加载。点击审批或提问提醒不会自动批准请求或提交答案。

需要支持 Notifications API 的桌面浏览器以及 HTTPS 或可信的 localhost 环境。 支持 Web Locks 且存储可用时,同源标签页协调去重;否则退化为页面内去重及相同 tag 的通知替换。关闭网页、页面冻结或离开未挂载的聊天后不保证提醒。嵌入式组件不自动 启用此能力;Channel 推送不在首版范围内。

WebShellWithProviders(及别名 StandaloneWebShell)支持通过 browserNotifications 接入通知并配置名称和图标:

<WebShellWithProviders
  baseUrl="https://daemon.example.com"
  sidebar
  browserNotifications={{
    defaultEnabled: true,
    appName: 'DataAgent',
    iconUrl: 'https://cdn.example.com/assets/dataagent.png',
  }}
/>

传入 {} 时使用默认 QwenCode 名称和随包图标;名称和图标均可单独省略,空白值也回退默认。标题显示“应用名称 · 会话标题”。图片 URL 由浏览器直接加载,可使用 HTTPS CDN 地址;加载失败不保证自动回退到默认图标。

defaultEnabled 默认 false;设为 true 时仅对没有保存通知偏好的浏览器站点默认开启。用户明确开启或关闭的选择优先,刷新后也保留;挂载后修改默认值不会覆盖当前选择。不传 browserNotifications 时保持嵌入入口原有行为,不接入通知。仅挂载不会申请权限;首次出现审批或提问面板时,若权限尚未决定且没有明确保存关闭选择,会为此浏览器站点自动尝试一次授权。浏览器可能拦截没有用户手势的申请;面板右上角的通知图标可悬停或点击查看状态、手动授权,以及 macOS/Windows 系统设置指引。不会重复自动申请,刷新后也保留尝试记录;存储不可用时只在当前挂载实例内去重。网页无法检测系统通知是否开启。修改品牌值不会重新加载当前会话。通知点击只导航所属实例,并遵守其当前锁定工作区。

低层 WebShell 的 daemon providers 由宿主管理,不支持这个配置属性;qwen serve 内置页面继续使用默认品牌。本配置仅影响通知,不改变侧边栏品牌、Chrome 来源地址或浏览器标志。

Tailwind 与 shadcn/ui

Web Shell 已配置 Tailwind CSS v4 和 shadcn/ui。shadcn 的 token 仅用于新增的 Tailwind/shadcn 组件;现有 CSS Modules 的主题色值保持不变。组件代码在仓库内, 可直接修改。

新增 UI 的约定

  • 新增通用 UI 或交互组件时,优先使用 shadcn/ui 已提供的组件,再根据 Web Shell 的需求修改生成到仓库中的源码。已有且稳定的 CSS Modules 组件不要求为了统一而 重写。
  • Tailwind class 使用标准的无前缀写法,例如 flex gap-2。发布构建会通过 PostCSS 将生成的选择器限制在 Web Shell root 和 portal root,并为全局动画、CSS property 注册增加 Web Shell 前缀,避免与接入方样式冲突。
  • shadcn 颜色必须使用 background、primary、muted 等语义 token,不要直接 引用 Web Shell 原有颜色变量。原有 CSS Modules 继续使用原来的 token,两套色值 各自维护。
  • Dialog、Popover、DropdownMenu、Tooltip 等包含 Portal 的组件,必须将内容挂载到 Web Shell 的 portal root。新增 shadcn 组件后,应参考现有 dialog.tsx,使用 useWebShellPortalRoot() 向 Radix Portal 传入 container。这样主题、旧 CSS 变量以及外部配置的 z-index 才能正确继承。
  • 保留组件上的 data-web-shell-* 属性和公开 CSS 变量。接入方可能通过这些属性或 --web-shell-dialog-backdrop-z-index、--web-shell-popover-z-index、 --web-shell-tooltip-z-index 等变量定制样式和层级。宿主自己的标题栏覆盖在 shell 之上但并不裁剪它时,必须通过 --web-shell-popover-safe-top 声明顶部 安全区,向上展开的浮层(输入历史、@ 引用)才能避开它;显式声明 0px 表示 没有顶部安全区,不会被默认值覆盖。

在 packages/web-shell 目录添加后续组件,例如:

npx shadcn@latest add button

生成后需要检查 diff。shadcn CLI 可能更新 globals.css、依赖或生成默认 Portal 实现,不应覆盖现有的 CSS scope、语义 token 和 portal root 适配。组件默认仅供 Web Shell 内部使用;没有明确的公共 API 需求时,不要从包入口导出。

Tailwind 会在发布前编译并内联到 npm 包,接入方不需要安装或配置 Tailwind,也不 需要额外引入 globals.css。

可选 Shadow DOM 隔离

宿主页面存在 *、h2、button 等全局规则时,可以按场景开启 Shadow DOM:

import customShadowStyles from './web-shell-shadow.css?inline';

<WebShellWithProviders
  shadowDom={{
    plugins: true,
    portals: true,
    styles: customShadowStyles,
  }}
/>;
  • plugins 隔离所有插件管理页面主体,包括统一的 Plugins 页面,以及 /extensions、/mcp、/skills 等兼容入口打开的页面。
  • portals 统一隔离 Web Shell 的所有弹窗层,包括 Dialog、Drawer、Popover、 DropdownMenu、Select 和 Tooltip;插件页面发起的弹窗也由这个开关管理。
  • styles 会追加到每个启用的 ShadowRoot,供 render props 等业务自定义内容继续 使用 class 样式。内联样式和通过 Web Shell style 设置的 CSS 变量不需要迁移。
  • --web-shell-portal-root-z-index 控制 Shadow portal host 的整体层级,默认 1000。需要与宿主自己的全局浮层协调时,可以通过 Web Shell style 覆盖。
  • shadowDom={true} 是同时开启 plugins 和 portals 的简写。

默认不开启,现有 Light DOM 接入行为不变。两个场景相互独立,例如 { plugins: true, portals: false } 会隔离插件页面主体,但所有弹窗仍挂载到原来的 Light DOM portal root。

Shadow 内部仍由原 React 树通过 portal 渲染,不会创建第二个 React root;props、 context、事件、ref 和状态语义保持不变。开启后,宿主普通选择器不会匹配 Shadow 内部节点,但宿主也无法再用普通选择器直接覆盖这些节点,所需定制样式应通过 shadowDom.styles 传入。

Web Shell 会在挂载 Shadow 内容前安装样式,并在浏览器支持时让多个 ShadowRoot 复用已经解析的 constructable stylesheet,以避免页面首次进入时的无样式闪烁和 重复解析 CSS。

图标约定

  • 新增图标统一优先使用 lucide-react,不要为已有的常见图标重复编写 SVG。
  • 使用具名静态导入,确保 Vite/Rollup 可以按需打包:
import { CheckIcon, XIcon } from 'lucide-react';
  • 不要使用 import * as Icons 后按名称动态取图标,这可能把整个图标库打入产物。
  • 图标默认使用 currentColor,尺寸优先交给 shadcn 组件或 Tailwind class 控制, 避免在每个调用处重复添加颜色、margin 和 padding。
  • 只有 Lucide 没有对应图标或需要产品专属图形时,才新增自定义 SVG。

安装

npm install @qwen-code/web-shell

Peer dependencies 需要同时安装:

npm install react react-dom @qwen-code/sdk

接入方式

WebShell 提供两种接入形态:

1. 独立接入(自带 Provider)

加载状态文案遵循有效设置 ui.accessibility.enableLoadingPhrases。设为 false 时隐藏内置和宿主自定义文案,保留加载动画、计时和取消提示。设为 true 或未配置时,可以通过 loadingPhrases={() => ['自定义文案']} 定制文案, 或返回空数组隐藏文案;该属性适用于 WebShell 和 WebShellWithProviders。 设置行的展示别名为 setting:loading-phrases。

适合只需要嵌入一个终端视图的场景。组件内部自建 DaemonWorkspaceProvider + DaemonSessionProvider。

import { WebShellWithProviders } from '@qwen-code/web-shell';

export function QwenCodePanel() {
  return (
    <WebShellWithProviders
      baseUrl="http://127.0.0.1:4170"
      token="your-bearer-token"
      sessionId="838e1811-9f84-4848-9915-d9a7f01ff5c6"
      sessionContext={{ kind: 'standalone' }}
      onSessionIdChange={(sessionId, _workspaceId, _workspaceCwd, context) => {
        console.log('current session:', sessionId, context);
      }}
      onSessionCreated={async (sessionId) => {
        await registerSession(sessionId);
      }}
      theme="dark"
      language="zh-CN"
    />
  );
}

2. 共享 Provider 接入(纯消费者)

适合同一个 React 应用中多个视图共享同一个 daemon session 的场景(如 chat + terminal)。宿主自行提供 Provider,WebShell 只消费 hooks。

import {
  DaemonWorkspaceProvider,
  DaemonSessionProvider,
  WebShell,
  useWorkspace,
} from '@qwen-code/web-shell';

function SessionViews() {
  const workspace = useWorkspace();
  if (!workspace.capabilities) {
    if (workspace.status === 'error') {
      return (
        <button
          onClick={() => void workspace.refreshCapabilities?.().catch(() => {})}
        >
          Try again
        </button>
      );
    }
    return <p role="status">Loading workspace…</p>;
  }
  return (
    <DaemonSessionProvider sessionId="...">
      <ChatPanel />
      <WebShell theme="dark" language="zh-CN" />
    </DaemonSessionProvider>
  );
}

export function App() {
  return (
    <DaemonWorkspaceProvider baseUrl="http://127.0.0.1:4170" token="...">
      <SessionViews />
    </DaemonWorkspaceProvider>
  );
}

恢复已有会话时,直接组合 Provider 的宿主需要像示例一样,等待首次 capabilities 成功后再挂载 DaemonSessionProvider,并在它上方提供发现失败的重试入口。 否则主工作区稍后确定时,会话上下文变化可能触发重复恢复。后续刷新失败会保留已知 capabilities,此时应保持会话挂载。该等待只用于首次发现,不应屏蔽真正的工作区切换。

注意:不要在已有 DaemonSessionProvider 下使用 WebShellWithProviders,否则会创建嵌套的重复 Provider。

3. 只读 ChatRecord JSONL

WebShellTranscript 只接收已经投影完成的 blocks,不连接 daemon,也不提供 composer、 审批或 session mutation。浏览器宿主可以逐行解析 JSONL,再通过 SDK 的 opt-in facade 投影:

只渲染 transcript 的宿主请从 @qwen-code/web-shell/transcript 子路径导入。包根会连带 App、daemon providers 和编辑器/终端相关代码,不要依赖 tree shaking 把它们摇掉。

import { projectChatRecordsToDaemonTranscript } from '@qwen-code/sdk/daemon/transcript';
import { WebShellTranscript } from '@qwen-code/web-shell/transcript';

const records = jsonl
  .split(/\r?\n/)
  .filter((line) => line.trim())
  .map((line) => JSON.parse(line) as unknown);
const projection = projectChatRecordsToDaemonTranscript(records);

<WebShellTranscript
  blocks={projection.blocks}
  theme="dark"
  language="zh-CN"
  style={{ height: 640 }}
/>;

宿主应显示 projection.diagnostics,并在 complete=false 或 truncated=true 时提示 历史可能不完整。组件需要一个可用高度;自定义 renderer 的副作用仍由宿主负责。

拖入文件的默认行为

通过 fileDropAction 指定拖入文件时的默认去向,适用于 WebShell 和 WebShellWithProviders:

<WebShellWithProviders fileDropAction="upload" fileUploadDirectory="uploads" />
<WebShellWithProviders fileDropAction="attach" />
  • upload:直接上传到工作区,并插入 @文件 引用。
  • attach:直接添加为当前消息的附件。
  • 不传:仅当上传和附件都可用时显示选择弹窗。

只有一种方式可用时直接使用它,即使配置的默认去向是另一种;两种都不可用时 不接收拖入文件。fileUploadEnabled={false} 只关闭工作区上传,不再关闭附件 拖入或添加附件入口。上传仍受 daemon 能力、工作区信任及目标路径检查约束。 修改默认去向或可用方式时,会关闭已经打开的选择弹窗;需要重新拖入文件。

消息操作

  • 已完成的 assistant 消息支持复制;具备持久化 checkpoint 时还支持分支。
  • 终态 turn error 支持复制显示的错误文本。重试入口保持独立,错误轮次不支持分支。

审批模式白名单

settings.json 可限制 Web Shell 提供的执行审批模式:

{
  "ui": {
    "allowedApprovalModes": ["default", "auto"]
  }
}

嵌入宿主可通过同名 Prop 配置,适用于 WebShell 和 WebShellWithProviders:

<WebShellWithProviders
  {...connectionProps}
  allowedApprovalModes={['default', 'auto']}
/>

宿主显式传入时优先于设置,包括空数组。不配置时仍显示全部四种模式: default、auto-edit、auto、yolo。不同设置作用域的列表使用替换合并, 不拼接;该键仅在用户、系统等操作者作用域生效,仓库工作区设置中的值会被忽略并警告。 支持导入 WebShellApprovalMode 类型。

主会话、分屏和侧边会话均受控:宿主 Prop 在各会话生效;未传时,各会话读取所属 工作区的设置接口(生效值仅来自操作者作用域)。下拉菜单、审批弹窗、快捷键循环和自定义页脚使用同一列表,保留内置顺序, 忽略重复值、未知值和 plan。空数组禁用下拉入口,弹窗无选项,快捷键不切换; 仅有一个且已处于该模式时,快捷键也不重复切换。非数组的错误设置回退到默认列表。

宿主 Prop 变化会更新打开的菜单和弹窗;settings.json 的编辑在下次加载 Web Shell 时生效。配置变化不会自动改变当前会话模式;当前模式被隐藏时, 入口仍显示真实模式,下一次快捷键切换到列表第一项。Plan 开关独立,不受白名单影响。 直接通过 Web Shell 斜杠命令选择隐藏模式也不会切换。

这是界面选项配置,不是后端权限约束,其他客户端和 daemon API 仍可切换模式。 该设置不在浏览器设置编辑器显示,请编辑配置文件;设置加载前使用默认列表, 独立会话无法读取工作区设置时可使用宿主 Prop。

Props

WebShellWithProviders

包含 WebShell 的所有 Props,加上 Provider 配置:

属性 类型 说明
urlNavigation WebShellUrlNavigationOptions 可选 URL 管理,含 basePath;默认关闭。详见 URL 导航。
browserNotifications WebShellBrowserNotificationsOptions 可选接入通知;appName 默认 QwenCode,iconUrl 默认内联 PNG(支持 CDN),defaultEnabled 默认 false;已保存偏好优先;不传时停用,不重建会话
baseUrl string daemon API 地址,未传时使用 window.location.origin
token string daemon API Bearer token
sessionId string 要连接的 session id;未传或 undefined 时保持空页面
workspaceId string 已注册工作区 id,主要用于定位已有 session;不会注册或锁定工作区
workspaceCwd string 已注册工作区路径,语义同 workspaceId;不会注册或锁定工作区,且优先于 workspaceId
sessionContext DaemonProductSessionContext 显式产品上下文;standalone 或 Live 上下文不能同时传 workspaceId、workspaceCwd 或 lockWorkspaceCwd
lockWorkspaceCwd string 锁定到指定工作区路径;未注册时自动持久注册,并隐藏其他工作区及添加、移除和选择入口
restartSseOnPrompt boolean 每次 prompt 被 daemon 接收后重建存活 SSE 流;流断开时提交 prompt 总会立即重建(与此开关无关);默认关闭
settings WebShellSettingsOptions 可选。控制原生 /settings 页面的呈现;见 原生设置呈现。
modelManagement WebShellModelManagementOptions 可选。控制 WebShell 内模型新增/删除交互,默认均允许;见 模型增删交互。
unavailableModels readonly WebShellUnavailableModel[] 可选。在输入框模型选择器中追加禁用模型;见 不可用模型目录。
mountUnavailableModelTooltip WebShellUnavailableModelTooltipMount 可选。为禁用模型挂载自定义提示层,Solid/原生 DOM 宿主可写入传入的 HTMLElement。

宿主目录选择

WebShell 和 WebShellWithProviders 均接受可选的 onPickWorkspaceDirectory,其公开类型 WebShellWorkspaceDirectoryPicker 为 (opts?: { signal?: AbortSignal }) => Promise<string | undefined>。 宿主返回当前已连接 daemon 文件系统中已存在目录的绝对路径;用户取消时返回 undefined,也可以抛出 AbortError。浏览器本机的目录句柄不能作为返回值。

import {
  WebShellWithProviders,
  type WebShellWorkspaceDirectoryPicker,
} from '@qwen-code/web-shell';

const onPickWorkspaceDirectory: WebShellWorkspaceDirectoryPicker = (opts) =>
  openHostDirectoryPicker({ daemonOrigin, signal: opts?.signal });

<WebShellWithProviders
  baseUrl={daemonOrigin}
  onPickWorkspaceDirectory={onPickWorkspaceDirectory}
/>;

示例中的 openHostDirectoryPicker 由宿主实现,并与 daemonOrigin 指向同一台 计算机。配置回调后,现有 Add Workspace 对话框的 Browse 按钮优先调用它; headless 或跨源 daemon 无需声明 native_directory_picker。 回调只服务于当前已连接目标,并要求 workspace 的 status === 'connected'; 状态不满足时不显示 Browse,已配置宿主回调时也不会回退到原生选择器。 不能用它为 Folder source 中另一台计算机选择路径。 返回路径只回填输入框,不会自动注册;用户仍通过 Add 完成现有注册、刷新和选中流程, 持久化能力检查和 trust 语义保持不变。选择进行中时 Add 按钮不可用,表单提交也会被 阻止,避免注册选择前的旧路径。

Git URL 来源的父目录 Browse 操作也使用此回调。宿主仍返回已存在的目录, 对话框将父目录与 URL 中的仓库名组合为克隆目标;选择本身不会启动克隆或注册, 仍需用户单独提交。它与本地目录选择共用取消、迟到结果保护、忙碌状态和焦点交接。

宿主选择器进行中时,Add Workspace 临时释放模态限制,允许点击和聚焦 Web Shell portal 外的宿主 UI;完成或取消后恢复。宿主负责自己的选择器可访问性、焦点管理和 层叠顺序。原生选择器保留原来的模态行为;这不增加公开的模态配置选项。

模态切换仍会重新挂载内容,不保证 DOM 节点身份;对话框保存并恢复正文滚动偏移。 浏览器观测确认重挂会重播入场动画;交接期间的动画抑制暂缓处理。 已获得焦点的宿主 UI 保持优先,否则经兼容 shadow root 的检查后,将焦点停放在 Add 面板。完成或取消后,以 preventScroll 将焦点返回新的 Browse 按钮;按钮不可用时 返回面板。选择期间面板设置 aria-busy,Browse 显示“正在选择目录…”。 并发打开其他 DialogShell 后,下层恢复时的 Radix 层级与 aria-hidden 排序仍未 解决;传统滚动条引起的水平布局偏移尚未验证。

Escape 仍沿用 Add Workspace 的关闭行为:宿主使用普通冒泡 Escape 处理器时, DialogShell 的 document 捕获处理会先关闭 Add 并取消选择器。宿主显式 Cancel 返回 undefined 则保留 Add;只取消宿主选择器的独立 Escape 归属仍待评审。

每次选择都会传入独立的 signal。对话框关闭、卸载、目标或连接变化、宿主/原生选择 方式切换、请求被后续选择替换时会触发 abort,宿主应据此关闭自己的 UI 并取消相关请求。即使宿主忽略 abort, 旧请求迟到的结果或错误也不会修改当前对话框。普通渲染中重建回调引用不会取消当前选择。 返回 undefined、AbortError 或其他错误均不会自动调用原生选择器;真实错误使用 现有错误提示。未配置回调时,保留原生选择器的能力、目标和同源检查及手动输入行为。 客户端取消不保证关闭服务端原生 OS 对话框。

API 位置、签名和宿主优先级是本次实现的选择,仍待维护者评审,详见 设计文档。

Workspace 会话创建超时

Workspace 创建由 SDK 分别约束能力查询和创建请求。能力查询沿用 SDK 的通用 30 秒 请求预算;创建请求按 daemon 通告的会话预算 limits.sessionRestoreTimeoutMs 加 10 秒余量, 低于 30 秒时按 30 秒计,尚未读到能力时使用 70 秒。WebShell 再按同一通告预算推导 创建动作的总上限:一次 30 秒能力预检 + 一次创建请求 + 15 秒余量,默认会话预算 60 秒下为 115 秒。冷缓存或缓存过期时,能力查询 20 秒、创建请求 15 秒可以在约 35 秒后成功,无需调整配置。 此规则也适用于已有会话时创建新会话;SDK standalone 创建保持原有超时行为。 并发能力刷新可能取代原预检并延长请求链;即使每个请求都未超过自身截止时间, 创建动作仍可能先触及该总上限。

配置/机制 默认值 作用与边界
WebShell workspace 创建动作 由通告预算推导,默认 115000 ms 兜底限制不响应 SDK 取消信号的传输;超时后成功返回的会话会被 detach。上限按 JavaScript 定时器的最大值截断。
onSessionCreated 回调 30000 ms 创建完成后才开始计时的独立宿主回调限制;没有公开的超时配置属性。

WebShell Provider 不透传 SDK 的 fetchTimeoutMs, 能力预检使用 SDK 的通用请求预算;创建与 worktree reset 请求,以及上面的创建动作上限, 都随 daemon 通告的会话预算增长。提高 --initialize-timeout-ms 会提高该通告预算, 但不改变能力预检的 30 秒超时;显式设置的 --session-restore-timeout-ms 优先。 load/resume 使用独立预算;服务端优先级、客户端覆盖顺序、能力缓存前提和缺失时的回退值见 restore 超时契约, SDK 和 WebShell 的 restore 余量见 serve 协议文档。

SDK query() 的 timeout.controlRequest 等参数属于 子进程接口,不控制 daemon HTTP 请求。兜底超时限制 WebShell 的等待时间,不保证 底层传输立即取消;迟到结果仍按原有机制清理。其他动作、会话清理和回调仍使用各自的超时。

daemon 参数的完整含义和配置方式见 daemon 配置文档。

WebShell

属性 类型 说明
onSessionIdChange (sessionId: string | undefined, workspaceId?: string, workspaceCwd?: string, sessionContext?: DaemonProductSessionContext) => void 当前 session、工作区或显式产品上下文变化时触发;standalone 和 Live 通过第四个参数上报
onRefreshModels ({ workspaceCwd: string, sessionId?: string }) => void | Promise<void> 用户刷新模型选择器时先调用;宿主可拉取并保存平台目录。未提供或返回空值都会继续重载 Qwen 模型目录并回读当前列表;拒绝则保留旧列表并显示错误。不会自动切换模型或通知其他页面。
onPickWorkspaceDirectory WebShellWorkspaceDirectoryPicker 可选。接入宿主的 daemon 目录选择器,优先于原生选择器;见 宿主目录选择。
onSessionCreated (sessionId: string) => Promise<void> | void 新 session 创建后触发;完成前会阻塞 session 初始化和 prompt 提交,最长等待 30 秒
theme 'dark' | 'light' UI 主题,默认 dark
onThemeChange (theme: WebShellTheme) => void /theme 命令切换主题后触发
onThemeResolved (theme: WebShellTheme) => void 未提供 theme 且 shell 从 daemon 设置解析出主题时触发;仅供宿主同步文档外观,不应持久化为宿主偏好
language 'en' | 'zh-CN' | 'zh' | 'zh-cn' | 'ru' | 'ru-ru' UI 语言;ru 目前只覆盖 goal.*/goals.*(部分目录),未覆盖的键按键回退英文
onLanguageChange (language: WebShellLanguage) => void /language ui 切换 UI 语言后触发
onLanguageResolved (language: WebShellLanguage) => void 未提供 language 时,有效 UI 语言在未成为宿主偏好时触发,包括设置解析、乐观切换与回滚;仅供同步文档外观
brand WebShellBrand 产品品牌(名称与 Logo,logo 为 React 节点);提供时整体取代 daemon 解析出的品牌,见下方「品牌(白标)」
onBrandResolved (brand: WebShellResolvedBrand) => void 品牌解析完成后触发,载荷只含 name 与 logoDataUri(不含 logo 节点),供宿主应用到自己的文档;shell 自身从不写 document.title 或 favicon
onSlashCommand (command: WebShellSlashCommand) => boolean | void 斜杠命令进入默认处理前触发;返回 true 时由宿主接管并跳过默认行为
onSessionArtifactsChange (change: WebShellSessionArtifactsChange) => void Session Artifact 初始恢复或变化后返回当前完整快照与 turn 投影
onAssistantTurnSettled (event: WebShellAssistantTurnSettledEvent) => void daemon 权威终态提交后触发;多个 provider 可能重复上报,宿主按 (sessionId, promptId) 去重
settings WebShellSettingsOptions 可选。控制原生 /settings 页面的呈现;见 原生设置呈现。
composerVariant 'default' | 'simple' 输入框展示模式;默认 default,simple 使用胶囊形简洁输入框,适用于主聊天、分屏和侧任务。
showToolCalls boolean 是否展示用户消息旁的工具调用入口;默认 false,独立页面设置为 true。
modelManagement WebShellModelManagementOptions 可选。控制 WebShell 内模型新增/删除交互,默认均允许;见 模型增删交互。
unavailableModels readonly WebShellUnavailableModel[] 可选。在输入框模型选择器中追加禁用模型;见 不可用模型目录。
mountUnavailableModelTooltip WebShellUnavailableModelTooltipMount 可选。为禁用模型挂载自定义提示层,Solid/原生 DOM 宿主可写入传入的 HTMLElement。

移动访问二维码入口由 header.showMobileAccess?: boolean 控制,默认隐藏,适用于主聊天和分屏页头。独立入口 main.tsx 显式设为 true,保留本地 Qwen Code 用户的入口。

宿主可以通过 onContextUsageOpen?: (sessionId: string) => void 接管上下文 详情的打开操作:

<WebShell onContextUsageOpen={(sessionId) => openContextDetails(sessionId)} />

此参数也适用于 WebShellWithProviders。提供回调后,上下文圆环的点击、 composer hover 弹层的「查看明细」、对话内上下文卡片的「查看明细」以及页头 上下文详情入口都会将来源会话 ID 交给宿主,不再打开内置右侧面板或自动读取 详情;分屏传入对应分屏的会话 ID。不提供回调则保留默认行为。压缩操作和已保存 面板的恢复不受影响。内嵌的 side-task 面板本身就在右侧面板里,没有可用的详情 入口,它的圆环点击仍会在该面板的对话中生成 /context 快照。

宿主可以监听命令,也可以返回 true 接管对应操作:

<WebShell
  onSlashCommand={({ command, args, input }) => {
    if (command !== 'deploy') return;
    openDeployDialog({ environment: args, source: input });
    return true;
  }}
/>

回调在主聊天和分屏聊天中都会触发,也可以在 daemon 断连时处理纯宿主操作。 命令名后必须是空白或输入结束,因此 /usr/local/bin/tool 等绝对路径不会触发 回调。如果回调抛出异常,Web Shell 会报告错误并继续执行默认命令流程。

宿主需要同步 Session 产物时,可以监听完整快照:

<WebShell
  onSessionArtifactsChange={({
    reason,
    sessionId,
    sequence,
    artifacts,
    artifactsByTurn,
  }) => {
    replaceSessionArtifacts({
      reason,
      sessionId,
      sequence,
      artifacts,
      artifactsByTurn,
    });
  }}
/>

restore 表示进入新 Session 后的首次恢复,可携带空快照;change 表示实时 Artifact 变化、同 Session 重连对账发现的差异,或 transcript 补齐后的 turn 归属变化。每次都返回完整 artifacts 和 artifactsByTurn,不提供增量。新 Artifact 可能先于 turn 归属交付,归属稍后补齐时会再交付一份完整快照。回调会等待 transcript load/catch-up 结束;同 Session 短暂断线保留去重基线并主动对账,因此宿主应接受重复 交付,但不需要从 SSE 时序推断遗漏。sequence 在每个新 Session 从 1 开始。宿主回调 抛错不会影响 WebShell 内置 Artifact 面板与轮末渲染。

嵌入宿主只展示普通任务会话时,可以隐藏 Sidebar 的“任务 / 频道”来源切换:

<WebShellWithProviders sidebar={{ showSessionSourceSwitch: false }} />

隐藏后,Sidebar 的会话目录固定查询 sourceType: "default";独立 WebShell 和未配置 该选项的宿主仍默认展示来源切换。

简洁输入框

通过 composerVariant="simple" 启用胶囊形输入框,适用于 WebShell 和 WebShellWithProviders:

<WebShellWithProviders composerVariant="simple" />
<WebShell composerVariant="simple" />

WebShell 仍需放在现有 daemon providers 中。省略该属性或传入 "default" 时使用原有输入框;该选项不增加 daemon 配置或持久化设置。

简洁模式在左侧保留 + 菜单,中间为编辑区,右侧显示当前主要操作:有内容时发送, 语音可用且输入为空时显示已有语音入口,运行中保留停止操作。语音不受支持或不可用 时,简洁布局右侧显示发送按钮,空内容的闲置状态下禁用,不提供麦克风占位入口。 长文本和附件可展开,继续使用已有文本编辑、输入法、附件和提交行为;简洁加号使用 中性灰。简洁录音/转写时,语音控件占据胶囊整行以容纳波形、计时和停止操作,暂时 隐藏编辑区、加号、模式和提交控件但不卸载编辑器,结束后恢复。高级/默认录音展示 保持原样。 触屏设备可通过语音/发送左侧的设置图标直接“切换至高级模式”,原地使用现有完整 移动端输入框,再通过原语音控件前的返回图标“切换至简洁模式”,顺序为返回 → 麦克风 → 发送,让麦克风紧挨发送。高级模式除返回 按钮外保持原有布局、工具栏、语音/发送行为和图标尺寸,不应用简洁模式的麦克风 放大或语音/发送互斥。切换保留文字、附件和编辑器节点,不打开菜单或全屏对话框, 也不修改 iOS 原生编辑菜单;默认模式及桌面端不新增此切换按钮。

简洁 + 菜单仅提供照片、拍照、添加文件附件、上传至工作区、引用文件、扩展、MCP、 技能八类入口,并遵守平台及能力检查。命令、历史、shell 模式、Plan、实时语音及次要 工具栏控件不放入简洁菜单,工作区、权限模式、模型、上下文和宿主控件在高级模式按 原方式展示。桌面菜单使用下拉形式,触屏使用抽屉。已有 composerToolbarActions 等配置继续决定默认和高级模式的可选操作,composerPlaceholders 占位文字覆盖继续 生效。

品牌(白标)

独立部署(qwen serve 打开的 Web Shell)用 settings.json 换名换 Logo,嵌入宿主用 brand 属性覆盖:

{
  "ui": {
    "brand": {
      "name": "QiuQiu Code",
      "logoPath": "~/.qwen/brand/logo.svg"
    }
  }
}

daemon 把该 SVG 读成 data:image/svg+xml URI,通过 GET /brand 下发。客户端始终以 <img> 渲染它,绝不作为 markup 注入:作为图片加载的 SVG 不能执行脚本,注入的可以,而 daemon 不净化它读到的文件。该配置只从 User / System / SystemDefaults 三层读取,工作区的 .qwen/settings.json 无法改写品牌 —— 那份文件通常来自打开 shell 的人并未撰写的仓库。

品牌名会替换 Sidebar 品牌行、Sidebar 底部版本 tooltip、欢迎页标题、About 面板的版本行 标签,以及独立部署下的 document.title;Logo 会替换 Sidebar 标记与 favicon。它不会替换 正文文案:本地化字符串里仍有若干处提到 Qwen Code,auth provider 标签也仍是 Qwen OAuth (那是身份提供方的名字,不是产品名)。

嵌入宿主传 brand 时整体接管名称与 Logo。logo 可以是任意 React 节点,因为宿主拥有自己 的文档与 CSP;宿主也拥有标签页标题和 favicon,shell 只在 standalone 入口写 document, 嵌入时通过 onBrandResolved 把名称与 Logo URI 交回宿主自行处理。该回调只在品牌确定后、 以及这两个值之一发生变化时触发,因此宿主可以直接传内联对象和内联函数,不会每次渲染都重放。 名称为空字符串等同于未设置,回退到内置名称。

优先级从高到低:sidebar.branding.render(整行替换,仍受支持)→ brand 属性 → daemon 解析值 → 内置默认。

<WebShellWithProviders
  brand={{ name: 'QiuQiu Code', logo: <MyLogo /> }}
  onBrandResolved={(brand) => {
    document.title = `${brand.name || 'Qwen Code'} — My Host`;
  }}
/>

Live 会话分组默认不向嵌入宿主展示;此前版本会默认展示,依赖该分组的宿主升级时 需要显式开启:

<WebShellWithProviders sidebar={{ showLive: true }} />

锁定工作区时,可以自定义 Sidebar 文件夹行的内容:

<WebShellWithProviders
  lockWorkspaceCwd="/path/to/workspace"
  sidebar={{
    lockedWorkspace: {
      render: (workspace, { expanded }) => (
        <span>
          {expanded ? '📂' : '📁'} {workspace.cwd}
        </span>
      ),
    },
  }}
/>

自定义内容仍使用内置的展开、收起行为,expanded 会随状态更新;文件夹行右侧的内置操作不会渲染。 未提供 lockWorkspaceCwd 时,该 renderer 不会执行。

左侧导航与嵌入布局

Breaking change(不兼容变更):省略 sidebar 从隐藏侧栏改为展示首页;省略导航/底部菜单列表从默认全部改为仅新建会话/收起。旧宿主若要保持隐藏,传 sidebar={false};若要保留原菜单,显式填写 primaryNav.items 和 footer.items。窄屏品牌默认显示,需隐藏时设置 branding.hideWhenCompact: true。

侧栏采用自动布局,不需要配置 layout。宿主不传 sidebar、传 true 或空配置时, 默认仅首页:展示 300px 二级栏,不展示一级图标栏。显式配置其他可用导航项后, 自动展示首页及这些一级入口。sidebar: false 或 enabled: false 仍完全隐藏侧栏。 primaryNav.items 默认仅 ['newTask'],footer.items 默认仅 ['collapse']; 独立 Web Shell 显式开启现有完整入口。宿主例如配置 sidebar={{ primaryNav: { items: ['newTask', 'plugins'] } }} 即显示首页和插件的一级栏。 未配置列表不再默认开放所有功能;宿主原有显式列表及 renderer 继续生效。

双栏模式下,桌面端左侧包含常驻图标栏和可收起的首页栏。 首页保留新建会话、置顶和工作区会话; 主要功能入口直接显示在图标栏;底部只保留「更多」与「展开/收起」, 版本号、设置、主题及其他已启用底部操作收进「更多」,关闭菜单不中断更新检查或本地文件连接。 仅首页时收起/展开按钮放在二级底部,收起保留原有 56px 窄条与悬浮会话面板。存在一级栏时按钮只放一级底部,所有二级栏均无此按钮;收起后二级完全隐藏,仅剩 56px 一级栏。点击首页或一级展开按钮恢复会话栏,点击功能图标打开对应的现有页面。 收起/展开及 Cmd/Ctrl+B 只改变侧栏显隐,保留当前页面、URL 和未保存的表单内容,不创建新会话或清除聊天草稿。

宿主现有 sidebar 配置保持有效:false 同时隐藏两栏,primaryNav.items 和 footer.items 继续控制内置入口;自定义品牌、导航及底部 renderer 保留在首页的宽区域。 仅首页时收起保留原有窄条与悬浮会话面板。Live 在嵌入模式默认关闭。双栏默认二级栏 300px,加一级图标栏 56px,总宽 356px; 单栏默认 300px。已有保存宽度优先,保存值仍表示侧栏总宽度,展开时至少为单栏 220px、双栏 276px;不对已有值直接增加 56px。分屏入口和分屏自动折叠使用容器宽度 1024px,分屏侧栏空间阈值为容器 1200px。 环境面板的停靠断点不计入图标栏的 56px(由消息区让出这部分宽度),因此面板停靠的窗口宽度阈值与仅首页侧栏一致——例如 1440px 窗口仍可停靠。

双栏的「频道」入口同时展示按工作区文件夹分组的频道会话和右侧设置页;频道归档在「设置 → 归档会话」内切换到「频道」查看。 文件夹保留展开收起、搜索与会话操作;工作区管理、添加及文件夹行操作仅在首页展示。 点击会话后右侧切为详情,再点击一级「频道」图标可重新打开管理页;左栏标题下显示分隔线, 不再提供齿轮入口,右侧标题为「设置」,双栏频道设置页不显示返回按钮。 单栏仍保留原任务/频道切换和直接管理入口。旧服务不支持来源元数据,或宿主 关闭来源切换、隐藏频道入口时,继续使用原有入口和筛选行为。

双栏开启 sidebar.showLive 且 primaryNav.items 允许 live 时,Live 作为独立一级项, 左侧展示现有语音通话入口和平铺 Live 会话,不再增加会话目录,右侧默认展示原「设置 → 实验性」中的 Qwen Live 设置。 点击会话展示详情,再次点击 Live 回到设置;导航本身不开麦,切换页面不结束通话。 Live 和频道右侧标题都为「设置」,左栏标题与首页品牌名共用字号、字重和分隔线留白。Live 表单使用限宽单列对齐布局。开关、地址、Key、模型、音色与快捷键先暂存,再由一个保存按钮统一提交;原生启用在保存时确认,取消或失败保留草稿。需要填写 Key 时在字段名称后显示红色星号。 二级栏隐藏或抽屉关闭时,语音入口回到输入框。全局设置在此时移除重复卡片;单栏、隐藏侧栏或未开放 Live 入口的宿主继续保留原设置入口。 频道图标为 MessagesSquare,Live 图标为 AudioLines;不新增通话或归档能力。

窄嵌入容器使用容器内抽屉,首页默认显示商标与品牌名;宿主可通过 branding: false 或 branding.hideWhenCompact: true 隐藏。抽屉不依赖宿主浏览器窗口宽度;宿主原有抽屉 API 仍可使用。 高度不足时图标栏可滚动。功能页表单的挂载行为沿用原页面,不承诺跨页面保留未保存的编辑。

原生设置呈现

嵌入方宿主可以在保留原生表单和模型选择器的前提下,隐藏单个原生设置项:

<WebShellWithProviders
  settings={{
    excludeItems: ['setting:fast-model', 'setting:vision-model'],
  }}
/>

也可以只开放少量条目,未列出的设置(包括上游新增设置)默认隐藏:

<WebShellWithProviders
  settings={{
    includeItems: ['setting:language', 'builtin:chat-width'],
  }}
/>

WebShellSettingsOptions.includeItems 与 excludeItems 接受 WebShellSettingItemId 值。可导入 WEB_SHELL_SETTING_ITEM_IDS 获取受支持的只读列表。这些 ID 是经过整理的别名,而不是 daemon 的配置路径:setting:language 对应语言控件(界面语言),setting:output-language 对应模型输出语言(用户作用域,仅用户标签页显示),setting:fast-model 对应快速模型选择器。即使内部 schema 路径变化,别名也保持稳定。未知的运行时 ID 不匹配任何条目;配置白名单后,尚无公开别名的字段也会隐藏。

前端内置块有自己的 ID:builtin:chat-width、builtin:browser-notifications、builtin:live-setup、builtin:local-control、builtin:connections、builtin:archived-sessions 和 builtin:model-management。builtin:connections 区块仅在 standalone 构建中渲染,而 standalone 入口不接收 settings 呈现配置,因此该 ID 目前对嵌入方没有作用。模型管理块与普通 Model 字段独立过滤;仅排除普通 Model 字段时,模型列表与选择仍然可用;配置白名单时需包含 builtin:model-management 才会显示该块。浏览器通知与聊天宽度相互独立。既有的能力和隐藏限制仍然适用,白名单不能强制显示不可用的控件,也不会改变排序。

归档会话管理位于 设置 → 归档会话,直接展示现有归档列表及工作区分组;侧边栏不再常驻“已归档”区块。只有进入该分类时才加载归档,离开后停止相关订阅。该分类沿用原有侧边栏归档功能的能力限制,禁用侧边栏的嵌入宿主不展示此分类;可通过 builtin:archived-sessions 控制其设置入口。嵌入宿主需在 sidebar.footer.items 中提供 settings,使用设置白名单时需加入 builtin:archived-sessions;既有白名单升级后不会自动加入该入口,隐藏设置入口或排除此分类会隐藏归档管理界面,底层归档 API 不受影响。支持来源元数据时,归档页内可独立切换任务/频道,不改变侧边栏活跃会话的筛选。

  • 未传 includeItems 时保持现有展示逻辑,仅应用 excludeItems;新增的上游设置默认仍然可见。
  • includeItems: [] 隐藏全部原生设置条目;它与未传入白名单不同。
  • 同时传入两个列表时,排除优先:仅展示白名单中且未被排除的条目。
  • 不传 settings、传入 {} 或仅传 excludeItems: [] 时,保持默认呈现。

过滤在工作区与用户两个作用域中都生效。被排空的分类会消失,分类导航回退到可用分类;全部隐藏则显示现有空状态。从设置面板打开的选择器会在来源条目不再可见时关闭,包括动态修改白名单的情况。

呈现限制不是访问控制。 白名单与排除列表只影响原生设置页展示,不启用或关闭底层功能,不改写已保存的配置,也不限制 daemon 写入、斜杠命令、其他入口的模型管理或直接文件访问。该选项不提供作用域策略、字段覆盖或条目级深链。

设计与验证范围见 English / 简体中文。

模型增删交互

宿主可以保留模型列表和切换,同时关闭 WebShell 内的新增与删除入口:

<WebShellWithProviders
  modelManagement={{ allowAdd: false, allowDelete: false }}
/>

公共类型 WebShellModelManagementOptions 的两个字段独立控制,省略均为 true。 allowAdd: false 隐藏新增按钮和 /auth 建议,并在主窗口、欢迎页、分屏和侧任务中拦截手动输入的 /auth 及其别名 /connect、/login。输入框提交时,拦截位于宿主 onSlashCommand 回调之后、隐藏命令转发之前,宿主回调返回 true 即可接管;消息编辑、重试和侧任务初始发送不调用该回调。命令快照就绪后,daemon 命令按解析身份判断,同名项目/用户命令可保留;App 本地 /auth 路由仍是配置弹框入口。快照未就绪时,输入框保守拒绝配置命令名称;侧任务保留这类初始提示词等待加载,五秒后提示等待状态,信息到达后重新检查。添加模型弹框无法打开或保存。 allowDelete: false 隐藏模型删除按钮并阻止删除动作。模型列表、当前标识、选择、/model、参数编辑及会话 /delete 保持原行为。

动态收紧策略会关闭相关弹框或确认,之后的浏览器队列发送读取最新策略。已交给 SDK/daemon 的请求不能由 props 撤销。恢复允许不会重新打开旧弹框。

这仅用于界面防误操作,不是安全权限。daemon API、CLI、配置文件写入和外部模型下发不受影响;settings.excludeItems 的呈现策略仍独立生效。

不可用模型目录

宿主可以把产品目录里当前不可用的模型追加到输入框选择器,显示为禁用行。这不是鉴权,也不会改 daemon 模型列表。/model、设置页和页脚 availableModels 保持原行为。与 daemon 已有模型 id 冲突时,可用模型优先。

纯文本提示:

<WebShellWithProviders
  unavailableModels={[
    { id: 'gpt-5', label: 'GPT-5', tooltip: '当前套餐不可用' },
  ]}
/>

提示层由 Web Shell 渲染为卡片:模型名作为标题,tooltip 作为说明。需要按钮(例如引导授权)时,使用 mountUnavailableModelTooltip;宿主只填标题下方的内容。公开载荷只有纯数据和一个 HTMLElement,Solid 或原生 DOM 宿主不必构造 React 元素:

import { render } from 'solid-js/web';
import { createSignal } from 'solid-js';

function mountUnavailableModelTooltip(container, info) {
  const [model, setModel] = createSignal(info.model);
  const dispose = render(
    () => (
      <div>
        <p>当前账号未授权该模型。</p>
        <button type="button" onClick={() => authorize(model().id)}>
          去授权
        </button>
      </div>
    ),
    container,
  );
  return {
    update(next) {
      setModel(next.model);
    },
    dispose,
  };
}

mount 成功则使用宿主内容;省略、返回 null/undefined 或同步失败则回退到 model.tooltip。mount 函数应保持引用稳定,并在 dispose 中拆除自己的视图。设计见 English / 简体中文。

Markdown 图表接入

WebShell 已内置 markdown-chart renderer 和 ECharts 运行时。宿主只需将 markdown-chart skill 安装到 Qwen Code 的项目级或用户级 skills 目录;例如项目级安装结果为:

.qwen/skills/markdown-chart/SKILL.md

安装 skill 后按原方式使用 WebShell,不需要额外安装或导入 ECharts,也不需要传入 图表配置:

<WebShellWithProviders baseUrl="http://127.0.0.1:4170" />

skill 默认输出 data.kind="inline" 的 canonical markdown-chart block。 WebShell 负责严格 JSON 校验、流式渲染、ECharts 生命周期以及 Chart/Data 切换;已经闭合的图表会立即渲染,只有末尾尚未闭合的 fence 显示 loading。

只有需要支持 skill 输出 data.kind="ref" 时,宿主才需要提供受控的 resolveDataRef:

import {
  createMarkdownChartRegistry,
  WebShellWithProviders,
} from '@qwen-code/web-shell';

const chartRegistry = createMarkdownChartRegistry({
  resolveDataRef: async (ref, context) =>
    loadControlledChartDataset(ref, context),
});
const markdown = { chart: { registry: chartRegistry } };

<WebShellWithProviders baseUrl="http://127.0.0.1:4170" markdown={markdown} />;

resolveDataRef 是 ref 数据的唯一读取入口;WebShell 不会自行读取 URL 或本地 路径。默认只接受规范化的 artifact:// 和 session-file:// ref,将 ref 规范化后交给 resolver,并在 30 秒后终止等待。markdown 及其中的 chart 对象应在图表挂载期间保持引用稳定。 Chart/Data 控件、无数据提示和错误提示默认跟随 WebShell 语言;需要覆盖个别 文案时可在稳定的 chart 对象上提供 labels。 协议和数据格式见 markdown-chart。

架构说明

@qwen-code/sdk/daemon         ← 协议层(SSE, REST, normalizer)
@qwen-code/web-shell          ← React adapter(Provider, hooks, store)+ 终端 UI 组件
  • WebShell 必须在 DaemonWorkspaceProvider 和 DaemonSessionProvider 之下使用。
  • WebShellWithProviders 是内置 Provider 的便捷 wrapper。
  • 同一个 React 树共享一个 DaemonSessionProvider 时只开一条 SSE。

已支持的斜杠命令

下面列出当前 web-shell 已支持的命令。支持方式分为两类:

  • 本地实现:web-shell 前端直接打开弹窗、调用 daemon REST API,或切换本地状态。
  • ACP 透传:web-shell 将命令发送给 daemon,由 daemon/ACP 执行。
命令 支持方式 说明
/help 本地实现 打开帮助弹窗,支持键盘浏览命令和快捷键。
/theme 本地实现 打开主题选择弹窗;支持 /theme light、/theme dark。
/settings 本地实现 打开设置面板,管理工作区与用户级(~/.qwen/settings.json)配置;两个作用域均可编辑并写回对应的 settings.json。
/language 本地实现 + ACP 透传 /language ui <lang> 会切换 web-shell UI 语言并同步给 daemon;其他语言能力由 daemon 执行。包含 ui、output 子命令。
/model 本地实现 + 部分透传 无参数打开模型弹窗;普通参数直接切换模型;/model --fast <model> 透传给 daemon。
/plan 本地实现 切换到 plan approval mode,并可继续发送后续 prompt。
/approval-mode 本地实现 打开审批模式弹窗或直接切换审批模式。
/mode 本地实现 web-shell 本地别名,用于切换审批模式。
/mcp 本地实现 打开 MCP 管理弹窗。
/skills 本地实现 + ACP 透传 无参数或 detail/details 打开 skills 弹窗;其他参数转换为直接 skill 命令(/skills review → /review)。
/tools 本地实现 打开 tools 弹窗,列表展示工具名称、启用状态和 description。
/memory 本地实现 打开 memory 弹窗,支持 show、refresh、add user、add project 等分支。
/agents 本地实现 打开 agents 弹窗,支持 manage、create user、create project 等分支。
/copy 本地实现 复制最后一条 assistant 输出;支持 code、语言名、LaTeX、inline LaTeX 等选择器。
/release 本地实现 释放 live session 连接,不删除历史会话记录。
/clear 本地实现 清空当前 web-shell transcript store。
/new 本地实现 创建新的 daemon session。
/reset 本地实现 与 /new 一样创建新的 daemon session。
/rename <name> 本地实现 修改当前 daemon session 的展示名称。
/resume 本地实现 无参数打开恢复会话弹窗;带 session id 时直接加载。
/status ACP 透传 daemon 支持,包含 paths 子命令。
/auth ACP 透传 连接 LLM provider。
/bug ACP 透传 提交错误报告。
/compress ACP 透传 通过摘要替换来压缩上下文。
/context ACP 透传 显示上下文窗口使用情况,包含 detail 子命令。
/diff ACP 透传 显示工作区相对 HEAD 的变更统计。
/docs ACP 透传 打开 Qwen Code 文档。
/doctor ACP 透传 执行安装与环境诊断,包含 memory 子命令。
/export ACP 透传 导出当前会话记录,包含 html、md、json、jsonl 子命令。
/goal ACP 透传 设置目标,并持续工作直到条件满足。
/init ACP 透传 分析项目并创建定制的 QWEN.md。
/stats ACP 透传 显示统计信息,包含 model、tools 子命令。
/summary ACP 透传 生成当前会话摘要。
/tasks 本地实现 打开环境信息面板并刷新后台任务。
/btw 本地实现 + ACP 透传 daemon 支持侧边任务时新建侧边任务;否则发送一个不影响主对话的侧边问题。
/fork 本地实现 + ACP 透传 启动共享当前上下文的后台智能体。
/insight ACP 透传 查看 insight 相关信息。

当前会话内容搜索

conversationSearchThreshold 控制搜索入口的消息数阈值,默认 10。 当前会话的用户和助手消息数严格超过阈值时显示入口(默认第 11 条起), 搜索图标位于左侧会话时间轴下方,采用适配窄栏的小尺寸;时间轴刻度隐藏时同步隐藏搜索入口。 WebShell 和 WebShellWithProviders 均支持此 prop,例如 <WebShellWithProviders conversationSearchThreshold={20} {...connectionProps} />。

弹框搜索用户和助手正文(包括代码),点击摘要可定位并高亮对应消息。 支持 turn navigation 的 daemon 会分页搜索持久化历史,不受当前可见区域限制; 旧 daemon 只能搜索已加载消息,弹框会明确提示。搜索结果最多展示 200 条, 超过时可缩小关键词范围。搜索不修改草稿或中断正在进行的回复。

宿主定位持久化消息

宿主先通过 WebShellWithProviders 的 sessionId 和 workspace props 打开目标会话,再调用公开 API:

import { useRef } from 'react';
import { WebShellWithProviders, type WebShellApi } from '@qwen-code/web-shell';

const shellRef = useRef<WebShellApi>(null);
// 将 shellRef 传给 <WebShellWithProviders shellRef={shellRef} {...connectionProps} />。
// 会话/历史就绪后,在宿主的结果点击处理函数里调用:
const result = await shellRef.current?.navigateToMessage({
  sessionId: selectedSessionId,
  recordId: selectedPersistedRecordId,
  signal: abortController.signal,
});

公开类型为 WebShellMessageNavigationRequest、WebShellMessageNavigationResult。 recordId 是持久化用户/助手转录记录 ID,不是渲染消息 ID 或摘要。 接口分页加载尚未渲染的历史、激活目标并在随后渲染中滚动高亮,不修改草稿; 搜索图标隐藏时也可调用。很旧的目标需要线性扫描历史,找到记录即停止。

结果 status 为 located、not_found、not_ready、session_mismatch、unsupported、 cancelled 或 error;located 不代表滚动动画已结束。未就绪时宿主须等待会话/视图就绪后再调用。聊天被面板或全页视图覆盖时返回 not_ready,宿主应先恢复聊天视图。 新请求、会话/工作目录变化、卸载或 AbortSignal 取消会使旧请求失效。 现有跨会话搜索接口仅返回会话和摘要,不提供 recordId;宿主搜索需补齐记录 ID 后才可精确定位。

URL 导航(可选)

WebShellWithProviders 支持 urlNavigation={{ basePath: '/agentic-code' }}。 独立入口默认启用同一实现,并推断既有部署基础路径(默认根路径)。嵌入组件默认不启用,原有受控 sessionId、workspaceId、workspaceCwd 和 sessionContext 接入保持兼容。 启用后,显式初始会话目标 props 优先于 URL,后续目标 props 变化 replace 地址; 宿主必须停止自行写 history,避免双重控制。lockWorkspaceCwd 仍是宿主约束。

基础路径下支持 /session/<id>、/plugins、/channels、/live、/scheduled-tasks、 /goals 和 /settings。会话保留原有 workspace / context 协议,页面仅定位 页面,设置不持久化分类或作用域。无关参数(包括宿主的实例参数)和 fragment 保留。 路由可用性遵守宿主显式传入的菜单列表;省略列表时的仅首页默认布局,不限制既有设置、插件、目标等页面 URL。/live 仍要求存在可用 Live 导航入口。 主动导航新增历史,重复点击不新增;浏览器前进后退恢复页面和会话。页面来源保存在 history.state,直接打开或复制到新标签页的页面没有来源时返回空白聊天,不创建会话。

宿主部署必须把基础路径和上述深层路径的文档请求返回宿主 HTML,并保留 API 路由。 basePath 只配置客户端,不创建服务端 rewrite。Qwen daemon 自带五个页面的文档 GET/HEAD 入口,JSON 请求、子路径和写请求仍走原有鉴权/API。 详见导航协议设计。