Skip to content

feat(chat): Session Workbench 多 Pane 工作台——多会话并列与终端 Pane(flag 灰度) - #521

Draft
yovinchen wants to merge 47 commits into
mainfrom
feat/session-workbench-multi-pane
Draft

feat(chat): Session Workbench 多 Pane 工作台——多会话并列与终端 Pane(flag 灰度)#521
yovinchen wants to merge 47 commits into
mainfrom
feat/session-workbench-multi-pane

Conversation

@yovinchen

Copy link
Copy Markdown
Collaborator

草稿 PR:功能整体在 feature flag VITE_LIVEAGENT_SESSION_WORKBENCH 之后,默认关闭、不改变现有单会话行为。仍有安全项(T-1 终端 cwd 校验)与实机验证未完成,见「进行中 / 后续」。

概述

引入 Session Workbench 多 Pane 工作台:在一个窗口内把多个会话本地/SSH 终端作为可拖拽、可分屏、可贴靠的并列 Pane 同屏工作,替代原先「页面 = 单一当前会话」的模型。40 个 commit,+13.7k/-0.7k,109 文件。

核心设计遵循 docs/design/session-workbench-pane-architecture.md 的四层模型:Surface Spec(布局 JSON)/ Runtime Binding(surfaceId→sessionId)/ View Lease(输出流互斥)/ PaneTree(纯布局内核)。布局 JSON 只存 launchSpec + surfaceId不存 sessionId——Pane JSON 不是授权凭据。

已完成

1. 布局内核(agent-ui/src/lib/workbench/,纯模型、零 React/Tauri 依赖)

  • PaneTree + split 树、命令引擎(OPEN/MOVE/CLOSE/RESIZE/FOCUS,revision 化)、几何求解、命中测试、邻接导航、不变量校验(含 duplicate-surface、分裂空间校验)
  • WorkbenchSurfaceSpec union:conversation | localTerminal | sshTerminal | unsupported;唯一性经 surfaceIdentityKey() 泛化
  • codec:显式 allow-list 投影序列化(防私有字段泄漏,配套 workbench-layout-privacy 测试);未知 kind 前向兼容透传
  • 按 surface kind 的最小尺寸(终端 220×140 按 20 cols×6 rows 折算,会话 320×220):reducer OPEN/MOVE 预检、drop 四类 target 预检、DividerLayer 双侧子树递归 clamp

2. 会话运行时脱离「当前会话」生命周期(agent-gui)

  • ConversationRuntimeRegistry + ConversationSurfaceController:会话态(approval/draft/queue/upload/stop 等)收敛为按 conversationId 订阅的 store,多 Pane 各自订阅、互不串扰
  • 双会话并发流式隔离、stop 状态写入隔离有专项测试(workbench-streaming-isolation)
  • ConversationPaneHost + Harness:后台会话挂载、Pane 移动不重挂(源码级守护,DOM 级测试见后续 R-1)

3. 终端 Pane(本地 + SSH)

  • 绑定/租约双 store:surfaceId→sessionId 走 sessionStorage 绑定;sessionId↔paneId 互斥走租约,Right Dock / SSH overlay 经 hiddenSessionIds/paneLeasedSessionIds 防输出流双消费、输入双写
  • drop 事务「几何先行」(terminalDropCommit.ts):newTerminal 先提交布局、PTY 挂载后异步创建;既有会话先写绑定再 OPEN_PANE,失败回滚
  • 重启恢复语义:死终端 Pane 保留为休眠占位(launchSpec 摘要 + 重新启动),不静默丢弃;窗口级 auto-launch 授权集区分「本会话显式创建」与「恢复」,恢复不自动拉 PTY
  • Pane 的 × = Detach(进程保留回 dock);kill 走两态确认按钮(武装→执行→3s 复位);SSH Pane 状态点 + user@host:port + 实时延迟(聚焦时 15s 轮询)+ 重连
  • 拖拽入口:dock 终端 tab、SSH overlay shell tab、dock 空态「新建终端」均可拖入画板;拖拽会话为纯状态机 workbenchDragMachine(独立可测)

4. 性能与健壮性

  • XTermViewport 接入 WebGL 渲染器(构造失败/context-loss 自动回退 DOM);Right Dock 非活跃 tab 懒挂载;resize 两级节流(视觉 fit 80ms + PTY resize 100ms 去抖);输出流按 sessionId 分桶 O(1) 派发
  • 极窄 Pane(<360px)渲染期自动紧凑 chrome,零 revision 开销;分隔条/拖拽手柄适配 forced-colors

