diff --git a/docs/design/qwen-code-electron-desktop/home.jpg b/docs/design/qwen-code-electron-desktop/home.jpg new file mode 100644 index 000000000..d69bc8f4b Binary files /dev/null and b/docs/design/qwen-code-electron-desktop/home.jpg differ diff --git a/docs/design/qwen-code-electron-desktop/qwen-code-electron-desktop-architecture.md b/docs/design/qwen-code-electron-desktop/qwen-code-electron-desktop-architecture.md new file mode 100644 index 000000000..d1bdeed91 --- /dev/null +++ b/docs/design/qwen-code-electron-desktop/qwen-code-electron-desktop-architecture.md @@ -0,0 +1,901 @@ +# 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? + - 不需要