---
url: /wave-agent/specs/desktop/desktop-shell.md
description: >-
  CodeWave IDE 桌面端窗口与外观：开箱即用（内置 CLI）、与插件端一致的交互、SSO 登录、无干扰通知、主题设置、菜单栏与右键菜单、macOS
  隐藏标题栏、退出时保护运行中的会话
---

# 功能规格说明：桌面端窗口与外观

> 本规格描述应用这个窗口本身：打开看到的第一屏、应用菜单栏与右键菜单、登录入口、外观配色与系统通知。安装包、内置 CLI 的交付与远端同步、自动更新通道见 [desktop-distribution.md](./desktop-distribution.md)。

## 用户场景与测试

### 用户故事：无 VS Code 用户使用 CodeWave IDE（优先级：P1）

作为一名没有安装 VS Code 的用户，我希望下载并安装 CodeWave IDE 桌面应用后直接开始使用，以便我无需配置编辑器即可获得 AI 编程助手。

**为什么是这个优先级**：这是 desktop 存在的根本理由——覆盖没有编辑器的用户群体。

**独立测试**：在一台没有 VS Code、未安装 Node.js/npm 的机器上安装打包后的应用，启动后（「最近打开」列表非空时）自动进入最近目录的新对话并可直接开始对话，或经工作目录选择器选择目录后开始对话；首次启动时由内置 CLI 在启动期按需下载 grep 依赖 rg（或已缓存跳过），断网时应用仍正常启动（grep 暂不可用，下次启动重试）。

**验收场景**：

1. **假设**应用已安装且内置 CLI 随安装包发布，**当**启动 desktop 应用，**则**应用必须立即可用，本地会话直接使用内置 CLI，无需用户预先安装 Node.js/npm 或 `wave-code`：**当**用户曾选择过工作目录（「最近打开」列表非空），**则**应用必须自动执行一次与点击侧边栏「新对话」完全一致的初始化——以「最近打开」列表首项（最近主动选择的仓库根）为工作目录启动一个新的空会话（输入框可用、上下文栏显示该目录），用户打开即见可用的新对话，无需再经过目录选择步骤（等价 2026-09-08 拍板「刚打开桌面端就跟点击新对话的效果一样」，语义见 [desktop-sessions.md](./desktop-sessions.md)「会话管理」场景 2；**未登录时输入框为禁用态并显示登录原因**，见 [sso-auth.md](../enterprise/sso-auth.md)「插件端更多菜单与欢迎页」场景 8）；**当**「最近打开」列表为空（从未选择过目录，含首次启动），**则**显示工作目录选择界面，用户选择/确认后即可开始会话。
2. **假设**用户机器上未安装 Node.js/npm，**当**启动 desktop 应用，**则**本地会话必须照常工作（内置 CLI 由 Electron 内置 Node 运行），不得提示安装 Node.js。
3. **假设**应用已启动，**当**用户发送消息，**则**主进程必须通过 `wave --stdio` 子进程运行 agent 并在 UI 中流式显示回复。
4. **假设**内置 CLI 的 grep 依赖 rg 尚未下载，**当**首次启动本地会话，**则**由 CLI 自己在**启动期**从 npmmirror 下载 `@vscode/ripgrep` JS 包装与当前平台的 rg 二进制到共享 `~/.wave/cli/node_modules/@vscode/`，并在依赖就位前不对外提供会话服务；主进程不参与下载、不弹 toast（等待期间由 webview 的加载态呈现）。
5. **假设**rg 已下载过（`~/.wave/cli/node_modules/@vscode/` 下存在当前平台的 rg 二进制），**当**再次启动应用，**则**直接复用缓存，不重复下载。
6. **假设**rg 下载失败（网络不可达等），**当**首次启动本地会话，**则**应用照常启动、不弹初始化错误 toast：本次运行 Grep 工具报"ripgrep is not available"（其余功能不受影响），下次启动自动重试下载——可选依赖不得把本地会话判死，应用也不再在启动 CLI 前代装。

***

### 用户故事：与插件端一致的交互能力（优先级：P1）

作为用户，我希望 desktop 中的确认（Confirmation）、AskUserQuestion、`@文件` 提及、图片粘贴、PDF 附件等交互与插件端完全一致，以便我获得相同的使用体验。

**为什么是这个优先级**：这些是 agent 交互的安全基础与核心场景，webview 已实现，desktop 必须保证宿主消息层正确接线。

**独立测试**：触发一个需要确认的工具调用并响应；输入 `@` 选择文件；粘贴图片发送。验证行为与插件端一致。

**验收场景**：

1. **假设** agent 发起工具确认或 AskUserQuestion 请求，**当**主进程收到请求，**则**必须渲染 webview 现有的确认/问答 UI，并将用户响应回传给 CLI。
2. **假设**用户在输入框键入 `@`，**当**用户选择文件，**则**应用必须通过主进程列出工作目录中的文件供选择（复用 webview 文件选择器）。
3. **假设**用户粘贴图片或附加 PDF，**当**消息发送，**则**媒体内容必须随消息发送给 agent（复用 webview 现有实现）。