5. 持久化与门槛

  • 布局本机持久化(SQLite,Rust workbench_layout.rs);损坏布局同步清理记录;恢复前 terminal_list 对账,list 失败安全降级
  • i18n 中英双语全量补齐;composer/转录空态适配窄面板

验证

  • ✅ GUI 前端测试 2075/2075(新增 20+ 个 workbench/终端专项测试文件:pane 树、drop 安全、租约转移竞态、min-size、流式隔离、布局隐私等)
  • ✅ WebUI 测试 586/586(最后一个 commit 修复了共享 XTermViewport 引入 @xterm/addon-webgl 后 webui 缺依赖的解析失败)
  • ✅ 双端 tsc --noEmit 零错误;check:ui-boundaries 通过(agent-ui 共享组件零 Tauri import,能力经 props 注入)

⚠️ 进行中 / 合入前待办

  • 🔴 T-1 终端 cwd 范围校验(安全,最高优先):拖既有终端入 pane 与布局恢复路径均未校验 cwd 与目标 project 同源;Rust registry.rsproject_path_key 缺省时由 cwd 反推。计划:前端 assertCwdWithinProject 双路径校验 + Rust 侧 canonicalize(cwd).starts_with(project_root) 强制。
  • 实机验证未做:cargo check/完整 tauri build 未跑;需以 flag 开启后实测「dock 拖出→移动 Pane 输出不闪→divider 压到最小宽度→关闭回 dock→重启占位恢复→SSH 拖入/重连/互斥」链路。
  • T-6:streamBuffer.flushResize() 缺相同 cols/rows 去重(拖回原位/theme 变更重复下发)。
  • T-7:内核已支持 insufficient-space 命令 context,ChatPage 的 drop/split 入口尚未传 canvasSize + dividerSize(注意拖拽层 6px 与 geometry 默认 8px 需显式对齐)。
  • T-4 裁决:关闭语义已按 Detach-first 落地(比设计文档的「确认终止」更安全),需回写架构文档 §15.8/§17/§28.13,并给 SSH 补断开确认。

后续(单独立项,不进本 PR)

  • R-1 DOM 级「Pane 移动不重挂」测试:需引入 jsdom 测试基建,Object.is 比对 DOM 节点实例
  • R-2/R-3 syncVisibleConversationRuntime 单槽位收敛 + Hydration 分桶(Map<string, HydrationState>):页面级 8 个镜像 state 逐一改为按 conversationId 订阅 Controller slice,改动面大、稳定期重构
  • 三平台硬化矩阵(Retina/混合 DPI/Wayland、进程树终止 PGID vs ConPTY)、读屏器 + CJK IME 人工实测、双流式性能预算 profiler 实测(架构文档 §24)
  • SSH Pane 内 SFTP(现留 overlay,独立通道不参与互斥)、resizeSplit 补传 context 启用内核 RESIZE clamp(一行接入)

刻意保留的设计边界(review 时请勿当缺陷)

  1. session-missing 不自动重建——避免静默新建 PTY,用户手动重试才按 launchSpec 新建
  2. 终端绑定只走 sessionStorage、不落 SQLite(生命周期恰好匹配 webview reload 后 Rust 会话仍存活)
  3. 拖出手势仅鼠标/笔主键,触控保留原滚动/点击

🤖 Generated with Claude Code

- pane 树类型与不变量校验(invariants)
- 分屏几何、邻接关系与命中测试(geometry/adjacency/hitTesting)
- 布局命令 reducer 与序列化编解码(commands/reducer/codec)
- 内部特性开关 readInternalFeatureFlag,默认关闭
- PaneFrame/PaneChrome/PaneSurfaceLayer 分层渲染,预览几何与提交几何分离
- DividerLayer 分栏拖拽,DockIntentOverlay 停靠意图高亮
- 本地终端与不支持类型的 pane surface 占位
- 画布契约测试:drop 经 revision 校验后提交
新增 workbench_layout 表与 load/save 命令:按 scope 存储
schema_version/revision/payload,payload 上限 96KB,revision 落后即拒绝;
布局只含稳定身份与空间信息,不参与 Gateway Settings Sync
toolApproval 增加 per-conversation listener 与缓存快照,
PendingToolApprovalBar 支持外部注入 approvals(工作台后台 pane 复用),
避免全局 version bump 导致所有会话审批条重渲染
- 草稿/队列/上传/审批各自成 store,useSyncExternalStore 订阅
- 运行时缓存改为可观察 registry,setRuntimeEntry 通知工作台 pane
- 上下文用量源抽出纯工厂 createContextUsageTokensSource,
  供后台 pane 每面板各建一份
