# Agent Note: Workspace 侧边栏顺序与折叠

Status: implemented

[English](2026-08-11-workspace-sidebar-order-and-folding.md) | 中文

## 问题

Session 很多的 Workspace 会占满整个侧边栏，把其他 Workspace 挤出可见范围。紧凑列表需要有界的默认高度，同时仍要提供到达每条 Session 的明确入口。侧边栏还需要面向活动时间的顺序，但 `WorkspaceView.sessionIds` 是持久的手动记账，不能被 Session 活动改写。

Workspace 分组本身没有用户可控的持久顺序。浏览器原生拖拽还会把列表外松手判为拒绝，并把行弹回原位，即使应用仍持有有效插入标记。Workspace 展开后，若只按组头命中，两个分组之间的视觉边界也不再等于任一组头的中点。

## 决策

### Workspace 顺序

Workspace 注册表持有持久 `workspaceIds` 顺序，并提供采用 DOM `insertBefore` 语义的 `insertBefore(id, beforeId?)`。Host RPC `workspace.insertBefore` 返回完整的已提交顺序；单纯顺序变更通过 `host/workspace-order-changed` 推送同一份完整顺序。未知来源或锚点 id 以 `workspace-not-found` 拒绝；以自身为锚点或移动到当前位置不会写入。

客户端对 Workspace 拖拽进行乐观安装。请求代次与帧代次保证只有最新一元回声可以替换本地顺序，且更新的 Host 帧优先于旧响应；最新请求被拒时会恢复最近一份由 Host 基线、帧或当前一元回声确认的完整顺序。每次成功的列表基线都会恢复 Host 顺序，因此重连会接纳其他位置提交的持久变更。

### Session 折叠与视图顺序

每个 Workspace 持久化一项浏览器本地打开状态：关闭表示零条 Session 行，打开表示最多五条非空白行，再把当前选中的空白“新会话”作为一条临时额外行。**展开其余**只在当前挂载期间显示仍被隐藏的条目；关闭整个 Workspace 会清除此临时展开，因此重新打开时恢复为有界折叠投影。只有在用户尚未为该 Workspace 存储明确状态时，当前 Session 所在分组才会自动打开。从 Workspace 行创建 Session 时会在启动 Session 前打开目标分组，使状态传播完成后新行保持可见。就绪的 Workspace 基线发生变化后，浏览器会移除基线中不存在 id 的展开状态、顺序和已观察时间戳记录，同时保留 Ungrouped 和单列表记账。

组合视图菜单在分组和单列表呈现中都提供**手动排序**和**最近更新**，每个记账各自持有一份浏览器本地持久顺序。真实 Workspace 从 `WorkspaceView.sessionIds` 初始化；Ungrouped 和跨 Workspace 的单列表从最近更新时间顺序初始化，且没有 Host Session 记账。进入最近更新时会执行一次完整的时间排序；后续 user prompt 或 steer 会将对应 Session 置顶一次，拖拽仍可编辑所得顺序。返回手动排序会保留当前顺序，只停用后续活动置顶。真实 Workspace 在手动模式下的拖拽还会写入 Host Session 记账，而 Ungrouped 和单列表的拖拽与活动置顶保留在浏览器本地。单列表没有父级层次，因此不显示空的左侧状态槽；存在可见状态时仍保留该槽。

创建“新会话”并选中空白 Session 时，浏览器会在其分组记账和单列表记账中各置顶一次。这次明确的创建置顶不会推进 `updatedAt`。Session 保持空白期间，分组折叠不会让它占用五条非空白行配额；首条提示词落地后，该行恢复普通配额，同时不会撤销手动模式下的拖拽。

### 拖拽与紧凑界面

Workspace 命中测试使用完整渲染分组区段，包括可见 Session 行。前一分组的下半部与后一分组的上半部共享同一条插入边界，指示器是一条带有相连右向尖角且不影响布局的绝对定位横线。树主体覆盖层会在滚动裁切区外以相同的负偏移绘制第一条边界，因此左侧尖角保持可见，列表位置也不会改变。折叠状态的 Session 拖拽按渲染行确定插入边界，把来源行放在该边界处所有隐藏记账成员之前，并拒绝会隐藏来源行的结果。Workspace 或 Session 拖拽期间，文档级 `dragover` 与 `drop` 处理器会接受原生操作；若在 Workspace 列表外松手，`dragend` 会提交最后一个有效标记。

