@deepseek-ai/dsh-client-ui-workspace 0.1.5-rc.2 → 0.1.6-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/client/ui-workspace/README.md
5
- README.md: 857b38c2ca08ca4e9078d8f9dfcfe8462be50722
6
- README.zh.md: cf6f2d273f00add793cc5ac588db968c22007256
5
+ README.md: 1edf9223c72e7094ce305734119975c3954cce59
6
+ README.zh.md: 122f1d97a5a4dd7c5b54b0d9a27e323cdc6fb8ca
package/README.md CHANGED
@@ -29,7 +29,7 @@ Use the sidebar to browse Workspaces and their Sessions, reorder them, and start
29
29
 
30
30
  ### Reordering and view options
31
31
 
32
- View options combine grouping with one browser-persisted Session order per account: **Manual** and **Last updated** apply in either presentation. Entering Last updated performs a complete recency sort and later user prompts or steers promote their Session once; entering Manual preserves every current position and disables later promotion. Dragging edits the current order in either mode; Manual-mode drags for real Workspaces also update the Host Session account, while Ungrouped and flat-list orders remain browser-local. In a collapsed group, drag boundaries follow rendered rows and place the source before intervening hidden rows, so a drag cannot hide its source. Workspace drag order is Host-durable in either Session order mode.
32
+ **Last updated** orders ordinary Sessions by their latest user prompt or steer time, newest first, in both grouped and flat views. **Manual** freezes the current displayed order and holds positions when activity changes; newly discovered ordinary Sessions append to the end, newest first when several arrive together. Returning to Last updated discards every manual position, and entering Manual again freezes the then-current recency order. The browser defaults to Last updated and remembers the selected mode across reloads. Dragging an ordinary Session applies the move locally and selects Manual. The selected blank **New Session** is always pinned first and cannot be dragged; after its first prompt it becomes an ordinary draggable row, retaining its first position in Manual or following its current timestamp in Last updated. In a collapsed group, drag boundaries follow rendered rows and place the source before intervening hidden rows, so a drag cannot hide its source. Session display orders for real Workspaces, Ungrouped, and the flat list are browser-local; Workspace group drag order remains Host-durable.
33
33
 
34
34
  ### Search
35
35
 
