# 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。 第一版视觉优先级不是花哨,而是让用户始终清楚看到: ```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 作为 P1,Cloud 暂缓。 左侧边栏分成四块: ```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 至少支持单终端、命令输出、复制输出、 中止命令、发送输出给 AI;P1 支持 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:用户新建 task,AI 读取相关文件,给出计划,请求运行测试,用户批准, AI 修改代码,右侧 Changes 显示 diff,用户添加 inline comment,AI 继续修改, 用户接受修改并 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, openPath(path: string): Promise, showItemInFolder(path: string): Promise, 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 ` - `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: ```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; } | { 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/permissions;MVP 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` 流式回到 server,server 归一化后推给 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` - 项目:`/.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 支持单 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。 方案分三层: 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 校验、窗口菜单、选择目录。 - 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`,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。 - 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`: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 >=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? - 不需要