搜索在折叠时是区头操作，展开后占据标题与尾部操作的空间。查询经清除首尾空白后为空时，点击外部会收起搜索；非空查询则会保留；轨道搜索手势仍在进行期间，外部点击监听器保持未挂载（[轨道搜索自我收起](../bug-fix/2026-08-18-rail-search-outside-click-self-dismissal.zh.md)）。紧凑的 Workspace 与 Session 行、24px 底部渐隐以及取消每个 Workspace 的 Session 数量共同节省纵向空间，同时保留导航入口。

## 考虑过的替代方案

**把每次活动置顶写入 `Workspace.sessionIds`。** 浏览器呈现偏好会在用户每次提交提示词时覆盖共享的 Host 记账。

**为手动排序和最近更新分别保留独立顺序。** 切换模式会用另一份顺序中的旧位置替换可见列表，而选择手动排序只表示后续活动不再移动条目。

**打开 Workspace 时始终显示全部 Session。** 大型 Workspace 仍会挤占其他分组；只记忆整个分组的打开状态无法限制其高度。

**持久化展开剩余状态。** 很久以后重新打开 Workspace 时，它可能意外占满侧边栏。只有有界折叠状态属于稳定导航偏好；显示剩余项只是一次本地查看。

**使用数字下标或只按组头命中拖拽。** 拖拽期间行发生变化会使下标漂移；Workspace 展开时，组头中点与可见边界不一致。锚点 id 与完整区段几何在两种情况下都保持稳定。

**让浏览器拒绝列表外松手。** 应用会提交最后一个有效标记，而浏览器同时播放拒绝动画，形成相互矛盾的反馈。

## 后果

- Workspace 顺序通过 Host 持久并共享；分组方式、打开状态、每个记账的 Session 视图顺序和查询状态仍是浏览器本地呈现偏好。Ungrouped 和单列表支持相同的拖拽与置顶规则，但因没有单一 Workspace 记账，其顺序只保存在浏览器本地。
- 最近更新模式会在进入时执行完整时间排序，随后保持手动调整，直到 user prompt 或 steer 推进某条 Session 并将其置顶。返回手动排序会保留所有当前位置。
- 新选中的空白“新会话”行会在分组和单列表顺序中各置顶一次。分组折叠会在五条非空白行之外额外渲染该行，直至首条提示词落地后恢复普通配额。
- 未执行明确的**展开其余**手势时，打开 Workspace 最多显示五条非空白 Session；当前选中的空白“新会话”可以增加一条临时行。关闭分组只重置这项临时手势。
- Host Session 记账继续采用[会话列表浏览与 Workspace 手动排序](2026-07-25-session-list-browsing-and-manual-order.zh.md)确立的手动顺序含义。

## 测试

领域与 Host 测试覆盖持久 Workspace 移动、无操作与无效锚点、重启恢复、完整顺序 RPC 响应、顺序帧以及每条 Host stream 基线只读取一份 Workspace 快照。运行时测试覆盖乐观顺序、帧／响应优先级、重叠拒绝后恢复 Host 已确认顺序、重连基线以及 New Session 目标优先级。UI 测试覆盖五行折叠、空白行配额与隐藏数量、跨隐藏行的折叠拖拽锚点、临时展开重置、Workspace 移除后清理持久状态、保持顺序的模式切换、一次性最近更新与“新会话”置顶、首条提示词落地后保留手动拖拽、浏览器本地 Ungrouped 与单列表拖拽持久化、无层级单列表行左侧间距、当前视图标记、展开区段的 Workspace 命中、未裁切的第一条插入边界、列表外 Workspace 与 Session 松手、搜索收起规则和紧凑 CSS 尺寸。真实组合 Web 快照固定五条既有行与临时“新会话”并列显示。
