* feat(web-shell): add installable PWA support * fix(build): skip incomplete Web Shell assets when bundling * feat(web-shell): declare browser support matrix and compat fallbacks * feat(web-shell): make the Web Shell installable as a PWA Serves /manifest.webmanifest and /sw.js pre-auth, registers the worker deferred to load, and replaces the earlier service-worker.js draft with sw.js as a Vite entry (PNG icons included). * feat(mobile-shell): add Android WebView shell skeleton WebView shell around the daemon-served Web Shell (no second UI), profile tuples of URL/token/display name with #token= fragment delivery, native WebView version check at the Chrome 107 floor, and a foreground-service skeleton for SSE/notifications. * chore(web-shell): drop superseded service worker draft files * test(web-shell): cover service worker routing and lifecycle * chore(web-shell): align @playwright/test with main lockfile * chore: register mobile-shell importer in pnpm lockfile * test(web-shell): parse sidebar stylesheet with postcss for @supports nesting * chore: register mobile-shell in pnpm rewrite set * fix(web-shell): address PWA and Android acceptance blockers * docs(mobile-shell): make remaining native acceptance limits explicit * fix(web-shell): address fourth PWA review round * fix(mobile-shell): restore web view on back --------- Co-authored-by: Shaojin Wen <[email protected]>
41 KiB
@qwen-code/web-shell
Qwen Code Web Shell 是面向浏览器的 daemon 会话终端 UI,可以作为 React 组件嵌入到其他项目中。
开发网页预览
独立 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 凭据。
环境要求
- 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 中仍挂载的聊天在回合结束或失败后 可以发送系统通知。取消回合、初次加载历史和前台已处理的回合保持静默。通知显示会话标题(最多 60 个 Unicode 码点)、回合状态,以及带“提问 / 回复”标签的本轮提问(最多 80 个码点)和回复(最多 120 个码点)纯文本摘录。 标题显示 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 时保持嵌入入口原有行为,不接入通知。即使默认开启,也不会自动申请权限,用户仍需在 Settings → UI 中允许浏览器通知。修改品牌值不会重新加载当前会话。通知点击只导航所属实例,并遵守其当前锁定工作区。
低层 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等变量定制样式和层级。
在 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 Shellstyle设置的 CSS 变量不需要迁移。--web-shell-portal-root-z-index控制 Shadow portal host 的整体层级,默认1000。需要与宿主自己的全局浮层协调时,可以通过 Web Shellstyle覆盖。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)
适合只需要嵌入一个终端视图的场景。组件内部自建
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 支持复制显示的错误文本。重试入口保持独立,错误轮次不支持分支。
Props
WebShellWithProviders
包含 WebShell 的所有 Props,加上 Provider 配置:
| 属性 | 类型 | 说明 |
|---|---|---|
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 页面的呈现;见 原生设置呈现。 |
WebShell
| 属性 | 类型 | 说明 |
|---|---|---|
onSessionIdChange |
(sessionId: string | undefined, workspaceId?: string, workspaceCwd?: string, sessionContext?: DaemonProductSessionContext) => void |
当前 session、工作区或显式产品上下文变化时触发;standalone 和 Live 通过第四个参数上报 |
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' |
UI 语言 |
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 投影 |
settings |
WebShellSettingsOptions |
可选。控制原生 /settings 页面的呈现;见 原生设置呈现。 |
宿主可以通过 onContextUsageOpen?: (sessionId: string) => void 接管上下文
详情的打开操作:
<WebShell onContextUsageOpen={(sessionId) => openContextDetails(sessionId)} />
此参数也适用于 WebShellWithProviders。提供回调后,composer hover 弹层的
「查看明细」及页头上下文详情入口会将来源会话 ID 交给宿主,不再打开内置右侧
面板或自动读取详情;分屏传入对应分屏的会话 ID。不提供回调则保留默认行为。
直接点击上下文圆环生成 /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 和未配置
该选项的宿主仍默认展示来源切换。
品牌(白标)
独立部署(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 不会执行。
原生设置呈现
嵌入方宿主可以在保留原生表单和模型选择器的前提下,隐藏单个原生设置项:
<WebShellWithProviders
settings={{
excludeItems: ['setting:fast-model', 'setting:vision-model'],
}}
/>
WebShellSettingsOptions.excludeItems 接受 WebShellSettingItemId 值。可导入 WEB_SHELL_SETTING_ITEM_IDS 获取受支持的只读列表。这些 ID 是经过整理的别名,而不是 daemon 的配置路径:setting:language 对应语言控件,setting:fast-model 对应快速模型选择器。即使内部 schema 路径变化,别名也保持稳定。新增的上游设置默认仍然可见;未知的运行时 ID 会被忽略。
前端内置块有自己的 ID:builtin:chat-width、builtin:browser-notifications、builtin:live-setup、builtin:local-control 和 builtin:model-management。隐藏普通 Model 字段时,模型列表与选择仍然可用;如需隐藏模型管理块,需显式排除 builtin:model-management。浏览器通知与聊天宽度相互独立。既有的能力限制仍然适用。
不传 settings、不传 excludeItems 或传入空列表,都会保持现有呈现。排除在两个设置作用域(工作区与用户)中都生效。被排空的分类会消失,分类导航回退到可用分类;排除全部条目则显示现有的空状态。从设置面板打开的选择器会在其来源条目被排除时关闭。
呈现限制不是访问控制。 排除不会改写已保存的配置,也不限制 daemon 写入、斜杠命令、其他入口的模型管理或直接文件访问。该选项不提供白名单、作用域策略、字段覆盖或条目级深链。
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 相关信息。 |