***

### 用户故事：SSO 登录（优先级：P1）

作为用户，我希望在 desktop 中完成 SSO 登录/登出，以便我可以使用企业账号访问 Wave 服务。

**为什么是这个优先级**：登录是首次使用的前置条件。SSO 登录流程（本地 HTTP 回调服务器、授权码交换、token 持久化）全程在 `wave --stdio` 子进程内完成，webview 已有登录按钮与 LoginDialog，desktop 只需接线宿主消息层。

**独立测试**：在未登录状态下启动应用，点击登录按钮，完成浏览器 SSO 授权后验证应用显示已登录状态并可以继续会话；对远程主机（未登录）执行登录，验证浏览器授权回调经端口转发返回远端 CLI、登录成功；在本地（已登录）与远程主机（未登录）会话间切换聚焦分屏，验证账户卡片菜单（已登录为个人信息行热区纯功能菜单、未登录为「更多」按钮菜单）的登录/退出登录项标注与登录态跟随切换并刷新（未登录时卡片为整条登录按钮）。

**验收场景**：

1. **假设**用户未登录，**当**用户点击登录按钮，**则**主进程必须向 CLI 发送 `login` 请求，收到 `authUrl` 通知后用系统默认浏览器打开授权页面。
2. **假设**用户在浏览器中完成授权，**当** CLI 子进程完成授权码交换，**则**主进程必须收到 `login` 响应并向 UI 回传 `loginResponse`，UI 显示已登录状态。
3. **假设**应用启动时已有有效 token，**当**主进程初始化，**则**必须通过 `getAuthStatus` 查询登录状态并通知 UI。
4. **假设**用户已登录，**当**用户触发登出，**则**主进程必须向 CLI 发送 `logout` 请求并更新 UI 为未登录状态。
5. **假设**桌面端同时存在本地与远程主机会话，**当**用户查看账户卡片（已登录态为点击个人信息行热区弹出的纯功能菜单，见 [desktop-account.md](./desktop-account.md)「账户卡片 · 个人信息行与纯功能菜单」场景 9），**则**菜单「设置 / 退出登录」项必须标注其作用的认证主体——当前聚焦分屏所属主机（远程主机显示为「设置（prod）」「退出登录（prod）」，本地主机不显示标注），菜单项的登录态必须是该主机当前的认证状态；已登录热区菜单不含登录项。未登录时账户卡片为整条「登录」按钮 + 「更多」按钮（更多菜单含登录项），点击登录按钮直接对当前聚焦分屏所属主机发起登录；VSCE/JetBrains 宿主无主机概念，菜单项保持无标注。
6. **假设**用户将聚焦分屏从一台主机切换到另一台主机或本地，**当**切换完成，**则**主进程必须重新查询新聚焦主机对应 CLI 的认证状态并推送，账户卡片（已登录热区纯功能菜单或未登录登录按钮/「更多」菜单）的登录/退出登录项（主机标注与登录态）必须刷新为新聚焦主机的最新状态，不得沿用前一主机的缓存值。
7. **假设**用户点击标注了主机名的登录/退出登录项，**当**点击生效，**则** `login`/`logout` 请求必须发给该主机（本地或远端）对应的 CLI 子进程；各主机的登录态相互独立（本地 token 与各远端主机 token 互不影响），登录/登出只作用于标注的主机。
8. **假设**用户对远程主机执行登录（点击标注主机名的登录项或未登录态账户卡片的登录按钮），**当**主进程收到该主机 CLI 的 `authUrl` 通知，**则**必须先经 SSH 把回调端口转发到本机回环（`ssh -N -L 127.0.0.1:<本地端口>:127.0.0.1:<远端端口>`，本地端口默认与远端相同、被占用时递增至首个空闲端口，仅绑定 127.0.0.1），将 `authUrl` 的 `callback_url` 重写为本地转发端口，待转发就绪后再用系统默认浏览器打开授权页面；**当**用户在浏览器完成授权、浏览器重定向到本地回调地址，**则**授权码必须经转发到达远端 CLI 完成授权码交换；`login` 请求结束后必须终止该转发进程并释放本地端口，不得残留孤儿进程。

***

### 用户故事：无干扰通知（优先级：P3）

作为用户，我希望版本更新提示和默认的 plan/通知提醒以非模态的方式出现在消息流中，以便我不会被系统弹窗打断。

**为什么是这个优先级**：desktop 无编辑器状态栏/状态对话框，需要替代通知通道。

**独立测试**：在会话中触发一个通知，验证它作为系统消息插入消息列表而非弹窗。

**验收场景**：

1. **假设** CLI 有新版本，**当**应用检测到更新，**则**必须在消息流中插入一条系统消息，而不是弹出模态对话框。
2. **假设**有后台任务或 plan 状态变化的通知，**当**通知到达，**则**默认在消息流中显示，不打扰当前输入。

***