为多 pane 并行会话做准备:后台会话的队列与上传不再依赖挂载中的
ChatPage 状态
ConversationSurface 不再直接持有 conversationId + 静态内容,改为
controller + snapshot 渲染:快照聚合转录/审批/压缩相位,
useConversationPaneHostBridge 暴露 composer 与滚动句柄给宿主
- terminalPaneBindingStore:pane 与终端会话的稳定绑定
- terminalPaneLeaseStore:被 pane 租用的会话从右侧 dock 隐藏,
  避免输出流双消费
- terminalPaneRuntime + terminalDropCommit:拖出 dock 后经
  revision 校验落位为终端 pane
- 附各 store 与 commit 链路的单测
- useWindowWorkbench:布局命令执行、焦点/邻接导航、恢复时过滤
  已缺失会话并折叠空分栏
- useWorkbenchDragSession:侧栏/dock 指针拖拽会话,移动阈值内不
  影响原有点击语义
- layoutPersistence:localStorage 快路径 + Tauri workbench_layout
  命令持久化,损坏 payload 留诊断副本
- Flag VITE_LIVEAGENT_SESSION_WORKBENCH 控制,默认关闭
- ConversationPaneHost:单 pane 内的转录 + 审批条 + 任务进度 +
  composer,上传 drop zone 收归 pane 内
- ConversationPaneHostEnvironment 注入宿主依赖,后台 pane
  经 ConversationPaneHarness 按 spec 挂载
- TerminalPaneHost 桥接终端 pane 运行时
- 契约测试锁定 controller/registry/harness 行为与 flag 默认值
- 会话/项目行支持指针拖入工作台,菜单新增「在分屏中打开」(Columns2)
- 终端 tab 可拖出 dock 成为终端 pane,grip 保留 tab 排序
- dock 隐藏被 pane 租用的会话(hiddenSessionIds)
- composer 紧凑断点从视口 media query 改为容器查询(@container),
  分屏内窄 pane 在宽屏上也能正确收起工具栏文案
- 转录空态改为相对滚动视口居中,竖向分屏半高 pane 不再溢出
- flag 开启时以 WorkbenchCanvas 承载多 pane,关闭时保持单会话路径
- 拖拽落点、分屏命令与布局持久化在页面层编排
- 原生文件拖拽上报悬停坐标,drop 落入悬停 pane
- 未持久化草稿 pane 重获焦点时以缓存为准,不再触发加载
- 同步 DOM 边界/文件拖拽路由/任务进度测试
- OPEN_SURFACE/MOVE_PANE 可携带画布像素上下文,两半低于会话硬最小尺寸时
  返回 insufficient-space 且布局不变;缺省上下文保持旧的宽松语义
- MOVE_PANE 以摘除后的树测量目标区域,腾出的空间参与判定
- geometry 抽出 splitRegionForTarget/canSplitRectOnAxis 供 reducer 与拖拽复用
- invariants 新增 findParentSplitId,支撑 pane 级等分命令
- pane 树测试覆盖水平/垂直不足、divider/canvas-edge 与 detach 后测量
- encode 按字段逐一重建 node/surface/pane,内存里多挂的属性不再落盘
  (持久化 payload 直达 SQLite/localStorage,无中间脱敏层)
- unsupported surface 仍整体透传 raw,保证新版本 round-trip 不丢字段
- PaneViewState 收敛为空槽位类型,decode/encode 双向丢弃未知 view 键
- 新增布局隐私测试:锁定持久化 shape、注入字段剥离与透传契约
- Idle→Armed→Dragging→Commit/Cancel 迁移与落点归一化收敛为纯 reducer,
  useWorkbenchDragSession 只剩 DOM 事件适配(监听、点击抑制、光标)
- 落点解析规则不变:own-pane 焦点化、侧栏 payload 自动停靠、
  空间不足的 split 目标一律拒绝
