Skip to content

功能规格说明:桌面端壳层与分发 ​

拆分自 desktop-app.md(CodeWave IDE 桌面端总纲),本文档仅承载该主题的用户故事与验收场景;跨主题的边界情况与非目标按主题分发至各文件。

用户场景与测试 ​

用户故事:无 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「IDE 插件更多菜单与欢迎页」场景 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 前代装。

用户故事:与 IDE 插件一致的交互能力(优先级:P1) ​

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

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

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

验收场景:

  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-and-settings.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 请求结束后必须终止该转发进程并释放本地端口,不得残留孤儿进程。

用户故事:内置 CLI 一致保障(本地与远程均按内容字节判定是否同步)(优先级:P2) ​

作为用户,我希望本地会话始终使用随应用发布的内置 CLI,且连接远程主机时自动检测并经 ssh 推送本机内置的同一份 CLI 到远端,使远端运行的代码与本机内置的 dist/bundle/wave.mjs 字节一致,以便我始终使用受支持的后端——同步判据是内容(sha256 字节)而非版本号:不依赖远端 npm 上 wave-code 包的发布进度(GUI 可单独发版而不 bump CLI 的 npm 包号,npm 最新版可能落后于 GUI 内置 CLI,旧机制下会因 npm install wave-code@<GUI版本> 404 而失效),且只比较版本号会漏掉「版本号未变但代码已更新」的副本(本地 dev 重装、GUI 单独发版都可能带着新字节而版本号未 bump)。

为什么是这个优先级:本地 CLI 随应用安装包发布,升级应用即升级 CLI,无需运行期升级;远程主机上的 CLI 由远端 daemon 长期托管且无内置运行时,仍需在连接时保障其与 GUI 内置 CLI 保持同一份代码——内置 CLI 是单文件纯 JS bundle(dist/bundle/wave.mjs,esbuild 打成,跨平台),可经本机已建立的 ssh 通道直接推送,远端只需系统 Node.js >= 22 即可运行(无需 npm 装包、远端无出网也能完成 CLI 推送);若远端副本与内置不一致(哪怕版本号相同)而 daemon 仍运行旧代码,修复与新增功能将永远不生效(例如远程会话运行状态修复),必须同步并重启 daemon。

独立测试:在一台未安装 Node.js 的机器上安装应用,验证本地会话直接用内置 CLI 工作;连接全新远程主机或装有与内置不一致 CLI(含「版本号相同但字节不同」的副本)的主机,验证应用经 ssh 推送内置 CLI、远端 daemon 以推送的 CLI 重启;GUI 升级但内置 CLI 字节未变时再连接,验证不推送、daemon 不重启;GUI 单独发版(wave-code 版本号未 bump)但内置字节已变时连接,验证仍推送并重启 daemon。

