Appearance
功能规格说明:桌面端壳层与分发
拆分自
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 暂不可用,下次启动重试)。
验收场景:
- 假设应用已安装且内置 CLI 随安装包发布,当启动 desktop 应用,则应用必须立即可用,本地会话直接使用内置 CLI,无需用户预先安装 Node.js/npm 或
wave-code:当用户曾选择过工作目录(「最近打开」列表非空),则应用必须自动执行一次与点击侧边栏「新对话」完全一致的初始化——以「最近打开」列表首项(最近主动选择的仓库根)为工作目录启动一个新的空会话(输入框可用、上下文栏显示该目录),用户打开即见可用的新对话,无需再经过目录选择步骤(等价 2026-09-08 拍板「刚打开桌面端就跟点击新对话的效果一样」,语义见 desktop-sessions.md「会话管理」场景 2;未登录时输入框为禁用态并显示登录原因,见 sso-auth.md「IDE 插件更多菜单与欢迎页」场景 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 前代装。
用户故事:与 IDE 插件一致的交互能力(优先级:P1)
作为用户,我希望 desktop 中的确认(Confirmation)、AskUserQuestion、@文件 提及、图片粘贴、PDF 附件等交互与 IDE 插件完全一致,以便我获得相同的使用体验。
为什么是这个优先级:这些是 agent 交互的安全基础与核心场景,webview 已实现,desktop 必须保证宿主消息层正确接线。
独立测试:触发一个需要确认的工具调用并响应;输入 @ 选择文件;粘贴图片发送。验证行为与 IDE 插件一致。
验收场景:
- 假设 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-and-settings.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请求结束后必须终止该转发进程并释放本地端口,不得残留孤儿进程。
用户故事:内置 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。
验收场景:
- 假设应用已安装,当应用启动,则本地会话直接使用随应用发布的内置 CLI(
resources/wave-cli/,其package.json中的 wave-code 版本与 GUI 版本相互独立,可不同号),不执行任何 npm 安装/升级。 - 假设应用升级到新版本,当新版本应用启动,则本地会话使用新版内置 CLI——运行时
~/.wave/cli/desktop/中dist/bundle/wave.mjs与内置resources/wave-cli/的同名文件字节(sha256)不一致时重新复制(仅替换 CLI 文件:dist/、入口、package.json),字节一致(同版本重装、或 GUI 单独发版未改 CLI)则不复制;旧 CLI 无残留运行,已缓存的运行时依赖(rg/sharp,node_modules/)保留、不重复下载。 - 假设用户连接远程主机,且远端固定目录
~/.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。 - 假设远端 CLI 升级(推送)成功且该主机旧 daemon 仍在运行(旧 daemon 运行的是升级前的代码),当升级完成,则应用必须重启该主机的 daemon(终止旧 daemon 并启动新 daemon);重启后历史会话仍可从远端转录恢复,断线前正在进行的任务终止(与 daemon 退出的既有语义一致,不得出现幽灵「运行中」状态)。
- 假设远端 CLI 推送失败(ssh 中断、磁盘错误等),当应用检测到失败,则必须向用户显示可操作的错误信息与重试提示(下次连接自动重试,不得无限重试);替换采用「临时目录写入 + 原子
mv」且失败不动旧目录,故运行中的旧 daemon 不受影响、可继续使用。 - 假设远端主机完全未安装 wave CLI(首次连接),当应用连接该主机,则必须推送与 GUI 内置 CLI 字节一致的 bundle(推送源与内容=内置
resources/wave-cli/,而非远端 npm registry 的wave-code包)到~/.wave/cli/desktop/;当远端无 Node.js(或主版本低于 22),则必须显示安装/升级远端 Node.js 的引导信息,不得进入不可用状态。 - 假设应用升级到新版本但内置 CLI 的 wave-code 版本未变(GUI 单独发版,不 bump CLI),当用户连接远程主机,则以字节为准:内置
wave.mjs与远端副本字节一致时不推送、不重启 daemon(无增量传输);字节已变(构建了新的 CLI 代码但未 bump 版本号)时必须推送并重启 daemon——版本号相同不再视为「已满足」。 - 假设远端 CLI 已就位但其 grep 依赖 rg(
@vscode/ripgrepJS 包装 + 平台二进制)缺失(如首次推送后),当远端以该 CLI 启动 daemon,则由远端 CLI 自己在启动期从 npmmirror 下载 rg 到共享~/.wave/cli/node_modules/@vscode/(只需远端 Node ≥ 22 与出网,不再要求远端安装 npm);下载失败不阻断 daemon 启动(grep 暂不可用、下次启动重试),CLI 推送本身不受影响(走 ssh、不经 registry)。
用户故事:无干扰通知(优先级:P3)
作为用户,我希望版本更新提示和默认的 plan/通知提醒以非模态的方式出现在消息流中,以便我不会被系统弹窗打断。
为什么是这个优先级:desktop 无编辑器状态栏/状态对话框,需要替代通知通道。
独立测试:在会话中触发一个通知,验证它作为系统消息插入消息列表而非弹窗。
验收场景:
- 假设 CLI 有新版本,当应用检测到更新,则必须在消息流中插入一条系统消息,而不是弹出模态对话框。
- 假设有后台任务或 plan 状态变化的通知,当通知到达,则默认在消息流中显示,不打扰当前输入。
用户故事:跨平台分发(优先级:P2)
作为团队,我希望 desktop 应用能打包为 macOS(arm64)和 Windows 安装包,以便覆盖主要用户平台。
为什么是这个优先级:分发能力决定功能是否能到达用户。
独立测试:在 CI 或本地分别打出 macOS arm64 与 Windows 包,安装后验证启动与会话功能。
验收场景:
- 假设配置了 electron-builder,当执行打包命令,则必须生成 macOS arm64 与 Windows 安装产物。
- 假设应用依赖 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);更新端点故障时验证手动检查给「检查更新失败」提示、自动检查(启动与轮询)静默;应用持续运行跨过一个轮询间隔时验证按间隔再次自动触发检查(自动、静默),且不自动下载。
验收场景:
- 假设用户已登录企业版(存在 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。 - 假设用户未登录(无 serverUrl),当应用触发更新检查,则 updateChannel 不生效,且不得执行任何更新检查:不得启动 electron-updater、不得查询 GitHub Releases(2026-09-09 拍板:未登录不查更新——曾有的 GitHub 回退手动提示链路与 checkForUpdate/updateChecker 代码一并删除),启动自动检查保持静默、手动检查提示「登录后可检查更新」;本地会话与其它功能不受影响。应用内 toast 基建保留(位置/形态与配色路由见 desktop-account-and-settings.md「设置页反馈语义」——应用级提示为顶部居中新形态),仅用于信息提示(登录引导、下载完成等),不再承载任何更新发现/下载/重启提醒。
- 假设已登录企业版且检查发现新版本,当收到 update-available 事件,则不得自动开始后台下载,必须把更新状态置为 idle 并随
desktopAccountInfo推送——账户卡片个人信息行右侧出现「更新」按钮(S1);是否下载由用户在 S2 确认框决定(2026-09-03 裁决:账户卡片不提供更新 toast,toast 自动下载链路已删除)。 - 假设用户在 S2 确认下载,当 webview 下发
desktopUpdateDownload命令,则宿主必须先把更新状态置为 downloading 并推送(卡片按钮转「正在下载更新…」并禁用,防重复下载,S3),再调用 electron-updater 开始后台下载;当下载完成收到 update-downloaded 事件,则必须把状态置为 ready 并推送(S4)——webview 据此自动弹出「重启应用」确认框(仅一次),选「稍后」后按钮转常驻「重启」(S5)。 - 假设更新服务故障(端点不可达 / 元数据解析失败 / 下载失败 / downloadUpdate 抛错),当下载失败且状态为 downloading,则必须把状态回退为 idle 并推送——卡片按钮恢复「更新」,用户可重新走 S2 重试(见 desktop-account-and-settings.md「账户卡片 · 更新按钮状态机 S0–S6」场景 8),不得静默失败也不得弹手动下载页 toast;当失败发生在已就绪(ready)之后的安装阶段,则不得把状态降级(重启时机由用户决定,避免把已就绪更新重置回可下载造成重复下载),仅记录日志;当失败发生在尚未发现更新的检查阶段,则自动检查(启动与轮询)静默、手动检查给出「检查更新失败,请稍后重试」提示。
- 假设用户触发手动检查(
checkForUpdates命令),当检查执行,则必须复用本故事场景 1-5 链路:已登录走 electron-updater(按当前 updateChannel 对应 feed 查询;无更新时 toast 提示按 updateChannel 区分,stable 见「接收 Beta 版更新」场景 5、beta 提示「当前已是最新版本」);未登录按场景 2 不执行检查,toast 提示「登录后可检查更新」。 - 假设已登录企业版且 electron-updater 返回更新信息,当更新下载执行,则必须使用更新元数据中的真实文件入口下载安装包,不得指向 manifest.json / feed 目录自身。
- 假设 macOS 上应用运行于不可写位置(如非 /Applications),当 S6 重启安装无法原地完成,则安装失败经 electron-updater error 事件上报,宿主按场景 5 处理(ready 态错误不降级、记录日志),用户仍可稍后再次执行重启安装或经手动渠道获取安装包,不得自动弹窗打断当前工作。
- 假设构建安装包(mac/win),当electron-builder 打包完成,则产物
resources/app-update.yml必须存在(afterPack 写入updaterCacheDirName)——electron-updater 下载前会读取该文件(configOnDisk),缺失时下载阶段抛 ENOENT 并降级为下载页提示(修复:构建未声明 publish 配置时 electron-builder 不生成该文件)。 - 假设用户在 S4 确认框选「立即重启」或点击 S5「重启」按钮,当 webview 下发
desktopUpdateRestart命令,则宿主必须立即复位更新状态并推送(按钮消失,S6)后调用 quitAndInstall 退出并安装新版本(Windows 静默安装 +--force-run装完自动重启,不再弹二次确认对话框)。重启安装前应用即将退出,无需任何 toast/加载态反馈——按钮消失即反馈(toast 加载链路已随 2026-09-03 裁决删除)。 - 假设应用已登录企业版且持续运行(不退出、不重启),当运行期跨过固定的检查间隔(首版 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,正式未追平已装测试版号时手动检查提示等待、不降级;未登录时开关置灰不可切换、不触发任何更新检查。
验收场景:
- 假设桌面端用户打开设置页「全局设置」,当查看其下「桌面端设置」区块,则必须显示「接收 Beta 版更新」开关(默认关闭 = updateChannel=stable);VSCE/JetBrains 设置页不得显示该区块与该行(仅桌面端渲染)。
- 假设用户未登录企业版(无 serverUrl),当查看「接收 Beta 版更新」开关,则开关必须置灰不可切换并说明原因(如「登录后可接收测试版更新」);更新检查按「桌面端自动更新」场景 2 不触发(未登录不查更新),与 beta feed 无交集。
- 假设已登录且开关关闭(updateChannel=stable),当用户开启「接收 Beta 版更新」,则宿主必须把 updateChannel 置为 beta 并持久化(重启后保持),并立即按
serverUrl/api/downloads/desktop-beta/{mac|win}/重查一次——发现更高版本则进入账户卡片更新按钮状态机 S0–S6(见 desktop-account-and-settings.md「账户卡片 · 更新按钮状态机 S0–S6」),无更高版本则保持现状(账户卡片不出现「更新」按钮)。 - 假设已登录且开关开启(updateChannel=beta),当应用触发更新检查(启动自动/手动),则一律查询 beta feed 而非 stable feed;当beta feed 不可达或元数据解析失败,则按自动更新故事场景 5 错误处理(启动自动检查静默、手动检查提示「检查更新失败,请稍后重试」),不得静默回退 stable feed、不得崩溃。
- 假设已登录且开关关闭(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 打开设置页验证不显示主题项。
验收场景:
- 假设用户打开桌面端设置页「全局设置」,当查看其下「桌面端设置」区块,则必须显示「主题」选择:跟随系统(默认)/ 浅色 / 深色三选项;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 点「文件 → 退出」验证退出应用。
验收场景:
- 假设应用在任一平台运行,当用户查看应用菜单栏,则必须依次显示「文件」「编辑」「对话」「面板」「视图」「窗口」六个中文菜单(macOS 另有应用名菜单),不得出现英文「File」「Edit」「View」「Window」标签。
- 假设应用在 Windows/Linux 运行,当用户展开「文件」菜单,则必须包含「退出」项;在 macOS 上「文件」菜单必须包含「关闭窗口」且不注册
Cmd+W加速器(该加速器专属「关闭分屏」)。 - 假设应用在任一平台运行,当用户展开「编辑」菜单,则必须包含撤销、重做、剪切、复制、粘贴、删除、全选(macOS 另含「粘贴并匹配样式」);「视图」菜单必须包含重新加载、开发者工具、缩放、全屏等项。
- 假设应用在任一平台运行且文本焦点在输入框,当用户按下
Ctrl+C/Ctrl+V/Ctrl+X/Ctrl+Z/Ctrl+A(macOS 为Cmd键),则剪切、复制、粘贴、撤销、全选行为必须正常。 - 假设菜单包含标准动作(撤销/剪切/复制/粘贴/重新加载/开发者工具/缩放等),当用户点击对应菜单项,则必须通过 Electron
role实现而非自定义click,以保持原生行为与快捷键。
用户故事:文本与图片右键菜单(优先级: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,当查看窗口顶部,则系统标题栏必须隐藏(无标题文字与栏体),内容区延伸至窗口顶部,左上角保留系统红绿灯浮于内容之上(BrowserWindow
titleBarStyle: "hidden",仅 darwin 生效)。 - 假设应用运行在 macOS 且侧边栏展开,当查看侧边栏顶部,则侧边栏首行必须显示 44px 窗口行(与聊天顶栏同高),视觉上与侧栏同色一体——无独立条带、无下边界线,即侧边栏自身的第一行内容:左侧为系统红绿灯落位(行内不绘制假圆点,避免重复;红绿灯中心落在窗口行垂直中心线上,与行内按钮同一条水平线,不显按钮偏下),红绿灯右侧同排左对齐显示「收起侧边栏」按钮(按钮左缘越过红绿灯区后紧邻绿点,可见间距约 16px);整行除按钮外均为窗口拖拽区(
-webkit-app-region: drag),按住空白处拖动可移动窗口、双击按 macOS 惯例缩放,按钮本身-webkit-app-region: no-drag、可点击收起侧边栏(收起后窗口行随侧栏一并消失);窗口行下方隔留白才是品牌 logo 行,logo 行右侧图标区保留「活动」等入口、不被窗口行挤占。 - 假设应用运行在 macOS 且侧边栏已收起,当查看占据窗口左端的视图顶栏(对话顶栏,或替换会话区的「活动」会话状态看板顶栏),则该顶栏最左端必须让出约 76px 的红绿灯区域(该段空白且同为窗口拖拽区),其「展开侧边栏」等控件整体右移,不被红绿灯遮挡;看板与对话顶栏共用同一段让位机制(同一收起条件与同一 76px 拖拽段),不得各写一套偏移。
- 假设应用运行在 Windows/Linux,当查看窗口顶部,则必须保持系统原生标题栏(标题文字与窗口控制按钮照旧),不得渲染 macOS 窗口行/让位——「收起侧边栏」按钮仍位于品牌行右侧图标组,既有布局与交互完全不变。
- 假设应用运行在 macOS 且窗口行已渲染,当用户操作聊天顶栏/侧边栏内其余控件(包括窗口行内的「收起侧边栏」按钮),则交互不受影响——窗口拖拽区仅限窗口行除按钮外的空白区域与收起态让位段;按钮已解除拖拽(
no-drag),点击即收起侧边栏,不误触窗口拖动。 - 假设原型预览等浏览器环境(无系统窗口 chrome)渲染桌面侧边栏,当查看其顶部,则继续按现状绘制假红绿灯圆点行,且红绿灯右侧同排渲染「收起侧边栏」按钮(与真机 macOS 形态一致),两者相互独立、互不影响。
- 假设应用运行在 macOS 且窗口进入全屏(系统红绿灯随全屏隐藏),当查看侧边栏顶部窗口行,则窗口行的红绿灯让位自动收起——「收起侧边栏」按钮左移至窗口行起点(不再预留红绿灯让位),左侧不再出现无红绿灯的空段;退出全屏后红绿灯恢复,让位与按钮位置随之还原(回到与红绿灯同排)。侧边栏收起时对话顶栏左端的红绿灯让位段(约 76px)行为一致:全屏下让位收起、控件贴左,退出全屏恢复让位。
- 假设应用运行在 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),不做「推迟到下次退出应用」的调度。