---
url: /wave-agent/specs/desktop/desktop-right-panel.md
description: CodeWave IDE 桌面端右侧面板槽位：一级 Tab 栏与实例语义、空态与「＋」新增入口、展开/折叠与空间守卫、preview guest 跨会话保活
---

# 功能规格说明：桌面端右侧面板

> 本规格描述右侧面板这个**槽位**本身：tab 栏怎么组织、怎么开合、占多少宽度、里面各个实例的生命周期。槽位里承载的差异面板、文件面板、终端面板、预览面板各有自己的规格（[desktop-diff-panel.md](./desktop-diff-panel.md)、[desktop-file-panel.md](./desktop-file-panel.md)、[desktop-terminal-panel.md](./desktop-terminal-panel.md)、[desktop-preview.md](./desktop-preview.md)）。

## 用户场景与测试

### 用户故事：右侧面板 · 一级 Tab 栏与实例语义（优先级：P1）

作为桌面端用户，我希望右侧面板采用浏览器式一级 Tab 栏：每个打开的实例对应一个 tab，preview 可同时开多个（每次从链接/「＋」打开都新增一个），而计划/差异/终端/文件面板同一时刻至多一个实例（重复打开时激活已有 tab 而非新建），以便同时对照多个预览页面，又不让面板类型重复堆积。

**为什么是这个优先级**：一级 Tab 栏是面板 tab 化模型的骨架语义：仅 preview 为多实例（`tryOpenPanel`：preview → `addTab`，其余 → `ensureUniqueTab`；文件面板同样是唯一 file tab，多个文件在面板内部以文件标签承载，见 [desktop-file-panel.md](./desktop-file-panel.md)「文件面板已打开文件的标签」），tests 与代码注释均以「仅 preview 多实例」为准——本故事以该实例语义为验收目标，必须先定（P1）。

**独立测试**：preview 连续从两条消息 localhost 链接打开验证新增两个 tab 且各自独立加载、tab 标签用页面标题；plan/diff/terminal/file 重复打开验证激活已有 tab 不新增；点击 tab 切换；关闭激活 tab 落到左侧邻居；关闭最后一个 tab 时右侧面板一并收起（不显示空态）；键盘 ←/→ 在 tab 间移动；tab 溢出可横向滚动；tab 标签不显示文件路径/文件名细节。

**验收场景**：

1. **假设**本地会话中点击消息中的 localhost 链接，**当**预览面板打开，**则**每次点击都在 tab 栏**新增**一个 preview tab（`data-panel-tab`）并激活，新页面在槽位加载；再次点击同一链接或不同链接均新增（多实例语义），不覆盖既有 tab。
2. **假设**远程会话中点击 localhost 链接，**当**预览面板打开，**则**若该链接的规范化 URL（同一 origin/路径、任意回环主机拼写）已有建立隧道的 preview tab 且隧道无错误，则激活该既有 tab、不新建（隧道复用、不重建）；路径/来源不同或隧道失败则新增 tab。
3. **假设**preview tab 打开中，**当**查看 tab 标签，**则**显示 guest 页面标题（页面 title 更新事件驱动），无标题时回退「host+path」，空地址回退「新预览」；不显示完整 URL。
4. **假设**用户通过「＋」菜单或空态入口打开 plan/diff/terminal/file，**当**该类面板已有打开实例，**则**只激活既有 tab（`ensureUniqueTab`），不新增重复实例；无实例时才新建。
5. **假设**消息中出现文件路径点击（desktop 宿主），**当**文件面板未打开，**则**新增唯一 file tab 并立即显示 loading 桩，宿主读取后回填内容（`desktopFileContent` 链路）。
6. **假设**file tab 已打开且显示文件 A，**当**点击另一条消息的文件 B 路径，**则**激活既有 file tab（不新建），并在面板内按 [desktop-file-panel.md](./desktop-file-panel.md)「文件面板已打开文件的标签」打开 B（新增一个文件标签，或激活已打开的同名标签），A 的标签保留；再次点击当前文件路径则激活该标签并重新读取刷新。
7. **假设**file tab 存在，**当**查看一级 tab 栏标签，**则**file tab 标签只显示面板名「文件」——**不泄露具体文件名**（文件名只出现在 FilePane 内部的文件标签栏与工具条路径中）。
8. **假设**存在多个 tab，**当**点击某个 tab，**则**切换激活并展示对应面板内容；被切换走的 tab 保持挂载（`display:none`），其 preview guest / 终端 PTY / 文件内容不销毁。
9. **假设**存在多个 tab，**当**关闭非激活 tab，**则**仅移除该实例（browser-tab 语义：preview guest 销毁、重开是全新加载；终端 PTY 为例外，见「面板关闭销毁」边界）；关闭激活 tab 时激活其**左侧相邻** tab。
10. **假设**用户关闭最后一个 tab（tab 关闭按钮，或在头部「面板」菜单中取消勾选最后一个类型），**当**关闭完成，**则**右侧面板随之收起（聊天区恢复全宽），**不显示空态引导**——「已展开但没有任何 tab」的空态只由用户显式展开面板触发（见「空态面板与＋新增入口」）；preview 全屏状态同时退出；收起后槽位宽度按会话记忆保留，再次展开按自动 40/60 分配或沿用已手动设定的宽度。
11. **假设**tab 栏存在，**当**按 `←`/`→` 方向键，**则**焦点在 tab 间循环移动（roving tabindex，Enter/Space 激活）；tab 栏聚焦时不与其它快捷键冲突。
12. **假设**tab 总宽超出槽位宽度，**当**查看 tab 栏，**则**可横向滚动浏览全部 tab（「＋」按钮粘在可视右缘）；新增 tab 后滚动到新 tab 可见。
13. **假设**preview tab 处于激活且用户点击 tab 栏全屏按钮，**当**进入全屏，**则**右侧面板占满内容区、聊天列隐藏；再次点击或按 Esc 退出全屏恢复（全屏语义见 [desktop-preview.md](./desktop-preview.md)「预览面板全屏」，本故事只规定全屏按钮位于 tab 栏右端、作用于整个槽位）。
14. **假设**某会话的 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 无勾选框、其余勾选反映是否已开、点击行为）。

