feat(chat): Session Workbench 多 Pane 工作台——多会话并列与终端 Pane(正式版默认开启,=0 回退) - #521
Conversation
- 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 普通订阅保持广播语义
冲突解决: - lib.rs: workbench_layout_* 与 settings_backup_* 命令注册并集 - App.tsx: 保留 SettingsPage 懒加载 Suspense,并入 main 的 reloadSettings - ChatPage.tsx: 保留多 Pane chatContent 架构,轨迹视图下沉集成: ConversationPaneBinding 新增可选 trajectory 槽位,当前会话 Pane 在 全局 Tabs 切到 trajectory 时以 TrajectoryView 替换转录、挂起输入区 (保持挂载,草稿不丢);背景 Pane 不提供轨迹,始终渲染常规转录。 approvalBar/FileDropOverlay 沿用已下沉到 ConversationPaneHost 的实现。
多 Pane 画板中,轨迹/对话切换以左上角圆点形式内嵌在 Pane chrome, 样式与右上角关闭点完全镜像(尺寸、配色、悬停显现);仅鼠标聚焦的 会话 Pane 显示,终端 Pane、背景 Pane 与草稿会话不显示。图标预告 点击后切换到的视图(Waypoints→轨迹,MessageSquareText→对话)。 多 Pane 时隐藏顶栏 ConversationViewTabs 避免重复入口,判定口径与 PaneSurfaceLayer 的 chromeless(paneCount < 2)一致;单 Pane 保留 顶栏 Tabs 不变。新增中英 i18n 键与 DOM 边界契约测试。
布局改为纯内存状态:应用打开直接进入单 Pane 新建会话页面,不再从 SQLite/localStorage 恢复上次的多看板布局。会话内的分屏/拖拽能力保留, 但不跨启动记忆。 删除内容: - layoutPersistence.ts / layoutStorage.ts(SQLite 适配 + crash shadow) - Rust 端 workbench_layout_load/save/delete 命令与建表语句 - useWindowWorkbench 的 attemptRestore/restoreReady/防抖保存/unload flush 与 filterLayoutToLiveSurfaces 恢复过滤 - ChatPage 的恢复触发、恢复前预取与 PaneLoadingSkeleton 分支;恢复期 终端对账调用(死绑定仍由 TerminalPaneHost 兜底清理) - index.html / AppBootShell 从 crash shadow 预渲染多 Pane 骨架的逻辑 - agent-ui codec.ts(encode/decodeWorkbenchLayout,仅持久化路径使用) - workbench-layout-privacy 测试及各测试文件中的恢复/codec 用例 老库中已存在的 workbench_layout 行保留不读,无害。
# Conflicts: # crates/agent-gui/src-tauri/src/lib.rs # crates/agent-gui/src/App.tsx # crates/agent-gui/src/pages/ChatPage.tsx
…h-multi-pane # Conflicts: # crates/agent-gui/src/pages/ChatPage.tsx
su-fen
left a comment
There was a problem hiding this comment.
结论:现在不能直接合入
CI 已全绿、分支相对 main 可快进,但 HEAD 会把 Session Workbench 作为默认桌面行为合入。PR 标题/正文仍按「草稿 / flag 灰度 / T-1 未做」描述,和实际代码不一致;同时多 Pane 下原生文件拖放会把附件挂到错误会话。
P1 — 原生文件拖放会挂到焦点会话,而不是落点 Pane
usePendingUploads.captureUploadTarget() 始终用 currentConversationIdRef + 页面级 workdir,而原生 drop 的命中测试会认任意可见 [data-file-upload-drop-zone](含背景 Pane 的 composer)。
Hover 虽然会先 handleSelectConversation,但 openInitial 是异步的,ref 要等下一轮渲染才更新。快放 / 几乎没有 hover 时,文件可以落到 Pane B,附件却写入 Pane A。
名为 “a file dropped inside a pane is attributed to that pane” 的测试只断言了几何 hit-test,没有断言 upload 的 conversationId。
相关:
crates/agent-gui/src/pages/chat/hooks/usePendingUploads.ts(captureUploadTarget)crates/agent-gui/src/pages/ChatPage.tsx(workbenchNativeDropHoverRef)crates/agent-gui/src/pages/chat/hooks/useTauriFileDrop.ts(drop 时先清 hover 再 import)crates/agent-gui/test/chat/workbench-drop-safety.test.mjs
修复方向: drop 当时从命中的 composer/pane 同步取出 conversationId(不要等 currentConversationIdRef),并补一条真正断言 upload 目标会话的测试。若暂时修不了,flag 应改回默认关闭。
冷启动现在是单 Pane,普通聊天不受影响;但默认开启后用户一分屏就会踩到这条静默串会话。
P1 — PR 描述与代码不符(默认开 vs 灰度)
| 正文仍写 | HEAD 实际 |
|---|---|
草稿;VITE_LIVEAGENT_SESSION_WORKBENCH 默认关 |
createSessionWorkbenchFeature 默认 true |
| 不改变现有单会话行为 | make dev 默认 DEV_SESSION_WORKBENCH=1,生产构建同样默认开 |
| T-1 cwd 校验未完成 | Rust canonicalize_workdir_within + 前端 projectScope.ts / invariants 已落地 |
| 布局恢复待验证 | 布局持久化已删除,启动一律单 Pane 新会话 |
按现在这样合,等于把约 +19k 行的默认 UX 变更当成实验 PR 推进 main。请先改标题/正文,写清:默认开还是关、持久化已移除、T-1 已做、还剩实机 QA。
P2 — 背景 Pane 的 Send 只聚焦、不发送
ChatPage 里背景 Pane 的 focusGuard:不是当前会话就只切焦点然后 return。Stop 已经按 conversationId 路由。不是静默串会话,但是双击陷阱,和 Stop 语义不一致。
看起来没问题的部分
- CI 全绿(含 Tauri Rust Check),
mergeable/CLEAN,相对main0 behind。 - T-1 cwd 校验是真的:create / create_ssh 都走
canonicalize_workdir_within,去掉了用 cwd 反推 project key。 - 终端租约、绑定兜底、exit 广播竞态、Dock 单宿主,单测覆盖很重。
VITE_LIVEAGENT_SESSION_WORKBENCH=0仍能回到旧单 Pane 路径。- 启动不再恢复多看板布局,比早期版本更安全。
残留风险(单独不构成合入否决)
- 主窗口
visible: false+ 每次page-load Started再 hide,show 完全依赖app_frontend_ready;index.html 大约重试 2s 后放弃,Rust 侧没有超时兜底。托盘 / Dock / 快捷键的show_main_window在 ready 之前会 no-op。 ChatPage从 2160 行涨到 3346 行;同一 PR 还混入了楼层导航预览卡、轨迹 chrome、启动壳。
合入前最少要做
- 修 drop 路由,或把 flag 改回默认关。 Drop 当时从命中 Pane 取出
conversationId,补 upload 目标会话测试。 - 重写 PR 标题和正文,与 HEAD 对齐。
- 至少一轮 flag 打开的人手冒烟: 双会话分屏拖文件、终端拖出 / Detach 回 dock、SSH overlay 互斥、reload。
在 drop 修掉(或明确继续灰度、默认关闭)并且描述和代码对齐之前,不建议合。
PR #521 review(4975151715)两个 P 级问题的修复: P1 拖放串会话:原生 drop 此前经 currentConversationIdRef(焦点会话)+ 页面级 workdir 决定附件归属,多 Pane 下 hover-聚焦是异步的,快速拖放会把 文件挂到旧焦点会话。现在 drop 时刻从命中的 composer 同步读取 data-file-upload-conversation-id,目标会话与其 workdir 沿 importUploadZonePaths → importReadableFilePaths → captureUploadTarget 显式传递;hover-聚焦仅保留视觉反馈职责。上限计数、目录挂载与错误路由 一并按落点会话解析。 P2 背景 Pane Send 只聚焦不发送:focusGuard 包住 onSend 形成双击陷阱, 与已按 conversationId 路由的 Stop 语义不一致。背景 Pane 改走 sendDraft 通路:空闲直发(conversationIdOverride + composerDraftOverride + uploadedFilesOverride),运行中经 enqueueComposerTurnForConversation 入队;Pane 侧 handler 发送即清 composer,失败恢复草稿。 controller.send 同步补上按目标会话取 uploads,堵住同类串会话隐患。 新增测试:native-drop-upload-conversation(drop 点归属 + upload store 落点断言 + 源级防回归)、pane-composer-send(clear/restore/单飞语义 + focusGuard 防回归)。
空草稿但有 staged uploads 时仍走 sendDraft,与焦点 Pane 的 hasSendableDraft 对齐;removePendingUpload 接受显式 conversationId,避免点背景芯片误改焦点会话。 Co-authored-by: Cursor <cursoragent@cursor.com>
Follow-up:上次复审的两个 P2 已修提交: P2 背景 Pane 纯附件 Send 是空操作 — 已修
覆盖: P2 删芯片仍走焦点会话 — 已修
覆盖: 仍未覆盖(不在这轮)
|
Cmd+Alt+方向键 / Tab 可把 caret 留在背景 composer,而 currentConversationIdRef 仍是焦点会话。importReadableFiles 现在接受显式 target,背景 Pane 传入本 pane 的 conversationId + workdir,与原生 drop 一致。 Co-authored-by: Cursor <cursoragent@cursor.com>
Follow-up:复审 P1 粘贴串会话已修提交: P1 背景 Pane 粘贴附件挂到焦点会话 — 已修原生 drop 已按落点 背景 composer 未禁用,且 Cmd/Ctrl+Alt+方向键 只切工作台焦点、不挪 caret。caret 还在 Pane B 时贴截图,附件会进焦点会话 A(跨项目时连 workdir 也会错)。Tab 进背景输入框再贴,同路径。 修复:
覆盖:
回形针仍包在 |
正式版说明
本 PR 将 Session Workbench 作为 LiveAgent 桌面端的正式版默认能力合入
main。正式版不新增“开启工作台”入口,应用启动后即可使用多会话、多终端 Pane 工作台。VITE_LIVEAGENT_SESSION_WORKBENCH未设置时启用 Workbench。VITE_LIVEAGENT_SESSION_WORKBENCH=0回到旧单 Pane 路径,仅用于故障隔离、兼容性验证和开发调试。交付范围
PaneTree、分屏、拖拽、移动、交换、关闭、Resize、Focus 和最小尺寸约束。conversationId隔离。ProjectRef,终端租约与绑定防止重复挂载和输入/输出双写。验证
Review 修复(4975151715)
data-file-upload-conversation-id,目标会话与其 workdir 沿importUploadZonePaths → importReadableFilePaths → captureUploadTarget显式传递,不再依赖异步的 hover-聚焦;上限计数、目录挂载、错误路由一并按落点会话解析。新增native-drop-upload-conversation.test.mjs断言 upload store 的真实归属。sendDraft通路,与 Stop 同语义按conversationId路由——空闲直发(conversationIdOverride+composerDraftOverride+uploadedFilesOverride),运行中入队;发送即清 composer,失败恢复草稿。controller.send同步按目标会话取 uploads。新增pane-composer-send.test.mjs。合入前发布门槛
VITE_LIVEAGENT_SESSION_WORKBENCH=0的回退用途不作为灰度发布描述。Closes #357
屏幕截图/预览