### 用户故事：主题设置（桌面端深色/浅色/跟随系统）（优先级：P1）

作为用户，我希望在设置页「全局设置」的「桌面端设置」区块（仅桌面端显示）中选择桌面应用的外观主题（跟随系统 / 浅色 / 深色），以便应用外观匹配个人偏好，而不受系统外观限制。

**为什么是这个优先级**：设置页（P1）已提供可编辑配置项，主题是桌面端独有的显示偏好——VSCE 跟随 VS Code 主题、JetBrains 跟随 IDE LaF，保持现状不暴露选项；webview 全程依赖 `--vscode-*` CSS 变量与 `<html data-theme>` 选主题，现有跟随系统链路（启动首帧防闪烁 + 运行中 `desktopThemeChange` 广播）天然承载「跟随系统」选项，本特性增量仅为「三态选择 UI + 持久化」，随设置页补全一并交付（2026-09-02 用户拍板新增）。

**独立测试**：桌面端在系统浅色与暗色外观下分别：选择「浅色」验证界面即时切浅色、重启后保持且首帧无闪烁；选择「深色」同理；选回「跟随系统」验证运行中切换系统外观自动跟随；运行中（含流式输出期间）切换主题验证即时生效、无需重启、不中断；VSCE/JetBrains 打开设置页验证不显示主题项。

**验收场景**：

1. **假设**用户打开桌面端设置页「全局设置」，**当**查看其下「桌面端设置」区块，**则**必须显示「主题」选择：跟随系统（默认）/ 浅色 / 深色三选项；VSCE/JetBrains 设置页不得显示该区块与主题项（保持跟随 IDE 主题的现状）。
2. **假设**用户选择「浅色」或「深色」，**当**选择生效，**则**应用必须立即切换为对应主题并持久化——无需重启、不重建 React 树、不刷新页面、不中断会话与流式（仅重赋 `data-theme` 与 CSS 变量）；重启后保持所选主题，首帧即呈现所选主题、无明暗闪烁（含系统外观为相反主题的场景）。
3. **假设**用户选择「跟随系统」，**当**应用启动且当前系统外观为暗色（或亮色），**则**首帧即呈现对应主题，不得出现明暗闪烁；**当**运行中用户切换操作系统外观（亮↔暗），**则**应用自动跟随切换，无需重启、不重建 React 树、不刷新页面、不中断会话与流式。
4. **假设**用户固定主题为「浅色」（或「深色」），**当**系统外观随后变化，**则**应用不得跟随系统，保持用户所选主题。
5. **假设**主题固定为「深色」/「浅色」之外的跟随系统且 `nativeTheme` 不可用，**当**主进程解析应有效主题失败，**则**必须回退为暗色主题，不得崩溃。

***

### 用户故事：应用菜单栏结构（优先级：P3）

作为用户，我希望桌面端菜单栏在三个平台（Windows/Linux/macOS）上结构一致且全部显示中文，以便不被 Electron 默认的英文菜单干扰。

**为什么是这个优先级**：Electron 的 role 菜单（fileMenu/editMenu/viewMenu）在 Windows/Linux 上硬编码英文标签、在 macOS 上跟随系统语言，导致菜单栏出现英文；按 Electron 官方最佳实践用 role 子项 + 中文 label 显式中文化属体验优化，不影响功能。

**独立测试**：三个平台启动应用，验证菜单栏均为「文件」「编辑」「对话」「面板」「视图」「窗口」全中文且无英文项；在文本输入框按 `Ctrl+C/V/X/Z/A`（macOS 为 `Cmd+C/V`）验证剪切/复制/粘贴/撤销/全选正常；点击「视图 → 开发者工具」验证打开 DevTools；Windows/Linux 点「文件 → 退出」验证退出应用，并按 `Ctrl+Q` 验证同样退出（与会话正在流式输出时按 `Ctrl+Q` 一并验证，见下一节的退出序列要求）。

**验收场景**：

1. **假设**应用在任一平台运行，**当**用户查看应用菜单栏，**则**必须依次显示「文件」「编辑」「对话」「面板」「视图」「窗口」六个中文菜单（macOS 另有应用名菜单），不得出现英文「File」「Edit」「View」「Window」标签。
2. **假设**应用在 Windows/Linux 运行，**当**用户展开「文件」菜单，**则**必须包含「退出」项，且该退出项的加速器必须是 `Ctrl+Q`（菜单上显示、按下即退出，对齐 Claude Code Desktop）；在 macOS 上「文件」菜单必须包含「关闭窗口」且不注册 `Cmd+W` 加速器（该加速器专属「关闭分屏」），也不注册 `Ctrl+Q`（macOS 的 `Cmd+Q` 由应用名菜单的 `quit` role 承担）。
3. **假设**应用在任一平台运行，**当**用户展开「编辑」菜单，**则**必须包含撤销、重做、剪切、复制、粘贴、删除、全选（macOS 另含「粘贴并匹配样式」）；「视图」菜单必须包含重新加载、开发者工具、缩放、全屏等项。
4. **假设**应用在任一平台运行且文本焦点在输入框，**当**用户按下 `Ctrl+C`/`Ctrl+V`/`Ctrl+X`/`Ctrl+Z`/`Ctrl+A`（macOS 为 `Cmd` 键），**则**剪切、复制、粘贴、撤销、全选行为必须正常。
5. **假设**菜单包含标准动作（撤销/剪切/复制/粘贴/重新加载/开发者工具/缩放等），**当**用户点击对应菜单项，**则**必须通过 Electron `role` 实现而非自定义 `click`，以保持原生行为与快捷键。

