Skip to content

功能规格说明:桌面端窗口与外观 ​

本规格描述应用这个窗口本身:打开看到的第一屏、应用菜单栏与右键菜单、登录入口、外观配色与系统通知。安装包、内置 CLI 的交付与远端同步、自动更新通道见 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「会话管理」场景 2;未登录时输入框为禁用态并显示登录原因,见 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「账户卡片 · 个人信息行与纯功能菜单」场景 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「账户卡片 · 更新按钮状态机 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)。
  • 不改变「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「会话重命名入口」的平台边界),且悬停光标必须与「纯展示 + 可拖窗」相符——标题与顶栏空白处均为系统箭头,不得出现文本编辑光标(I-beam 竖杠);该区域的光标由系统接管(-webkit-app-region: drag 区不接收指针事件,CSS cursor 实测无效,macOS 真机 2026-11 验证),因此不显示抓取手型也不算缺陷(grab 属于分屏重排的手势语义,见下);当对话区存在两个及以上分屏,则对话顶栏不承载窗口拖拽——它仍是分屏重排的拖拽手柄(按住拖动即换位/跨行,见 desktop-split-pane.md),顶栏与标题沿用现有 grab/grabbing 手型,窗口拖拽继续走侧边栏窗口行与侧栏收起时的 76px 让位段。窗口拖拽区与分屏重排手柄互斥(同一块手势),按分屏数量二选一,不得同时生效。

边界情况 ​

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

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

  • 隐藏到托盘期间崩溃/卡死:崩溃恢复与卡死恢复的对话框是挂在主窗口上的模态,窗口藏在托盘里时对话框等于没弹(用户只会看到「点了没反应」),因此这类事件发生时必须先恢复窗口再到恢复流程(见 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「文件面板」场景 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 翻译实现,不是系统能力),故右键菜单只保证系统词典查词与系统「服务」子菜单;不实现自研翻译,也不为此引入原生模块或翻译服务。