- 阈值/窄画布判定导出为 exceedsDragThreshold/canvasAllowsPointerSplit
- 新增状态机测试:武装/激活/取消/单次提交与各 payload 落点语义
- sshSessionStatus/sshSessionEndpointLabel 抽为共享模块:以后端 ssh.status
  为准,进程已停一律视为 disconnected,未知状态保守处理
- SSH Pane 在 Local surface 之上叠紧凑状态行(状态点/端点/重连按钮),
  SFTP 仍留在 workspace overlay,Pane 内只承载 shell 视口
- 状态推导与端点标签测试覆盖 running/status 组合
- 悬停显示的 kill 按钮:点一次武装、再点执行,3s 超时/失焦自动复位
- 与 Pane 关闭(Detach,进程保留回 dock)语义分离;省略回调则无入口
- 被画板 Pane 租用的会话在 overlay 内显示"已在画板中打开"占位,
  可一键前往 Pane;SFTP tab 走独立通道不受互斥影响
- shell tab pointerdown 上报拖出意图(SFTP tab 不可拖),阈值与点击抑制
  由工作台拖拽会话统一处理
- 状态推导/端点标签改用共享 sshSessionStatus 模块
- 按钮 pointerdown 上报拖出意图(仅鼠标/笔主键),点击行为不变
- 回调缺省时 dock 行为与拖拽入口完全不变
- sshTerminal surface 渲染 SshTerminalPaneSurface,本地继续走 Local
- 显式 kill:结束进程→回收绑定→页面收尾关 Pane,失败也按 kill 语义收尾
- SSH 重连注入宿主调用,"already in progress"视为自动重连已接管不报错
- SSH overlay shell tab 与空态"新建终端"接入工作台拖拽通路,
  租约会话提供"前往 Pane"聚焦
- 快捷键统一到 Meta/Ctrl+Alt:Shift+方向移动 pane、W 关闭、=/+ 等分
  父级 split;少于两个 pane 时全部短路
- pane 区域标签按 surface 细分(会话标题+工作区/终端/不支持),
  屏幕阅读器可区分各 pane
- unsupported 占位改用 UnsupportedPaneSurface 组件
- 新增 workbench_layout_delete 命令并注册
- 前端 clearCorrupted 在原生环境下同时删除 SQLite 行,
  避免下次启动重新加载同一份损坏 payload
- 强制对比色下主题背景被丢弃,改用 CanvasText/Highlight 系统色保证可见
- 分隔条过渡动画尊重 motion-reduce
- commitTerminalDrop 各 payload 的落点提交、revision CAS 与失败回滚
- 绑定/租约 store 的单消费约束与源码契约断言
- 后台 pane 会话经 runtime registry/surface controller 流式更新时,
  transcript/草稿互不串扰
- stop 链路的 tool status 断言写入所属会话自己的 transcript store,
  stub 改为按会话建 store,共享对象无法再掩盖串写
- 新增 streamHandleRegistry:Map<sessionId, Set<handle>> 定桶,
  output 事件从遍历全部 handle 过滤改为 O(1) 派发
- 桶内快照遍历防 accept 中 dispose 自删破坏迭代,空桶回收
- tauriTerminalClient 换用注册表,handle 记录 sessionId 供
  dispose 定桶清理;terminal:event 普通订阅保持广播语义
- @xterm/addon-webgl@0.19.0(与 xterm 6.0 同 release train):
  构造失败静默回退 DOM 渲染器,onContextLoss 时 dispose 回退,
  cleanup 先释放 addon 再 term.dispose
- resize 两级化:视觉 fit 以 80ms 节流在拖动过程中跟手执行,
  PTY resize 独立 100ms 尾沿去抖,结束后保证最终尺寸送达
- 原为全部挂载 + CSS hidden,每个隐藏 tab 常驻一条 attach 流
- 改为仅活跃 tab 渲染 XTermViewport,非活跃卸载释放流;
  切回时经 offset 快照重建 scrollback,initialSnapshot 语义不变
- 新增 auto-launch 授权集:本会话内显式创建(拖入/newTerminal)
  的 surfaceId 才自动 ensure 建会话;恢复的 Pane 进入休眠占位,
  点重启才按 launchSpec 重建。非消费式,抗 StrictMode 双挂载
- filterLayoutToLiveSurfaces 终端 pane 一律保留,attemptRestore
  的 liveTerminalSurfaceIds 参数移除;terminal_list 失败也不丢
  Pane——占位本身不建 PTY,天然安全