**验收场景**：

1. **假设**用户显式展开右侧面板（头部面板按钮，或菜单栏「面板」菜单）且当前没有任何打开的 tab，**当**查看槽位，**则**显示空态（`data-testid="panel-empty-state"`）：标题「还没有打开任何面板」+ 副标题「点击下方功能页打开对应面板」+ 五个能力入口按钮网格（preview/plan/diff/terminal/file，含图标、面板名与快捷键提示，`data-testid="panel-empty-item-<kind>"`）；关闭最后一个 tab 不会进入该状态（面板一并收起，见「一级 Tab 栏与实例语义」场景 10）。
2. **假设**空态五个入口各自对应能力描述（预览网页应用/查看实施计划/查看代码改动/打开终端/查看文件内容），**当** hover 或查看，**则**入口提供对应描述（title 或可见文案）。
3. **假设**某类面板当前不可用（如无 workdir 的会话中 diff/terminal），**当**查看空态，**则**对应入口 disabled 并提示「当前会话不可用」，点击无效果。
4. **假设**用户点击空态某入口，**当**点击发生，**则**preview 新增一个空 preview tab；plan/diff/terminal/file 走 open-or-activate（已有则激活，无则新建）；空间不足被拒时（见「展开/折叠、空间守卫与欢迎页共存」故事场景 3）给出轻量提示且不开任何 tab。
5. **假设**tab 栏存在，**当**点击「＋」（`data-testid="panel-tabs-add"`），**则**弹出面板类型菜单：五种类型齐全；preview 项无勾选框（多实例随时可加）；其余项以勾选反映「该类型当前有打开实例」；菜单打开时锚定「＋」按钮左缘对齐，右侧贴边时钳制不溢出。
6. **假设**「＋」菜单中点选某类型，**当**点击菜单项，**则**菜单关闭并执行对应语义：preview 新增空 preview tab（其地址栏供输入 URL）；plan/diff/terminal/file open-or-activate 唯一实例；disabled 类型项不可点。
7. **假设**用户从消息内容触发面板打开（链接/路径/plan 内容/diff），**当**槽位处于收起或无 tab 状态，**则**打开语义自动展开面板并新建/激活对应 tab（调起即优先显示），无需先手动展开。
8. **假设**某 preview tab 被关闭后，**当**用户再次打开 preview，**则**为全新实例（guest 重新加载，browser-tab 关闭语义）；关闭 terminal tab 仅移除视图、宿主 PTY 不销毁（重开重挂接，见「内嵌终端面板」场景 4 与「面板关闭销毁」边界）。
9. **假设**右侧面板槽位展开但没有任何打开的 tab（空态），**当**用户拖拽槽位左缘分割线调整宽度，**则**与打开具体面板时行为一致：宽度实时跟随调整、钳制在 \[202, 容器 − 对话列最小宽度]，并转为「手动宽度」（后续折叠/再展开/换开其它面板均保持，见「展开/折叠、空间守卫与欢迎页共存」故事场景 5/8）——空态只是槽位的无 tab 形态，分割线拖拽入口不得缺失。
10. **假设**右侧面板槽位较窄（拖拽到接近下限，两列已放不下舒适宽度的入口），**当**查看空态，**则**五个入口自动由两列网格切换为**单列垂直**排列：按钮占满整行宽度，图标、面板名与快捷键提示完整保留，顺序与宽面板一致（预览/计划/差异/终端/文件依次自然下叠，`data-testid` 不变）；槽位加宽越过阈值即恢复两列网格，切换随拖拽实时发生、无需刷新。