***

### 用户故事：Windows 关闭窗口最小化到托盘（优先级：P2）

作为 Windows 用户，我希望点击窗口右上角的关闭按钮时应用不退出、而是收进系统托盘继续在后台运行，以便在长任务（正在生成的回复、执行中的工具、终端里的命令、远端会话）进行中误点关闭时不被中断，随时可以从托盘回到界面继续。

**为什么是这个优先级**：当前 Windows 上点关闭即销毁窗口并触发 `window-all-closed` → `app.quit()`，正在跑的任务连同 CLI 子进程一起被杀；而「关闭收进托盘、退出走托盘菜单」是 Windows 聊天类应用的通行预期。它改变的是窗口生命周期的主链路（不是纯体验优化），故为 P2。平台范围明确只做 Windows：macOS 关窗本就不结束进程（Dock 可重新激活），Linux 保持「关窗即退出」的桌面惯例。

**独立测试**：Windows 实机在会话正在流式输出时点关闭按钮，验证窗口消失、任务栏不再有该窗口、系统托盘出现 CodeWave IDE 图标，且后台仍在继续生成（从托盘恢复窗口后可见消息仍在追加）；单击/双击托盘图标验证窗口回到前台并聚焦；右键托盘验证菜单含「显示 CodeWave IDE」与「退出」；点「退出」验证应用真正退出（任务管理器无残留 `wave` CLI 子进程与 crashpad-handler）；从菜单栏「文件 → 退出」验证同样真正退出；窗口恢复可见并聚焦后按 `Ctrl+Q` 验证同样真正退出；macOS/Linux 实机验证无托盘图标、关闭行为不变。

**验收场景**：

1. **假设**应用在 Windows 运行且窗口可见，**当**用户点击窗口关闭按钮，**则**窗口必须隐藏到系统托盘（`hide()`，不销毁窗口），应用进程与所有后台会话继续运行（流式输出不中断、工具执行不中断、终端 PTY 不被终止），任务栏中不再显示该窗口。
2. **假设**窗口已隐藏到托盘，**当**用户单击或双击托盘图标，或右键托盘菜单选「显示 CodeWave IDE」，**则**窗口必须恢复显示、若处于最小化则先还原，并聚焦到前台；应用状态与隐藏前完全一致（不刷新页面、不重建 React 树、不中断流式）。
3. **假设**应用处于托盘常驻状态，**当**用户右键托盘图标，**则**菜单必须至少包含「显示 CodeWave IDE」与「退出」两项；不得出现空菜单。
4. **假设**用户从托盘菜单「退出」、菜单栏「文件 → 退出」，或在窗口拥有焦点时按下 `Ctrl+Q`（Windows/Linux）发起退出，**则**必须走既有退出序列（`before-quit` → dispose 主机 → 结束 CLI 子进程 → 正常 quit 序列），不得被「关闭即隐藏」拦成隐藏窗口——退出路径上窗口的 `close` 必须放行。窗口已隐藏到托盘时不要求 `Ctrl+Q` 可用（隐藏后应用不持有键盘焦点，这也正是必须保留托盘菜单「退出」的原因）：不注册全局快捷键。
5. **假设**应用运行在 macOS 或 Linux，**当**用户关闭窗口，**则**行为必须保持现状（macOS 关窗不结束进程、再次激活重建窗口；Linux 关窗即退出），不得出现托盘图标，也不得因本特性改变既有菜单与退出序列。
6. **假设**托盘创建失败（图标资源缺失、系统不允许创建托盘等），**当**用户点击关闭按钮，**则**必须回退为原有行为（正常销毁窗口并按平台规则退出），不得出现「窗口已隐藏、托盘图标又不存在」的不可达状态——托盘必须先确认创建成功，关闭才允许只隐藏。

***

### 用户故事：退出时保护运行中的会话（优先级：P2）

作为桌面端用户，我希望在还有会话正在跑的时候点退出会被问一次，而不是安静地把正在生成的内容和执行中的工具一起杀掉，以便一次误点的代价不是「这一轮白干」。

