qwen-code/docs/design/qwen-code-electron-desktop/qwen-code-electron-desktop-architecture.md

901 lines
47 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 serverReact 前端通过
HTTP/WS 访问 serverserver 再管理 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。
第一版视觉优先级不是花哨,而是让用户始终清楚看到:
```text
AI 正在做什么
AI 读了哪些文件
AI 想运行什么命令
AI 改了哪些代码
用户如何接受或撤销
任务最终是否完成
```
## 推荐架构
推荐采用四层结构:
```text
┌──────────────────────────────────────────────────────────────┐
│ 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
```text
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 作为 P1Cloud 暂缓。
左侧边栏分成四块:
```text
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 形态:
```text
[ 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 至少支持单终端、命令输出、复制输出、
中止命令、发送输出给 AIP1 支持 dev/test/git 多终端 tab 和命令历史。
页面和组件清单:
```text
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 数据结构建议:
```ts
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
```text
New Task
Task:
[ 帮我修复登录页 bug并保持改动最小 ]
Mode:
(*) Local
( ) Worktree
( ) Cloud
Permissions:
(*) Ask before running commands
( ) Auto-run safe commands
( ) Read-only
[ Create Task ]
```
核心用户流程必须被 E2E 覆盖:
1. 首次使用:打开 app配置 API key 或登录,选择本地项目目录,扫描项目基本信息,
显示项目概览,创建第一个 AI task。
2. 修复 bug用户新建 taskAI 读取相关文件,给出计划,请求运行测试,用户批准,
AI 修改代码,右侧 Changes 显示 diff用户添加 inline commentAI 继续修改,
用户接受修改并 commit。
3. 解释项目结构用户输入请求AI 读取 `package.json`、入口文件、路由文件,
中间对话输出结构说明,右侧 Summary 显示关键文件列表。
4. 运行测试并修复失败项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 只暴露白名单:
```ts
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 sessionbody: `{ 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
```ts
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' };
```
## 会话生命周期
1. Renderer 启动后调用 `getServerInfo()`,配置 API base URL 和 token。
2. `projectStore.fetchProjects()``/api/projects` 拉取最近项目和 Git 元数据。
3. 用户选择项目目录后,`POST /api/projects/open` 注册项目,刷新 branch/status。
4. `sessionStore.fetchSessions()` 按项目从 `/api/sessions?cwd=` 拉取历史线程。
5. 用户新建线程时选择 mode/permissionsMVP mode 为 Local调用 ACP
`newSession({ cwd })`
6. Server 缓存 session 的 models、modes、commands、skills并打开 WS。
7. 用户发送消息WS handler 调用 `acp.prompt({ sessionId, prompt })`
8. ACP agent 在 CLI 子进程内复用 core配置、认证、模型、工具、权限、hooks、录制。
9. `sessionUpdate` 流式回到 serverserver 归一化后推给 renderer。
10. 工具需要权限时ACP `requestPermission` 被 server 转为 WS 请求renderer 展示
`@qwen-code/webui``PermissionDrawer`,响应后 server resolve ACP promise。
11. AI 修改文件后renderer 刷新 `/api/projects/:id/git/diff`,右侧 Changes 面板
展示 changed files、diff 和 accept/revert/comment 操作。
12. 用户审查完成后,可以 stage/accept 改动并通过 `/api/projects/:id/git/commit`
创建提交push 和 PR 不属于 P0 默认操作。
13. 生成结束后server 发送 `message_complete`renderer 刷新 usage、session
summary、Git status 和 session 列表。
14. 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 |
数据流:
```text
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/kill` IPC。
- 终端 cwd 默认当前 session workspace。
- 终端支持展开/收起、复制输出、清空显示、中止命令、把选中输出发送给 AI。
- MVP 支持单 terminalP1 支持 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 mainNode 原生能力更强 |
| Bun compiled server sidecar | MVP 不需要 server sidecarmain 内启动 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 tokenWS token 放 query 并在握手时校验。
- Renderer 无 Node integrationpreload 只暴露白名单 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。
方案分三层:
1. **协议层 E2E**:启动 `DesktopServer` + fake ACP agent覆盖 REST/WS 协议、
token 校验、session 创建、消息发送、permission request/response、model/mode
切换。此层运行快,适合放在 `packages/desktop` 的 vitest 测试中。
2. **真实 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/`
3. **打包后 E2E / smoke**:先 `npm run build && npm run bundle`,再
`npm run package:dir --workspace=packages/desktop`。启动打包目录中的 app验证
renderer、preload、bundled CLI、`ELECTRON_RUN_AS_NODE=1` ACP 子进程都能工作。
为了让 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 Code` renderer 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 不是纯黑或空白。
- 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/desktop` workspace。
- Electron main/preload/renderer 骨架。
- Main 启动 `DesktopServer`renderer 获取 server URL。
- `/health`、token 校验、窗口菜单、选择目录。
- WelcomePageAPI 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`spawn `qwen --acp --channel=Desktop`
- 实现 session create/load/list/prompt/cancel/mode/model。
- 实现 WS per-session 通道和 `QwenSessionUpdateHandler` 风格的事件转换。
- ChatThread 展示用户消息、AI 消息、Plan、steps、file refs、tool calls、usage。
- E2Efake 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 tabsChanges、Files、Artifacts、Summary。
- DiffViewer 支持文件树、hunk 展示、inline comment、Open in Editor。
- Commit UIcommit message 输入、stage/commit、失败提示push/PR 留到 P1。
- E2Efake file change 后 Changes 显示 diffaccept/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/smokesettings 保存后刷新仍保留;打包产物能加载 renderer 并启动 bundled CLI ACP。
### P1/P2 后续阶段
- P1Worktree 并行任务、浏览器预览、页面批注、自动化任务、slash commands、
拖拽文件/图片、浮动小窗、通知、主题/编辑器增强、GitHub PR review context。
- P2Skills 工作流、MCP/插件系统、Web Search、Memories、IDE 同步、Computer Use。
## 测试计划
单元测试:
- `AcpProcessClient`mock `ClientSideConnection`,覆盖 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。
验收命令后续应接入:
```bash
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 >=20CI 加 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 APIE2E 覆盖 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
- 不需要