***

### 用户故事：右侧面板 · 展开/折叠、空间守卫与欢迎页共存（优先级：P2）

作为桌面端用户，我希望面板可经头部按钮展开/折叠，折叠时打开中的 tab 与宽度完整保留；窗口过窄放不下最小对话列时拒绝开面板并轻量提示；欢迎页（新对话空态）与展开的面板并存不互相遮挡错位。

**为什么是这个优先级**：面板展开/折叠与空间守卫决定 tab 化布局在窄窗/欢迎页下的可用性，属布局正确性兜底（P2），但欢迎页遮挡问题经既有回归修复后仍须保证 tab 化不引入回退。

**独立测试**：开两个 preview tab 后折叠面板，验证槽位隐藏但 tab/激活/宽度保留，再展开原样恢复；折叠状态退出全屏；宽容器（行宽 ≥ 900px）展开面板验证对话列按 40% 分配、宽于 360px 保底，窄容器（<900px）展开验证对话列停在 360px 保底（场景 7/9）；窗口缩窄至对话列最小宽度不足时开面板被拒并提示；欢迎页打开面板验证不遮挡不错位（几何断言）。

**验收场景**：

1. **假设**面板已展开且存在若干 tab，**当**点击头部面板按钮（收起），**则**右侧槽位隐藏（聊天区恢复全宽），但所有 tab、激活 tab 与面板宽度保持挂载保留（`display:none`）；再次点击展开时原样恢复，preview guest 不重新加载、终端 PTY 不中断。
2. **假设**面板处于全屏状态，**当**用户折叠面板，**则**全屏同时退出（全屏与折叠互斥：折叠即退出全屏）；展开不会自动重进全屏。
3. **假设**窗口宽度不足以让对话列保持最小宽度（`containerW − PANEL_MIN_WIDTH < CHAT_MAIN_MIN_WIDTH`），**当**用户通过任何入口请求开面板，**则**拒绝开启并显示轻量提示「空间不足，无法开启面板」，不开任何 tab、不改动既有 tab。
4. **假设**窗口在打开面板后缩小，**当**行宽不足以并排容纳「对话列 360px 保底 + 面板当前宽度」，**则**对话列保持 360px 不被压缩，缩小压力全部落在右侧面板：面板宽度相应收窄（极端窄窗下可低于其 202px 下限——该下限只约束开启与手动拖拽动作，不约束窗口缩小），不溢出、不横向滚动、不压垮对话列。
5. **假设**面板宽度经拖拽调整，**当**调整后展开/折叠/会话切换，**则**宽度按会话记忆保留（随会话面板记忆保存，见场景 8），钳制范围下限不变。
6. **假设**欢迎页（新对话空态）显示且用户打开面板，**当**查看布局，**则**聊天主列与右侧面板在同一行内正常分配（主列 `flex:1; min-width:0`），欢迎内容不左偏/不溢出、输入卡不被面板盖住（既有欢迎页与面板并存的布局语义，修复后保持不回归）。
7. **假设**某会话的面板槽位从未被手动拖宽过（「自动 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 固定默认宽度。
8. **假设**槽位宽度曾自动分配或处于默认宽度，**当**用户手动拖拽槽位边缘调整宽度，**则**该会话转为「手动宽度」：此后折叠再展开、关闭再打开、加新 tab、窗口尺寸变化，均不再自动按比例分配，保持用户设定宽度（钳制范围 \[202, 容器 − 360] 不变，随会话记忆保留见场景 5）；应用重启后随「面板宽度不跨重启」规则回到自动 40/60 比例分配默认行为。
9. **假设**从未手动拖宽过的会话所在窗口尺寸变化，**当**再次展开或打开面板，**则**按当前行宽重新执行 40/60 比例分配（含 360px 对话列保底，跟随窗口实时行宽）；已手动拖宽的会话不受影响。
10. **假设**某会话折叠了面板（槽位隐藏但 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、进出全屏预览、分屏移动重建后断言均不重载。