**为什么是这个优先级**：本地会话的 CLI 子进程**随应用退出而死**（`desktopHost.ts` 的 dispose 只销毁本地 agent，远端 agent 留在 daemon 上继续跑），所以退出是当前唯一会真正中断正在工作的动作，而它现在是**完全静默**的——菜单「文件 → 退出」、`Ctrl+Q`、托盘「退出」、Linux 关窗四条路径都直接走退出序列。对齐 Claude 桌面端：它在退出前会数运行中的会话并弹一次确认（`Claude is still working` / `Quit anyway` / `Wait for Claude`）。自动更新那条路径会绕开本框——用户在 S4/S5 点「立即重启」时已在对话框里知情确认过会中断任务，宿主据此记一次性标记、退出保护不再问第二遍（见 [desktop-account.md](./desktop-account.md)「账户卡片 · 更新按钮状态机 S0–S6」场景 7）。

**独立测试**：开两条会话各发一条长任务（一条在流式生成、一条在等工具确认），在 Windows 上从菜单「文件 → 退出」发起退出，验证弹出确认框、正文给出运行中会话数量；点「取消」验证应用不退、两条会话继续跑；再发起退出并点「仍然退出」，验证正常退出且无残留 CLI 子进程。再在没有任何运行中会话时发起退出，验证**不弹框、直接退出**。再只开远端主机会话并让它跑起来，验证退出时不弹框。最后把窗口关进托盘，从托盘菜单点「退出」验证窗口先回到前台再弹框。

**验收场景**：

1. **假设**应用存在至少 1 个**本地**会话处于「运行中」，**当**用户发起退出（菜单「文件 → 退出」、`Ctrl+Q`、托盘「退出」，或 Linux 上关闭窗口），**则**必须先弹出确认框并**中止本次退出**，不得直接进入退出序列。运行中口径为：正在流式生成、正在执行工具、或正等待用户确认（三者任一即算；侧栏行上的「完成且未查看」绿点不是判据）。
2. **假设**确认框已弹出，**则**标题必须给出运行中的会话数量（如「还有 2 个会话正在运行」），正文说明退出会中断正在生成的内容、执行中的工具与后台命令，按钮为「取消 / 仍然退出」；默认焦点在「取消」上，`Esc` 等价于点「取消」，不得让回车默认执行退出。
3. **假设**用户点「取消」或按 `Esc`，**则**退出必须被中止，窗口与全部会话保持原状（不暂停、不重启、不丢失已生成的内容），用户可以继续操作，之后可再次发起退出。
4. **假设**用户点「仍然退出」，**则**必须走既有退出序列（`before-quit` → dispose 主机 → 结束本地 CLI 子进程 → 正常 quit），行为与本次改动前完全一致，超时兜底不变。
5. **假设**没有任何本地会话在运行（含「应用刚启动、只有空白会话」），**当**用户发起退出，**则**不得弹框、直接退出——本特性不得给常规退出增加一次点击。
6. **假设**正在运行的会话全部在远端主机上，**当**用户发起退出，**则**不得弹框：远端会话跑在远端 daemon 上，退出不会中断它们（「真后台」语义）。本地与远端混跑时，只按本地会话计数。
7. **假设**窗口已隐藏到系统托盘（Windows）或已最小化，**当**用户从托盘菜单点「退出」，**则**必须先把窗口恢复并聚焦到前台，再弹出确认框——不得在窗口不可见的状态下弹出无处可点的对话框。
8. **假设**确认框弹出后会话状态发生变化（会话结束或又有新会话开始），**则**弹框内的数量以**弹出时刻**为准，不在框内实时刷新（避免文案跳动）；无论数量如何变化，「取消」与「仍然退出」的语义不变。
9. **假设**用户在确认框打开期间通过关闭窗口（Windows）操作，**则**按既有「关闭即隐藏到托盘」语义处理，不得因此绕过或关闭该确认框。

## 边界情况

* **运行态的实时性**：以宿主侧 agent 的真实运行态为准（`agents` 池里对应 agent 的流式/执行状态），不以侧栏快照或上一轮推送为准——避免「刚结束还没推送」导致的多余弹框，或「刚启动还没推送」导致的漏拦。
* **空白会话**：尚未发送过任何消息的空白 agent 不算运行中，不得因为它弹框。
* **确认框与更新重启同时到达**：由更新重启（S4/S5「立即重启」）发起的退出不弹本框——用户刚在更新对话框里确认过，宿主持一次性标记（`consumeUpdateRestartQuit`）直接放行（见 desktop-account.md 场景 7）；两者不得同时向用户提问。
* **多分屏**：运行中会话数与分屏数量无关，同一会话显示在两个分屏时只计一次。

## 非目标（明确排除）

* **不提供关闭该确认的设置项**：弹框只在真正有活在跑时出现，不引入「别再问我」开关。
* **不做「退出前自动暂停 / 自动保存会话」**：退出仅做一次确认，不新增暂停、快照或续跑机制（后台会话的续跑见 [desktop-sessions.md](./desktop-sessions.md)）。
* **不改变「Windows 关窗即隐藏到托盘」**：本特性挂在退出路径上，关窗行为与既有场景 1–2 逐字一致。

***

### 用户故事：文本与图片右键菜单（优先级：P3）

