Appearance
功能规格说明:桌面端设置与账户
用户场景与测试
用户故事:设置页面(优先级:P1)
作为用户,我希望通过侧边栏账户卡片菜单(已登录为点击个人信息行热区弹出的纯功能菜单,未登录为「更多」按钮菜单;两态菜单均含「设置」项)中的「设置」打开完整的设置页面,在左侧导航分组中浏览当前生效的全部配置与状态(语言、主题(桌面端)、上下文长度、AGENTS.md、自动记忆、项目插件、技能、子代理、钩子、MCP 服务;桌面端导航里没有「插件市场」——它由侧边栏「新对话」下方的入口打开整页,VSCE / JetBrains 仍以设置页导航项承载,见 plugin.md「插件市场」场景 3 与 A-016),并可直接修改「全局设置」与「个性化」中的基础配置项,以便了解并调整当前环境的配置全貌。
为什么是这个优先级:设置页是桌面端的管理中心,用户拍板按原型做成全页面(覆盖会话区,2026-08-31 曾拍板全部导航项只读展示;2026-09-01 原型走查用户恢复部分可编辑:全局设置(AI 回复语言/上下文长度)与个性化(自动记忆开关/轮次)提供编辑控件与保存按钮(对齐旧配置弹窗交互),保存经 stdio updateConfiguration RPC 写回配置并即时生效(2026-09-10 变更:用户偏好改走写用户级 ~/.wave/settings.json + SDK 实时重载,不再经 AgentOptions 覆盖层下发,保存不再重建会话,详见下文「用户偏好的保存路径与重建时机」),其余视图(AGENTS.md 文本区、技能/子代理/钩子/MCP)保持只读浏览或原有管理形态;2026-09-04 用户拍板「个性化」AGENTS.md 文本区一并恢复可编辑(用户级/项目级各提供独立保存按钮,写回对应 AGENTS.md 文件,与 2026-08 批次 2 的读写 RPC 同源),此后仅技能/子代理/钩子/MCP 视图保持只读浏览或原有管理形态;2026-09-08 用户按设计师高保真走查拍板:设置页进一步占满整个 view——连会话侧边栏一并覆盖(不再「侧边栏保持可见」),macOS 隐藏标题栏下系统红绿灯由设置页自身左导航顶部承接;VSCE/JetBrains 无侧边栏账户区,同样以编辑器区域标签页 webview 承载(对齐 plan 标签页的呈现机制,从现有配置入口/命令打开),内容与 desktop 一致。2026-09-02 走查追加「主题」选择(仅桌面端展示,三态与行为见「主题设置」故事):主题为桌面端本地显示偏好(不写入共享配置),选择即生效并持久化。2026-09-08 拍板:「主题」与「接收 Beta 版更新」从「基础设置」卡片拆出,独立成「桌面端设置」区块,仍位于「全局设置」视图内(非独立导航项)——两者是桌面端独有的本地设置通道(host 直连 setThemeSource/setUpdateChannel 持久化于 configStore,不走 stdio updateConfiguration、不写入共享 settings.json),与「基础设置」中走 stdio 的语言/上下文长度区分展示;仅桌面端渲染该区块。2026-09-10 用户拍板(保存路径根因治理):设置页「全局设置」/「个性化」的用户偏好不再经 stdio updateConfiguration 当 AgentOptions 覆盖层下发(该覆盖层优先级高于 settings.json,会永久遮蔽实时配置——这正是此前「保存必须重建全部会话」的根因),改为写用户级 ~/.wave/settings.json 并由 SDK 实时重载(LiveConfigManager)在各会话下一轮开始时生效,保存因此立即回执、界面即时刷新、不重建任何会话;插件装卸等变更也不再走重建(2026-09-18 拍板改为就地重载——变更落盘只出现一次提示,由用户敲 /reload-plugins 触发换装,见 plugin.md「插件变更的就地重载」),本端不弹任何重建确认框。
独立测试:desktop 经账户卡片菜单(已登录热区纯功能菜单/未登录「更多」按钮菜单)点「设置」验证全页面导航(7 项 3 组,全部可进入;桌面端导航里没有「插件市场」项)与会话侧边栏被覆盖(单会话与分屏两形态均覆盖、返回恢复原布局);全局设置/个性化修改语言、上下文长度、自动记忆开关与轮次并保存,验证新值生效(配置回发刷新展示)、保存不重建任何 live 会话(会话不销毁、排队消息不清空、流式不打断);未修改任何值直接保存验证同样不重建;插件变更场景验证不弹任何重建确认框、不重建任何会话,只出现一次「插件已变更」提示,敲 /reload-plugins 后在同一会话内生效;VSCE/JetBrains 从命令面板/设置入口打开编辑器区域标签页 webview,验证内容与 desktop 一致;钩子视图按事件分组展示已配置命令(用户级/项目级双 tab);MCP 视图展示服务器列表(类型/端点/连接状态/工具数)并可连接/断开;项目设置视图展示 SDD 开关状态并可切换。
验收场景:
- 假设desktop 侧边栏账户卡片已展示,当用户经账户卡片菜单(已登录为点击个人信息行热区、未登录为「更多」按钮菜单)点击「设置」,则设置页占满整个 view——会话侧边栏一并被覆盖(不再保留),页面仅显示设置页自身:左侧导航与右侧内容区,导航顶到窗口最左、内容顶到窗口最上;macOS 隐藏标题栏下系统红绿灯浮于设置页左导航顶部左上角(见 desktop-shell「macOS 隐藏标题栏」设置页场景);再次点击设置页左上角「返回应用」(现文案「返回」)或关闭方式可回到会话视图(会话侧边栏随之恢复)。
- 假设设置页面已打开,当用户查看左侧导航,则必须分为三组共 7 项:「通用」(全局设置/个性化)、「工作区」(项目设置)、「AI 与扩展」(技能/子代理/钩子/MCP 服务),当前激活项有高亮态;桌面端不出现「插件市场」项(它由侧边栏「新对话」下方的入口打开整页,见 plugin.md「插件市场」场景 3 与 A-016),VSCE / JetBrains 没有侧边栏整页、设置页这项是它们唯一入口;浏览类视图不出现编辑控件,「全局设置」「个性化」仅对可编辑项展示控件。
- 假设用户点击「全局设置」,当进入该视图,则必须显示两个区块:「基础设置」卡片与「桌面端设置」区块(后者仅桌面端渲染)。「基础设置」卡片展示当前生效的 AI 回复语言(中文 zh-CN / English en-US)与上下文长度(K 值):语言为下拉选择(中文/English)、上下文长度为数字输入(16–1000 K,读写同一个全局键
env.WAVE_MAX_INPUT_TOKENS,行内须给出一句可见说明如「全局默认;当前模型自带上下文上限时以模型配置为准」——2026-09-10 拍板,语义见 agent-config.md 边界说明「上下文长度的落点」),提供「保存」按钮;文件里没有该键时不得假装一个有值:语言下拉首项为「未设置(默认:中文)」并选中、上下文长度输入框留空并以灰字占位符「跟随模型配置(默认 200K)」显示系统默认(完整规则见 agent-config.md「IDE 插件配置入口」故事场景 7–8);被更高层覆盖的键必须如实显示生效值,不得回退显示用户文件里的值、也不得显示「未设置」态(见 agent-config.md「IDE 插件配置入口」故事场景 9 与边界说明「用户偏好的层与来源」):企业下发的 Remote 组织配置(用户本地无法覆盖)→ 置灰 + 行内提示「由组织配置管理」;机器环境变量给值 → 显示生效值 + 一句来源说明但仍可编辑(保存即写进用户级文件并覆盖环境变量的值);保存把用户偏好写入用户级~/.wave/settings.json(经 SDK 实时重载在各会话下一轮开始时生效,见 agent-config.md「设置实时重载」)且只把改动过的字段放进载荷(省略键 = 不改该键),保存回执不等待任何会话重建——不销毁/重建 live 会话、不清空排队消息、不打断流式输出,界面即时刷新为新值;保存结果经宿主全局 toast 提示(成功「保存成功」/失败「保存失败:<原因>」,语义见本故事「设置页反馈语义」),设置页自身不渲染页面内自建提示文字。「桌面端设置」区块位于「基础设置」下方(2026-09-08 拍板:原属「基础设置」卡片的「主题」「接收 Beta 版更新」两行拆出独立成区),展示桌面端独有的两条本地偏好:「主题」选择(跟随系统/浅色/深色,默认跟随系统)与「接收 Beta 版更新」开关——两者均选择即时生效并持久化、不依赖「保存」按钮;持久化经 host 直连通道(setThemeSource/setUpdateChannel)写入桌面 configStore,不走 stdioupdateConfiguration、不写入共享 settings.json(语义见 desktop-shell「主题设置」「接收 Beta 版更新」故事),VSCE/JetBrains 设置页不显示该区块。 - 假设用户点击「个性化」,当进入该视图,则必须显示两块内容:「AGENTS.md」(用户级/项目级两个 tab,文本区可编辑对应文件内容,各作用域提供独立「保存」按钮(文案含当前作用域,如「保存用户级配置」);保存经
setAgentsContentRPC 写回对应文件——用户级~/.wave/AGENTS.md、项目级当前项目 AGENTS.md,保存结果经宿主全局 toast 提示(成功「保存成功」/失败「保存失败:<原因>」,语义见「设置页反馈语义」))与「自动记忆规则」(开启自动记忆为开关、触发记忆提取会话轮次为数字输入(1–100 轮),提供「保存」按钮;文件无该键时:轮次输入框留空并以灰字占位符「默认 1 轮」显示系统默认(见 agent-config.md「IDE 插件配置入口」故事场景 7),开关不做占位态(其真实默认即「开」,三态开关更难用)继续显示为开;被更高层覆盖时同样如实显示生效值:Remote 组织配置 → 置灰 + 「由组织配置管理」,机器环境变量 → 显示生效值 + 来源说明但仍可编辑(见 agent-config.md「IDE 插件配置入口」故事场景 9);保存只写改动过的字段(省略键 = 不改该键);保存即生效且影响运行中的会话——关闭后从下一轮起不再触发记忆提取(不写入自动记忆文件)、新建会话同样不提取,重新开启后恢复且计数从开启时刻重新累计,频率按新值生效(语义见 memory-management「自动记忆开关」);保存同样写入用户级~/.wave/settings.json、不重建会话(含自动记忆安全区目录随开关热更新,见 agent-config.md「设置实时重载」);保存成功/失败经宿主全局 toast 提示(语义见「设置页反馈语义」))。项目级 AGENTS.md 仅针对当前项目,无项目切换按钮。 - 假设「个性化」AGENTS.md 文本区可编辑且两个作用域内容均已加载,当用户在用户级编辑草稿后切换到项目级再切回用户级、或点击「保存用户级配置」,则切换 tab 不触发任何保存/写入,各作用域草稿相互独立保留(切换只改变文本区绑定,未保存的修改不丢失);仅点击当前作用域的保存按钮时经
setAgentsContent写回该作用域对应文件,host 回发agentsContentSaved后经宿主全局 toast 提示保存结果(成功「保存成功」/失败「保存失败:<原因>」,语义见「设置页反馈语义」);设置页不再渲染页面内自建提示文字,提示生命周期由宿主 toast 通道既有规则管理(无「切换导航项即清除」的页面内状态)。 - 假设用户点击「钩子」,当进入该视图,则必须只读展示钩子配置,提供「用户级/项目级」双 tab 切换(对齐个性化视图):按事件分组(如 PreToolUse、PostToolUse、UserPromptSubmit、SessionStart 等)展示每个事件下已配置的命令及可选参数(异步、超时、匹配器),未配置任何钩子的事件组不显示或显示空态说明;用户级/项目级条目提供新建/编辑/删除(行为与 hooks-command / hooks.md 管理故事一致:编辑预填提示词并打开 settings.json、删除二次确认后直接删配置),插件级条目只读;删除成功经宿主全局 toast 提示「已删除钩子「
<hookName>」」(失败提示沿用该故事错误提示语义)。 - 假设用户点击「MCP 服务」,当进入该视图,则必须展示 MCP 服务器列表(名称、类型(stdio/sse/http)、命令或端点 URL、连接状态(已连接/连接中/断开/错误)、可用工具数),提供「用户级/项目级」双 tab 切换:用户级 tab 读取
~/.wave/mcp.json、项目级 tab 读取项目根.mcp.json;每个服务器提供「连接/断开」操作(对齐现有 MCP 连接管理能力);用户级/项目级服务器提供新增/编辑/删除(与 mcp.md 管理故事一致:编辑预填提示词并打开配置文件、删除二次确认后直接删配置),插件级服务器只读;删除成功经宿主全局 toast 提示「已移除 MCP 服务器「<serverName>」」(失败提示沿用既有「移除 MCP 服务器失败」错误语义)。 - 假设用户点击「项目设置」,当进入该视图,则必须展示内置插件 SDD 开关状态(读取项目
.wave/settings.json合并后的enabledPlugins);开关为唯一交互控件——切换开关写回配置文件并生效(不重建会话、不弹重建确认框:变更落盘后出现一次「插件已变更」提示,用户敲/reload-plugins后在同一会话内使插件技能与引导生效,见 plugin.md「插件变更的就地重载」),其余内容只读。开关状态每次进入该视图都以当前会话/活动项目的工作目录重新读取,不得沿用上次进入缓存的旧值:会话/工作目录切换后重入视图必须刷新为该工作目录的真实配置;外部变更(手动编辑.wave/settings.json或其他端切换该开关)后重入视图同样必须刷新一致——避免出现「配置文件已启用(sdd@builtin: true)而开关仍显示关闭」的状态矛盾。 - 假设用户点击「技能」或「子代理」,当进入该视图,则按来源 Tab 展示列表(项目来源 Tab 平铺,无项目分组卡片),提供详情查看、新建、编辑、删除等管理操作,与 agents-command / skills-command 规格一致;删除成功经宿主全局 toast 提示「已删除技能「
<name>」」/「已删除子代理「<name>」」(失败提示沿用各删除故事错误提示语义)。 - 假设VSCE/JetBrains 宿主环境,当用户通过命令面板(如「打开设置」)或现有设置入口打开设置,则必须在编辑器区域打开设置标签页 webview(对齐 plan 标签页:VSCE 为编辑器区域面板、JetBrains 为编辑器标签页),内容与 desktop 设置页一致(同一导航结构与视图);再次调用打开时复用已打开的标签页而非新建。
- 假设设置页任一视图打开,当用户修改了全局设置/个性化的可编辑项但未点击保存,则切换导航项或离开设置页不产生任何消息、不写入任何配置;仅点击「保存」时写用户级
~/.wave/settings.json(经 SDK 实时重载生效,不重建会话);打开设置页、切换导航项、切换视图 tab 本身不得触发任何写入或会话重建。 - 假设desktop 处于分屏(多个 pane)或单会话布局且已打开设置页,当查看设置页布局,则设置页必须覆盖整个窗口——会话侧边栏与所有 pane 一并被隐藏,仅设置页可见;分屏的 pane 数量/尺寸/会话不因打开设置而销毁(会话在后台继续运行,含流式输出);点击「返回」关闭设置页后恢复打开前的布局(分屏 pane 布局、侧边栏展开/收起、聚焦会话、消息滚动位置原样保留),不新建会话、不打断流式。
- 假设用户在「全局设置」或「个性化」点击「保存」且实际值与当前生效值完全相同(一个字都没改,含控件仍处于「未设置」态的情形),当保存完成,则只落盘并回执保存成功(toast「保存成功」)——载荷里不含任何未改动的键(省略键 = 不改该键,未设置态不写该键,见 agent-config.md 边界说明),不得重建任何会话、不得清空排队消息、不得打断流式输出——不得把「无差异保存」当作配置变更处理。
- 假设本次保存只涉及热生效键(AI 回复语言、上下文长度、自动记忆开关/轮次等),当保存完成,则不得重建任何会话;假设本次变更包含插件安装/卸载/启停/更新或内置插件开关,当变更落盘,则同样不得重建任何会话、不得弹任何重建确认框——变更只产生一个「待应用」信号并由宿主提示一次(逐字文案见 plugin.md「插件变更提示」),由用户敲
/reload-plugins触发就地换装。 - 假设用户敲
/reload-plugins,当重载执行,则在同一会话内就地换装、会话 id 不变、消息与转录连续、不打断正在生成的回合;假设用户未敲命令,则不存在任何自动重建路径,也不登记「待重建」待办——变更只对新建对话自动生效(完整语义见 plugin.md「插件变更的就地重载」)。
设置页反馈语义(2026-09-09 设计师走查拍板:设置页不得自建一套提示)
- 统一走宿主全局 toast:设置页的「结果型」反馈——配置保存(全局设置/自动记忆规则)、AGENTS.md 保存、四视图删除类操作——的成功/失败均经宿主全局 toast 通道提示(desktop 为 host
showToast→ webview 应用级 ToastStack(顶部居中新形态,见下「toast 形态与路由」);VSCE 为vscode.window.showInformationMessage/showErrorMessage原生通知;JetBrains 为 IDE 通知气球(Info 变体,失败沿用既有IdeService.showError));设置页自身不渲染任何页面内自建提示文字(如卡片内/文本区旁的灰色小字成功/失败消息),不再保留「切换导航项即清除」的页面内瞬态反馈状态。 - 成功/失败文案模板:保存类成功「保存成功」/失败「保存失败:<原因>」;删除类成功「已删除技能「<对象名>」」「已删除子代理「<对象名>」」「已删除钩子「
<hookName>」」「已移除 MCP 服务器「<对象名>」」/失败「删除/移除技能/子代理/钩子/MCP 服务器失败:<原因>」。提示停留时长遵循各宿主通道既有规则(桌面端应用内 toast 见下「信息型 toast 停留时长」;不新增按提示类型自定义的停留逻辑)。 - 宿主在结果确定时发出:toast 由 host 在操作结果确定后发出(desktop 删除/连接命令的 try/catch 成功分支与配置保存的持久化结果处——
handleUpdateConfiguration落盘并写用户级 settings.json 后即刻回包处,不得等会话重建完成、不得以重建完成与否决定成功;handleSetAgentsContent结果处,VSCE 两路由 handler,JB MessageHandler),不以 webview 乐观时序为准。 - 写操作需 live agent:删除/保存类写操作在宿主侧 agent 缺失(未初始化/无会话)时不得静默成功或空转——须给出失败 toast(对齐 JB
setAgentsContent「智能体未初始化」诚实失败先例);Webview 列表视图在无 agent 时读到的空列表不产生删除入口,但 host 仍须防御直接命令调用。 - 非本拍板范围的提示保持不变:MCP 连接/断开成功不弹提示(列表状态即结果)、失败给出错误提示(见下「toast 形态与路由」的语义色范围);「保存中…/连接中…/断开中…」进行态仍由按钮禁用与文案承载。
- toast 形态与路由(2026-09-10 设计师拍板):桌面端应用内 toast 按宿主显式声明的
position(不以「是否带按钮」推断)分两个互不干扰的栈——应用级全局提示(position缺省"top",含本故事的设置页结果型反馈)渲染为顶部居中 toast 条(水平锚定内容列/工作区中心、圆描边语义图标、自顶向下落下动效);后台会话确认提示(position: "bottomRight",见 desktop-sessions.md「后台会话确认提醒」)保持既有右下角通知形态(深底浅字、自底向上滑入、无语义图标),不并入顶部新形态,两栈可同屏共存。语义三色只用于设置页结果型提示(保存成功/失败、删除/移除技能·子代理·钩子·MCP 成功/失败、MCP 连接/断开失败;插件变更的两句一次性提示不在其列——它们按中性提示呈现,见 plugin.md「插件变更提示」):成功=成功色、失败=失败色,均配浅色底 + 同色图标/文字(不只靠色);其余应用级提示一律中性——默认浮层底色、不渲染成功/失败/信息图标,不得缺省回退为任一彩色语义。 - 信息型 toast 停留时长(2026-09-22 拍板:2 秒 → 8 秒):无操作按钮的信息型 toast(两个栈通用)统一在 8 秒后自动消失;带操作按钮的 toast 不自动消失,等用户处理或手动关闭。时长是 ToastStack 的单一全局常量,不按提示类型 / 单条提示自定义字段,也不做悬停暂停(「不新增自建停留逻辑」仍成立——改的是通道自身的既有规则,不是给个别提示开后门)。起因:2026-09-22 用户反馈 2 秒根本读不完——安装 / 卸载 / 更新插件会同时出现两条无按钮提示(如
已安装「X」(作用域:user)+插件已变更。运行 /reload-plugins 使其生效。),两条同刻挂载、同刻消失,第二条还带一条要照抄的命令;叠加提示条本身无悬停暂停、到点直接卸载,用户来不及读完。8 秒的取值来源:对齐 Claude Code 的通知默认超时(DEFAULT_TIMEOUT_MS = 8000),不另立数字。影响范围:仅桌面端应用内 toast(VSCE / JB 的同类提示走宿主原生通知,停留时长由宿主与系统设置决定,无此常量)。
用户偏好的保存路径与重建时机(2026-09-10 拍板:保存不重建会话)
- 保存路径(用户偏好):设置页「全局设置」(AI 回复语言、上下文长度)与「个性化」(自动记忆开关/轮次)保存的用户偏好,一律写入用户级
~/.wave/settings.json,由 SDK 的LiveConfigManager实时重载(用户在设置页改的即文件里的值),在各会话下一轮开始时生效(每轮开始取一次配置快照;见 agent-config.md「设置实时重载」)。不得把用户偏好经 stdioinitialize/updateConfig当AgentOptions覆盖层下发(该层优先级高于 settings.json,会永久遮蔽实时配置,正是此前必须重建会话的根因)。 - 回执与刷新:保存的回执(
configurationResponse)与界面刷新在落盘后立即给出,不等待任何会话重建;保存的成功/失败 toast 亦在持久化结果确定时发出。 - 不重建:纯热生效键的保存(无论值有无变化)不重建任何会话——不销毁/重建 live 会话、不清空排队消息、不打断流式输出;无差异保存(值未变)同样只落盘回执,不得触发重建与队列清空。
- 不再有需要重建的构造期副作用:插件装卸(安装/卸载/启用/禁用/更新插件、内置插件开关,如项目设置里的 SDD)2026-09-18 起改为就地重载——变更落盘后不重建会话、不弹任何确认框,由用户敲
/reload-plugins在同一会话内换装(见 plugin.md「插件变更的就地重载」);自动记忆开关/频率早在 2026-09-10 改为热生效。至此没有任何配置变更需要重建会话(见 agent-config.md「配置变更不再需要重建会话」故事)。 - 重建确认框已废止(2026-09-18 拍板):插件变更 2026-09-18 起改为就地重载,本端不再有任何重建确认框——原先的「立即重启 / 稍后重启」两按钮、N/M 文案、
Esc等同「稍后重启」等条款一并取消。变更落盘后只出现一次「插件已变更。运行 /reload-plugins 使其生效。」提示,由用户自己决定何时敲命令应用(见 plugin.md「插件变更提示」)。 - 就地重载不打断任何会话:敲
/reload-plugins时若某会话正在生成回复、有待确认权限(等待用户确认/回答)或存在运行中后台任务,重载照常执行、不打断该回合(与已废止的重建路径相反——后者在同样情形下必须保持旧配置);重载以同一sessionId就地换装、消息与转录连续、不丢消息。 - 不做惰性重建、不提供持久可见标识:插件变更后的任何时刻(含用户敲过
/reload-plugins之后)host 不登记任何「待重建」待办、会话/pane 再次被激活时不得自动重建;新建会话直接按磁盘当前的插件状态装载。变更与重载都只经一次性提示告知(两份逐字文案见 plugin.md「插件变更提示」)——不提供持久可见标识(2026-09-10 拍板:不新增状态条/角标/列表标记,也不提供「查看哪些对话待应用」的入口——此为刻意取舍,非遗漏)。 - 落盘边界(不进会话):桌面端独有的两条本地偏好——
theme(主题)与updateChannel(接收 Beta 版更新)——仍保留在桌面自己的wave-desktop.json(经 host 直连setThemeSource/setUpdateChannel持久化),不写入~/.wave/settings.json、不进入会话配置(选择即时生效、不依赖「保存」按钮);serverUrl等连接级字段同样不进用户偏好文件。 - 三端一致(2026-09-10 追加拍板:三端同批落地):VSCE / JetBrains / desktop 三端设置页的保存路径同语义(均写用户级
~/.wave/settings.json经实时重载生效,均不把用户偏好当AgentOptions覆盖层下发),以防多端行为漂移(见 agent-config.md「IDE 插件配置入口」);IDE 端不再经updateAllSessionsConfig重建会话生效,本轮与桌面端同批实现。用户在 IDE 内连远端时(VS Code Remote/SSH、JetBrains 远程),写入的是会话所在进程的用户级~/.wave/settings.json(远端机器上的该文件),语义与本地一致。2026-09-18 起不再有「重建确认框」这个桌面端例外:插件变更一律走就地重载、三端同语义(见 plugin.md「插件变更的就地重载」),既没有「等重建才回执」的症状,也没有需要选择时机的确认框(见 agent-config.md 边界说明「重建确认框已废止」)。 - 旧宿主存储不迁移 + 语言默认值不在宿主侧注入(2026-09-10 追加):改造前
wave-desktop.json曾存过language/contextLength/autoMemoryEnabled/autoMemoryFrequency(VSCE 在globalState、JB 在wave.xml)——这些键不再被读取,且不做一次性迁移(取舍与升级影响见 agent-config.md 边界说明「升级用户的一次性迁移:不做」):升级后旧值静默失效、回落默认(语言zh-CN、其余 SDK 既有默认),用户需在设置页重选一次。同时getConfiguration()不得再注入language: "Chinese"(改造前的宿主兜底已删除):语言默认值只在 SDK 解析链末尾有一处(zh-CN),与设置页下拉默认项同串,保证「未设置时显示 ≡ 生效」(见 agent-config.md 边界说明「语言默认值」)。 - 设置页展示的是生效值,不只是用户文件的值(2026-09-10 追加):用户级
~/.wave/settings.json是用户偏好的落点,但企业下发的 Remote 组织配置 / 机器环境变量可以盖过它(层序与归因范围见 agent-config.md 边界说明「用户偏好的层与来源」)。因此设置页回填的是生效值:宿主把getUserSettings回包里的用户偏好(含每键来源层preferenceSources)原样并入configurationData给 webview;来源为remote的键显示生效值 + 置灰 + 提示「由组织配置管理」(组织策略不可被本地覆盖),来源为env(机器环境变量)的键同样显示生效值 + 一句来源说明、但保持可编辑(用户级文件优先级高于 OS 环境变量,在此保存即写进该文件并覆盖环境值),user/default的键与改造前一致、无来源说明;键缺失仍表示「无任何提供者」(设置页按未设置态展示),preferenceSources是纯增量字段、老宿主缺它时全部可编辑。本 PR 的归因范围为进程级可达的层(Remote / 用户级文件 / 机器环境变量 / 默认),project/local覆盖的如实标注记后续(见 agent-config.md 边界说明「用户偏好的层与来源」)。 - 验证分层(单测 / e2e / 真 host)(2026-09-10 追加拍板):本故事与「设置页面」故事中「保存不重建」的验收要求,按三层落地——单测(
packages/desktop/tests,宿主被 mock)验证宿主自己发出的命令与状态;e2e(packages/webview/e2e,真浏览器 + 真 webview bundle)验证「保存后输入框仍在、未弹重建确认框」以及插件变更后的提示与敲/reload-plugins的交互(不弹任何确认框);真 host(packages/desktop/tests/integration,真DesktopHost+ 真wave --stdio+ 真文件系统,pnpm -F wave-desktop test:realhost)验证保存后真 CLI 进程未被重启(sessionId不变)、会话不丢、settings.json被真实写入且下一轮读到新值(language 一例)、插件变更的就地重载(敲/reload-plugins后同一 CLI 进程 sessionId 不变、六类能力就地生效、会话未被重建);watcher 门禁(启动时文件不存在)与轮内快照等只能由 SDK 单测覆盖的场景在对照表中逐个标明未落到真 host 的原因。逐条对照表见 agent-config.md 边界说明「验证分层(单测 / e2e / 真 host)」。真 host 层挂在 main-only CI job(Linuxreal-host-e2e;Windowscheck-windows同样跑真 host 与 desktop 单元套件),e2e 层在 PR CI 中为跳过项(非门禁)。
用户故事:上下文用量指示器(优先级:P2)
作为用户,我希望在输入工具栏看到当前上下文已用百分比(圆环进度 + 数字),以便了解上下文占用情况并在接近上限时通过 /compact 主动压缩。
为什么是这个优先级:压缩是 CLI 已有能力(/compact 命令与自动压缩),webview 需新增用量推送与展示,是批次 2 的原型功能之一;用量展示为纯展示元素(非按钮、无点击行为),压缩入口不依赖该元素。
独立测试:三端(desktop/VSCE/JetBrains)打开会话并发送消息后验证工具栏出现上下文用量指示器与百分比;悬停指示器弹出含「上下文」字样的气泡(与「发送/停止」同一种气泡);指示器非按钮(不在 Tab 序、点击无行为);输入 /compact 触发压缩并在完成时更新百分比;欢迎页(新会话未发送消息)验证指示器不显示;用量为 0 或宿主尚未推送用量时验证指示器整块不渲染(工具栏无占位、不出现空提示框)。
验收场景:
- 假设会话已开始(非欢迎页)且输入工具栏可见,当用户查看工具栏,则权限模式按钮左侧必须显示「上下文用量」指示器(用量为 0 时整块不渲染,与「宿主尚未推送用量」同语义,见场景 4),视觉形态为圆环进度图标 + 百分比数字(对齐原型,如仅显示「24%」,环的填充比例与百分比一致);悬停指示器必须弹出设计系统统一的气泡提示(复用
Tooltip组件、role="tooltip",与「发送/停止」同一种气泡,贴指示器上方弹出),文案为「上下文已使用 24%」——点明该数字是上下文用量,同一文案同时作为aria-label供屏幕阅读器读取;指示器本体为纯展示元素(span,非按钮),不在 Tab 序中、点击无任何行为,气泡仅随鼠标悬停出现、不引入新的可聚焦控件。 - 假设用户需要手动压缩上下文,当用户在输入框输入
/compact并确认时,则触发与 CLI 等效的上下文压缩(压缩入口为斜杠命令与自动压缩,不依赖工具栏指示器);压缩完成后指示器百分比随最新用量更新。 - 假设会话处于欢迎页(新对话未发送消息),当用户查看输入区,则不得显示上下文用量指示器(对齐原型:仅非欢迎态显示)。
- 假设宿主尚未推送用量信息,当会话内容渲染完成,则不得渲染上下文用量指示器——圆环、百分比数字、气泡与
aria-label一并消失,工具栏不留占位、不出现空提示框(用量为 0 时同样整块不渲染:0 是宿主可达值,用量按Math.round(tokens / max × 100)计算(packages/code/src/stdio/agentBridge.ts),极低占用即得 0;2026-09-16 拍板:0与「未知」同语义,取代此前「未推送用量 → 显示空圆环」的写法);收到用量推送后显示圆环填充与百分比及对应气泡文案。 - 假设会话上下文用量随对话增长,当用量推送到达,则指示器百分比实时更新(含压缩后回落)。
- 假设用户切换到已有上下文用量的历史会话,当会话恢复完成,则指示器显示该会话上次的用量百分比(对齐 CLI:用量随会话恢复加载,无需等待新一轮对话或新消息)。
- 假设宿主 Webview 被重建(窗口重载、面板重新打开),当会话内容重新渲染完成,则指示器同样立即显示该会话的用量百分比(重建不产生新的用量推送,宿主在会话内容拉取时一并取回百分比并补发)。
用户故事:账户卡片 · 用量常驻区(套餐额度条与 API 余额展示与明细气泡)(优先级:P1)
作为桌面端用户,我希望登录后账户卡片上部常驻展示用量信息(套餐两根额度条——滚动月与自然周——外加套餐到期日与计费结论行,以及 API 余额一行;明细与预警收进 hover ⓘ 气泡),以便我随时知道:套餐能用到什么时候、当前是哪个限额不足、以及现在的请求到底在扣套餐积分还是扣 API 余额,而不必点击任何入口。
为什么是这个优先级:账户卡片是桌面端侧边栏核心运营入口;把用量从「点击浮层」升级为「常驻可见」是账户卡片的核心变化(P1);hover 明细气泡是其中唯一的明细载体。
独立测试:desktop 登录态下发含 billing/apiQuota 的 desktopAccountInfo,验证常驻区渲染套餐两根条(各维余量百分比 + progressbar role)、到期日与 API 余额「¥8,846.86」+ ⓘ;切换「不限制(limit = null)/ 不可用(limit = 0)/ 已用尽(reason = month / week)/ 已到期(reason = no_plan)/ 阻断(mode = blocked,三个 code)」各态验证条与结论行;table-driven 遍历每个已知 reason / code 断言结论行文案非空,再各给一个规格外取值(reason 与 blocked 的 code)断言渲染兜底文案 + 色调、且不再是「什么都不画 / 空文案红框」(见场景 23);hover ⓘ 弹明细气泡、移出自动收起、点击金额文本不触发。
验收场景:
- 假设当前请求判定走套餐(
billing.mode = "plan")且两维限额均为正数,当查看账户卡片,则套餐块显示标题「套餐用量」+ 两根额度条(本月 = 滚动月 / 本周 = 自然周),每根条按自己那维的窗口量显示余量百分比max(0, round((1 − used / limit) × 100))(分母为该维限额、分子为该维窗口内用量,均由宿主下发,客户端不自行推算窗口);每根条带progressbarrole、aria-valuenow= 该维余量百分比;无结论行。 - 假设某维
limit = null(管理员未设置 = 不限制),当查看该维,则该维不画条、显示「无额度限制」;另一维照常。(不得把null当0—— 会把不限额的企业误报成「不可用」。) - 假设某维
limit = 0(显式 0 = 该维度不可用)且billing.reason = "dimension_unavailable",当查看该维,则该维不画条、置灰显示「不可用」(不是「已用尽」,也不是「无额度限制」),并出现结论行「套餐额度不可用(限额为 0),当前按 API 余额计费」。 - 假设本人本滚动月用量触顶(
billing.reason = "month"),当查看月条,则月条显示空条 +「已用尽」(不显示「0%」——触顶只是该窗口用完、下个窗口恢复),周条照常,结论行「本月额度已用尽,当前按 API 余额计费」。 - 假设本人本自然周用量触顶(
billing.reason = "week",月未触顶),当查看周条,则周条显示空条 +「已用尽」,月条照常,结论行「本周额度已用尽,当前按 API 余额计费」。 - 假设
billing.reason = "no_plan"且billing.plan带expireDate(存在已到期的套餐订单),当查看套餐块,则套餐块保留并显示到期日与结论行「套餐已到期(<expireDate>到期),当前按 API 余额计费」(不再整块消失)。「已到期订单」的判据以服务端为准:type='plan'且status='active'(未作废)且end_date <今日,与生效分支同谓词——已作废订单不参与,全部套餐订单已作废时视同「无套餐」(plan = null,落到场景 10 的兜底)。 - 假设
billing.reason = "enterprise"(企业维度本期额度触顶),当查看套餐块,则本人两根条照常显示个人读数(企业池触顶不改个人数字),并渲染结论行「企业本期额度已用尽,当前按 API 余额计费」,色调为琥珀预警(与月/周同族:下个窗口自动恢复,不属「需人工干预」的错误色)。2026-09-22 口径变更:原先(2026-09-21)此原因不渲染结论行,理由是「漏提示优于误提示」——该理由已判定站不住:说「企业本期额度已用尽」是事实,且与个人两根条不冲突(条 = 个人读数,结论行 = 计费结论);真正有误读风险的是把企业池的数字画成个人的条,不是这句话本身。当时(不渲染时)成员看到的是「个人两根条健康 + API 余额在掉」而毫无解释(缺陷单 3479183306383616 据此报障)。本次只补呈现层,不动计费口径(企业池与个人条之和的折算摩擦属产品口径、不在本场景范围)。 - 假设
billing.mode = "blocked"(套餐与 API 均不可用;code ∈ EXPIRED_NO_API / USER_QUOTA_ZERO / TEAM_QUOTA_ZERO),当查看账户卡片,则结论行复用 proxy 402 的同一句文案(按 code 三选一:EXPIRED_NO_API=「您订购的套餐已过期,无法使用本产品!」/USER_QUOTA_ZERO=「您的 API 额度已用完,请联系公司管理员分配额度后使用!」/TEAM_QUOTA_ZERO=「团队 API 额度已用完,请联系公司管理员充值后使用!」);blocked不下发plan四数,故此时套餐块只有该结论行。 - 假设套餐块渲染且
billing.plan带expireDate,当查看标题行,则右侧显示套餐到期日(YYYY-MM-DD 到期)。 - 假设
billing.plan为null(无生效套餐、也没有未作废的已到期订单),当查看账户卡片,则不渲染套餐块(未购买的企业无法登录桌面端,实际不出现;全部套餐订单已作废同样落在这里——作废不是到期,不得拿作废单的截止日渲染「套餐已到期」;仅作契约兜底)。 - 假设宿主下发了
apiQuota(限额数字、余额充足:剩余 ≥ 限额 20%),当查看 API 余额行,则行内展示标签「API 余额」+ 金额「¥N」(正常色)+ ⓘ;金额格式 = ¥ 前缀 + 两位小数 + 千位分隔;金额不带「剩余」前缀(label 已含「余额」语义),随账户消息实时刷新。 - 假设API 余额不足(剩余 < 限额 20% 且 > 0),当查看 API 余额行,则金额以预警色(琥珀,参考 #d18616,最终色值以当前主题变量/视觉代码为准)展示,行内其余元素不变色。
- 假设API 余额用完(剩余 ≤ 0),当查看 API 余额行,则行内以错误警示色(主题变量,如
--vscode-errorForeground)显示「已用完」。 - 假设API 不限额(
apiQuota.limit = null),当查看 API 余额行,则行内显示「不限额」(正常色)+ ⓘ,不显示金额。 - 假设API 余额行内容放得下,当查看其布局,则标签与「金额+ⓘ」同行;容器放不下时金额组整体换行到下一行(完整一行不拆两段、不溢出卡片、右对齐),不得把金额与 ⓘ 拆散。
- 假设用户 hover 或键盘 Tab 聚焦 API 余额行的 ⓘ 图标(
data-testid="api-quota-info",独立按钮,aria-label="API 余额明细"),当气泡弹出,则气泡标题「API 余额」+ 两行「已用 ¥n / 剩余 ¥N」,金额右对齐且等宽数字(便于比较);气泡贴着「API 余额」行顶向上弹出(间距约 4px),宽度与卡片等宽,盖住上方套餐用量区。 - 假设API 不限额,当hover ⓘ 弹出气泡,则气泡两行显示「已用 ¥n」(本人累计)与剩余行「不限额」。
- 假设API 余额不足(剩余 < 限额 20%),当hover ⓘ 弹出气泡,则气泡内附加琥珀色提示「余额不足20%,建议及时充值」。
- 假设API 余额用完,当hover ⓘ 弹出气泡,则气泡内剩余金额红字 ¥0.00 + 红色提示「额度已用完,请联系管理员充值」。
- 假设气泡已展开,当鼠标移出 ⓘ 图标与气泡区域约 150ms,则气泡自动收起(防图标↔气泡间移动闪烁);按
Esc或点击其他区域则立即强制收起。 - 假设用户点击 API 余额行的金额文本,当点击发生,则不触发气泡(金额区域非热区,仅 ⓘ 为热区),也不触发其他行为。
- 假设宿主下发
billing与apiQuota均为空/未下发,当查看账户卡片,则用量常驻区整体不渲染,个人信息行右侧也不显示用量显隐按钮。 - 假设宿主下发的
billing.reason是规格外取值(服务端先于客户端发版新增的枚举值),当渲染账户卡片,则结论行照常渲染「套餐额度当前不可用,按 API 余额计费」(琥珀预警)——不得静默不渲染(原先switch无default,未识别的reason会隐式返回、结论行整块消失,用户只看到「本人两根条健康 + API 余额在掉」而零解释,与场景 7 的缺陷单 3479183306383616 同款失效);假设宿主下发的billing.code是规格外取值(mode = "blocked"),当渲染账户卡片,则渲染兜底阻断文案「当前用量受限,请联系公司管理员」(错误色)——不得渲染一条没有文案的结论行(空红框:查表得undefined但元素照挂);两处均按客户端既有约定console.warn记一条未知枚举日志(不新建日志/打点基建)。2026-09-22 追加:兜底文案「说少而准」——结论行要回答的是「为什么在扣 API」,而这件事只需mode = "api"就能确定,故不猜是哪一池(月/周/企业)用完;色调沿用本规格既有的「会不会自愈」分档(见场景 3–7 的说明):未知原因不预设「需人工干预」(那是错误色那档的语义),给琥珀,避免无谓制造焦虑。起因:客户端遇到不认识的枚举就静默是同一类失效——09-22 修掉enterprise(场景 7)后复核发现同类还有这两处,本次一并收口;根因是两端不同机发布(服务端先加枚举、客户端旧版本不认识),故除运行期兜底外,reason的switch另加default分支做编译期穷尽性检查(const _exhaustive: never = billing.reason,仓内新增AccountBillingDegradeReason却忘了处理时直接编译失败——但它拦不住跨仓先发,两者都要有)。
用户故事:账户卡片 · 用量常驻区收起/展开(优先级:P2)
作为桌面端用户,我希望用量常驻区可以收起/展开,且显隐状态独立记忆、不受个人信息菜单开合影响,以便在只想看账户身份时把卡片收成一行。
为什么是这个优先级:空间收纳是常驻区的辅助能力,先保证常驻展示正确(P1 故事)后再谈收起;对密集使用多会话/远程主机的用户有用。
独立测试:登录态点显隐按钮收起用量区,验证按钮 icon 变仪表盘;再点展开恢复;收起状态下打开/关闭个人信息菜单验证用量区显隐不受影响;billing+apiQuota 均未下发时验证显隐按钮不渲染。
验收场景:
- 假设用量常驻区展开,当点击个人信息行右侧显隐按钮(icon:chevron-up,
aria-label="收起用量"),则用量常驻区收起,卡片仅剩个人信息一行,按钮 icon 变为仪表盘(dashboard,aria-label="展开用量")。 - 假设用量常驻区已收起,当再次点击显隐按钮,则用量常驻区展开恢复收起前的展示,按钮 icon 恢复为 chevron-up。
- 假设用量常驻区收起/展开状态确定,当打开或关闭个人信息菜单、切换会话、账户数据刷新,则显隐状态保持不变(独立记忆,不被菜单开合联动)。
- 假设用量区展开且个人信息菜单打开,当点击显隐按钮收起用量区,则仅收起用量区,个人信息菜单保持打开状态不受影响(显隐与菜单解耦)。
用户故事:账户卡片 · 个人信息行与纯功能菜单(优先级:P1)
作为桌面端用户,我希望点击头像/姓名热区弹出纯功能菜单(设置/企业控制台/帮助文档/退出登录),菜单贴着个人信息行向上弹出、与卡片等宽,再次点击热区或失焦/Esc 可收起,以便操作账户相关功能且不被用量信息干扰。
为什么是这个优先级:这是账户卡片新模型对既有「更多按钮 + 用量浮层」双入口的结构性替换(已登录不再有独立「…」更多按钮),是账户卡片的核心差异,必须最先明确(P1)。
独立测试:登录态验证热区点击开/关菜单、菜单含 4 项且无用量块、贴行弹出盖住用量区、与卡片等宽(右缘对齐);点击外部与 Esc 收起;方向键/Enter/Space 键盘操作;未登录态验证整条登录按钮 + 更多按钮仍保留;远程主机标注。
验收场景:
- 假设用户未登录,当查看账户卡片,则显示整条「登录」按钮(占满卡片宽度)+ 右侧「更多」(…)按钮;点登录走现有 SSO 流程;「更多」菜单含登录项。
- 假设用户已登录,当查看个人信息行,则显示头像(邮箱前缀首字母圆形占位)+ 姓名(邮箱前缀;无邮箱显示「已登录」),行右侧为更新按钮(有更新时)+ 用量显隐按钮;不显示独立「…」更多按钮。
- 假设已登录用户点击头像/姓名热区(
data-testid="account-card-hotzone",热区aria-expanded反映菜单开合,支持 Enter/空格开合),当菜单展开,则菜单纯功能、仅 4 项:「设置 / 企业控制台(↗)/ 帮助文档(↗)/ 退出登录」(退出登录为危险色 + 上方分隔线),不含任何用量信息。 - 假设菜单打开,当查看菜单位置与尺寸,则菜单贴着个人信息行顶部向上弹出(底部与个人信息行间距 0),直接盖住上方用量常驻区;宽度与账户卡片等宽(右缘对齐,实测误差 ≤ 1px),内容自动换行不超出面板。
- 假设菜单已打开,当再次点击个人信息热区,则菜单收起(toggle);用量常驻区显隐不受影响(解耦)。
- 假设菜单已打开,当点击菜单外部任意区域或按
Esc,则菜单收起(焦点回到触发热区)。 - 假设菜单已打开,当使用键盘(方向键在菜单项间移动焦点、Enter/Space 激活、Esc 关闭——roving tabindex),则焦点按方向键循环移动,激活项执行对应行为。
- 假设用户选择菜单项,当点击「设置」,则打开设置页面;点击「企业控制台(↗)」则打开对应网址(新标签页/外部);点击「帮助文档(↗)」则浏览器新标签页打开
serverUrl + /docs;点击「退出登录」则执行登出并回到未登录卡片态。 - 假设账户卡片处于远程主机会话(host 非 local),当查看菜单,则「设置 / 退出登录」项旁标注主机名(如「设置(prod)」),本地主机不标注;用量与菜单登录项跟随聚焦分屏所属主机。
用户故事:账户卡片 · 更新按钮状态机 S0–S6(优先级:P2)
作为桌面端用户,我希望有新版本时在个人信息行右侧出现「更新」按钮,走「下载二次确认 → 下载中禁用 → 就绪自动弹重启确认」状态机,由我决定重启时机,以便在知情的前提下更新应用而不被自动重启打断。
为什么是这个优先级:按钮状态机依赖宿主下发 update.status 并响应下载/重启命令,涉及 desktop 主进程改动(宿主须把 electron-updater 事件映射为 update.status 推送并响应 desktopUpdateDownload/desktopUpdateRestart 命令);宿主侧就绪前按钮无数据可渲染,故整体排 P2(在 P1 的用量常驻/菜单稳定后接线)。webview 侧状态机与宿主接线均属本规格验收范围。
独立测试:webview 侧下发 update: { available: true, version: "1.2.0", status: "idle" },验证 S1 按钮「更新」→ 点击弹 S2 确认框 → 确认后发 desktopUpdateDownload 且按钮进入「正在下载更新…」disabled;模拟宿主把 status 改为 ready,验证自动弹 S4 重启确认(仅一次)、[稍后] 转「重启」按钮、[立即重启] 发 desktopUpdateRestart;Esc 关闭 S2 后不下载;status 回退 idle 时按钮恢复「更新」。
验收场景:
- 假设无更新(
update.available: false或不带update),当查看个人信息行,则不显示更新按钮(S0)。 - 假设检测到新版本(
available: true且status: "idle"),当查看个人信息行,则更新按钮显示「更新」(S1),点击进入 S2。 - 假设用户点击「更新」,当 S2 下载二次确认对话框弹出,则标题「更新到新版本」,正文含版本号
vX.Y.Z与「安装完成后由你选择重启时机,不会自动重启客户端」语义,按钮「取消 / 下载更新」;仅Esc可关闭(点对话框外部不关闭)。 - 假设用户在 S2 点击「下载更新」,当确认下载,则 webview 向宿主发送
desktopUpdateDownload命令,更新按钮变为「正在下载更新…」并 disabled(防重复下载,点击无反应,S3)。 - 假设宿主将
update.status推送为"ready",当 webview 收到新快照,则自动弹出「重启以完成更新」确认框(S4),按钮「稍后 / 立即重启」,正文提示「重启会中断正在运行的任务,建议先保存工作」;不自动重启;该弹框每轮就绪只自动弹一次(防重复弹窗)。 - 假设用户在 S4 点击「稍后」,当对话框关闭,则更新按钮文案变为「重启」(S5),期间应用可正常使用;再次点击按钮重新弹出 S4 确认框。
- 假设用户在 S4/S5 点击「立即重启」,当确认重启,则 webview 向宿主发送
desktopUpdateRestart命令,更新状态复位、按钮消失(S6);是否实际退出安装由宿主处理。 - 假设下载过程中宿主推送
status: "idle"(下载失败/取消),当按钮状态刷新,则按钮恢复「更新」文案与可点状态;重新点击回到 S2 确认流程。 - 假设更新按钮处于下载中(disabled),当用户点击它,则无任何反应,不弹对话框、不发命令。
边界情况
- 未登录:账户卡片仅登录按钮 + 更多按钮;无用量区、无个人信息行、无更新提示(用量区不渲染时显隐按钮也不显示)。
- 未配置套餐 / 均未下发:
billing.plan为null(无生效套餐、且没有未作废的已到期订单——从未购买、或订单已全部作废)时套餐块不渲染;billing与apiQuota均未下发时用量区整体不渲染、显隐按钮不显示。 - 套餐三态别混:
limit = null(不限制)与limit = 0(不可用)是两种状态,都不画条,但文案与视觉必须区分;触顶(该维used ≥ limit)读「已用尽」(空条),不读「0%」——触顶只是该窗口用完、下窗口恢复,与「不可用 / 被拒」语义不同。旧「整单大池子」口径(monthlyQuota × months)随旧plan字段一并退出卡片:后端仍保留旧字段兼容已发布客户端,新卡片不再取数。 - 结论行优先级:
blocked> 不可用(dimension_unavailable)> 已用尽(月优先于周)> 无结论行;enterprise照渲染(企业本期额度触顶 ⇒ 琥珀预警,与月/周同档;2026-09-22 起,原先刻意不渲染)。 - 枚举是开放集合(服务端可能先于客户端发版):
billing.reason/billing.code的新取值由服务端先上线、已发布的客户端不认识(两端不同机发布)。客户端必须对未识别取值给兜底结论行(文案与色调见场景 23),不得静默;兜底文案不含对具体原因的猜测(不点名月/周/企业哪一池用完)。spec 背书、因此维持不渲染的三种情形(本次不碰、也不是缺陷):mode = "plan"(套餐可用,无话可说)、reason = "no_plan"且plan = null(从未购买或订单已全部作废,无到期日可讲)、billing与apiQuota双缺(用量区整体不渲染)。 - API 余额:行内默认仅余额金额或状态短词,明细/预警收进 ⓘ 气泡;剩余 < 限额 20% 金额预警色;剩余 ≤ 0 显示「已用完」错误色;
limit = null显示「不限额」。 - 下载中:更新按钮 disabled,点击无反应。
- 对话框:S2 仅 Esc 关闭;S4 稍后后可再次弹确认;均不自动关闭/不自动重启。
- 远程主机:个人信息菜单「设置 / 退出登录」标注主机名;用量与登录态跟随聚焦分屏所属主机。
- 无差异保存:用户偏好值与当前生效值相同(未改任何值)时点「保存」,仍落盘并回执成功 toast,但不重建任何会话、不清空排队消息(≠ 配置变更)。
- 未敲
/reload-plugins的后果:插件类变更落盘后不做任何自动重建、也不自动重载;既有对话持续用旧配置,新建对话按新配置生效;用户若要让某个既有对话用上新配置,敲/reload-plugins即可(无需新开对话)。不因重启客户端之外的无关操作而静默改变行为。 - 重载与流式/权限并存:敲
/reload-plugins时会话若正在生成回复、存在待确认权限或运行中后台任务,重载照常执行、不打断该回合(与已废止的重建路径相反——后者在同样情形下不得重建该会话)。 - 用量轮询失败:保留上次成功显示。
数据契约与命令
desktopAccountInfo(窗口级推送,无 paneId)
update? 字段承载更新按钮状态机输入(S0–S6),完整契约如下:
ts
export interface AccountUpdateInfo {
available: boolean; // 有新版本
version?: string; // 新版本号(对话框/按钮 tooltip 文案使用)
status?: "idle" | "downloading" | "ready"; // 更新过程状态
}
/** 套餐两根额度条的四个数 + 到期日(codechat `GET /api/v1/account` → `billing.plan`). */
export interface AccountBillingPlanUsage {
monthUsed: number;
monthLimit: number | null; // 滚动月限额;null = 不限制,0 = 该维度不可用
weekUsed: number;
weekLimit: number | null; // 自然周限额;null = 不限制,0 = 该维度不可用
expireDate: string; // 套餐到期日(YYYY-MM-DD)
}
/**
* 降级原因(客户端据此选结论行文案;枚举由后端定义,见 codechat spec 场景 5–11).
* 五个原因**各有结论行**(`enterprise` 自 2026-09-22 起也渲染,见场景 7);唯一
* 「不渲染」的组合是 `no_plan` 且 `plan = null`(从未购买)。
* 该类型只是**客户端侧的枚举快照**:服务端可能先发版新增取值,客户端必须对规格外
* 取值渲染兜底结论行(见场景 23),不得静默。
*/
export type AccountBillingDegradeReason =
| "month"
| "week"
| "enterprise"
| "no_plan"
| "dimension_unavailable";
/** 阻断码(客户端按 code 复用 proxy 402 文案;同样只是枚举快照,规格外取值走场景 23 的兜底文案). */
export type AccountBillingCode =
| "EXPIRED_NO_API"
| "USER_QUOTA_ZERO"
| "TEAM_QUOTA_ZERO";
/**
* 计费结论(codechat 形态甲:闸口镜像)。判定出自后端 `getBillingVerdict`——与 proxy 扣费
* **同一份代码**;客户端只渲染、**不自行判断「走套餐还是走 API」**(旧方案由客户端比较数字,
* 已因限额「0」语义翻转发生过一次静默失效)。`plan` 子对象:有生效套餐 = 四数 + 到期日;
* 无生效套餐但有**未作废**的已到期订单 = 仅 `{ expireDate }`;从未购买、或订单已全部作废 = null。
*/
export type AccountBillingInfo =
| { mode: "plan"; plan: AccountBillingPlanUsage }
| {
mode: "api";
reason: AccountBillingDegradeReason;
plan: AccountBillingPlanUsage | { expireDate: string } | null;
}
| { mode: "blocked"; code: AccountBillingCode };
export interface DesktopAccountInfoMessage {
command: "desktopAccountInfo";
isAuthenticated: boolean;
user?: { id: string; email?: string } | null;
billing?: AccountBillingInfo | null; // 套餐两根条 + 到期日 + 计费结论
apiQuota?: AccountApiQuotaInfo | null; // { limit: number|null, used }
update?: AccountUpdateInfo | null; // 应用更新状态(S0–S6 输入)
}下发规则:
- 未登录:
isAuthenticated: false,billing/apiQuota/update可不带。 - 无生效套餐、且没有未作废的已到期订单(从未购买,或订单已全部作废):
billing.plan: null(套餐块不渲染);billing与apiQuota均未下发时用量区整体不渲染、显隐按钮不显示。 - API 不限额:
apiQuota.limit: null(共用团队余额)。 - 无更新:不带
update或update.available: false。 - 账户/用量/结论/更新状态变化时宿主主动推送新快照;轮询失败保留上次成功数据。
- 宿主只做透传:
billing由 codechatGET /api/v1/account下发,经 CLIgetAccountInfo→ desktopHostaccountCache原样带到卡片;宿主不重算任何判定或窗口(窗口起点、限额聚合口径都在服务端,客户端一无所知)。
套餐额度条计算(按余量计)
barPercent(used, limit) = max(0, round((1 − used / limit) × 100)) // limit > 0- 每根条用自己那维的数(月条 = monthUsed/monthLimit,周条 = weekUsed/weekLimit);
limit = null或0时不画条(分别显示「无额度限制」「不可用」)。 - 100% = 满(未使用);触顶时该条显示「已用尽」(空条),不显示 0%。
命令契约
desktopUpdateDownload(webview → host):用户确认 S2 后通知宿主开始下载。desktopUpdateRestart(webview → host):用户在 S4/S5 选择立即重启后通知宿主安装并重启。- 宿主接线:webview 侧 S0–S6 状态机调用上述两个命令;宿主(desktopHost.ts)把 electron-updater 事件映射为
update.status随desktopAccountInfo推送并响应两命令;toast 更新链路不提供(autoDownload=false,下载仅在 S2 确认后启动)。触发检查的时机(启动自动 / 手动 / 运行期固定间隔轮询,见 desktop-shell.md「桌面端自动更新」场景 11)不改变状态机语义——任一时机发现新版本都只置 idle(S1)。降级边界见 desktop-shell.md「桌面端自动更新」故事场景 5。
非目标(明确排除)
- 设置页其余 2 个导航项的完整功能:钩子/MCP 服务本批仅占位(「即将推出」),不提供可编辑控件。
- 设置页在 IDE 宿主的高保真容器差异:VSCE/JetBrains 的设置标签页 webview 内容与 desktop 设置页一致,但容器(编辑器区域标签页)为宿主自身形态,不做 desktop 全页面式布局。
- 上下文压缩的自动触发:压缩仅由用户点击按钮或
/compact命令触发,不引入自动压缩策略;用量百分比仅展示,不做压缩建议弹窗。 - 账户卡片的通知语义:账户卡片不含 toast/通知语义(信息类 toast 自动消失、带操作按钮 toast 不自动消失等通知行为由会话/后台通知故事承载,桌面端更新提醒由「更新按钮状态机」全权接管)。
- 用户偏好不落桌面本地配置:设置页「全局设置」/「个性化」的用户偏好不写入桌面
wave-desktop.json、不作为AgentOptions会话级覆盖项下发,落点唯一为用户级~/.wave/settings.json(theme/updateChannel例外:仅存桌面 configStore,不进会话)。 - 保存不涉及会话重建的实现机制:本规格只约束「保存路径与生效时机」的外部行为(立即回执、不重建、必要时确认),不规定 SDK 侧热重载的内部实现(监听/快照等属 agent-config 规格范围)。
- 「待应用」的持久可见标识:插件变更后不提供常驻标识(无状态条、无列表角标、无会话标记),仅一次性提示(文案为「插件已变更。运行 /reload-plugins 使其生效。」,见 plugin.md「插件变更提示」);不提供「查看哪些对话待应用」的入口与查询面,也不做惰性重建(2026-09-10 拍板;2026-09-18 后「待应用」即等于「等用户敲 /reload-plugins」)。
- 不猜未识别的降级原因、也不扩张「静默」的范围:对规格外的
billing.reason,卡片只说明「套餐额度当前不可用,按 API 余额计费」,不猜测哪一池(月/周/企业)用完、也不据此推断恢复时间或提示用户找谁(见场景 23)。同时明确:spec 背书的静默仍是静默——mode = 'plan'(无话可说)、no_plan且plan = null(无到期日可讲)、billing/apiQuota双缺(用量区整体不渲染)这三种维持不渲染,本次兜底只针对「客户端不认识服务端下发的取值」这一类失效(2026-09-22)。