**验收场景**：

1. **假设**会话 A 的某 preview tab 已加载页面并产生了页面内状态（地址栏／页内导航后的地址、页面自身状态、滚动位置），**当**用户切到同分屏的会话 B，**则** A 的 guest 必须保活（离开可视区停驻、不销毁）；切回 A 时立即显示原页面——不得重新加载、不得回到初始 URL 或初始状态。
2. **假设**preview guest 处于保活（不可见）状态，**当**其所属会话重新可见（侧边栏切回、分屏移动后重建、折叠面板后展开），**则**必须复用同一 guest 不重载，并按恢复后的实际面板宽度重新计算缩放（沿用 [desktop-preview.md](./desktop-preview.md)「localhost 原型预览」场景 8）。
3. **假设**保活中的 preview guest 已达上限（全局 4 个，含当前可见者），**当**第 5 个会话需要保活，**则**按最久未使用淘汰最旧的一个：被淘汰者销毁、腾出额度，其会话切回时按记忆地址重新加载（见「一级 Tab 栏与实例语义」场景 14）；任何时刻保活总量不得超过 4。
4. **假设**某 preview guest 已保活，**当**发生以下任一动作，**则**该 guest 必须销毁并释放额度（下次打开／切回为全新加载）：用户关闭该 preview tab；该会话被删除；该分屏被关闭；退出应用。
5. **假设**保活中的 guest 不可见（其会话不在前台），**当**该 guest 原本处于元素拾取模式，**则**保活时必须自动退出拾取、恢复页面正常交互，且不得向任何会话的输入框追加评论（元素评论只来自当前可见的 preview，见 [desktop-preview.md](./desktop-preview.md)「预览页元素评论」）；切回不残留拾取态，也不得抢焦点或覆盖当前会话 UI。
6. **假设**某 preview tab 已加载，**当**发生以下任一动作——面板折叠（头部按钮）、一级 tab 切换、进入／退出全屏预览、窗口尺寸变化，**则**preview guest 一律不重载（沿用「展开/折叠、空间守卫与欢迎页共存」场景 1 与「一级 Tab 栏与实例语义」场景 8）：可见与保活只是几何位置的切换。
7. **假设**guest 保活期间页面照常运行（dev server HMR 连接保持、页面内定时器与动画继续），**当**用户切回，**则**不得出现双份页面、重复注入的拾取脚本／样式或状态错位——同一 preview tab 全程单一 guest 实例。

## 边界情况

* **新会话状态（尚无 sessionId）的 diff/terminal 入口**：分屏处于新会话状态时，差异与终端作用于该分屏当前工作目录；无工作目录时对应类型在空态入口与「＋」菜单中 disabled（提示「当前会话不可用」，见「空态面板与＋新增入口」场景 3），preview/文件不受影响；新会话状态的面板 tab 集合归该分屏暂存，发出首条消息、会话绑定 sessionId 后归到该会话名下继续记忆。

* **删除已记住面板组的会话**：其会话级面板组记忆一并清除；若该会话正显示在某分屏且终端面板打开，先终止 PTY 再走清理流程。

* **菜单栏「面板」项与快捷键**：菜单栏「面板 → <类型>」（preview `Shift+Cmd+P` / `Ctrl+Shift+P`，plan 无快捷键，diff `Shift+Cmd+D` / `Ctrl+Shift+D`，terminal `` Ctrl+` ``，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 顺序按打开/新增时序固定，不支持手动拖拽排序。
