Skip to content

功能规格说明:桌面端面板与工作区 ​

用户场景与测试 ​

用户故事:右侧面板 · 一级 Tab 栏与实例语义(优先级:P1) ​

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

为什么是这个优先级:一级 Tab 栏是面板 tab 化模型的骨架语义:仅 preview 为多实例(tryOpenPanel:preview → addTab,其余 → ensureUniqueTab;文件面板另走路径切换复用),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(不新建),切换其路径为 B 并软刷新(旧内容保留至宿主回复落地);再次点击当前文件路径则重新读取刷新。
  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「预览面板全屏」,本故事只规定全屏按钮位于 tab 栏右端、作用于整个槽位)。
  14. 假设某会话的 preview tab 内曾通过地址栏手动输入 URL 或 guest 页面内导航到达新地址,当切换到其它会话再切回该会话(或该 tab 所在面板分屏移动重建),则预览必须恢复该会话最后实际浏览的地址并自动加载——地址栏输入与页内跳转后的地址与链接打开/转发回复的地址同级持久(随会话面板记忆保存,含 preview 空 tab 首次手动输入的场景),不得回到最初请求的 URL、清空或显示空白,避免用户每次切回都要重新输入。

用户故事:右侧面板 · 空态面板与「+」新增入口(优先级: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(空态),当用户拖拽槽位左缘分割线调整宽度,则与打开具体面板时行为一致:宽度实时跟随调整、钳制在 [320, 容器 − 对话列最小宽度],并转为「手动宽度」(后续折叠/再展开/换开其它面板均保持,见「展开/折叠、空间守卫与欢迎页共存」故事场景 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 不被压缩,缩小压力全部落在右侧面板:面板宽度相应收窄(极端窄窗下可低于其 320px 下限——该下限只约束开启与手动拖拽动作,不约束窗口缩小),不溢出、不横向滚动、不压垮对话列。
  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、窗口尺寸变化,均不再自动按比例分配,保持用户设定宽度(钳制范围 [320, 容器 − 360] 不变,随会话记忆保留见场景 5);应用重启后随「面板宽度不跨重启」规则回到自动 40/60 比例分配默认行为。
  9. 假设从未手动拖宽过的会话所在窗口尺寸变化,当再次展开或打开面板,则按当前行宽重新执行 40/60 比例分配(含 360px 对话列保底,跟随窗口实时行宽);已手动拖宽的会话不受影响。
  10. 假设某会话折叠了面板(槽位隐藏但 tab 仍保留挂载),当切换到其它会话再切回该会话(或该 tab 所在面板分屏移动重建),则折叠/展开状态随会话面板记忆原样恢复——切回后保持折叠,不得因该会话仍打开着 tab 而自动展开(2026-09-08 拍板:折叠/展开与 tab 集合、宽度同级,逐会话记忆);目标会话自身的折叠/展开记忆互不影响。

用户故事:会话变更差异面板(优先级:P1) ​

作为桌面端用户,我希望在对话旁打开差异面板,实时查看本次会话相对仓库基线的全部改动(agent 已提交到会话分支的、工作区尚未提交的、以及尚未跟踪的新文件),以便不离开应用即可审查 agent 这一轮到底改了什么。

为什么是这个优先级:审查改动是 agent 编程的核心回路,desktop 无编辑器内建 diff 视图,必须自备;且 worktree 会话中 agent 的产出常以 commit 形式落地——若面板只显示未提交改动,agent 一提交面板就会变成「无改动」而误导用户,变更基准语义是该面板可用性的前提(P1)。

独立测试:在 git 仓库会话中让 agent 修改若干文件,打开差异面板验证逐文件差异区块与折叠交互;让 agent 在 worktree 会话中产生 commit,验证面板仍显示这些改动(相对变更基准);手动改动工作区文件后点击刷新验证更新;非 git 目录下验证空状态提示;在无 origin、无 main/master 的仓库验证退化为 HEAD 基准。

验收场景:

  1. 假设用户在 git 仓库的会话中通过空态入口/「+」菜单/菜单栏「面板」打开差异面板,当面板打开,则必须展示变更基准到当前工作树的全部改动:变更基准 = 会话分支相对仓库默认分支的分叉点(git merge-base HEAD <默认分支>),因此 agent 已提交到会话分支的改动与尚未提交的工作区改动一并计入;未跟踪的新文件按纯新增计入。
  2. 假设面板解析变更基准,当查找默认分支,则按以下顺序取第一个可用者:远端默认分支(refs/remotes/origin/HEAD 指向的分支,如 main)→ 本地存在的 main → 本地存在的 master;解析不到任何默认分支、或分叉点计算失败(无共同祖先)时,变更基准退化为 HEAD——此时面板只显示未提交改动(等价于无基线能力时的既有行为),不得报错、不得展示空面板。
  3. 假设仓库尚无任何 commit(无 HEAD),当面板打开,则变更基准退化为已暂存内容(--cached 语义),面板展示已暂存与未跟踪的文件,不报错。
  4. 假设面板已打开,当查看面板工具栏,则必须以只读形式展示当前对比的两端 {变更基准} → 工作树(两端不可点击、无选择器;变更基准显示默认分支名,退化为 HEAD 时显示 HEAD,完整 sha 在 title 提示中给出),并展示改动文件数与总增删统计(N 个文件 / +A / −D)。
  5. 假设面板打开且当前范围有改动,当查看内容,则必须以手风琴形式逐文件展示改动:每个文件一个可折叠区块,区块头部显示相对路径、状态标识(新增/修改/删除/重命名/未跟踪)与增删行统计,展开区显示该文件差异内容(增删行高亮,风格与消息内 diff 块一致,含词级高亮,见场景 9);默认展开第一个文件,点击头部折叠/展开;文件级导航改由文件树承载(见「差异面板文件树与导航」)。
  6. 假设面板已打开且可见,当该分屏会话的一轮生成结束(agent 可能改动了文件),则面板必须自动刷新为最新改动;面板同时必须提供手动刷新按钮。自动或手动刷新期间必须保留现有内容直至新数据到达后一次性替换,不得清空为加载占位导致闪烁;仅切换会话/工作目录时才重置为加载状态;刷新进行期间刷新按钮显示旋转动效。
  7. 假设工作目录不是 git 仓库或 git 不可用,当面板打开,则必须显示「非 git 仓库」空状态提示,不得报错影响聊天区。
  8. 假设变更基准到工作树之间没有任何改动,当面板打开,则必须显示「无改动」空状态。
  9. 假设工作区有多个改动文件,当面板打开或用户点击某个文件头部,则差异数据仍一次性加载(一次请求取回全部文件路径与差异内容),但同一时刻只展开一个文件区块:点击某文件会自动收起当前展开的其他文件,点击已展开文件则收起它;DOM 仅保留该文件的差异内容与全部文件头部,刷新时保持当前展开的文件不变。
  10. 假设展开区显示某文件的差异内容,当同一处改动含删除行与新增行,则删除行与新增行按位置逐对配对做词级高亮:行内仅变化的词以更深的底色标出(删除词附删除线,新增词底色更深),未变化的部分保持行级底色,风格与消息内 diff 块一致;无配对对象的行(纯新增、纯删除的整块行)整行以词级高亮底色标出(与消息内 diff 块一致);配对按 hunk 内连续块进行,遇到上下文行或下一个 hunk 头时结束当前配对块。
  11. 假设同一文件既在会话分支上被提交过、在工作区又有未提交改动,当面板展示该文件,则该文件只出现一次,展示「变更基准 → 工作树」的合并差异(已提交部分与未提交部分不拆成两条,统计为合并后的净增删)。
  12. 假设面板已打开,当用户在该分屏切换到另一条同样打开了差异面板的会话,则面板必须按新会话的工作目录与变更基准重新拉取并展示最新改动;目标会话无 diff tab/无任何 tab 时,槽位随会话面板记忆切换(见「一级 Tab 栏与实例语义」场景 8 与「展开/折叠」会话切换语义),不把上一会话的 diff 内容带过去。

用户故事:差异面板提交选择(优先级:P2) ​

作为桌面端用户,我希望在差异面板里按 commit 逐个查看本次会话产生的提交,以便定位某个提交具体改了什么,而不是只能看累计的全部改动。

为什么是这个优先级:会话改动常被 agent 拆成多个 commit,累计 diff 难以对应到具体一步;逐 commit 查看是 claude.ai 差异面板的既有能力,数据可由宿主随改动数据一次取回,无需新增请求链路(P2)。

独立测试:让 agent 在会话分支产生两个 commit,打开差异面板验证提交列表(「全部改动」+ 两条 commit);点击某条 commit 验证面板只显示该提交的 diff、文件数/统计/文件树随之变化;切回「全部改动」验证恢复;无 commit 时验证不出现列表。

验收场景:

  1. 假设变更基准到 HEAD 之间存在 ≥1 个 commit,当面板打开,则面板提供提交选择(位于文件树下方):首项为「全部改动」(默认选中,对应变更基准 → 工作树),其后按新→旧列出各 commit(短 sha + 提交标题)。
  2. 假设用户选择某条 commit,当选择生效,则面板改为只展示该 commit 自身的改动(该提交与其父提交的差异;无父提交的根提交按整棵树的首次提交处理):文件数、总增删统计、文件树、手风琴内容均切换为该提交的改动;工具栏标题改为单提交标识(如 {短 sha},不再展示「→ 工作树」)。
  3. 假设用户从某条 commit 切回「全部改动」,当切换生效,则恢复变更基准 → 工作树的全部改动与标题;已展开的文件若在新范围内仍存在则保持展开,否则回落到该范围的默认展开规则。
  4. 假设变更基准到 HEAD 之间没有任何 commit(改动全在工作区未提交),当面板打开,则只提供「全部改动」(不渲染提交列表),不得出现空列表占位或报错。
  5. 假设用户已选择某条 commit 作为查看范围,当切换会话/工作目录或刷新触发,则保持当前选择的提交;该提交在当前仓库已不存在(如 rebase 后)时回落到「全部改动」,不得展示错误态。

用户故事:差异面板文件树与导航(优先级:P2) ​

作为桌面端用户,我希望差异面板左侧有一棵可折叠的改动文件树,以便改动文件较多时快速定位并跳到关心的那个文件,而不是在长手风琴里滚动查找。

为什么是这个优先级:改动文件多时逐个头扫读成本高;文件树是 claude.ai 差异面板的主导航方式,且所需数据已在面板手中(沿用同一次改动数据,不新增请求)(P2)。

独立测试:造出多层目录的多个改动文件,验证文件树层级、目录折叠、文件行统计、测试文件默认不展开、显隐按钮;点击树中某文件验证手风琴展开该文件并滚动到可见;切换会话验证显隐状态记忆。

验收场景:

  1. 假设面板已加载改动,当查看面板,则左侧展示可折叠的文件树:按路径层级组织(目录可折叠/展开),每个文件行显示文件名与 +N −M 统计(完整路径在 title 提示中给出);节点顺序与手风琴中的文件顺序一致,仓库根目录本身不作为节点显示。
  2. 假设文件树已展示,当用户点击某个文件行,则手风琴展开该文件(互斥收起其它文件)并滚动到该文件可见;导航不改变手风琴的折叠语义(点击已展开的文件不产生额外展开态变化)。
  3. 假设改动文件包含测试文件(路径含 __tests__ / __snapshots__ / *.test.* / *.spec.* / test_*.py / *.snap),当面板首次加载选择默认展开文件,则默认展开的是第一个非测试文件(测试文件不自动展开);当范围内全部是测试文件时展开第一个。
  4. 假设面板已加载改动,当用户点击工具栏的文件树显隐按钮,则左侧栏(文件树与位于其下方的提交选择一并)收起(差异区占满面板宽度)或展开;显隐状态随会话面板记忆保留,切换会话、折叠面板再展开后原样恢复。
  5. 假设面板槽位宽度不足以并排容纳文件树与可用宽度的差异内容(小于 文件树宽度 240px + 差异区最小可读宽度),当查看面板,则左侧栏自动隐藏、差异区占满槽位;槽位加宽越过阈值即自动恢复显示——该自动降级不改写用户的显隐记忆状态。
  6. 假设用户选择了某条 commit 作为查看范围,当查看文件树,则文件树完整列出该范围的改动文件(统计取自该范围数据),并同样支持场景 2 的导航。

用户故事:差异视图切换与大规模差异降级(优先级:P2) ​

作为桌面端用户,我希望能在统一视图与并排视图之间切换,并且在差异特别大时面板不被拖垮(自动折叠全部文件并说明、对超长文件关掉逐词高亮),以便既能按习惯读 diff,又能在一次巨大改动下保持可用。

为什么是这个优先级:并排视图是 claude.ai 差异面板的既有读法;降级规则决定面板在大型改动(重命名整个目录、生成产物、minified 文件)下是否可用——这是本面板的可用性兜底(P2)。

独立测试:切换统一/并排视图验证同一 hunk 的删除/新增行左右对齐、双列行号、词级高亮仍生效、且切换不发出新的数据请求;构造总增删 > 5000 行的改动验证默认不展开任何文件并出现提示;构造单文件 > 1000 行的改动验证该文件无词级高亮但行级展示正常。

验收场景:

  1. 假设面板已加载改动,当用户点击工具栏的视图切换按钮,则在统一视图与并排视图之间切换:统一视图为上下排列的增删行 + 左侧单列行号,并排视图为左旧右新两列(含旧/新两列行号);默认统一视图(保持既有默认读法,不因本次改动改变既有用户打开面板时的外观)。
  2. 假设处于并排视图,当渲染某个 hunk,则 hunk 内按位置配对的删除行与新增行左右并排显示(左列 = 删除行,右列 = 新增行),配对不足的一侧补空行占位;上下文行左右两列同显;hunk 头横跨两列显示;左右两列不对同一行错位。
  3. 假设处于并排视图且某对删除/新增行已配对,当渲染,则词级高亮规则不变(行内变化的词以更深底色标出,删除词附删除线):同一对行在并排两列中各自标出变化词,规则与统一视图一致。
  4. 假设用户触发视图切换,当切换完成,则不得重新向宿主请求改动数据(同一份 hunk 数据换一种排布);已展开的文件保持不变;展开中的行评论框关闭且未提交草稿丢弃(与刷新一致,见「差异面板行评论」场景 4)。
  5. 假设当前查看范围内的总增删行数超过 5000,当面板加载该数据,则默认不展开任何文件(覆盖「默认展开第一个非测试文件」),并在面板顶部显示说明(如「差异过大,已折叠全部文件」);用户仍可手动展开单个文件查看。
  6. 假设某文件的差异行数超过 1000,当该文件展开,则该文件不做词级配对高亮(只保留行级增删底色与 ± 标识),展示内容与其余交互(行评论、视图切换)不受影响;行数回到阈值以下后恢复正常词级高亮。
  7. 假设某文件的差异行数超过单文件内容上限(2000 行),当该文件展开,则按既有截断规则展示并提示「差异过大,已截断…」;三条规则的层级为:先按范围总行数决定是否默认折叠全部文件 → 再按单文件行数决定是否关词级高亮 → 最后按内容上限裁剪。

用户故事:差异面板行评论(优先级:P3) ​

作为用户,我希望像 GitHub/GitLab 的 diff 评论那样,在差异面板里悬停某行出现评论按钮、点击展开输入框写下评论,评论连同文件路径与差异行上下文一起追加到消息输入框,以便我连续评论多行后统一发给 agent,agent 精准理解我指的是哪个文件的哪一处改动。

为什么是这个优先级:把「指哪改哪」闭环从原型预览扩展到代码 diff 审查;沿用代码审查工具通用的行内评论交互,无需额外学习;不改变差异面板只读属性。

独立测试:打开差异面板 → 悬停某增/删行验证行首出现评论按钮 → 点击按钮在行下展开评论框 → 输入评论并添加 → 验证评论连同文件路径与行内容追加到消息输入框(不直接发送)→ 在另一行重复添加第二条 → 验证两条评论都在输入框中,发送后聊天中出现含全部评论的用户消息。

验收场景:

  1. 假设差异面板已加载改动,当用户鼠标悬停某行(增/删/上下文行,统一视图与并排视图下均适用),则该行行首(左侧 gutter)必须出现评论按钮(「+」图标,GitHub 风格):小圆角方形按钮、浅色底 + 蓝色强调色的小号加号图标、完全可见(不透明),与行背景对比鲜明一眼可辨;鼠标移到另一行时按钮跟随到新行,离开 diff 行则按钮消失;悬停按钮本身时底色加深。点击评论按钮必须在该行下方展开行内评论框(含文本输入框与添加按钮),被评论行保持可见。
  2. 假设用户在评论框输入评论并点击添加按钮(或回车),则不得直接发送给 agent,必须将评论追加到消息输入框中,追加内容包含:文件路径、差异行内容(或片段)、评论文本;输入框已有内容时必须保留并在其后追加;评论框关闭,由用户在输入框中统一编辑后手动发送,发送后的消息呈现与普通用户消息一致。
  3. 假设评论框已展开,当用户按 Esc 或点击取消,则评论框关闭且不提交;用户可继续悬停其他行添加评论,无需开关任何模式即可连续评论多行。
  4. 假设面板发生刷新(自动/手动)、切换会话/工作目录、切换提交范围或切换统一/并排视图,则展开中的评论框必须关闭、未提交的草稿丢弃,防止行内容变化后界面状态不一致。

用户故事:内嵌终端面板(优先级:P2) ​

作为用户,我希望在对话旁打开一个真正的终端,直接在该对话工作目录里运行命令(包括 vim、top 等交互式程序),以便我无需离开应用即可验证 agent 的改动或执行辅助操作。

为什么是这个优先级:终端是 desktop 对标编辑器内建终端的关键能力;真 PTY 方案(node-pty)才能承载交互式程序,是该能力的可用性底线。

独立测试:打开终端面板,运行 ls、启动 top 后按 q 退出、Ctrl+C 中断一个长命令;调整面板宽度验证终端换行自适应;折叠面板再展开验证会话与输出保留;关闭终端 tab 再重新打开验证宿主 PTY 重挂接(同一会话 + 滚动缓冲回放)。

验收场景:

  1. 假设用户打开终端面板(空态入口/「+」菜单/菜单栏「面板 → 终端」),当面板首次打开,则必须以该对话工作目录(worktree 会话为 worktree 路径)为 cwd 启动用户默认 shell 的登录交互会话(macOS/Linux 取 $SHELL,Windows 取 PowerShell),渲染完整提示符与环境变量。
  2. 假设终端已打开,当用户输入命令、运行交互式 TUI 程序(如 vim/top)、按 Ctrl+C,则行为必须与系统终端一致:按键全量透传、程序输出(含 ANSI 颜色)正确渲染、Ctrl+C 中断前台程序。
  3. 假设用户调整面板宽度或窗口大小,当终端可视尺寸变化,则必须同步 PTY 行列数,输出按新宽度重排。
  4. 假设终端面板已打开,当用户折叠面板(头部按钮)再展开,则槽位仅 display:none 隐藏,终端 tab 保持挂载、shell 会话与滚动输出原样保留(隐藏不终止);关闭 terminal tab 仅移除该视图——宿主 PTY 不销毁(键 term-<paneId>,见「面板关闭销毁」边界),再次打开终端面板时重挂接同一 shell 并回放滚动缓冲;面板工具栏必须提供「重启终端」操作(终止并重建会话)。
  5. 假设用户关闭分屏、删除会话或退出应用,当对应终端存在,则必须终止其 PTY 进程,不得残留孤儿进程。
  6. 假设终端面板可见时该分屏切换到另一条会话,则原会话的终端随会话切换终止 PTY(跨会话切换不保留滚动输出);目标会话记忆有终端 tab 时按目标会话工作目录新建终端会话,切回原会话重新显示终端时同样按原会话工作目录重建。
  7. 假设终端已打开,当用户选中文本复制或使用系统粘贴快捷键,则必须与系统剪贴板互通。
  8. 假设 node-pty 原生模块加载失败,当用户打开终端面板,则面板内必须显示可操作的错误提示,不得崩溃或影响其他面板。
  9. 假设终端输出中包含 http(s) 链接(如 http://localhost:5173),当用户点击该链接,则行为必须与消息中的链接一致:localhost 链接(localhost、127.0.0.1、[::1],任意端口)在右侧预览面板打开并加载该 URL(新增/激活 preview tab 的实例语义见「一级 Tab 栏与实例语义」场景 1-2,预览未打开时自动展开面板);非 localhost 链接用系统默认浏览器打开。

边界情况 ​

  • 被评论行随后被刷新覆盖:差异行评论携带提交时刻的上下文快照(文件路径与行内容);刷新后行内容/行序变化不构成错误,后续评论需重新悬停添加。
  • 新会话状态(尚无 sessionId)的 diff/terminal 入口:分屏处于新会话状态时,差异与终端作用于该分屏当前工作目录;无工作目录时对应类型在空态入口与「+」菜单中 disabled(提示「当前会话不可用」,见「空态面板与+新增入口」场景 3),preview/文件不受影响;新会话状态的面板 tab 集合归该分屏暂存,发出首条消息、会话绑定 sessionId 后归到该会话名下继续记忆。
  • 删除已记住面板组的会话:其会话级面板组记忆一并清除;若该会话正显示在某分屏且终端面板打开,先终止 PTY 再走清理流程。
  • 删除 worktree 会话时其终端仍在运行:删除/清理流程必须先终止该分屏的终端 PTY,再执行 worktree 清理,避免进程占用目录导致清理失败。
  • 终端内进程正在运行:退出应用或删除会话时强制终止 PTY(含其子进程),不等待前台命令结束。
  • 差异面板 diff 输出过大:单文件 diff 超大(如 minified 文件)时必须截断展示并提示,不得阻塞渲染(降级规则的完整层级见「差异视图切换与大规模差异降级」场景 7)。
  • 变更基准解析失败或退化:解析不到仓库默认分支(无 origin、无 main/master)或分叉点计算失败(无共同祖先)时,变更基准退化为 HEAD——面板只显示未提交改动,不报错、不展示空面板;解析顺序与退化条件见「会话变更差异面板」场景 2。
  • 会话分支即默认分支:分叉点等于 HEAD,面板行为与退化时一致(只显示未提交改动),用户不应因此看到空面板或错误提示。
  • rebase / 重置后基准变化:变更基准随当前分支状态重新计算,刷新后按新基准展示;已选择的单条 commit 范围在当前仓库中不再存在时回落到「全部改动」(见「差异面板提交选择」场景 5)。
  • 未跟踪文件与提交范围的关系:未跟踪文件属于工作树,只出现在「全部改动」范围;切换到单条 commit 范围时不出现。
  • 变更基准名过长:工具栏标题按可用宽度截断,优先保证「→ 工作树」一侧可读;完整值在 title 提示中给出。
  • 各分屏/会话的终端 tab 相互独立:终端 tab 按分屏(pane)寻址(term-<paneId>),输入输出互不串台;同一会话切换分屏展示时终端按新 pane 独立。
  • 菜单栏「面板」项与快捷键:菜单栏「面板 → <类型>」(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 保底与面板 320px 下限的层级:对话列 360px(CHAT_MAIN_MIN_WIDTH)是绝对保底——自动 40/60 分配、手动拖拽、窗口缩小任何路径都不得把对话列压到 360px 以下;面板 320px(PANEL_MIN_WIDTH)仅是开启与手动拖拽动作的下限,窗口缩小场景允许面板被压至其下(缩小只落在面板一侧,见场景 4);两列无法同守各自下限(行宽 < 680px)时拒绝开启(场景 3)。
  • 面板关闭销毁:关闭 preview tab 即销毁该 guest 实例(重开全新加载);关闭 terminal tab 仅移除视图、宿主 PTY 不销毁(键 term-<paneId>,重开重挂接同一 shell 并回放滚动缓冲——真正销毁发生在分屏关闭/会话切换/删除会话/重启终端/退出应用);关闭最后一个 tab 时槽位随之一并收起(不显示空态,见「一级 Tab 栏与实例语义」场景 10);折叠(头部按钮)才隐藏槽位并保留全部实例。

非目标(明确排除) ​

  • 右侧面板与面板栏:VSCE/JetBrains 复用 IDE 自身的面板与终端能力,不提供桌面式右侧面板与一级 Tab 栏。
  • 终端多标签与终端内分屏:首版每会话至多一个终端 tab(唯一实例),终端面板内不再分屏。
  • 终端内搜索:首版不做。
  • 差异面板的写操作:首版只读,不提供暂存、提交、丢弃改动、逐行回退等操作。
  • 差异面板的任意 ref 对比:只提供「变更基准 → 工作树」与「单个 commit」两种查看范围;不提供 base/head 选择器,不支持任选两个分支/tag/commit 对比。
  • 差异面板的补丁导出与应用:不做下载 patch、应用补丁等操作(面板始终只读,写操作边界见「差异面板的写操作」)。
  • 全文件同时展开的虚拟化列表:保持「同一时刻至多展开一个文件」的手风琴模型,不做全部文件同时展开的堆叠卡片与虚拟化渲染。
  • 面板 tab 集合与宽度跨重启持久化:运行期间按会话记忆,重启后随「每次启动全新开始」规则清空。
  • 面板脱离所属会话/分屏独立放置:面板仅限其所属会话/分屏内部(单槽位),不支持跨分屏摆放或窗口级独立面板行。
  • 面板 tab 手动排序:tab 顺序按打开/新增时序固定,不支持手动拖拽排序。