DSH / Atlas
2026-09-18implementedfeaturearchived 2026-09-30

Session pinning and the sidebar archived filter

Session pinning and the sidebar archived filter

The sidebar session list had no way to keep important sessions at the top, and archived sessions could only be reached through a settings page (`ui-settings-unarchive-sessions`) far from the list they came from. Users archived a session and then lost sight of it. A visibility filter can hide a Session without removing it from its Workspace. Saving filtered rows as the account's complete order loses hidden positions.

English

Problem

The sidebar session list had no way to keep important sessions at the top, and archived sessions could only be reached through a settings page (ui-settings-unarchive-sessions) far from the list they came from. Users archived a session and then lost sight of it.

A visibility filter can hide a Session without removing it from its Workspace. Saving filtered rows as the account's complete order loses hidden positions. Independent pin ordering also makes dragging depend on a second sequence instead of the Session order the user edits.

Decision

The dsh-workspace registry stores registry-global pin and archive sets as Session id arrays (pinnedSessionIds and archivedSessionIds), with durable pinSession/unpinSession operations. The pin array keeps the most recently pinned id first. Pinning and archival are mutually exclusive: archiving drops the session's pin in the same durable write, and pinning an archived session fails with WorkspaceArchivedSessionPinError.

Complete Session order

Each browser-local account has one complete Session sequence, including pinned and archived members. Workspace membership bounds each account; registry-global pin and archive sets never add a Session to an unrelated Workspace. Grouped, Ungrouped, and flat views use the same rules.

  • Existing members retain their positions. Missing pinned members prepend in the pin array's order. New ordinary forks enter immediately before their source, including nested forks, without inheriting its pin. Saved members keep their relative order, and visible pinned rows still lead ordinary forks. Other missing members append by descending Session updatedAt, with missing archived members last. Supplementation never duplicates an id.
  • Supplementation is an in-memory projection, not an automatic storage migration. A Session-order write records the complete supplemented sequence, including hidden archived rows. Filtering alone never rewrites Session positions.
  • A successful Pin action marks the Session pinned and moves its position to the front of its complete account sequence. Completion uses current saved positions and the latest Workspace membership, preserving reorders completed while the request was pending. Unpin removes the mark without restoring an earlier position. Both pinned and ordinary drags edit the same complete sequence; display indices never replace its hidden members.
  • The current blank New Session precedes both partitions; other pinned Sessions lead unpinned Sessions. Manual preserves the complete sequence's relative order within both partitions. Last updated sorts both partitions strictly by descending updatedAt, with Session id as the tie-break. Pin records carry only Session identities, not timestamps.
  • Archive takes precedence if both sets contain a Session. Archiving clears the Host pin but retains the Session's account position. The archive filter changes visibility and disabled-row presentation, not membership or ordering.

The existing mode-switch and provisional-New-Session rules remain: a drag selects Manual, Last updated discards the prior manual layout, entering Manual captures the current chronological order, and the selected blank Session retains its provisional first slot. An explicit Pin updates saved positions without changing the selected mode; chronological display still ignores those positions.

Archive restoration

WorkspaceRegistry.unarchiveSession removes one id from the global archive set on the same serialized operation chain as archive and other registry writes. An absent id is an idempotent no-op with no durable write or notification. Restoration preserves Workspace membership and Session logs, and it neither restores a pin nor changes saved ordering.

Unarchive performs no Session-existence probe: removing an id cannot introduce an unknown referent, so even an entry whose Session is gone can be removed without listing persistence. The Remote returns the complete archive set, and the existing archived increment carries the same state to other Clients. A Client installs a unary reply only while it remains the latest archive-set request; a newer request, follow increment, or replacement baseline supersedes it. Failed writes leave the set unchanged, and the sidebar logs the rejection while retaining the row, except the running-work refusal, which opens the stop-and-archive confirmation (archiving a Session with running work).

Archived visibility and row motion

