Appearance
功能规格说明:桌面端右侧面板
本规格描述右侧面板这个槽位本身:tab 栏怎么组织、怎么开合、占多少宽度、里面各个实例的生命周期。槽位里承载的差异面板、文件面板、终端面板、预览面板各有自己的规格(desktop-diff-panel.md、desktop-file-panel.md、desktop-terminal-panel.md、desktop-preview.md)。
用户场景与测试
用户故事:右侧面板 · 一级 Tab 栏与实例语义(优先级:P1)
作为桌面端用户,我希望右侧面板采用浏览器式一级 Tab 栏:每个打开的实例对应一个 tab,preview 可同时开多个(每次从链接/「+」打开都新增一个),而计划/差异/终端/文件面板同一时刻至多一个实例(重复打开时激活已有 tab 而非新建),以便同时对照多个预览页面,又不让面板类型重复堆积。
为什么是这个优先级:一级 Tab 栏是面板 tab 化模型的骨架语义:仅 preview 为多实例(tryOpenPanel:preview → addTab,其余 → ensureUniqueTab;文件面板同样是唯一 file tab,多个文件在面板内部以文件标签承载,见 desktop-file-panel.md「文件面板已打开文件的标签」),tests 与代码注释均以「仅 preview 多实例」为准——本故事以该实例语义为验收目标,必须先定(P1)。
独立测试:preview 连续从两条消息 localhost 链接打开验证新增两个 tab 且各自独立加载、tab 标签用页面标题;plan/diff/terminal/file 重复打开验证激活已有 tab 不新增;点击 tab 切换;关闭激活 tab 落到左侧邻居;关闭最后一个 tab 时右侧面板一并收起(不显示空态);键盘 ←/→ 在 tab 间移动;tab 溢出可横向滚动;tab 标签不显示文件路径/文件名细节。
验收场景:
- 假设本地会话中点击消息中的 localhost 链接,当预览面板打开,则每次点击都在 tab 栏新增一个 preview tab(
data-panel-tab)并激活,新页面在槽位加载;再次点击同一链接或不同链接均新增(多实例语义),不覆盖既有 tab。 - 假设远程会话中点击 localhost 链接,当预览面板打开,则若该链接的规范化 URL(同一 origin/路径、任意回环主机拼写)已有建立隧道的 preview tab 且隧道无错误,则激活该既有 tab、不新建(隧道复用、不重建);路径/来源不同或隧道失败则新增 tab。
- 假设preview tab 打开中,当查看 tab 标签,则显示 guest 页面标题(页面 title 更新事件驱动),无标题时回退「host+path」,空地址回退「新预览」;不显示完整 URL。
- 假设用户通过「+」菜单或空态入口打开 plan/diff/terminal/file,当该类面板已有打开实例,则只激活既有 tab(
ensureUniqueTab),不新增重复实例;无实例时才新建。 - 假设消息中出现文件路径点击(desktop 宿主),当文件面板未打开,则新增唯一 file tab 并立即显示 loading 桩,宿主读取后回填内容(
desktopFileContent链路)。 - 假设file tab 已打开且显示文件 A,当点击另一条消息的文件 B 路径,则激活既有 file tab(不新建),并在面板内按 desktop-file-panel.md「文件面板已打开文件的标签」打开 B(新增一个文件标签,或激活已打开的同名标签),A 的标签保留;再次点击当前文件路径则激活该标签并重新读取刷新。
- 假设file tab 存在,当查看一级 tab 栏标签,则file tab 标签只显示面板名「文件」——不泄露具体文件名(文件名只出现在 FilePane 内部的文件标签栏与工具条路径中)。
- 假设存在多个 tab,当点击某个 tab,则切换激活并展示对应面板内容;被切换走的 tab 保持挂载(
display:none),其 preview guest / 终端 PTY / 文件内容不销毁。 - 假设存在多个 tab,当关闭非激活 tab,则仅移除该实例(browser-tab 语义:preview guest 销毁、重开是全新加载;终端 PTY 为例外,见「面板关闭销毁」边界);关闭激活 tab 时激活其左侧相邻 tab。
- 假设用户关闭最后一个 tab(tab 关闭按钮,或在头部「面板」菜单中取消勾选最后一个类型),当关闭完成,则右侧面板随之收起(聊天区恢复全宽),不显示空态引导——「已展开但没有任何 tab」的空态只由用户显式展开面板触发(见「空态面板与+新增入口」);preview 全屏状态同时退出;收起后槽位宽度按会话记忆保留,再次展开按自动 40/60 分配或沿用已手动设定的宽度。
- 假设tab 栏存在,当按
←/→方向键,则焦点在 tab 间循环移动(roving tabindex,Enter/Space 激活);tab 栏聚焦时不与其它快捷键冲突。 - 假设tab 总宽超出槽位宽度,当查看 tab 栏,则可横向滚动浏览全部 tab(「+」按钮粘在可视右缘);新增 tab 后滚动到新 tab 可见。
- 假设preview tab 处于激活且用户点击 tab 栏全屏按钮,当进入全屏,则右侧面板占满内容区、聊天列隐藏;再次点击或按 Esc 退出全屏恢复(全屏语义见 desktop-preview.md「预览面板全屏」,本故事只规定全屏按钮位于 tab 栏右端、作用于整个槽位)。
- 假设某会话的 preview tab 内曾通过地址栏手动输入 URL 或 guest 页面内导航到达新地址,当切换到其它会话再切回该会话(或该 tab 所在面板分屏移动重建),则预览必须恢复该会话最后实际浏览的地址并自动加载——地址栏输入与页内跳转后的地址与链接打开/转发回复的地址同级持久(随会话面板记忆保存,含 preview 空 tab 首次手动输入的场景),不得回到最初请求的 URL、清空或显示空白,避免用户每次切回都要重新输入。(2026-10 补充) 该 guest 仍处于保活状态时直接复用活页面、不重载(更优路径);只有它已被销毁(关闭 tab/删除会话/关闭分屏/超出保活上限被淘汰)时才按上述记忆地址重新加载——保活语义与上限见「右侧面板 · preview guest 跨会话保活」。
用户故事:右侧面板 · 空态面板与「+」新增入口(优先级:P1)
作为桌面端用户,我希望在没有任何面板时显式展开右侧面板(头部面板按钮)能看到空态引导(五个能力入口卡片),并能从 tab 栏「+」按钮的菜单选择要打开的面板类型,以便首次使用时不迷失入口。
为什么是这个优先级:空态与「+」是单槽位 + tab 化后唯一的显式开面板入口(旧的多选并排模型已被取代),是面板改造可用性的关键(P1)。
独立测试:桌面端展开面板且无 tab 时验证空态标题/副标题与五个入口(preview/plan/diff/terminal/file)网格、图标与快捷键提示;点击各入口验证对应打开语义;无 workdir 的会话验证 diff/terminal 入口 disabled;「+」菜单验证 checklist 语义(preview 无勾选框、其余勾选反映是否已开、点击行为)。
验收场景:
- 假设用户显式展开右侧面板(头部面板按钮,或菜单栏「面板」菜单)且当前没有任何打开的 tab,当查看槽位,则显示空态(
data-testid="panel-empty-state"):标题「还没有打开任何面板」+ 副标题「点击下方功能页打开对应面板」+ 五个能力入口按钮网格(preview/plan/diff/terminal/file,含图标、面板名与快捷键提示,data-testid="panel-empty-item-<kind>");关闭最后一个 tab 不会进入该状态(面板一并收起,见「一级 Tab 栏与实例语义」场景 10)。 - 假设空态五个入口各自对应能力描述(预览网页应用/查看实施计划/查看代码改动/打开终端/查看文件内容),当 hover 或查看,则入口提供对应描述(title 或可见文案)。
- 假设某类面板当前不可用(如无 workdir 的会话中 diff/terminal),当查看空态,则对应入口 disabled 并提示「当前会话不可用」,点击无效果。
- 假设用户点击空态某入口,当点击发生,则preview 新增一个空 preview tab;plan/diff/terminal/file 走 open-or-activate(已有则激活,无则新建);空间不足被拒时(见「展开/折叠、空间守卫与欢迎页共存」故事场景 3)给出轻量提示且不开任何 tab。
- 假设tab 栏存在,当点击「+」(
data-testid="panel-tabs-add"),则弹出面板类型菜单:五种类型齐全;preview 项无勾选框(多实例随时可加);其余项以勾选反映「该类型当前有打开实例」;菜单打开时锚定「+」按钮左缘对齐,右侧贴边时钳制不溢出。 - 假设「+」菜单中点选某类型,当点击菜单项,则菜单关闭并执行对应语义:preview 新增空 preview tab(其地址栏供输入 URL);plan/diff/terminal/file open-or-activate 唯一实例;disabled 类型项不可点。
- 假设用户从消息内容触发面板打开(链接/路径/plan 内容/diff),当槽位处于收起或无 tab 状态,则打开语义自动展开面板并新建/激活对应 tab(调起即优先显示),无需先手动展开。
- 假设某 preview tab 被关闭后,当用户再次打开 preview,则为全新实例(guest 重新加载,browser-tab 关闭语义);关闭 terminal tab 仅移除视图、宿主 PTY 不销毁(重开重挂接,见「内嵌终端面板」场景 4 与「面板关闭销毁」边界)。
- 假设右侧面板槽位展开但没有任何打开的 tab(空态),当用户拖拽槽位左缘分割线调整宽度,则与打开具体面板时行为一致:宽度实时跟随调整、钳制在 [202, 容器 − 对话列最小宽度],并转为「手动宽度」(后续折叠/再展开/换开其它面板均保持,见「展开/折叠、空间守卫与欢迎页共存」故事场景 5/8)——空态只是槽位的无 tab 形态,分割线拖拽入口不得缺失。
- 假设右侧面板槽位较窄(拖拽到接近下限,两列已放不下舒适宽度的入口),当查看空态,则五个入口自动由两列网格切换为单列垂直排列:按钮占满整行宽度,图标、面板名与快捷键提示完整保留,顺序与宽面板一致(预览/计划/差异/终端/文件依次自然下叠,
data-testid不变);槽位加宽越过阈值即恢复两列网格,切换随拖拽实时发生、无需刷新。
用户故事:右侧面板 · 展开/折叠、空间守卫与欢迎页共存(优先级:P2)
作为桌面端用户,我希望面板可经头部按钮展开/折叠,折叠时打开中的 tab 与宽度完整保留;窗口过窄放不下最小对话列时拒绝开面板并轻量提示;欢迎页(新对话空态)与展开的面板并存不互相遮挡错位。
为什么是这个优先级:面板展开/折叠与空间守卫决定 tab 化布局在窄窗/欢迎页下的可用性,属布局正确性兜底(P2),但欢迎页遮挡问题经既有回归修复后仍须保证 tab 化不引入回退。
独立测试:开两个 preview tab 后折叠面板,验证槽位隐藏但 tab/激活/宽度保留,再展开原样恢复;折叠状态退出全屏;宽容器(行宽 ≥ 900px)展开面板验证对话列按 40% 分配、宽于 360px 保底,窄容器(<900px)展开验证对话列停在 360px 保底(场景 7/9);窗口缩窄至对话列最小宽度不足时开面板被拒并提示;欢迎页打开面板验证不遮挡不错位(几何断言)。
验收场景:
- 假设面板已展开且存在若干 tab,当点击头部面板按钮(收起),则右侧槽位隐藏(聊天区恢复全宽),但所有 tab、激活 tab 与面板宽度保持挂载保留(
display:none);再次点击展开时原样恢复,preview guest 不重新加载、终端 PTY 不中断。 - 假设面板处于全屏状态,当用户折叠面板,则全屏同时退出(全屏与折叠互斥:折叠即退出全屏);展开不会自动重进全屏。
- 假设窗口宽度不足以让对话列保持最小宽度(
containerW − PANEL_MIN_WIDTH < CHAT_MAIN_MIN_WIDTH),当用户通过任何入口请求开面板,则拒绝开启并显示轻量提示「空间不足,无法开启面板」,不开任何 tab、不改动既有 tab。 - 假设窗口在打开面板后缩小,当行宽不足以并排容纳「对话列 360px 保底 + 面板当前宽度」,则对话列保持 360px 不被压缩,缩小压力全部落在右侧面板:面板宽度相应收窄(极端窄窗下可低于其 202px 下限——该下限只约束开启与手动拖拽动作,不约束窗口缩小),不溢出、不横向滚动、不压垮对话列。
- 假设面板宽度经拖拽调整,当调整后展开/折叠/会话切换,则宽度按会话记忆保留(随会话面板记忆保存,见场景 8),钳制范围下限不变。
- 假设欢迎页(新对话空态)显示且用户打开面板,当查看布局,则聊天主列与右侧面板在同一行内正常分配(主列
flex:1; min-width:0),欢迎内容不左偏/不溢出、输入卡不被面板盖住(既有欢迎页与面板并存的布局语义,修复后保持不回归)。 - 假设某会话的面板槽位从未被手动拖宽过(「自动 40/60 比例分配」语义,2026-09-09 拍板替代 2026-09-03 的「铺满剩余空间」——即本故事场景 7-9),当用户展开面板(从收起态/空态展开,或打开任意新 tab),则对话列与槽位按行宽比例分配:对话列取行宽的 40%(四舍五入),且不低于 360px 保底(
CHAT_MAIN_MIN_WIDTH,即行宽 < 900px 时对话列停在 360px、面板取「行宽 − 360px」),面板取行宽剩余部分。宽容器下对话列随 40% 增宽(如 1400px 行宽 → 对话列 560px / 面板 840px),不再被恒压到最小 360px,也不再停留 420px 固定默认宽度。 - 假设槽位宽度曾自动分配或处于默认宽度,当用户手动拖拽槽位边缘调整宽度,则该会话转为「手动宽度」:此后折叠再展开、关闭再打开、加新 tab、窗口尺寸变化,均不再自动按比例分配,保持用户设定宽度(钳制范围 [202, 容器 − 360] 不变,随会话记忆保留见场景 5);应用重启后随「面板宽度不跨重启」规则回到自动 40/60 比例分配默认行为。
- 假设从未手动拖宽过的会话所在窗口尺寸变化,当再次展开或打开面板,则按当前行宽重新执行 40/60 比例分配(含 360px 对话列保底,跟随窗口实时行宽);已手动拖宽的会话不受影响。
- 假设某会话折叠了面板(槽位隐藏但 tab 仍保留挂载),当切换到其它会话再切回该会话(或该 tab 所在面板分屏移动重建),则折叠/展开状态随会话面板记忆原样恢复——切回后保持折叠,不得因该会话仍打开着 tab 而自动展开(2026-09-08 拍板:折叠/展开与 tab 集合、宽度同级,逐会话记忆);目标会话自身的折叠/展开记忆互不影响。
用户故事:右侧面板 · preview guest 跨会话保活(优先级:P2)
作为桌面端用户,我希望切换会话或分屏重排时预览面板不被销毁重建:切走时页面在后台活着,切回立即还是刚才那一页(地址、页面内状态、滚动位置都在),而不是重新加载回初始态。
为什么是这个优先级:切会话是桌面端最高频动作,而现状是每次切换都把 preview guest 连同面板槽一起重建(ChatApp 会话 swap effect 的注释即写明「panel content rebuilds for the new context … preview reloads」)——页面重载、页面内状态归零、dev server 重新握手;设计师走查原型这类「多会话来回切、又要求预览停在同一页」的场景尤其刺眼(原型页面自身的工具条状态被重置即由此而来)。参考实现 Claude 桌面端(Browser pane)即保活语义:切走只把视图移出可视区停驻(必要时以截图占位),仅预览 tab 关闭/会话删除/预览服务停止时销毁。本故事把该语义落到 Wave 的 preview 上(P2:不改面板模型与实例语义,只改 guest 生命周期)。范围只含 preview:diff 按新会话上下文重取、terminal PTY 随会话切换终止(见「内嵌终端面板」场景 6)等其余语义一律不变。
独立测试:会话 A 打开 preview 并在页面内产生状态,切到会话 B 再切回 A,断言 guest 未重建(无重新加载、地址/页面内状态/滚动位置保持);关闭 tab、删除会话、关闭分屏、超出保活上限被淘汰后重新打开,断言为全新加载;连续保活第 5 个 guest 时断言最久未用者被销毁;折叠面板、切一级 tab、进出全屏预览、分屏移动重建后断言均不重载。
验收场景:
- 假设会话 A 的某 preview tab 已加载页面并产生了页面内状态(地址栏/页内导航后的地址、页面自身状态、滚动位置),当用户切到同分屏的会话 B,则 A 的 guest 必须保活(离开可视区停驻、不销毁);切回 A 时立即显示原页面——不得重新加载、不得回到初始 URL 或初始状态。
- 假设preview guest 处于保活(不可见)状态,当其所属会话重新可见(侧边栏切回、分屏移动后重建、折叠面板后展开),则必须复用同一 guest 不重载,并按恢复后的实际面板宽度重新计算缩放(沿用 desktop-preview.md「localhost 原型预览」场景 8)。
- 假设保活中的 preview guest 已达上限(全局 4 个,含当前可见者),当第 5 个会话需要保活,则按最久未使用淘汰最旧的一个:被淘汰者销毁、腾出额度,其会话切回时按记忆地址重新加载(见「一级 Tab 栏与实例语义」场景 14);任何时刻保活总量不得超过 4。
- 假设某 preview guest 已保活,当发生以下任一动作,则该 guest 必须销毁并释放额度(下次打开/切回为全新加载):用户关闭该 preview tab;该会话被删除;该分屏被关闭;退出应用。
- 假设保活中的 guest 不可见(其会话不在前台),当该 guest 原本处于元素拾取模式,则保活时必须自动退出拾取、恢复页面正常交互,且不得向任何会话的输入框追加评论(元素评论只来自当前可见的 preview,见 desktop-preview.md「预览页元素评论」);切回不残留拾取态,也不得抢焦点或覆盖当前会话 UI。
- 假设某 preview tab 已加载,当发生以下任一动作——面板折叠(头部按钮)、一级 tab 切换、进入/退出全屏预览、窗口尺寸变化,则preview guest 一律不重载(沿用「展开/折叠、空间守卫与欢迎页共存」场景 1 与「一级 Tab 栏与实例语义」场景 8):可见与保活只是几何位置的切换。
- 假设guest 保活期间页面照常运行(dev server HMR 连接保持、页面内定时器与动画继续),当用户切回,则不得出现双份页面、重复注入的拾取脚本/样式或状态错位——同一 preview tab 全程单一 guest 实例。
边界情况
新会话状态(尚无 sessionId)的 diff/terminal 入口:分屏处于新会话状态时,差异与终端作用于该分屏当前工作目录;无工作目录时对应类型在空态入口与「+」菜单中 disabled(提示「当前会话不可用」,见「空态面板与+新增入口」场景 3),preview/文件不受影响;新会话状态的面板 tab 集合归该分屏暂存,发出首条消息、会话绑定 sessionId 后归到该会话名下继续记忆。
删除已记住面板组的会话:其会话级面板组记忆一并清除;若该会话正显示在某分屏且终端面板打开,先终止 PTY 再走清理流程。
菜单栏「面板」项与快捷键:菜单栏「面板 → <类型>」(preview
Shift+Cmd+P/Ctrl+Shift+P,plan 无快捷键,diffShift+Cmd+D/Ctrl+Shift+D,terminalCtrl+`,file 无快捷键)为开/关切换语义(复选框选中态 = 该类有打开 tab):有打开实例时再次触发关闭该类全部 tab,无实例时等价于打开(新增/激活,见「+」菜单项语义);文件面板无快捷键(Ctrl+Shift+F与输入法冲突,仅菜单项)。preview 唯一实例边界:
planContent路由(ExitPlanMode)打开 plan tab 时走唯一实例语义(已有则激活并更新内容)。面板空间不足:拒绝开启 + 轻量提示(「空间不足,无法开启面板」),不开任何 tab、不改动既有 tab(旧多选并排模型的「关旧开新」腾空间做法已废弃,tab 化后不存在)。
对话列 360px 保底与面板 202px 下限的层级:对话列 360px(
CHAT_MAIN_MIN_WIDTH)是绝对保底——自动 40/60 分配、手动拖拽、窗口缩小任何路径都不得把对话列压到 360px 以下;面板 202px(PANEL_MIN_WIDTH,2026-10-30 设计评审由 320 下调,取值依据是空态副标题 13px 文字不折行所需的最小宽度)仅是开启与手动拖拽动作的下限,窗口缩小场景允许面板被压至其下(缩小只落在面板一侧,见场景 4);两列无法同守各自下限(行宽 < 562px)时拒绝开启(场景 3)。面板关闭销毁:关闭 preview tab 即销毁该 guest 实例(重开全新加载);会话切换不销毁 preview guest(移出可视区保活,见「preview guest 跨会话保活」,与下面终端 PTY 的语义相反);关闭 terminal tab 仅移除视图、宿主 PTY 不销毁(键
term-<paneId>,重开重挂接同一 shell 并回放滚动缓冲——真正销毁发生在分屏关闭/会话切换/删除会话/重启终端/退出应用);关闭最后一个 tab 时槽位随之一并收起(不显示空态,见「一级 Tab 栏与实例语义」场景 10);折叠(头部按钮)才隐藏槽位并保留全部实例。preview guest 保活额度:全局上限 4 个(含当前可见者),超出按最久未使用淘汰并销毁;额度是窗口级、跨分屏共享,不是按分屏或按会话各记一份。
非目标(明确排除)
右侧面板与面板栏:VSCE/JetBrains 复用 IDE 自身的面板与终端能力,不提供桌面式右侧面板与一级 Tab 栏。
面板 tab 集合与宽度跨重启持久化:运行期间按会话记忆,重启后随「每次启动全新开始」规则清空。
面板脱离所属会话/分屏独立放置:面板仅限其所属会话/分屏内部(单槽位),不支持跨分屏摆放或窗口级独立面板行。
面板 tab 手动排序:tab 顺序按打开/新增时序固定,不支持手动拖拽排序。