验收场景:

  1. 假设应用已安装,当应用启动,则本地会话直接使用随应用发布的内置 CLI(resources/wave-cli/,其 package.json 中的 wave-code 版本与 GUI 版本相互独立,可不同号),不执行任何 npm 安装/升级。
  2. 假设应用升级到新版本,当新版本应用启动,则本地会话使用新版内置 CLI——运行时 ~/.wave/cli/desktop/ 中 dist/bundle/wave.mjs 与内置 resources/wave-cli/ 的同名文件字节(sha256)不一致时重新复制(仅替换 CLI 文件:dist/、入口、package.json),字节一致(同版本重装、或 GUI 单独发版未改 CLI)则不复制;旧 CLI 无残留运行,已缓存的运行时依赖(rg/sharp,node_modules/)保留、不重复下载。
  3. 假设用户连接远程主机,且远端固定目录 ~/.wave/cli/desktop/ 中 dist/bundle/wave.mjs 与本机内置同名文件的字节(sha256)一致,当连接建立,则直接复用该 CLI 与(可能)正在运行的 daemon,不推送、不重启;当远端 CLI 缺失、损坏(内容探针无法读取或算出一致的哈希)或字节与内置不一致(即使 package.json 的版本号相同),则应用必须把本机内置 CLI(bin/wave-code.js + dist/bundle/wave.mjs + package.json)经 ssh 写入远端临时目录后原子替换到 ~/.wave/cli/desktop/,再连接/创建该主机的 daemon。
  4. 假设远端 CLI 升级(推送)成功且该主机旧 daemon 仍在运行(旧 daemon 运行的是升级前的代码),当升级完成,则应用必须重启该主机的 daemon(终止旧 daemon 并启动新 daemon);重启后历史会话仍可从远端转录恢复,断线前正在进行的任务终止(与 daemon 退出的既有语义一致,不得出现幽灵「运行中」状态)。
  5. 假设远端 CLI 推送失败(ssh 中断、磁盘错误等),当应用检测到失败,则必须向用户显示可操作的错误信息与重试提示(下次连接自动重试,不得无限重试);替换采用「临时目录写入 + 原子 mv」且失败不动旧目录,故运行中的旧 daemon 不受影响、可继续使用。
  6. 假设远端主机完全未安装 wave CLI(首次连接),当应用连接该主机,则必须推送与 GUI 内置 CLI 字节一致的 bundle(推送源与内容=内置 resources/wave-cli/,而非远端 npm registry 的 wave-code 包)到 ~/.wave/cli/desktop/;当远端无 Node.js(或主版本低于 22),则必须显示安装/升级远端 Node.js 的引导信息,不得进入不可用状态。
  7. 假设应用升级到新版本但内置 CLI 的 wave-code 版本未变(GUI 单独发版,不 bump CLI),当用户连接远程主机,则以字节为准:内置 wave.mjs 与远端副本字节一致时不推送、不重启 daemon(无增量传输);字节已变(构建了新的 CLI 代码但未 bump 版本号)时必须推送并重启 daemon——版本号相同不再视为「已满足」。
  8. 假设远端 CLI 已就位但其 grep 依赖 rg(@vscode/ripgrep JS 包装 + 平台二进制)缺失(如首次推送后),当远端以该 CLI 启动 daemon,则由远端 CLI 自己在启动期从 npmmirror 下载 rg 到共享 ~/.wave/cli/node_modules/@vscode/(只需远端 Node ≥ 22 与出网,不再要求远端安装 npm);下载失败不阻断 daemon 启动(grep 暂不可用、下次启动重试),CLI 推送本身不受影响(走 ssh、不经 registry)。

用户故事:无干扰通知(优先级:P3) ​

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

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

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

验收场景:

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

用户故事:跨平台分发(优先级:P2) ​

作为团队,我希望 desktop 应用能打包为 macOS(arm64)和 Windows 安装包,以便覆盖主要用户平台。

为什么是这个优先级:分发能力决定功能是否能到达用户。

独立测试:在 CI 或本地分别打出 macOS arm64 与 Windows 包,安装后验证启动与会话功能。

验收场景:

  1. 假设配置了 electron-builder,当执行打包命令,则必须生成 macOS arm64 与 Windows 安装产物。
  2. 假设应用依赖 native node 模块,当在 macOS 上打包 Windows 产物失败,则必须先通过 CI matrix 调研解决,再实现打包流程。

用户故事:桌面端自动更新(优先级:P1) ​

作为用户,我希望桌面应用检测到新版本后按我的节奏完成下载与重启安装,以便我无需手动访问下载页面即可获得修复与新功能,也不会在操作中途被强制重启打断。

为什么是这个优先级:应用更新是修复与功能到达用户的唯一通道;现状仅「提示 + 给下载 URL」,用户不主动访问下载页就拿不到修复。macOS 签名 + 公证已就绪(ed2d9bf7 + 6f00c44a),自动更新链路可以端到端闭环。企业版用户登录后 serverUrl 已知,更新源(codechat 的 file-center 更新元数据)运行时注入,构建期不写死。交互承载已裁决(2026-09-03):删除 toast 自动下载/重启提醒链路,由账户卡片「更新按钮状态机 S0–S6」(见 desktop-account-and-settings.md「账户卡片 · 更新按钮状态机 S0–S6」故事)全权接管——检测发现新版本只把状态推为 idle(卡片出现「更新」按钮),下载须经用户在 S2 确认框确认,下载完成自动弹 S4 重启确认。本故事仅约束更新源 / 分发 / 降级边界,按钮文案、对话框步骤与状态流转语义不在此重复。

