Appearance
功能规格说明:桌面端窗口与外观
本规格描述应用这个窗口本身:打开看到的第一屏、应用菜单栏与右键菜单、登录入口、外观配色与系统通知。安装包、内置 CLI 的交付与远端同步、自动更新通道见 desktop-distribution.md。
用户场景与测试
用户故事:无 VS Code 用户使用 CodeWave IDE(优先级:P1)
作为一名没有安装 VS Code 的用户,我希望下载并安装 CodeWave IDE 桌面应用后直接开始使用,以便我无需配置编辑器即可获得 AI 编程助手。
为什么是这个优先级:这是 desktop 存在的根本理由——覆盖没有编辑器的用户群体。
独立测试:在一台没有 VS Code、未安装 Node.js/npm 的机器上安装打包后的应用,启动后(「最近打开」列表非空时)自动进入最近目录的新对话并可直接开始对话,或经工作目录选择器选择目录后开始对话;首次启动时由内置 CLI 在启动期按需下载 grep 依赖 rg(或已缓存跳过),断网时应用仍正常启动(grep 暂不可用,下次启动重试)。
验收场景:
- 假设应用已安装且内置 CLI 随安装包发布,当启动 desktop 应用,则应用必须立即可用,本地会话直接使用内置 CLI,无需用户预先安装 Node.js/npm 或
wave-code:当用户曾选择过工作目录(「最近打开」列表非空),则应用必须自动执行一次与点击侧边栏「新对话」完全一致的初始化——以「最近打开」列表首项(最近主动选择的仓库根)为工作目录启动一个新的空会话(输入框可用、上下文栏显示该目录),用户打开即见可用的新对话,无需再经过目录选择步骤(等价 2026-09-08 拍板「刚打开桌面端就跟点击新对话的效果一样」,语义见 desktop-sessions.md「会话管理」场景 2;未登录时输入框为禁用态并显示登录原因,见 sso-auth.md「插件端更多菜单与欢迎页」场景 8);当「最近打开」列表为空(从未选择过目录,含首次启动),则显示工作目录选择界面,用户选择/确认后即可开始会话。 - 假设用户机器上未安装 Node.js/npm,当启动 desktop 应用,则本地会话必须照常工作(内置 CLI 由 Electron 内置 Node 运行),不得提示安装 Node.js。
- 假设应用已启动,当用户发送消息,则主进程必须通过
wave --stdio子进程运行 agent 并在 UI 中流式显示回复。 - 假设内置 CLI 的 grep 依赖 rg 尚未下载,当首次启动本地会话,则由 CLI 自己在启动期从 npmmirror 下载
@vscode/ripgrepJS 包装与当前平台的 rg 二进制到共享~/.wave/cli/node_modules/@vscode/,并在依赖就位前不对外提供会话服务;主进程不参与下载、不弹 toast(等待期间由 webview 的加载态呈现)。 - 假设rg 已下载过(
~/.wave/cli/node_modules/@vscode/下存在当前平台的 rg 二进制),当再次启动应用,则直接复用缓存,不重复下载。 - 假设rg 下载失败(网络不可达等),当首次启动本地会话,则应用照常启动、不弹初始化错误 toast:本次运行 Grep 工具报"ripgrep is not available"(其余功能不受影响),下次启动自动重试下载——可选依赖不得把本地会话判死,应用也不再在启动 CLI 前代装。
用户故事:与插件端一致的交互能力(优先级:P1)
作为用户,我希望 desktop 中的确认(Confirmation)、AskUserQuestion、@文件 提及、图片粘贴、PDF 附件等交互与插件端完全一致,以便我获得相同的使用体验。
为什么是这个优先级:这些是 agent 交互的安全基础与核心场景,webview 已实现,desktop 必须保证宿主消息层正确接线。
独立测试:触发一个需要确认的工具调用并响应;输入 @ 选择文件;粘贴图片发送。验证行为与插件端一致。
验收场景:
- 假设 agent 发起工具确认或 AskUserQuestion 请求,当主进程收到请求,则必须渲染 webview 现有的确认/问答 UI,并将用户响应回传给 CLI。
- 假设用户在输入框键入
@,当用户选择文件,则应用必须通过主进程列出工作目录中的文件供选择(复用 webview 文件选择器)。 - 假设用户粘贴图片或附加 PDF,当消息发送,则媒体内容必须随消息发送给 agent(复用 webview 现有实现)。
用户故事:SSO 登录(优先级:P1)
作为用户,我希望在 desktop 中完成 SSO 登录/登出,以便我可以使用企业账号访问 Wave 服务。
为什么是这个优先级:登录是首次使用的前置条件。SSO 登录流程(本地 HTTP 回调服务器、授权码交换、token 持久化)全程在 wave --stdio 子进程内完成,webview 已有登录按钮与 LoginDialog,desktop 只需接线宿主消息层。
独立测试:在未登录状态下启动应用,点击登录按钮,完成浏览器 SSO 授权后验证应用显示已登录状态并可以继续会话;对远程主机(未登录)执行登录,验证浏览器授权回调经端口转发返回远端 CLI、登录成功;在本地(已登录)与远程主机(未登录)会话间切换聚焦分屏,验证账户卡片菜单(已登录为个人信息行热区纯功能菜单、未登录为「更多」按钮菜单)的登录/退出登录项标注与登录态跟随切换并刷新(未登录时卡片为整条登录按钮)。
验收场景:
- 假设用户未登录,当用户点击登录按钮,则主进程必须向 CLI 发送
login请求,收到authUrl通知后用系统默认浏览器打开授权页面。 - 假设用户在浏览器中完成授权,当 CLI 子进程完成授权码交换,则主进程必须收到
login响应并向 UI 回传loginResponse,UI 显示已登录状态。 - 假设应用启动时已有有效 token,当主进程初始化,则必须通过
getAuthStatus查询登录状态并通知 UI。 - 假设用户已登录,当用户触发登出,则主进程必须向 CLI 发送
logout请求并更新 UI 为未登录状态。 - 假设桌面端同时存在本地与远程主机会话,当用户查看账户卡片(已登录态为点击个人信息行热区弹出的纯功能菜单,见 desktop-account.md「账户卡片 · 个人信息行与纯功能菜单」场景 9),则菜单「设置 / 退出登录」项必须标注其作用的认证主体——当前聚焦分屏所属主机(远程主机显示为「设置(prod)」「退出登录(prod)」,本地主机不显示标注),菜单项的登录态必须是该主机当前的认证状态;已登录热区菜单不含登录项。未登录时账户卡片为整条「登录」按钮 + 「更多」按钮(更多菜单含登录项),点击登录按钮直接对当前聚焦分屏所属主机发起登录;VSCE/JetBrains 宿主无主机概念,菜单项保持无标注。
- 假设用户将聚焦分屏从一台主机切换到另一台主机或本地,当切换完成,则主进程必须重新查询新聚焦主机对应 CLI 的认证状态并推送,账户卡片(已登录热区纯功能菜单或未登录登录按钮/「更多」菜单)的登录/退出登录项(主机标注与登录态)必须刷新为新聚焦主机的最新状态,不得沿用前一主机的缓存值。
- 假设用户点击标注了主机名的登录/退出登录项,当点击生效,则
login/logout请求必须发给该主机(本地或远端)对应的 CLI 子进程;各主机的登录态相互独立(本地 token 与各远端主机 token 互不影响),登录/登出只作用于标注的主机。 - 假设用户对远程主机执行登录(点击标注主机名的登录项或未登录态账户卡片的登录按钮),当主进程收到该主机 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 无编辑器状态栏/状态对话框,需要替代通知通道。
独立测试:在会话中触发一个通知,验证它作为系统消息插入消息列表而非弹窗。
验收场景:
- 假设 CLI 有新版本,当应用检测到更新,则必须在消息流中插入一条系统消息,而不是弹出模态对话框。
- 假设有后台任务或 plan 状态变化的通知,当通知到达,则默认在消息流中显示,不打扰当前输入。
用户故事:主题设置(桌面端深色/浅色/跟随系统)(优先级:P1)
作为用户,我希望在设置页「全局设置」的「桌面端设置」区块(仅桌面端显示)中选择桌面应用的外观主题(跟随系统 / 浅色 / 深色),以便应用外观匹配个人偏好,而不受系统外观限制。
为什么是这个优先级:设置页(P1)已提供可编辑配置项,主题是桌面端独有的显示偏好——VSCE 跟随 VS Code 主题、JetBrains 跟随 IDE LaF,保持现状不暴露选项;webview 全程依赖 --vscode-* CSS 变量与 <html data-theme> 选主题,现有跟随系统链路(启动首帧防闪烁 + 运行中 desktopThemeChange 广播)天然承载「跟随系统」选项,本特性增量仅为「三态选择 UI + 持久化」,随设置页补全一并交付(2026-09-02 用户拍板新增)。
独立测试:桌面端在系统浅色与暗色外观下分别:选择「浅色」验证界面即时切浅色、重启后保持且首帧无闪烁;选择「深色」同理;选回「跟随系统」验证运行中切换系统外观自动跟随;运行中(含流式输出期间)切换主题验证即时生效、无需重启、不中断;VSCE/JetBrains 打开设置页验证不显示主题项。
验收场景:
- 假设用户打开桌面端设置页「全局设置」,当查看其下「桌面端设置」区块,则必须显示「主题」选择:跟随系统(默认)/ 浅色 / 深色三选项;VSCE/JetBrains 设置页不得显示该区块与主题项(保持跟随 IDE 主题的现状)。
- 假设用户选择「浅色」或「深色」,当选择生效,则应用必须立即切换为对应主题并持久化——无需重启、不重建 React 树、不刷新页面、不中断会话与流式(仅重赋
data-theme与 CSS 变量);重启后保持所选主题,首帧即呈现所选主题、无明暗闪烁(含系统外观为相反主题的场景)。 - 假设用户选择「跟随系统」,当应用启动且当前系统外观为暗色(或亮色),则首帧即呈现对应主题,不得出现明暗闪烁;当运行中用户切换操作系统外观(亮↔暗),则应用自动跟随切换,无需重启、不重建 React 树、不刷新页面、不中断会话与流式。
- 假设用户固定主题为「浅色」(或「深色」),当系统外观随后变化,则应用不得跟随系统,保持用户所选主题。
- 假设主题固定为「深色」/「浅色」之外的跟随系统且
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 一并验证,见下一节的退出序列要求)。
验收场景:
- 假设应用在任一平台运行,当用户查看应用菜单栏,则必须依次显示「文件」「编辑」「对话」「面板」「视图」「窗口」六个中文菜单(macOS 另有应用名菜单),不得出现英文「File」「Edit」「View」「Window」标签。
- 假设应用在 Windows/Linux 运行,当用户展开「文件」菜单,则必须包含「退出」项,且该退出项的加速器必须是
Ctrl+Q(菜单上显示、按下即退出,对齐 Claude Code Desktop);在 macOS 上「文件」菜单必须包含「关闭窗口」且不注册Cmd+W加速器(该加速器专属「关闭分屏」),也不注册Ctrl+Q(macOS 的Cmd+Q由应用名菜单的quitrole 承担)。 - 假设应用在任一平台运行,当用户展开「编辑」菜单,则必须包含撤销、重做、剪切、复制、粘贴、删除、全选(macOS 另含「粘贴并匹配样式」);「视图」菜单必须包含重新加载、开发者工具、缩放、全屏等项。
- 假设应用在任一平台运行且文本焦点在输入框,当用户按下
Ctrl+C/Ctrl+V/Ctrl+X/Ctrl+Z/Ctrl+A(macOS 为Cmd键),则剪切、复制、粘贴、撤销、全选行为必须正常。 - 假设菜单包含标准动作(撤销/剪切/复制/粘贴/重新加载/开发者工具/缩放等),当用户点击对应菜单项,则必须通过 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 实机验证无托盘图标、关闭行为不变。
验收场景:
- 假设应用在 Windows 运行且窗口可见,当用户点击窗口关闭按钮,则窗口必须隐藏到系统托盘(
hide(),不销毁窗口),应用进程与所有后台会话继续运行(流式输出不中断、工具执行不中断、终端 PTY 不被终止),任务栏中不再显示该窗口。 - 假设窗口已隐藏到托盘,当用户单击或双击托盘图标,或右键托盘菜单选「显示 CodeWave IDE」,则窗口必须恢复显示、若处于最小化则先还原,并聚焦到前台;应用状态与隐藏前完全一致(不刷新页面、不重建 React 树、不中断流式)。
- 假设应用处于托盘常驻状态,当用户右键托盘图标,则菜单必须至少包含「显示 CodeWave IDE」与「退出」两项;不得出现空菜单。
- 假设用户从托盘菜单「退出」、菜单栏「文件 → 退出」,或在窗口拥有焦点时按下
Ctrl+Q(Windows/Linux)发起退出,则必须走既有退出序列(before-quit→ dispose 主机 → 结束 CLI 子进程 → 正常 quit 序列),不得被「关闭即隐藏」拦成隐藏窗口——退出路径上窗口的close必须放行。窗口已隐藏到托盘时不要求Ctrl+Q可用(隐藏后应用不持有键盘焦点,这也正是必须保留托盘菜单「退出」的原因):不注册全局快捷键。 - 假设应用运行在 macOS 或 Linux,当用户关闭窗口,则行为必须保持现状(macOS 关窗不结束进程、再次激活重建窗口;Linux 关窗即退出),不得出现托盘图标,也不得因本特性改变既有菜单与退出序列。
- 假设托盘创建失败(图标资源缺失、系统不允许创建托盘等),当用户点击关闭按钮,则必须回退为原有行为(正常销毁窗口并按平台规则退出),不得出现「窗口已隐藏、托盘图标又不存在」的不可达状态——托盘必须先确认创建成功,关闭才允许只隐藏。
用户故事:退出时保护运行中的会话(优先级: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 个本地会话处于「运行中」,当用户发起退出(菜单「文件 → 退出」、
Ctrl+Q、托盘「退出」,或 Linux 上关闭窗口),则必须先弹出确认框并中止本次退出,不得直接进入退出序列。运行中口径为:正在流式生成、正在执行工具、或正等待用户确认(三者任一即算;侧栏行上的「完成且未查看」绿点不是判据)。 - 假设确认框已弹出,则标题必须给出运行中的会话数量(如「还有 2 个会话正在运行」),正文说明退出会中断正在生成的内容、执行中的工具与后台命令,按钮为「取消 / 仍然退出」;默认焦点在「取消」上,
Esc等价于点「取消」,不得让回车默认执行退出。 - 假设用户点「取消」或按
Esc,则退出必须被中止,窗口与全部会话保持原状(不暂停、不重启、不丢失已生成的内容),用户可以继续操作,之后可再次发起退出。 - 假设用户点「仍然退出」,则必须走既有退出序列(
before-quit→ dispose 主机 → 结束本地 CLI 子进程 → 正常 quit),行为与本次改动前完全一致,超时兜底不变。 - 假设没有任何本地会话在运行(含「应用刚启动、只有空白会话」),当用户发起退出,则不得弹框、直接退出——本特性不得给常规退出增加一次点击。
- 假设正在运行的会话全部在远端主机上,当用户发起退出,则不得弹框:远端会话跑在远端 daemon 上,退出不会中断它们(「真后台」语义)。本地与远端混跑时,只按本地会话计数。
- 假设窗口已隐藏到系统托盘(Windows)或已最小化,当用户从托盘菜单点「退出」,则必须先把窗口恢复并聚焦到前台,再弹出确认框——不得在窗口不可见的状态下弹出无处可点的对话框。
- 假设确认框弹出后会话状态发生变化(会话结束或又有新会话开始),则弹框内的数量以弹出时刻为准,不在框内实时刷新(避免文案跳动);无论数量如何变化,「取消」与「仍然退出」的语义不变。
- 假设用户在确认框打开期间通过关闭窗口(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 实机在消息区选中一个英文单词右键,验证首项为 查词 “<单词>”、点击弹出系统词典面板;在输入框选中文字右键,验证菜单含剪切/复制/粘贴/全选;在消息区选中文字右键并展开「服务」子菜单,验证系统服务项非灰;右键消息中的图片,验证仍是「复制图片」且可粘贴出去;在无选区、非图片的空白处右键,验证不弹菜单。
验收场景:
- 假设应用运行在 macOS,当用户在消息区(非编辑区)选中非空文字后右键,则必须弹出菜单,首项为
查词 “<选中文本>”(选中文本为空或右键点在链接上时不出现该项),点击该项必须调用webContents.showDefinitionForSelection()弹出系统词典面板(原生查词,不引入外部词典服务与网络请求);菜单还须含「复制」(有选区时可用)与「全选」。 - 假设应用运行在 macOS,当用户在输入框等可编辑区右键,则菜单必须在只读区项目之外增加剪切/复制/粘贴/全选,各项可用性跟随
params.editFlags(不可用时置灰而非隐藏),且这些项必须通过 Electronrole实现以保持原生命令语义。 - 假设应用运行在 macOS,当用户展开右键菜单中的「服务」子菜单,则必须通过
role: "services"提供系统服务项(含「在字典中查询」等),且menu.popup()必须传frame: params.frame(该 webContents 的WebFrameMain)——Electron 官方明确 macOS 的 Writing Tools/AutoFill/Services 在右键菜单中默认禁用,只有传 frame 才启用(不传则子菜单全灰)。 - 假设用户右键点击图片(消息内图片、文件面板内联预览等),当菜单弹出,则必须保持现有「复制图片」行为(
webContents.copyImageAt(params.x, params.y))不变;图片项与文字项互斥,图片右键不得同时出现查词/复制/全选。 - 假设用户在无选区且非图片的位置右键(空白、消息容器、非图片面板),当右键发生,则不得弹出菜单(保持现状),不得出现空菜单或仅有分隔线的菜单。
- 假设应用运行在 Windows/Linux,当用户选中文字后右键,则菜单必须提供复制/全选(可编辑区另含剪切/粘贴),不得出现 macOS 专属的「查词」与系统「服务」项(
showDefinitionForSelection与 Services 均仅 darwin 有效)。 - 假设应用发生任一次右键,当菜单构造与弹出,则开销必须只发生在该次右键(菜单模板按次构造,不注册常驻监听或轮询);不得为「拼写建议」开启
spellcheck(不消费params.dictionarySuggestions)——系统拼写检查会给整页带来持续开销,而查词与复制不需要它。
用户故事:macOS 隐藏标题栏(优先级:P1)
作为 macOS 用户,我希望窗口顶部不显示系统标题栏整条(无「CodeWave IDE」标题文字),内容区直达窗口顶部、左上角系统红绿灯保留并浮于内容之上,以便获得与 VS Code/Slack 一致的沉浸式窗口体验——隐藏的是标题栏本身,红绿灯仍是系统原生的(不做自绘窗口控制按钮)。
为什么是这个优先级:窗口 chrome 是桌面端打开应用的第一眼体验,隐藏标题栏方案已由用户拍板;仅 macOS 生效,Windows/Linux 视觉与行为不受影响。
独立测试:macOS 实机启动应用,验证窗口顶部无标题栏文字/栏体、侧边栏首行(44px 窗口行)左端显示系统红绿灯、红绿灯与右侧同排「收起侧边栏」按钮垂直同心(同一水平中心线,不显按钮偏下/红绿灯贴顶)且按钮可点击(点击后侧边栏收起,行视觉同色一体无条带);按住窗口行空白处拖动窗口、双击按系统惯例缩放;对话区只有一个分屏时按住对话顶栏空白处(标题与右侧按钮之外)同样能拖动窗口、双击缩放,且标题点击与各按钮点击行为不变、悬停标题不再出现文本光标(竖杠)也不出现抓取手型,再开第二个分屏后同一区域拖动变成分屏换位(光标恢复抓取手型)而非移动窗口;收起侧边栏后验证对话顶栏左端让出红绿灯区域(约 76px 空白拖拽段),展开侧边栏/新对话控件整体右移不与其重叠(点开「活动」会话状态看板后同样验证其顶栏让位,「展开侧边栏」按钮不被红绿灯压住);进入设置页(占满整个 view、会话侧边栏被覆盖)后验证红绿灯改由设置页左导航顶部让位承接(见「macOS 隐藏标题栏」设置页场景)且返回后让位交还侧边栏;进入全屏后验证红绿灯消失的同时窗口行让位自动收起(「收起侧边栏」按钮左移至行首)与收起态顶栏让位段收起,退出全屏后两者还原;Windows/Linux 实机验证仍为系统原生标题栏(含标题文字),无窗口行与让位,「收起侧边栏」按钮保留在品牌 logo 行右侧。
验收场景:
- 假设应用运行在 macOS,当查看窗口顶部,则系统标题栏必须隐藏(无标题文字与栏体),内容区延伸至窗口顶部,左上角保留系统红绿灯浮于内容之上。
- 假设应用运行在 macOS 且侧边栏展开,当查看侧边栏顶部,则侧边栏首行必须承载系统红绿灯(行内不绘制假圆点,避免与系统重复),红绿灯右侧同排显示「收起侧边栏」按钮;整行除按钮外均为窗口拖拽区(按住空白处拖动可移动窗口、双击按 macOS 惯例缩放),按钮本身不参与拖拽、点击即收起侧边栏(收起后窗口行随侧栏一并消失);品牌 logo 行位于窗口行之下,logo 行右侧图标区保留「活动」等入口、不被窗口行挤占。
- 假设应用运行在 macOS 且侧边栏已收起,当查看占据窗口左端的视图顶栏(对话顶栏,或替换会话区的「活动」会话状态看板顶栏),则该顶栏最左端必须让出约 76px 的红绿灯区域(该段空白且同为窗口拖拽区),其「展开侧边栏」等控件整体右移,不被红绿灯遮挡;看板与对话顶栏共用同一段让位机制(同一收起条件与同一 76px 拖拽段),不得各写一套偏移。
- 假设应用运行在 Windows/Linux,当查看窗口顶部,则必须保持系统原生标题栏(标题文字与窗口控制按钮照旧),不得渲染 macOS 窗口行/让位——「收起侧边栏」按钮仍位于品牌行右侧图标组,既有布局与交互完全不变。
- 假设应用运行在 macOS 且窗口行已渲染,当用户操作聊天顶栏/侧边栏内其余控件(包括窗口行内的「收起侧边栏」按钮),则交互不受影响——窗口拖拽区仅限窗口行除按钮外的空白区域、收起态让位段,以及单分屏时的对话顶栏空白处(见场景 9);按钮已解除拖拽(
no-drag),点击即收起侧边栏,不误触窗口拖动。 - 假设原型预览等浏览器环境(无系统窗口 chrome)渲染桌面侧边栏,当查看其顶部,则继续按现状绘制假红绿灯圆点行,且红绿灯右侧同排渲染「收起侧边栏」按钮(与真机 macOS 形态一致),两者相互独立、互不影响。
- 假设应用运行在 macOS 且窗口进入全屏(系统红绿灯随全屏隐藏),当查看侧边栏顶部窗口行,则窗口行的红绿灯让位自动收起——「收起侧边栏」按钮左移至窗口行起点(不再预留红绿灯让位),左侧不再出现无红绿灯的空段;退出全屏后红绿灯恢复,让位与按钮位置随之还原(回到与红绿灯同排)。侧边栏收起时对话顶栏左端的红绿灯让位段(约 76px)行为一致:全屏下让位收起、控件贴左,退出全屏恢复让位。
- 假设应用运行在 macOS 且设置页已打开(占满整个 view、会话侧边栏被覆盖,见 desktop-settings「设置页面」场景 1/12),当查看设置页窗口左上角,则系统红绿灯必须由设置页自身左导航顶部承接:红绿灯浮于设置页导航左上角,导航首行预留红绿灯让位(设置页自身内容不被红绿灯遮挡);该让位区为窗口拖拽区,按住可拖动窗口,区内不放置任何控件;「返回应用」按钮位于该让位区下方单独一行(不与红绿灯同排,让位区无按钮、无拖拽/点击冲突),点击即关闭设置页回到会话视图,红绿灯让位随之交还侧边栏;设置页打开期间窗口进入全屏(红绿灯隐藏),则设置页顶部让位区整行收起(内容上移贴顶),退出全屏恢复让位(行为与侧边栏窗口行一致)。
- 假设应用运行在 macOS 且对话区只有一个分屏(侧边栏展开或收起均可),当按住对话顶栏的空白处(会话标题与右侧按钮之外的区域)拖动,则必须移动窗口(双击按 macOS 惯例缩放)——该分屏的顶栏空白即窗口拖拽区;当用户点到会话标题或顶栏内任一按钮,则不触发窗口拖动(它们是
no-drag,标题保持纯展示、按钮照旧点击,见 session-management.md「会话重命名入口」的平台边界),且悬停光标必须与「纯展示 + 可拖窗」相符——标题与顶栏空白处均为系统箭头,不得出现文本编辑光标(I-beam 竖杠);该区域的光标由系统接管(-webkit-app-region: drag区不接收指针事件,CSScursor实测无效,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 翻译实现,不是系统能力),故右键菜单只保证系统词典查词与系统「服务」子菜单;不实现自研翻译,也不为此引入原生模块或翻译服务。