diff --git a/.agents/notes/implemented/feature/2026-08-11-workspace-sidebar-order-and-folding.i18n.yaml b/.agents/notes/implemented/feature/2026-08-11-workspace-sidebar-order-and-folding.i18n.yaml index a389aedae9..f8d950d416 100644 --- a/.agents/notes/implemented/feature/2026-08-11-workspace-sidebar-order-and-folding.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-08-11-workspace-sidebar-order-and-folding.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-08-11-workspace-sidebar-order-and-folding.md -2026-08-11-workspace-sidebar-order-and-folding.md: 799c972ead9ac5d56fa70d2f6eedda58986ab65e -2026-08-11-workspace-sidebar-order-and-folding.zh.md: 7ac93e08aeaea5ddd455f5cde0395c667a38cca1 +2026-08-11-workspace-sidebar-order-and-folding.md: 223f164b12415e98e5c21ce472e8094dfc13747f +2026-08-11-workspace-sidebar-order-and-folding.zh.md: dc3ca4fb3a75f2dca289394df088d0efee08f4f2 diff --git a/.agents/notes/implemented/feature/2026-08-11-workspace-sidebar-order-and-folding.md b/.agents/notes/implemented/feature/2026-08-11-workspace-sidebar-order-and-folding.md index 799c972ead..223f164b12 100644 --- a/.agents/notes/implemented/feature/2026-08-11-workspace-sidebar-order-and-folding.md +++ b/.agents/notes/implemented/feature/2026-08-11-workspace-sidebar-order-and-folding.md @@ -22,17 +22,19 @@ The client installs a Workspace drag optimistically. Request and frame generatio Each Workspace persists one browser-local open state: closed means zero Session rows and open means up to five. When more Sessions exist, **Show more** reveals the remainder only for the current mount; closing the whole Workspace clears this transient expansion, so reopening returns to five. The current Session's group opens automatically only when the user has not already stored an explicit state for that Workspace. -The combined view menu offers **Manual** and **Last updated**. Manual follows the Host account in `WorkspaceView.sessionIds`. Last updated maintains a browser-local per-Workspace order that users may still edit by dragging; whenever a Session summary's `updatedAt` advances, that Session is promoted to the front. This view order never writes the Host Session account. The flat list uses recent-update order because it has no single Workspace account for durable Session drag. +The combined view menu offers **Manual** and **Last updated** over one browser-local, persisted per-Workspace order initialized from `WorkspaceView.sessionIds`. Entering Last updated performs one complete recency sort; a later user prompt or steer promotes that Session once, and dragging may edit the resulting order. Returning to Manual preserves the current order and only disables later activity promotion. Manual-mode drags also write the Host Session account, while activity promotion remains a browser-local presentation choice. The flat list uses recency because it has no single Workspace account for durable Session drag. ### Drag and compact chrome -Workspace hit testing uses the complete rendered group section, including visible Session rows. One insertion boundary is shared by the preceding group's lower half and the following group's upper half, and the indicator is an absolutely positioned line that does not affect layout. During a Session drag, document-level `dragover` and `drop` handlers accept the native operation; if release occurs outside the Workspace list, `dragend` commits the last valid marker. +Workspace hit testing uses the complete rendered group section, including visible Session rows. One insertion boundary is shared by the preceding group's lower half and the following group's upper half, and the indicator is an absolutely positioned line that does not affect layout. A tree-body overlay draws the first boundary at the same negative offset outside the scrolling clip, so its hollow leading circle remains visible without moving the list. During a Session drag, document-level `dragover` and `drop` handlers accept the native operation; if release occurs outside the Workspace list, `dragend` commits the last valid marker. Search is a header action while collapsed and expands across the title and trailing actions. An outside click collapses an empty search but retains a non-empty query. Compact Workspace and Session rows, a 24px bottom fade, and the absence of per-Workspace Session counts preserve vertical space without removing navigation affordances. ## Alternatives considered -**Persist the recent-update view into `Workspace.sessionIds`.** Activity would overwrite a deliberate manual order and recreate two competing meanings for the same Host field. +**Write every activity promotion into `Workspace.sessionIds`.** A browser presentation preference would overwrite the shared Host account whenever a user submits a prompt. + +**Keep independent Manual and Last updated orders.** Switching modes would replace the visible list with stale positions from the other order, even though choosing Manual only means that later activity stops moving rows. **Always show every Session in an open Workspace.** One large Workspace would continue to crowd out the rest, and remembering only the whole-group open state would not bound its height. @@ -44,11 +46,11 @@ Search is a header action while collapsed and expands across the title and trail ## Consequences -- Workspace order is durable and shared through the Host, while grouping, open state, recent-update Session order, and query state remain browser-local presentation preferences. -- Recent-update mode preserves manual edits until a Session becomes active again; a newer `updatedAt` intentionally promotes that Session to the front. +- Workspace order is durable and shared through the Host, while grouping, open state, the shared Session view order, and query state remain browser-local presentation preferences. +- Last updated performs a complete recency sort on entry, then preserves manual adjustments until a user prompt or steer advances one Session and moves it to the front. Returning to Manual preserves every current position. - Opening a Workspace never shows more than five Sessions without an explicit **Show more** gesture, and closing it resets only that transient gesture. - The Host Session account retains the manual-order meaning established by [Session List Browsing and Manual Workspace Order](2026-07-25-session-list-browsing-and-manual-order.md). ## Testing -Domain and Host tests cover durable Workspace moves, no-op and invalid anchors, restart recovery, full-order RPC responses, and order frames. Runtime tests cover optimistic order, frame/response precedence, rejection rollback, reconnect baselines, and New Session target priority. UI tests cover five-row folding, transient expansion reset, recent-update promotion with manual drag, selected view indicators, expanded-section Workspace hit testing, outside-list Session drops, search collapse rules, and compact CSS dimensions. +Domain and Host tests cover durable Workspace moves, no-op and invalid anchors, restart recovery, full-order RPC responses, and order frames. Runtime tests cover optimistic order, frame/response precedence, rejection rollback, reconnect baselines, and New Session target priority. UI tests cover five-row folding, transient expansion reset, order-preserving mode switches, one-time recent-update promotion, selected view indicators, expanded-section Workspace hit testing, an unclipped first insertion boundary, outside-list Session drops, search collapse rules, and compact CSS dimensions. diff --git a/.agents/notes/implemented/feature/2026-08-11-workspace-sidebar-order-and-folding.zh.md b/.agents/notes/implemented/feature/2026-08-11-workspace-sidebar-order-and-folding.zh.md index 7ac93e08ae..dc3ca4fb3a 100644 --- a/.agents/notes/implemented/feature/2026-08-11-workspace-sidebar-order-and-folding.zh.md +++ b/.agents/notes/implemented/feature/2026-08-11-workspace-sidebar-order-and-folding.zh.md @@ -22,17 +22,19 @@ Workspace 注册表持有持久 `workspaceIds` 顺序,并提供采用 DOM `ins 每个 Workspace 持久化一项浏览器本地打开状态:关闭表示零条 Session 行,打开表示最多五条。存在更多 Session 时,**展开其余**只在当前挂载期间显示剩余项;关闭整个 Workspace 会清除此临时展开,因此重新打开时恢复为五条。只有在用户尚未为该 Workspace 存储明确状态时,当前 Session 所在分组才会自动打开。 -组合视图菜单提供**手动排序**和**最近更新**。手动排序遵循 `WorkspaceView.sessionIds` 中的 Host 记账。最近更新为每个 Workspace 维护一份浏览器本地顺序,用户仍可通过拖拽编辑;每当 Session 摘要的 `updatedAt` 增大时,该 Session 会被移到最前。此视图顺序绝不写入 Host Session 记账。平铺列表使用最近更新顺序,因为它没有可承载持久 Session 拖拽的单一 Workspace 记账。 +组合视图菜单在同一份按 Workspace 持久于浏览器本地、由 `WorkspaceView.sessionIds` 初始化的顺序上提供**手动排序**和**最近更新**。进入最近更新时会执行一次完整的时间排序;后续 user prompt 或 steer 会将对应 Session 置顶一次,拖拽仍可编辑所得顺序。返回手动排序会保留当前顺序,只停用后续活动置顶。手动模式下的拖拽还会写入 Host Session 记账,而活动置顶仍属于浏览器本地呈现选择。平铺列表没有可承载持久 Session 拖拽的单一 Workspace 记账,因此使用最近更新顺序。 ### 拖拽与紧凑界面 -Workspace 命中测试使用完整渲染分组区段,包括可见 Session 行。前一分组的下半部与后一分组的上半部共享同一条插入边界,指示器是一条不影响布局的绝对定位横线。Session 拖拽期间,文档级 `dragover` 与 `drop` 处理器会接受原生操作;若在 Workspace 列表外松手,`dragend` 会提交最后一个有效标记。 +Workspace 命中测试使用完整渲染分组区段,包括可见 Session 行。前一分组的下半部与后一分组的上半部共享同一条插入边界,指示器是一条不影响布局的绝对定位横线。树主体覆盖层会在滚动裁切区外以相同的负偏移绘制第一条边界,因此左侧空心圆保持可见,列表位置也不会改变。Session 拖拽期间,文档级 `dragover` 与 `drop` 处理器会接受原生操作;若在 Workspace 列表外松手,`dragend` 会提交最后一个有效标记。 搜索在折叠时是区头操作,展开后占据标题与尾部操作的空间。点击外部会收起空搜索,但保留非空查询。紧凑的 Workspace 与 Session 行、24px 底部渐隐以及取消每个 Workspace 的 Session 数量共同节省纵向空间,同时保留导航入口。 ## 考虑过的替代方案 -**把最近更新视图持久化到 `Workspace.sessionIds`。** 活动会覆盖用户明确安排的手动顺序,并让同一 Host 字段重新承担两种相互竞争的含义。 +**把每次活动置顶写入 `Workspace.sessionIds`。** 浏览器呈现偏好会在用户每次提交提示词时覆盖共享的 Host 记账。 + +**为手动排序和最近更新分别保留独立顺序。** 切换模式会用另一份顺序中的旧位置替换可见列表,而选择手动排序只表示后续活动不再移动条目。 **打开 Workspace 时始终显示全部 Session。** 大型 Workspace 仍会挤占其他分组;只记忆整个分组的打开状态无法限制其高度。 @@ -44,11 +46,11 @@ Workspace 命中测试使用完整渲染分组区段,包括可见 Session 行 ## 后果 -- Workspace 顺序通过 Host 持久并共享;分组方式、打开状态、最近更新 Session 顺序和查询状态仍是浏览器本地呈现偏好。 -- 最近更新模式会保持手动调整,直到某条 Session 再次活跃;更大的 `updatedAt` 会有意把它移到最前。 +- Workspace 顺序通过 Host 持久并共享;分组方式、打开状态、共享 Session 视图顺序和查询状态仍是浏览器本地呈现偏好。 +- 最近更新模式会在进入时执行完整时间排序,随后保持手动调整,直到 user prompt 或 steer 推进某条 Session 并将其置顶。返回手动排序会保留所有当前位置。 - 未执行明确的**展开其余**手势时,打开 Workspace 最多显示五条 Session;关闭分组只重置这项临时手势。 - Host Session 记账继续采用[会话列表浏览与 Workspace 手动排序](2026-07-25-session-list-browsing-and-manual-order.md)确立的手动顺序含义。 ## 测试 -领域与 Host 测试覆盖持久 Workspace 移动、无操作与无效锚点、重启恢复、完整顺序 RPC 响应和顺序帧。运行时测试覆盖乐观顺序、帧/响应优先级、拒绝回滚、重连基线以及 New Session 目标优先级。UI 测试覆盖五行折叠、临时展开重置、最近更新置顶与手动拖拽、当前视图标记、展开区段的 Workspace 命中、列表外 Session 松手、搜索收起规则和紧凑 CSS 尺寸。 +领域与 Host 测试覆盖持久 Workspace 移动、无操作与无效锚点、重启恢复、完整顺序 RPC 响应和顺序帧。运行时测试覆盖乐观顺序、帧/响应优先级、拒绝回滚、重连基线以及 New Session 目标优先级。UI 测试覆盖五行折叠、临时展开重置、保持顺序的模式切换、一次性最近更新置顶、当前视图标记、展开区段的 Workspace 命中、未裁切的第一条插入边界、列表外 Session 松手、搜索收起规则和紧凑 CSS 尺寸。 diff --git a/packages/client/ui-workspace/src/client/WorkspaceBrowser.module.css b/packages/client/ui-workspace/src/client/WorkspaceBrowser.module.css index ee79d4b56a..3d61895fe7 100644 --- a/packages/client/ui-workspace/src/client/WorkspaceBrowser.module.css +++ b/packages/client/ui-workspace/src/client/WorkspaceBrowser.module.css @@ -299,7 +299,9 @@ margin-left: -4px; margin-right: calc(-1 * var(--dsh-session-list-edge-inset)); padding-left: 4px; - overflow: hidden; + /* The list remains the scroll clip. This seat stays visible so the + absolutely positioned first-boundary marker can occupy the header gap. */ + overflow: visible; } .rail .listArea { @@ -387,6 +389,7 @@ margin-top: 4px; } +.listTopDropIndicator, .workspaceDropBefore::before, .workspaceDropAfter::after { content: ''; @@ -408,6 +411,18 @@ pointer-events: none; } +/* The first insertion boundary keeps the same -8px coordinate as every + Workspace boundary, but lives outside the scrolling clip. */ +.listTopDropIndicator { + top: -8px; + left: -4px; + right: calc(var(--dsh-session-list-edge-inset) + 4px); +} + +.listTopDropActive > .workspaceDropBefore:first-child::before { + display: none; +} + .workspaceDropBefore::before { top: -8px; } diff --git a/packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx b/packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx index 305cabad08..cbc2e05159 100644 --- a/packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx +++ b/packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx @@ -299,15 +299,13 @@ function SessionTree({ ? group.sessions.length : group.sessions.findIndex(session => session.id === anchor) if (sourceIndex !== -1 && (anchorIndex === sourceIndex || anchorIndex === sourceIndex + 1)) return - if (orderBy === 'updated') { - const account = orderedWorkspaces.find(workspace => workspace.workspaceId === activeDrag.workspaceId) - if (account === undefined) return - const nextOrder = account.sessionIds.filter(id => id !== activeDrag.sessionId) - const insertAt = anchor === undefined ? nextOrder.length : nextOrder.indexOf(anchor) - nextOrder.splice(insertAt === -1 ? nextOrder.length : insertAt, 0, activeDrag.sessionId) - setRecentSessionOrder(activeDrag.workspaceId, nextOrder.map(id => id as string)) - return - } + const account = orderedWorkspaces.find(workspace => workspace.workspaceId === activeDrag.workspaceId) + if (account === undefined) return + const nextOrder = account.sessionIds.filter(id => id !== activeDrag.sessionId) + const insertAt = anchor === undefined ? nextOrder.length : nextOrder.indexOf(anchor) + nextOrder.splice(insertAt === -1 ? nextOrder.length : insertAt, 0, activeDrag.sessionId) + setRecentSessionOrder(activeDrag.workspaceId, nextOrder.map(id => id as string)) + if (orderBy === 'updated') return insertSessionBefore(activeDrag.workspaceId, activeDrag.sessionId, anchor).catch((reason: unknown) => { console.warn('session reorder rejected:', reason) }) @@ -332,10 +330,18 @@ function SessionTree({ console.warn('workspace reorder rejected:', reason) }) } + const workspaceDropAtListStart = groups[0]?.workspaceId !== undefined + && workspaceDrag?.over?.id === groups[0].workspaceId + && workspaceDrag.over.half === 'before' return (
-
+ {workspaceDropAtListStart &&