独立测试:登录后触发检查,验证「发现更新(S1)→ S2 确认下载(S3 下载中禁用)→ 就绪自动弹重启确认(S4)→ 立即重启(S6 复位 + quitAndInstall)」全流程(按钮语义按 desktop-account-and-settings.md「账户卡片 · 更新按钮状态机 S0–S6」故事断言);无 serverUrl 时验证不触发任何更新检查(启动自动检查静默、手动检查提示登录);下载失败时验证按钮恢复「更新」可重试(不弹手动下载页 toast);更新端点故障时验证手动检查给「检查更新失败」提示、自动检查(启动与轮询)静默;应用持续运行跨过一个轮询间隔时验证按间隔再次自动触发检查(自动、静默),且不自动下载。

验收场景:

  1. 假设用户已登录企业版(存在 serverUrl),当应用触发更新检查,则必须通过 electron-updater 的 generic provider 查询更新元数据——feed 目录按当前 updateChannel 运行时注入:updateChannel=stable(默认)查 serverUrl/api/downloads/desktop/{mac|win}/,updateChannel=beta(「接收 Beta 版更新」开启,见下文故事)查 serverUrl/api/downloads/desktop-beta/{mac|win}/(均按当前平台区分 mac/win);不得再查询 GitHub Releases。
  2. 假设用户未登录(无 serverUrl),当应用触发更新检查,则 updateChannel 不生效,且不得执行任何更新检查:不得启动 electron-updater、不得查询 GitHub Releases(2026-09-09 拍板:未登录不查更新——曾有的 GitHub 回退手动提示链路与 checkForUpdate/updateChecker 代码一并删除),启动自动检查保持静默、手动检查提示「登录后可检查更新」;本地会话与其它功能不受影响。应用内 toast 基建保留(位置/形态与配色路由见 desktop-account-and-settings.md「设置页反馈语义」——应用级提示为顶部居中新形态),仅用于信息提示(登录引导、下载完成等),不再承载任何更新发现/下载/重启提醒。
  3. 假设已登录企业版且检查发现新版本,当收到 update-available 事件,则不得自动开始后台下载,必须把更新状态置为 idle 并随 desktopAccountInfo 推送——账户卡片个人信息行右侧出现「更新」按钮(S1);是否下载由用户在 S2 确认框决定(2026-09-03 裁决:账户卡片不提供更新 toast,toast 自动下载链路已删除)。
  4. 假设用户在 S2 确认下载,当 webview 下发 desktopUpdateDownload 命令,则宿主必须先把更新状态置为 downloading 并推送(卡片按钮转「正在下载更新…」并禁用,防重复下载,S3),再调用 electron-updater 开始后台下载;当下载完成收到 update-downloaded 事件,则必须把状态置为 ready 并推送(S4)——webview 据此自动弹出「重启应用」确认框(仅一次),选「稍后」后按钮转常驻「重启」(S5)。
  5. 假设更新服务故障(端点不可达 / 元数据解析失败 / 下载失败 / downloadUpdate 抛错),当下载失败且状态为 downloading,则必须把状态回退为 idle 并推送——卡片按钮恢复「更新」,用户可重新走 S2 重试(见 desktop-account-and-settings.md「账户卡片 · 更新按钮状态机 S0–S6」场景 8),不得静默失败也不得弹手动下载页 toast;当失败发生在已就绪(ready)之后的安装阶段,则不得把状态降级(重启时机由用户决定,避免把已就绪更新重置回可下载造成重复下载),仅记录日志;当失败发生在尚未发现更新的检查阶段,则自动检查(启动与轮询)静默、手动检查给出「检查更新失败,请稍后重试」提示。
  6. 假设用户触发手动检查(checkForUpdates 命令),当检查执行,则必须复用本故事场景 1-5 链路:已登录走 electron-updater(按当前 updateChannel 对应 feed 查询;无更新时 toast 提示按 updateChannel 区分,stable 见「接收 Beta 版更新」场景 5、beta 提示「当前已是最新版本」);未登录按场景 2 不执行检查,toast 提示「登录后可检查更新」。
  7. 假设已登录企业版且 electron-updater 返回更新信息,当更新下载执行,则必须使用更新元数据中的真实文件入口下载安装包,不得指向 manifest.json / feed 目录自身。
  8. 假设 macOS 上应用运行于不可写位置(如非 /Applications),当 S6 重启安装无法原地完成,则安装失败经 electron-updater error 事件上报,宿主按场景 5 处理(ready 态错误不降级、记录日志),用户仍可稍后再次执行重启安装或经手动渠道获取安装包,不得自动弹窗打断当前工作。
  9. 假设构建安装包(mac/win),当electron-builder 打包完成,则产物 resources/app-update.yml 必须存在(afterPack 写入 updaterCacheDirName)——electron-updater 下载前会读取该文件(configOnDisk),缺失时下载阶段抛 ENOENT 并降级为下载页提示(修复:构建未声明 publish 配置时 electron-builder 不生成该文件)。
  10. 假设用户在 S4 确认框选「立即重启」或点击 S5「重启」按钮,当 webview 下发 desktopUpdateRestart 命令,则宿主必须立即复位更新状态并推送(按钮消失,S6)后调用 quitAndInstall 退出并安装新版本(Windows 静默安装 + --force-run 装完自动重启,不再弹二次确认对话框)。重启安装前应用即将退出,无需任何 toast/加载态反馈——按钮消失即反馈(toast 加载链路已随 2026-09-03 裁决删除)。
  11. 假设应用已登录企业版且持续运行(不退出、不重启),当运行期跨过固定的检查间隔(首版 1 小时),则宿主必须再次自动触发更新检查(非手动,与启动自动检查同一路径:按当前 updateChannel 查对应 feed,见场景 1;未登录按场景 2 不执行检查,静默);该轮询只负责「查」,仍必须遵守场景 3 的约束不得自动开始后台下载、不得自动安装——发现新版本只把更新状态置 idle 并随 desktopAccountInfo 推送(账户卡片出现「更新」按钮,S1),下载与重启由用户走 S2–S6;轮询检查失败按场景 5 静默处理(等同启动自动检查),不影响后续轮询;应用退出(dispose)后必须停止轮询,不得留下悬挂定时器。轮询发现新版本后的表现与手动检查完全一致。