- commitTerminalDrop 的 newTerminal/绑定命中路径接 authorize
- surfaceMinSize:conversation 沿用 320x220;终端 220x140(按
  fontSize 13/lineHeight 1.3 折算约 20 cols x 6 rows 加 chrome);
  unsupported 160x120
- subtreeMinSizeForAxis 递归求子树最小需求(同轴求和加分隔条、
  异轴取 max),canSplitRectForMinSizes/clampRatioToSideMinSizes
  支持双侧非对称 clamp;超小窗口退化为对称 clamp 不崩溃
- reducer OPEN/MOVE 预检按被挤压侧与入侵侧的 kind 计算空间,
  RESIZE_SPLIT 有 context 时按双侧子树需求 clamp;旧 API 保留
  为薄封装,conversation 行为不变
- resolveWorkbenchDropTarget 四类落点(pane-edge/自动贴靠/
  canvas-edge/divider)按 payload kind 取入侵侧最小尺寸,被挤压
  侧按 pane kind 或子树需求判定,空间不足不给落点
- DividerLayer 新增 minSizesForSplit:拖分隔条时终端侧可压到
  220px、会话侧保 320px;WorkbenchCanvas 从 layout 计算每个
  split 双侧子树需求传入
- PaneSurfaceLayer 由 rect 宽度 < 360px 派生 isCompact,与
  view.compactChrome 显式覆盖位 OR;渲染期派生不写回 layout,
  resize 抖动零 revision 开销
- PaneChrome 紧凑时拖柄收窄、pill 缩短,加 data 锚点
- SshTerminalPaneSurface 新增 latencyMs:<100 绿 / <300 黄 /
  其余红,未知显示 --;紧凑宽度下隐藏端点标签只留状态点与延迟
- i18n 新增 workbench.sshLatency(en/zh)
- SSH Pane 仅聚焦且 running 时以 15s 轮询 sshLatency,失败静默
  置空,卸载/降级清理定时器
- 恢复路径无 auto-launch 授权时进入休眠占位(exited 复用重启
  按钮),点击授权后走常规 ensure
- ChatPage 恢复对账不再向 attemptRestore 传存活集,布线
  isCompact 与 authorizeAutoLaunch
- 拖入链路 bind→open→acquire 后 dock 隐藏集正确
- 陈旧 release 令牌重放不误杀新租约;detach 后可再拖入
- 冲突 acquire 抛错且原租约完好;授权集非消费式语义
- Rust 侧新增 canonicalize_workdir_within,create/create_ssh 双路径强制校验,
  删除 project_path_key 缺省时由 cwd 反推的漏洞
- 前端 projectScope.ts 词法护栏:拖放提交、布局恢复、invariants 三道
  (新增 terminal-cwd-outside-project issue)
- SSH launchSpec.cwd 语义统一为本地 project 锚点(SFTP local root),
  校验对两种终端 surface 一致生效,修正误导性测试夹具
- 本地会话由过滤改为保留 + leased 标记,视口层互斥防输出流双消费
- 终端 tab 右键/键盘菜单「在工作台打开/聚焦工作台面板」补齐键盘可达性
- 空态新建终端拖柄接到 RightDockLauncher 真实渲染路径,删除不可达死代码
- SSH Pane 断开改用专属两态确认文案
- useWindowWorkbench 在 dispatch 内从 geometryRef 注入 canvasSize 与
  dividerSize,拖放/菜单/键盘/resize/终端管线一次覆盖
- insufficient-space 经 onCommandError 转 noSpaceForSplit 提示
- killSession 同时撤销启动资格,防止 kill 被 ensure 变成静默重启
- 关闭语义更新为 Detach-first 裁决(含理由)
- 测试矩阵与工作项勾选同步;新增剩余工作记录
  (Permission-Changed 无信号的调研证据、R-1~R-3 立项)
@su-fen
su-fen marked this pull request as ready for review August 17, 2026 03:39
@github-actions

Copy link
Copy Markdown
Contributor

PR governance checks failed — this PR has been converted to draft.

  • No linked issue: the PR body must contain Closes #123 / Fixes #123 / Resolves #123. This project requires an issue before a PR — see the contribution guidelines.
  • UI change without screenshots: this PR modifies frontend code. Please add before/after screenshots or a recording under "Screenshots / preview" in the PR body.

Fix the items above, then click Ready for review to re-run the checks.

@StackCairn
StackCairn marked this pull request as draft August 17, 2026 03:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant