47 KiB
Qwen Code Electron Desktop Architecture
目标:在现有 Qwen Code CLI、VS Code Companion、Core 能力之上,新增一个 Electron 桌面端本地 AI 编码助手工作台。本文是产品、架构与验证设计文档, 不包含代码实现。
背景与参考
当前仓库已经有三类可复用资产:
packages/core:模型配置、认证、工具注册与执行、权限、会话录制、SessionService、ChatRecordingService、MCP、hooks、subagents、memory 等核心能力。packages/cli:命令行入口、交互式 UI、非交互stream-json、ACP agent、 slash command、配置加载、认证入口。packages/vscode-ide-companion与packages/webui:VS Code WebView 聊天界面、 ACP 子进程连接、权限回调、会话更新转换,以及可复用的 React 消息、 ToolCall、PermissionDrawer、ChatViewer 组件。
参考项目 ~/Documents/cc-haha/desktop 的核心思想是:桌面壳只负责窗口、
原生能力和本地服务生命周期;复杂业务放进本地 HTTP/WebSocket 服务;服务再
编排 CLI 子进程。外部架构文档
https://claudecode-haha.relakkesyang.org/desktop/02-architecture.html
可以访问,和本地实现一致:Tauri 主进程启动 sidecar server,React 前端通过
HTTP/WS 访问 server,server 再管理 CLI 子进程。
Qwen Code 不应照搬这套实现,原因是:
- 本方案必须使用 Electron,不使用 Tauri。
- Electron 主进程本身具备 Node 运行时,不需要 Bun-compiled sidecar 才能跑本地服务。
- Qwen Code 已有 ACP agent 和
@agentclientprotocol/sdk集成;桌面端应复用 ACP, 而不是像cc-haha那样再翻译stream-json。 - Qwen Code 已有
@qwen-code/webui共享 UI,桌面端应复用组件和类型转换逻辑, 而不是从参考项目复制整套 React/store。 - 可以参考现代 AI coding desktop 的工作台思路,但不能复制其他产品的品牌、 名称、图标或具体视觉设计。
产品定位与 MVP 范围
桌面端定位是一个本地 AI 编码助手工作台。用户选择本地项目目录后,可以创建 AI 任务线程,让 AI 阅读代码、解释代码、修改代码、运行命令、查看 diff、 提交 Git 变更,并在一个桌面界面内完成从提需求到审查代码的流程。
P0 MVP 必须覆盖:
| 能力 | UI 位置 | 验收标准 |
|---|---|---|
| 项目选择/最近项目 | Welcome、左侧边栏顶部 | 用户能打开本地目录,最近项目保留项目名、路径、分支 |
| 项目与线程列表 | 左侧边栏 | 每个项目下能展示多个 thread/task,并支持新建/切换 |
| AI 对话线程 | 中间主区域 | 展示用户消息、AI 计划、执行步骤、文件引用、最终总结 |
| Composer | 中间底部 | 支持多行输入、发送、附件入口、@file、/command 入口 |
| 任务状态 | 顶部栏、线程标题、消息内 | 显示 Idle/Running/Needs Approval/Done/Error |
| 文件读取追踪 | AI 消息内 | AI 读过的文件以 chip/list 形式展示 |
| 文件修改审查 | 右侧 Changes 面板 | 修改后必须显示 changed files 和 diff |
| 接受/撤销修改 | 右侧 Changes 面板 | 支持按全部、文件、hunk 接受或撤销 |
| 集成终端 | 底部抽屉 | 当前项目/线程作用域内运行命令,输出可复制/发送给 AI |
| 命令审批 | 对话流或终端上方 | AI 运行命令前暂停,用户可 approve once/session 或 deny |
| Git 状态与提交 | 顶部栏、右侧 Summary/Changes | 显示分支、modified/staged 数量,可填写 message commit |
| 设置 | 左下角、顶部菜单、命令面板 | 配置模型/API key/权限/主题/编辑器/终端/Git |
| Chrome DevTools MCP | 开发/测试模式 | 可以连接 renderer,检查 DOM/console/network/screenshot |
| E2E | packages/desktop 或集成测试 |
P0 用户流程必须有可重复自动化验证 |
P1 建议覆盖 Worktree 并行任务、浏览器预览、页面批注、自动化任务、Automation Inbox、slash commands、文件/图片拖拽、浮动小窗、通知、主题设置、默认编辑器和 GitHub PR review context。P2 再考虑 Skills 工作流、MCP/插件设置页、Web Search、 Memories、IDE 同步和 Computer Use。
第一版视觉优先级不是花哨,而是让用户始终清楚看到:
AI 正在做什么
AI 读了哪些文件
AI 想运行什么命令
AI 改了哪些代码
用户如何接受或撤销
任务最终是否完成
推荐架构
推荐采用四层结构:
┌──────────────────────────────────────────────────────────────┐
│ Electron Main │
│ - BrowserWindow / Menu / Tray / App lifecycle │
│ - Local DesktopServer host │
│ - Native IPC: dialog, shell, window controls, optional PTY │
│ - ACP child process lifecycle │
└──────────────┬───────────────────────────────┬───────────────┘
│ preload IPC │ spawn stdio ACP
▼ ▼
┌──────────────────────────────┐ ┌────────────────────────┐
│ Electron Renderer │ │ qwen --acp child │
│ React + Zustand + webui │ │ existing CLI ACP agent │
│ HTTP + WebSocket client │ │ core/tools/auth/session│
└──────────────┬────────────────┘ └────────────────────────┘
│ HTTP/WS 127.0.0.1 random port
▼
┌──────────────────────────────────────────────────────────────┐
│ DesktopServer (Node module hosted by Electron Main) │
│ - REST API: sessions/settings/models/auth/runtime │
│ - WS API: per-session chat stream and permission routing │
│ - AcpProcessClient: ClientSideConnection to qwen --acp │
│ - Session/update normalization for renderer │
└──────────────────────────────────────────────────────────────┘
MVP 中 DesktopServer 作为 Electron main 进程内模块启动,监听
127.0.0.1:0 随机端口。这样比 cc-haha 少一个 server sidecar 进程,打包和调试
更简单。接口边界仍然按本地 HTTP/WS 设计;如果后续需要更强隔离,可以把
DesktopServer 平移到 Electron utilityProcess,渲染层和 REST/WS 协议不变。
qwen --acp 仍作为独立子进程运行。生产包中通过 Electron 可执行文件加
ELECTRON_RUN_AS_NODE=1 启动打包后的 dist/cli.js,开发态可以使用系统 Node
或 tsx 启动源码。选用 Electron 版本时必须确保内置 Node 满足 Qwen Code 的
>=20 运行要求。
为什么首选 ACP
cc-haha 的 server 通过 stream-json 与 CLI 子进程通信,然后自己转换
content_delta、tool_use_complete、permission_request 等事件。Qwen Code
已经有更合适的协议边界:
packages/cli/src/acp-integration/acpAgent.ts暴露newSession、loadSession、unstable_listSessions、prompt、cancel、setSessionMode、unstable_setSessionModel、extMethod(deleteSession/renameSession/getAccountInfo)。packages/cli/src/acp-integration/session/Session.ts直接调用 core 的GeminiChat、ToolRegistry、hooks、permission、cron、session recording。packages/vscode-ide-companion/src/services/acpConnection.ts和packages/channels/base/src/AcpBridge.ts已证明 ACP 可以作为外部宿主和 CLI 的边界。- VS Code 的
QwenSessionUpdateHandler已经处理agent_message_chunk、agent_thought_chunk、tool_call、tool_call_update、plan、available_commands_update、usage metadata。
因此桌面端应复用 ACP 子进程能力。stream-json 只作为 fallback 或 SDK 兼容路径,
不作为桌面主链路。
包与目录规划
新增 workspace:
packages/desktop/
├── package.json
├── electron.vite.config.ts
├── src/
│ ├── main/
│ │ ├── main.ts
│ │ ├── windows/MainWindow.ts
│ │ ├── lifecycle/AppLifecycle.ts
│ │ ├── ipc/registerIpc.ts
│ │ ├── native/dialogs.ts
│ │ ├── native/shell.ts
│ │ └── terminal/PtyManager.ts # scoped terminal, P0
│ ├── preload/
│ │ └── index.ts # contextBridge whitelist
│ ├── server/
│ │ ├── index.ts # startDesktopServer()
│ │ ├── http/router.ts
│ │ ├── http/auth.ts
│ │ ├── ws/SessionSocketHub.ts
│ │ ├── acp/AcpProcessClient.ts
│ │ ├── acp/AcpEventRouter.ts
│ │ ├── acp/permissionBridge.ts
│ │ ├── services/projectService.ts
│ │ ├── services/sessionService.ts
│ │ ├── services/gitService.ts
│ │ ├── services/reviewService.ts
│ │ ├── services/settingsService.ts
│ │ ├── services/runtimeService.ts
│ │ ├── services/terminalService.ts
│ │ ├── services/artifactService.ts
│ │ └── types.ts
│ └── renderer/
│ ├── main.tsx
│ ├── App.tsx
│ ├── api/client.ts
│ ├── api/websocket.ts
│ ├── stores/projectStore.ts
│ ├── stores/chatStore.ts
│ ├── stores/sessionStore.ts
│ ├── stores/reviewStore.ts
│ ├── stores/artifactStore.ts
│ ├── stores/settingsStore.ts
│ ├── stores/modelStore.ts
│ ├── stores/terminalStore.ts
│ ├── stores/uiStore.ts
│ ├── components/layout/
│ └── pages/
└── assets/
构建顺序上,packages/desktop 应放在 packages/core、packages/cli、
packages/webui 之后。根 scripts/build.js 后续增加 desktop build;根
npm run bundle 产出的 dist/cli.js 和必要 vendor/native 资源作为桌面包资源。
应用布局与页面结构
应用采用信息密度较高的开发者工具风格,深色模式优先,避免营销页和装饰性视觉。 主界面是经典工作台布局:
| 区域 | 位置 | 作用 |
|---|---|---|
| 顶部栏 | 窗口顶部 | 当前项目、Git 分支、任务模式、运行状态、快捷按钮 |
| 左侧边栏 | 左侧固定栏 | Projects、Threads、Automations、Skills、Settings |
| 中间主区域 | 屏幕中间 | AI 对话线程、计划、执行步骤、审批、用户输入框 |
| 右侧审查面板 | 屏幕右侧可折叠 | Changes、Files、Artifacts、Summary 四个 tab |
| 底部终端抽屉 | 底部可展开/收起 | 集成终端、命令输出、测试结果 |
| 弹窗/命令面板 | 全局浮层 | 新建任务、打开项目、搜索、快捷命令、设置 |
顶部栏从左到右显示当前项目名、当前 Git 分支、任务模式、运行状态,以及打开终端、 打开 diff、打开浏览器预览、命令面板和设置入口。MVP 任务模式先实现 Local, Worktree 作为 P1,Cloud 暂缓。
左侧边栏分成四块:
Projects
- project-a
- project-b
Threads
- 修复登录报错
- 解释项目结构
- 添加单元测试
Automations
- 每天检查 CI 报错
- 每周生成代码变更摘要
Bottom
- Skills
- Settings
- User Account
Projects 和 Threads 必须支持搜索、折叠和上下文菜单。MVP 右键/更多菜单至少包括: 重命名线程、归档线程、删除线程、在文件管理器中打开项目、刷新 Git 状态。
中间主区域的每条 AI 消息应能展示:
- 可折叠 Plan;
- step list,例如 1/4、2/4;
- 读取文件 chip,例如
src/App.tsx; - 修改文件卡片;
- 命令执行摘要;
- 错误卡片;
- 最终 Summary,列出改了什么、如何验证。
Composer 形态:
[ Attach ] [ @file ] [ /command ] [ text input .......... ] [ Send ]
右侧审查面板是核心体验,包含:
| Tab | 内容 |
|---|---|
| Changes | changed files、diff viewer、accept/revert file、accept/revert hunk、comment |
| Files | 项目文件树、搜索、复制路径、打开文件、让 AI 解释文件 |
| Artifacts | Markdown、HTML、JSON、测试报告、图片等非代码产物预览 |
| Summary | 当前任务完成内容、验证方式、建议下一步 |
底部终端抽屉作用域为当前项目或当前线程。MVP 至少支持单终端、命令输出、复制输出、 中止命令、发送输出给 AI;P1 支持 dev/test/git 多终端 tab 和命令历史。
页面和组件清单:
pages/
- WelcomePage 首次启动、登录/API Key、打开项目
- WorkspacePage 主工作台
- SettingsPage 设置
- AutomationsPage 自动化任务,P1
- SkillsPage 技能/工作流,P2
components/
- AppTopBar
- ProjectSidebar
- ThreadList
- ChatThread
- MessageBubble
- Composer
- ApprovalDialog
- ReviewPanel
- DiffViewer
- FileTree
- ArtifactViewer
- TerminalDrawer
- SettingsModal
- CommandPalette
核心 UI 数据结构建议:
type Project = {
id: string;
name: string;
path: string;
gitBranch?: string;
lastOpenedAt: number;
};
type Thread = {
id: string;
projectId: string;
title: string;
mode: 'local' | 'worktree' | 'cloud';
status: 'idle' | 'running' | 'waiting_approval' | 'done' | 'error';
createdAt: number;
updatedAt: number;
};
type Message = {
id: string;
threadId: string;
role: 'user' | 'assistant' | 'system' | 'tool';
content: string;
createdAt: number;
metadata?: {
files?: string[];
commands?: string[];
artifacts?: string[];
};
};
type FileChange = {
id: string;
threadId: string;
filePath: string;
status: 'added' | 'modified' | 'deleted';
diff: string;
accepted: boolean;
};
type CommandRun = {
id: string;
threadId: string;
command: string;
cwd: string;
status: 'pending_approval' | 'running' | 'success' | 'failed' | 'denied';
output: string;
createdAt: number;
};
任务模式与核心用户流程
任务模式:
| 模式 | MVP | 说明 |
|---|---|---|
| Local | 是 | AI 直接在当前项目目录读取和修改文件,所有命令和文件访问受审批控制 |
| Worktree | P1 | 每个任务创建独立 Git worktree,避免多个任务互相污染 |
| Cloud | 否 | 远程容器或云开发环境,MVP 暂缓 |
新建线程弹窗需要包含 task input、mode、permission policy:
New Task
Task:
[ 帮我修复登录页 bug,并保持改动最小 ]
Mode:
(*) Local
( ) Worktree
( ) Cloud
Permissions:
(*) Ask before running commands
( ) Auto-run safe commands
( ) Read-only
[ Create Task ]
核心用户流程必须被 E2E 覆盖:
- 首次使用:打开 app,配置 API key 或登录,选择本地项目目录,扫描项目基本信息, 显示项目概览,创建第一个 AI task。
- 修复 bug:用户新建 task,AI 读取相关文件,给出计划,请求运行测试,用户批准, AI 修改代码,右侧 Changes 显示 diff,用户添加 inline comment,AI 继续修改, 用户接受修改并 commit。
- 解释项目结构:用户输入请求,AI 读取
package.json、入口文件、路由文件, 中间对话输出结构说明,右侧 Summary 显示关键文件列表。 - 运行测试并修复失败项:AI 请求执行测试,终端显示失败输出,AI 读取日志并修改, 再次请求运行测试,测试通过后生成总结。
Electron Main
主进程职责保持薄:
- 创建主窗口,管理 macOS/Windows/Linux 菜单、关闭、重启、深色模式。
- 启动
DesktopServer,获得serverUrl与一次性随机serverToken。 - 通过 preload 暴露
getServerInfo(),renderer 再通过 HTTP/WS 连接本地服务。 - 注册安全 IPC:选择目录、打开文件、在系统文件管理器中显示、窗口控制、可选 PTY。
- 管理
qwen --acp子进程生命周期:app 退出时关闭 ACP、清理 pending permission。 - CSP 限制 renderer 只连接
self、http://127.0.0.1:*、ws://127.0.0.1:*。
Preload 只暴露白名单:
window.qwenDesktop = {
getServerInfo(): Promise<{ url: string; token: string }>,
selectDirectory(): Promise<string | null>,
openPath(path: string): Promise<void>,
showItemInFolder(path: string): Promise<void>,
window: { minimize(); maximize(); close(); isMaximized() },
terminal?: { spawn(); write(); resize(); kill() },
}
Renderer 禁用 Node integration,启用 context isolation。禁止 renderer 直接访问
fs、child_process、任意 IPC channel。
DesktopServer
DesktopServer 是 Electron main 内部启动的 Node HTTP/WS 服务,绑定
127.0.0.1 随机端口。所有 REST 请求要求:
Authorization: Bearer <serverToken>Origin必须为 app 允许来源或为空- WebSocket 使用
ws://127.0.0.1:{port}/ws/{sessionId}?token=...
核心模块:
| 模块 | 职责 |
|---|---|
AcpProcessClient |
spawn qwen --acp --channel=Desktop,建立 ClientSideConnection,封装 initialize/auth/session/prompt/cancel/mode/model/extMethod |
AcpEventRouter |
将 ACP sessionUpdate 按 sessionId 分发到 SessionSocketHub,复用 VS Code 的更新转换逻辑 |
permissionBridge |
将 ACP requestPermission / ask-user-question 转为 WS 请求,等待 renderer 回包,超时默认 cancel |
SessionSocketHub |
管理每个 session 的 WS 客户端、心跳、重连、pending 消息、广播 |
sessionService |
对 SessionService 和 ACP session API 做薄封装,提供列表、历史、创建、恢复、重命名、删除 |
projectService |
管理最近项目、项目元数据、Git 分支、当前 workspace 范围校验 |
gitService |
读取 status/diff、stage/unstage/revert、commit,禁止默认 push/force push |
reviewService |
将 Git diff 转为右侧 Changes 面板数据,支持 file/hunk accept、revert、comment |
settingsService |
读写 ~/.qwen/settings.json,复用 Storage、Coding Plan 常量、modelProviders 结构 |
runtimeService |
当前 session 的 model、approval mode、auth/account info、available commands/skills |
terminalService |
管理 scoped terminal,限制 cwd 到当前项目,输出流推送到 renderer,支持中止和输出摘要 |
artifactService |
管理 AI 生成的 Markdown/HTML/JSON/测试报告等产物,提供右侧 Artifacts 预览数据 |
REST API MVP:
| Method | Path | 说明 |
|---|---|---|
GET |
/health |
本地服务健康检查 |
GET |
/api/runtime |
CLI 路径、版本、平台、当前 auth/account 摘要 |
GET |
/api/projects |
最近项目列表,含 name/path/gitBranch/lastOpenedAt |
POST |
/api/projects/open |
选择或注册本地项目目录,刷新 Git 元数据 |
GET |
/api/sessions?cwd=&cursor=&size= |
使用 core SessionService 或 ACP list 列出会话 |
POST |
/api/sessions |
创建新 ACP session,body: { cwd } |
POST |
/api/sessions/:id/load |
恢复历史 session,调用 ACP loadSession |
GET |
/api/sessions/:id/messages |
从 Qwen JSONL 重建历史消息,供 UI 首屏渲染 |
PATCH |
/api/sessions/:id |
rename,优先 ACP extMethod('renameSession') |
DELETE |
/api/sessions/:id |
delete,优先 ACP extMethod('deleteSession') |
GET |
/api/sessions/:id/slash-commands |
返回缓存的 available_commands_update |
GET |
/api/sessions/:id/summary |
当前任务摘要、验证结果、下一步建议 |
GET |
/api/sessions/:id/artifacts |
当前任务产生的 artifacts 列表与预览元数据 |
GET |
/api/models |
当前模型和可用模型,来自 ACP NewSessionResponse.models |
PUT |
/api/sessions/:id/model |
调用 ACP unstable_setSessionModel |
GET/PUT |
/api/sessions/:id/mode |
读取/设置 approval mode |
GET |
/api/projects/:id/git/status |
当前分支、modified/staged/untracked 数量 |
GET |
/api/projects/:id/git/diff |
changed files 与 unified diff/hunk 数据 |
POST |
/api/projects/:id/git/revert |
按 all/file/hunk 撤销未提交改动 |
POST |
/api/projects/:id/git/stage |
按 all/file/hunk stage,作为 accept 的实现基础 |
POST |
/api/projects/:id/git/commit |
使用用户确认的 commit message 创建提交 |
POST |
/api/terminals |
创建当前项目/线程作用域的 terminal |
POST |
/api/terminals/:id/write |
写入终端 stdin |
POST |
/api/terminals/:id/kill |
中止终端当前进程或关闭 terminal |
GET/PUT |
/api/settings/user |
读写桌面设置和 ~/.qwen/settings.json |
POST |
/api/auth/:method |
调用 ACP authenticate 或写入 API key 后重连 |
WS 协议 MVP:
type ClientMessage =
| { type: 'user_message'; content: string; attachments?: AttachmentRef[] }
| { type: 'permission_response'; requestId: string; optionId: string }
| {
type: 'ask_user_question_response';
requestId: string;
optionId: string;
answers?: Record<string, string>;
}
| {
type: 'set_permission_mode';
mode: 'plan' | 'default' | 'auto-edit' | 'yolo';
}
| { type: 'set_model'; modelId: string }
| {
type: 'git_review_comment';
filePath: string;
line?: number;
body: string;
}
| {
type: 'terminal_output_to_prompt';
terminalId: string;
range?: OutputRange;
}
| { type: 'stop_generation' }
| { type: 'ping' };
type ServerMessage =
| { type: 'connected'; sessionId: string }
| {
type: 'message_delta';
role: 'assistant' | 'thinking' | 'user';
text: string;
}
| { type: 'tool_call'; data: ToolCallUpdateData }
| { type: 'plan'; entries: PlanEntry[] }
| {
type: 'permission_request';
requestId: string;
request: RequestPermissionRequest;
}
| {
type: 'ask_user_question';
requestId: string;
request: AskUserQuestionRequest;
}
| { type: 'usage'; data: UsageStatsPayload }
| { type: 'mode_changed'; mode: string }
| {
type: 'available_commands';
commands: AvailableCommand[];
skills: string[];
}
| { type: 'file_reference'; filePath: string; reason?: string }
| {
type: 'file_change';
filePath: string;
status: 'added' | 'modified' | 'deleted';
}
| {
type: 'git_status';
branch: string | null;
modified: number;
staged: number;
untracked: number;
}
| {
type: 'terminal_output';
terminalId: string;
text: string;
stream: 'stdout' | 'stderr';
}
| {
type: 'terminal_exit';
terminalId: string;
exitCode: number | null;
signal: string | null;
}
| { type: 'message_complete'; stopReason?: string }
| { type: 'error'; message: string; code: string; retryable?: boolean }
| { type: 'pong' };
会话生命周期
- Renderer 启动后调用
getServerInfo(),配置 API base URL 和 token。 projectStore.fetchProjects()从/api/projects拉取最近项目和 Git 元数据。- 用户选择项目目录后,
POST /api/projects/open注册项目,刷新 branch/status。 sessionStore.fetchSessions()按项目从/api/sessions?cwd=拉取历史线程。- 用户新建线程时选择 mode/permissions;MVP mode 为 Local,调用 ACP
newSession({ cwd })。 - Server 缓存 session 的 models、modes、commands、skills,并打开 WS。
- 用户发送消息,WS handler 调用
acp.prompt({ sessionId, prompt })。 - ACP agent 在 CLI 子进程内复用 core:配置、认证、模型、工具、权限、hooks、录制。
sessionUpdate流式回到 server,server 归一化后推给 renderer。- 工具需要权限时,ACP
requestPermission被 server 转为 WS 请求;renderer 展示@qwen-code/webui的PermissionDrawer,响应后 server resolve ACP promise。 - AI 修改文件后,renderer 刷新
/api/projects/:id/git/diff,右侧 Changes 面板 展示 changed files、diff 和 accept/revert/comment 操作。 - 用户审查完成后,可以 stage/accept 改动并通过
/api/projects/:id/git/commit创建提交;push 和 PR 不属于 P0 默认操作。 - 生成结束后,server 发送
message_complete,renderer 刷新 usage、session summary、Git status 和 session 列表。 - App 退出时 main 关闭 WS、停止 server、终止 ACP 子进程和 scoped terminals。
并发约束:
- 同一个 session 只允许一个 active prompt;后续消息可排队或提示当前正在生成。
- 不同 session 可以共享同一个 ACP 子进程;如果发现单进程互相影响,再演进为 per-workspace ACP process pool。
- permission request 必须有超时,默认选择 cancel,避免窗口关闭后 ACP 永久挂起。
配置与认证复用
桌面端不维护独立配置格式。用户设置仍写入:
- 全局:
~/.qwen/settings.json - 项目:
<workspace>/.qwen/settings.json - runtime output:沿用
Storage.getRuntimeBaseDir()与QWEN_RUNTIME_DIR
复用策略:
- 认证真实执行仍通过
qwen --acp的authenticate()与Config.refreshAuth()。 - API key / Coding Plan 表单复用 VS Code
settingsWriter.ts的逻辑,但应抽成 非 VS Code 依赖的共享模块,避免复制两份 JSON 写入规则。 - 模型列表来自
Config.getAllConfiguredModels()经 ACP 返回,桌面不自己推断 provider。 - approval mode 使用 Qwen 的
plan/default/auto-edit/yolo,不引入cc-haha的bypassPermissions命名。 - 权限持久化仍走 core
PermissionManager与 settings 中的permissions.allow/ask/deny。
Settings 页面采用左侧分类 + 右侧表单布局。MVP 分类:
| 分类 | MVP 设置项 |
|---|---|
| General | 默认项目目录、默认打开方式、多行输入快捷键、长任务防止睡眠 |
| Model | API provider、model、API key、reasoning effort、temperature |
| Permissions | 文件访问范围、命令运行策略、网络访问、高危命令确认、审批白名单 |
| Git | 默认 branch 命名规则、是否允许 push、commit message 生成规则 |
| Terminal | 默认 shell、环境变量、输出最大行数、命令超时 |
| Appearance | Light/Dark/System、UI font、code font、font size、accent color |
| Notifications | 任务完成、需要审批、自动化任务结果、后台运行通知 |
Browser、Integrations、Memories、Advanced 可以先放入口或占位,真实能力进入 P1/P2。
渲染进程与状态管理
Renderer 使用 React + Vite + Zustand。主界面必须是工作台,而不是只有聊天窗口。
UI 不复刻 VS Code WebView,但复用 @qwen-code/webui 中适合桌面端的消息、
工具、权限和输入组件:
- 消息:
UserMessage、AssistantMessage、ThinkingMessage、WaitingMessage - 工具:shared ToolCall components、
ToolCallContainer、AgentToolCall - 权限:
PermissionDrawer、AskUserQuestionDialog - 输入:
InputForm、CompletionMenu、SessionSelector - 历史只读视图:
ChatViewer
推荐 stores:
| Store | 状态 |
|---|---|
projectStore |
recent projects、activeProjectId、git branch/status、open/refresh/remove project |
sessionStore |
thread list、activeSessionId、mode、status、create/load/archive/delete/rename |
chatStore |
per-thread messages、plan、steps、file refs、tool calls、permission、usage、commands |
reviewStore |
changed files、selected file、diff hunks、accept/revert/comment、commit draft |
artifactStore |
generated artifacts、preview selection、test reports、summary data |
modelStore |
current model、available models、per-session model override |
settingsStore |
auth/account、approval mode、theme、editor、terminal shell、Git options、desktop settings |
uiStore |
sidebar collapsed、right panel tab、terminal drawer、command palette、dialogs |
terminalStore |
scoped terminal tabs、command output、running state、history、output selection |
数据流:
Component → Zustand action → REST/WS → DesktopServer → ACP → core/CLI
← Zustand reducer ← WS event ← DesktopServer ← ACP sessionUpdate
首屏建议是 Welcome + Workspace 的组合:首次没有项目时展示登录/API key、Open Project、最近项目;有项目后进入 WorkspacePage。WorkspacePage 必须包含顶部栏、 左侧 Projects/Threads、中心 ChatThread、右侧 ReviewPanel、底部 TerminalDrawer。
命令执行与终端
Agent 的 shell/file/tool 执行不由 Electron 实现,继续由 core 工具系统执行:
- ShellTool/Edit/Write/Read/WebFetch/MCP 等工具保持现状。
- Desktop 只负责展示 tool_call、diff、输出、权限确认。
- 这样 CLI、VS Code、Desktop 的安全语义一致。
P0 需要内置 scoped terminal,但它是用户终端,不是 agent 工具系统的替代:
- 使用仓库已有 optional dependency
@lydell/node-pty。 - Electron main 暴露
terminal:spawn/write/resize/killIPC。 - 终端 cwd 默认当前 session workspace。
- 终端支持展开/收起、复制输出、清空显示、中止命令、把选中输出发送给 AI。
- MVP 支持单 terminal;P1 支持 dev server、test、git 多 terminal tab。
- AI 要运行命令时仍走 ACP/core permission flow,并在对话中展示审批卡片; 终端抽屉显示命令输出和测试结果,但不能绕过命令审批。
- 高风险命令如
rm -rf、sudo、删除大量文件、force push、网络安装依赖等必须 高亮风险,并要求用户明确确认。
与 cc-haha 的差异化适配
| cc-haha 设计 | Qwen Desktop 适配 |
|---|---|
| Tauri/Rust 主进程 | Electron main,Node 原生能力更强 |
| Bun compiled server sidecar | MVP 不需要 server sidecar,main 内启动 Node DesktopServer |
| Server spawn 每个 session 的 CLI | 默认一个 ACP 子进程管理多个 Qwen sessions |
| stream-json 翻译成 WS 事件 | 复用 ACP sessionUpdate 和 requestPermission |
~/.claude 会话解析 |
复用 ~/.qwen、Storage、SessionService、ChatRecordingService |
| 独立 React 组件体系 | 复用 @qwen-code/webui 和 VS Code 的事件转换经验 |
| Tauri commands 做 native bridge | Electron preload + typed IPC 白名单 |
安全模型
- 本地 server 只绑定
127.0.0.1,使用随机端口和随机 token。 - REST 必须校验 bearer token;WS token 放 query 并在握手时校验。
- Renderer 无 Node integration,preload 只暴露白名单 API。
- 禁止任意命令 IPC;用户命令执行只能通过 Qwen 工具系统或可选 terminal。
- 权限请求必须显示 tool name、kind、input、diff/command/path,并支持 deny。
- 默认 AI 只能访问当前项目目录。跨目录文件读取/修改必须被权限层拦截或显式审批。
- 默认命令策略是 Ask every time。Read-only 和 Auto-approve safe commands 作为 新建线程与设置页可选权限模式。
- 文件修改必须通过右侧 diff 审查面板可见,用户可以接受或撤销;不得把修改结果 只藏在聊天消息里。
- 网络访问由设置控制,MVP 默认保守;远程 URL 访问、安装依赖、push 等动作必须 进入审批流程。
- App 退出、窗口关闭、WS 断开时,pending permission 默认 cancel。
- 外部链接用
shell.openExternal,需要 URL scheme allowlist。 - 打包时 CSP 禁止远程 script,允许本地 server connect-src。
- Chrome DevTools Protocol 只在显式开发/测试模式启用,默认生产关闭。启用时
只能绑定
127.0.0.1,端口来自QWEN_DESKTOP_CDP_PORT或专用测试脚本, 不允许监听公网地址。
E2E 与 Chrome DevTools MCP 方案
桌面端必须把 E2E 当作 MVP 验收的一部分,而不是只依赖单元测试和 smoke test。 方案分三层:
- 协议层 E2E:启动
DesktopServer+ fake ACP agent,覆盖 REST/WS 协议、 token 校验、session 创建、消息发送、permission request/response、model/mode 切换。此层运行快,适合放在packages/desktop的 vitest 测试中。 - 真实 Electron E2E:使用 Playwright Electron 启动
packages/desktop, 注入临时 HOME/QWEN_RUNTIME_DIR/workspace 和 fake ACP CLI,断言首屏不是黑屏、Connected状态出现、选择目录、新建会话、发送消息、停止生成、权限弹层响应、 settings 保存、模型/模式选择。测试应收集 screenshot、console errors、failed requests 和主进程日志,失败时写入.qwen/e2e-tests/electron-desktop/。 - 打包后 E2E / smoke:先
npm run build && npm run bundle,再npm run package:dir --workspace=packages/desktop。启动打包目录中的 app,验证 renderer、preload、bundled CLI、ELECTRON_RUN_AS_NODE=1ACP 子进程都能工作。
为了让 Codex、Ralph 或人工调试工具能看到真实 Electron renderer,桌面端要提供 Chrome DevTools MCP 可访问入口:
- Electron main 在
app.whenReady()之前读取QWEN_DESKTOP_CDP_PORT。存在时调用app.commandLine.appendSwitch('remote-debugging-address', '127.0.0.1')和app.commandLine.appendSwitch('remote-debugging-port', port)。 - 新增或保留一个开发脚本,例如
QWEN_DESKTOP_CDP_PORT=9222 npm run start --workspace=packages/desktop。脚本启动后, chrome-devtools-mcp 连接http://127.0.0.1:9222,选择Qwen Coderenderer page。 - E2E harness 启动 app 后读取 CDP endpoint,至少验证:
- page URL 是
file://.../packages/desktop/dist/renderer/index.html或 dev-server URL; - DOM 中存在
Qwen Code、Connected、Runtime、Settings; - console 没有 uncaught exception;
- network 没有 renderer asset 404;
- screenshot 不是纯黑或空白。
- page URL 是
- Chrome DevTools MCP 用于可视检查和调试,不替代可重复的 Playwright/Vitest E2E。
如果 MCP 能看到问题,必须把复现步骤固化为自动化 E2E 或在
.qwen/e2e-tests/electron-desktop/记录不能自动化的原因。
实施阶段
Phase 0: E2E Harness 与可观测性
- 新增/完善 desktop E2E harness,支持 fake ACP、临时 HOME/QWEN_RUNTIME_DIR、 临时 workspace、截图、console/network 诊断。
- 开发/测试模式支持
QWEN_DESKTOP_CDP_PORT,chrome-devtools-mcp 能连接 renderer 并检查首屏、console、network。 - 建立 “首屏不是黑屏” E2E:启动 app,断言
Qwen Code、Open Project、Settings、空项目状态可见。
Phase 1: 桌面壳、本地服务与 Welcome
- 新增/整理
packages/desktopworkspace。 - Electron main/preload/renderer 骨架。
- Main 启动
DesktopServer,renderer 获取 server URL。 /health、token 校验、窗口菜单、选择目录。- WelcomePage:API key/login 入口、Open Project、最近项目空状态。
- E2E:首次启动、服务 connected、选择临时项目目录、最近项目持久化。
Phase 2: Workspace 三栏布局与项目/线程模型
- AppTopBar、ProjectSidebar、ThreadList、ChatThread、ReviewPanel、 TerminalDrawer、CommandPalette 骨架。
- Project store 和 Thread store;左侧 Projects/Threads 支持搜索、折叠、上下文菜单。
- 顶部栏显示项目名、Git branch、mode、status、terminal/diff/settings 快捷按钮。
- E2E:打开项目后进入 WorkspacePage,创建 Local thread,切换线程,刷新 Git 状态。
Phase 3: ACP 会话链路与 AI 对话
- 实现
AcpProcessClient,spawnqwen --acp --channel=Desktop。 - 实现 session create/load/list/prompt/cancel/mode/model。
- 实现 WS per-session 通道和
QwenSessionUpdateHandler风格的事件转换。 - ChatThread 展示用户消息、AI 消息、Plan、steps、file refs、tool calls、usage。
- E2E:fake ACP 返回计划、文件引用、assistant delta、tool update、final summary。
Phase 4: 权限、命令审批与终端抽屉
- 接入 permission request 和 ask-user-question。
- ApprovalDialog 支持 Approve once、Approve for this thread、Deny。
- TerminalDrawer 支持 scoped terminal、命令输出、复制、清空、中止、发送给 AI。
- 高风险命令高亮并二次确认;终端不能绕过 agent 命令审批。
- E2E:命令审批 pending/running/denied/success;终端输出可见并可发送给 AI。
Phase 5: Diff Review、文件树、Artifacts 与 Commit
- gitService/reviewService 支持 status、diff、accept/revert all/file/hunk、comment。
- Right panel tabs:Changes、Files、Artifacts、Summary。
- DiffViewer 支持文件树、hunk 展示、inline comment、Open in Editor。
- Commit UI:commit message 输入、stage/commit、失败提示;push/PR 留到 P1。
- E2E:fake file change 后 Changes 显示 diff;accept/revert/commit 流程可验证。
Phase 6: 设置、认证、模型与打包
- 抽取 VS Code settings writer 的通用部分。
- SettingsPage/SettingsModal 覆盖 General、Model、Permissions、Git、Terminal、 Appearance、Notifications。
- 实现 Coding Plan / OpenAI-compatible provider 配置 UI。
- 打包包含
dist/cli.js、vendor ripgrep、native optional deps。 - 生产态使用
ELECTRON_RUN_AS_NODE=1启动 CLI ACP 子进程。 - E2E/smoke:settings 保存后刷新仍保留;打包产物能加载 renderer 并启动 bundled CLI ACP。
P1/P2 后续阶段
- P1:Worktree 并行任务、浏览器预览、页面批注、自动化任务、slash commands、 拖拽文件/图片、浮动小窗、通知、主题/编辑器增强、GitHub PR review context。
- P2:Skills 工作流、MCP/插件系统、Web Search、Memories、IDE 同步、Computer Use。
测试计划
单元测试:
AcpProcessClient:mockClientSideConnection,覆盖 initialize/new/load/prompt/cancel。AcpEventRouter:覆盖 message/tool/plan/usage/available_commands 映射。permissionBridge:覆盖 allow/deny/timeout/window closed。settingsService:使用临时QWEN_RUNTIME_DIR/ HOME,验证 settings JSON 写入。- renderer stores:连接、发送、streaming、tool update、permission 状态。
集成测试:
- 启动 DesktopServer + fake ACP agent,验证 REST/WS 协议。
- 启动真实
qwen --acp,用临时 workspace 创建 session,发送简单 prompt。 - 验证 session JSONL 可被 core
SessionService读取。
E2E:
packages/desktop增加 Electron E2E harness。优先使用 Playwright Electron; 如果仓库不引入额外 Playwright 依赖,则用 Electron 的 CDP endpoint 配合 chrome-devtools-mcp/DevTools Protocol 做可视与 DOM 断言,但最终仍要沉淀成可重复 的自动化测试。- 每个用户可见行为切片必须先写 E2E 场景,至少覆盖:
- 首次使用:启动 app 后首屏不是黑屏,配置 API key/login 入口可见,选择临时 项目目录后进入 Workspace;
- 修复 bug:新建 Local thread,发送“修复登录页点击无反应”,fake ACP 返回计划、 文件读取、命令审批、文件修改,右侧 Changes 显示 diff,用户 comment 后继续修改, 最后 accept 并 commit;
- 解释项目结构:fake ACP 读取
package.json、入口文件、路由文件,中心消息和 Summary tab 显示关键文件列表; - 运行测试并修复失败项:命令审批
npm test,终端显示失败输出,AI 修改文件, 再次请求测试,最终 Summary 标记测试通过; - 权限与安全:permission request 和 ask-user-question 能从 renderer 响应到 fake ACP, denied/timeout 都有清晰 UI 状态;
- 设置:settings/auth/model/mode/terminal/theme 保存后刷新仍保留;
- 取消生成:会调用 ACP cancel 并更新 UI;
- 打包目录启动后 renderer asset、preload、bundled CLI ACP 都可用。
- E2E 运行时必须收集失败诊断:Electron main stderr/stdout、renderer console、 failed network requests、截图、CDP page list、DesktopServer URL/token 状态摘要。
- chrome-devtools-mcp 调试路径必须纳入验收:使用固定本地 CDP 端口启动 app, 通过 MCP 连接 renderer,确认页面 URL、DOM、console/network 状态。发现黑屏或 asset 404 时先修复 Vite base/path,再重跑 E2E。
验收命令后续应接入:
npm run build
npm run typecheck
npm test --workspace=packages/desktop
npm run test:e2e --workspace=packages/desktop
npm run package:dir --workspace=packages/desktop
npm run smoke:package --workspace=packages/desktop
npm run smoke:package --workspace=packages/desktop -- --launch
风险与应对
| 风险 | 应对 |
|---|---|
| Electron 内置 Node 版本不满足 CLI | 固定 Electron 版本时校验 Node >=20;CI 加 smoke test |
| packaged app 无法启动 CLI JS | 生产态使用 ELECTRON_RUN_AS_NODE=1;明确资源路径;打包 smoke test |
| ACP 单进程多 session 互相影响 | 先复用现有 ACP;若发现阻塞,升级为 per-workspace process pool |
| VS Code settings writer 被复制 | 抽成 core/desktop 共用的 settings writer 模块 |
| 本地 server 被其他本地进程调用 | 127.0.0.1 + random token + origin 校验 |
| native deps 打包遗漏 | 列出 @lydell/node-pty、ripgrep、clipboard 等资源清单;打包后 smoke test |
| renderer 与 ACP 事件类型漂移 | 复用/抽取 QwenSessionUpdateHandler 测试夹具 |
| MVP 退化成聊天壳 | 每个切片必须保留工作台布局和右侧审查/底部终端可见验收 |
| hunk 级 accept/revert 误伤代码 | 优先使用 Git/index 或结构化 patch API;E2E 覆盖 file/hunk 操作 |
| 终端绕过命令审批 | 用户 terminal 和 agent command permission 分离;AI 命令仍走 ACP 审批 |
开放问题
- 桌面端是否需要第一版就支持多窗口,还是先单窗口多 tab?
- 单窗口多 tab
- 是否把
AcpProcessClient抽成packages/acp-client,供 VS Code、channels、desktop 共用?- 不需要,先放 desktop 内部
- Desktop settings UI 是只覆盖认证/模型/权限,还是完整暴露
settings.json?- MVP 覆盖 General、Model、Permissions、Git、Terminal、Appearance、Notifications; 不直接暴露完整 JSON 编辑器
- 是否在 MVP 中内置 terminal,还是先专注 chat/workspace?
- MVP 需要 scoped terminal drawer,但它是用户终端,不绕过 agent 命令审批
- 是否需要独立桌面命令
qwen desktop启动 Electron?- 不需要