The sidebar derives its visible rows in ui-workspace:

  • Archived rows keep their accounting slots, render grayed, and are not openable. The view-options menu owns an ArchivedFilter (default hide / show / only) applied to lists and search alike; the settings-page archived list is deleted with its package.
  • Archiving raises a toast with undo and filter-archived actions. Parent rerenders do not extend its lifetime; the fully faded banner cannot retain clickable actions.
  • Row motion uses a private React component with native position and opacity animations. It captures row positions around structural commits without observers or polling; removed rows leave inert fading copies outside the scrolling list. Initial loading, drag commits, overflow expansion/collapse, and grouping/ordering/filter switches settle immediately.

Persistence

Workspace KV, Remote values, and Client snapshots store archive and pin membership as Session id arrays, without object wrappers or pin timestamps. Missing collections default to empty; object-form pin entries are not accepted. Pinning and restoration do not change the Workspace domain version or Session-log format, and neither operation edits Session logs.

Alternatives considered

Keep the settings-page archived list. Two homes for one state; the sidebar filter shows archived rows in the browsing context they came from, so the package is removed outright.

A restore action without an archived-row filter. Hidden rows cannot expose actions. The filter makes those rows reachable, so restoration can live with the Session list instead of a separate settings page.

A separate restore delta or an existence check on unarchive. A second frame type duplicates the complete-set response and requires another merge path. An existence probe would turn removal of an orphan archive id into an error even though removal cannot introduce an unresolved id. Restoration therefore shares the archive-set protocol and omits the probe.

Render archive ids whose Session summaries are missing. Such entries could explain orphan state, but they cannot provide the normal conversation row. The sidebar displays loaded summaries; direct restoration remains available through the API, and orphan management remains outside this browser.

A separately editable pin sequence. This duplicates ordering authority and requires another drag write path. The global pin array only seeds missing pinned members; established positions belong to the complete Session sequence.

Restore pre-pin or pre-archive positions. Keeping Pin separate from manual positions would restore pre-pin slots, but Pin is an intentional reorder. A separate restoration record adds a product guarantee that this interaction does not require; hiding an archived row already preserves its existing position.

Rank pins by their pin timestamp. This makes Last updated mean something other than Session activity. A newly pinned old Session need not lead the pinned partition in that mode; Manual shows the Pin action's first position.

Persist every derived or filtered list. Rendering and visibility changes are not ordering commands. An incomplete or filtered projection cannot replace the complete saved sequence.

Animate every list pass. Revealing hidden rows, committing a drag, or switching the filter moves rows wholesale and reads as exaggerated motion; those passes apply instantly while pin jumps, archive fades, and neighboring-row movement retain their animations.

Consequences

The sidebar owns archived visibility end to end and settings loses a package. The pin set is registry-global, so pins survive grouping and ordering switches. Row animation owns no persistent state or dependency.

Filtering and reordering share one complete membership sequence, so hidden archives survive drags and filter switches. Older saved orders recover omitted members in memory and adopt the supplemented sequence on their next Session-order write. Host pin/archive membership semantics remain unchanged. The browser gives up pin-time promotion in Last updated and automatic restoration of pre-pin positions.

An archived id without a loaded Session summary has no visible row or sidebar restore action, even though the API can remove it. This UI limit does not change the registry's idempotent restoration behavior.

Unit suites cover supplementation, complete-sequence dragging, Pin completion after intervening changes, New Session priority, archived navigation guards, filters, toast lifetime, and animation lifecycle. Workspace and Controller tests cover pin/archive exclusivity, identity-only persistence and snapshots, idempotent no-op restoration, missing-Session restoration without persistence probes, restart recovery, and stale unary replies superseded by newer requests or pushed state. The keyless sidebar restore scenario verifies the row action and reload durability.

Related

中文

Problem

侧边栏会话列表无法把重要会话固定在顶部,已归档会话只能通过远离列表的设置页(ui-settings-unarchive-sessions)访问。用户归档一个会话之后就再也看不到它了。