作为用户，我希望在消息区、输入框和面板里选中文字后右键能弹出原生右键菜单（macOS 上首项是系统词典「查词」，并带系统「服务」子菜单），以便不离开应用就能查词、用系统服务处理选中文本、复制或全选；右键图片时保持现有的「复制图片」。

**为什么是这个优先级**：Electron 默认不提供任何右键菜单（官方文档：`No context menu will appear by default in Electron`），且不会回退到 Chromium 自带那套（Chrome 的 Look Up 是 Chrome 自己实现、底层调 `ShowDefinitionForSelection`），因此须由应用自建；属体验补全，不影响既有功能，故为 P3。

**独立测试**：macOS 实机在消息区选中一个英文单词右键，验证首项为 `查词 “<单词>”`、点击弹出系统词典面板；在输入框选中文字右键，验证菜单含剪切/复制/粘贴/全选；在消息区选中文字右键并展开「服务」子菜单，验证系统服务项非灰；右键消息中的图片，验证仍是「复制图片」且可粘贴出去；在无选区、非图片的空白处右键，验证不弹菜单。

**验收场景**：

1. **假设**应用运行在 macOS，**当**用户在消息区（非编辑区）选中非空文字后右键，**则**必须弹出菜单，首项为 `查词 “<选中文本>”`（选中文本为空或右键点在链接上时不出现该项），点击该项必须调用 `webContents.showDefinitionForSelection()` 弹出系统词典面板（原生查词，不引入外部词典服务与网络请求）；菜单还须含「复制」（有选区时可用）与「全选」。
2. **假设**应用运行在 macOS，**当**用户在输入框等可编辑区右键，**则**菜单必须在只读区项目之外增加剪切/复制/粘贴/全选，各项可用性跟随 `params.editFlags`（不可用时置灰而非隐藏），且这些项必须通过 Electron `role` 实现以保持原生命令语义。
3. **假设**应用运行在 macOS，**当**用户展开右键菜单中的「服务」子菜单，**则**必须通过 `role: "services"` 提供系统服务项（含「在字典中查询」等），且 `menu.popup()` 必须传 `frame: params.frame`（该 webContents 的 `WebFrameMain`）——Electron 官方明确 macOS 的 Writing Tools/AutoFill/Services 在右键菜单中默认禁用，只有传 frame 才启用（不传则子菜单全灰）。
4. **假设**用户右键点击图片（消息内图片、文件面板内联预览等），**当**菜单弹出，**则**必须保持现有「复制图片」行为（`webContents.copyImageAt(params.x, params.y)`）不变；图片项与文字项互斥，图片右键不得同时出现查词/复制/全选。
5. **假设**用户在无选区且非图片的位置右键（空白、消息容器、非图片面板），**当**右键发生，**则**不得弹出菜单（保持现状），不得出现空菜单或仅有分隔线的菜单。
6. **假设**应用运行在 Windows/Linux，**当**用户选中文字后右键，**则**菜单必须提供复制/全选（可编辑区另含剪切/粘贴），不得出现 macOS 专属的「查词」与系统「服务」项（`showDefinitionForSelection` 与 Services 均仅 darwin 有效）。
7. **假设**应用发生任一次右键，**当**菜单构造与弹出，**则**开销必须只发生在该次右键（菜单模板按次构造，不注册常驻监听或轮询）；不得为「拼写建议」开启 `spellcheck`（不消费 `params.dictionarySuggestions`）——系统拼写检查会给整页带来持续开销，而查词与复制不需要它。

***

### 用户故事：macOS 隐藏标题栏（优先级：P1）

作为 macOS 用户，我希望窗口顶部不显示系统标题栏整条（无「CodeWave IDE」标题文字），内容区直达窗口顶部、左上角系统红绿灯保留并浮于内容之上，以便获得与 VS Code/Slack 一致的沉浸式窗口体验——隐藏的是标题栏本身，红绿灯仍是系统原生的（不做自绘窗口控制按钮）。

**为什么是这个优先级**：窗口 chrome 是桌面端打开应用的第一眼体验，隐藏标题栏方案已由用户拍板；仅 macOS 生效，Windows/Linux 视觉与行为不受影响。

**独立测试**：macOS 实机启动应用，验证窗口顶部无标题栏文字/栏体、侧边栏首行（44px 窗口行）左端显示系统红绿灯、红绿灯与右侧同排「收起侧边栏」按钮垂直同心（同一水平中心线，不显按钮偏下/红绿灯贴顶）且按钮可点击（点击后侧边栏收起，行视觉同色一体无条带）；按住窗口行空白处拖动窗口、双击按系统惯例缩放；对话区只有一个分屏时按住对话顶栏空白处（标题与右侧按钮之外）同样能拖动窗口、双击缩放，且标题点击与各按钮点击行为不变、悬停标题不再出现文本光标（竖杠）也不出现抓取手型，再开第二个分屏后同一区域拖动变成分屏换位（光标恢复抓取手型）而非移动窗口；收起侧边栏后验证对话顶栏左端让出红绿灯区域（约 76px 空白拖拽段），展开侧边栏/新对话控件整体右移不与其重叠（点开「活动」会话状态看板后同样验证其顶栏让位，「展开侧边栏」按钮不被红绿灯压住）；进入设置页（占满整个 view、会话侧边栏被覆盖）后验证红绿灯改由设置页左导航顶部让位承接（见「macOS 隐藏标题栏」设置页场景）且返回后让位交还侧边栏；进入全屏后验证红绿灯消失的同时窗口行让位自动收起（「收起侧边栏」按钮左移至行首）与收起态顶栏让位段收起，退出全屏后两者还原；Windows/Linux 实机验证仍为系统原生标题栏（含标题文字），无窗口行与让位，「收起侧边栏」按钮保留在品牌 logo 行右侧。