用户故事:接收 Beta 版更新(设置项)(优先级:P1) ​

作为已登录企业版的桌面端用户,我希望在设置页「全局设置」的「桌面端设置」区块(2026-09-08 拍板:与「主题」一道从「基础设置」拆出独立成区)中开启「接收 Beta 版更新」,以便提前试用通过 codechat 下载管理测试通道分发的版本。

为什么是这个优先级:版本模型 S1(每构建 Z+1、三端同号,beta/正式仅靠分发通道区分)下,桌面 beta 与正式同源——codechat 下载管理加 channel(vusion/codechat #33),客户端差异仅为 feed 目录(desktop-beta/ vs desktop/)。开关是桌面端接触测试版本的唯一入口,默认关闭(stable),正式用户不受影响;未登录用户无 serverUrl、本就无 codechat feed,开关置灰与 beta 无交集。

独立测试:登录后开启「接收 Beta 版更新」→ 手动检查命中 desktop-beta feed 的更高版本并进入账户卡片 S0–S6 全流程;关闭 → 切回 stable feed,正式未追平已装测试版号时手动检查提示等待、不降级;未登录时开关置灰不可切换、不触发任何更新检查。

验收场景:

  1. 假设桌面端用户打开设置页「全局设置」,当查看其下「桌面端设置」区块,则必须显示「接收 Beta 版更新」开关(默认关闭 = updateChannel=stable);VSCE/JetBrains 设置页不得显示该区块与该行(仅桌面端渲染)。
  2. 假设用户未登录企业版(无 serverUrl),当查看「接收 Beta 版更新」开关,则开关必须置灰不可切换并说明原因(如「登录后可接收测试版更新」);更新检查按「桌面端自动更新」场景 2 不触发(未登录不查更新),与 beta feed 无交集。
  3. 假设已登录且开关关闭(updateChannel=stable),当用户开启「接收 Beta 版更新」,则宿主必须把 updateChannel 置为 beta 并持久化(重启后保持),并立即按 serverUrl/api/downloads/desktop-beta/{mac|win}/ 重查一次——发现更高版本则进入账户卡片更新按钮状态机 S0–S6(见 desktop-account-and-settings.md「账户卡片 · 更新按钮状态机 S0–S6」),无更高版本则保持现状(账户卡片不出现「更新」按钮)。
  4. 假设已登录且开关开启(updateChannel=beta),当应用触发更新检查(启动自动/手动),则一律查询 beta feed 而非 stable feed;当beta feed 不可达或元数据解析失败,则按自动更新故事场景 5 错误处理(启动自动检查静默、手动检查提示「检查更新失败,请稍后重试」),不得静默回退 stable feed、不得崩溃。
  5. 假设已登录且开关关闭(updateChannel=stable),当用户手动检查更新且已装版本高于 stable feed 最新版本(曾接收测试版、正式尚未追平),则不得降级、不得改查 beta feed,必须提示「正式版发布后将自动更新」而非「当前已是最新版本」;当 stable feed 最新版本高于已装版本,则按自动更新故事场景 1-6 正常进入 S0–S6 升级;启动自动检查在无更新时保持静默。

用户故事:主题设置(桌面端深色/浅色/跟随系统)(优先级: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 点「文件 → 退出」验证退出应用。

验收场景:

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

用户故事:文本与图片右键菜单(优先级: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,当查看窗口顶部,则系统标题栏必须隐藏(无标题文字与栏体),内容区延伸至窗口顶部,左上角保留系统红绿灯浮于内容之上(BrowserWindow titleBarStyle: "hidden",仅 darwin 生效)。
  2. 假设应用运行在 macOS 且侧边栏展开,当查看侧边栏顶部,则侧边栏首行必须显示 44px 窗口行(与聊天顶栏同高),视觉上与侧栏同色一体——无独立条带、无下边界线,即侧边栏自身的第一行内容:左侧为系统红绿灯落位(行内不绘制假圆点,避免重复;红绿灯中心落在窗口行垂直中心线上,与行内按钮同一条水平线,不显按钮偏下),红绿灯右侧同排左对齐显示「收起侧边栏」按钮(按钮左缘越过红绿灯区后紧邻绿点,可见间距约 16px);整行除按钮外均为窗口拖拽区(-webkit-app-region: drag),按住空白处拖动可移动窗口、双击按 macOS 惯例缩放,按钮本身 -webkit-app-region: no-drag、可点击收起侧边栏(收起后窗口行随侧栏一并消失);窗口行下方隔留白才是品牌 logo 行,logo 行右侧图标区保留「活动」等入口、不被窗口行挤占。
  3. 假设应用运行在 macOS 且侧边栏已收起,当查看占据窗口左端的视图顶栏(对话顶栏,或替换会话区的「活动」会话状态看板顶栏),则该顶栏最左端必须让出约 76px 的红绿灯区域(该段空白且同为窗口拖拽区),其「展开侧边栏」等控件整体右移,不被红绿灯遮挡;看板与对话顶栏共用同一段让位机制(同一收起条件与同一 76px 拖拽段),不得各写一套偏移。
  4. 假设应用运行在 Windows/Linux,当查看窗口顶部,则必须保持系统原生标题栏(标题文字与窗口控制按钮照旧),不得渲染 macOS 窗口行/让位——「收起侧边栏」按钮仍位于品牌行右侧图标组,既有布局与交互完全不变。
  5. 假设应用运行在 macOS 且窗口行已渲染,当用户操作聊天顶栏/侧边栏内其余控件(包括窗口行内的「收起侧边栏」按钮),则交互不受影响——窗口拖拽区仅限窗口行除按钮外的空白区域与收起态让位段;按钮已解除拖拽(no-drag),点击即收起侧边栏,不误触窗口拖动。
  6. 假设原型预览等浏览器环境(无系统窗口 chrome)渲染桌面侧边栏,当查看其顶部,则继续按现状绘制假红绿灯圆点行,且红绿灯右侧同排渲染「收起侧边栏」按钮(与真机 macOS 形态一致),两者相互独立、互不影响。
  7. 假设应用运行在 macOS 且窗口进入全屏(系统红绿灯随全屏隐藏),当查看侧边栏顶部窗口行,则窗口行的红绿灯让位自动收起——「收起侧边栏」按钮左移至窗口行起点(不再预留红绿灯让位),左侧不再出现无红绿灯的空段;退出全屏后红绿灯恢复,让位与按钮位置随之还原(回到与红绿灯同排)。侧边栏收起时对话顶栏左端的红绿灯让位段(约 76px)行为一致:全屏下让位收起、控件贴左,退出全屏恢复让位。
  8. 假设应用运行在 macOS 且设置页已打开(占满整个 view、会话侧边栏被覆盖,见 desktop-account-and-settings「设置页面」场景 1/12),当查看设置页窗口左上角,则系统红绿灯必须由设置页自身左导航顶部承接:红绿灯浮于设置页导航左上角(窗口最左 20px、中心与导航顶行同一水平中心线),导航首行需预留红绿灯让位(约与侧边栏窗口行同高同宽的让位区,设置页自身内容不被红绿灯遮挡);该让位区为窗口拖拽区,按住可拖动窗口,区内不放置任何控件;「返回应用」按钮位于该让位区下方单独一行(不与红绿灯同排,红绿灯让位区无按钮、无拖拽/点击冲突),按钮左缘与导航项对齐,点击即关闭设置页回到会话视图,红绿灯让位随之交还侧边栏;设置页顶部无独立条带/下边界,让位区与导航背景同色一体;设置页打开期间窗口进入全屏(红绿灯隐藏),则设置页顶部让位区整行收起(高度归零、内容上移贴顶),退出全屏恢复让位(行为与侧边栏窗口行一致)。

边界情况 ​

  • 内置 CLI 缺失或损坏:本地会话的内置 CLI 文件随安装包发布,若缺失或不可执行(安装损坏),应用必须显示重新安装应用的引导信息,不得尝试从网络安装。
  • 内置 CLI 复制到用户目录:安装目录只读,内置 CLI 在首次启动或内置与运行时副本的 dist/bundle/wave.mjs 字节(sha256)不一致时复制到 ~/.wave/cli/<end>/(入口 bin/wave-code.js);复制只替换 CLI 文件(dist/、入口、package.json),保留 node_modules/(运行时依赖缓存,rg 与 sharp)。
  • 运行时依赖自装与缓存(rg 与 sharp 同一套机制):宿主自带的那份 CLI 不带 node_modules,依赖由 CLI 在启动期按「运行时依赖表」自装到共享 ~/.wave/cli/node_modules/(rg 在 @vscode/ 下),落点与旧实现相同 ⇒ 已有缓存直接命中、不重复下载;npm 安装的 wave-code 自带这些依赖、第一步即短路。版本选择、sha512 校验、原子落地、保留可执行位等细节见 stdio-transport.md「边界情况 · 运行时依赖自装与缓存」。
  • 运行时依赖下载失败只降级、不阻断启动:CLI 在启动期等依赖就位(避免「刚启动就贴图/搜索」撞上还没装好),但安装失败不抛错、不弹 toast——记一行警告后照常提供会话服务(Grep 报"ripgrep is not available"、超限图片按降级路径处理);同一进程内不重试,下次启动再试。应用不再在启动 CLI 前代装或拦截(旧行为:rg 下载失败即 toast 提示初始化失败)。
  • 本地不依赖系统 Node.js:本地会话由 Electron 内置 Node 运行内置 CLI,客户系统未安装 Node.js/npm 或版本低于 22 均不影响本地会话;SSH 远程主机会话仍依赖远端 Node.js >= 22(远端以系统 Node 运行推送的 CLI)。
  • 远端 CLI 布局镜像本地:远端 CLI 固定位于 ~/.wave/cli/desktop/(bin/wave-code.js + dist/bundle/wave.mjs + package.json),与本地 per-end 目录同名;远端运行时依赖(rg/sharp)位于共享 ~/.wave/cli/node_modules/(升级替换 desktop/ 目录时保留,不重复下载)。
  • 远端同步判据取内置 CLI 内容而非版本号:GUI 与内置 CLI 版本相互独立(publish.yml 明示 GUI 可单独发版不发布 CLI npm 包),同步判据是内置 resources/wave-cli/dist/bundle/wave.mjs 与远端副本的字节(sha256)比较,绝不是 app.getVersion(),也不以 package.json 的 wave-code 版本号为准(版本号未 bump 但字节已变也必须同步)——旧机制曾对 npm 上不存在的 GUI 版本号发起安装而 404。
  • 远端运行时依赖由远端 CLI 自取:远端 CLI 不带 node_modules,rg/sharp 由远端 CLI 在启动期自己从 npmmirror 下载到共享 ~/.wave/cli/node_modules/——只需远端有 Node ≥ 22 与出网,不再要求远端安装 npm(旧机制在远端执行 npm install --prefix ~/.wave/cli);server 无出网时 CLI 推送不受影响(走 ssh),daemon 照常启动、仅 grep 与超限图片降级,恢复网络后下次启动自动补齐。
  • 退出应用时会话仍在运行:退出应用必须终止 CLI 子进程,避免孤儿进程。
  • auth/token 过期:401 等鉴权失败的表现与当前 webview/stdio 行为保持一致(由 CLI 侧现有错误处理透出),本特性不做额外处理。
  • 系统外观在流式输出期间变化:系统外观在 agent 流式输出期间切换,应用必须立即跟随、不中断流式、不重建会话状态(渲染进程仅重赋 CSS 变量)。
  • nativeTheme 不可用:主进程读取 nativeTheme.shouldUseDarkColors 失败时必须回退为暗色主题,不得崩溃。
  • 远程会话的 SSO/登录态:由远端 CLI 自身管理,与 desktop 本地登录态相互独立;authUrl 通知到达时,桌面端必须先将回调端口经 SSH 转发到本机回环、重写 callback_url 后路由到系统浏览器(远端 CLI 的回调服务器监听在远端 127.0.0.1,本机浏览器直接访问不到),登录结束后终止转发。
  • desktop-beta feed 依赖 codechat 通道:beta feed(/api/downloads/desktop-beta/{mac|win}/)由 vusion/codechat #33 提供;codechat 侧通道上线前,开启「接收 Beta 版更新」后的检查按自动更新故事场景 5 错误处理(启动静默、手动提示检查失败),不得静默回退 stable feed 或崩溃。
  • updateChannel 不进共享配置:「接收 Beta 版更新」开关持久化于桌面 configStore 顶层(userData/wave-desktop.json,theme 先例),不写入 settings.json、不同步到远程 CLI 或账号级配置;默认 stable。
  • 未登录不触发更新检查:无 serverUrl 时,启动自动检查与手动 checkForUpdates 均不得执行更新检查(不查 GitHub Releases、不启动 electron-updater),手动检查 toast 提示「登录后可检查更新」;曾用于未登录回退的 checkForUpdate/updateChecker 链路已删除(2026-09-09 拍板)。
  • 运行期轮询只查不下载:轮询与启动自动检查一样是「非手动」检查,发现新版本只置 idle(S1),绝不自动下载/安装(autoDownload=false 保持);间隔为固定常量(首版 1 小时),与启动那次的「只跑一次」检查相互独立(轮询不得让它变成两次启动语义);定时器生命周期随宿主 dispose 结束。

非目标(明确排除) ​

  • 多窗口:单窗口架构,会话在当前窗口内切换。
  • webview 内部逻辑修改:不改写 ChatApp/useChat 等核心交互逻辑,仅做布局扩展与组件抽取;预览特性在共享 webview 侧仅新增链接点击委托,按宿主类型分流,IDE 宿主行为不变。
  • Linux 打包:首版仅 macOS(arm64)与 Windows。
  • 系统「翻译」文本服务:macOS 原生右键里那条顶级 Translate “<text>” 由 AppKit 为 NSTextView 注入,Chromium/Electron 没有对应 API(Chrome 的「翻译选中内容」是 Chrome 自接 Google 翻译实现,不是系统能力),故右键菜单只保证系统词典查词与系统「服务」子菜单;不实现自研翻译,也不为此引入原生模块或翻译服务。
  • Windows 更新包签名校验:Windows 安装包未签名,electron-updater 对未签名当前应用不校验更新签名(与现分发一致,SmartScreen 警告不在自动更新范围解决)。
  • Linux 自动更新:首版仅 macOS(arm64)与 Windows。
  • CI 发布自动上传:CI 保持 --publish never 零改动,安装包由 ops 手动上传 codechat file-center,与 vscode/jetbrains 分发方式一致。
  • 更新安装的推迟调度:重启安装立即执行(quitAndInstall),不做「推迟到下次退出应用」的调度。