显隐筛选可以隐藏会话,但不改变它的 Workspace 归属。把筛选后的行保存成所属列表的完整顺序,会丢失隐藏项的位置。单独维护可编辑的置顶顺序,也会让拖拽依赖另一份序列,而不是用户正在编辑的会话顺序。

Decision

dsh-workspace registry 将全局置顶与归档集合存为会话 id 数组(pinnedSessionIds 和 archivedSessionIds),并提供持久化的 pinSession/unpinSession 操作。置顶数组把最近置顶的 id 放在前面。置顶与归档互斥:归档会在同一次持久化写入中移除该会话的置顶,置顶已归档会话则以 WorkspaceArchivedSessionPinError 失败。

完整会话顺序

每份浏览器本地排序记录包含一条完整会话序列,包括置顶和归档成员。每份序列受 Workspace 成员关系约束,全局置顶与归档集合不会把会话补进无关的 Workspace。分组、Ungrouped 和平铺视图使用同一套规则。

  • 已有成员保留位置。缺失的置顶成员按置顶数组中的顺序补到头部。新增的普通 fork 紧邻来源之前插入,包括连续 fork,不继承来源的置顶。已保存成员保持相对顺序,可见置顶行仍排在普通 fork 之前。其他缺失成员按会话 updatedAt 降序追加,缺失的归档成员放在最后。补齐不重复加入同一个 id。
  • 补齐是内存派生,不自动迁移存储。一次会话顺序写入保存补齐后的完整序列,包括隐藏的归档行。单纯切换筛选不重写会话位置。
  • 置顶成功后,会话被标记为置顶,同时移到所属完整序列的首位。完成时使用当前保存的位置与最新 Workspace 成员关系,保留请求等待期间完成的重排序。取消置顶只移除标记,不恢复更早的位置。置顶行与普通行的拖拽都修改同一条完整序列,显示下标不会覆盖其中的隐藏成员。
  • 当前空白新会话排在两个分区之前,其余会话再按置顶区、非置顶区排列。手动模式下,两区分别保持完整序列中的相对顺序。最近更新模式下,两区都严格按 updatedAt 降序排序,以会话 id 打破平局。置顶记录只包含会话身份,不包含时间戳。
  • 会话同时命中两个集合时,归档优先。归档会清除 Host 的置顶标记,但保留会话在所属序列中的位置。归档筛选只改变可见性和禁用行的展示,不改变成员关系或排序。

既有的模式切换与临时新会话规则保持不变:拖拽选择手动模式,最近更新丢弃先前的手动布局,进入手动模式记录当前时间顺序,当前选中的空白会话保留临时首位。显式置顶更新保存的位置,但不切换所选模式;时间视图仍忽略这些位置。

取消归档

WorkspaceRegistry.unarchiveSession 与归档及其他注册表写入共用串行操作链,从全局归档集合移除一个 id。id 不在集合中时是幂等空操作,不产生持久写入或通知。恢复保留 Workspace 成员关系与 Session 日志,不恢复置顶,也不改变保存的顺序。

取消归档不探测 Session 是否存在:移除 id 不会引入未知引用,因此即使 Session 已不存在,也能在不列举持久化记录的情况下移除归档项。Remote 返回完整归档集合,既有 archived 增量把同一状态传给其他 Client。Client 只在一元响应仍属于最新归档集合请求时安装它;更新的请求、follow 增量或替换基线会覆盖旧响应的资格。写入失败不改变集合,侧边栏记录拒绝原因并保留原行;唯一例外是“仍有工作在跑”的拒绝,它会打开停止并归档的确认(归档仍有工作在跑的 Session)。

归档显隐与行动画