**验收场景**：

1. **假设**应用运行在 macOS，**当**查看窗口顶部，**则**系统标题栏必须隐藏（无标题文字与栏体），内容区延伸至窗口顶部，左上角保留系统红绿灯浮于内容之上。
2. **假设**应用运行在 macOS 且侧边栏展开，**当**查看侧边栏顶部，**则**侧边栏首行必须承载系统红绿灯（行内不绘制假圆点，避免与系统重复），红绿灯右侧同排显示「收起侧边栏」按钮；整行除按钮外均为窗口拖拽区（按住空白处拖动可移动窗口、双击按 macOS 惯例缩放），按钮本身不参与拖拽、点击即收起侧边栏（收起后窗口行随侧栏一并消失）；品牌 logo 行位于窗口行之下，logo 行右侧图标区保留「活动」等入口、不被窗口行挤占。
3. **假设**应用运行在 macOS 且侧边栏已收起，**当**查看占据窗口左端的视图顶栏（对话顶栏，或替换会话区的「活动」会话状态看板顶栏），**则**该顶栏最左端必须让出约 76px 的红绿灯区域（该段空白且同为窗口拖拽区），其「展开侧边栏」等控件整体右移，不被红绿灯遮挡；看板与对话顶栏共用同一段让位机制（同一收起条件与同一 76px 拖拽段），不得各写一套偏移。
4. **假设**应用运行在 Windows/Linux，**当**查看窗口顶部，**则**必须保持系统原生标题栏（标题文字与窗口控制按钮照旧），不得渲染 macOS 窗口行/让位——「收起侧边栏」按钮仍位于品牌行右侧图标组，既有布局与交互完全不变。
5. **假设**应用运行在 macOS 且窗口行已渲染，**当**用户操作聊天顶栏/侧边栏内其余控件（包括窗口行内的「收起侧边栏」按钮），**则**交互不受影响——窗口拖拽区仅限窗口行除按钮外的空白区域、收起态让位段，以及单分屏时的对话顶栏空白处（见场景 9）；按钮已解除拖拽（`no-drag`），点击即收起侧边栏，不误触窗口拖动。
6. **假设**原型预览等浏览器环境（无系统窗口 chrome）渲染桌面侧边栏，**当**查看其顶部，**则**继续按现状绘制假红绿灯圆点行，且红绿灯右侧同排渲染「收起侧边栏」按钮（与真机 macOS 形态一致），两者相互独立、互不影响。
7. **假设**应用运行在 macOS 且窗口进入全屏（系统红绿灯随全屏隐藏），**当**查看侧边栏顶部窗口行，**则**窗口行的红绿灯让位自动收起——「收起侧边栏」按钮左移至窗口行起点（不再预留红绿灯让位），左侧不再出现无红绿灯的空段；退出全屏后红绿灯恢复，让位与按钮位置随之还原（回到与红绿灯同排）。侧边栏收起时对话顶栏左端的红绿灯让位段（约 76px）行为一致：全屏下让位收起、控件贴左，退出全屏恢复让位。
8. **假设**应用运行在 macOS 且设置页已打开（占满整个 view、会话侧边栏被覆盖，见 desktop-settings「设置页面」场景 1/12），**当**查看设置页窗口左上角，**则**系统红绿灯必须由设置页自身左导航顶部承接：红绿灯浮于设置页导航左上角，导航首行预留红绿灯让位（设置页自身内容不被红绿灯遮挡）；该让位区为窗口拖拽区，按住可拖动窗口，区内不放置任何控件；「返回应用」按钮位于该让位区**下方单独一行**（不与红绿灯同排，让位区无按钮、无拖拽/点击冲突），点击即关闭设置页回到会话视图，红绿灯让位随之交还侧边栏；设置页打开期间窗口进入全屏（红绿灯隐藏），**则**设置页顶部让位区整行收起（内容上移贴顶），退出全屏恢复让位（行为与侧边栏窗口行一致）。
9. **假设**应用运行在 macOS 且对话区只有一个分屏（侧边栏展开或收起均可），**当**按住对话顶栏的空白处（会话标题与右侧按钮之外的区域）拖动，**则**必须移动窗口（双击按 macOS 惯例缩放）——该分屏的顶栏空白即窗口拖拽区；**当**用户点到会话标题或顶栏内任一按钮，**则**不触发窗口拖动（它们是 `no-drag`，标题保持纯展示、按钮照旧点击，见 [session-management.md](../ui/session-management.md)「会话重命名入口」的平台边界），且悬停光标必须与「纯展示 + 可拖窗」相符——标题与顶栏空白处均为系统箭头，不得出现文本编辑光标（I-beam 竖杠）；该区域的光标由系统接管（`-webkit-app-region: drag` 区不接收指针事件，CSS `cursor` 实测无效，macOS 真机 2026-11 验证），因此不显示抓取手型也不算缺陷（`grab` 属于分屏重排的手势语义，见下）；**当**对话区存在两个及以上分屏，**则**对话顶栏不承载窗口拖拽——它仍是分屏重排的拖拽手柄（按住拖动即换位/跨行，见 [desktop-split-pane.md](./desktop-split-pane.md)），顶栏与标题沿用现有 `grab`/`grabbing` 手型，窗口拖拽继续走侧边栏窗口行与侧栏收起时的 76px 让位段。窗口拖拽区与分屏重排手柄互斥（同一块手势），按分屏数量二选一，不得同时生效。