@@ -67,7 +67,7 @@ Each registration declares a **directory-flow child hole** (`single` kind: `conv
67
67
 
68
68
  ### View state
69
69
 
70
- Once the Workspace list baseline is ready, browser-persisted expansion and Session-order records retain only current Workspace ids plus Ungrouped and the flat-list account. Real Workspaces initialize from `WorkspaceView.sessionIds`, while Ungrouped and the cross-Workspace flat list initialize from recency. The shared sidebar projection hides rows whose durable Session summary has `origin: 'subagent'`, and each visible ordinary row inherits the blue activity indicator while any descendant reached through uninterrupted subagent-origin lineage is running. The same pure derivation reads the Schedule key from list projection values for grouped, flat, and search nodes; the package uses only the type-only `@deepseek-ai/dsh-schedule/client` dependency and does not import the Schedule runtime or `ui-schedule`.
70
+ Once the Workspace list baseline is ready, browser-persisted expansion and manual Session-order records retain only current Workspace ids plus Ungrouped and the flat-list account. `WorkspaceView.sessionIds` supplies real-Workspace membership, not Session display order. View actions require the current account orders explicitly. Flat-list membership and ordering use Session ids; row rendering adds status indicators once. Entering Manual snapshots every active account from the current display; reconciliation retains saved members that still belong to the account, removes departed members, and appends newly known members by recency. A new membership entry without a Session summary is omitted until that summary arrives, while an already saved slot survives a temporarily missing summary. During Workspace reconnection, Manual records an observed blank Session at the front of its saved flat and known group orders without removing other saved members; full membership reconciliation waits for the Workspace baseline. This reconciliation remains mounted while the sidebar is a rail or search replaces its body. Last updated derives directly from each current list snapshot without reading or writing saved positions; equal timestamps use Session ids as a stable tie-break. The shared sidebar projection hides rows whose durable Session summary has `origin: 'subagent'`, and each visible ordinary row inherits the blue activity indicator while any descendant reached through uninterrupted subagent-origin lineage is running. The same pure derivation reads the Schedule key from list projection values for grouped, flat, and search nodes; the package uses only the type-only `@deepseek-ai/dsh-schedule/client` dependency and does not import the Schedule runtime or `ui-schedule`.
71
71
 
72
72
  ### Hover cards
73
73
 
@@ -85,7 +85,7 @@ These pages cover the sidebar host, the hero surface, and the picking backends.
85
85
  - [ui-sidebar](../ui-sidebar/README.md) — the sidebar shell hosting the `sidebar.workspaces` hole.
86
86
  - [ui-conversation](../ui-conversation/README.md) — the chat surface hosting the Session Intent hero's picker hole.
87
87
  - [directory-picker-native](../../host/directory-picker-native/README.md) — the OS-chooser backend filling the directory-flow hole.
88
- - [Workspace Controller](../../api/workspace-controller/README.md) — the Host mutations and framework-neutral Client projection that own workspaces and ordering.
88
+ - [Workspace Controller](../../api/workspace-controller/README.md) — the Host mutations and framework-neutral Client projection that own Workspaces, membership, and Workspace group order.
89
89
 
90
90
  -----
91
91
 
@@ -106,7 +106,7 @@ None; this package neither assembles nor sends a provider request.
106
106
  These limits define the search depth, the archive surface, and the picking carrier; they are current package constraints.
107
107
 
108
108
  - **No fuzzy content search or event deep links** — the content backend uses literal token/phrase matching, and selecting a result opens the Session rather than the matching event.
109
- - **No Session deletion or unarchive control** — sessions can be archived, but archived sessions have no viewing or unarchive surface, and Workspace registration deletion does not delete Sessions.
109
+ - **No Session deletion, and unarchive lives in Settings** — sessions can be archived but never deleted; the archived-sessions Settings page ([ui-settings-unarchive-sessions](../ui-settings-unarchive-sessions/README.md)) owns viewing and restoring them, and Workspace registration deletion does not delete Sessions.
110
110
  - **Pending user interaction is not aggregated into collapsed groups** — a waiting row inside a collapsed group lights no group-header indicator and becomes visible only after that group is expanded.
111
111
  - **Native folder selection depends on the local Host carrier** — under the `-native` composition, in-process or remote browser deployments cannot open a local operating-system dialog; remote-capable picking is the `-browse` composition's in-app flow.
112
112
 
@@ -120,4 +120,4 @@ None.
120
120
 
121
121
  </details>
122
122
 
123
- **Runtime invariant:** No companion is published. A pure-consumer plugin registering presentational components into two host-declared slots plus its locale dictionaries — its inject face is stateless RPC wrappers plus a create-and-open call; it emits no cordis events and owns no cross-plugin mutable state.
123
+ **Runtime invariant:** No companion is published. This is a pure-consumer plugin that registers presentational components into two host-declared slots and registers its locale dictionaries; its inject face consists of stateless RPC wrappers plus a create-and-open call. It emits no Cordis events and owns no cross-plugin mutable state.
package/README.zh.md CHANGED
@@ -1,5 +1,5 @@
1
1
  ---
2
- description: "dsh Web 客户端的共享 Workspace 浏览器与选择器插件:分组或扁平的会话行、添加/重命名/重排序、搜索、fork、归档,以及目录流选取子 slot。"
2
+ description: "dsh Web 客户端的共享 Workspace 浏览器与选择器插件:分组或扁平的会话行、添加、重命名、重排序、搜索、fork、归档,以及目录流选取子 slot。"
3
3
  kind: "package-reference"
4
4
  ---
5
5
 
@@ -29,7 +29,7 @@ kind: "package-reference"
29
29
 
30
30
  ### 重排序与视图选项
31
31
 
32
- 视图选项把分组方式和每个记账各自的一份浏览器持久化 Session 顺序放在一起:**手动排序**和**最近更新**在两种呈现方式下都可用。进入最近更新时会执行一次完整的时间排序,后续 user prompt 或 steer 会将对应 Session 置顶一次;进入手动排序则保留所有当前位置并停用后续置顶。两种模式下的拖拽都会编辑当前顺序;真实 Workspace 在手动模式下的拖拽还会更新 Host Session 记账,而 Ungrouped 和单列表的顺序始终只保存在浏览器本地。折叠分组的拖拽边界按渲染行确定,并把来源行放在中间隐藏行之前,因此拖拽不会隐藏来源行。无论采用哪种 Session 顺序,Workspace 拖拽顺序都由 Host 持久化。
32
+ **最近更新**在分组和单列表视图中都按普通 Session 的最近一次用户提示词或 steering(中途引导)时间降序排列。**手动排序**会冻结当前显示顺序,并在活动变化时保持已有位置;新发现的普通 Session 追加到末尾,多条同时到达时按最近更新时间降序排列。返回最近更新会丢弃所有手动位置;再次进入手动排序时,则冻结当时的最近更新顺序。浏览器默认采用最近更新,并在重新加载后记住所选模式。拖拽普通 Session 会在浏览器本地应用移动并选中手动排序。当前选中的空白**新会话**始终固定在首位且无法拖拽;首条提示词落地后,它成为可拖拽的普通行,在手动排序中保留首位,在最近更新中按当前时间戳排列。折叠分组的拖拽边界按渲染行确定,并把来源行放在中间隐藏行之前,因此拖拽不会隐藏来源行。真实 Workspace、Ungrouped 与单列表的 Session 显示顺序都保留在浏览器本地;Workspace 分组的拖拽顺序仍由 Host 持久化。
33
33
 
34
34
  ### 搜索
35
35
 
@@ -45,13 +45,13 @@ Session 行渲染运行时的实时 `pendingInteraction` 分类:审批显示**
45
45
 
46
46
  ### 活动 Schedule 标识
47
47
 
48
- 分组与平铺 Session 行以及搜索结果会在 `SessionSummary.projectionValues.schedule` 为非空数组时显示一枚轮廓闹钟。标识位于标题之后;普通行的更新时间仍位于标识之后,搜索结果则没有更新时间。它不是按钮,没有独立 pointer 行为或 Tab stop,点击所在区域仍会打开整行。本地化 tooltip 与同义读屏标签均为**有活动定时任务**。
48
+ 分组与平铺 Session 行以及搜索结果会在 `SessionSummary.projectionValues.schedule` 为非空数组时显示一枚轮廓闹钟。标识位于标题之后;普通行的更新时间仍位于标识之后,搜索结果则没有更新时间。它不是按钮,没有独立 pointer 行为或 Tab stop,点击所在区域仍会打开整行。本地化 tooltip 与文本相同的读屏标签均为**有活动定时任务**。
49
49
 
50
- 对于 cold Session,该值有意采用尽力而为语义。身份匹配且可用的 projection-cache 行可以在不打开 Session 的情况下预热闹钟;cache 缺失或陈旧可能造成短暂漏显或残留。标识只表示当前列表值包含尚未 dispatch 或 delete 的 Schedule 记录,不表示 Schedule runtime 当前 live 或能够唤醒该 Session。
50
+ 对于 cold Session,该值有意采用尽力而为语义。身份匹配且可用的 projection-cache 行可以在不打开 Session 的情况下预热闹钟;cache 缺失或陈旧可能造成短暂漏显或残留。标识只表示当前列表值包含尚未 dispatch 或 delete 的 Schedule 记录,不表示 Schedule 运行时当前 live 或能够唤醒该 Session。
51
51
 
52
52
  -----
53
53
 
54
- `ctx.uiWorkspace.openSession(id)` 将选中会话与中央区域返回会话界面作为一次 UI 导航操作,包括目标会话已经是当前会话的情况。`openWorkspace(id, beforeOpen?)` 和 `forkSession(id)` 仅在请求未被后续导航替代时打开结果;新会话使用 `openWorkspace`。可选的同步准备回调仅对仍有效的工作区请求执行,因此过期请求不会搬移 composer 草稿。后续导航或所有者释放会阻止晚到的 UI 提交,但不取消底层会话创建。选中失败时保留当前全局面板。会话行读取 `usePanelInfo`,在全局面板活跃时不显示会话选中样式;仅把焦点移到搜索框或目录选择器不会离开该面板。
54
+ `ctx.uiWorkspace.openSession(id)` 会选中会话,并让主区域返回会话界面;这两项构成一次 UI 导航操作,即使目标会话已经是当前会话也同样执行。`openWorkspace(id, beforeOpen?)` 和 `forkSession(id)` 仅在请求未被后续导航替代时打开结果;新会话使用 `openWorkspace`。可选的同步准备回调仅对仍有效的工作区请求执行,因此过期请求不会搬移 composer 草稿。后续导航或所有者释放会阻止晚到的 UI 提交,但不取消底层会话创建。选中失败时保留当前全局面板。会话行读取 `usePanelInfo`,在全局面板活跃时不显示会话选中样式;仅把焦点移到搜索框或目录选择器不会离开该面板。
55
55
 
56
56
  <a id="understand-the-implementation"></a>
57
57
  ## 理解实现
@@ -67,7 +67,7 @@ Session 行渲染运行时的实时 `pendingInteraction` 分类:审批显示**
67
67
 
68
68
  ### 视图状态
69
69
 
70
- Workspace 列表基线就绪后,浏览器持久化的展开状态与 Session 顺序记录只保留当前 Workspace id、Ungrouped 与单列表记账。真实 Workspace 从 `WorkspaceView.sessionIds` 初始化,Ungrouped 与跨 Workspace 单列表从最近更新时间顺序初始化。共享侧边栏投影会隐藏持久化 Session 摘要中带有 `origin: 'subagent'` 的行;每个可见普通行都会在经不间断的 subagent 谱系可达的任一后代运行时继承蓝色活动指示器。同一份纯派生还会为分组、平铺与搜索节点读取列表 projection value 中的 Schedule key;本包只使用纯类型依赖 `@deepseek-ai/dsh-schedule/client`,不会导入 Schedule runtime 或 `ui-schedule`。
70
+ Workspace 列表基线就绪后,浏览器持久化的展开状态和手动 Session 顺序记录只保留当前 Workspace id、Ungrouped 和单列表记账。`WorkspaceView.sessionIds` 提供真实 Workspace 的成员关系,而不提供 Session 显示顺序。视图操作要求显式传入当前各记账的顺序。单列表的成员筛选和排序使用 Session id,行渲染只计算一次状态指示。进入手动排序会从当前显示结果一次性记录每个有效记账;对账会保留仍属于该记账的已保存成员、移除已经离开的成员,并按最近更新时间追加新发现的成员。尚无 Session 摘要的新成员会等摘要到达后再加入,而已经保存的位置在摘要暂时缺失时仍会保留。Workspace 重连期间,手动排序会将已观察到的空白 Session 记录到已保存的单列表及已知分组顺序首位,不移除其他已保存成员;完整成员对账等待 Workspace 基线到齐。即使侧边栏收成窄栏或搜索替代列表主体,这项对账也保持挂载。最近更新直接从每份当前列表快照派生,不读取或写入已保存位置;时间相同时按 Session id 稳定排序。共享侧边栏投影会隐藏持久化 Session 摘要中带有 `origin: 'subagent'` 的行;每个可见普通行都会在经不间断的 subagent 谱系可达的任一后代运行时继承蓝色活动指示器。同一项纯派生逻辑还会为分组、平铺与搜索节点读取列表 projection value 中的 Schedule key;本包只使用纯类型依赖 `@deepseek-ai/dsh-schedule/client`,不会导入 Schedule 运行时或 `ui-schedule`。
71
71
 
72
72
  ### 悬浮卡片
73
73
 
@@ -85,7 +85,7 @@ Workspace 与 Session 悬浮卡片会复制对应行被截断的值:激活 Wor
85
85
  - [ui-sidebar](../ui-sidebar/README.zh.md)——承载 `sidebar.workspaces` 子 slot 的侧边栏外壳。
86
86
  - [ui-conversation](../ui-conversation/README.zh.md)——承载 Session Intent 主视觉区选择器子 slot 的聊天界面。
87
87
  - [directory-picker-native](../../host/directory-picker-native/README.zh.md)——填充目录流子 slot 的 OS 选择器后端。
88
- - [Workspace Controller](../../api/workspace-controller/README.zh.md)——负责 Workspace 与排序的 Host 变更和框架无关 Client 投影。
88
+ - [Workspace Controller](../../api/workspace-controller/README.zh.md)——负责 Workspace、成员关系与 Workspace 分组顺序的 Host 变更和框架无关 Client 投影。
89
89
 
90
90
  -----
91
91
 
@@ -106,7 +106,7 @@ Workspace 与 Session 悬浮卡片会复制对应行被截断的值:激活 Wor
106
106
  这些限制定义搜索深度、归档界面与选取载体;它们是当前包约束。
107
107
 
108
108
  - **没有模糊内容搜索或事件深链接**:内容后端采用字面 token/短语匹配,选择结果会打开 Session,而不是匹配的事件。
109
- - **没有 Session 删除与取消归档控件**:会话可以归档,但已归档会话没有查看或取消归档入口;删除 Workspace 注册记录不会删除 Session。
109
+ - **没有 Session 删除,取消归档位于设置中**:会话可以归档但绝不会被删除;已归档会话的查看与恢复由「已归档会话」设置页([ui-settings-unarchive-sessions](../ui-settings-unarchive-sessions/README.zh.md))负责,删除 Workspace 注册记录不会删除 Session。
110
110
  - **待处理的用户交互不会聚合到折叠的分组上**:折叠分组内正在等待的行不会点亮分组头指示,只有展开该分组后才可见。
111
111
  - **原生文件夹选择依赖本地 Host 载体**:在 `-native` 组合下,进程内部署或远程浏览器部署无法打开本地操作系统对话框;可远程的选取是 `-browse` 组合的应用内流程。
112
112
 
@@ -120,4 +120,4 @@ Workspace 与 Session 悬浮卡片会复制对应行被截断的值:激活 Wor
120
120
 
121
121
  </details>
122
122
 
123
- **运行时不变式:** 不发布伴生入口。这是纯消费插件,只注册展示组件和 locale dictionary;inject face 是无状态 RPC wrapper 加 create-and-open 调用,不发出事件或持有跨插件可变状态。
123
+ **运行时不变式:** 不发布伴生入口。这是一个纯消费方插件,只向两个由宿主声明的 slot 注册展示组件,并注册自身的 locale dictionaries;inject face 由无状态 RPC 包装层和一次 create-and-open 调用组成;本插件不发出 Cordis 事件,也不持有跨插件可变状态。
package/lib/client.js CHANGED
@@ -97,6 +97,9 @@ window.__ModuleLoader__.load({
97
97
  async archiveSession(sessionId) {
98
98
  await this.workspaces.archiveSession(sessionId);
99
99
  }
100
+ async unarchiveSession(sessionId) {
101
+ await this.workspaces.unarchiveSession(sessionId);
102
+ }
100
103
  async pickDirectory() {
101
104
  const result = await this.directoryPicker.pick();
102
105
  if (!result.ok) throw new Error(`directory picker failed: ${result.error.message}`);
@@ -187,6 +190,10 @@ window.__ModuleLoader__.load({
187
190
  */
188
191
  /** Browser-local order account for the hierarchy-free flat Session list. */
189
192
  const FLAT_SESSION_ORDER_KEY = "__flat_session_order__";
193
+ /** Copy read-only projections into the persisted mutable store representation. */
194
+ function copySessionOrders(orders) {
195
+ return Object.fromEntries(Object.entries(orders).map(([key, order]) => [key, [...order]]));
196
+ }
190
197
  /**
191
198
  * Create the workspace browser viewing store handle.
192
199
  * @returns the store handle (spec + type + identity + factory in one).
@@ -197,15 +204,16 @@ window.__ModuleLoader__.load({
197
204
  groupBy: "workspace",
198
205
  orderBy: "updated",
199
206
  groupExpansion: {},
200
- sessionOrderByAccount: {},
201
- sessionUpdatedAtByAccount: {}
207
+ sessionOrderByAccount: {}
202
208
  }),
203
209
  persist: "dsh.workspace.view.v5",
204
210
  actions: {
205
211
  setGroupBy: (d, mode) => {
206
212
  d.groupBy = mode;
207
213
  },
208
- setOrderBy: (d, mode) => {
214
+ setOrderBy: (d, mode, initialOrders) => {
215
+ if (mode === d.orderBy) return;
216
+ d.sessionOrderByAccount = mode === "manual" ? copySessionOrders(initialOrders) : {};
209
217
  d.orderBy = mode;
210
218
  },
211
219
  setGroupExpanded: (d, key, expanded) => {
@@ -215,14 +223,16 @@ window.__ModuleLoader__.load({
215
223
  const retained = new Set(workspaceKeys);
216
224
  d.groupExpansion = Object.fromEntries(Object.entries(d.groupExpansion).filter(([key]) => retained.has(key)));
217
225
  d.sessionOrderByAccount = Object.fromEntries(Object.entries(d.sessionOrderByAccount).filter(([key]) => retained.has(key)));
218
- d.sessionUpdatedAtByAccount = Object.fromEntries(Object.entries(d.sessionUpdatedAtByAccount).filter(([key]) => retained.has(key)));
226
+ delete d.sessionUpdatedAtByAccount;
219
227
  },
220
- syncSessionOrderAccount: (d, accountKey, order, updatedAt) => {
221
- d.sessionOrderByAccount[accountKey] = order;
222
- d.sessionUpdatedAtByAccount[accountKey] = updatedAt;
228
+ syncSessionOrders: (d, orders) => {
229
+ if (d.orderBy !== "manual") return;
230
+ Object.assign(d.sessionOrderByAccount, copySessionOrders(orders));
223
231
  },
224
- setSessionOrder: (d, accountKey, order) => {
225
- d.sessionOrderByAccount[accountKey] = order;
232
+ setSessionOrder: (d, accountKey, order, initialOrders) => {
233
+ if (d.orderBy === "updated") d.sessionOrderByAccount = copySessionOrders(initialOrders);
234
+ d.orderBy = "manual";
235
+ d.sessionOrderByAccount[accountKey] = [...order];
226
236
  }
227
237
  }
228
238
  });
@@ -324,10 +334,57 @@ window.__ModuleLoader__.load({
324
334
  const base = workspaceTitleOf(cwd);
325
335
  return base !== "" ? base : cwd;
326
336
  }
327
- /** Recency comparator: newest first, id as the deterministic tiebreak (ids are unique per group). */
328
- function byRecency(a, b) {
329
- if (b.updatedAt !== a.updatedAt) return b.updatedAt - a.updatedAt;
330
- return a.id < b.id ? -1 : 1;
337
+ /**
338
+ * Project known account members by current Session recency.
339
+ * @param sessionIds - authoritative account membership.
340
+ * @param summaries - current Session summaries; members without a summary are omitted until it arrives.
341
+ * @returns known members newest first, with Session identity as the deterministic tie-break.
342
+ */
343
+ function orderByRecency(sessionIds, summaries) {
344
+ return sessionIds.flatMap((id) => {
345
+ const summary = summaries[id];
346
+ return summary === void 0 ? [] : [{
347
+ id,
348
+ updatedAt: summary.updatedAt
349
+ }];
350
+ }).sort((a, b) => {
351
+ if (a.updatedAt !== b.updatedAt) return b.updatedAt - a.updatedAt;
352
+ return a.id < b.id ? -1 : 1;
353
+ }).map((member) => member.id);
354
+ }
355
+ /**
356
+ * Reconcile a browser-local manual order with current account membership.
357
+ * @param memberIds - authoritative account membership.
358
+ * @param savedOrder - previously saved browser-local order.
359
+ * @param summaries - current Session summaries used to append newly known members by recency.
360
+ * @returns retained saved slots followed by newly known members; departed members and unknown new members are omitted.
361
+ */
362
+ function reconcileManualOrder(memberIds, savedOrder, summaries) {
363
+ const members = new Map(memberIds.map((id) => [id, id]));
364
+ const included = /* @__PURE__ */ new Set();
365
+ const ordered = [];
366
+ for (const key of savedOrder ?? []) {
367
+ const id = members.get(key);
368
+ if (id === void 0 || included.has(key)) continue;
369
+ ordered.push(id);
370
+ included.add(key);
371
+ }
372
+ for (const id of orderByRecency(memberIds, summaries)) {
373
+ if (included.has(id)) continue;
374
+ ordered.push(id);
375
+ included.add(id);
376
+ }
377
+ return ordered;
378
+ }
379
+ /**
380
+ * Keep the selected provisional New Session ahead of either base order.
381
+ * @param order - recency or reconciled manual order.
382
+ * @param currentBlank - selected blank Session in this account, when present.
383
+ * @returns a copy with the selected blank first and no duplicate slot.
384
+ */
385
+ function pinCurrentBlank(order, currentBlank) {
386
+ if (currentBlank === void 0) return [...order];
387
+ return [currentBlank, ...order.filter((id) => id !== currentBlank)];
331
388
  }
332
389
  /**
333
390
  * Ordinary sessions are visible; among blank sessions, only the current one
@@ -351,40 +408,30 @@ window.__ModuleLoader__.load({
351
408
  return (session.projectionValues?.schedule?.length ?? 0) > 0;
352
409
  }
353
410
  /** Build one group without projecting session lineage into presentation. */
354
- function buildGroup(key, workspaceId, cwd, createdAt, label, members, order) {
355
- const sessions = [...members];
356
- if (order === "recency") sessions.sort(byRecency);
411
+ function buildGroup(key, workspaceId, cwd, createdAt, label, members) {
357
412
  return {
358
413
  key,
359
414
  workspaceId,
360
415
  cwd,
361
416
  createdAt,
362
417
  label,
363
- sessions
418
+ sessions: [...members]
364
419
  };
365
420
  }
366
421
  /** Apply a stored Ungrouped order and append newly loose Sessions by recency. */
367
- function orderedUngrouped(members, stored) {
422
+ function orderedUngrouped(members, stored, summaries) {
368
423
  const byId = new Map(members.map((session) => [session.id, session]));
369
- const included = /* @__PURE__ */ new Set();
370
- const ordered = [];
371
- for (const key of stored) {
372
- const session = byId.get(key);
373
- if (session === void 0 || included.has(key)) continue;
374
- ordered.push(session);
375
- included.add(key);
376
- }
377
- for (const session of [...members].sort(byRecency)) {
378
- if (included.has(session.id)) continue;
379
- ordered.push(session);
380
- }
381
- return ordered;
424
+ return (stored === void 0 ? orderByRecency(members.map((session) => session.id), summaries) : reconcileManualOrder(members.map((session) => session.id), stored, summaries)).flatMap((id) => {
425
+ const session = byId.get(id);
426
+ /* v8 ignore next -- ids are projected exclusively from the members used to build byId. */
427
+ return session === void 0 ? [] : [session];
428
+ });
382
429
  }
383
430
  /**
384
- * Group Sessions by Host Workspace: one group per entity in stable Host
385
- * order, with members resolved from sessionIds in their stored order. Sessions
386
- * outside every Workspace trail in the browser-local Ungrouped order, which
387
- * falls back to recency before that order is initialized.
431
+ * Group Sessions by Workspace: one group per caller-ordered entity, with
432
+ * members resolved from caller-ordered sessionIds. Sessions outside every
433
+ * Workspace trail in the browser-local Ungrouped order, which falls back to
434
+ * recency before that order is initialized.
388
435
  */
389
436
  function groupByWorkspace(list, workspaces, archived, ungroupedOrder) {
390
437
  const groups = [];
@@ -398,10 +445,10 @@ window.__ModuleLoader__.load({
398
445
  if (!sessionVisible(summary, list.current, archived)) continue;
399
446
  members.push(summary);
400
447
  }
401
- groups.push(buildGroup(workspace.workspaceId, workspace.workspaceId, workspace.path, Date.parse(workspace.createdAt), workspace.title, members, "account"));
448
+ groups.push(buildGroup(workspace.workspaceId, workspace.workspaceId, workspace.path, Date.parse(workspace.createdAt), workspace.title, members));
402
449
  }
403
450
  const stray = list.ids.map((id) => list.byId[id]).filter((s) => s !== void 0 && !accounted.has(s.id) && sessionVisible(s, list.current, archived));
404
- if (stray.length > 0) groups.push(buildGroup("", void 0, void 0, void 0, "", ungroupedOrder === void 0 ? stray : orderedUngrouped(stray, ungroupedOrder), ungroupedOrder === void 0 ? "recency" : "account"));
451
+ if (stray.length > 0) groups.push(buildGroup("", void 0, void 0, void 0, "", orderedUngrouped(stray, ungroupedOrder, list.byId)));
405
452
  return groups;
406
453
  }
407
454
  /** Keep navigation presentation independent from domain-owned interaction objects. */
@@ -436,7 +483,7 @@ window.__ModuleLoader__.load({
436
483
  * Content search lives outside this derivation
437
484
  * (see {@link deriveSearchResults}).
438
485
  * @param list - sessions list snapshot (`current` feeds containsCurrent).
439
- * @param workspaces - real workspaces in stable Host order.
486
+ * @param workspaces - real Workspaces in Host group order with caller-projected Session order.
440
487
  * @param archivedSessionIds - registry-global archive set.
441
488
  * @param pendingInteractions - pending UI interactions by Session.
442
489
  * @param view - local expansion arrays.
@@ -465,26 +512,28 @@ window.__ModuleLoader__.load({
465
512
  return groups;
466
513
  }
467
514
  /**
468
- * Derive the flat session list ("In one list" mode): every session — fork
469
- * children included — as a top-level row, strictly newest-first. No grouping,
470
- * no parent/child adjacency. Content search lives outside this derivation
471
- * (see {@link deriveSearchResults}).
515
+ * Select flat-list members without deriving row presentation or ordering.
472
516
  * @param list - sessions list snapshot.
473
517
  * @param archivedSessionIds - registry-global archive set.
474
- * @param pendingInteractions - pending UI interactions by Session.
475
- * @returns flat rows in render order.
518
+ * @returns known visible Session ids in list order, including ordinary forks and only the current blank.
476
519
  */
477
- function deriveFlat(list, archivedSessionIds, pendingInteractions) {
520
+ function visibleSessionIds(list, archivedSessionIds) {
478
521
  const archived = new Set(archivedSessionIds);
479
- const descendants = indexSubagentDescendants(list.byId);
480
- const rows = [];
481
- for (const id of list.ids) {
522
+ return list.ids.filter((id) => {
482
523
  const s = list.byId[id];
483
- if (s === void 0 || !sessionVisible(s, list.current, archived)) continue;
484
- rows.push(s);
485
- }
486
- rows.sort(byRecency);
487
- return rows.map((session) => sessionNode(session, descendants, pendingInteractions));
524
+ return s !== void 0 && sessionVisible(s, list.current, archived);
525
+ });
526
+ }
527
+ /**
528
+ * Derive flat rows from the browser's ordered visible Session ids.
529
+ * @param list - sessions list snapshot used to select the ids.
530
+ * @param sessionIds - known visible members in render order, including any pinned blank.
531
+ * @param pendingInteractions - pending UI interactions by Session.
532
+ * @returns flat rows in the supplied order with current status indicators.
533
+ */
534
+ function deriveFlat(list, sessionIds, pendingInteractions) {
535
+ const descendants = indexSubagentDescendants(list.byId);
536
+ return sessionIds.map((id) => sessionNode(list.byId[id], descendants, pendingInteractions));
488
537
  }
489
538
  /**
490
539
  * Merge immediate title/Workspace substring matches with ranked Host content
@@ -518,7 +567,8 @@ window.__ModuleLoader__.load({
518
567
  if (summary === void 0 || summary.blank || !sessionVisible(summary, list.current, archived)) continue;
519
568
  if (sessionTitle(summary).toLowerCase().includes(q) || labelOf(summary).toLowerCase().includes(q)) local.push(summary);
520
569
  }
521
- local.sort(byRecency);
570
+ const localById = new Map(local.map((summary) => [summary.id, summary]));
571
+ const orderedLocal = orderByRecency(local.map((summary) => summary.id), list.byId).map((id) => localById.get(id));
522
572
  const ordered = [];
523
573
  const included = /* @__PURE__ */ new Set();
524
574
  const include = (summary) => {
@@ -526,7 +576,7 @@ window.__ModuleLoader__.load({
526
576
  included.add(summary.id);
527
577
  ordered.push(summary);
528
578
  };
529
- for (const summary of local) include(summary);
579
+ for (const summary of orderedLocal) include(summary);
530
580
  for (const item of content.items) {
531
581
  const summary = list.byId[item.sessionId];
532
582
  if (summary !== void 0 && !summary.blank && sessionVisible(summary, list.current, archived)) include(summary);
@@ -927,7 +977,7 @@ window.__ModuleLoader__.load({
927
977
  * @param props.onFork - fork a session at its last completed turn.
928
978
  * @param props.onArchive - archive a session by id.
929
979
  * @param props.onReveal - scroll this row into view after search navigation, then acknowledge it.
930
- * @param props.drag - optional draggable-row wiring.
980
+ * @param props.drag - optional row-drag target wiring; blank rows cannot start a drag.
931
981
  * @param props.flat - omit the empty status slot in the hierarchy-free flat list.
932
982
  * @param props.t - the browser root's locale seat.
933
983
  * @returns the session row.
@@ -938,6 +988,7 @@ window.__ModuleLoader__.load({
938
988
  const selected = node.id === currentId;
939
989
  const statuses = sessionStatuses(node, t);
940
990
  const showStatus = statuses[0].state !== "done" || row.completed;
991
+ const draggable = drag !== void 0 && !row.blank;
941
992
  const [menuOpen, setMenuOpen] = (0, react.useState)(false);
942
993
  const rowRef = (0, react.useRef)(null);
943
994
  (0, react.useEffect)(() => {
@@ -971,13 +1022,13 @@ window.__ModuleLoader__.load({
971
1022
  onClick: () => {
972
1023
  onOpen(node.id);
973
1024
  },
974
- draggable: drag !== void 0,
975
- onDragStart: drag === void 0 ? void 0 : (e) => {
1025
+ draggable,
1026
+ onDragStart: drag === void 0 || row.blank ? void 0 : (e) => {
976
1027
  e.dataTransfer.effectAllowed = "move";
977
1028
  e.dataTransfer.setData("text/plain", node.id);
978
1029
  drag.start();
979
1030
  },
980
- onDragEnd: drag?.end,
1031
+ onDragEnd: drag === void 0 || row.blank ? void 0 : drag.end,
981
1032
  onDragOver: drag === void 0 ? void 0 : (e) => {
982
1033
  if (!drag.active) return;
983
1034
  e.preventDefault();
@@ -1338,58 +1389,6 @@ window.__ModuleLoader__.load({
1338
1389
  };
1339
1390
  }, [active]);
1340
1391
  }
1341
- /** Reconcile a stored view order with the Workspace's current session account. */
1342
- function reconciledSessionOrder(sessionIds, stored) {
1343
- if (stored === void 0) return [...sessionIds];
1344
- const byId = new Map(sessionIds.map((id) => [id, id]));
1345
- const ordered = [];
1346
- const included = /* @__PURE__ */ new Set();
1347
- for (const key of stored) {
1348
- const id = byId.get(key);
1349
- if (id === void 0 || included.has(key)) continue;
1350
- ordered.push(id);
1351
- included.add(key);
1352
- }
1353
- for (const id of sessionIds) {
1354
- if (included.has(id)) continue;
1355
- ordered.push(id);
1356
- }
1357
- return ordered;
1358
- }
1359
- /** Newest update first with stable Session identity as the tie-break. */
1360
- function compareSessionRecency(a, b, byId) {
1361
- const aUpdatedAt = byId[a]?.updatedAt ?? Number.NEGATIVE_INFINITY;
1362
- const bUpdatedAt = byId[b]?.updatedAt ?? Number.NEGATIVE_INFINITY;
1363
- if (aUpdatedAt !== bUpdatedAt) return bUpdatedAt - aUpdatedAt;
1364
- return a < b ? -1 : 1;
1365
- }
1366
- /** Reconcile one editable order account and apply its activity-promotion policy. */
1367
- function nextSessionOrderAccount({ sessionIds, previousOrder, previousUpdatedAt, list, orderBy, sortByRecency }) {
1368
- let order = reconciledSessionOrder(sessionIds, previousOrder);
1369
- if (sortByRecency) order.sort((a, b) => compareSessionRecency(a, b, list.byId));
1370
- else if (orderBy === "updated") {
1371
- const promoted = sessionIds.filter((id) => {
1372
- const session = list.byId[id];
1373
- return session !== void 0 && (previousUpdatedAt[id] === void 0 || session.updatedAt > previousUpdatedAt[id]);
1374
- }).sort((a, b) => compareSessionRecency(a, b, list.byId));
1375
- if (promoted.length > 0) {
1376
- const promotedIds = new Set(promoted);
1377
- order = [...promoted, ...order.filter((id) => !promotedIds.has(id))];
1378
- }
1379
- }
1380
- const updatedAt = {};
1381
- for (const id of sessionIds) {
1382
- const session = list.byId[id];
1383
- if (session !== void 0) updatedAt[id] = session.updatedAt;
1384
- }
1385
- const orderChanged = previousOrder === void 0 || order.length !== previousOrder.length || order.some((id, index) => id !== previousOrder[index]);
1386
- const timestampsChanged = Object.keys(updatedAt).length !== Object.keys(previousUpdatedAt).length || Object.entries(updatedAt).some(([id, timestamp]) => previousUpdatedAt[id] !== timestamp);
1387
- return {
1388
- order,
1389
- updatedAt,
1390
- changed: orderChanged || timestampsChanged
1391
- };
1392
- }
1393
1392
  /** Grouping and ordering menu; own open state so it resets with the wide chrome. */
1394
1393
  function ViewOptionsMenu({ groupBy, orderBy, onGroupPick, onOrderPick, t }) {
1395
1394
  const [open, setOpen] = (0, react.useState)(false);
@@ -1461,9 +1460,8 @@ window.__ModuleLoader__.load({
1461
1460
  return e.clientY < rect.top + rect.height / 2 ? "before" : "after";
1462
1461
  }
1463
1462
  /** The scrolling session tree; unmounting drops the sessions subscription and expand-all state. */
1464
- function SessionTree({ useSessions, useSessionPendingInteraction, startSession, open, forkSession, workspaces, archivedSessionIds, workspaceReady, usePanelInfo, onRenameRequest, onDeleteRequest, onSessionRename, onSessionArchive, insertWorkspaceBefore, insertSessionBefore, orderBy, groupExpansion, setGroupExpanded, sessionOrderByAccount, sessionUpdatedAtByAccount, syncSessionOrderAccount, setSessionOrder, home, t, revealSessionId, onSessionRevealed }) {
1463
+ function SessionTree({ list, useSessionPendingInteraction, startSession, open, forkSession, workspaces, ungroupedSessionIds, archivedSessionIds, workspaceReady, usePanelInfo, onRenameRequest, onDeleteRequest, onSessionRename, onSessionArchive, insertWorkspaceBefore, groupExpansion, setGroupExpanded, setSessionOrder, home, t, revealSessionId, onSessionRevealed }) {
1465
1464
  const panelActive = usePanelInfo((info) => info.activePanelId !== null);
1466
- const list = useSessions((s) => s);
1467
1465
  const pendingInteractions = useSessionPendingInteraction((s) => s);
1468
1466
  const current = panelActive ? void 0 : list.current;
1469
1467
  const revealGroup = revealSessionId === void 0 || !workspaceReady ? void 0 : owningGroupKey(workspaces, revealSessionId);
@@ -1472,7 +1470,6 @@ window.__ModuleLoader__.load({
1472
1470
  const sessionDropCommitted = (0, react.useRef)(false);
1473
1471
  const [workspaceDrag, setWorkspaceDrag] = (0, react.useState)(null);
1474
1472
  const workspaceDropCommitted = (0, react.useRef)(false);
1475
- const previousOrderBy = (0, react.useRef)(orderBy);
1476
1473
  useNativeDragAcceptance(drag !== null || workspaceDrag !== null);
1477
1474
  const currentGroup = current === void 0 || !workspaceReady ? void 0 : owningGroupKey(workspaces, current);
1478
1475
  (0, react.useEffect)(() => {
@@ -1485,63 +1482,16 @@ window.__ModuleLoader__.load({
1485
1482
  groupExpansion
1486
1483
  ]);
1487
1484
  const expandedGroups = (0, react.useMemo)(() => Object.entries(groupExpansion).filter(([, expanded]) => expanded).map(([key]) => key), [groupExpansion]);
1488
- const ungroupedSessionIds = (0, react.useMemo)(() => {
1489
- const accounted = new Set(workspaces.flatMap((workspace) => workspace.sessionIds));
1490
- return list.ids.filter((id) => list.byId[id] !== void 0 && !accounted.has(id));
1491
- }, [list, workspaces]);
1492
- (0, react.useEffect)(() => {
1493
- if (list.phase !== "ready") return;
1494
- const switchedToUpdated = previousOrderBy.current !== "updated" && orderBy === "updated";
1495
- previousOrderBy.current = orderBy;
1496
- const accounts = [...workspaces.map((workspace) => ({
1497
- key: workspace.workspaceId,
1498
- sessionIds: workspace.sessionIds.filter((id) => list.byId[id] !== void 0)
1499
- })), {
1500
- key: "",
1501
- sessionIds: ungroupedSessionIds
1502
- }];
1503
- for (const { key, sessionIds } of accounts) {
1504
- const previousOrder = sessionOrderByAccount[key];
1505
- const next = nextSessionOrderAccount({
1506
- sessionIds,
1507
- previousOrder,
1508
- previousUpdatedAt: sessionUpdatedAtByAccount[key] ?? {},
1509
- list,
1510
- orderBy,
1511
- sortByRecency: orderBy === "updated" && (previousOrder === void 0 || switchedToUpdated)
1512
- });
1513
- if (next.changed) syncSessionOrderAccount(key, next.order.map((id) => id), next.updatedAt);
1514
- }
1515
- }, [
1516
- list,
1517
- orderBy,
1518
- sessionOrderByAccount,
1519
- sessionUpdatedAtByAccount,
1520
- syncSessionOrderAccount,
1521
- ungroupedSessionIds,
1522
- workspaces
1523
- ]);
1524
- const orderedWorkspaces = (0, react.useMemo)(() => {
1525
- return workspaces.map((workspace) => {
1526
- const stored = sessionOrderByAccount[workspace.workspaceId];
1527
- const sessionIds = reconciledSessionOrder(workspace.sessionIds, stored);
1528
- return {
1529
- ...workspace,
1530
- sessionIds
1531
- };
1532
- });
1533
- }, [sessionOrderByAccount, workspaces]);
1534
- const orderedUngroupedSessionIds = (0, react.useMemo)(() => reconciledSessionOrder(ungroupedSessionIds, sessionOrderByAccount[""]), [sessionOrderByAccount, ungroupedSessionIds]);
1535
- const groups = (0, react.useMemo)(() => deriveGroups(list, orderedWorkspaces, archivedSessionIds, pendingInteractions, {
1485
+ const groups = (0, react.useMemo)(() => deriveGroups(list, workspaces, archivedSessionIds, pendingInteractions, {
1536
1486
  expandedGroups,
1537
- ...sessionOrderByAccount[""] === void 0 ? {} : { ungroupedOrder: sessionOrderByAccount[""] }
1487
+ ungroupedOrder: ungroupedSessionIds
1538
1488
  }), [
1539
1489
  list,
1540
- orderedWorkspaces,
1490
+ workspaces,
1541
1491
  archivedSessionIds,
1542
1492
  pendingInteractions,
1543
1493
  expandedGroups,
1544
- sessionOrderByAccount
1494
+ ungroupedSessionIds
1545
1495
  ]);
1546
1496
  (0, react.useEffect)(() => {
1547
1497
  if (revealGroup === void 0 || groupExpansion[revealGroup] === true) return;
@@ -1580,8 +1530,8 @@ window.__ModuleLoader__.load({
1580
1530
  if (targetWithoutSourceIndex === -1) return;
1581
1531
  const visibleInsertAt = over.half === "before" ? targetWithoutSourceIndex : targetWithoutSourceIndex + 1;
1582
1532
  if (sourceIndex !== -1 && visibleInsertAt === sourceIndex) return;
1583
- const accountSessionIds = activeDrag.accountKey === "" ? orderedUngroupedSessionIds : orderedWorkspaces.find((workspace) => workspace.workspaceId === activeDrag.accountKey)?.sessionIds;
1584
- if (accountSessionIds === void 0) return;
1533
+ const accountSessionIds = activeDrag.accountKey === "" ? ungroupedSessionIds : workspaces.find((workspace) => workspace.workspaceId === activeDrag.accountKey)?.sessionIds;
1534
+ if (accountSessionIds === void 0 || !accountSessionIds.includes(activeDrag.sessionId)) return;
1585
1535
  const nextOrder = accountSessionIds.filter((id) => id !== activeDrag.sessionId);
1586
1536
  let anchor;
1587
1537
  if (sessionsExpanded) anchor = over.half === "before" ? over.id : renderedSessions[targetIndex + 1]?.id;
@@ -1603,11 +1553,8 @@ window.__ModuleLoader__.load({
1603
1553
  return node === void 0 ? [] : [node];
1604
1554
  })).rows.some((node) => node.id === activeDrag.sessionId)) return;
1605
1555
  }
1606
- setSessionOrder(activeDrag.accountKey, nextOrder.map((id) => id));
1607
- if (orderBy === "updated" || activeDrag.accountKey === "") return;
1608
- insertSessionBefore(activeDrag.accountKey, activeDrag.sessionId, anchor).catch((reason) => {
1609
- console.warn("session reorder rejected:", reason);
1610
- });
1556
+ const currentBlank = group.sessions.find((node) => node.blank)?.id;
1557
+ setSessionOrder(activeDrag.accountKey, pinCurrentBlank(nextOrder, currentBlank));
1611
1558
  };
1612
1559
  const commitWorkspaceDrag = (activeDrag, over) => {
1613
1560
  if (workspaceDropCommitted.current) return;
@@ -1714,6 +1661,7 @@ window.__ModuleLoader__.load({
1714
1661
  }),
1715
1662
  (sessionsExpanded ? group.sessions : collapsed.rows).map((node) => {
1716
1663
  const sameGroupDrag = drag !== null && drag.accountKey === group.key;
1664
+ const normalizeHalf = (half) => node.blank ? "after" : half;
1717
1665
  const dragProps = {
1718
1666
  start: () => {
1719
1667
  sessionDropCommitted.current = false;
@@ -1731,7 +1679,7 @@ window.__ModuleLoader__.load({
1731
1679
  ...d,
1732
1680
  over: {
1733
1681
  id: node.id,
1734
- half
1682
+ half: normalizeHalf(half)
1735
1683
  }
1736
1684
  });
1737
1685
  },
@@ -1740,7 +1688,7 @@ window.__ModuleLoader__.load({
1740
1688
  if (drag === null) return;
1741
1689
  commitSessionDrag(drag, {
1742
1690
  id: node.id,
1743
- half
1691
+ half: normalizeHalf(half)
1744
1692
  });
1745
1693
  },
1746
1694
  end: () => {
@@ -1782,50 +1730,13 @@ window.__ModuleLoader__.load({
1782
1730
  });
1783
1731
  }
1784
1732
  /** The flat "In one list" body: every session is one draggable top-level row. */
1785
- function FlatList({ useSessions, useSessionPendingInteraction, open, forkSession, onSessionRename, onSessionArchive, archivedSessionIds, usePanelInfo, orderBy, sessionOrderByAccount, sessionUpdatedAtByAccount, syncSessionOrderAccount, setSessionOrder, revealSessionId, onSessionRevealed, t }) {
1733
+ function FlatList({ list, sessionIds, useSessionPendingInteraction, open, forkSession, onSessionRename, onSessionArchive, usePanelInfo, setSessionOrder, revealSessionId, onSessionRevealed, t }) {
1786
1734
  const panelActive = usePanelInfo((info) => info.activePanelId !== null);
1787
- const list = useSessions((s) => s);
1788
1735
  const pendingInteractions = useSessionPendingInteraction((s) => s);
1789
- const baseRows = (0, react.useMemo)(() => deriveFlat(list, archivedSessionIds, pendingInteractions), [
1736
+ const rows = (0, react.useMemo)(() => deriveFlat(list, sessionIds, pendingInteractions), [
1790
1737
  list,
1791
- archivedSessionIds,
1792
- pendingInteractions
1793
- ]);
1794
- const sessionIds = (0, react.useMemo)(() => baseRows.map((row) => row.id), [baseRows]);
1795
- const previousOrderBy = (0, react.useRef)(orderBy);
1796
- (0, react.useEffect)(() => {
1797
- if (list.phase !== "ready") return;
1798
- const previousOrder = sessionOrderByAccount[FLAT_SESSION_ORDER_KEY];
1799
- const previousUpdatedAt = sessionUpdatedAtByAccount["__flat_session_order__"] ?? {};
1800
- const switchedToUpdated = previousOrderBy.current !== "updated" && orderBy === "updated";
1801
- previousOrderBy.current = orderBy;
1802
- const next = nextSessionOrderAccount({
1803
- sessionIds,
1804
- previousOrder,
1805
- previousUpdatedAt,
1806
- list,
1807
- orderBy,
1808
- sortByRecency: orderBy === "updated" && (previousOrder === void 0 || switchedToUpdated)
1809
- });
1810
- if (next.changed) syncSessionOrderAccount(FLAT_SESSION_ORDER_KEY, next.order.map((id) => id), next.updatedAt);
1811
- }, [
1812
- list,
1813
- orderBy,
1814
- sessionOrderByAccount,
1815
- sessionUpdatedAtByAccount,
1816
1738
  sessionIds,
1817
- syncSessionOrderAccount
1818
- ]);
1819
- const rows = (0, react.useMemo)(() => {
1820
- const byId = new Map(baseRows.map((row) => [row.id, row]));
1821
- return reconciledSessionOrder(sessionIds, sessionOrderByAccount[FLAT_SESSION_ORDER_KEY]).flatMap((id) => {
1822
- const row = byId.get(id);
1823
- return row === void 0 ? [] : [row];
1824
- });
1825
- }, [
1826
- baseRows,
1827
- sessionOrderByAccount,
1828
- sessionIds
1739
+ pendingInteractions
1829
1740
  ]);
1830
1741
  const [drag, setDrag] = (0, react.useState)(null);
1831
1742
  const dropCommitted = (0, react.useRef)(false);
@@ -1844,7 +1755,8 @@ window.__ModuleLoader__.load({
1844
1755
  const nextOrder = rows.map((row) => row.id).filter((id) => id !== activeDrag.sessionId);
1845
1756
  const insertAt = anchor === void 0 ? nextOrder.length : nextOrder.indexOf(anchor);
1846
1757
  nextOrder.splice(insertAt === -1 ? nextOrder.length : insertAt, 0, activeDrag.sessionId);
1847
- setSessionOrder(FLAT_SESSION_ORDER_KEY, nextOrder.map((id) => id));
1758
+ const currentBlank = rows.find((node) => node.blank)?.id;
1759
+ setSessionOrder(FLAT_SESSION_ORDER_KEY, pinCurrentBlank(nextOrder, currentBlank));
1848
1760
  };
1849
1761
  const now = Date.now();
1850
1762
  return (0, react_jsx_runtime.jsxs)("div", {
@@ -1858,6 +1770,7 @@ window.__ModuleLoader__.load({
1858
1770
  children: t("empty.none")
1859
1771
  }), rows.map((node) => {
1860
1772
  const active = drag !== null;
1773
+ const normalizeHalf = (half) => node.blank ? "after" : half;
1861
1774
  return (0, react_jsx_runtime.jsx)(SessionNodeItem, {
1862
1775
  node,
1863
1776
  currentId: panelActive ? void 0 : list.current,
@@ -1886,14 +1799,14 @@ window.__ModuleLoader__.load({
1886
1799
  ...current,
1887
1800
  over: {
1888
1801
  id: node.id,
1889
- half
1802
+ half: normalizeHalf(half)
1890
1803
  }
1891
1804
  });
1892
1805
  },
1893
1806
  drop: (half) => {
1894
1807
  if (drag !== null) commitDrag(drag, {
1895
1808
  id: node.id,
1896
- half
1809
+ half: normalizeHalf(half)
1897
1810
  });
1898
1811
  },
1899
1812
  end: () => {
@@ -1973,8 +1886,9 @@ window.__ModuleLoader__.load({
1973
1886
  * @param props - composed slot props (shell owner share + store + injected actions).
1974
1887
  * @returns the region element tree.
1975
1888
  */
1976
- function WorkspaceBrowser({ wide, usePanelInfo, expandSidebar, useSessions, useSessionPendingInteraction, useWorkspaces, useStore, actions, startSession, open, renameSession, forkSession, renameWorkspace, deleteWorkspace, insertWorkspaceBefore, archiveSession, insertSessionBefore, createWorkspace, searchSessions, searchResultLimit, useDirectoryFlow, useHostInfo, renderSlot, t }) {
1889
+ function WorkspaceBrowser({ wide, usePanelInfo, expandSidebar, useSessions, useSessionPendingInteraction, useWorkspaces, useStore, actions, startSession, open, renameSession, forkSession, renameWorkspace, deleteWorkspace, insertWorkspaceBefore, archiveSession, createWorkspace, searchSessions, searchResultLimit, useDirectoryFlow, useHostInfo, renderSlot, t }) {
1977
1890
  const home = useHostInfo((info) => info.home);
1891
+ const list = useSessions((state) => state);
1978
1892
  const workspaces = useWorkspaces((state) => state.items);
1979
1893
  const workspacePhase = useWorkspaces((state) => state.phase);
1980
1894
  const workspaceStreamState = useWorkspaces((state) => state.state);
@@ -1984,34 +1898,54 @@ window.__ModuleLoader__.load({
1984
1898
  const orderBy = useStore((s) => s.orderBy);
1985
1899
  const groupExpansion = useStore((s) => s.groupExpansion);
1986
1900
  const sessionOrderByAccount = useStore((s) => s.sessionOrderByAccount);
1987
- const sessionUpdatedAtByAccount = useStore((s) => s.sessionUpdatedAtByAccount);
1988
- const currentBlankSessionId = useSessions((state) => {
1989
- const current = state.current;
1990
- return current !== void 0 && state.byId[current]?.blank === true ? current : void 0;
1991
- });
1992
- const currentBlankAccount = currentBlankSessionId === void 0 || workspacePhase !== "ready" ? void 0 : owningGroupKey(workspaces, currentBlankSessionId);
1993
- const promotedBlank = (0, react.useRef)(void 0);
1994
- (0, react.useEffect)(() => {
1995
- if (currentBlankSessionId === void 0 || currentBlankAccount === void 0) {
1996
- promotedBlank.current = void 0;
1997
- return;
1998
- }
1999
- const promoted = promotedBlank.current;
2000
- if (promoted !== void 0 && promoted.sessionId === currentBlankSessionId && promoted.accountKey === currentBlankAccount) return;
2001
- promotedBlank.current = {
2002
- sessionId: currentBlankSessionId,
2003
- accountKey: currentBlankAccount
1901
+ const workspaceReady = workspacePhase === "ready" && workspaceStreamState !== "loading";
1902
+ const currentBlank = list.current !== void 0 && list.byId[list.current]?.blank === true ? list.current : void 0;
1903
+ const ungroupedMemberIds = (0, react.useMemo)(() => {
1904
+ const accounted = new Set(workspaces.flatMap((workspace) => workspace.sessionIds));
1905
+ return list.ids.filter((id) => list.byId[id] !== void 0 && !accounted.has(id));
1906
+ }, [list, workspaces]);
1907
+ const flatMemberIds = (0, react.useMemo)(() => visibleSessionIds(list, archivedSessionIds), [archivedSessionIds, list]);
1908
+ const orderedWorkspaces = (0, react.useMemo)(() => workspaces.map((workspace) => {
1909
+ const memberIds = workspace.sessionIds;
1910
+ const baseOrder = orderBy === "updated" ? orderByRecency(memberIds, list.byId) : reconcileManualOrder(memberIds, sessionOrderByAccount[workspace.workspaceId], list.byId);
1911
+ return {
1912
+ ...workspace,
1913
+ sessionIds: pinCurrentBlank(baseOrder, currentBlank !== void 0 && memberIds.includes(currentBlank) ? currentBlank : void 0)
2004
1914
  };
2005
- for (const accountKey of new Set([currentBlankAccount, FLAT_SESSION_ORDER_KEY])) {
2006
- const previous = sessionOrderByAccount[accountKey] ?? [];
2007
- actions.setSessionOrder(accountKey, [currentBlankSessionId, ...previous.filter((id) => id !== currentBlankSessionId)]);
2008
- }
1915
+ }), [
1916
+ currentBlank,
1917
+ list.byId,
1918
+ orderBy,
1919
+ sessionOrderByAccount,
1920
+ workspaces
1921
+ ]);
1922
+ const orderedUngroupedSessionIds = (0, react.useMemo)(() => {
1923
+ return pinCurrentBlank(orderBy === "updated" ? orderByRecency(ungroupedMemberIds, list.byId) : reconcileManualOrder(ungroupedMemberIds, sessionOrderByAccount[""], list.byId), currentBlank !== void 0 && ungroupedMemberIds.includes(currentBlank) ? currentBlank : void 0);
2009
1924
  }, [
2010
- actions.setSessionOrder,
2011
- currentBlankAccount,
2012
- currentBlankSessionId,
1925
+ currentBlank,
1926
+ list.byId,
1927
+ orderBy,
1928
+ sessionOrderByAccount,
1929
+ ungroupedMemberIds
1930
+ ]);
1931
+ const orderedFlatSessionIds = (0, react.useMemo)(() => {
1932
+ return pinCurrentBlank(orderBy === "updated" ? orderByRecency(flatMemberIds, list.byId) : reconcileManualOrder(flatMemberIds, sessionOrderByAccount[FLAT_SESSION_ORDER_KEY], list.byId), currentBlank !== void 0 && flatMemberIds.includes(currentBlank) ? currentBlank : void 0);
1933
+ }, [
1934
+ currentBlank,
1935
+ flatMemberIds,
1936
+ list.byId,
1937
+ orderBy,
2013
1938
  sessionOrderByAccount
2014
1939
  ]);
1940
+ const activeSessionOrders = (0, react.useMemo)(() => Object.fromEntries([
1941
+ ...orderedWorkspaces.map((workspace) => [workspace.workspaceId, workspace.sessionIds]),
1942
+ ["", orderedUngroupedSessionIds],
1943
+ [FLAT_SESSION_ORDER_KEY, orderedFlatSessionIds]
1944
+ ]), [
1945
+ orderedFlatSessionIds,
1946
+ orderedUngroupedSessionIds,
1947
+ orderedWorkspaces
1948
+ ]);
2015
1949
  (0, react.useEffect)(() => {
2016
1950
  if (workspacePhase !== "ready") return;
2017
1951
  actions.retainAccountKeys([
@@ -2024,6 +1958,44 @@ window.__ModuleLoader__.load({
2024
1958
  workspacePhase,
2025
1959
  workspaces
2026
1960
  ]);
1961
+ (0, react.useEffect)(() => {
1962
+ if (list.phase !== "ready" || workspaceReady || orderBy !== "manual" || currentBlank === void 0) return;
1963
+ const changed = {};
1964
+ for (const [key, ids] of Object.entries(activeSessionOrders)) {
1965
+ if (key !== "__flat_session_order__" && workspacePhase !== "ready") continue;
1966
+ const saved = sessionOrderByAccount[key] ?? [];
1967
+ if (ids[0] !== currentBlank || saved[0] === currentBlank) continue;
1968
+ changed[key] = [currentBlank, ...saved.filter((id) => id !== currentBlank)];
1969
+ }
1970
+ if (Object.keys(changed).length > 0) actions.syncSessionOrders(changed);
1971
+ }, [
1972
+ actions.syncSessionOrders,
1973
+ activeSessionOrders,
1974
+ currentBlank,
1975
+ list.phase,
1976
+ orderBy,
1977
+ sessionOrderByAccount,
1978
+ workspacePhase,
1979
+ workspaceReady
1980
+ ]);
1981
+ (0, react.useEffect)(() => {
1982
+ if (list.phase !== "ready" || !workspaceReady || orderBy !== "manual") return;
1983
+ const changed = Object.fromEntries(Object.entries(activeSessionOrders).filter(([key, ids]) => {
1984
+ const saved = sessionOrderByAccount[key];
1985
+ return saved === void 0 || saved.length !== ids.length || ids.some((id, index) => id !== saved[index]);
1986
+ }));
1987
+ if (Object.keys(changed).length > 0) actions.syncSessionOrders(changed);
1988
+ }, [
1989
+ actions.syncSessionOrders,
1990
+ activeSessionOrders,
1991
+ list.phase,
1992
+ orderBy,
1993
+ sessionOrderByAccount,
1994
+ workspaceReady
1995
+ ]);
1996
+ const saveSessionOrder = (accountKey, order) => {
1997
+ actions.setSessionOrder(accountKey, order, activeSessionOrders);
1998
+ };
2027
1999
  const [query, setQuery] = (0, react.useState)("");
2028
2000
  const [searchExpanded, setSearchExpanded] = (0, react.useState)(false);
2029
2001
  const [revealSessionId, setRevealSessionId] = (0, react.useState)(void 0);
@@ -2296,7 +2268,7 @@ window.__ModuleLoader__.load({
2296
2268
  actions.setGroupBy(mode);
2297
2269
  },
2298
2270
  onOrderPick: (mode) => {
2299
- actions.setOrderBy(mode);
2271
+ actions.setOrderBy(mode, activeSessionOrders);
2300
2272
  },
2301
2273
  t
2302
2274
  }), directoryFlowAvailable && (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
@@ -2367,42 +2339,34 @@ window.__ModuleLoader__.load({
2367
2339
  t
2368
2340
  }) : groupBy === "flat" ? (0, react_jsx_runtime.jsx)(FlatList, {
2369
2341
  usePanelInfo,
2370
- useSessions,
2342
+ list,
2343
+ sessionIds: orderedFlatSessionIds,
2371
2344
  useSessionPendingInteraction,
2372
2345
  open,
2373
2346
  forkSession,
2374
2347
  onSessionRename,
2375
2348
  onSessionArchive,
2376
- archivedSessionIds,
2377
- orderBy,
2378
- sessionOrderByAccount,
2379
- sessionUpdatedAtByAccount,
2380
- syncSessionOrderAccount: actions.syncSessionOrderAccount,
2381
- setSessionOrder: actions.setSessionOrder,
2349
+ setSessionOrder: saveSessionOrder,
2382
2350
  revealSessionId,
2383
2351
  onSessionRevealed: acknowledgeSessionReveal,
2384
2352
  t
2385
2353
  }) : (0, react_jsx_runtime.jsx)(SessionTree, {
2386
2354
  usePanelInfo,
2387
- useSessions,
2355
+ list,
2388
2356
  useSessionPendingInteraction,
2389
2357
  onSessionRename,
2390
2358
  onSessionArchive,
2391
2359
  forkSession,
2392
- workspaces,
2393
- workspaceReady: workspacePhase === "ready" && workspaceStreamState !== "loading",
2360
+ workspaces: orderedWorkspaces,
2361
+ ungroupedSessionIds: orderedUngroupedSessionIds,
2362
+ workspaceReady,
2394
2363
  groupExpansion,
2395
2364
  setGroupExpanded: actions.setGroupExpanded,
2396
- sessionOrderByAccount,
2397
- sessionUpdatedAtByAccount,
2398
- syncSessionOrderAccount: actions.syncSessionOrderAccount,
2399
- setSessionOrder: actions.setSessionOrder,
2365
+ setSessionOrder: saveSessionOrder,
2400
2366
  archivedSessionIds,
2401
2367
  startSession,
2402
2368
  open,
2403
2369
  insertWorkspaceBefore,
2404
- insertSessionBefore,
2405
- orderBy,
2406
2370
  revealSessionId,
2407
2371
  onSessionRevealed: acknowledgeSessionReveal,
2408
2372
  home,
@@ -2778,9 +2742,6 @@ window.__ModuleLoader__.load({
2778
2742
  archiveSession: async (sessionId) => {
2779
2743
  await uiWorkspace.archiveSession(sessionId);
2780
2744
  },
2781
- insertSessionBefore: async (workspaceId, sessionId, beforeSessionId) => {
2782
- await workspaces.insertSessionBefore(workspaceId, sessionId, beforeSessionId);
2783
- },
2784
2745
  createWorkspace: (input) => workspaces.create(input),
2785
2746
  hooks: {
2786
2747
  directoryFlow: browserFlowSource,
@@ -130,12 +130,6 @@ export type WorkspaceBrowserInjected = {
130
130
  * session clears the selection into the New Session view state.
131
131
  */
132
132
  archiveSession: (sessionId: SessionId) => Promise<void>;
133
- /**
134
- * Reorder a session inside its Workspace account (DOM-insertBefore
135
- * semantics: omitted anchor appends to the end). The view refreshes from
136
- * the Host response/changed frame; failures leave the order unchanged.
137
- */
138
- insertSessionBefore: (workspaceId: WorkspaceId, sessionId: SessionId, beforeSessionId?: SessionId) => Promise<void>;
139
133
  /** Adopt a picked host directory as a real Workspace before targeting a Session. */
140
134
  createWorkspace: (input: {
141
135
  path: string;
@@ -40,6 +40,11 @@ export interface UiWorkspace {
40
40
  * @param sessionId - Session to archive.
41
41
  */
42
42
  archiveSession(sessionId: SessionId): Promise<void>;
43
+ /**
44
+ * Unarchive a Session, restoring it to its recorded Workspace position.
45
+ * @param sessionId - Session to unarchive.
46
+ */
47
+ unarchiveSession(sessionId: SessionId): Promise<void>;
43
48
  /**
44
49
  * Open the Host-native directory picker.
45
50
  * @returns the selected directory, or null when cancelled.
@@ -93,6 +98,7 @@ declare class UiWorkspaceService extends Service implements UiWorkspace {
93
98
  forkSession(sessionId: SessionId): Promise<void>;
94
99
  startSession(workspaceId?: WorkspaceId): void;
95
100
  archiveSession(sessionId: SessionId): Promise<void>;
101
+ unarchiveSession(sessionId: SessionId): Promise<void>;
96
102
  pickDirectory(): Promise<string | null>;
97
103
  listDirectory(path?: string, signal?: AbortSignal): Promise<DirectoryListing>;
98
104
  createDirectory(path: string, name: string): Promise<string>;
@@ -78,7 +78,7 @@ export declare function SearchResultItem({ result, currentId, onOpen, t }: {
78
78
  * @param props.onFork - fork a session at its last completed turn.
79
79
  * @param props.onArchive - archive a session by id.
80
80
  * @param props.onReveal - scroll this row into view after search navigation, then acknowledge it.
81
- * @param props.drag - optional draggable-row wiring.
81
+ * @param props.drag - optional row-drag target wiring; blank rows cannot start a drag.
82
82
  * @param props.flat - omit the empty status slot in the hierarchy-free flat list.
83
83
  * @param props.t - the browser root's locale seat.
84
84
  * @returns the session row.
@@ -96,7 +96,7 @@ export declare function SessionNodeItem({ node, currentId, now, onOpen, onRename
96
96
  onArchive: (id: SessionNode['id']) => void;
97
97
  /** Scroll this row into view after search navigation, then acknowledge it. */
98
98
  onReveal?: (() => void) | undefined;
99
- /** Present only on draggable rows (workspace-group sessions outside search). */
99
+ /** Present on reorderable-list rows so every row can remain a drop target. */
100
100
  drag?: RowDragProps | undefined;
101
101
  /** The row is rendered without a parent Workspace header. */
102
102
  flat?: boolean | undefined;
@@ -4,5 +4,5 @@ import type { WorkspaceBrowserProps } from '../contract/slots.ts';
4
4
  * @param props - composed slot props (shell owner share + store + injected actions).
5
5
  * @returns the region element tree.
6
6
  */
7
- export declare function WorkspaceBrowser({ wide, usePanelInfo, expandSidebar, useSessions, useSessionPendingInteraction, useWorkspaces, useStore, actions, startSession, open, renameSession, forkSession, renameWorkspace, deleteWorkspace, insertWorkspaceBefore, archiveSession, insertSessionBefore, createWorkspace, searchSessions, searchResultLimit, useDirectoryFlow, useHostInfo, renderSlot, t, }: WorkspaceBrowserProps): import("react").JSX.Element;
7
+ export declare function WorkspaceBrowser({ wide, usePanelInfo, expandSidebar, useSessions, useSessionPendingInteraction, useWorkspaces, useStore, actions, startSession, open, renameSession, forkSession, renameWorkspace, deleteWorkspace, insertWorkspaceBefore, archiveSession, createWorkspace, searchSessions, searchResultLimit, useDirectoryFlow, useHostInfo, renderSlot, t, }: WorkspaceBrowserProps): import("react").JSX.Element;
8
8
  //# sourceMappingURL=WorkspaceBrowser.d.ts.map
@@ -10,7 +10,7 @@ import { type EngineStoreHandle } from '@deepseek-ai/dsh-client-store';
10
10
  export declare const FLAT_SESSION_ORDER_KEY = "__flat_session_order__";
11
11
  /** Session-list grouping mode: workspace sections or one flat recency list. */
12
12
  export type SessionGroupBy = 'workspace' | 'flat';
13
- /** Session order: user-arranged only, or user-arranged plus activity promotion. */
13
+ /** Session order: saved manual positions or current recency. */
14
14
  export type SessionOrderBy = 'manual' | 'updated';
15
15
  /** Workspace browser viewing state persisted across surface remounts and reloads. */
16
16
  type WorkspaceViewState = {
@@ -18,10 +18,8 @@ type WorkspaceViewState = {
18
18
  orderBy: SessionOrderBy;
19
19
  /** Explicit zero-or-five-session state keyed by Workspace group identity. */
20
20
  groupExpansion: Record<string, boolean>;
21
- /** Shared editable order per Workspace group plus the browser-local flat-list account. */
21
+ /** Saved manual order per Workspace group plus the browser-local flat-list account. */
22
22
  sessionOrderByAccount: Record<string, string[]>;
23
- /** Last observed update timestamps per order account for one-time promotion events. */
24
- sessionUpdatedAtByAccount: Record<string, Record<string, number>>;
25
23
  };
26
24
  /**
27
25
  * Annotation twin of the actions literal below (the export needs a declared
@@ -29,11 +27,11 @@ type WorkspaceViewState = {
29
27
  */
30
28
  type WorkspaceViewActions = {
31
29
  setGroupBy: (draft: WorkspaceViewState, mode: SessionGroupBy) => void;
32
- setOrderBy: (draft: WorkspaceViewState, mode: SessionOrderBy) => void;
30
+ setOrderBy: (draft: WorkspaceViewState, mode: SessionOrderBy, initialOrders: Readonly<Record<string, readonly string[]>>) => void;
33
31
  setGroupExpanded: (draft: WorkspaceViewState, key: string, expanded: boolean) => void;
34
32
  retainAccountKeys: (draft: WorkspaceViewState, workspaceKeys: readonly string[]) => void;
35
- syncSessionOrderAccount: (draft: WorkspaceViewState, accountKey: string, order: string[], updatedAt: Record<string, number>) => void;
36
- setSessionOrder: (draft: WorkspaceViewState, accountKey: string, order: string[]) => void;
33
+ syncSessionOrders: (draft: WorkspaceViewState, orders: Readonly<Record<string, readonly string[]>>) => void;
34
+ setSessionOrder: (draft: WorkspaceViewState, accountKey: string, order: readonly string[], initialOrders: Readonly<Record<string, readonly string[]>>) => void;
37
35
  };
38
36
  /**
39
37
  * Create the workspace browser viewing store handle.
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Derives the workspace browser tree from Host Workspace order and membership.
3
- * Unassigned Sessions trail under Ungrouped; only the selected blank Session
4
- * remains visible.
2
+ * Derives the workspace browser tree from caller-projected Workspace and
3
+ * Session order. Unassigned Sessions trail under Ungrouped; only the selected
4
+ * blank Session remains visible.
5
5
  */
6
6
  import { type SessionListState, type SessionSearchResultItem } from '@deepseek-ai/dsh-api-session-controller/client';
7
7
  import type { WorkspaceId, WorkspaceView } from '@deepseek-ai/dsh-api-workspace-controller/client';
@@ -91,6 +91,28 @@ export interface TreeView {
91
91
  * @returns basename, the raw cwd when it has no basename, or an empty ungrouped marker.
92
92
  */
93
93
  export declare function workspaceLabel(cwd: string | undefined): string;
94
+ /**
95
+ * Project known account members by current Session recency.
96
+ * @param sessionIds - authoritative account membership.
97
+ * @param summaries - current Session summaries; members without a summary are omitted until it arrives.
98
+ * @returns known members newest first, with Session identity as the deterministic tie-break.
99
+ */
100
+ export declare function orderByRecency(sessionIds: readonly SessionId[], summaries: SessionListState['byId']): SessionId[];
101
+ /**
102
+ * Reconcile a browser-local manual order with current account membership.
103
+ * @param memberIds - authoritative account membership.
104
+ * @param savedOrder - previously saved browser-local order.
105
+ * @param summaries - current Session summaries used to append newly known members by recency.
106
+ * @returns retained saved slots followed by newly known members; departed members and unknown new members are omitted.
107
+ */
108
+ export declare function reconcileManualOrder(memberIds: readonly SessionId[], savedOrder: readonly string[] | undefined, summaries: SessionListState['byId']): SessionId[];
109
+ /**
110
+ * Keep the selected provisional New Session ahead of either base order.
111
+ * @param order - recency or reconciled manual order.
112
+ * @param currentBlank - selected blank Session in this account, when present.
113
+ * @returns a copy with the selected blank first and no duplicate slot.
114
+ */
115
+ export declare function pinCurrentBlank(order: readonly SessionId[], currentBlank: SessionId | undefined): SessionId[];
94
116
  /**
95
117
  * Derive the workspace browser groups with every session as a top-level row.
96
118
  *
@@ -100,7 +122,7 @@ export declare function workspaceLabel(cwd: string | undefined): string;
100
122
  * Content search lives outside this derivation
101
123
  * (see {@link deriveSearchResults}).
102
124
  * @param list - sessions list snapshot (`current` feeds containsCurrent).
103
- * @param workspaces - real workspaces in stable Host order.
125
+ * @param workspaces - real Workspaces in Host group order with caller-projected Session order.
104
126
  * @param archivedSessionIds - registry-global archive set.
105
127
  * @param pendingInteractions - pending UI interactions by Session.
106
128
  * @param view - local expansion arrays.
@@ -108,16 +130,20 @@ export declare function workspaceLabel(cwd: string | undefined): string;
108
130
  */
109
131
  export declare function deriveGroups(list: SessionListState, workspaces: readonly WorkspaceView[], archivedSessionIds: readonly SessionId[], pendingInteractions: SessionPendingInteractions, view: TreeView): GroupNode[];
110
132
  /**
111
- * Derive the flat session list ("In one list" mode): every session — fork
112
- * children included — as a top-level row, strictly newest-first. No grouping,
113
- * no parent/child adjacency. Content search lives outside this derivation
114
- * (see {@link deriveSearchResults}).
133
+ * Select flat-list members without deriving row presentation or ordering.
115
134
  * @param list - sessions list snapshot.
116
135
  * @param archivedSessionIds - registry-global archive set.
136
+ * @returns known visible Session ids in list order, including ordinary forks and only the current blank.
137
+ */
138
+ export declare function visibleSessionIds(list: SessionListState, archivedSessionIds: readonly SessionId[]): SessionId[];
139
+ /**
140
+ * Derive flat rows from the browser's ordered visible Session ids.
141
+ * @param list - sessions list snapshot used to select the ids.
142
+ * @param sessionIds - known visible members in render order, including any pinned blank.
117
143
  * @param pendingInteractions - pending UI interactions by Session.
118
- * @returns flat rows in render order.
144
+ * @returns flat rows in the supplied order with current status indicators.
119
145
  */
120
- export declare function deriveFlat(list: SessionListState, archivedSessionIds: readonly SessionId[], pendingInteractions: SessionPendingInteractions): SessionNode[];
146
+ export declare function deriveFlat(list: SessionListState, sessionIds: readonly SessionId[], pendingInteractions: SessionPendingInteractions): SessionNode[];
121
147
  /**
122
148
  * Merge immediate title/Workspace substring matches with ranked Host content
123
149
  * matches. Local rows lead newest-first, content-only rows retain backend
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-client-ui-workspace",
3
3
  "description": "Workspace picker plugin: one WorkspacePicker registered into the sidebar and empty-state workspace slots",
4
- "version": "0.1.5-rc.2",
4
+ "version": "0.1.6-alpha.1",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -50,25 +50,25 @@
50
50
  "@types/react": "~18.3.1",
51
51
  "react": "^18.2.0",
52
52
  "clsx": "^2.0.0",
53
- "@deepseek-ai/dsh-api-remotes": "^0.1.5-rc.2",
54
- "@deepseek-ai/dsh-api-session-controller": "^0.1.5-rc.2",
55
- "@deepseek-ai/dsh-api-workspace-controller": "^0.1.5-rc.2",
56
- "@deepseek-ai/dsh-client-locale": "^0.1.5-rc.2",
57
- "@deepseek-ai/dsh-client-store": "^0.1.5-rc.2",
58
- "@deepseek-ai/dsh-client-connection": "^0.1.5-rc.2",
59
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.2",
60
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.2",
61
- "@deepseek-ai/dsh-client-ui-layout": "^0.1.5-rc.2",
62
- "@deepseek-ai/dsh-client-ui-renderer": "^0.1.5-rc.2",
63
- "@deepseek-ai/dsh-client-ui-sidebar": "^0.1.5-rc.2",
64
- "@deepseek-ai/dsh-client-ui-session": "^0.1.5-rc.2",
65
- "@deepseek-ai/dsh-schedule": "^0.1.5-rc.2",
66
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.2",
67
- "@deepseek-ai/dsh-typert-protocol": "^0.1.5-rc.2",
68
- "@deepseek-ai/cordis": "^4.0.2",
69
- "@deepseek-ai/dsh-util-workspace-path": "^0.1.5-rc.2",
70
- "@deepseek-ai/dsh-session": "^0.1.5-rc.2",
71
- "@deepseek-ai/dsh-client-test-runtime": "^0.1.5-rc.2"
53
+ "@deepseek-ai/dsh-api-remotes": "^0.1.6-alpha.1",
54
+ "@deepseek-ai/dsh-api-session-controller": "^0.1.6-alpha.1",
55
+ "@deepseek-ai/dsh-api-workspace-controller": "^0.1.6-alpha.1",
56
+ "@deepseek-ai/dsh-client-connection": "^0.1.6-alpha.1",
57
+ "@deepseek-ai/dsh-client-locale": "^0.1.6-alpha.1",
58
+ "@deepseek-ai/dsh-client-store": "^0.1.6-alpha.1",
59
+ "@deepseek-ai/dsh-client-test-runtime": "^0.1.6-alpha.1",
60
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.6-alpha.1",
61
+ "@deepseek-ai/dsh-client-ui-layout": "^0.1.6-alpha.1",
62
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.6-alpha.1",
63
+ "@deepseek-ai/dsh-client-ui-session": "^0.1.6-alpha.1",
64
+ "@deepseek-ai/dsh-client-ui-sidebar": "^0.1.6-alpha.1",
65
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.6-alpha.1",
66
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.6-alpha.1",
67
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.6-alpha.1",
68
+ "@deepseek-ai/dsh-session": "^0.1.6-alpha.1",
69
+ "@deepseek-ai/dsh-util-workspace-path": "^0.1.6-alpha.1",
70
+ "@deepseek-ai/dsh-schedule": "^0.1.6-alpha.1",
71
+ "@deepseek-ai/cordis": "^4.0.2"
72
72
  },
73
73
  "files": [
74
74
  "lib/index.js",