侧边栏在 ui-workspace 中派生可见行:

  • 已归档行保留其记账位置,置灰渲染且不可打开。视图选项菜单持有 ArchivedFilter(default 隐藏 / show / only),同时作用于列表与搜索;设置页的归档列表连同其包一并删除。
  • 归档动作弹出携带撤销与筛选已归档操作的 toast。父组件重渲染不会延长其生命周期,完全淡出后不会保留可点击操作。
  • 行动画由私有 React 组件使用浏览器原生位移与透明度动画实现。组件在结构性提交前后记录行位置,不使用观察器或轮询;被移除的行在滚动列表外留下不可交互的淡出副本。初始加载、拖拽提交、展开/收起其余会话,以及分组/排序/筛选切换直接完成。

持久化

Workspace KV、Remote 值与 Client 快照都用会话 id 数组存储归档和置顶成员,不包含对象包装或置顶时间戳。缺失集合默认为空,不接受对象形式的置顶条目。置顶与恢复不改变 Workspace 领域版本或 Session 日志格式,也不编辑 Session 日志。

Alternatives considered

保留设置页的归档列表。 同一状态出现两个入口;侧边栏筛选让已归档行显示在它们原本所在的浏览上下文中,因此该包被整体移除。

只提供恢复操作,不提供已归档筛选。 隐藏行无法提供操作。筛选让这些行可以被访问,因此恢复可以与 Session 列表放在一起,而不需要独立设置页。

独立恢复增量,或在取消归档时检查存在性。 新增帧类型会重复完整集合响应,并增加合并路径。存在性探测会把移除孤立归档 id 变成错误,尽管移除不可能引入未解析 id。因此恢复复用归档集合协议,不执行该探测。

显示缺少 Session 摘要的归档 id。 这些条目可以解释孤立状态,但不能提供普通对话行。侧边栏显示已加载摘要,API 仍支持直接恢复;孤立项管理不属于当前浏览器。

单独维护可编辑的置顶序列。 这会产生两个排序来源,也需要另一条拖拽写入路径。全局置顶数组只为缺失置顶成员提供初始位置,已有位置由完整会话序列管理。

恢复置顶或归档之前的位置。 让置顶独立于手动位置可以恢复置顶前的位置,但置顶本身就是一次有意的重排序。另存恢复记录增加了这套交互不需要的产品保证;隐藏归档行本身已经保留其已有位置。

用置顶时间给置顶行排名。 这会让最近更新不再严格表达会话活动。在该模式下,新置顶的旧会话不必排在置顶区首位;手动模式才显示置顶操作调整后的首位。

保存每次派生或筛选后的列表。 渲染和显隐变化不是排序命令。不完整或经过筛选的投影不能覆盖完整的已保存序列。

为每次列表变化播放动画。 显示隐藏行、提交拖拽或切换筛选会整体移动行,观感夸张;这些变化即时应用,而置顶跳动、归档淡出和相邻行补位仍保留动画。

Consequences

侧边栏端到端拥有已归档可见性,设置页少了一个包。置顶集合是 registry 全局的,因此置顶在分组与排序切换后仍然保留。行动画不持有持久状态,也不引入依赖。

筛选与重排序共用一条完整成员序列,隐藏的归档项不会因拖拽或切换筛选丢失。旧排序遗漏的成员在内存中补齐,并在下一次会话顺序写入时随完整序列保存。Host 的置顶与归档成员语义保持不变。浏览器不再用置顶时间提升最近更新模式下的位置,也不自动恢复置顶前的位置。

归档 id 没有已加载 Session 摘要时,不会产生可见行或侧边栏恢复操作,尽管 API 仍能移除它。这个 UI 限制不改变注册表的幂等恢复行为。

单元测试覆盖补齐、完整序列拖拽、期间发生其他变更后的 Pin 完成、新会话优先级、归档导航保护、筛选、toast 生命周期与行动画生命周期。Workspace 和 Controller 测试覆盖置顶与归档互斥、只含身份的持久化及快照、幂等空操作恢复、缺失 Session 时不探测持久化的恢复、重启恢复,以及被新请求或推送状态取代的旧一元响应。无密钥的侧边栏恢复场景验证行操作与重载后的持久性。

相关