## 边界情况

* **退出应用时会话仍在运行**：退出应用必须终止 CLI 子进程，避免孤儿进程。

* **隐藏到托盘期间的长任务**：窗口隐藏不改变任何后台语义——流式输出、工具执行、终端 PTY、远端连接照旧；恢复窗口是纯显示动作，不得触发重载或重建会话（与 crashRecovery 的「窗口仍在，任务在跑」一致）。退出应用时的清理仍由既有退出序列负责。

* **隐藏到托盘期间崩溃/卡死**：崩溃恢复与卡死恢复的对话框是挂在主窗口上的模态，窗口藏在托盘里时对话框等于没弹（用户只会看到「点了没反应」），因此这类事件发生时必须先恢复窗口再到恢复流程（见 [desktop-crash-recovery.md](./desktop-crash-recovery.md)），不得在隐藏状态下弹窗。

* **auth/token 过期**：401 等鉴权失败的表现与当前 webview/stdio 行为保持一致（由 CLI 侧现有错误处理透出），本特性不做额外处理。

* **系统外观在流式输出期间变化**：系统外观在 agent 流式输出期间切换，应用必须立即跟随、不中断流式、不重建会话状态（渲染进程仅重赋 CSS 变量）。

* **nativeTheme 不可用**：主进程读取 `nativeTheme.shouldUseDarkColors` 失败时必须回退为暗色主题，不得崩溃。

* **界面内链接点击不得导航主窗口**：放行判据是「是否为本次窗口加载的那份 `webview/index.html`」（忽略 `#`/`?` 后缀），不是协议前缀——`file:` 下的任何子路径、`about:`、`data:`、`javascript:` 一律拦下；`http:`/`https:` 外链改开系统浏览器（复用 `setWindowOpenHandler` 的同一条路径），`will-navigate` 与 `will-redirect` 都要拦（重定向绕过 `will-navigate`）。原因：markdown 预览里点相对链接时 href 相对**应用文档**解析，会落在 `app.asar/webview/…` 下，一旦放行 Chromium 就用错误页顶掉整个界面（并被误判为「界面加载失败」）。这类链接在面板内的正确行为见 [desktop-file-panel.md](./desktop-file-panel.md)「文件面板」场景 22。

* **远程会话的 SSO/登录态**：由远端 CLI 自身管理，与 desktop 本地登录态相互独立；`authUrl` 通知到达时，桌面端必须先将回调端口经 SSH 转发到本机回环、重写 `callback_url` 后路由到系统浏览器（远端 CLI 的回调服务器监听在远端 127.0.0.1，本机浏览器直接访问不到），登录结束后终止转发。

## 非目标（明确排除）

* **多窗口**：单窗口架构，会话在当前窗口内切换。

* **「关闭行为」设置项**：Windows 上关闭即最小化到托盘为固定行为，不提供开关、不做「最小化/退出」二选一的偏好项。

* **托盘增强**：不做托盘气泡提示、未读角标、托盘内会话列表或快捷新建对话——托盘只承担「恢复窗口 / 退出」两件事。

* **webview 内部逻辑修改**：不改写 ChatApp/useChat 等核心交互逻辑，仅做布局扩展与组件抽取；预览特性在共享 webview 侧仅新增链接点击委托，按宿主类型分流，插件端行为不变。

* **系统「翻译」文本服务**：macOS 原生右键里那条顶级 `Translate “<text>”` 由 AppKit 为 `NSTextView` 注入，Chromium/Electron 没有对应 API（Chrome 的「翻译选中内容」是 Chrome 自接 Google 翻译实现，不是系统能力），故右键菜单只保证系统词典查词与系统「服务」子菜单；不实现自研翻译，也不为此引入原生模块或翻译服务。
