@deepseek-ai/dsh-client-ui-workspace 0.1.6-alpha.2 → 0.1.7-alpha.2

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/lib/client.js CHANGED
@@ -4,11 +4,781 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
- let _deepseek_ai_cordis = require("@deepseek-ai/cordis");
8
7
  let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
8
+ let _deepseek_ai_cordis = require("@deepseek-ai/cordis");
9
9
  let react_jsx_runtime = require("react/jsx-runtime");
10
10
  let react = require("react");
11
11
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
12
+ //#region lib/types/client/contract/slots.js
13
+ /**
14
+ * Bind the row's render occurrence into the entries' `useMenuOpenState` hook:
15
+ * the owner supplies its open-state pair as the occurrence's `hookContext`,
16
+ * and the hook hands that pair back.
17
+ * @param _standard - framework standard props (unused).
18
+ * @param state - the menu's open-state pair from the render occurrence.
19
+ * @returns the hook the entry calls.
20
+ */
21
+ const menuOpenStateFactory = (_standard, state) => () => state;
22
+ //#endregion
23
+ //#region lib/types/client/locales.js
24
+ /**
25
+ * `workspace` namespace dictionaries: the browsing region (section header,
26
+ * search, tree rows, dialogs) and the pick/add flow. Runtime failure
27
+ * messages (wire error strings) pass through untranslated by policy.
28
+ */
29
+ /** Simplified Chinese dictionary (the key-set source of truth). */
30
+ const zh = {
31
+ "defaultWorkspace.failed": "无法创建默认工作区,请通过“选择工作区”选择文件夹",
32
+ "defaultWorkspace.title": "默认工作区",
33
+ "group.ungrouped": "未分组",
34
+ "session.new": "新会话",
35
+ "section.workspaces": "工作区",
36
+ "section.sessions": "会话",
37
+ "viewOptions.label": "视图选项",
38
+ "groupBy.label": "分组方式",
39
+ "groupBy.workspace": "按工作区",
40
+ "groupBy.workspaceTree": "按工作区树",
41
+ "groupBy.flat": "单列表",
42
+ "orderBy.label": "排序方式",
43
+ "orderBy.manual": "手动排序",
44
+ "orderBy.updated": "最近更新",
45
+ "filterBy.label": "筛选会话",
46
+ "viewOptions.showArchived": "显示已归档",
47
+ "viewOptions.onlyArchived": "仅显示已归档",
48
+ "sessions.expand": "展开其余 {n} 个会话",
49
+ "sessions.collapse": "收起",
50
+ "empty.none": "暂无会话",
51
+ "empty.noMatches": "无匹配结果",
52
+ "workspace.add": "添加工作区",
53
+ "search.sessions.aria": "搜索会话",
54
+ "search.placeholder": "搜索会话名称",
55
+ "search.clear": "清除搜索",
56
+ "search.results.aria": "搜索结果",
57
+ "search.pending": "正在搜索会话历史…",
58
+ "search.noMatches": "无匹配会话",
59
+ "search.hasMore": "仅显示前 {n} 条结果,请缩小搜索范围。",
60
+ "menu.addWorkspace": "添加工作区…",
61
+ "picker.loading": "正在加载工作区…",
62
+ "conflict.named": "已存在名为“{name}”的工作区。",
63
+ "folderError.title": "无法打开文件夹",
64
+ "folderError.retry": "重新选择",
65
+ "rename": "重命名",
66
+ "rename.workspace.title": "重命名工作区",
67
+ "rename.session.title": "重命名会话",
68
+ "field.workspaceName": "工作区名称",
69
+ "field.sessionName": "会话名称",
70
+ "delete.workspace": "删除工作区",
71
+ "delete.desc": "将把“{name}”从工作区列表中移除。文件夹与会话记录会保留,其会话将显示在“未分组”下。",
72
+ "delete.pending": "正在删除工作区…",
73
+ "menu.fork": "分叉会话",
74
+ "menu.archiveSession": "归档会话",
75
+ "menu.unarchiveSession": "取消归档",
76
+ "menu.pinSession": "置顶会话",
77
+ "menu.unpinSession": "取消置顶",
78
+ "row.archived": "已归档",
79
+ "row.pinned": "已置顶",
80
+ "toast.archivedNotOpenable": "已归档对话暂时无法查看,请取消归档后查看",
81
+ "toast.archived": "会话已归档,可",
82
+ "toast.stoppedAndArchived": "已停止并归档,可",
83
+ "archive.confirm.title": "停止并归档此会话?",
84
+ "archive.confirm.desc": "“{title}”仍有正在进行的工作。归档会先停止这些工作;之后可在侧栏“显示已归档”中恢复会话,被停止的工作不会自动继续。",
85
+ "archive.confirm.activity": "将被停止的工作",
86
+ "archive.confirm.turn": "进行中的回合",
87
+ "archive.confirm.subagents.one": "{n} 个运行中的子代理:{names}",
88
+ "archive.confirm.subagents.other": "{n} 个运行中的子代理:{names}",
89
+ "archive.confirm.jobs.one": "{n} 个后台任务:{names}",
90
+ "archive.confirm.jobs.other": "{n} 个后台任务:{names}",
91
+ "archive.confirm.schedules.one": "{n} 条定时提醒:{names}",
92
+ "archive.confirm.schedules.other": "{n} 条定时提醒:{names}",
93
+ "archive.confirm.other.one": "{n} 项其他工作({kind})",
94
+ "archive.confirm.other.other": "{n} 项其他工作({kind})",
95
+ "archive.confirm.listSeparator": "、",
96
+ "archive.confirm.action": "停止并归档",
97
+ "archive.confirm.pending": "正在停止并归档…",
98
+ "toast.archivedUndo": "撤销",
99
+ "toast.archivedOr": "或",
100
+ "toast.archivedFilter": "筛选已归档会话",
101
+ "toast.pinFailed": "置顶失败,请稍后重试",
102
+ "toast.unpinFailed": "取消置顶失败,请稍后重试",
103
+ "toast.createFailed": "新建会话失败:{message}",
104
+ "sessions.count.one": "{n} 个会话",
105
+ "sessions.count.other": "{n} 个会话",
106
+ "actions.workspace.aria": "工作区“{name}”的操作",
107
+ "actions.session.aria": "会话“{name}”的操作",
108
+ "actions.archive": "归档会话",
109
+ "actions.unarchive": "取消归档",
110
+ "actions.pin": "置顶会话",
111
+ "actions.unpin": "取消置顶",
112
+ "actions.newSession": "新会话",
113
+ "actions.newSession.aria": "在“{name}”中新建会话",
114
+ "status.running": "进行中",
115
+ "status.subagentsRunning.one": "{n} 个子代理运行中",
116
+ "status.subagentsRunning.other": "{n} 个子代理运行中",
117
+ "status.idle": "空闲",
118
+ "status.waitingApproval": "等待审批",
119
+ "status.planReview": "计划待审",
120
+ "status.waitingAnswer": "等待回答",
121
+ "status.compact.approval": "待审批",
122
+ "status.compact.planReview": "计划待审",
123
+ "status.compact.answer": "待回答",
124
+ "status.completed": "已完成",
125
+ "schedule.active": "有活动定时任务",
126
+ "hover.created": "创建于 {time}",
127
+ "hover.copied": "已复制",
128
+ "date.ymd": "{y}年{m}月{d}日",
129
+ "time.now": "刚刚",
130
+ "time.minutes": "{n}分钟",
131
+ "time.hours": "{n}小时",
132
+ "time.days": "{n}天",
133
+ "time.months": "{n}个月",
134
+ "time.years": "{n}年",
135
+ "time.ago": "{t}前"
136
+ };
137
+ /** English dictionary, checked complete against the zh key set. */
138
+ const en = {
139
+ "defaultWorkspace.failed": "Unable to create default workspace. Use Choose workspace to select a folder.",
140
+ "defaultWorkspace.title": "Default workspace",
141
+ "group.ungrouped": "Ungrouped",
142
+ "session.new": "New Session",
143
+ "section.workspaces": "Workspaces",
144
+ "section.sessions": "Sessions",
145
+ "viewOptions.label": "View options",
146
+ "groupBy.label": "Group by",
147
+ "groupBy.workspace": "WorkSpace",
148
+ "groupBy.workspaceTree": "Workspace Tree",
149
+ "groupBy.flat": "In one list",
150
+ "orderBy.label": "Order by",
151
+ "orderBy.manual": "Manual",
152
+ "orderBy.updated": "Last updated",
153
+ "filterBy.label": "Filter sessions",
154
+ "viewOptions.showArchived": "Show archived",
155
+ "viewOptions.onlyArchived": "Archived only",
156
+ "sessions.expand": "Show {n} more sessions",
157
+ "sessions.collapse": "Show less",
158
+ "empty.none": "No sessions yet",
159
+ "empty.noMatches": "No matches",
160
+ "workspace.add": "Add workspace",
161
+ "search.sessions.aria": "Search sessions",
162
+ "search.placeholder": "Search session names",
163
+ "search.clear": "Clear search",
164
+ "search.results.aria": "Search results",
165
+ "search.pending": "Searching session history…",
166
+ "search.noMatches": "No matching sessions",
167
+ "search.hasMore": "Showing the first {n} results. Narrow your search.",
168
+ "menu.addWorkspace": "Add workspace…",
169
+ "picker.loading": "Loading workspaces…",
170
+ "conflict.named": "A workspace named “{name}” already exists.",
171
+ "folderError.title": "Couldn’t open folder",
172
+ "folderError.retry": "Choose again",
173
+ "rename": "Rename",
174
+ "rename.workspace.title": "Rename workspace",
175
+ "rename.session.title": "Rename session",
176
+ "field.workspaceName": "Workspace name",
177
+ "field.sessionName": "Session name",
178
+ "delete.workspace": "Delete workspace",
179
+ "delete.desc": "This removes “{name}” from the workspace list. The folder and session logs will be kept. Its sessions will appear under Ungrouped.",
180
+ "delete.pending": "Deleting workspace…",
181
+ "menu.fork": "Fork session",
182
+ "menu.archiveSession": "Archive session",
183
+ "menu.unarchiveSession": "Unarchive session",
184
+ "menu.pinSession": "Pin session",
185
+ "menu.unpinSession": "Unpin session",
186
+ "row.archived": "Archived",
187
+ "row.pinned": "Pinned",
188
+ "toast.archivedNotOpenable": "Archived sessions cannot be opened. Unarchive it to view.",
189
+ "toast.archived": "Session archived. You can ",
190
+ "toast.stoppedAndArchived": "Session stopped and archived. You can ",
191
+ "archive.confirm.title": "Stop and archive this session?",
192
+ "archive.confirm.desc": "“{title}” still has work in progress. Archiving stops it first; you can restore the session later from “Show archived” in the sidebar, and the stopped work will not resume on its own.",
193
+ "archive.confirm.activity": "Work that will be stopped",
194
+ "archive.confirm.turn": "The turn in progress",
195
+ "archive.confirm.subagents.one": "{n} running subagent: {names}",
196
+ "archive.confirm.subagents.other": "{n} running subagents: {names}",
197
+ "archive.confirm.jobs.one": "{n} background job: {names}",
198
+ "archive.confirm.jobs.other": "{n} background jobs: {names}",
199
+ "archive.confirm.schedules.one": "{n} scheduled reminder: {names}",
200
+ "archive.confirm.schedules.other": "{n} scheduled reminders: {names}",
201
+ "archive.confirm.other.one": "{n} other item of work ({kind})",
202
+ "archive.confirm.other.other": "{n} other items of work ({kind})",
203
+ "archive.confirm.listSeparator": ", ",
204
+ "archive.confirm.action": "Stop and archive",
205
+ "archive.confirm.pending": "Stopping and archiving…",
206
+ "toast.archivedUndo": "undo",
207
+ "toast.archivedOr": " or ",
208
+ "toast.archivedFilter": "filter archived sessions",
209
+ "toast.pinFailed": "Pin failed. Try again later.",
210
+ "toast.unpinFailed": "Unpin failed. Try again later.",
211
+ "toast.createFailed": "New session failed: {message}",
212
+ "sessions.count.one": "{n} session",
213
+ "sessions.count.other": "{n} sessions",
214
+ "actions.workspace.aria": "Workspace actions for {name}",
215
+ "actions.session.aria": "Session actions for {name}",
216
+ "actions.archive": "Archive",
217
+ "actions.unarchive": "Unarchive",
218
+ "actions.pin": "Pin",
219
+ "actions.unpin": "Unpin",
220
+ "actions.newSession": "New session",
221
+ "actions.newSession.aria": "New session in {name}",
222
+ "status.running": "Running",
223
+ "status.subagentsRunning.one": "{n} subagent running",
224
+ "status.subagentsRunning.other": "{n} subagents running",
225
+ "status.idle": "Idle",
226
+ "status.waitingApproval": "Waiting for approval",
227
+ "status.planReview": "Plan awaiting review",
228
+ "status.waitingAnswer": "Waiting for answer",
229
+ "status.compact.approval": "Approval",
230
+ "status.compact.planReview": "Plan review",
231
+ "status.compact.answer": "Answer",
232
+ "status.completed": "Completed",
233
+ "schedule.active": "Has active scheduled task",
234
+ "hover.created": "Created {time}",
235
+ "hover.copied": "Copied",
236
+ "date.ymd": "{y}-{m}-{d}",
237
+ "time.now": "now",
238
+ "time.minutes": "{n}min",
239
+ "time.hours": "{n}h",
240
+ "time.days": "{n}d",
241
+ "time.months": "{n}mo",
242
+ "time.years": "{n}y",
243
+ "time.ago": "{t} ago"
244
+ };
245
+ //#endregion
246
+ //#region ../../util/values/src/index.ts
247
+ /**
248
+ * Mark an unreachable closed-union branch.
249
+ * @param value - impossible value; an unhandled typed variant fails at the call site.
250
+ * @param context - optional switch-site label included in the failure message.
251
+ * @returns never; a runtime value that escaped its type always throws.
252
+ */
253
+ function assertNever$1(value, context) {
254
+ const rendered = JSON.stringify(value) ?? String(value);
255
+ throw new Error(`unreachable variant${context ? ` in ${context}` : ""}: ${rendered}`);
256
+ }
257
+ //#endregion
258
+ //#region ../../util/workspace-path/src/index.ts
259
+ /** Whether a path uses a Windows drive or UNC prefix. */
260
+ function isWindowsStylePath(value) {
261
+ return /^[A-Za-z]:[/\\]/.test(value) || value.startsWith("\\\\");
262
+ }
263
+ /**
264
+ * Abbreviate a POSIX home directory for display.
265
+ * @param path - Absolute or already-short display path.
266
+ * @param home - Host account home; absent skips abbreviation.
267
+ * @returns `~` or `~/…` for the POSIX home and its descendants, otherwise `path`.
268
+ */
269
+ function abbreviateHomePath(path, home) {
270
+ if (home === void 0 || home === "") return path;
271
+ if (isWindowsStylePath(path) || isWindowsStylePath(home)) return path;
272
+ const root = home.replace(/\/+$/, "");
273
+ if (root === "" || root === "/") return path;
274
+ if (path.replace(/\/+$/, "") === root) return "~";
275
+ if (path.startsWith(`${root}/`)) return `~${path.slice(root.length)}`;
276
+ return path;
277
+ }
278
+ /**
279
+ * Read the final non-empty segment of a Workspace path for display.
280
+ * Workspace-label surfaces use this helper instead of deriving another basename.
281
+ * @param path - Workspace directory path using POSIX or Windows separators.
282
+ * @returns the final segment, or an empty string for a separator-only path.
283
+ */
284
+ function workspaceTitleOf(path) {
285
+ const trimmed = path.replace(/[/\\]+$/, "");
286
+ const separator = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
287
+ return trimmed.slice(separator + 1);
288
+ }
289
+ /**
290
+ * Resolve the Workspace browser group that owns one Session.
291
+ * @param workspaces - authoritative Workspace membership.
292
+ * @param sessionId - Session whose browser group is required.
293
+ * @returns owning Workspace id, or {@link UNGROUPED_KEY} when no Workspace accounts for it.
294
+ */
295
+ function owningGroupKey(workspaces, sessionId) {
296
+ return workspaces.find((workspace) => workspace.sessionIds.includes(sessionId))?.workspaceId ?? "";
297
+ }
298
+ function mainSessionId(list) {
299
+ return Object.values(list.byId).find((session) => (session.retainedBy.mainView ?? 0) > 0)?.id;
300
+ }
301
+ /**
302
+ * Directory display label: basename of the path (both separators accepted).
303
+ * Ungrouped-bucket fallback for surfaces without a workspace title.
304
+ * @param cwd - directory path, or undefined for the ungrouped bucket.
305
+ * @returns basename, the raw cwd when it has no basename, or an empty ungrouped marker.
306
+ */
307
+ function workspaceLabel(cwd) {
308
+ if (cwd === void 0 || cwd === "") return "";
309
+ const base = workspaceTitleOf(cwd);
310
+ return base !== "" ? base : cwd;
311
+ }
312
+ /**
313
+ * Project known account members by current Session recency.
314
+ * @param sessionIds - authoritative account membership.
315
+ * @param summaries - current Session summaries; members without a summary are omitted until it arrives.
316
+ * @returns known members newest first, with Session identity as the deterministic tie-break.
317
+ */
318
+ function orderByRecency(sessionIds, summaries) {
319
+ return sessionIds.flatMap((id) => {
320
+ const summary = summaries[id];
321
+ if (summary === void 0) return [];
322
+ return [{
323
+ id,
324
+ rank: summary.updatedAt
325
+ }];
326
+ }).sort((a, b) => {
327
+ if (a.rank !== b.rank) return b.rank - a.rank;
328
+ return a.id < b.id ? -1 : 1;
329
+ }).map((member) => member.id);
330
+ }
331
+ /**
332
+ * Reconcile a browser-local manual order with current account membership.
333
+ * New ordinary forks precede their sources without changing saved entries' relative order.
334
+ * @param memberIds - authoritative account membership.
335
+ * @param savedOrder - previously saved browser-local order.
336
+ * @param summaries - current Session metadata; unknown new members wait for their summaries.
337
+ * @param rowState - global pin and archive membership; only account members can supplement the order.
338
+ * @returns saved relative positions plus missing members ordered by pin, fork source, recency, and archive status.
339
+ */
340
+ function reconcileManualOrder(memberIds, savedOrder, summaries, rowState) {
341
+ const members = new Map(memberIds.map((id) => [id, id]));
342
+ const included = /* @__PURE__ */ new Set();
343
+ const ordered = [];
344
+ for (const key of savedOrder ?? []) {
345
+ const id = members.get(key);
346
+ if (id === void 0 || included.has(key)) continue;
347
+ ordered.push(id);
348
+ included.add(key);
349
+ }
350
+ const archived = new Set(rowState?.archivedSessionIds);
351
+ const pins = [];
352
+ for (const sessionId of rowState?.pinnedSessionIds ?? []) {
353
+ const id = members.get(sessionId);
354
+ if (id === void 0 || included.has(id) || archived.has(id) || summaries[id] === void 0) continue;
355
+ pins.push(id);
356
+ included.add(id);
357
+ }
358
+ const ordinary = [];
359
+ const archives = [];
360
+ for (const id of orderByRecency([...members.values()].filter((id) => !included.has(id)), summaries)) if (archived.has(id)) archives.push(id);
361
+ else ordinary.push(id);
362
+ const result = [
363
+ ...pins,
364
+ ...ordered,
365
+ ...ordinary,
366
+ ...archives
367
+ ];
368
+ const pending = new Set(ordinary);
369
+ const placeFork = (id) => {
370
+ if (!pending.delete(id)) return;
371
+ const parentId = summaries[id]?.parentId;
372
+ if (parentId === void 0 || parentId === id || !result.includes(parentId)) return;
373
+ placeFork(parentId);
374
+ result.splice(result.indexOf(id), 1);
375
+ result.splice(result.indexOf(parentId), 0, id);
376
+ };
377
+ for (const id of [...ordinary].reverse()) placeFork(id);
378
+ return result;
379
+ }
380
+ /**
381
+ * Keep the selected provisional New Session ahead of either base order.
382
+ * @param order - recency or reconciled manual order.
383
+ * @param currentBlank - selected blank Session in this account, when present.
384
+ * @returns a copy with the selected blank first and no duplicate slot.
385
+ */
386
+ function pinCurrentBlank(order, currentBlank) {
387
+ if (currentBlank === void 0) return [...order];
388
+ return [currentBlank, ...order.filter((id) => id !== currentBlank)];
389
+ }
390
+ /**
391
+ * Ordinary sessions are visible; among blank sessions, only the current one
392
+ * is visible. Subagent children use their parent header catalog; archived
393
+ * sessions follow the archived filter, while their accounting slots remain
394
+ * either way so unarchiving restores position.
395
+ */
396
+ function sessionVisible(session, current, archived, archivedFilter) {
397
+ if (session.origin === "subagent") return false;
398
+ if (session.blank && session.id !== current) return false;
399
+ switch (archivedFilter) {
400
+ case "default": return !archived.has(session.id);
401
+ case "show": return true;
402
+ case "only": return archived.has(session.id);
403
+ /* v8 ignore next 2 -- closed-union backstop; only reached if the filter is forged */
404
+ default: return assertNever$1(archivedFilter);
405
+ }
406
+ }
407
+ /**
408
+ * Keep the visible New Session placeholder first, then partition pinned and
409
+ * ordinary rows without changing either partition's caller order.
410
+ */
411
+ function sectionMembers(members, pinned, archived) {
412
+ const placeholders = [];
413
+ const leading = [];
414
+ const rest = [];
415
+ for (const member of members) if (member.blank) placeholders.push(member);
416
+ else if (!archived.has(member.id) && pinned.has(member.id)) leading.push(member);
417
+ else rest.push(member);
418
+ return [
419
+ ...placeholders,
420
+ ...leading,
421
+ ...rest
422
+ ];
423
+ }
424
+ /**
425
+ * A blank session is the selected Workspace's provisional New Session row;
426
+ * its canonical title never enters search (blank rows are query-excluded)
427
+ * and the renderer localizes its display label.
428
+ */
429
+ function sessionTitle(session) {
430
+ return session.blank ? "" : session.displayTitle;
431
+ }
432
+ /** The list projection alone owns the best-effort active-Schedule indicator. */
433
+ function hasActiveSchedule(session) {
434
+ return (session.projectionValues?.schedule?.length ?? 0) > 0;
435
+ }
436
+ /** Build one group without projecting session lineage into presentation. */
437
+ function buildGroup(key, workspaceId, cwd, createdAt, label, members) {
438
+ return {
439
+ key,
440
+ workspaceId,
441
+ cwd,
442
+ createdAt,
443
+ label,
444
+ sessions: [...members]
445
+ };
446
+ }
447
+ /** Apply a stored Ungrouped order and append newly loose Sessions by recency. */
448
+ function orderedUngrouped(members, stored, summaries) {
449
+ const byId = new Map(members.map((session) => [session.id, session]));
450
+ return (stored === void 0 ? orderByRecency(members.map((session) => session.id), summaries) : reconcileManualOrder(members.map((session) => session.id), stored, summaries)).flatMap((id) => {
451
+ const session = byId.get(id);
452
+ /* v8 ignore next -- ids are projected exclusively from the members used to build byId. */
453
+ return session === void 0 ? [] : [session];
454
+ });
455
+ }
456
+ /**
457
+ * Group Sessions by Workspace: one group per caller-ordered entity, with
458
+ * members resolved from caller-ordered sessionIds. Sessions outside every
459
+ * Workspace trail in the browser-local Ungrouped order, which falls back to
460
+ * recency before that order is initialized.
461
+ */
462
+ function groupByWorkspace(list, workspaces, archived, archivedFilter, ungroupedOrder) {
463
+ const current = mainSessionId(list);
464
+ const groups = [];
465
+ const accounted = /* @__PURE__ */ new Set();
466
+ for (const workspace of workspaces) {
467
+ const members = [];
468
+ for (const id of workspace.sessionIds) {
469
+ const summary = list.byId[id];
470
+ if (summary === void 0) continue;
471
+ accounted.add(id);
472
+ if (!sessionVisible(summary, current, archived, archivedFilter)) continue;
473
+ members.push(summary);
474
+ }
475
+ groups.push(buildGroup(workspace.workspaceId, workspace.workspaceId, workspace.path, Date.parse(workspace.createdAt), workspace.title, members));
476
+ }
477
+ const stray = list.ids.map((id) => list.byId[id]).filter((s) => s !== void 0 && !accounted.has(s.id) && sessionVisible(s, current, archived, archivedFilter));
478
+ if (stray.length > 0) groups.push(buildGroup("", void 0, void 0, void 0, "", orderedUngrouped(stray, ungroupedOrder, list.byId)));
479
+ return groups;
480
+ }
481
+ /** Keep navigation presentation independent from domain-owned interaction objects. */
482
+ function visiblePendingKind(kind) {
483
+ switch (kind) {
484
+ case "approval":
485
+ case "plan-review":
486
+ case "question": return kind;
487
+ default: return;
488
+ }
489
+ }
490
+ function runningChildCount(list, parentId, statuses) {
491
+ return list.projectionsBySession[parentId]?.values.subagentCatalog?.reduce((count, child) => count + ((statuses.get(child.id)?.running ?? list.byId[child.id]?.running) === true ? 1 : 0), 0) ?? 0;
492
+ }
493
+ function sessionNode(s, list, statuses, pinned, archived) {
494
+ const status = statuses.get(s.id);
495
+ const pendingInteraction = visiblePendingKind(status?.pendingInteraction?.kind);
496
+ return {
497
+ id: s.id,
498
+ title: sessionTitle(s),
499
+ blank: s.blank,
500
+ running: status?.running ?? s.running,
501
+ runningSubagentCount: runningChildCount(list, s.id, statuses),
502
+ completed: status?.completionUnread === true,
503
+ hasActiveSchedule: hasActiveSchedule(s),
504
+ pinned: !archived.has(s.id) && pinned.has(s.id),
505
+ archived: archived.has(s.id),
506
+ updatedAt: s.updatedAt,
507
+ ...pendingInteraction === void 0 ? {} : { pendingInteraction }
508
+ };
509
+ }
510
+ /**
511
+ * Derive the workspace browser groups with every session as a top-level row.
512
+ *
513
+ * Every group shows; sessions populate under expanded groups with pinned rows
514
+ * leading in the selected local order. Blank sessions are
515
+ * excluded except for the selected provisional New Session row; archived
516
+ * sessions keep their slots and appear per the archived filter. Content
517
+ * search lives outside this derivation (see {@link deriveSearchResults}).
518
+ * @param list - sessions list snapshot (`mainView` retention feeds containsCurrent).
519
+ * @param workspaces - real Workspaces in Host group order with caller-projected Session order.
520
+ * @param rowState - registry-global pin and archive sets plus the archived filter.
521
+ * @param statuses - unified UI status by Session.
522
+ * @param view - local expansion arrays.
523
+ * @returns group sections in render order.
524
+ */
525
+ function deriveGroups(list, workspaces, rowState, statuses, view) {
526
+ const archived = new Set(rowState.archivedSessionIds);
527
+ const pinned = new Set(rowState.pinnedSessionIds);
528
+ const expandedGroups = new Set(view.expandedGroups);
529
+ const current = mainSessionId(list);
530
+ const currentGroup = current === void 0 ? void 0 : owningGroupKey(workspaces, current);
531
+ const groups = [];
532
+ for (const g of groupByWorkspace(list, workspaces, archived, rowState.archivedFilter, view.ungroupedOrder)) {
533
+ const expanded = expandedGroups.has(g.key);
534
+ groups.push({
535
+ key: g.key,
536
+ workspaceId: g.workspaceId,
537
+ cwd: g.cwd,
538
+ createdAt: g.createdAt,
539
+ label: g.label,
540
+ sessionCount: g.sessions.length,
541
+ expanded,
542
+ containsCurrent: g.key === currentGroup,
543
+ sessions: expanded ? sectionMembers(g.sessions, pinned, archived).map((session) => sessionNode(session, list, statuses, pinned, archived)) : []
544
+ });
545
+ }
546
+ return groups;
547
+ }
548
+ /**
549
+ * Select complete flat-list membership, independently of archive visibility.
550
+ * @param list - sessions list snapshot.
551
+ * @returns known ordinary Session ids, including archives and only the current blank.
552
+ */
553
+ function sessionMemberIds(list) {
554
+ return visibleSessionIds(list, [], "show");
555
+ }
556
+ /**
557
+ * Select visible flat-list members without deriving row presentation or ordering.
558
+ * @param list - sessions list snapshot.
559
+ * @param archivedSessionIds - registry-global archive set.
560
+ * @param archivedFilter - archived-row visibility choice.
561
+ * @returns known visible Session ids in list order, including ordinary forks and only the current blank.
562
+ */
563
+ function visibleSessionIds(list, archivedSessionIds, archivedFilter) {
564
+ const archived = new Set(archivedSessionIds);
565
+ const current = mainSessionId(list);
566
+ return list.ids.filter((id) => {
567
+ const s = list.byId[id];
568
+ return s !== void 0 && sessionVisible(s, current, archived, archivedFilter);
569
+ });
570
+ }
571
+ /**
572
+ * Derive flat rows from the browser's complete ordered Session ids, with
573
+ * pinned rows fronted ahead of the supplied order.
574
+ * @param list - sessions list snapshot used to select the ids.
575
+ * @param sessionIds - complete account members in the selected order, including hidden archives.
576
+ * @param rowState - registry-global pin and archive sets plus the archived filter.
577
+ * @param statuses - unified UI status by Session.
578
+ * @returns flat rows in sectioned order with current status indicators.
579
+ */
580
+ function deriveFlat(list, sessionIds, rowState, statuses) {
581
+ const archived = new Set(rowState.archivedSessionIds);
582
+ const pinned = new Set(rowState.pinnedSessionIds);
583
+ const current = mainSessionId(list);
584
+ return sectionMembers(sessionIds.flatMap((id) => {
585
+ const session = list.byId[id];
586
+ return session !== void 0 && sessionVisible(session, current, archived, rowState.archivedFilter) ? [session] : [];
587
+ }), pinned, archived).map((session) => sessionNode(session, list, statuses, pinned, archived));
588
+ }
589
+ /**
590
+ * Merge immediate title/Workspace substring matches with ranked Host content
591
+ * matches. Local rows lead newest-first, content-only rows retain backend
592
+ * order, and duplicate sessions receive the backend snippet in place.
593
+ * @param list - session metadata authority.
594
+ * @param workspaces - Workspace membership and display labels.
595
+ * @param query - caller text; surrounding whitespace is ignored.
596
+ * @param archivedSessionIds - registry-global archive set (members match per the archived filter).
597
+ * @param archivedFilter - archived-row visibility choice; search follows it.
598
+ * @param statuses - unified UI status by Session.
599
+ * @param content - ranked Host content-search page.
600
+ * @param limit - protocol-owned maximum merged row count.
601
+ * @returns bounded deduplicated flat rows and a refine-query hint bit.
602
+ */
603
+ function deriveSearchResults(list, workspaces, query, archivedSessionIds, archivedFilter, statuses, content, limit) {
604
+ const q = query.trim().toLowerCase();
605
+ if (q === "") return {
606
+ items: [],
607
+ hasMore: false
608
+ };
609
+ const archived = new Set(archivedSessionIds);
610
+ const current = mainSessionId(list);
611
+ const workspaceBySession = /* @__PURE__ */ new Map();
612
+ for (const workspace of workspaces) for (const sessionId of workspace.sessionIds) if (!workspaceBySession.has(sessionId)) workspaceBySession.set(sessionId, workspace.title);
613
+ const labelOf = (summary) => workspaceBySession.get(summary.id) ?? workspaceLabel(summary.cwd);
614
+ const contentBySession = /* @__PURE__ */ new Map();
615
+ for (const item of content.items) if (!contentBySession.has(item.sessionId)) contentBySession.set(item.sessionId, item);
616
+ const local = [];
617
+ for (const id of list.ids) {
618
+ const summary = list.byId[id];
619
+ if (summary === void 0 || summary.blank || !sessionVisible(summary, current, archived, archivedFilter)) continue;
620
+ if (sessionTitle(summary).toLowerCase().includes(q) || labelOf(summary).toLowerCase().includes(q)) local.push(summary);
621
+ }
622
+ const localById = new Map(local.map((summary) => [summary.id, summary]));
623
+ const orderedLocal = orderByRecency(local.map((summary) => summary.id), list.byId).map((id) => localById.get(id));
624
+ const ordered = [];
625
+ const included = /* @__PURE__ */ new Set();
626
+ const include = (summary) => {
627
+ if (included.has(summary.id)) return;
628
+ included.add(summary.id);
629
+ ordered.push(summary);
630
+ };
631
+ for (const summary of orderedLocal) include(summary);
632
+ for (const item of content.items) {
633
+ const summary = list.byId[item.sessionId];
634
+ if (summary !== void 0 && !summary.blank && sessionVisible(summary, current, archived, archivedFilter)) include(summary);
635
+ }
636
+ return {
637
+ items: ordered.slice(0, limit).map((summary) => {
638
+ const match = contentBySession.get(summary.id);
639
+ const status = statuses.get(summary.id);
640
+ const pendingInteraction = visiblePendingKind(status?.pendingInteraction?.kind);
641
+ return {
642
+ id: summary.id,
643
+ title: sessionTitle(summary),
644
+ workspace: labelOf(summary),
645
+ running: status?.running ?? summary.running,
646
+ runningSubagentCount: runningChildCount(list, summary.id, statuses),
647
+ ...pendingInteraction === void 0 ? {} : { pendingInteraction },
648
+ completed: status?.completionUnread === true,
649
+ hasActiveSchedule: hasActiveSchedule(summary),
650
+ archived: archived.has(summary.id),
651
+ ...match === void 0 ? {} : { snippet: match.snippet }
652
+ };
653
+ }),
654
+ hasMore: content.hasMore || ordered.length > limit
655
+ };
656
+ }
657
+ /** Normalize separators for comparison without interpreting POSIX backslashes as separators. */
658
+ function folderPath(path) {
659
+ return (/^[A-Za-z]:[/\\]/.test(path) || path.startsWith("\\\\") ? path.replaceAll("\\", "/") : path).replace(/\/+$/, "");
660
+ }
661
+ /**
662
+ * Find the nearest registered ancestor, excluding the Workspace directory itself.
663
+ * Paths use Host spelling; matching is case-sensitive, like Workspace identity.
664
+ * @param path - Workspace directory.
665
+ * @param parents - registered Workspace directory paths.
666
+ * @returns the owning parent path, or undefined when no parent contains the Workspace.
667
+ */
668
+ function owningParentFolder(path, parents) {
669
+ const child = folderPath(path);
670
+ let owner;
671
+ let length = -1;
672
+ for (const parent of parents) {
673
+ const root = folderPath(parent);
674
+ if (root.length > length && child !== root && child.startsWith(`${root}/`)) {
675
+ owner = parent;
676
+ length = root.length;
677
+ }
678
+ }
679
+ return owner;
680
+ }
681
+ //#endregion
682
+ //#region lib/types/client/stores.js
683
+ /**
684
+ * The workspace browser's viewing store: the session-list grouping mode,
685
+ * persisted across reloads. Module level exports the factory only (a
686
+ * module-level handle would pin the store identity across plugin reloads);
687
+ * register() receives the factory and the browser derives its PropsStore
688
+ * share from the return type.
689
+ */
690
+ /** Browser-local order account for the hierarchy-free flat Session list. */
691
+ const FLAT_SESSION_ORDER_KEY = "__flat_session_order__";
692
+ /** Copy read-only projections into the persisted mutable store representation. */
693
+ function copySessionOrders(orders) {
694
+ return Object.fromEntries(Object.entries(orders).map(([key, order]) => [key, [...order]]));
695
+ }
696
+ /**
697
+ * Create the workspace browser viewing store handle.
698
+ * @returns the store handle (spec + type + identity + factory in one).
699
+ */
700
+ function createWorkspaceViewStore() {
701
+ return (0, _deepseek_ai_dsh_client_store.defineStore)({
702
+ init: () => ({
703
+ groupBy: "workspace",
704
+ orderBy: "updated",
705
+ groupExpansion: {},
706
+ sessionOrderByAccount: {},
707
+ archivedFilter: "default"
708
+ }),
709
+ persist: "dsh.workspace.view.v5",
710
+ actions: {
711
+ setGroupBy: (d, mode) => {
712
+ d.groupBy = mode;
713
+ },
714
+ setOrderBy: (d, mode, initialOrders) => {
715
+ if (mode === d.orderBy) return;
716
+ d.sessionOrderByAccount = mode === "manual" ? copySessionOrders(initialOrders) : {};
717
+ d.orderBy = mode;
718
+ },
719
+ setGroupExpanded: (d, key, expanded) => {
720
+ d.groupExpansion[key] = expanded;
721
+ },
722
+ retainAccountKeys: (d, workspaceKeys) => {
723
+ const retained = new Set(workspaceKeys);
724
+ d.groupExpansion = Object.fromEntries(Object.entries(d.groupExpansion).filter(([key]) => retained.has(key)));
725
+ d.sessionOrderByAccount = Object.fromEntries(Object.entries(d.sessionOrderByAccount).filter(([key]) => retained.has(key)));
726
+ delete d.sessionUpdatedAtByAccount;
727
+ },
728
+ syncSessionOrders: (d, orders) => {
729
+ if (d.orderBy !== "manual") return;
730
+ Object.assign(d.sessionOrderByAccount, copySessionOrders(orders));
731
+ },
732
+ setSessionOrder: (d, accountKey, order, initialOrders) => {
733
+ if (d.orderBy === "updated") d.sessionOrderByAccount = copySessionOrders(initialOrders);
734
+ else Object.assign(d.sessionOrderByAccount, copySessionOrders(initialOrders));
735
+ d.orderBy = "manual";
736
+ d.sessionOrderByAccount[accountKey] = [...order];
737
+ },
738
+ pinSessionOrder: (d, sessionId, accountKeys, source) => {
739
+ const selected = new Set(accountKeys);
740
+ d.sessionOrderByAccount = Object.fromEntries(Object.entries(source.members).map(([key, members]) => {
741
+ const order = reconcileManualOrder(members, d.sessionOrderByAccount[key], source.summaries, source.rowState);
742
+ return [key, selected.has(key) ? [sessionId, ...order.filter((id) => id !== sessionId)] : order];
743
+ }));
744
+ },
745
+ setArchivedFilter: (d, filter) => {
746
+ d.archivedFilter = filter;
747
+ }
748
+ }
749
+ });
750
+ }
751
+ //#endregion
752
+ //#region lib/types/client/pin-order.js
753
+ /**
754
+ * Every account's complete membership: each Workspace, Ungrouped, and the flat list.
755
+ * @param workspaces - current Host Workspaces.
756
+ * @param list - current Session list snapshot.
757
+ * @param rowState - registry-global pin and archive sets.
758
+ * @returns the order source for one pin write.
759
+ */
760
+ function pinOrderSource(workspaces, list, rowState) {
761
+ const accounted = new Set(workspaces.flatMap((workspace) => workspace.sessionIds));
762
+ return {
763
+ members: Object.fromEntries([
764
+ ...workspaces.map((workspace) => [workspace.workspaceId, workspace.sessionIds]),
765
+ ["", list.ids.filter((id) => list.byId[id] !== void 0 && !accounted.has(id))],
766
+ [FLAT_SESSION_ORDER_KEY, sessionMemberIds(list)]
767
+ ]),
768
+ summaries: list.byId,
769
+ rowState
770
+ };
771
+ }
772
+ /**
773
+ * The accounts a pinned Session leads: its group (or Ungrouped) and the flat list.
774
+ * @param workspaces - current Host Workspaces.
775
+ * @param sessionId - the Session being pinned.
776
+ * @returns the account keys `pinSessionOrder` fronts.
777
+ */
778
+ function pinOrderAccounts(workspaces, sessionId) {
779
+ return [owningGroupKey(workspaces, sessionId), FLAT_SESSION_ORDER_KEY];
780
+ }
781
+ //#endregion
12
782
  //#region lib/types/client/navigation.js
13
783
  /** Workspace archive and directory UI capability. */
14
784
  /** Structured directory failure exposed to directory UI consumers. */
@@ -26,6 +796,8 @@ window.__ModuleLoader__.load({
26
796
  directoryPicker;
27
797
  workspaces;
28
798
  sessions;
799
+ view;
800
+ notify;
29
801
  connecting = /* @__PURE__ */ new Map();
30
802
  lifetime = new AbortController();
31
803
  selection = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)({}, { persist: { name: "dsh.sessions.current" } });
@@ -35,12 +807,16 @@ window.__ModuleLoader__.load({
35
807
  * @param directoryPicker - the directory-picking Remote namespace.
36
808
  * @param workspaces - pure Workspace Controller.
37
809
  * @param sessions - pure Session Controller.
810
+ * @param view - the browser's viewing-store write set (one instance shared with its registration).
811
+ * @param notify - show one notice through the Workspace notice channel.
38
812
  */
39
- constructor(ctx, directoryPicker, workspaces, sessions) {
813
+ constructor(ctx, directoryPicker, workspaces, sessions, view, notify) {
40
814
  super(ctx, "uiWorkspace");
41
815
  this.directoryPicker = directoryPicker;
42
816
  this.workspaces = workspaces;
43
817
  this.sessions = sessions;
818
+ this.view = view;
819
+ this.notify = notify;
44
820
  ctx.effect(() => {
45
821
  const stop = this.watchNavigation();
46
822
  return () => {
@@ -57,34 +833,56 @@ window.__ModuleLoader__.load({
57
833
  if (workspace === void 0) throw new Error(`uiWorkspace.connectWorkspace: unknown workspace ${workspaceId}`);
58
834
  const inflight = this.connecting.get(workspaceId);
59
835
  if (inflight !== void 0) return inflight;
836
+ const attempt = this.reuseOrCreateBlank(workspace).finally(() => {
837
+ this.connecting.delete(workspaceId);
838
+ });
839
+ this.connecting.set(workspaceId, attempt);
840
+ return attempt;
841
+ }
842
+ reuseOrCreateBlank(workspace) {
60
843
  const archived = this.workspaces.list.getSnapshot().archivedSessionIds;
61
844
  const sessions = this.sessions.list.getSnapshot();
62
845
  for (const id of sessions.ids) {
63
846
  const summary = sessions.byId[id];
64
- if (summary !== void 0 && summary.blank && summary.cwd === workspace.path && workspace.sessionIds.includes(summary.id) && !archived.includes(summary.id)) return summary.id;
847
+ if (summary === void 0 || !summary.blank || summary.cwd !== workspace.path || !workspace.sessionIds.includes(id) || archived.includes(id)) continue;
848
+ return this.reuseBlank(workspace.workspaceId, id);
849
+ }
850
+ return this.sessions.create({ workspaceId: workspace.workspaceId });
851
+ }
852
+ async reuseBlank(workspaceId, sessionId) {
853
+ try {
854
+ return await this.sessions.create({
855
+ workspaceId,
856
+ sessionId
857
+ });
858
+ } catch (error) {
859
+ if (sessionCreateErrorOf(error)?.rpcError.code !== "session/writer-held") throw error;
860
+ return this.sessions.create({ workspaceId });
65
861
  }
66
- const attempt = this.sessions.create({ workspaceId }).finally(() => {
67
- this.connecting.delete(workspaceId);
68
- });
69
- this.connecting.set(workspaceId, attempt);
70
- return attempt;
71
862
  }
72
863
  openSession(target) {
73
- this.replaceMain(target, this.lifetime.signal);
864
+ this.replaceMain(target, this.lifetime.signal, "reveal");
74
865
  }
75
866
  async openWorkspace(workspaceId, beforeOpen) {
76
867
  const navigation = AbortSignal.any([this.ctx.layout.beginNavigation(), this.lifetime.signal]);
77
- const sessionId = await this.connectWorkspace(workspaceId);
868
+ let sessionId;
869
+ try {
870
+ sessionId = await this.connectWorkspace(workspaceId);
871
+ } catch (error) {
872
+ if (!navigation.aborted) this.notify({
873
+ kind: "createFailed",
874
+ message: creationFailureMessage(error)
875
+ });
876
+ throw error;
877
+ }
78
878
  if (navigation.aborted) return;
79
- this.replaceMain(sessionId, navigation, beforeOpen);
879
+ this.replaceMain(sessionId, navigation, "reveal", beforeOpen);
80
880
  }
81
881
  async forkSession(sessionId) {
82
- const navigation = AbortSignal.any([this.ctx.layout.beginNavigation(), this.lifetime.signal]);
83
- const childId = await this.sessions.fork({
882
+ await this.sessions.fork({
84
883
  sessionId,
85
884
  increaseTitle: true
86
885
  });
87
- if (!navigation.aborted) this.replaceMain(childId, navigation);
88
886
  }
89
887
  startSession(workspaceId) {
90
888
  const workspace = this.workspaces.list.getSnapshot();
@@ -101,13 +899,24 @@ window.__ModuleLoader__.load({
101
899
  console.warn("new session failed:", reason);
102
900
  });
103
901
  }
104
- async archiveSession(sessionId) {
105
- await this.workspaces.archiveSession(sessionId);
902
+ async archiveSession(sessionId, options = {}) {
903
+ await this.workspaces.archiveSession(sessionId, options);
106
904
  if (this.mainReference?.sessionId === sessionId) this.clearMain();
107
905
  }
108
906
  async unarchiveSession(sessionId) {
109
907
  await this.workspaces.unarchiveSession(sessionId);
110
908
  }
909
+ async pinSession(sessionId) {
910
+ await this.workspaces.pinSession(sessionId);
911
+ const { items, pinnedSessionIds, archivedSessionIds } = this.workspaces.list.getSnapshot();
912
+ this.view.pinSessionOrder(sessionId, pinOrderAccounts(items, sessionId), pinOrderSource(items, this.sessions.list.getSnapshot(), {
913
+ pinnedSessionIds,
914
+ archivedSessionIds
915
+ }));
916
+ }
917
+ async unpinSession(sessionId) {
918
+ await this.workspaces.unpinSession(sessionId);
919
+ }
111
920
  async pickDirectory() {
112
921
  const result = await this.directoryPicker.pick();
113
922
  if (!result.ok) throw new Error(`directory picker failed: ${result.error.message}`);
@@ -136,34 +945,13 @@ window.__ModuleLoader__.load({
136
945
  initial = "done";
137
946
  return;
138
947
  }
139
- const saved = this.selection.getSnapshot();
140
- const savedTarget = saved.subagentAddress ?? (saved.sessionId !== void 0 && sessions.byId[saved.sessionId] !== void 0 ? saved.sessionId : void 0);
141
- if (savedTarget !== void 0) {
142
- initial = "connecting";
143
- try {
144
- if (saved.subagentAddress !== void 0) this.sessions.refreshSubagents(saved.subagentAddress.parentSessionId);
145
- this.openSession(savedTarget);
146
- initial = "done";
147
- } catch (reason) {
148
- initial = "waiting";
149
- console.warn("initial Session restoration failed:", reason);
150
- }
151
- return;
152
- }
153
- const target = recentWorkspace(workspace.items, sessions.byId);
154
- if (target === void 0) {
155
- initial = "done";
156
- return;
157
- }
158
948
  initial = "connecting";
159
- this.connectWorkspace(target).then((sessionId) => {
160
- if (this.mainReference === void 0) this.openSession(sessionId);
161
- }).then(() => {
949
+ this.restoreSelection(workspace, sessions).then(() => {
162
950
  initial = "done";
163
951
  }, (reason) => {
164
952
  if (this.lifetime.signal.aborted) return;
165
953
  initial = "waiting";
166
- console.warn("initial workspace selection failed:", reason);
954
+ console.warn("initial Session restoration failed:", reason);
167
955
  });
168
956
  };
169
957
  const disposeWorkspaces = this.workspaces.list.subscribe(reconcile);
@@ -171,534 +959,149 @@ window.__ModuleLoader__.load({
171
959
  reconcile();
172
960
  return () => {
173
961
  this.lifetime.abort();
174
- disposeSessions();
175
- disposeWorkspaces();
176
- };
177
- }
178
- /** @returns true when an archived current selection was cleared. */
179
- clearArchivedCurrent() {
180
- const current = this.mainReference?.sessionId;
181
- if (current === void 0 || !this.workspaces.list.getSnapshot().archivedSessionIds.includes(current)) return false;
182
- this.clearMain();
183
- return true;
184
- }
185
- clearMain() {
186
- const previous = this.mainReference;
187
- this.mainReference = void 0;
188
- this.selection.set({});
189
- previous?.release();
190
- this.ctx.layout.selectPanel(null);
191
- }
192
- replaceMain(target, signal, beforeOpen) {
193
- signal.throwIfAborted();
194
- const reference = this.sessions.retain(target, { source: "mainView" });
195
- try {
196
- signal.throwIfAborted();
197
- beforeOpen?.(reference.sessionId);
198
- if (signal.aborted) {
199
- reference.release();
200
- return;
201
- }
202
- const subagentAddress = typeof target === "string" ? this.sessions.subagentAddress(reference.sessionId) : target;
203
- this.selection.set({
204
- sessionId: reference.sessionId,
205
- ...subagentAddress === void 0 ? {} : { subagentAddress }
206
- });
207
- } catch (error) {
208
- reference.release();
209
- throw error;
210
- }
211
- const previous = this.mainReference;
212
- this.mainReference = reference;
213
- previous?.release();
214
- this.sessions.refreshSubagents(reference.sessionId);
215
- this.ctx.layout.selectPanel(null);
216
- }
217
- };
218
- /** Stable tie-breaking follows Host Workspace order. */
219
- function recentWorkspace(workspaces, sessions) {
220
- let selected;
221
- let selectedTime = Number.NEGATIVE_INFINITY;
222
- for (const workspace of workspaces) {
223
- let latest = Number.NEGATIVE_INFINITY;
224
- for (const sessionId of workspace.sessionIds) {
225
- const session = sessions[sessionId];
226
- if (session !== void 0) latest = Math.max(latest, session.updatedAt);
227
- }
228
- if (latest === Number.NEGATIVE_INFINITY) latest = Date.parse(workspace.createdAt);
229
- if (selected === void 0 || latest > selectedTime) {
230
- selected = workspace.workspaceId;
231
- selectedTime = latest;
232
- }
233
- }
234
- return selected;
235
- }
236
- //#endregion
237
- //#region lib/types/client/stores.js
238
- /**
239
- * The workspace browser's viewing store: the session-list grouping mode,
240
- * persisted across reloads. Module level exports the factory only (a
241
- * module-level handle would pin the store identity across plugin reloads);
242
- * register() receives the factory and the browser derives its PropsStore
243
- * share from the return type.
244
- */
245
- /** Browser-local order account for the hierarchy-free flat Session list. */
246
- const FLAT_SESSION_ORDER_KEY = "__flat_session_order__";
247
- /** Copy read-only projections into the persisted mutable store representation. */
248
- function copySessionOrders(orders) {
249
- return Object.fromEntries(Object.entries(orders).map(([key, order]) => [key, [...order]]));
250
- }
251
- /**
252
- * Create the workspace browser viewing store handle.
253
- * @returns the store handle (spec + type + identity + factory in one).
254
- */
255
- function createWorkspaceViewStore() {
256
- return (0, _deepseek_ai_dsh_client_store.defineStore)({
257
- init: () => ({
258
- groupBy: "workspace",
259
- orderBy: "updated",
260
- groupExpansion: {},
261
- sessionOrderByAccount: {}
262
- }),
263
- persist: "dsh.workspace.view.v5",
264
- actions: {
265
- setGroupBy: (d, mode) => {
266
- d.groupBy = mode;
267
- },
268
- setOrderBy: (d, mode, initialOrders) => {
269
- if (mode === d.orderBy) return;
270
- d.sessionOrderByAccount = mode === "manual" ? copySessionOrders(initialOrders) : {};
271
- d.orderBy = mode;
272
- },
273
- setGroupExpanded: (d, key, expanded) => {
274
- d.groupExpansion[key] = expanded;
275
- },
276
- retainAccountKeys: (d, workspaceKeys) => {
277
- const retained = new Set(workspaceKeys);
278
- d.groupExpansion = Object.fromEntries(Object.entries(d.groupExpansion).filter(([key]) => retained.has(key)));
279
- d.sessionOrderByAccount = Object.fromEntries(Object.entries(d.sessionOrderByAccount).filter(([key]) => retained.has(key)));
280
- delete d.sessionUpdatedAtByAccount;
281
- },
282
- syncSessionOrders: (d, orders) => {
283
- if (d.orderBy !== "manual") return;
284
- Object.assign(d.sessionOrderByAccount, copySessionOrders(orders));
285
- },
286
- setSessionOrder: (d, accountKey, order, initialOrders) => {
287
- if (d.orderBy === "updated") d.sessionOrderByAccount = copySessionOrders(initialOrders);
288
- d.orderBy = "manual";
289
- d.sessionOrderByAccount[accountKey] = [...order];
290
- }
291
- }
292
- });
293
- }
294
- //#endregion
295
- //#region ../../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
296
- function r(e) {
297
- var t, f, n = "";
298
- if ("string" == typeof e || "number" == typeof e) n += e;
299
- else if ("object" == typeof e) if (Array.isArray(e)) {
300
- var o = e.length;
301
- for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
302
- } else for (f in e) e[f] && (n && (n += " "), n += f);
303
- return n;
304
- }
305
- function clsx() {
306
- for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
307
- return n;
308
- }
309
- //#endregion
310
- //#region ../../util/workspace-path/src/index.ts
311
- /** Whether a path uses a Windows drive or UNC prefix. */
312
- function isWindowsStylePath(value) {
313
- return /^[A-Za-z]:[/\\]/.test(value) || value.startsWith("\\\\");
314
- }
315
- /**
316
- * Abbreviate a POSIX home directory for display.
317
- * @param path - Absolute or already-short display path.
318
- * @param home - Host account home; absent skips abbreviation.
319
- * @returns `~` or `~/…` for the POSIX home and its descendants, otherwise `path`.
320
- */
321
- function abbreviateHomePath(path, home) {
322
- if (home === void 0 || home === "") return path;
323
- if (isWindowsStylePath(path) || isWindowsStylePath(home)) return path;
324
- const root = home.replace(/\/+$/, "");
325
- if (root === "" || root === "/") return path;
326
- if (path.replace(/\/+$/, "") === root) return "~";
327
- if (path.startsWith(`${root}/`)) return `~${path.slice(root.length)}`;
328
- return path;
329
- }
330
- /**
331
- * Read the final non-empty segment of a Workspace path for display.
332
- * Workspace-label surfaces use this helper instead of deriving another basename.
333
- * @param path - Workspace directory path using POSIX or Windows separators.
334
- * @returns the final segment, or an empty string for a separator-only path.
335
- */
336
- function workspaceTitleOf(path) {
337
- const trimmed = path.replace(/[/\\]+$/, "");
338
- const separator = Math.max(trimmed.lastIndexOf("/"), trimmed.lastIndexOf("\\"));
339
- return trimmed.slice(separator + 1);
340
- }
341
- //#endregion
342
- //#region lib/types/client/subagent-lineage.js
343
- /** UI Workspace-owned projection of descendant counts from Session summaries. */
344
- /**
345
- * Index uninterrupted subagent descendants under each ancestor.
346
- * @param summaries - Session summaries keyed by id.
347
- * @returns descendant totals keyed by possible parent id.
348
- */
349
- function indexSubagentDescendants(summaries) {
350
- const indexed = /* @__PURE__ */ new Map();
351
- for (const descendant of Object.values(summaries)) {
352
- if (descendant.origin !== "subagent") continue;
353
- const seen = /* @__PURE__ */ new Set();
354
- let current = descendant;
355
- while (current?.origin === "subagent" && current.parentId !== void 0 && !seen.has(current.id)) {
356
- seen.add(current.id);
357
- const aggregate = indexed.get(current.parentId);
358
- if (aggregate === void 0) indexed.set(current.parentId, {
359
- count: 1,
360
- runningCount: descendant.running ? 1 : 0
361
- });
362
- else {
363
- aggregate.count += 1;
364
- if (descendant.running) aggregate.runningCount += 1;
365
- }
366
- current = summaries[current.parentId];
367
- }
368
- }
369
- return indexed;
370
- }
371
- /**
372
- * Resolve the Workspace browser group that owns one Session.
373
- * @param workspaces - authoritative Workspace membership.
374
- * @param sessionId - Session whose browser group is required.
375
- * @returns owning Workspace id, or {@link UNGROUPED_KEY} when no Workspace accounts for it.
376
- */
377
- function owningGroupKey(workspaces, sessionId) {
378
- return workspaces.find((workspace) => workspace.sessionIds.includes(sessionId))?.workspaceId ?? "";
379
- }
380
- function mainSessionId(list) {
381
- return Object.values(list.byId).find((session) => (session.retainedBy.mainView ?? 0) > 0)?.id;
382
- }
383
- /**
384
- * Directory display label: basename of the path (both separators accepted).
385
- * Ungrouped-bucket fallback for surfaces without a workspace title.
386
- * @param cwd - directory path, or undefined for the ungrouped bucket.
387
- * @returns basename, the raw cwd when it has no basename, or an empty ungrouped marker.
388
- */
389
- function workspaceLabel(cwd) {
390
- if (cwd === void 0 || cwd === "") return "";
391
- const base = workspaceTitleOf(cwd);
392
- return base !== "" ? base : cwd;
393
- }
394
- /**
395
- * Project known account members by current Session recency.
396
- * @param sessionIds - authoritative account membership.
397
- * @param summaries - current Session summaries; members without a summary are omitted until it arrives.
398
- * @returns known members newest first, with Session identity as the deterministic tie-break.
399
- */
400
- function orderByRecency(sessionIds, summaries) {
401
- return sessionIds.flatMap((id) => {
402
- const summary = summaries[id];
403
- return summary === void 0 ? [] : [{
404
- id,
405
- updatedAt: summary.updatedAt
406
- }];
407
- }).sort((a, b) => {
408
- if (a.updatedAt !== b.updatedAt) return b.updatedAt - a.updatedAt;
409
- return a.id < b.id ? -1 : 1;
410
- }).map((member) => member.id);
411
- }
412
- /**
413
- * Reconcile a browser-local manual order with current account membership.
414
- * @param memberIds - authoritative account membership.
415
- * @param savedOrder - previously saved browser-local order.
416
- * @param summaries - current Session summaries used to append newly known members by recency.
417
- * @returns retained saved slots followed by newly known members; departed members and unknown new members are omitted.
418
- */
419
- function reconcileManualOrder(memberIds, savedOrder, summaries) {
420
- const members = new Map(memberIds.map((id) => [id, id]));
421
- const included = /* @__PURE__ */ new Set();
422
- const ordered = [];
423
- for (const key of savedOrder ?? []) {
424
- const id = members.get(key);
425
- if (id === void 0 || included.has(key)) continue;
426
- ordered.push(id);
427
- included.add(key);
428
- }
429
- for (const id of orderByRecency(memberIds, summaries)) {
430
- if (included.has(id)) continue;
431
- ordered.push(id);
432
- included.add(id);
962
+ disposeSessions();
963
+ disposeWorkspaces();
964
+ };
433
965
  }
434
- return ordered;
435
- }
436
- /**
437
- * Keep the selected provisional New Session ahead of either base order.
438
- * @param order - recency or reconciled manual order.
439
- * @param currentBlank - selected blank Session in this account, when present.
440
- * @returns a copy with the selected blank first and no duplicate slot.
441
- */
442
- function pinCurrentBlank(order, currentBlank) {
443
- if (currentBlank === void 0) return [...order];
444
- return [currentBlank, ...order.filter((id) => id !== currentBlank)];
445
- }
446
- /**
447
- * Ordinary sessions are visible; among blank sessions, only the current one
448
- * is visible. Subagent children use their parent header catalog; archived
449
- * sessions are visible nowhere, while their accounting slots remain so
450
- * unarchiving restores position.
451
- */
452
- function sessionVisible(session, current, archived) {
453
- return session.origin !== "subagent" && !archived.has(session.id) && (!session.blank || session.id === current);
454
- }
455
- /**
456
- * A blank session is the selected Workspace's provisional New Session row;
457
- * its canonical title never enters search (blank rows are query-excluded)
458
- * and the renderer localizes its display label.
459
- */
460
- function sessionTitle(session) {
461
- return session.blank ? "" : session.displayTitle;
462
- }
463
- /** The list projection alone owns the best-effort active-Schedule indicator. */
464
- function hasActiveSchedule(session) {
465
- return (session.projectionValues?.schedule?.length ?? 0) > 0;
466
- }
467
- /** Build one group without projecting session lineage into presentation. */
468
- function buildGroup(key, workspaceId, cwd, createdAt, label, members) {
469
- return {
470
- key,
471
- workspaceId,
472
- cwd,
473
- createdAt,
474
- label,
475
- sessions: [...members]
476
- };
477
- }
478
- /** Apply a stored Ungrouped order and append newly loose Sessions by recency. */
479
- function orderedUngrouped(members, stored, summaries) {
480
- const byId = new Map(members.map((session) => [session.id, session]));
481
- return (stored === void 0 ? orderByRecency(members.map((session) => session.id), summaries) : reconcileManualOrder(members.map((session) => session.id), stored, summaries)).flatMap((id) => {
482
- const session = byId.get(id);
483
- /* v8 ignore next -- ids are projected exclusively from the members used to build byId. */
484
- return session === void 0 ? [] : [session];
485
- });
486
- }
487
- /**
488
- * Group Sessions by Workspace: one group per caller-ordered entity, with
489
- * members resolved from caller-ordered sessionIds. Sessions outside every
490
- * Workspace trail in the browser-local Ungrouped order, which falls back to
491
- * recency before that order is initialized.
492
- */
493
- function groupByWorkspace(list, workspaces, archived, ungroupedOrder) {
494
- const current = mainSessionId(list);
495
- const groups = [];
496
- const accounted = /* @__PURE__ */ new Set();
497
- for (const workspace of workspaces) {
498
- const members = [];
499
- for (const id of workspace.sessionIds) {
500
- const summary = list.byId[id];
501
- if (summary === void 0) continue;
502
- accounted.add(id);
503
- if (!sessionVisible(summary, current, archived)) continue;
504
- members.push(summary);
966
+ async restoreSelection(workspaces, sessions) {
967
+ const saved = this.selection.getSnapshot();
968
+ if (saved.subagentAddress !== void 0) {
969
+ this.replaceMain(saved.subagentAddress, this.lifetime.signal, "preserve");
970
+ return;
505
971
  }
506
- groups.push(buildGroup(workspace.workspaceId, workspace.workspaceId, workspace.path, Date.parse(workspace.createdAt), workspace.title, members));
972
+ const summary = saved.sessionId === void 0 ? void 0 : sessions.byId[saved.sessionId];
973
+ const workspace = summary === void 0 ? void 0 : workspaces.items.find((item) => item.sessionIds.includes(summary.id));
974
+ if (summary !== void 0 && (!summary.blank || workspace === void 0)) {
975
+ this.replaceMain(summary.id, this.lifetime.signal, "preserve");
976
+ return;
977
+ }
978
+ const navigation = AbortSignal.any([this.ctx.layout.beginNavigation(), this.lifetime.signal]);
979
+ let sessionId;
980
+ if (summary !== void 0 && workspace !== void 0 && summary.cwd === workspace.path && !workspaces.archivedSessionIds.includes(summary.id)) sessionId = await this.reuseBlank(workspace.workspaceId, summary.id);
981
+ let target = workspace?.workspaceId ?? recentWorkspace(workspaces.items, sessions.byId);
982
+ if (target === void 0 && workspaces.items.length === 0 && sessions.ids.length === 0) {
983
+ const prepared = await this.initializeDefaultWorkspace(navigation);
984
+ if (navigation.aborted) return;
985
+ target = prepared?.workspaceId;
986
+ }
987
+ if (sessionId === void 0 && target !== void 0) sessionId = await this.connectWorkspace(target);
988
+ if (sessionId !== void 0 && !navigation.aborted) this.replaceMain(sessionId, navigation, "preserve");
507
989
  }
508
- const stray = list.ids.map((id) => list.byId[id]).filter((s) => s !== void 0 && !accounted.has(s.id) && sessionVisible(s, current, archived));
509
- if (stray.length > 0) groups.push(buildGroup("", void 0, void 0, void 0, "", orderedUngrouped(stray, ungroupedOrder, list.byId)));
510
- return groups;
511
- }
512
- /** Keep navigation presentation independent from domain-owned interaction objects. */
513
- function visiblePendingKind(kind) {
514
- switch (kind) {
515
- case "approval":
516
- case "plan-review":
517
- case "question": return kind;
518
- default: return;
990
+ async initializeDefaultWorkspace(signal) {
991
+ const language = this.ctx.locale.getSnapshot().active.toLowerCase().split("-")[0];
992
+ const title = (language === "zh" ? zh : en)["defaultWorkspace.title"];
993
+ try {
994
+ return await this.workspaces.initializeDefault({
995
+ directoryName: language === "zh" || language === "en" ? title : "default-workspace",
996
+ title
997
+ }, signal);
998
+ } catch (_error) {
999
+ if (!signal.aborted) this.notify({ kind: "defaultWorkspaceFailed" });
1000
+ return;
1001
+ }
519
1002
  }
520
- }
521
- function sessionNode(s, descendants, statuses) {
522
- const status = statuses.get(s.id);
523
- const pendingInteraction = visiblePendingKind(status?.pendingInteraction?.kind);
524
- return {
525
- id: s.id,
526
- title: sessionTitle(s),
527
- blank: s.blank,
528
- running: status?.running ?? s.running,
529
- runningSubagentCount: descendants.get(s.id)?.runningCount ?? 0,
530
- completed: status?.completionUnread === true,
531
- hasActiveSchedule: hasActiveSchedule(s),
532
- updatedAt: s.updatedAt,
533
- ...pendingInteraction === void 0 ? {} : { pendingInteraction }
534
- };
535
- }
536
- /**
537
- * Derive the workspace browser groups with every session as a top-level row.
538
- *
539
- * Every group shows; sessions populate under expanded groups in the selected
540
- * local order. Blank sessions are excluded except for the selected
541
- * provisional New Session row; archived sessions are excluded everywhere.
542
- * Content search lives outside this derivation
543
- * (see {@link deriveSearchResults}).
544
- * @param list - sessions list snapshot (`mainView` retention feeds containsCurrent).
545
- * @param workspaces - real Workspaces in Host group order with caller-projected Session order.
546
- * @param archivedSessionIds - registry-global archive set.
547
- * @param statuses - unified UI status by Session.
548
- * @param view - local expansion arrays.
549
- * @returns group sections in render order.
550
- */
551
- function deriveGroups(list, workspaces, archivedSessionIds, statuses, view) {
552
- const archived = new Set(archivedSessionIds);
553
- const expandedGroups = new Set(view.expandedGroups);
554
- const descendants = indexSubagentDescendants(list.byId);
555
- const current = mainSessionId(list);
556
- const currentGroup = current === void 0 ? void 0 : owningGroupKey(workspaces, current);
557
- const groups = [];
558
- for (const g of groupByWorkspace(list, workspaces, archived, view.ungroupedOrder)) {
559
- const expanded = expandedGroups.has(g.key);
560
- groups.push({
561
- key: g.key,
562
- workspaceId: g.workspaceId,
563
- cwd: g.cwd,
564
- createdAt: g.createdAt,
565
- label: g.label,
566
- sessionCount: g.sessions.length,
567
- expanded,
568
- containsCurrent: g.key === currentGroup,
569
- sessions: expanded ? g.sessions.map((session) => sessionNode(session, descendants, statuses)) : []
570
- });
1003
+ /** @returns true when an archived current selection was cleared. */
1004
+ clearArchivedCurrent() {
1005
+ const current = this.mainReference?.sessionId;
1006
+ if (current === void 0 || !this.workspaces.list.getSnapshot().archivedSessionIds.includes(current)) return false;
1007
+ this.clearMain();
1008
+ return true;
571
1009
  }
572
- return groups;
573
- }
574
- /**
575
- * Select flat-list members without deriving row presentation or ordering.
576
- * @param list - sessions list snapshot.
577
- * @param archivedSessionIds - registry-global archive set.
578
- * @returns known visible Session ids in list order, including ordinary forks and only the current blank.
579
- */
580
- function visibleSessionIds(list, archivedSessionIds) {
581
- const archived = new Set(archivedSessionIds);
582
- const current = mainSessionId(list);
583
- return list.ids.filter((id) => {
584
- const s = list.byId[id];
585
- return s !== void 0 && sessionVisible(s, current, archived);
586
- });
587
- }
588
- /**
589
- * Derive flat rows from the browser's ordered visible Session ids.
590
- * @param list - sessions list snapshot used to select the ids.
591
- * @param sessionIds - known visible members in render order, including any pinned blank.
592
- * @param statuses - unified UI status by Session.
593
- * @returns flat rows in the supplied order with current status indicators.
594
- */
595
- function deriveFlat(list, sessionIds, statuses) {
596
- const descendants = indexSubagentDescendants(list.byId);
597
- return sessionIds.map((id) => sessionNode(list.byId[id], descendants, statuses));
598
- }
599
- /**
600
- * Merge immediate title/Workspace substring matches with ranked Host content
601
- * matches. Local rows lead newest-first, content-only rows retain backend
602
- * order, and duplicate sessions receive the backend snippet in place.
603
- * @param list - session metadata authority.
604
- * @param workspaces - Workspace membership and display labels.
605
- * @param query - caller text; surrounding whitespace is ignored.
606
- * @param archivedSessionIds - registry-global archive set (members never match).
607
- * @param statuses - unified UI status by Session.
608
- * @param content - ranked Host content-search page.
609
- * @param limit - protocol-owned maximum merged row count.
610
- * @returns bounded deduplicated flat rows and a refine-query hint bit.
611
- */
612
- function deriveSearchResults(list, workspaces, query, archivedSessionIds, statuses, content, limit) {
613
- const q = query.trim().toLowerCase();
614
- if (q === "") return {
615
- items: [],
616
- hasMore: false
617
- };
618
- const archived = new Set(archivedSessionIds);
619
- const descendants = indexSubagentDescendants(list.byId);
620
- const current = mainSessionId(list);
621
- const workspaceBySession = /* @__PURE__ */ new Map();
622
- for (const workspace of workspaces) for (const sessionId of workspace.sessionIds) if (!workspaceBySession.has(sessionId)) workspaceBySession.set(sessionId, workspace.title);
623
- const labelOf = (summary) => workspaceBySession.get(summary.id) ?? workspaceLabel(summary.cwd);
624
- const contentBySession = /* @__PURE__ */ new Map();
625
- for (const item of content.items) if (!contentBySession.has(item.sessionId)) contentBySession.set(item.sessionId, item);
626
- const local = [];
627
- for (const id of list.ids) {
628
- const summary = list.byId[id];
629
- if (summary === void 0 || summary.blank || !sessionVisible(summary, current, archived)) continue;
630
- if (sessionTitle(summary).toLowerCase().includes(q) || labelOf(summary).toLowerCase().includes(q)) local.push(summary);
1010
+ clearMain() {
1011
+ const previous = this.mainReference;
1012
+ this.mainReference = void 0;
1013
+ this.selection.set({});
1014
+ previous?.release();
1015
+ this.ctx.layout.selectPanel(null);
631
1016
  }
632
- const localById = new Map(local.map((summary) => [summary.id, summary]));
633
- const orderedLocal = orderByRecency(local.map((summary) => summary.id), list.byId).map((id) => localById.get(id));
634
- const ordered = [];
635
- const included = /* @__PURE__ */ new Set();
636
- const include = (summary) => {
637
- if (included.has(summary.id)) return;
638
- included.add(summary.id);
639
- ordered.push(summary);
640
- };
641
- for (const summary of orderedLocal) include(summary);
642
- for (const item of content.items) {
643
- const summary = list.byId[item.sessionId];
644
- if (summary !== void 0 && !summary.blank && sessionVisible(summary, current, archived)) include(summary);
1017
+ replaceMain(target, signal, panel, beforeOpen) {
1018
+ signal.throwIfAborted();
1019
+ const reference = this.sessions.retain(target, { source: "mainView" });
1020
+ try {
1021
+ signal.throwIfAborted();
1022
+ beforeOpen?.(reference.sessionId);
1023
+ if (signal.aborted) {
1024
+ reference.release();
1025
+ return;
1026
+ }
1027
+ const subagentAddress = typeof target === "string" ? this.sessions.subagentAddress(reference.sessionId) : target;
1028
+ this.selection.set({
1029
+ sessionId: reference.sessionId,
1030
+ ...subagentAddress === void 0 ? {} : { subagentAddress }
1031
+ });
1032
+ } catch (error) {
1033
+ reference.release();
1034
+ throw error;
1035
+ }
1036
+ const previous = this.mainReference;
1037
+ this.mainReference = reference;
1038
+ previous?.release();
1039
+ if (panel === "reveal") this.ctx.layout.selectPanel(null);
645
1040
  }
646
- return {
647
- items: ordered.slice(0, limit).map((summary) => {
648
- const match = contentBySession.get(summary.id);
649
- const status = statuses.get(summary.id);
650
- const pendingInteraction = visiblePendingKind(status?.pendingInteraction?.kind);
651
- return {
652
- id: summary.id,
653
- title: sessionTitle(summary),
654
- workspace: labelOf(summary),
655
- running: status?.running ?? summary.running,
656
- runningSubagentCount: descendants.get(summary.id)?.runningCount ?? 0,
657
- ...pendingInteraction === void 0 ? {} : { pendingInteraction },
658
- completed: status?.completionUnread === true,
659
- hasActiveSchedule: hasActiveSchedule(summary),
660
- ...match === void 0 ? {} : { snippet: match.snippet }
661
- };
662
- }),
663
- hasMore: content.hasMore || ordered.length > limit
664
- };
665
- }
666
- /** Normalize separators for comparison without interpreting POSIX backslashes as separators. */
667
- function folderPath(path) {
668
- return (/^[A-Za-z]:[/\\]/.test(path) || path.startsWith("\\\\") ? path.replaceAll("\\", "/") : path).replace(/\/+$/, "");
669
- }
1041
+ };
670
1042
  /**
671
- * Find the nearest registered ancestor, excluding the Workspace directory itself.
672
- * Paths use Host spelling; matching is case-sensitive, like Workspace identity.
673
- * @param path - Workspace directory.
674
- * @param parents - registered Workspace directory paths.
675
- * @returns the owning parent path, or undefined when no parent contains the Workspace.
1043
+ * `error` as the Session Controller's creation failure, or undefined when it
1044
+ * is not one. Client plugin bundles do not share error-class identity, so the
1045
+ * name decides.
676
1046
  */
677
- function owningParentFolder(path, parents) {
678
- const child = folderPath(path);
679
- let owner;
680
- let length = -1;
681
- for (const parent of parents) {
682
- const root = folderPath(parent);
683
- if (root.length > length && child !== root && child.startsWith(`${root}/`)) {
684
- owner = parent;
685
- length = root.length;
1047
+ function sessionCreateErrorOf(error) {
1048
+ return error instanceof Error && error.name === "SessionCreateError" ? error : void 0;
1049
+ }
1050
+ /**
1051
+ * The words a failed Session creation is reported in: a Host refusal keeps its
1052
+ * stable code and message; any other failure keeps its own message.
1053
+ */
1054
+ function creationFailureMessage(error) {
1055
+ const refused = sessionCreateErrorOf(error);
1056
+ if (refused !== void 0) return `${refused.rpcError.code}: ${refused.rpcError.message}`;
1057
+ return error instanceof Error ? error.message : String(error);
1058
+ }
1059
+ /** Stable tie-breaking follows Host Workspace order. */
1060
+ function recentWorkspace(workspaces, sessions) {
1061
+ let selected;
1062
+ let selectedTime = Number.NEGATIVE_INFINITY;
1063
+ for (const workspace of workspaces) {
1064
+ let latest = Number.NEGATIVE_INFINITY;
1065
+ for (const sessionId of workspace.sessionIds) {
1066
+ const session = sessions[sessionId];
1067
+ if (session !== void 0) latest = Math.max(latest, session.updatedAt);
1068
+ }
1069
+ if (latest === Number.NEGATIVE_INFINITY) latest = Date.parse(workspace.createdAt);
1070
+ if (selected === void 0 || latest > selectedTime) {
1071
+ selected = workspace.workspaceId;
1072
+ selectedTime = latest;
686
1073
  }
687
1074
  }
688
- return owner;
1075
+ return selected;
1076
+ }
1077
+ //#endregion
1078
+ //#region ../../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
1079
+ function r(e) {
1080
+ var t, f, n = "";
1081
+ if ("string" == typeof e || "number" == typeof e) n += e;
1082
+ else if ("object" == typeof e) if (Array.isArray(e)) {
1083
+ var o = e.length;
1084
+ for (t = 0; t < o; t++) e[t] && (f = r(e[t])) && (n && (n += " "), n += f);
1085
+ } else for (f in e) e[f] && (n && (n += " "), n += f);
1086
+ return n;
1087
+ }
1088
+ function clsx() {
1089
+ for (var e, t, f = 0, n = "", o = arguments.length; f < o; f++) (e = arguments[f]) && (t = r(e)) && (n && (n += " "), n += t);
1090
+ return n;
689
1091
  }
690
1092
  //#endregion
691
1093
  //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-workspace/src/client/rows/Rows.module.css.mjs
692
- const css$2 = ".YDXeBa_projectRow,.YDXeBa_sessionRow{padding:0 8px;cursor:pointer;user-select:none;color:var(--dsw-alias-label-primary);border-radius:8px;align-items:center;gap:6px;padding-inline-start:calc(8px + var(--dsh-workspace-indent,0px));display:flex}.YDXeBa_projectRow:hover,.YDXeBa_sessionRow:hover,.YDXeBa_sessionRow.YDXeBa_selected{background:var(--dsw-alias-interactive-bg-hover)}.YDXeBa_searchResultRow{box-sizing:border-box;cursor:pointer;text-align:left;width:100%;min-height:48px;color:var(--dsw-alias-label-primary);background:0 0;border:none;border-radius:8px;flex-direction:column;align-items:stretch;padding:4px 8px;display:flex}.YDXeBa_searchResultRow:hover,.YDXeBa_searchResultRow.YDXeBa_selected{background:var(--dsw-alias-interactive-bg-hover)}.YDXeBa_searchResultHeading{align-items:center;min-width:0;display:flex}.YDXeBa_searchResultTitle{text-overflow:ellipsis;white-space:nowrap;flex:0 auto;min-width:0;margin-left:4px;font-size:14px;line-height:20px;overflow:hidden}.YDXeBa_searchResultMeta{align-items:center;gap:6px;min-width:0;margin-left:20px;display:flex}.YDXeBa_searchResultWorkspace,.YDXeBa_searchResultSnippet{text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:17px;overflow:hidden}.YDXeBa_searchResultWorkspace{max-width:40%;color:var(--dsw-alias-label-tertiary);flex:none}.YDXeBa_searchResultSnippet{min-width:0;color:var(--dsw-alias-label-secondary);flex:1}.YDXeBa_projectRow{box-sizing:border-box;align-items:center;height:34px}.YDXeBa_projectRow .YDXeBa_rowActions{height:20px}.YDXeBa_sessionRow{height:32px;animation:YDXeBa_row-in .15s var(--ds-ease-in-out);gap:0}.YDXeBa_sessionRow .YDXeBa_title{margin:0 6px 0 4px}.YDXeBa_flatSessionRowWithoutStatus .YDXeBa_title{margin-left:0}@keyframes YDXeBa_row-in{0%{opacity:0}}.YDXeBa_slot{width:16px;height:20px;color:var(--dsw-alias-label-tertiary);flex:none;justify-content:center;align-items:center;display:inline-flex}.YDXeBa_visuallyHidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}.YDXeBa_folderActive{color:var(--dsw-alias-state-business-primary)}.YDXeBa_projectRow .YDXeBa_chevron{display:none}.YDXeBa_projectRow:hover .YDXeBa_chevron{display:inline-flex}.YDXeBa_projectRow:hover .YDXeBa_folder{display:none}.YDXeBa_arrow{transition:transform .15s var(--ds-ease-in-out)}.YDXeBa_arrowOpen{transform:rotate(90deg)}.YDXeBa_projectText{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex}.YDXeBa_title{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:14px;line-height:20px;overflow:hidden}.YDXeBa_renameInput{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-button-elevated-fill);min-width:0;color:inherit;border-radius:4px;outline:none;padding:0 2px;font-size:14px;line-height:20px}.YDXeBa_sessionRow .YDXeBa_title{scroll-behavior:smooth;flex:1}@media (hover:hover){.YDXeBa_sessionRow:hover .YDXeBa_title{text-overflow:clip}}.YDXeBa_meta{text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:20px;overflow:hidden}.YDXeBa_time{color:var(--dsw-alias-label-tertiary);flex:none;font-size:12px;line-height:20px}.YDXeBa_scheduleIndicator{width:16px;height:20px;color:var(--dsw-alias-label-tertiary);flex:none;justify-content:center;align-items:center;margin-right:6px;display:inline-flex}.YDXeBa_searchScheduleIndicator{margin-left:4px;margin-right:0}.YDXeBa_dot{flex:none}.YDXeBa_rowActions{flex:none;align-items:center;gap:12px;display:none}.YDXeBa_projectRow:hover .YDXeBa_rowActions,.YDXeBa_sessionRow:hover .YDXeBa_rowActions,.YDXeBa_projectRow.YDXeBa_menuOpen .YDXeBa_rowActions,.YDXeBa_sessionRow.YDXeBa_menuOpen .YDXeBa_rowActions{display:inline-flex}.YDXeBa_sessionRow:hover .YDXeBa_time,.YDXeBa_sessionRow.YDXeBa_menuOpen .YDXeBa_time{display:none}.YDXeBa_projectRow.YDXeBa_menuOpen,.YDXeBa_sessionRow.YDXeBa_menuOpen{background:var(--dsw-alias-interactive-bg-hover)}.YDXeBa_sessionRow.YDXeBa_dropBefore,.YDXeBa_sessionRow.YDXeBa_dropAfter{position:relative}.YDXeBa_sessionRow.YDXeBa_dropBefore:before,.YDXeBa_sessionRow.YDXeBa_dropAfter:after{content:\"\";z-index:1;background:linear-gradient(55deg, transparent calc(50% - 1px), var(--dsw-alias-state-business-primary) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)) 0 0 / 5px 7px no-repeat, linear-gradient(125deg, transparent calc(50% - 1px), var(--dsw-alias-state-business-primary) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)) 0 5px / 5px 7px no-repeat, linear-gradient(var(--dsw-alias-state-business-primary) 0 0) 4px 5px / calc(100% - 4px) 2px no-repeat;pointer-events:none;height:12px;position:absolute;left:0;right:4px}.YDXeBa_sessionRow.YDXeBa_dropBefore:before{top:-7px}.YDXeBa_sessionRow.YDXeBa_dropAfter:after{bottom:-7px}.YDXeBa_hoverContent{flex-direction:column;gap:8px;display:flex}.YDXeBa_hoverTitle{color:#fff;overflow-wrap:break-word;font-size:14px;line-height:20px}.YDXeBa_hoverPath{color:#cfd3d6;word-break:break-all;font-size:12px;line-height:16px}.YDXeBa_hoverTime{color:#cfd3d6;font-size:12px;line-height:16px}.YDXeBa_hoverStatus{color:#adb2b8;align-items:center;gap:8px;font-size:12px;line-height:20px;display:flex}.YDXeBa_iconButton{cursor:pointer;width:16px;height:16px;color:var(--dsw-alias-label-tertiary);background:0 0;border:none;border-radius:4px;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.YDXeBa_iconButton:hover{color:var(--dsw-alias-label-primary)}.YDXeBa_chevron{color:var(--dsw-alias-label-caption)}@media (prefers-reduced-motion:reduce){.YDXeBa_sessionRow,.YDXeBa_arrow{transition:none;animation:none}.YDXeBa_sessionRow .YDXeBa_title{scroll-behavior:auto}}";
693
- const tagId$2 = "@deepseek-ai/dsh-client-ui-workspace/Rows.module.css";
694
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
1094
+ const css$3 = ".YDXeBa_projectRow,.YDXeBa_sessionRow{padding:0 8px;cursor:pointer;user-select:none;color:var(--dsw-alias-label-primary);border-radius:8px;align-items:center;gap:6px;padding-inline-start:calc(8px + var(--dsh-workspace-indent,0px));display:flex}.YDXeBa_projectRow:hover,.YDXeBa_sessionRow:hover,.YDXeBa_sessionRow.YDXeBa_selected{background:var(--dsw-alias-interactive-bg-hover)}.YDXeBa_searchResultRow{box-sizing:border-box;cursor:pointer;text-align:left;width:100%;min-height:48px;color:var(--dsw-alias-label-primary);background:0 0;border:none;border-radius:8px;flex-direction:column;align-items:stretch;padding:4px 8px;display:flex}.YDXeBa_searchResultRow:hover,.YDXeBa_searchResultRow.YDXeBa_selected{background:var(--dsw-alias-interactive-bg-hover)}.YDXeBa_searchResultHeading{align-items:center;min-width:0;display:flex}.YDXeBa_searchResultTitle{text-overflow:ellipsis;white-space:nowrap;flex:0 auto;min-width:0;margin-left:4px;font-size:14px;line-height:20px;overflow:hidden}.YDXeBa_searchResultMeta{align-items:center;gap:6px;min-width:0;margin-left:20px;display:flex}.YDXeBa_searchResultWorkspace,.YDXeBa_searchResultSnippet{text-overflow:ellipsis;white-space:nowrap;font-size:12px;line-height:17px;overflow:hidden}.YDXeBa_searchResultWorkspace{max-width:40%;color:var(--dsw-alias-label-tertiary);flex:none}.YDXeBa_searchResultSnippet{min-width:0;color:var(--dsw-alias-label-secondary);flex:1}.YDXeBa_projectRow{box-sizing:border-box;align-items:center;height:34px}.YDXeBa_projectRow .YDXeBa_rowActions{height:20px}.YDXeBa_sessionRow{gap:0;height:32px}.YDXeBa_sessionRow .YDXeBa_title{margin:0 6px 0 4px}.YDXeBa_flatSessionRowWithoutStatus .YDXeBa_title{margin-left:0}.YDXeBa_slot{width:16px;height:20px;color:var(--dsw-alias-label-tertiary);flex:none;justify-content:center;align-items:center;display:inline-flex}.YDXeBa_visuallyHidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}.YDXeBa_folderActive{color:var(--dsw-alias-state-business-primary)}.YDXeBa_projectRow .YDXeBa_chevron{display:none}.YDXeBa_projectRow:hover .YDXeBa_chevron{display:inline-flex}.YDXeBa_projectRow:hover .YDXeBa_folder{display:none}.YDXeBa_arrow{transition:transform .15s var(--ds-ease-in-out)}.YDXeBa_arrowOpen{transform:rotate(90deg)}.YDXeBa_projectText{flex-direction:column;flex:1;gap:2px;min-width:0;display:flex}.YDXeBa_title{text-overflow:ellipsis;white-space:nowrap;min-width:0;font-size:14px;line-height:20px;overflow:hidden}.YDXeBa_renameInput{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-button-elevated-fill);min-width:0;color:inherit;border-radius:4px;outline:none;padding:0 2px;font-size:14px;line-height:20px}.YDXeBa_sessionRow .YDXeBa_title{flex:1}.YDXeBa_sessionRow .YDXeBa_title[data-scrolled]{mask-image:linear-gradient(90deg,#0000,#000 12px)}.YDXeBa_sessionRow .YDXeBa_title[data-clipped]{mask-image:linear-gradient(270deg,#0000,#000 12px)}.YDXeBa_sessionRow .YDXeBa_title[data-scrolled][data-clipped]{mask-image:linear-gradient(90deg,#0000,#000 12px calc(100% - 12px),#0000)}.YDXeBa_meta{text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:20px;overflow:hidden}.YDXeBa_time{color:var(--dsw-alias-label-caption);flex:none;font-size:10px;line-height:16px}.YDXeBa_scheduleIndicator{width:16px;height:20px;color:var(--dsw-alias-label-tertiary);flex:none;justify-content:center;align-items:center;margin-right:6px;display:inline-flex}.YDXeBa_searchScheduleIndicator{margin-left:4px;margin-right:0}.YDXeBa_pinIndicator{width:16px;height:20px;color:var(--dsw-alias-label-caption);flex:none;justify-content:center;align-items:center;margin-left:6px;display:inline-flex}.YDXeBa_sessionRow.YDXeBa_archived .YDXeBa_title,.YDXeBa_sessionRow.YDXeBa_archived .YDXeBa_time,.YDXeBa_searchResultRow.YDXeBa_archived .YDXeBa_searchResultTitle,.YDXeBa_searchResultRow.YDXeBa_archived .YDXeBa_searchResultWorkspace,.YDXeBa_searchResultRow.YDXeBa_archived .YDXeBa_searchResultSnippet{color:var(--dsw-alias-label-caption)}.YDXeBa_dot{flex:none}.YDXeBa_rowActions{flex:none;align-items:center;gap:10px;display:none}.YDXeBa_projectRow:hover .YDXeBa_rowActions,.YDXeBa_sessionRow:hover .YDXeBa_rowActions,.YDXeBa_searchResultRow:hover .YDXeBa_rowActions,.YDXeBa_projectRow.YDXeBa_menuOpen .YDXeBa_rowActions,.YDXeBa_sessionRow.YDXeBa_menuOpen .YDXeBa_rowActions{display:inline-flex}.YDXeBa_searchResultHeading .YDXeBa_rowActions{margin-left:auto}.YDXeBa_sessionRow:hover .YDXeBa_time,.YDXeBa_sessionRow.YDXeBa_menuOpen .YDXeBa_time,.YDXeBa_sessionRow:hover .YDXeBa_pinIndicator,.YDXeBa_sessionRow.YDXeBa_menuOpen .YDXeBa_pinIndicator{display:none}@media (hover:hover){.YDXeBa_sessionRow:hover .YDXeBa_title,.YDXeBa_sessionRow.YDXeBa_menuOpen .YDXeBa_title{text-overflow:clip}}.YDXeBa_projectRow.YDXeBa_menuOpen,.YDXeBa_sessionRow.YDXeBa_menuOpen{background:var(--dsw-alias-interactive-bg-hover)}.YDXeBa_sessionRow.YDXeBa_dropBefore,.YDXeBa_sessionRow.YDXeBa_dropAfter{position:relative}.YDXeBa_sessionRow.YDXeBa_dropBefore:before,.YDXeBa_sessionRow.YDXeBa_dropAfter:after{content:\"\";z-index:1;background:linear-gradient(55deg, transparent calc(50% - 1px), var(--dsw-alias-state-business-primary) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)) 0 0 / 5px 7px no-repeat, linear-gradient(125deg, transparent calc(50% - 1px), var(--dsw-alias-state-business-primary) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)) 0 5px / 5px 7px no-repeat, linear-gradient(var(--dsw-alias-state-business-primary) 0 0) 4px 5px / calc(100% - 4px) 2px no-repeat;pointer-events:none;height:12px;position:absolute;left:0;right:4px}.YDXeBa_sessionRow.YDXeBa_dropBefore:before{top:-7px}.YDXeBa_sessionRow.YDXeBa_dropAfter:after{bottom:-7px}.YDXeBa_hoverContent{flex-direction:column;gap:8px;display:flex}.YDXeBa_hoverTitle{color:#fff;overflow-wrap:break-word;font-size:14px;line-height:20px}.YDXeBa_hoverPath{color:#cfd3d6;word-break:break-all;font-size:12px;line-height:16px}.YDXeBa_hoverTime{color:#cfd3d6;font-size:12px;line-height:16px}.YDXeBa_hoverStatus{color:#adb2b8;align-items:center;gap:8px;font-size:12px;line-height:20px;display:flex}.YDXeBa_hoverArchived svg{flex-shrink:0;margin:0 -4px}.YDXeBa_iconButton{cursor:pointer;width:16px;height:16px;color:var(--dsw-alias-label-tertiary);background:0 0;border:none;border-radius:4px;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.YDXeBa_iconButton:hover{color:var(--dsw-alias-label-primary)}.YDXeBa_chevron{color:var(--dsw-alias-label-caption)}@media (prefers-reduced-motion:reduce){.YDXeBa_arrow{transition:none;animation:none}}";
1095
+ const tagId$3 = "@deepseek-ai/dsh-client-ui-workspace/Rows.module.css";
1096
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$3) + "]") === null) {
695
1097
  const tag = document.createElement("style");
696
1098
  tag.dataset.plugin = "@deepseek-ai/dsh-client-ui-workspace";
697
- tag.dataset.pluginCss = tagId$2;
698
- tag.textContent = css$2;
1099
+ tag.dataset.pluginCss = tagId$3;
1100
+ tag.textContent = css$3;
699
1101
  document.head.appendChild(tag);
700
1102
  }
701
1103
  var Rows_module_css_default = {
1104
+ "archived": "YDXeBa_archived",
702
1105
  "arrow": "YDXeBa_arrow",
703
1106
  "arrowOpen": "YDXeBa_arrowOpen",
704
1107
  "chevron": "YDXeBa_chevron",
@@ -708,6 +1111,7 @@ window.__ModuleLoader__.load({
708
1111
  "flatSessionRowWithoutStatus": "YDXeBa_flatSessionRowWithoutStatus",
709
1112
  "folder": "YDXeBa_folder",
710
1113
  "folderActive": "YDXeBa_folderActive",
1114
+ "hoverArchived": "YDXeBa_hoverArchived",
711
1115
  "hoverContent": "YDXeBa_hoverContent",
712
1116
  "hoverPath": "YDXeBa_hoverPath",
713
1117
  "hoverStatus": "YDXeBa_hoverStatus",
@@ -716,10 +1120,10 @@ window.__ModuleLoader__.load({
716
1120
  "iconButton": "YDXeBa_iconButton",
717
1121
  "menuOpen": "YDXeBa_menuOpen",
718
1122
  "meta": "YDXeBa_meta",
1123
+ "pinIndicator": "YDXeBa_pinIndicator",
719
1124
  "projectRow": "YDXeBa_projectRow",
720
1125
  "projectText": "YDXeBa_projectText",
721
1126
  "renameInput": "YDXeBa_renameInput",
722
- "row-in": "YDXeBa_row-in",
723
1127
  "rowActions": "YDXeBa_rowActions",
724
1128
  "scheduleIndicator": "YDXeBa_scheduleIndicator",
725
1129
  "searchResultHeading": "YDXeBa_searchResultHeading",
@@ -742,37 +1146,99 @@ window.__ModuleLoader__.load({
742
1146
  * Workspace browser tree row components (figma Cell set 14:3080): pure presentational —
743
1147
  * all data and callbacks arrive via props. Hover swaps (folder->chevron,
744
1148
  * time->ellipsis, action buttons) are CSS-only, and a session row's clipped
745
- * title is scrolled programmatically while the row is hovered. Row ... menus are
746
- * visual-only except workspace Rename/Delete and session Rename/Fork/Archive; the
1149
+ * title marquees programmatically while the row is hovered. Workspace row
1150
+ * menus are visual-only except Rename/Delete. A Session row's "..." menu and
1151
+ * its hover buttons are the `sidebar.workspaces.session.menu.item` and
1152
+ * `sidebar.workspaces.session.row.action` lists, rendered through the
1153
+ * browser's `renderSlot` with the menu's open state as the occurrence's hook
1154
+ * context; this package's own actions are entries like any plugin's. The
747
1155
  * session and workspace hover cards are suppressed while a menu is open.
748
1156
  */
749
1157
  /** Row display title: blank rows show the localized New Session label. */
750
1158
  function displayTitle(node, t) {
751
1159
  return node.blank ? t("session.new") : node.title;
752
1160
  }
1161
+ const MIN_TITLE_REVEAL_PX = 8;
1162
+ const TITLE_MARQUEE_PX_PER_MS = .03;
753
1163
  /**
754
- * Reveal a title wider than its one-line cell while its row is hovered: the
755
- * title clips its own text, so the far edge (a fork's incremented title, for
756
- * example) is reachable by scrolling the element to its end. Leaving returns it
757
- * to the start in one step, because the resting ellipsis and the narrowed cell
758
- * would otherwise meet the text while it travelled back. A title that fits has
759
- * no scroll range to move, and the stylesheet decides whether either move
760
- * glides or jumps.
1164
+ * Place the title's scroll position and publish the stylesheet's fade-mask
1165
+ * hooks: `data-scrolled` while the title has left its start (left fade) and
1166
+ * `data-clipped` while text remains beyond the right edge (right fade).
761
1167
  * @param title - the row's clipping title element.
762
- * @param revealed - whether the pointer is on the row.
1168
+ * @param left - scroll offset in CSS pixels.
1169
+ * @param range - the title's maximum scroll offset in CSS pixels.
763
1170
  */
764
- function revealClippedTitle(title, revealed) {
765
- /* v8 ignore next -- defensive: the title span renders unconditionally. */
766
- if (title === null) return;
767
- if (revealed) {
768
- title.scrollLeft = title.scrollWidth - title.clientWidth;
769
- return;
770
- }
1171
+ function placeTitle(title, left, range) {
1172
+ if (typeof title.scrollTo === "function") title.scrollTo({
1173
+ left,
1174
+ behavior: "instant"
1175
+ });
1176
+ else title.scrollLeft = left;
1177
+ if (left > 0) title.dataset.scrolled = "";
1178
+ else delete title.dataset.scrolled;
1179
+ if (left < range) title.dataset.clipped = "";
1180
+ else delete title.dataset.clipped;
1181
+ }
1182
+ /**
1183
+ * Return the title to its resting state: scrolled to the start with both fade
1184
+ * masks off, so the resting ellipsis renders at full strength.
1185
+ * @param title - the row's clipping title element.
1186
+ */
1187
+ function restTitle(title) {
771
1188
  if (typeof title.scrollTo === "function") title.scrollTo({
772
1189
  left: 0,
773
1190
  behavior: "instant"
774
1191
  });
775
1192
  else title.scrollLeft = 0;
1193
+ delete title.dataset.scrolled;
1194
+ delete title.dataset.clipped;
1195
+ }
1196
+ /**
1197
+ * Marquee a title wider than its one-line cell while its row is hovered: the
1198
+ * title clips its own text, so entering crawls it at a constant speed until the
1199
+ * far edge (a fork's incremented title, for example) is in view, then rests
1200
+ * there under the pointer. Overflow of at most {@link MIN_TITLE_REVEAL_PX}
1201
+ * stays put — a barely-clipped title moving a few pixels reads as jitter, not a
1202
+ * reveal. Leaving returns the title to the start in one step, because the
1203
+ * resting ellipsis and the narrowed cell would otherwise meet the text while it
1204
+ * travelled back. Reduced motion jumps to the far edge instead of crawling.
1205
+ * @param title - ref to the row's clipping title element.
1206
+ * @returns stable pointer enter/leave handlers for the row.
1207
+ */
1208
+ function useTitleMarquee(title) {
1209
+ const frame = (0, react.useRef)(0);
1210
+ (0, react.useEffect)(() => () => {
1211
+ cancelAnimationFrame(frame.current);
1212
+ }, []);
1213
+ return (0, react.useMemo)(() => ({
1214
+ enter: () => {
1215
+ /* v8 ignore next -- defensive: the title span renders unconditionally. */
1216
+ if (title.current === null) return;
1217
+ const element = title.current;
1218
+ const range = element.scrollWidth - element.clientWidth;
1219
+ if (range <= MIN_TITLE_REVEAL_PX) return;
1220
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
1221
+ placeTitle(element, range, range);
1222
+ return;
1223
+ }
1224
+ cancelAnimationFrame(frame.current);
1225
+ let previous;
1226
+ let position = 0;
1227
+ const step = (now) => {
1228
+ position += previous === void 0 ? 0 : (now - previous) * TITLE_MARQUEE_PX_PER_MS;
1229
+ previous = now;
1230
+ placeTitle(element, Math.min(position, range), range);
1231
+ if (position < range) frame.current = requestAnimationFrame(step);
1232
+ };
1233
+ frame.current = requestAnimationFrame(step);
1234
+ },
1235
+ leave: () => {
1236
+ cancelAnimationFrame(frame.current);
1237
+ /* v8 ignore next -- defensive: the title span renders unconditionally. */
1238
+ if (title.current === null) return;
1239
+ restTitle(title.current);
1240
+ }
1241
+ }), [title]);
776
1242
  }
777
1243
  /** Localized compact relative time ("刚刚"/"5分钟" in zh, "now"/"5min" in en). */
778
1244
  function timeLabel(updatedAt, now, t) {
@@ -845,15 +1311,16 @@ window.__ModuleLoader__.load({
845
1311
  const workspaceMenuItems = [{
846
1312
  id: "rename",
847
1313
  label: t("rename"),
848
- icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, {})
1314
+ icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutlineRegular, {})
849
1315
  }, {
850
1316
  id: "delete",
851
1317
  label: t("delete.workspace"),
852
- icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, {}),
1318
+ icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutlineRegular, {}),
853
1319
  danger: true
854
1320
  }];
855
1321
  const ownRow = (0, react_jsx_runtime.jsxs)("div", {
856
1322
  className: clsx(Rows_module_css_default.projectRow, menuOpen && Rows_module_css_default.menuOpen),
1323
+ "data-row-key": `workspace:${group.key}`,
857
1324
  role: "treeitem",
858
1325
  "aria-expanded": row.expanded,
859
1326
  onClick: onToggle,
@@ -867,11 +1334,11 @@ window.__ModuleLoader__.load({
867
1334
  children: [
868
1335
  (0, react_jsx_runtime.jsx)("span", {
869
1336
  className: clsx(Rows_module_css_default.slot, Rows_module_css_default.folder, active && Rows_module_css_default.folderActive),
870
- children: row.expanded ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpen16, {}) : (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderClose16, {})
1337
+ children: row.expanded ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpenRegular, {}) : (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderCloseRegular, {})
871
1338
  }),
872
1339
  (0, react_jsx_runtime.jsx)("span", {
873
1340
  className: clsx(Rows_module_css_default.slot, Rows_module_css_default.chevron),
874
- children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTriangleRightFill14, { className: clsx(Rows_module_css_default.arrow, row.expanded && Rows_module_css_default.arrowOpen) })
1341
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTriangleRightFillRegular, { className: clsx(Rows_module_css_default.arrow, row.expanded && Rows_module_css_default.arrowOpen) })
875
1342
  }),
876
1343
  (0, react_jsx_runtime.jsx)("span", {
877
1344
  className: Rows_module_css_default.projectText,
@@ -905,17 +1372,23 @@ window.__ModuleLoader__.load({
905
1372
  e.stopPropagation();
906
1373
  setMenuOpen((v) => !v);
907
1374
  },
908
- children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEllipsisOutline16, {})
1375
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEllipsisOutlineRegular, {})
1376
+ })
1377
+ }), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
1378
+ label: t("actions.newSession"),
1379
+ side: "bottom",
1380
+ align: "end",
1381
+ delayMs: 500,
1382
+ children: (0, react_jsx_runtime.jsx)("button", {
1383
+ type: "button",
1384
+ className: Rows_module_css_default.iconButton,
1385
+ "aria-label": t("actions.newSession.aria", { name: label }),
1386
+ onClick: (e) => {
1387
+ e.stopPropagation();
1388
+ onCreate();
1389
+ },
1390
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconNewChatOutlineRegular, {})
909
1391
  })
910
- }), (0, react_jsx_runtime.jsx)("button", {
911
- type: "button",
912
- className: Rows_module_css_default.iconButton,
913
- "aria-label": t("actions.newSession.aria", { name: label }),
914
- onClick: (e) => {
915
- e.stopPropagation();
916
- onCreate();
917
- },
918
- children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutline16, {})
919
1392
  })]
920
1393
  })
921
1394
  ]
@@ -929,6 +1402,7 @@ window.__ModuleLoader__.load({
929
1402
  createdAt: row.createdAt,
930
1403
  t
931
1404
  }),
1405
+ openDelayMs: 800,
932
1406
  disabled: menuOpen,
933
1407
  copyText: row.cwd,
934
1408
  copyLabel: t("copy"),
@@ -953,19 +1427,22 @@ window.__ModuleLoader__.load({
953
1427
  case "approval":
954
1428
  pending = {
955
1429
  state: "warning",
956
- label: t("status.waitingApproval")
1430
+ label: t("status.waitingApproval"),
1431
+ trailingLabel: t("status.compact.approval")
957
1432
  };
958
1433
  break;
959
1434
  case "plan-review":
960
1435
  pending = {
961
1436
  state: "warning",
962
- label: t("status.planReview")
1437
+ label: t("status.planReview"),
1438
+ trailingLabel: t("status.compact.planReview")
963
1439
  };
964
1440
  break;
965
1441
  case "question":
966
1442
  pending = {
967
1443
  state: "warning",
968
- label: t("status.waitingAnswer")
1444
+ label: t("status.waitingAnswer"),
1445
+ trailingLabel: t("status.compact.answer")
969
1446
  };
970
1447
  break;
971
1448
  case void 0: break;
@@ -986,7 +1463,7 @@ window.__ModuleLoader__.load({
986
1463
  label: t("status.completed")
987
1464
  }];
988
1465
  return [{
989
- state: "done",
1466
+ state: "idle",
990
1467
  label: t("status.idle")
991
1468
  }];
992
1469
  }
@@ -1005,12 +1482,23 @@ window.__ModuleLoader__.load({
1005
1482
  role: "img",
1006
1483
  "aria-label": label,
1007
1484
  title: label,
1008
- children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconAlarmClockOutline16, {})
1485
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconAlarmClockOutlineRegular, {})
1486
+ });
1487
+ }
1488
+ /** Non-interactive pinned-row marker; the enclosing row remains the only action. */
1489
+ function PinnedIndicator({ t }) {
1490
+ const label = t("row.pinned");
1491
+ return (0, react_jsx_runtime.jsx)("span", {
1492
+ className: Rows_module_css_default.pinIndicator,
1493
+ role: "img",
1494
+ "aria-label": label,
1495
+ title: label,
1496
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPinFillRegular, { size: 14 })
1009
1497
  });
1010
1498
  }
1011
1499
  /** Hover-card body: full title, relative time, and every relevant live status. */
1012
1500
  function SessionHoverContent({ node, now, t }) {
1013
- const statuses = sessionStatuses(node, t);
1501
+ const statuses = sessionStatuses(node, t).filter((status) => !(node.archived && (status.state === "done" || status.state === "idle")));
1014
1502
  return (0, react_jsx_runtime.jsxs)("div", {
1015
1503
  className: Rows_module_css_default.hoverContent,
1016
1504
  children: [
@@ -1025,29 +1513,35 @@ window.__ModuleLoader__.load({
1025
1513
  statuses.map((status) => (0, react_jsx_runtime.jsxs)("div", {
1026
1514
  className: Rows_module_css_default.hoverStatus,
1027
1515
  children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.StateDot, { state: status.state }), (0, react_jsx_runtime.jsx)("span", { children: status.label })]
1028
- }, status.label))
1516
+ }, status.label)),
1517
+ node.archived && (0, react_jsx_runtime.jsxs)("div", {
1518
+ className: clsx(Rows_module_css_default.hoverStatus, Rows_module_css_default.hoverArchived),
1519
+ children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconArchiveOutlineRegular, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: t("row.archived") })]
1520
+ })
1029
1521
  ]
1030
1522
  });
1031
1523
  }
1032
1524
  /**
1033
1525
  * One flat search result: title, Workspace context, and optional content
1034
1526
  * excerpt. Search navigation opens the session only; it does not address an
1035
- * event inside the conversation.
1527
+ * event inside the conversation. Archived rows carry a hover unarchive
1528
+ * button, because search is where the filter surfaces them for recovery.
1036
1529
  * @param props.result - merged local/content search row.
1037
1530
  * @param props.currentId - selected session id.
1038
1531
  * @param props.onOpen - open the selected session.
1532
+ * @param props.onUnarchive - unarchive an archived result row.
1039
1533
  * @param props.t - Workspace-browser translation seat.
1040
- * @returns the result button.
1534
+ * @returns the result row.
1041
1535
  */
1042
- function SearchResultItem({ result, currentId, onOpen, t }) {
1536
+ function SearchResultItem({ result, currentId, onOpen, onUnarchive, t }) {
1043
1537
  const selected = result.id === currentId;
1044
1538
  const statuses = sessionStatuses(result, t);
1045
1539
  const primaryStatus = statuses[0];
1046
- return (0, react_jsx_runtime.jsxs)("button", {
1047
- type: "button",
1048
- className: clsx(Rows_module_css_default.searchResultRow, selected && Rows_module_css_default.selected),
1540
+ return (0, react_jsx_runtime.jsxs)("div", {
1541
+ className: clsx(Rows_module_css_default.searchResultRow, selected && Rows_module_css_default.selected, result.archived && Rows_module_css_default.archived),
1049
1542
  role: "treeitem",
1050
1543
  "aria-selected": selected,
1544
+ "aria-description": result.archived ? t("toast.archivedNotOpenable") : void 0,
1051
1545
  onClick: () => {
1052
1546
  onOpen(result.id);
1053
1547
  },
@@ -1056,7 +1550,7 @@ window.__ModuleLoader__.load({
1056
1550
  children: [
1057
1551
  (0, react_jsx_runtime.jsx)("span", {
1058
1552
  className: Rows_module_css_default.slot,
1059
- children: (primaryStatus.state !== "done" || result.completed) && (0, react_jsx_runtime.jsx)(SessionStatusDots, { statuses })
1553
+ children: !result.archived && primaryStatus.state !== "idle" && (0, react_jsx_runtime.jsx)(SessionStatusDots, { statuses })
1060
1554
  }),
1061
1555
  (0, react_jsx_runtime.jsx)("span", {
1062
1556
  className: Rows_module_css_default.searchResultTitle,
@@ -1065,6 +1559,25 @@ window.__ModuleLoader__.load({
1065
1559
  result.hasActiveSchedule && (0, react_jsx_runtime.jsx)(ActiveScheduleIndicator, {
1066
1560
  t,
1067
1561
  search: true
1562
+ }),
1563
+ result.archived && (0, react_jsx_runtime.jsx)("span", {
1564
+ className: Rows_module_css_default.rowActions,
1565
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
1566
+ label: t("actions.unarchive"),
1567
+ side: "bottom",
1568
+ align: "end",
1569
+ delayMs: 500,
1570
+ children: (0, react_jsx_runtime.jsx)("button", {
1571
+ type: "button",
1572
+ className: Rows_module_css_default.iconButton,
1573
+ "aria-label": t("menu.unarchiveSession"),
1574
+ onClick: (e) => {
1575
+ e.stopPropagation();
1576
+ onUnarchive(result.id);
1577
+ },
1578
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconUnarchiveOutlineRegular, { size: 14 })
1579
+ })
1580
+ })
1068
1581
  })
1069
1582
  ]
1070
1583
  }), (0, react_jsx_runtime.jsxs)("span", {
@@ -1081,74 +1594,58 @@ window.__ModuleLoader__.load({
1081
1594
  }
1082
1595
  /**
1083
1596
  * One top-level 34px session row: status dot (pending user interaction outranks
1084
- * own or descendant activity), title, relative time, and the row actions menu.
1597
+ * own or descendant activity), title, relative time or compact pending label,
1598
+ * and the row actions menu.
1085
1599
  * @param props.node - derived session node.
1086
1600
  * @param props.currentId - selected session id (row highlight).
1087
1601
  * @param props.now - epoch ms for relative-time formatting.
1088
1602
  * @param props.onOpen - open a session by id.
1089
- * @param props.onRename - open the session rename dialog (id + current title).
1090
- * @param props.onFork - fork a session at its last completed turn.
1091
- * @param props.onArchive - archive a session by id.
1603
+ * @param props.onRenameRequest - open the rename dialog from a title double-click (id + current title).
1604
+ * @param props.renderSlot - render the row's `sidebar.workspaces.session.menu.item` and `sidebar.workspaces.session.row.action` lists.
1092
1605
  * @param props.onReveal - scroll this row into view after search navigation, then acknowledge it.
1093
1606
  * @param props.drag - optional row-drag target wiring; blank rows cannot start a drag.
1094
1607
  * @param props.flat - omit the empty status slot in the hierarchy-free flat list.
1095
1608
  * @param props.t - the browser root's locale seat.
1096
1609
  * @returns the session row.
1097
1610
  */
1098
- function SessionNodeItem({ node, currentId, now, onOpen, onRename, onFork, onArchive, onReveal, drag, flat = false, t }) {
1611
+ function SessionNodeItem({ node, currentId, now, onOpen, onRenameRequest, renderSlot, onReveal, drag, flat = false, t }) {
1099
1612
  const row = node;
1100
1613
  const title = displayTitle(node, t);
1101
1614
  const selected = node.id === currentId;
1102
1615
  const statuses = sessionStatuses(node, t);
1103
- const showStatus = statuses[0].state !== "done" || row.completed;
1104
- const draggable = drag !== void 0 && !row.blank;
1616
+ const primaryStatus = statuses[0];
1617
+ const showStatus = primaryStatus.state !== "idle";
1618
+ const draggable = drag !== void 0 && !row.blank && !row.archived;
1105
1619
  const [menuOpen, setMenuOpen] = (0, react.useState)(false);
1620
+ const menuOpenState = (0, react.useMemo)(() => [menuOpen, setMenuOpen], [menuOpen]);
1106
1621
  const rowRef = (0, react.useRef)(null);
1107
1622
  const titleRef = (0, react.useRef)(null);
1623
+ const marquee = useTitleMarquee(titleRef);
1108
1624
  (0, react.useEffect)(() => {
1109
1625
  if (onReveal === void 0) return;
1110
1626
  rowRef.current?.scrollIntoView({ block: "nearest" });
1111
1627
  onReveal();
1112
1628
  }, [onReveal]);
1113
- const sessionMenuItems = [
1114
- {
1115
- id: "rename",
1116
- label: t("rename"),
1117
- icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, {})
1118
- },
1119
- {
1120
- id: "fork",
1121
- label: t("menu.fork"),
1122
- icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconBranchOutline16, {})
1123
- },
1124
- {
1125
- id: "archive",
1126
- label: t("menu.archiveSession"),
1127
- icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconArchiveOutline20, { size: 16 })
1128
- }
1129
- ];
1130
1629
  return (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.HoverCard, {
1131
1630
  anchor: (0, react_jsx_runtime.jsxs)("div", {
1132
1631
  ref: rowRef,
1133
- className: clsx(Rows_module_css_default.sessionRow, selected && Rows_module_css_default.selected, menuOpen && Rows_module_css_default.menuOpen, flat && !showStatus && Rows_module_css_default.flatSessionRowWithoutStatus, drag?.marker === "before" && Rows_module_css_default.dropBefore, drag?.marker === "after" && Rows_module_css_default.dropAfter),
1632
+ "data-row-key": `session:${node.id}`,
1633
+ className: clsx(Rows_module_css_default.sessionRow, selected && Rows_module_css_default.selected, menuOpen && Rows_module_css_default.menuOpen, row.archived && Rows_module_css_default.archived, flat && !showStatus && Rows_module_css_default.flatSessionRowWithoutStatus, drag?.marker === "before" && Rows_module_css_default.dropBefore, drag?.marker === "after" && Rows_module_css_default.dropAfter),
1134
1634
  role: "treeitem",
1135
1635
  "aria-selected": selected,
1636
+ "aria-description": row.archived ? t("toast.archivedNotOpenable") : void 0,
1136
1637
  onClick: () => {
1137
1638
  onOpen(node.id);
1138
1639
  },
1139
- onPointerEnter: () => {
1140
- revealClippedTitle(titleRef.current, true);
1141
- },
1142
- onPointerLeave: () => {
1143
- revealClippedTitle(titleRef.current, false);
1144
- },
1640
+ onPointerEnter: marquee.enter,
1641
+ onPointerLeave: marquee.leave,
1145
1642
  draggable,
1146
- onDragStart: drag === void 0 || row.blank ? void 0 : (e) => {
1643
+ onDragStart: !draggable ? void 0 : (e) => {
1147
1644
  e.dataTransfer.effectAllowed = "move";
1148
1645
  e.dataTransfer.setData("text/plain", node.id);
1149
1646
  drag.start();
1150
1647
  },
1151
- onDragEnd: drag === void 0 || row.blank ? void 0 : drag.end,
1648
+ onDragEnd: !draggable ? void 0 : drag.end,
1152
1649
  onDragOver: drag === void 0 ? void 0 : (e) => {
1153
1650
  if (!drag.active) return;
1154
1651
  e.preventDefault();
@@ -1163,45 +1660,53 @@ window.__ModuleLoader__.load({
1163
1660
  children: [
1164
1661
  (!flat || showStatus) && (0, react_jsx_runtime.jsx)("span", {
1165
1662
  className: Rows_module_css_default.slot,
1166
- children: showStatus && (0, react_jsx_runtime.jsx)(SessionStatusDots, { statuses })
1663
+ children: !row.archived && showStatus && (0, react_jsx_runtime.jsx)(SessionStatusDots, { statuses })
1167
1664
  }),
1168
1665
  (0, react_jsx_runtime.jsx)("span", {
1169
1666
  ref: titleRef,
1170
1667
  className: Rows_module_css_default.title,
1668
+ onDoubleClick: row.blank ? void 0 : (e) => {
1669
+ e.stopPropagation();
1670
+ onRenameRequest(node.id, row.title);
1671
+ },
1171
1672
  children: title
1172
1673
  }),
1173
1674
  row.hasActiveSchedule && (0, react_jsx_runtime.jsx)(ActiveScheduleIndicator, { t }),
1174
1675
  !row.blank && (0, react_jsx_runtime.jsx)("span", {
1175
1676
  className: Rows_module_css_default.time,
1176
- children: timeLabel(row.updatedAt, now, t)
1677
+ "aria-hidden": primaryStatus.trailingLabel === void 0 ? void 0 : true,
1678
+ children: primaryStatus.trailingLabel ?? timeLabel(row.updatedAt, now, t)
1177
1679
  }),
1178
- !row.blank && (0, react_jsx_runtime.jsx)("span", {
1680
+ row.pinned && !row.archived && (0, react_jsx_runtime.jsx)(PinnedIndicator, { t }),
1681
+ !row.blank && (0, react_jsx_runtime.jsxs)("span", {
1179
1682
  className: Rows_module_css_default.rowActions,
1180
- children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
1683
+ onClick: (e) => {
1684
+ e.stopPropagation();
1685
+ },
1686
+ children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
1181
1687
  open: menuOpen,
1182
1688
  onClose: () => {
1183
1689
  setMenuOpen(false);
1184
1690
  },
1185
- items: sessionMenuItems,
1186
- onSelect: (id) => {
1187
- setMenuOpen(false);
1188
- if (id === "rename") onRename(node.id, row.title);
1189
- if (id === "fork") onFork(node.id);
1190
- if (id === "archive") onArchive(node.id);
1191
- },
1192
1691
  portal: true,
1193
1692
  closeOnPointerLeave: true,
1194
1693
  anchor: (0, react_jsx_runtime.jsx)("button", {
1195
1694
  type: "button",
1196
1695
  className: Rows_module_css_default.iconButton,
1197
1696
  "aria-label": t("actions.session.aria", { name: title }),
1198
- onClick: (e) => {
1199
- e.stopPropagation();
1697
+ onClick: () => {
1200
1698
  setMenuOpen((v) => !v);
1201
1699
  },
1202
- children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEllipsisOutline16, {})
1203
- })
1204
- })
1700
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEllipsisOutlineRegular, {})
1701
+ }),
1702
+ children: renderSlot("sidebar.workspaces.session.menu.item", {
1703
+ sessionId: node.id,
1704
+ displayTitle: row.title
1705
+ }, { hookContext: menuOpenState })
1706
+ }), renderSlot("sidebar.workspaces.session.row.action", {
1707
+ sessionId: node.id,
1708
+ displayTitle: row.title
1709
+ })]
1205
1710
  })
1206
1711
  ]
1207
1712
  }),
@@ -1210,6 +1715,7 @@ window.__ModuleLoader__.load({
1210
1715
  now,
1211
1716
  t
1212
1717
  }),
1718
+ openDelayMs: 800,
1213
1719
  disabled: menuOpen || drag?.active === true,
1214
1720
  copyText: row.blank ? void 0 : row.title,
1215
1721
  copyLabel: t("copy"),
@@ -1217,6 +1723,182 @@ window.__ModuleLoader__.load({
1217
1723
  });
1218
1724
  }
1219
1725
  //#endregion
1726
+ //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-workspace/src/client/rows/AnimatedRows.module.css.mjs
1727
+ const css$2 = ".Qg_5Eq_exits{contain:strict;pointer-events:none;position:absolute;inset:0;overflow:clip}";
1728
+ const tagId$2 = "@deepseek-ai/dsh-client-ui-workspace/AnimatedRows.module.css";
1729
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
1730
+ const tag = document.createElement("style");
1731
+ tag.dataset.plugin = "@deepseek-ai/dsh-client-ui-workspace";
1732
+ tag.dataset.pluginCss = tagId$2;
1733
+ tag.textContent = css$2;
1734
+ document.head.appendChild(tag);
1735
+ }
1736
+ var AnimatedRows_module_css_default = { "exits": "Qg_5Eq_exits" };
1737
+ //#endregion
1738
+ //#region lib/types/client/rows/AnimatedRows.js
1739
+ /** React-commit-driven movement and entry/exit fades for the sidebar's keyed rows. */
1740
+ const ROW_FADE_MS = 100;
1741
+ const ROW_GLIDE_MS = 200;
1742
+ function sameRows(previous, next) {
1743
+ return previous.rowKeys.length === next.rowKeys.length && previous.rowKeys.every((key, index) => key === next.rowKeys[index]);
1744
+ }
1745
+ function intersects(row, viewport) {
1746
+ return row.bottom > viewport.top && row.top < viewport.bottom && row.right > viewport.left && row.left < viewport.right;
1747
+ }
1748
+ /**
1749
+ * Animates keyed sidebar rows only when their rendered membership or order changes.
1750
+ * Motion starts after the first pointer or keyboard input inside the mounted list.
1751
+ * The parent supplies a positioned container for the inert exit overlay.
1752
+ */
1753
+ var AnimatedRows = class extends react.Component {
1754
+ armed = false;
1755
+ list = (0, react.createRef)();
1756
+ overlay = (0, react.createRef)();
1757
+ movements = /* @__PURE__ */ new Map();
1758
+ exits = /* @__PURE__ */ new Map();
1759
+ getSnapshotBeforeUpdate(previous) {
1760
+ const list = this.list.current;
1761
+ if (!this.armed || sameRows(previous, this.props) || previous.resetKey !== this.props.resetKey || !previous.ready || !this.props.ready || list === null || typeof list.animate !== "function" || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return null;
1762
+ const viewport = list.getBoundingClientRect();
1763
+ const positions = this.readPositions();
1764
+ const nextKeys = new Set(this.props.rowKeys);
1765
+ const removed = /* @__PURE__ */ new Map();
1766
+ for (const [key, row] of positions) {
1767
+ if (nextKeys.has(key) || !intersects(row.rect, viewport)) continue;
1768
+ const clone = row.element.cloneNode(true);
1769
+ clone.removeAttribute("data-row-key");
1770
+ clone.inert = true;
1771
+ clone.style.setProperty("--dsh-workspace-indent", getComputedStyle(row.element).getPropertyValue("--dsh-workspace-indent"));
1772
+ removed.set(key, {
1773
+ ...row,
1774
+ element: clone
1775
+ });
1776
+ }
1777
+ return {
1778
+ positions,
1779
+ removed
1780
+ };
1781
+ }
1782
+ componentDidUpdate(previous, _state, snapshot) {
1783
+ if (snapshot === null) {
1784
+ if (!sameRows(previous, this.props) || previous.resetKey !== this.props.resetKey || previous.ready !== this.props.ready) this.clear();
1785
+ return;
1786
+ }
1787
+ this.cancelMovements();
1788
+ const list = this.list.current;
1789
+ const overlay = this.overlay.current;
1790
+ const viewport = list.getBoundingClientRect();
1791
+ const origin = overlay.getBoundingClientRect();
1792
+ const positions = this.readPositions();
1793
+ for (const [key, row] of positions) {
1794
+ this.removeExit(key);
1795
+ const previousRow = snapshot.positions.get(key);
1796
+ if (!intersects(row.rect, viewport) && (previousRow === void 0 || !intersects(previousRow.rect, viewport))) continue;
1797
+ if (previousRow === void 0) {
1798
+ this.move(row.element, [{ opacity: 0 }, { opacity: 1 }], ROW_FADE_MS);
1799
+ continue;
1800
+ }
1801
+ const dx = previousRow.rect.left - row.rect.left;
1802
+ const dy = previousRow.rect.top - row.rect.top;
1803
+ if (dx === 0 && dy === 0 && previousRow.opacity === 1) continue;
1804
+ this.move(row.element, [{
1805
+ transform: `translate(${String(dx)}px, ${String(dy)}px)`,
1806
+ opacity: previousRow.opacity
1807
+ }, {
1808
+ transform: "translate(0, 0)",
1809
+ opacity: 1
1810
+ }], ROW_GLIDE_MS);
1811
+ }
1812
+ for (const [key, row] of snapshot.removed) {
1813
+ const { element } = row;
1814
+ this.removeExit(key);
1815
+ Object.assign(element.style, {
1816
+ position: "absolute",
1817
+ margin: "0",
1818
+ transform: "none",
1819
+ boxSizing: "border-box",
1820
+ left: `${String(row.rect.left - origin.left)}px`,
1821
+ top: `${String(row.rect.top - origin.top)}px`,
1822
+ width: `${String(row.rect.width)}px`,
1823
+ height: `${String(row.rect.height)}px`
1824
+ });
1825
+ overlay.append(element);
1826
+ const animation = element.animate([{ opacity: row.opacity }, { opacity: 0 }], {
1827
+ duration: ROW_FADE_MS,
1828
+ easing: "ease-out",
1829
+ fill: "forwards"
1830
+ });
1831
+ this.exits.set(key, {
1832
+ element,
1833
+ animation
1834
+ });
1835
+ animation.onfinish = () => {
1836
+ this.removeExit(key);
1837
+ };
1838
+ }
1839
+ }
1840
+ componentWillUnmount() {
1841
+ this.clear();
1842
+ }
1843
+ readPositions() {
1844
+ const rows = this.list.current.querySelectorAll("[data-row-key]");
1845
+ return new Map(Array.from(rows, (element) => [element.dataset.rowKey, {
1846
+ element,
1847
+ rect: element.getBoundingClientRect(),
1848
+ opacity: this.movements.has(element) ? Number(getComputedStyle(element).opacity) : 1
1849
+ }]));
1850
+ }
1851
+ move(element, keyframes, duration) {
1852
+ const animation = element.animate(keyframes, {
1853
+ duration,
1854
+ easing: "ease-out"
1855
+ });
1856
+ this.movements.set(element, animation);
1857
+ animation.onfinish = () => {
1858
+ this.movements.delete(element);
1859
+ animation.cancel();
1860
+ };
1861
+ }
1862
+ cancelMovements() {
1863
+ for (const animation of this.movements.values()) {
1864
+ animation.onfinish = null;
1865
+ animation.cancel();
1866
+ }
1867
+ this.movements.clear();
1868
+ }
1869
+ removeExit(key) {
1870
+ const exit = this.exits.get(key);
1871
+ if (exit === void 0) return;
1872
+ exit.animation.onfinish = null;
1873
+ exit.animation.cancel();
1874
+ exit.element.remove();
1875
+ this.exits.delete(key);
1876
+ }
1877
+ clear() {
1878
+ this.cancelMovements();
1879
+ for (const key of this.exits.keys()) this.removeExit(key);
1880
+ }
1881
+ render() {
1882
+ return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)("div", {
1883
+ ref: this.list,
1884
+ className: this.props.className,
1885
+ role: "tree",
1886
+ "aria-label": this.props.label,
1887
+ onPointerDownCapture: () => {
1888
+ this.armed = true;
1889
+ },
1890
+ onKeyDownCapture: () => {
1891
+ this.armed = true;
1892
+ },
1893
+ children: this.props.children
1894
+ }), (0, react_jsx_runtime.jsx)("div", {
1895
+ ref: this.overlay,
1896
+ className: AnimatedRows_module_css_default.exits,
1897
+ "aria-hidden": "true"
1898
+ })] });
1899
+ }
1900
+ };
1901
+ //#endregion
1220
1902
  //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-workspace/src/client/WorkspacePicker.module.css.mjs
1221
1903
  const css$1 = "._G5b-a_modalAction{min-width:72px}._G5b-a_modalError,._G5b-a_menuStatus{margin-top:8px;font-size:12px;line-height:18px}._G5b-a_modalError{color:var(--dsw-alias-state-error-primary)}._G5b-a_menuStatus{color:var(--dsw-alias-label-secondary)}";
1222
1904
  const tagId$1 = "@deepseek-ai/dsh-client-ui-workspace/WorkspacePicker.module.css";
@@ -1256,14 +1938,14 @@ window.__ModuleLoader__.load({
1256
1938
  const addEntries = flowAvailable ? [{
1257
1939
  id: ADD_WORKSPACE,
1258
1940
  label: t("menu.addWorkspace"),
1259
- icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutline16, { size: 16 }),
1941
+ icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPlusOutlineRegular, { size: 16 }),
1260
1942
  disabled: flowBusy
1261
1943
  }] : [];
1262
1944
  const pinAdd = !addOnly && workspaces.length > 0;
1263
1945
  const items = pinAdd ? workspaces.map((workspace) => ({
1264
1946
  id: workspace.workspaceId,
1265
1947
  label: workspace.title,
1266
- icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderClose16, { size: 16 }),
1948
+ icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderCloseRegular, { size: 16 }),
1267
1949
  disabled: flowBusy
1268
1950
  })) : addEntries;
1269
1951
  const menuIsEmpty = items.length === 0;
@@ -1388,7 +2070,7 @@ window.__ModuleLoader__.load({
1388
2070
  }
1389
2071
  //#endregion
1390
2072
  //#region \0dsh-css:/home/runner/work/deepseek-harness/deepseek-harness/packages/client/ui-workspace/src/client/rows/WorkspaceBrowser.module.css.mjs
1391
- const css = ".bhn1Oq_root{--dsh-session-list-edge-inset:var(--dsh-sidebar-inline-padding);--dsh-session-list-scrollbar-width:8px;--dsh-session-list-scrollbar-offset:2px;box-sizing:border-box;min-height:0;padding-right:var(--dsh-session-list-edge-inset);flex-direction:column;flex:1;display:flex}.bhn1Oq_root.bhn1Oq_rail{padding-right:0}.bhn1Oq_iconButton{corner-shape:round;cursor:pointer;width:28px;height:28px;color:var(--dsw-alias-label-secondary);background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.bhn1Oq_iconButton:hover{background:var(--dsw-alias-interactive-bg-hover)}.bhn1Oq_sectionHeader{box-sizing:border-box;height:36px;color:var(--dsw-alias-label-tertiary);border-radius:12px;flex:none;justify-content:flex-end;align-items:center;gap:4px;margin-bottom:4px;padding-left:4px;display:flex;overflow:hidden}.bhn1Oq_root:not(.bhn1Oq_rail) .bhn1Oq_sectionHeader{margin-top:2px;margin-right:-4px}.bhn1Oq_sectionLabel{white-space:nowrap;opacity:1;visibility:visible;min-width:0;max-width:45%;transition:max-width .18s var(--ds-ease-in-out), margin-right .18s var(--ds-ease-in-out), opacity .12s var(--ds-ease-in-out), transform .18s var(--ds-ease-in-out), visibility 0s linear;flex:none;line-height:20px;overflow:hidden}.bhn1Oq_sectionLabelHidden{opacity:0;visibility:hidden;max-width:0;margin-right:-4px;transition-delay:0s,0s,0s,0s,.18s;transform:translate(-4px)}.bhn1Oq_searchSlot{box-sizing:border-box;min-width:0;max-width:28px;transition:max-width .18s var(--ds-ease-in-out), padding-left .18s var(--ds-ease-in-out);flex:1;align-items:center;margin-left:auto;padding-left:0;display:flex}.bhn1Oq_searchSlotExpanded{max-width:100%;padding-left:0}.bhn1Oq_headerActions{opacity:1;visibility:visible;max-width:60px;transition:max-width .18s var(--ds-ease-in-out), opacity .12s var(--ds-ease-in-out), transform .18s var(--ds-ease-in-out), visibility 0s linear;flex:none;align-items:center;gap:4px;display:flex;overflow:hidden}.bhn1Oq_headerActionsHidden{opacity:0;visibility:hidden;pointer-events:none;max-width:0;transition-delay:0s,0s,0s,.18s;transform:translate(4px)}.bhn1Oq_search{box-sizing:border-box;corner-shape:round;cursor:text;width:100%;height:28px;color:var(--dsw-alias-label-secondary);transition:width .18s var(--ds-ease-in-out), padding .18s var(--ds-ease-in-out), border-color .18s var(--ds-ease-in-out), background-color .18s var(--ds-ease-in-out);background:0 0;border:none;border-radius:50%;flex:none;align-items:center;gap:0;margin:0;padding:0;display:flex;overflow:hidden}.bhn1Oq_searchExpanded{border:.5px solid var(--dsw-alias-border-l4);width:calc(100% + 4px);height:30px;color:var(--dsw-alias-label-caption);background:0 0;border-radius:10px;margin-inline:-2px;padding:0 4px 0 0}.bhn1Oq_searchButton{corner-shape:round;cursor:pointer;width:28px;height:28px;color:inherit;background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.bhn1Oq_searchExpanded .bhn1Oq_searchButton{width:28px;height:30px}.bhn1Oq_searchButton:hover{background:var(--dsw-alias-interactive-bg-hover)}.bhn1Oq_searchExpanded .bhn1Oq_searchButton:hover{background:0 0}.bhn1Oq_searchInput{opacity:0;pointer-events:none;width:0;min-width:0;color:var(--dsw-alias-label-primary);transition:opacity .12s var(--ds-ease-in-out);background:0 0;border:none;outline:none;flex:1;font-size:13px;line-height:18px}.bhn1Oq_searchExpanded .bhn1Oq_searchInput{opacity:1;pointer-events:auto;margin-left:-2px}.bhn1Oq_searchInput::placeholder{color:var(--dsw-alias-label-tertiary)}.bhn1Oq_clearButton{corner-shape:round;cursor:pointer;width:24px;height:24px;color:var(--dsw-alias-label-secondary);background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.bhn1Oq_clearButton:hover{background:var(--dsw-alias-interactive-bg-hover)}.bhn1Oq_rail .bhn1Oq_sectionHeader{justify-content:flex-start;gap:0;margin-bottom:12px;padding-left:0}.bhn1Oq_rail .bhn1Oq_headerActions{max-width:none}.bhn1Oq_rail .bhn1Oq_iconButton{width:36px;height:36px;color:var(--dsw-alias-label-primary);border-radius:12px}.bhn1Oq_rail .bhn1Oq_search{background:0 0;border-color:#0000;border-radius:12px;gap:0;width:36px;height:36px;margin:0 0 12px;padding:0}.bhn1Oq_rail .bhn1Oq_searchButton{width:36px;height:36px;color:var(--dsw-alias-label-primary);border-radius:12px}.bhn1Oq_rail .bhn1Oq_searchButton:hover{background:var(--dsw-alias-interactive-bg-hover)}.bhn1Oq_listArea{min-height:0;margin-left:-4px;margin-right:calc(-1 * var(--dsh-session-list-edge-inset));flex-direction:column;flex:1;padding-left:4px;display:flex;overflow:visible}.bhn1Oq_rail .bhn1Oq_listArea{margin-left:0;margin-right:0;padding-left:0}.bhn1Oq_treeBody{flex-direction:column;flex:1;min-height:0;display:flex;position:relative}.bhn1Oq_fade{left:0;right:var(--dsh-session-list-edge-inset);background:linear-gradient(to bottom, transparent, var(--dsw-specific-sidebar-fill));pointer-events:none;height:24px;position:absolute;bottom:0}[data-platform=darwin] .bhn1Oq_fade{display:none}.bhn1Oq_wide{animation:bhn1Oq_wide-in .2s var(--ds-ease-in-out)}@keyframes bhn1Oq_wide-in{0%{opacity:0}}.bhn1Oq_list{min-height:0;margin-left:-4px;margin-right:var(--dsh-session-list-scrollbar-offset);padding-left:4px;padding-right:calc(var(--dsh-session-list-edge-inset) - var(--dsh-session-list-scrollbar-width) - var(--dsh-session-list-scrollbar-offset));scrollbar-gutter:stable;flex:1;padding-bottom:16px;overflow-y:auto}.bhn1Oq_flatList>*+*,.bhn1Oq_searchTree>[role=treeitem]+[role=treeitem],.bhn1Oq_groupSection>*+*{margin-top:2px}.bhn1Oq_searchStatus,.bhn1Oq_searchWarning{color:var(--dsw-alias-label-tertiary);padding:10px 12px;font-size:12px;line-height:18px}.bhn1Oq_searchWarning{color:var(--dsw-alias-label-secondary)}.bhn1Oq_groupSection{position:relative}.bhn1Oq_groupSection+.bhn1Oq_groupSection{margin-top:4px}.bhn1Oq_listTopDropIndicator,.bhn1Oq_workspaceDropBefore:before,.bhn1Oq_workspaceDropAfter:after{content:\"\";z-index:1;background:linear-gradient(55deg, transparent calc(50% - 1px), var(--dsw-alias-state-business-primary) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)) 0 0 / 5px 7px no-repeat, linear-gradient(125deg, transparent calc(50% - 1px), var(--dsw-alias-state-business-primary) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)) 0 5px / 5px 7px no-repeat, linear-gradient(var(--dsw-alias-state-business-primary) 0 0) 4px 5px / calc(100% - 4px) 2px no-repeat;pointer-events:none;height:12px;position:absolute;left:0;right:0}.bhn1Oq_listTopDropIndicator{top:-8px;left:0;right:var(--dsh-session-list-edge-inset)}.bhn1Oq_listTopDropActive>.bhn1Oq_workspaceDropBefore:first-child:before{display:none}.bhn1Oq_workspaceDropBefore:before{top:-8px}.bhn1Oq_workspaceDropAfter:after{bottom:-8px}.bhn1Oq_sessionOverflowButton{width:100%;height:28px;padding:0 12px 0 calc(28px + var(--dsh-workspace-indent,0px));cursor:pointer;text-align:left;color:var(--dsw-alias-label-tertiary);background:0 0;border:none;border-radius:8px;font-size:12px}.bhn1Oq_groupSection>.bhn1Oq_sessionOverflowButton{margin-top:0}.bhn1Oq_sessionOverflowButton:hover{color:var(--dsw-alias-label-secondary);background:0 0}.bhn1Oq_empty{color:var(--dsw-alias-label-tertiary);padding:16px 12px;font-size:13px}.bhn1Oq_renameInput{box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l4);width:100%;height:44px;color:var(--dsw-alias-label-primary);background:0 0;border-radius:22px;outline:none;padding:7px 14px;font-size:14px;font-weight:400;line-height:22px}.bhn1Oq_renameInput:disabled{color:var(--dsw-alias-label-dimmed)}.bhn1Oq_renameError{color:var(--dsw-alias-state-error-primary);margin-top:8px;font-size:12px;line-height:18px}.bhn1Oq_deleteAction:not(:disabled){color:var(--dsw-alias-state-error-primary)}.bhn1Oq_deleteStatus{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}@media (prefers-reduced-motion:reduce){.bhn1Oq_wide{animation:none}.bhn1Oq_search,.bhn1Oq_sectionLabel,.bhn1Oq_searchSlot,.bhn1Oq_searchInput,.bhn1Oq_headerActions{transition:none}}";
2073
+ const css = ".bhn1Oq_root{--dsh-session-list-edge-inset:var(--dsh-sidebar-inline-padding);--dsh-session-list-scrollbar-width:5px;--dsh-session-list-scrollbar-offset:2px;box-sizing:border-box;min-height:0;padding-right:var(--dsh-session-list-edge-inset);flex-direction:column;flex:1;display:flex}.bhn1Oq_root.bhn1Oq_rail{padding-right:0}.bhn1Oq_iconButton{corner-shape:round;cursor:pointer;width:28px;height:28px;color:var(--dsw-alias-label-secondary);background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.bhn1Oq_iconButton:hover{background:var(--dsw-alias-interactive-bg-hover)}.bhn1Oq_viewOptionsMenu{min-width:200px}.bhn1Oq_sectionHeader{box-sizing:border-box;height:36px;color:var(--dsw-alias-label-tertiary);border-radius:12px;flex:none;justify-content:flex-end;align-items:center;gap:4px;margin-bottom:4px;padding-left:4px;display:flex;overflow:hidden}.bhn1Oq_root:not(.bhn1Oq_rail) .bhn1Oq_sectionHeader{margin-top:2px;margin-right:-4px}.bhn1Oq_sectionLabel{white-space:nowrap;opacity:1;visibility:visible;min-width:0;max-width:45%;transition:max-width .18s var(--ds-ease-in-out), margin-right .18s var(--ds-ease-in-out), opacity .12s var(--ds-ease-in-out), transform .18s var(--ds-ease-in-out), visibility 0s linear;flex:none;line-height:20px;overflow:hidden}.bhn1Oq_sectionLabelHidden{opacity:0;visibility:hidden;max-width:0;margin-right:-4px;transition-delay:0s,0s,0s,0s,.18s;transform:translate(-4px)}.bhn1Oq_searchSlot{box-sizing:border-box;min-width:0;max-width:28px;transition:max-width .18s var(--ds-ease-in-out), padding-left .18s var(--ds-ease-in-out);flex:1;align-items:center;margin-left:auto;padding-left:0;display:flex}.bhn1Oq_searchSlotExpanded{max-width:100%;padding-left:0}.bhn1Oq_headerActions{opacity:1;visibility:visible;max-width:60px;transition:max-width .18s var(--ds-ease-in-out), opacity .12s var(--ds-ease-in-out), transform .18s var(--ds-ease-in-out), visibility 0s linear;flex:none;align-items:center;gap:4px;display:flex;overflow:hidden}.bhn1Oq_headerActionsHidden{opacity:0;visibility:hidden;pointer-events:none;max-width:0;transition-delay:0s,0s,0s,.18s;transform:translate(4px)}.bhn1Oq_search{box-sizing:border-box;corner-shape:round;cursor:text;width:100%;height:28px;color:var(--dsw-alias-label-secondary);transition:width .18s var(--ds-ease-in-out), padding .18s var(--ds-ease-in-out), border-color .18s var(--ds-ease-in-out), background-color .18s var(--ds-ease-in-out);background:0 0;border:none;border-radius:50%;flex:none;align-items:center;gap:0;margin:0;padding:0;display:flex;overflow:hidden}.bhn1Oq_searchExpanded{border:.5px solid var(--dsw-alias-border-l4);width:calc(100% + 4px);height:30px;color:var(--dsw-alias-label-caption);background:0 0;border-radius:10px;margin-inline:-2px;padding:0 4px 0 0}.bhn1Oq_searchButton{corner-shape:round;cursor:pointer;width:28px;height:28px;color:inherit;background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.bhn1Oq_searchExpanded .bhn1Oq_searchButton{width:28px;height:30px}.bhn1Oq_searchButton:hover{background:var(--dsw-alias-interactive-bg-hover)}.bhn1Oq_searchExpanded .bhn1Oq_searchButton:hover{background:0 0}.bhn1Oq_searchInput{opacity:0;pointer-events:none;width:0;min-width:0;color:var(--dsw-alias-label-primary);transition:opacity .12s var(--ds-ease-in-out);background:0 0;border:none;outline:none;flex:1;font-size:13px;line-height:18px}.bhn1Oq_searchExpanded .bhn1Oq_searchInput{opacity:1;pointer-events:auto;margin-left:-2px}.bhn1Oq_searchInput::placeholder{color:var(--dsw-alias-label-tertiary)}.bhn1Oq_clearButton{corner-shape:round;cursor:pointer;width:24px;height:24px;color:var(--dsw-alias-label-secondary);background:0 0;border:none;border-radius:50%;flex:none;justify-content:center;align-items:center;padding:0;display:inline-flex}.bhn1Oq_clearButton:hover{background:var(--dsw-alias-interactive-bg-hover)}.bhn1Oq_rail .bhn1Oq_sectionHeader{justify-content:flex-start;gap:0;margin-bottom:12px;padding-left:0}.bhn1Oq_rail .bhn1Oq_headerActions{max-width:none}.bhn1Oq_rail .bhn1Oq_iconButton{width:36px;height:36px;color:var(--dsw-alias-label-primary);border-radius:12px}.bhn1Oq_rail .bhn1Oq_search{background:0 0;border-color:#0000;border-radius:12px;gap:0;width:36px;height:36px;margin:0 0 12px;padding:0}.bhn1Oq_rail .bhn1Oq_searchButton{width:36px;height:36px;color:var(--dsw-alias-label-primary);border-radius:12px}.bhn1Oq_rail .bhn1Oq_searchButton:hover{background:var(--dsw-alias-interactive-bg-hover)}.bhn1Oq_listArea{min-height:0;margin-left:-4px;margin-right:calc(-1 * var(--dsh-session-list-edge-inset));flex-direction:column;flex:1;padding-left:4px;display:flex;overflow:visible}.bhn1Oq_rail .bhn1Oq_listArea{margin-left:0;margin-right:0;padding-left:0}.bhn1Oq_treeBody{flex-direction:column;flex:1;min-height:0;display:flex;position:relative}.bhn1Oq_fade{left:0;right:var(--dsh-session-list-edge-inset);background:linear-gradient(to bottom, transparent, var(--dsw-specific-sidebar-fill));pointer-events:none;height:24px;position:absolute;bottom:0}[data-platform=darwin] .bhn1Oq_fade{display:none}.bhn1Oq_wide{animation:bhn1Oq_wide-in .2s var(--ds-ease-in-out)}@keyframes bhn1Oq_wide-in{0%{opacity:0}}.bhn1Oq_list{min-height:0;margin-left:-4px;margin-right:var(--dsh-session-list-scrollbar-offset);padding-left:4px;padding-right:calc(var(--dsh-session-list-edge-inset) - var(--dsh-session-list-scrollbar-width) - var(--dsh-session-list-scrollbar-offset));scrollbar-gutter:stable;flex:1;padding-bottom:16px;overflow-y:auto}.bhn1Oq_flatList>*+*,.bhn1Oq_searchTree>[role=treeitem]+[role=treeitem],.bhn1Oq_groupSection>*+*{margin-top:2px}.bhn1Oq_searchStatus{color:var(--dsw-alias-label-tertiary);padding:10px 12px;font-size:12px;line-height:18px}.bhn1Oq_skeletonRow{box-sizing:border-box;align-items:flex-start;gap:8px;min-height:48px;padding:6px 8px 7px;display:flex}.bhn1Oq_skeletonDot{corner-shape:round;border-radius:50%;flex:none;width:16px;height:16px}.bhn1Oq_skeletonBars{flex-direction:column;flex:1;gap:6px;min-width:0;display:flex}.bhn1Oq_skeletonDot,.bhn1Oq_skeletonBar{background:var(--dsw-alias-bg-skeleton);animation:2s cubic-bezier(.36,0,.64,1) infinite bhn1Oq_search-skeleton}.bhn1Oq_skeletonBar{border-radius:4px;width:65%;height:16px}.bhn1Oq_skeletonBarWide{width:90%;height:13px}@keyframes bhn1Oq_search-skeleton{0%{opacity:1}40%{opacity:.6}80%,to{opacity:1}}.bhn1Oq_groupSection{position:relative}.bhn1Oq_groupSection+.bhn1Oq_groupSection{margin-top:4px}.bhn1Oq_listTopDropIndicator,.bhn1Oq_workspaceDropBefore:before,.bhn1Oq_workspaceDropAfter:after{content:\"\";z-index:1;background:linear-gradient(55deg, transparent calc(50% - 1px), var(--dsw-alias-state-business-primary) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)) 0 0 / 5px 7px no-repeat, linear-gradient(125deg, transparent calc(50% - 1px), var(--dsw-alias-state-business-primary) calc(50% - 1px) calc(50% + 1px), transparent calc(50% + 1px)) 0 5px / 5px 7px no-repeat, linear-gradient(var(--dsw-alias-state-business-primary) 0 0) 4px 5px / calc(100% - 4px) 2px no-repeat;pointer-events:none;height:12px;position:absolute;left:0;right:0}.bhn1Oq_listTopDropIndicator{top:-8px;left:0;right:var(--dsh-session-list-edge-inset)}.bhn1Oq_listTopDropActive>.bhn1Oq_workspaceDropBefore:first-child:before{display:none}.bhn1Oq_workspaceDropBefore:before{top:-8px}.bhn1Oq_workspaceDropAfter:after{bottom:-8px}.bhn1Oq_sessionOverflowButton{width:100%;height:28px;padding:0 12px 0 calc(28px + var(--dsh-workspace-indent,0px));cursor:pointer;text-align:left;color:var(--dsw-alias-label-tertiary);background:0 0;border:none;border-radius:8px;font-size:12px}.bhn1Oq_groupSection>.bhn1Oq_sessionOverflowButton{margin-top:0}.bhn1Oq_sessionOverflowButton:hover{color:var(--dsw-alias-label-secondary);background:0 0}.bhn1Oq_empty{color:var(--dsw-alias-label-tertiary);padding:16px 12px;font-size:13px}.bhn1Oq_renameInput{box-sizing:border-box;border:.5px solid var(--dsw-alias-border-l4);width:100%;height:44px;color:var(--dsw-alias-label-primary);background:0 0;border-radius:22px;outline:none;padding:7px 14px;font-size:14px;font-weight:400;line-height:22px}.bhn1Oq_renameInput:disabled{color:var(--dsw-alias-label-dimmed)}.bhn1Oq_renameError{color:var(--dsw-alias-state-error-primary);margin-top:8px;font-size:12px;line-height:18px}.bhn1Oq_deleteAction:not(:disabled){color:var(--dsw-alias-state-error-primary)}.bhn1Oq_deleteStatus{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px}.bhn1Oq_archiveActivity{color:var(--dsw-alias-label-primary);margin:0 0 8px;padding-left:18px;font-size:13px;line-height:20px}.bhn1Oq_archiveActivity li{overflow-wrap:anywhere}@media (prefers-reduced-motion:reduce){.bhn1Oq_wide,.bhn1Oq_skeletonDot,.bhn1Oq_skeletonBar{animation:none}.bhn1Oq_search,.bhn1Oq_sectionLabel,.bhn1Oq_searchSlot,.bhn1Oq_searchInput,.bhn1Oq_headerActions{transition:none}}";
1392
2074
  const tagId = "@deepseek-ai/dsh-client-ui-workspace/WorkspaceBrowser.module.css";
1393
2075
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
1394
2076
  const tag = document.createElement("style");
@@ -1398,6 +2080,7 @@ window.__ModuleLoader__.load({
1398
2080
  document.head.appendChild(tag);
1399
2081
  }
1400
2082
  var WorkspaceBrowser_module_css_default = {
2083
+ "archiveActivity": "bhn1Oq_archiveActivity",
1401
2084
  "clearButton": "bhn1Oq_clearButton",
1402
2085
  "deleteAction": "bhn1Oq_deleteAction",
1403
2086
  "deleteStatus": "bhn1Oq_deleteStatus",
@@ -1417,6 +2100,7 @@ window.__ModuleLoader__.load({
1417
2100
  "renameInput": "bhn1Oq_renameInput",
1418
2101
  "root": "bhn1Oq_root",
1419
2102
  "search": "bhn1Oq_search",
2103
+ "search-skeleton": "bhn1Oq_search-skeleton",
1420
2104
  "searchButton": "bhn1Oq_searchButton",
1421
2105
  "searchExpanded": "bhn1Oq_searchExpanded",
1422
2106
  "searchInput": "bhn1Oq_searchInput",
@@ -1424,12 +2108,17 @@ window.__ModuleLoader__.load({
1424
2108
  "searchSlotExpanded": "bhn1Oq_searchSlotExpanded",
1425
2109
  "searchStatus": "bhn1Oq_searchStatus",
1426
2110
  "searchTree": "bhn1Oq_searchTree",
1427
- "searchWarning": "bhn1Oq_searchWarning",
1428
2111
  "sectionHeader": "bhn1Oq_sectionHeader",
1429
2112
  "sectionLabel": "bhn1Oq_sectionLabel",
1430
2113
  "sectionLabelHidden": "bhn1Oq_sectionLabelHidden",
1431
2114
  "sessionOverflowButton": "bhn1Oq_sessionOverflowButton",
2115
+ "skeletonBar": "bhn1Oq_skeletonBar",
2116
+ "skeletonBarWide": "bhn1Oq_skeletonBarWide",
2117
+ "skeletonBars": "bhn1Oq_skeletonBars",
2118
+ "skeletonDot": "bhn1Oq_skeletonDot",
2119
+ "skeletonRow": "bhn1Oq_skeletonRow",
1432
2120
  "treeBody": "bhn1Oq_treeBody",
2121
+ "viewOptionsMenu": "bhn1Oq_viewOptionsMenu",
1433
2122
  "wide": "bhn1Oq_wide",
1434
2123
  "wide-in": "bhn1Oq_wide-in",
1435
2124
  "workspaceDropAfter": "bhn1Oq_workspaceDropAfter",
@@ -1446,7 +2135,12 @@ window.__ModuleLoader__.load({
1446
2135
  * rail entry path, each requesting expansion through the owner share. Adding
1447
2136
  * is the header button's one action, so it raises the directory flow with no
1448
2137
  * menu in between; the flow and its error dialog live in WorkspacePicker
1449
- * (same package — direct composition, no slot between them).
2138
+ * (same package — direct composition, no slot between them). A Session row's
2139
+ * "..." menu and hover buttons are the `sidebar.workspaces.session.menu.item`
2140
+ * and `sidebar.workspaces.session.row.action` lists rendered through this
2141
+ * entry's `renderSlot`; the actions in them, this package's own included,
2142
+ * are slot entries with their own behavior, so this component threads no
2143
+ * action callbacks and hosts no action surface.
1450
2144
  */
1451
2145
  /**
1452
2146
  * Column slide length (--ds-transition-duration-slow): rail-search focus waits it out —
@@ -1457,15 +2151,15 @@ window.__ModuleLoader__.load({
1457
2151
  const SEARCH_DEBOUNCE_MS = 250;
1458
2152
  /** `session.search` wire bound, measured in JavaScript UTF-16 code units. */
1459
2153
  const SEARCH_QUERY_MAX_CODE_UNITS = 500;
1460
- /** Session rows visible per Workspace before the local overflow control. */
2154
+ /** Idle Session rows visible per Workspace before the local overflow control. */
1461
2155
  const COLLAPSED_SESSION_LIMIT = 5;
1462
- /** Fold one Workspace without charging its provisional New Session against the ordinary-row limit. */
1463
- function collapsedSessionRows(sessions) {
1464
- let ordinaryCount = 0;
2156
+ /** Keep provisional and running rows outside the idle-session quota, including parents with running children. */
2157
+ function collapsedSessionRows(sessions, limit = COLLAPSED_SESSION_LIMIT) {
2158
+ let idleCount = 0;
1465
2159
  const rows = sessions.filter((session) => {
1466
- if (session.blank) return true;
1467
- if (ordinaryCount >= COLLAPSED_SESSION_LIMIT) return false;
1468
- ordinaryCount += 1;
2160
+ if (session.blank || session.running || session.runningSubagentCount > 0) return true;
2161
+ if (idleCount >= limit) return false;
2162
+ idleCount += 1;
1469
2163
  return true;
1470
2164
  });
1471
2165
  return {
@@ -1483,10 +2177,6 @@ window.__ModuleLoader__.load({
1483
2177
  if (last >= 55296 && last <= 56319 && next >= 56320 && next <= 57343) end--;
1484
2178
  return withoutNul.slice(0, end);
1485
2179
  }
1486
- /** Immutable membership toggle for the local expand-all array. */
1487
- function toggled(list, key) {
1488
- return list.includes(key) ? list.filter((k) => k !== key) : [...list, key];
1489
- }
1490
2180
  /**
1491
2181
  * Accept the native drag at document level while a row drag is active: row
1492
2182
  * hover still owns the insertion marker, and releasing outside the list must
@@ -1510,8 +2200,8 @@ window.__ModuleLoader__.load({
1510
2200
  };
1511
2201
  }, [active]);
1512
2202
  }
1513
- /** Grouping and ordering menu; own open state so it resets with the wide chrome. */
1514
- function ViewOptionsMenu({ groupBy, orderBy, onGroupPick, onOrderPick, t }) {
2203
+ /** Grouping, ordering, and archived-filter menu; own open state so it resets with the wide chrome. */
2204
+ function ViewOptionsMenu({ groupBy, orderBy, archivedFilter, onGroupPick, onOrderPick, onArchivedFilterPick, t }) {
1515
2205
  const [open, setOpen] = (0, react.useState)(false);
1516
2206
  return (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
1517
2207
  open,
@@ -1526,15 +2216,18 @@ window.__ModuleLoader__.load({
1526
2216
  },
1527
2217
  {
1528
2218
  id: "workspace",
1529
- label: t("groupBy.workspace")
2219
+ label: t("groupBy.workspace"),
2220
+ icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderCloseRegular, {})
1530
2221
  },
1531
2222
  {
1532
2223
  id: "workspace-tree",
1533
- label: t("groupBy.workspaceTree")
2224
+ label: t("groupBy.workspaceTree"),
2225
+ icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWorkspaceTreeOutlineRegular, {})
1534
2226
  },
1535
2227
  {
1536
2228
  id: "flat",
1537
- label: t("groupBy.flat")
2229
+ label: t("groupBy.flat"),
2230
+ icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFlatListOutlineRegular, {})
1538
2231
  },
1539
2232
  {
1540
2233
  type: "separator",
@@ -1547,21 +2240,50 @@ window.__ModuleLoader__.load({
1547
2240
  },
1548
2241
  {
1549
2242
  id: "manual",
1550
- label: t("orderBy.manual")
2243
+ label: t("orderBy.manual"),
2244
+ icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconChevronsUpDownOutlineRegular, {})
1551
2245
  },
1552
2246
  {
1553
2247
  id: "updated",
1554
- label: t("orderBy.updated")
2248
+ label: t("orderBy.updated"),
2249
+ icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconClockOutlineRegular, {})
2250
+ },
2251
+ {
2252
+ type: "separator",
2253
+ id: "archived-filter-separator"
2254
+ },
2255
+ {
2256
+ type: "label",
2257
+ id: "filter-by",
2258
+ text: t("filterBy.label")
2259
+ },
2260
+ {
2261
+ id: "show-archived",
2262
+ label: t("viewOptions.showArchived"),
2263
+ icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconArchiveOutlineRegular, {})
2264
+ },
2265
+ {
2266
+ id: "only-archived",
2267
+ label: t("viewOptions.onlyArchived"),
2268
+ icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconArchiveCheckOutlineRegular, {})
1555
2269
  }
1556
2270
  ],
1557
- selectedIds: [groupBy, orderBy],
2271
+ selectedIds: [
2272
+ groupBy,
2273
+ orderBy,
2274
+ ...archivedFilter === "show" ? ["show-archived"] : [],
2275
+ ...archivedFilter === "only" ? ["only-archived"] : []
2276
+ ],
1558
2277
  onSelect: (id) => {
1559
2278
  if (id === "workspace" || id === "workspace-tree" || id === "flat") onGroupPick(id);
1560
2279
  else if (id === "manual" || id === "updated") onOrderPick(id);
2280
+ else if (id === "show-archived") onArchivedFilterPick(archivedFilter === "show" ? "default" : "show");
2281
+ else if (id === "only-archived") onArchivedFilterPick(archivedFilter === "only" ? "default" : "only");
1561
2282
  setOpen(false);
1562
2283
  },
1563
2284
  align: "end",
1564
2285
  dense: true,
2286
+ listClassName: WorkspaceBrowser_module_css_default.viewOptionsMenu,
1565
2287
  portal: true,
1566
2288
  anchor: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
1567
2289
  label: t("viewOptions.label"),
@@ -1574,28 +2296,43 @@ window.__ModuleLoader__.load({
1574
2296
  onClick: () => {
1575
2297
  setOpen((v) => !v);
1576
2298
  },
1577
- children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPersonalizationOutline16, {})
2299
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSlidersTwoOutlineRegular, {})
1578
2300
  })
1579
2301
  })
1580
2302
  });
1581
2303
  }
2304
+ /** Apply a visible drop to the complete account without removing hidden members. */
2305
+ function sessionDragOrder(order, rows, drag, over) {
2306
+ const source = rows.find((row) => row.id === drag.sessionId);
2307
+ const target = rows.find((row) => row.id === over.id);
2308
+ if (source === void 0 || target === void 0 || source.blank || source.pinned !== drag.pinned || target.pinned !== drag.pinned || source.id === target.id || !order.includes(source.id)) return;
2309
+ const section = rows.filter((row) => row.pinned === drag.pinned);
2310
+ const sourceIndex = section.findIndex((row) => row.id === source.id);
2311
+ if (section.filter((row) => row.id !== source.id).findIndex((row) => row.id === target.id) + (over.half === "after" ? 1 : 0) === sourceIndex) return;
2312
+ const next = order.filter((id) => id !== source.id);
2313
+ const targetIndex = next.indexOf(target.id);
2314
+ if (targetIndex === -1) return;
2315
+ next.splice(targetIndex + (over.half === "after" ? 1 : 0), 0, source.id);
2316
+ return pinCurrentBlank(next, rows.find((row) => row.blank)?.id);
2317
+ }
1582
2318
  /** Resolve an insertion side across the Workspace header, descendants, and Sessions. */
1583
2319
  function workspaceGroupHalf(e) {
1584
2320
  const rect = e.currentTarget.getBoundingClientRect();
1585
2321
  return e.clientY < rect.top + rect.height / 2 ? "before" : "after";
1586
2322
  }
1587
- /** The scrolling session tree; unmounting drops the sessions subscription and expand-all state. */
1588
- function SessionTree({ list, useSessionStatus, startSession, open, forkSession, workspaces, ungroupedSessionIds, archivedSessionIds, workspaceReady, usePanelInfo, onRenameRequest, onDeleteRequest, onSessionRename, onSessionArchive, insertWorkspaceBefore, nestWorkspaces, groupExpansion, setGroupExpanded, setSessionOrder, home, t, revealSessionId, onSessionRevealed }) {
2323
+ /** The scrolling session tree; unmounting drops the sessions subscription and local row limits. */
2324
+ function SessionTree({ list, useSessionStatus, startSession, open, workspaces, ungroupedSessionIds, rowState, workspaceReady, animationResetKey, usePanelInfo, onRenameRequest, onDeleteRequest, onSessionRenameRequest, renderSlot, insertWorkspaceBefore, nestWorkspaces, groupExpansion, setGroupExpanded, setSessionOrder, home, t, revealSessionId, onSessionRevealed }) {
1589
2325
  const panelActive = usePanelInfo((info) => info.activePanelId !== null);
1590
2326
  const statuses = useSessionStatus((s) => s);
1591
2327
  const current = panelActive ? void 0 : Object.values(list.byId).find((session) => (session.retainedBy.mainView ?? 0) > 0)?.id;
1592
2328
  const revealGroup = revealSessionId === void 0 || !workspaceReady ? void 0 : owningGroupKey(workspaces, revealSessionId);
1593
- const [expandedSessionGroups, setExpandedSessionGroups] = (0, react.useState)([]);
2329
+ const [sessionLimits, setSessionLimits] = (0, react.useState)({});
1594
2330
  const [drag, setDrag] = (0, react.useState)(null);
1595
2331
  const sessionDropCommitted = (0, react.useRef)(false);
1596
2332
  const [workspaceDrag, setWorkspaceDrag] = (0, react.useState)(null);
1597
2333
  const workspaceDropCommitted = (0, react.useRef)(false);
1598
- useNativeDragAcceptance(drag !== null || workspaceDrag !== null);
2334
+ const nativeDragActive = drag !== null || workspaceDrag !== null;
2335
+ useNativeDragAcceptance(nativeDragActive);
1599
2336
  const currentGroup = current === void 0 || !workspaceReady ? void 0 : owningGroupKey(workspaces, current);
1600
2337
  (0, react.useEffect)(() => {
1601
2338
  if (current === void 0 || currentGroup === void 0 || Object.hasOwn(groupExpansion, currentGroup)) return;
@@ -1628,13 +2365,13 @@ window.__ModuleLoader__.load({
1628
2365
  parents,
1629
2366
  workspaces
1630
2367
  ]);
1631
- const groups = (0, react.useMemo)(() => deriveGroups(list, workspaces, archivedSessionIds, statuses, {
2368
+ const groups = (0, react.useMemo)(() => deriveGroups(list, workspaces, rowState, statuses, {
1632
2369
  expandedGroups,
1633
2370
  ungroupedOrder: ungroupedSessionIds
1634
2371
  }), [
1635
2372
  list,
1636
2373
  workspaces,
1637
- archivedSessionIds,
2374
+ rowState,
1638
2375
  statuses,
1639
2376
  expandedGroups,
1640
2377
  ungroupedSessionIds
@@ -1652,7 +2389,10 @@ window.__ModuleLoader__.load({
1652
2389
  const group = groups.find((candidate) => candidate.key === revealGroup);
1653
2390
  if (group === void 0 || !group.expanded || !group.sessions.some((row) => row.id === revealSessionId)) return;
1654
2391
  if (collapsedSessionRows(group.sessions).rows.some((row) => row.id === revealSessionId)) return;
1655
- setExpandedSessionGroups((keys) => keys.includes(revealGroup) ? keys : [...keys, revealGroup]);
2392
+ setSessionLimits((limits) => limits[revealGroup] === Infinity ? limits : {
2393
+ ...limits,
2394
+ [revealGroup]: Infinity
2395
+ });
1656
2396
  }, [
1657
2397
  groups,
1658
2398
  revealGroup,
@@ -1665,42 +2405,12 @@ window.__ModuleLoader__.load({
1665
2405
  setDrag(null);
1666
2406
  const group = groups.find((candidate) => candidate.key === activeDrag.accountKey);
1667
2407
  if (group === void 0) return;
1668
- const sessionsExpanded = expandedSessionGroups.includes(group.key);
1669
- const renderedSessions = sessionsExpanded ? group.sessions : collapsedSessionRows(group.sessions).rows;
1670
- const targetIndex = renderedSessions.findIndex((session) => session.id === over.id);
1671
- if (targetIndex === -1) return;
1672
- const sourceIndex = renderedSessions.findIndex((session) => session.id === activeDrag.sessionId);
1673
2408
  if (over.id === activeDrag.sessionId) return;
1674
- const withoutSource = renderedSessions.filter((session) => session.id !== activeDrag.sessionId);
1675
- const targetWithoutSourceIndex = withoutSource.findIndex((session) => session.id === over.id);
1676
- if (targetWithoutSourceIndex === -1) return;
1677
- const visibleInsertAt = over.half === "before" ? targetWithoutSourceIndex : targetWithoutSourceIndex + 1;
1678
- if (sourceIndex !== -1 && visibleInsertAt === sourceIndex) return;
1679
2409
  const accountSessionIds = activeDrag.accountKey === "" ? ungroupedSessionIds : workspaces.find((workspace) => workspace.workspaceId === activeDrag.accountKey)?.sessionIds;
1680
- if (accountSessionIds === void 0 || !accountSessionIds.includes(activeDrag.sessionId)) return;
1681
- const nextOrder = accountSessionIds.filter((id) => id !== activeDrag.sessionId);
1682
- let anchor;
1683
- if (sessionsExpanded) anchor = over.half === "before" ? over.id : renderedSessions[targetIndex + 1]?.id;
1684
- else {
1685
- const previousVisible = withoutSource[visibleInsertAt - 1]?.id;
1686
- if (previousVisible === void 0) anchor = nextOrder[0];
1687
- else {
1688
- const previousIndex = nextOrder.indexOf(previousVisible);
1689
- if (previousIndex === -1) return;
1690
- anchor = nextOrder[previousIndex + 1];
1691
- }
1692
- }
1693
- const insertAt = anchor === void 0 ? nextOrder.length : nextOrder.indexOf(anchor);
1694
- nextOrder.splice(insertAt === -1 ? nextOrder.length : insertAt, 0, activeDrag.sessionId);
1695
- if (!sessionsExpanded && sourceIndex !== -1) {
1696
- const nodes = new Map(group.sessions.map((node) => [node.id, node]));
1697
- if (!collapsedSessionRows(nextOrder.flatMap((id) => {
1698
- const node = nodes.get(id);
1699
- return node === void 0 ? [] : [node];
1700
- })).rows.some((node) => node.id === activeDrag.sessionId)) return;
1701
- }
1702
- const currentBlank = group.sessions.find((node) => node.blank)?.id;
1703
- setSessionOrder(activeDrag.accountKey, pinCurrentBlank(nextOrder, currentBlank));
2410
+ if (accountSessionIds === void 0) return;
2411
+ const renderedSessions = collapsedSessionRows(group.sessions, sessionLimits[group.key]).rows;
2412
+ const nextOrder = sessionDragOrder(accountSessionIds, renderedSessions, activeDrag, over);
2413
+ if (nextOrder !== void 0) setSessionOrder(activeDrag.accountKey, nextOrder);
1704
2414
  };
1705
2415
  const commitWorkspaceDrag = (activeDrag, over) => {
1706
2416
  if (workspaceDropCommitted.current) return;
@@ -1731,12 +2441,19 @@ window.__ModuleLoader__.load({
1731
2441
  }, [groups, parents]);
1732
2442
  const rootGroups = childrenByParent.get(void 0) ?? [];
1733
2443
  const workspaceDropAtListStart = rootGroups[0]?.workspaceId !== void 0 && workspaceDrag?.over?.id === rootGroups[0].workspaceId && workspaceDrag.over.half === "before";
2444
+ const rowKeys = groups.length === 0 ? ["empty"] : [];
1734
2445
  const renderGroup = (group, depth) => {
1735
2446
  const workspaceId = group.workspaceId;
1736
2447
  const children = childrenByParent.get(group.key) ?? [];
1737
2448
  const compatibleDrag = workspaceDrag !== null && parents.get(workspaceDrag.workspaceId) === parents.get(group.key);
1738
2449
  const collapsed = collapsedSessionRows(group.sessions);
1739
- const sessionsExpanded = expandedSessionGroups.includes(group.key);
2450
+ const visible = collapsedSessionRows(group.sessions, sessionLimits[group.key]);
2451
+ const sessionsExpanded = visible.hiddenCount === 0;
2452
+ rowKeys.push(`workspace:${group.key}`);
2453
+ const childRows = group.expanded ? children.map((child) => renderGroup(child, depth + 1)) : [];
2454
+ const sessions = visible.rows;
2455
+ for (const node of sessions) rowKeys.push(`session:${node.id}`);
2456
+ if (collapsed.hiddenCount > 0) rowKeys.push(`overflow:${group.key}`);
1740
2457
  const workspaceMarker = workspaceId !== void 0 && workspaceDrag?.over?.id === workspaceId ? workspaceDrag.over.half : null;
1741
2458
  const workspaceDragProps = workspaceId === void 0 ? void 0 : {
1742
2459
  start: () => {
@@ -1801,7 +2518,10 @@ window.__ModuleLoader__.load({
1801
2518
  home,
1802
2519
  t,
1803
2520
  onToggle: () => {
1804
- if (group.expanded) setExpandedSessionGroups((keys) => keys.filter((key) => key !== group.key));
2521
+ if (group.expanded) setSessionLimits((limits) => ({
2522
+ ...limits,
2523
+ [group.key]: COLLAPSED_SESSION_LIMIT
2524
+ }));
1805
2525
  setGroupExpanded(group.key, !group.expanded);
1806
2526
  },
1807
2527
  onCreate: () => {
@@ -1822,12 +2542,13 @@ window.__ModuleLoader__.load({
1822
2542
  }
1823
2543
  }
1824
2544
  }),
1825
- group.expanded && children.length > 0 && (0, react_jsx_runtime.jsx)("div", {
2545
+ childRows.length > 0 && (0, react_jsx_runtime.jsx)("div", {
1826
2546
  role: "group",
1827
- children: children.map((child) => renderGroup(child, depth + 1))
2547
+ children: childRows
1828
2548
  }),
1829
- (sessionsExpanded ? group.sessions : collapsed.rows).map((node) => {
2549
+ sessions.map((node) => {
1830
2550
  const sameGroupDrag = drag !== null && drag.accountKey === group.key;
2551
+ const compatibleTarget = sameGroupDrag && drag.pinned === node.pinned;
1831
2552
  const normalizeHalf = (half) => node.blank ? "after" : half;
1832
2553
  const dragProps = {
1833
2554
  start: () => {
@@ -1835,10 +2556,11 @@ window.__ModuleLoader__.load({
1835
2556
  setDrag({
1836
2557
  accountKey: group.key,
1837
2558
  sessionId: node.id,
2559
+ pinned: node.pinned,
1838
2560
  over: null
1839
2561
  });
1840
2562
  },
1841
- active: sameGroupDrag,
2563
+ active: compatibleTarget,
1842
2564
  marker: sameGroupDrag && drag.over?.id === node.id ? drag.over.half : null,
1843
2565
  hover: (half) => {
1844
2566
  /* v8 ignore next -- narrowing guard: Rows gates hover on `active`, which is false while the drag state is null. */
@@ -1869,9 +2591,8 @@ window.__ModuleLoader__.load({
1869
2591
  currentId: current,
1870
2592
  now,
1871
2593
  onOpen: open,
1872
- onRename: onSessionRename,
1873
- onFork: forkSession,
1874
- onArchive: onSessionArchive,
2594
+ onRenameRequest: onSessionRenameRequest,
2595
+ renderSlot,
1875
2596
  onReveal: node.id === revealSessionId && group.key === revealGroup ? () => {
1876
2597
  onSessionRevealed(node.id);
1877
2598
  } : void 0,
@@ -1882,15 +2603,20 @@ window.__ModuleLoader__.load({
1882
2603
  collapsed.hiddenCount > 0 && (0, react_jsx_runtime.jsx)("button", {
1883
2604
  type: "button",
1884
2605
  className: WorkspaceBrowser_module_css_default.sessionOverflowButton,
2606
+ "data-row-key": `overflow:${group.key}`,
1885
2607
  "aria-expanded": sessionsExpanded,
1886
2608
  onClick: () => {
1887
- setExpandedSessionGroups((keys) => toggled(keys, group.key));
2609
+ setSessionLimits((limits) => ({
2610
+ ...limits,
2611
+ [group.key]: sessionsExpanded ? COLLAPSED_SESSION_LIMIT : visible.hiddenCount <= COLLAPSED_SESSION_LIMIT ? Infinity : (limits[group.key] ?? COLLAPSED_SESSION_LIMIT) + COLLAPSED_SESSION_LIMIT
2612
+ }));
1888
2613
  },
1889
- children: sessionsExpanded ? t("sessions.collapse") : t("sessions.expand", { n: collapsed.hiddenCount })
2614
+ children: sessionsExpanded ? t("sessions.collapse") : t("sessions.expand", { n: visible.hiddenCount })
1890
2615
  })
1891
2616
  ]
1892
2617
  }, group.key);
1893
2618
  };
2619
+ const groupRows = rootGroups.map((group) => renderGroup(group, 0));
1894
2620
  return (0, react_jsx_runtime.jsxs)("div", {
1895
2621
  className: clsx(WorkspaceBrowser_module_css_default.treeBody, WorkspaceBrowser_module_css_default.wide),
1896
2622
  children: [
@@ -1898,26 +2624,30 @@ window.__ModuleLoader__.load({
1898
2624
  className: WorkspaceBrowser_module_css_default.listTopDropIndicator,
1899
2625
  "aria-hidden": "true"
1900
2626
  }),
1901
- (0, react_jsx_runtime.jsxs)("div", {
2627
+ (0, react_jsx_runtime.jsxs)(AnimatedRows, {
1902
2628
  className: clsx(WorkspaceBrowser_module_css_default.list, workspaceDropAtListStart && WorkspaceBrowser_module_css_default.listTopDropActive),
1903
- role: "tree",
1904
- "aria-label": t("section.sessions"),
2629
+ label: t("section.sessions"),
2630
+ rowKeys,
2631
+ ready: list.phase === "ready" && workspaceReady && !nativeDragActive,
2632
+ resetKey: JSON.stringify([animationResetKey, sessionLimits]),
1905
2633
  children: [groups.length === 0 && (0, react_jsx_runtime.jsx)("div", {
1906
2634
  className: WorkspaceBrowser_module_css_default.empty,
2635
+ "data-row-key": "empty",
1907
2636
  children: t("empty.none")
1908
- }), rootGroups.map((group) => renderGroup(group, 0))]
2637
+ }), groupRows]
1909
2638
  }),
1910
2639
  (0, react_jsx_runtime.jsx)("span", { className: WorkspaceBrowser_module_css_default.fade })
1911
2640
  ]
1912
2641
  });
1913
2642
  }
1914
2643
  /** The flat "In one list" body: every session is one draggable top-level row. */
1915
- function FlatList({ list, sessionIds, useSessionStatus, open, forkSession, onSessionRename, onSessionArchive, usePanelInfo, setSessionOrder, revealSessionId, onSessionRevealed, t }) {
2644
+ function FlatList({ list, sessionIds, rowState, useSessionStatus, open, onSessionRenameRequest, renderSlot, usePanelInfo, setSessionOrder, workspaceReady, animationResetKey, revealSessionId, onSessionRevealed, t }) {
1916
2645
  const panelActive = usePanelInfo((info) => info.activePanelId !== null);
1917
2646
  const statuses = useSessionStatus((s) => s);
1918
- const rows = (0, react.useMemo)(() => deriveFlat(list, sessionIds, statuses), [
2647
+ const rows = (0, react.useMemo)(() => deriveFlat(list, sessionIds, rowState, statuses), [
1919
2648
  list,
1920
2649
  sessionIds,
2650
+ rowState,
1921
2651
  statuses
1922
2652
  ]);
1923
2653
  const [drag, setDrag] = (0, react.useState)(null);
@@ -1928,40 +2658,32 @@ window.__ModuleLoader__.load({
1928
2658
  if (dropCommitted.current) return;
1929
2659
  dropCommitted.current = true;
1930
2660
  setDrag(null);
1931
- const targetIndex = rows.findIndex((row) => row.id === over.id);
1932
- if (targetIndex === -1) return;
1933
- const anchor = over.half === "before" ? over.id : rows[targetIndex + 1]?.id;
1934
- if (anchor === activeDrag.sessionId) return;
1935
- const sourceIndex = rows.findIndex((row) => row.id === activeDrag.sessionId);
1936
- const anchorIndex = anchor === void 0 ? rows.length : rows.findIndex((row) => row.id === anchor);
1937
- if (sourceIndex !== -1 && (anchorIndex === sourceIndex || anchorIndex === sourceIndex + 1)) return;
1938
- const nextOrder = rows.map((row) => row.id).filter((id) => id !== activeDrag.sessionId);
1939
- const insertAt = anchor === void 0 ? nextOrder.length : nextOrder.indexOf(anchor);
1940
- nextOrder.splice(insertAt === -1 ? nextOrder.length : insertAt, 0, activeDrag.sessionId);
1941
- const currentBlank = rows.find((node) => node.blank)?.id;
1942
- setSessionOrder(FLAT_SESSION_ORDER_KEY, pinCurrentBlank(nextOrder, currentBlank));
2661
+ const nextOrder = sessionDragOrder(sessionIds, rows, activeDrag, over);
2662
+ if (nextOrder !== void 0) setSessionOrder(FLAT_SESSION_ORDER_KEY, nextOrder);
1943
2663
  };
1944
2664
  const now = Date.now();
1945
2665
  return (0, react_jsx_runtime.jsxs)("div", {
1946
2666
  className: clsx(WorkspaceBrowser_module_css_default.treeBody, WorkspaceBrowser_module_css_default.wide),
1947
- children: [(0, react_jsx_runtime.jsxs)("div", {
2667
+ children: [(0, react_jsx_runtime.jsxs)(AnimatedRows, {
1948
2668
  className: clsx(WorkspaceBrowser_module_css_default.list, WorkspaceBrowser_module_css_default.flatList),
1949
- role: "tree",
1950
- "aria-label": t("section.sessions"),
2669
+ label: t("section.sessions"),
2670
+ rowKeys: rows.length === 0 ? ["empty"] : rows.map((row) => `session:${row.id}`),
2671
+ ready: list.phase === "ready" && workspaceReady && drag === null,
2672
+ resetKey: animationResetKey,
1951
2673
  children: [rows.length === 0 && (0, react_jsx_runtime.jsx)("div", {
1952
2674
  className: WorkspaceBrowser_module_css_default.empty,
2675
+ "data-row-key": "empty",
1953
2676
  children: t("empty.none")
1954
2677
  }), rows.map((node) => {
1955
- const active = drag !== null;
2678
+ const active = drag !== null && drag.pinned === node.pinned;
1956
2679
  const normalizeHalf = (half) => node.blank ? "after" : half;
1957
2680
  return (0, react_jsx_runtime.jsx)(SessionNodeItem, {
1958
2681
  node,
1959
2682
  currentId,
1960
2683
  now,
1961
2684
  onOpen: open,
1962
- onRename: onSessionRename,
1963
- onFork: forkSession,
1964
- onArchive: onSessionArchive,
2685
+ onRenameRequest: onSessionRenameRequest,
2686
+ renderSlot,
1965
2687
  onReveal: node.id === revealSessionId ? () => {
1966
2688
  onSessionRevealed(node.id);
1967
2689
  } : void 0,
@@ -1972,6 +2694,7 @@ window.__ModuleLoader__.load({
1972
2694
  setDrag({
1973
2695
  accountKey: FLAT_SESSION_ORDER_KEY,
1974
2696
  sessionId: node.id,
2697
+ pinned: node.pinned,
1975
2698
  over: null
1976
2699
  });
1977
2700
  },
@@ -2005,7 +2728,7 @@ window.__ModuleLoader__.load({
2005
2728
  });
2006
2729
  }
2007
2730
  /** Flat search body: local metadata matches plus the current Host result page. */
2008
- function SearchResults({ useSessions, useSessionStatus, open, workspaces, archivedSessionIds, query, remote, resultLimit, usePanelInfo, t }) {
2731
+ function SearchResults({ useSessions, useSessionStatus, open, onUnarchive, workspaces, archivedSessionIds, archivedFilter, query, remote, resultLimit, usePanelInfo, t }) {
2009
2732
  const panelActive = usePanelInfo((info) => info.activePanelId !== null);
2010
2733
  const list = useSessions((s) => s);
2011
2734
  const statuses = useSessionStatus((s) => s);
@@ -2015,17 +2738,17 @@ window.__ModuleLoader__.load({
2015
2738
  items: [],
2016
2739
  hasMore: false
2017
2740
  };
2018
- const results = (0, react.useMemo)(() => deriveSearchResults(list, workspaces, query, archivedSessionIds, statuses, currentRemote, resultLimit), [
2741
+ const results = (0, react.useMemo)(() => deriveSearchResults(list, workspaces, query, archivedSessionIds, archivedFilter, statuses, currentRemote, resultLimit), [
2019
2742
  list,
2020
2743
  workspaces,
2021
2744
  query,
2022
2745
  archivedSessionIds,
2746
+ archivedFilter,
2023
2747
  statuses,
2024
2748
  currentRemote,
2025
2749
  resultLimit
2026
2750
  ]);
2027
2751
  const pending = currentRemote.status === "loading";
2028
- const failed = currentRemote.status === "error";
2029
2752
  const currentId = panelActive ? void 0 : Object.values(list.byId).find((session) => (session.retainedBy.mainView ?? 0) > 0)?.id;
2030
2753
  return (0, react_jsx_runtime.jsxs)("div", {
2031
2754
  className: clsx(WorkspaceBrowser_module_css_default.treeBody, WorkspaceBrowser_module_css_default.wide),
@@ -2040,18 +2763,21 @@ window.__ModuleLoader__.load({
2040
2763
  result,
2041
2764
  currentId,
2042
2765
  onOpen: open,
2766
+ onUnarchive,
2043
2767
  t
2044
2768
  }, result.id))
2045
2769
  }),
2046
2770
  pending && (0, react_jsx_runtime.jsx)("div", {
2047
- className: WorkspaceBrowser_module_css_default.searchStatus,
2048
- role: "status",
2049
- children: t("search.pending")
2050
- }),
2051
- failed && (0, react_jsx_runtime.jsx)("div", {
2052
- className: WorkspaceBrowser_module_css_default.searchWarning,
2053
2771
  role: "status",
2054
- children: t("search.unavailable")
2772
+ "aria-label": t("search.pending"),
2773
+ children: (results.items.length === 0 ? [0, 1] : [0]).map((i) => (0, react_jsx_runtime.jsxs)("div", {
2774
+ className: WorkspaceBrowser_module_css_default.skeletonRow,
2775
+ "aria-hidden": "true",
2776
+ children: [(0, react_jsx_runtime.jsx)("span", { className: WorkspaceBrowser_module_css_default.skeletonDot }), (0, react_jsx_runtime.jsxs)("span", {
2777
+ className: WorkspaceBrowser_module_css_default.skeletonBars,
2778
+ children: [(0, react_jsx_runtime.jsx)("span", { className: WorkspaceBrowser_module_css_default.skeletonBar }), (0, react_jsx_runtime.jsx)("span", { className: clsx(WorkspaceBrowser_module_css_default.skeletonBar, WorkspaceBrowser_module_css_default.skeletonBarWide) })]
2779
+ })]
2780
+ }, i))
2055
2781
  }),
2056
2782
  !pending && results.items.length === 0 && (0, react_jsx_runtime.jsx)("div", {
2057
2783
  className: WorkspaceBrowser_module_css_default.empty,
@@ -2070,18 +2796,27 @@ window.__ModuleLoader__.load({
2070
2796
  * @param props - composed slot props (shell owner share + store + injected actions).
2071
2797
  * @returns the region element tree.
2072
2798
  */
2073
- function WorkspaceBrowser({ wide, usePanelInfo, expandSidebar, useSessions, useSessionStatus, useWorkspaces, useStore, actions, startSession, open, renameSession, forkSession, renameWorkspace, deleteWorkspace, insertWorkspaceBefore, archiveSession, createWorkspace, searchSessions, searchResultLimit, useDirectoryFlow, useHostInfo, renderSlot, t }) {
2799
+ function WorkspaceBrowser({ wide, usePanelInfo, expandSidebar, useSessions, useSessionStatus, useWorkspaces, useStore, actions, startSession, open, requestSessionRename, notifyArchivedNotOpenable, renameWorkspace, deleteWorkspace, insertWorkspaceBefore, unarchiveSession, createWorkspace, searchSessions, searchResultLimit, useDirectoryFlow, useHostInfo, renderSlot, t }) {
2074
2800
  const home = useHostInfo((info) => info.home);
2075
2801
  const list = useSessions((state) => state);
2076
2802
  const workspaces = useWorkspaces((state) => state.items);
2077
2803
  const workspacePhase = useWorkspaces((state) => state.phase);
2078
2804
  const workspaceStreamState = useWorkspaces((state) => state.state);
2079
2805
  const archivedSessionIds = useWorkspaces((state) => state.archivedSessionIds);
2806
+ const pinnedSessionIds = useWorkspaces((state) => state.pinnedSessionIds);
2080
2807
  const directoryFlowAvailable = useDirectoryFlow((occupied) => occupied);
2081
2808
  const groupBy = useStore((s) => s.groupBy);
2082
2809
  const orderBy = useStore((s) => s.orderBy);
2810
+ const archivedFilter = useStore((s) => s.archivedFilter ?? "default");
2083
2811
  const groupExpansion = useStore((s) => s.groupExpansion);
2084
2812
  const sessionOrderByAccount = useStore((s) => s.sessionOrderByAccount);
2813
+ const guardedOpen = (sessionId) => {
2814
+ if (archivedSessionIds.includes(sessionId)) {
2815
+ notifyArchivedNotOpenable();
2816
+ return;
2817
+ }
2818
+ open(sessionId);
2819
+ };
2085
2820
  const workspaceReady = workspacePhase === "ready" && workspaceStreamState !== "loading";
2086
2821
  const mainSessionId = Object.values(list.byId).find((session) => (session.retainedBy.mainView ?? 0) > 0)?.id;
2087
2822
  const currentBlank = mainSessionId !== void 0 && list.byId[mainSessionId]?.blank === true ? mainSessionId : void 0;
@@ -2089,10 +2824,18 @@ window.__ModuleLoader__.load({
2089
2824
  const accounted = new Set(workspaces.flatMap((workspace) => workspace.sessionIds));
2090
2825
  return list.ids.filter((id) => list.byId[id] !== void 0 && !accounted.has(id));
2091
2826
  }, [list, workspaces]);
2092
- const flatMemberIds = (0, react.useMemo)(() => visibleSessionIds(list, archivedSessionIds), [archivedSessionIds, list]);
2827
+ const orderState = (0, react.useMemo)(() => ({
2828
+ pinnedSessionIds,
2829
+ archivedSessionIds
2830
+ }), [archivedSessionIds, pinnedSessionIds]);
2831
+ const rowState = (0, react.useMemo)(() => ({
2832
+ ...orderState,
2833
+ archivedFilter
2834
+ }), [orderState, archivedFilter]);
2835
+ const flatMemberIds = (0, react.useMemo)(() => sessionMemberIds(list), [list]);
2093
2836
  const orderedWorkspaces = (0, react.useMemo)(() => workspaces.map((workspace) => {
2094
2837
  const memberIds = workspace.sessionIds;
2095
- const baseOrder = orderBy === "updated" ? orderByRecency(memberIds, list.byId) : reconcileManualOrder(memberIds, sessionOrderByAccount[workspace.workspaceId], list.byId);
2838
+ const baseOrder = orderBy === "updated" ? orderByRecency(memberIds, list.byId) : reconcileManualOrder(memberIds, sessionOrderByAccount[workspace.workspaceId], list.byId, orderState);
2096
2839
  return {
2097
2840
  ...workspace,
2098
2841
  sessionIds: pinCurrentBlank(baseOrder, currentBlank !== void 0 && memberIds.includes(currentBlank) ? currentBlank : void 0)
@@ -2101,25 +2844,28 @@ window.__ModuleLoader__.load({
2101
2844
  currentBlank,
2102
2845
  list.byId,
2103
2846
  orderBy,
2847
+ orderState,
2104
2848
  sessionOrderByAccount,
2105
2849
  workspaces
2106
2850
  ]);
2107
2851
  const orderedUngroupedSessionIds = (0, react.useMemo)(() => {
2108
- return pinCurrentBlank(orderBy === "updated" ? orderByRecency(ungroupedMemberIds, list.byId) : reconcileManualOrder(ungroupedMemberIds, sessionOrderByAccount[""], list.byId), currentBlank !== void 0 && ungroupedMemberIds.includes(currentBlank) ? currentBlank : void 0);
2852
+ return pinCurrentBlank(orderBy === "updated" ? orderByRecency(ungroupedMemberIds, list.byId) : reconcileManualOrder(ungroupedMemberIds, sessionOrderByAccount[""], list.byId, orderState), currentBlank !== void 0 && ungroupedMemberIds.includes(currentBlank) ? currentBlank : void 0);
2109
2853
  }, [
2110
2854
  currentBlank,
2111
2855
  list.byId,
2112
2856
  orderBy,
2857
+ orderState,
2113
2858
  sessionOrderByAccount,
2114
2859
  ungroupedMemberIds
2115
2860
  ]);
2116
2861
  const orderedFlatSessionIds = (0, react.useMemo)(() => {
2117
- 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);
2862
+ return pinCurrentBlank(orderBy === "updated" ? orderByRecency(flatMemberIds, list.byId) : reconcileManualOrder(flatMemberIds, sessionOrderByAccount[FLAT_SESSION_ORDER_KEY], list.byId, orderState), currentBlank !== void 0 && flatMemberIds.includes(currentBlank) ? currentBlank : void 0);
2118
2863
  }, [
2119
2864
  currentBlank,
2120
2865
  flatMemberIds,
2121
2866
  list.byId,
2122
2867
  orderBy,
2868
+ orderState,
2123
2869
  sessionOrderByAccount
2124
2870
  ]);
2125
2871
  const activeSessionOrders = (0, react.useMemo)(() => Object.fromEntries([
@@ -2164,15 +2910,12 @@ window.__ModuleLoader__.load({
2164
2910
  workspaceReady
2165
2911
  ]);
2166
2912
  (0, react.useEffect)(() => {
2167
- if (list.phase !== "ready" || !workspaceReady || orderBy !== "manual") return;
2168
- const changed = Object.fromEntries(Object.entries(activeSessionOrders).filter(([key, ids]) => {
2169
- const saved = sessionOrderByAccount[key];
2170
- return saved === void 0 || saved.length !== ids.length || ids.some((id, index) => id !== saved[index]);
2171
- }));
2172
- if (Object.keys(changed).length > 0) actions.syncSessionOrders(changed);
2913
+ if (list.phase !== "ready" || !workspaceReady || orderBy !== "manual" || currentBlank === void 0) return;
2914
+ if (Object.entries(activeSessionOrders).some(([key, ids]) => ids[0] === currentBlank && sessionOrderByAccount[key]?.[0] !== currentBlank)) actions.syncSessionOrders(activeSessionOrders);
2173
2915
  }, [
2174
2916
  actions.syncSessionOrders,
2175
2917
  activeSessionOrders,
2918
+ currentBlank,
2176
2919
  list.phase,
2177
2920
  orderBy,
2178
2921
  sessionOrderByAccount,
@@ -2197,6 +2940,10 @@ window.__ModuleLoader__.load({
2197
2940
  const wsPlusRef = (0, react.useRef)(null);
2198
2941
  const composingRef = (0, react.useRef)(false);
2199
2942
  const openSearchResult = (sessionId) => {
2943
+ if (archivedSessionIds.includes(sessionId)) {
2944
+ notifyArchivedNotOpenable();
2945
+ return;
2946
+ }
2200
2947
  setRevealSessionId(sessionId);
2201
2948
  setQuery("");
2202
2949
  setSearchExpanded(false);
@@ -2311,40 +3058,9 @@ window.__ModuleLoader__.load({
2311
3058
  setRenameError(reason instanceof Error ? reason.message : String(reason));
2312
3059
  });
2313
3060
  };
2314
- const [sessionRenameTarget, setSessionRenameTarget] = (0, react.useState)(null);
2315
- const [sessionRenameDraft, setSessionRenameDraft] = (0, react.useState)("");
2316
- const [sessionRenaming, setSessionRenaming] = (0, react.useState)(false);
2317
- const [sessionRenameError, setSessionRenameError] = (0, react.useState)(null);
2318
- const sessionRenameTrimmed = sessionRenameDraft.trim();
2319
- const sessionRenameBlocked = sessionRenaming || sessionRenameTrimmed === "" || sessionRenameTarget === null;
2320
- const closeSessionRename = () => {
2321
- if (sessionRenaming) return;
2322
- setSessionRenameTarget(null);
2323
- setSessionRenameError(null);
2324
- };
2325
- const confirmSessionRename = () => {
2326
- if (sessionRenameBlocked) return;
2327
- setSessionRenaming(true);
2328
- setSessionRenameError(null);
2329
- renameSession(sessionRenameTarget.sessionId, sessionRenameTrimmed).then(() => {
2330
- setSessionRenaming(false);
2331
- setSessionRenameTarget(null);
2332
- }).catch((reason) => {
2333
- setSessionRenaming(false);
2334
- setSessionRenameError(reason instanceof Error ? reason.message : String(reason));
2335
- });
2336
- };
2337
- const onSessionRename = (sessionId, currentTitle) => {
2338
- setSessionRenameTarget({
2339
- sessionId,
2340
- currentTitle
2341
- });
2342
- setSessionRenameDraft(currentTitle);
2343
- setSessionRenameError(null);
2344
- };
2345
- const onSessionArchive = (sessionId) => {
2346
- archiveSession(sessionId).catch((reason) => {
2347
- console.warn("session archive rejected:", reason);
3061
+ const onSessionUnarchive = (sessionId) => {
3062
+ unarchiveSession(sessionId).catch((reason) => {
3063
+ console.warn("session unarchive rejected:", reason);
2348
3064
  });
2349
3065
  };
2350
3066
  const [deleteTarget, setDeleteTarget] = (0, react.useState)(null);
@@ -2410,7 +3126,7 @@ window.__ModuleLoader__.load({
2410
3126
  setWsPickerOpen(false);
2411
3127
  setSearchExpanded(true);
2412
3128
  },
2413
- children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16, { size: searchExpanded ? 11 : 14 })
3129
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutlineRegular, { size: searchExpanded ? 11 : 14 })
2414
3130
  })
2415
3131
  }),
2416
3132
  (0, react_jsx_runtime.jsx)("input", {
@@ -2439,7 +3155,7 @@ window.__ModuleLoader__.load({
2439
3155
  setQuery("");
2440
3156
  setSearchExpanded(false);
2441
3157
  },
2442
- children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseFill14, {})
3158
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconCloseFillRegular, {})
2443
3159
  })
2444
3160
  ]
2445
3161
  })
@@ -2449,12 +3165,12 @@ window.__ModuleLoader__.load({
2449
3165
  children: [wide && (0, react_jsx_runtime.jsx)(ViewOptionsMenu, {
2450
3166
  groupBy,
2451
3167
  orderBy,
2452
- onGroupPick: (mode) => {
2453
- actions.setGroupBy(mode);
2454
- },
3168
+ archivedFilter,
3169
+ onGroupPick: actions.setGroupBy,
2455
3170
  onOrderPick: (mode) => {
2456
3171
  actions.setOrderBy(mode, activeSessionOrders);
2457
3172
  },
3173
+ onArchivedFilterPick: actions.setArchivedFilter,
2458
3174
  t
2459
3175
  }), directoryFlowAvailable && (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
2460
3176
  label: t("workspace.add"),
@@ -2468,7 +3184,7 @@ window.__ModuleLoader__.load({
2468
3184
  onClick: () => {
2469
3185
  setWsPickerOpen((v) => !v);
2470
3186
  },
2471
- children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconProjectAddOutline16, { size: wide ? 16 : 18 })
3187
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconProjectAddOutlineRegular, { size: wide ? 16 : 18 })
2472
3188
  })
2473
3189
  })]
2474
3190
  }),
@@ -2505,7 +3221,7 @@ window.__ModuleLoader__.load({
2505
3221
  setSearchOnExpand(true);
2506
3222
  expandSidebar();
2507
3223
  },
2508
- children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16, { size: 18 })
3224
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutlineRegular, { size: 18 })
2509
3225
  })
2510
3226
  })
2511
3227
  }),
@@ -2516,8 +3232,10 @@ window.__ModuleLoader__.load({
2516
3232
  useSessions,
2517
3233
  useSessionStatus,
2518
3234
  open: openSearchResult,
3235
+ onUnarchive: onSessionUnarchive,
2519
3236
  workspaces,
2520
3237
  archivedSessionIds,
3238
+ archivedFilter,
2521
3239
  query: normalizedQuery,
2522
3240
  remote: remoteSearch,
2523
3241
  resultLimit: searchResultLimit,
@@ -2526,11 +3244,13 @@ window.__ModuleLoader__.load({
2526
3244
  usePanelInfo,
2527
3245
  list,
2528
3246
  sessionIds: orderedFlatSessionIds,
3247
+ rowState,
3248
+ workspaceReady,
3249
+ animationResetKey: `${groupBy}/${orderBy}/${archivedFilter}`,
2529
3250
  useSessionStatus,
2530
- open,
2531
- forkSession,
2532
- onSessionRename,
2533
- onSessionArchive,
3251
+ open: guardedOpen,
3252
+ onSessionRenameRequest: requestSessionRename,
3253
+ renderSlot,
2534
3254
  setSessionOrder: saveSessionOrder,
2535
3255
  revealSessionId,
2536
3256
  onSessionRevealed: acknowledgeSessionReveal,
@@ -2539,19 +3259,19 @@ window.__ModuleLoader__.load({
2539
3259
  usePanelInfo,
2540
3260
  list,
2541
3261
  useSessionStatus,
2542
- onSessionRename,
2543
- onSessionArchive,
2544
- forkSession,
3262
+ onSessionRenameRequest: requestSessionRename,
3263
+ renderSlot,
2545
3264
  workspaces: orderedWorkspaces,
2546
3265
  ungroupedSessionIds: orderedUngroupedSessionIds,
2547
3266
  workspaceReady,
2548
3267
  nestWorkspaces: groupBy === "workspace-tree",
3268
+ animationResetKey: `${groupBy}/${orderBy}/${archivedFilter}`,
2549
3269
  groupExpansion,
2550
3270
  setGroupExpanded: actions.setGroupExpanded,
2551
3271
  setSessionOrder: saveSessionOrder,
2552
- archivedSessionIds,
3272
+ rowState,
2553
3273
  startSession,
2554
- open,
3274
+ open: guardedOpen,
2555
3275
  insertWorkspaceBefore,
2556
3276
  revealSessionId,
2557
3277
  onSessionRevealed: acknowledgeSessionReveal,
@@ -2629,53 +3349,6 @@ window.__ModuleLoader__.load({
2629
3349
  })
2630
3350
  ]
2631
3351
  }),
2632
- (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
2633
- open: sessionRenameTarget !== null,
2634
- onClose: closeSessionRename,
2635
- closeLabel: t("close"),
2636
- title: t("rename.session.title"),
2637
- footer: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2638
- variant: "outline",
2639
- disabled: sessionRenaming,
2640
- onClick: closeSessionRename,
2641
- children: t("cancel")
2642
- }), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2643
- variant: "primary",
2644
- disabled: sessionRenameBlocked,
2645
- onClick: confirmSessionRename,
2646
- children: t("rename")
2647
- })] }),
2648
- children: [(0, react_jsx_runtime.jsx)("input", {
2649
- className: WorkspaceBrowser_module_css_default.renameInput,
2650
- value: sessionRenameDraft,
2651
- "aria-label": t("field.sessionName"),
2652
- autoFocus: true,
2653
- disabled: sessionRenaming,
2654
- onFocus: (e) => {
2655
- e.target.select();
2656
- },
2657
- onChange: (e) => {
2658
- setSessionRenameDraft(e.target.value);
2659
- setSessionRenameError(null);
2660
- },
2661
- onCompositionStart: () => {
2662
- composingRef.current = true;
2663
- },
2664
- onCompositionEnd: () => {
2665
- composingRef.current = false;
2666
- },
2667
- onKeyDown: (e) => {
2668
- if (e.key === "Enter" && !composingRef.current) {
2669
- e.preventDefault();
2670
- confirmSessionRename();
2671
- }
2672
- }
2673
- }), sessionRenameError !== null && (0, react_jsx_runtime.jsx)("div", {
2674
- className: WorkspaceBrowser_module_css_default.renameError,
2675
- role: "alert",
2676
- children: sessionRenameError
2677
- })]
2678
- }),
2679
3352
  (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
2680
3353
  open: deleteTarget !== null,
2681
3354
  onClose: closeDelete,
@@ -2708,146 +3381,449 @@ window.__ModuleLoader__.load({
2708
3381
  });
2709
3382
  }
2710
3383
  //#endregion
2711
- //#region lib/types/client/locales.js
3384
+ //#region lib/types/client/session-actions/ArchiveSession.js
3385
+ /**
3386
+ * The archive action: a `sidebar.workspaces.session.menu.item` row and a
3387
+ * `sidebar.workspaces.session.row.action` button over one injected behavior,
3388
+ * plus the `shell.overlay` dialog that confirms stopping a Session's running
3389
+ * work before archiving it. The same entries restore an archived row; the
3390
+ * notice a successful archive raises and the diagnostics for Host rejections
3391
+ * live in the injected callbacks, not here.
3392
+ */
3393
+ /**
3394
+ * Menu row (order 400): archive, or restore an archived row.
3395
+ * @param props - owner share, the archive share, and the menu open state.
3396
+ * @returns the row.
3397
+ */
3398
+ function ArchiveSessionMenuItem({ sessionId, useArchived, useMenuOpenState, archiveSession, unarchiveSession, t }) {
3399
+ const [, setMenuOpen] = useMenuOpenState();
3400
+ const archived = useArchived((set) => set.has(sessionId));
3401
+ return (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MenuItemButton, {
3402
+ icon: archived ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconUnarchiveOutlineRegular, { size: 14 }) : (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconArchiveOutlineRegular, { size: 14 }),
3403
+ onSelect: () => {
3404
+ setMenuOpen(false);
3405
+ (archived ? unarchiveSession : archiveSession)(sessionId);
3406
+ },
3407
+ children: t(archived ? "menu.unarchiveSession" : "menu.archiveSession")
3408
+ });
3409
+ }
3410
+ /**
3411
+ * Hover button (order 100): archive, or restore an archived row.
3412
+ * @param props - owner share and the archive share.
3413
+ * @returns the button.
3414
+ */
3415
+ function ArchiveSessionRowButton({ sessionId, useArchived, archiveSession, unarchiveSession, t }) {
3416
+ const archived = useArchived((set) => set.has(sessionId));
3417
+ return (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
3418
+ label: t(archived ? "actions.unarchive" : "actions.archive"),
3419
+ side: "bottom",
3420
+ align: "end",
3421
+ delayMs: 500,
3422
+ children: (0, react_jsx_runtime.jsx)("button", {
3423
+ type: "button",
3424
+ className: Rows_module_css_default.iconButton,
3425
+ "aria-label": t(archived ? "menu.unarchiveSession" : "menu.archiveSession"),
3426
+ onClick: () => {
3427
+ (archived ? unarchiveSession : archiveSession)(sessionId);
3428
+ },
3429
+ children: archived ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconUnarchiveOutlineRegular, { size: 14 }) : (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconArchiveOutlineRegular, { size: 14 })
3430
+ })
3431
+ });
3432
+ }
3433
+ /**
3434
+ * The `shell.overlay` entry: nothing while no confirmation is pending,
3435
+ * otherwise one dialog per request (keyed by the Session). Confirming asks
3436
+ * the Host to stop the listed work and archive; cancelling leaves the
3437
+ * Session running and visible.
3438
+ * @param props - the request hook, its settlement, the stop-and-archive hop, and the locale seat.
3439
+ * @returns the open dialog, or null.
3440
+ */
3441
+ function SessionArchiveConfirmDialog({ useArchiveRequest, settleSessionArchive, stopAndArchiveSession, t }) {
3442
+ const request = useArchiveRequest((pending) => pending);
3443
+ if (request === null) return null;
3444
+ return (0, react_jsx_runtime.jsx)(ArchiveConfirmForm, {
3445
+ request,
3446
+ stopAndArchiveSession,
3447
+ onSettle: settleSessionArchive,
3448
+ t
3449
+ }, request.sessionId);
3450
+ }
3451
+ /** One request's dialog: in-flight and error state die with it. */
3452
+ function ArchiveConfirmForm({ request, stopAndArchiveSession, onSettle, t }) {
3453
+ const [archiving, setArchiving] = (0, react.useState)(false);
3454
+ const [error, setError] = (0, react.useState)(null);
3455
+ const close = () => {
3456
+ if (archiving) return;
3457
+ onSettle();
3458
+ };
3459
+ const confirm = () => {
3460
+ setArchiving(true);
3461
+ setError(null);
3462
+ stopAndArchiveSession(request.sessionId).then(() => {
3463
+ setArchiving(false);
3464
+ onSettle();
3465
+ }).catch((reason) => {
3466
+ setArchiving(false);
3467
+ setError(reason instanceof Error ? reason.message : String(reason));
3468
+ });
3469
+ };
3470
+ return (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
3471
+ open: true,
3472
+ onClose: close,
3473
+ closeLabel: t("close"),
3474
+ title: t("archive.confirm.title"),
3475
+ description: t("archive.confirm.desc", { title: request.displayTitle }),
3476
+ footer: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
3477
+ variant: "outline",
3478
+ disabled: archiving,
3479
+ onClick: close,
3480
+ children: t("cancel")
3481
+ }), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
3482
+ variant: "outline",
3483
+ className: WorkspaceBrowser_module_css_default.deleteAction,
3484
+ disabled: archiving,
3485
+ onClick: confirm,
3486
+ children: t("archive.confirm.action")
3487
+ })] }),
3488
+ children: [
3489
+ (0, react_jsx_runtime.jsx)("ul", {
3490
+ className: WorkspaceBrowser_module_css_default.archiveActivity,
3491
+ "aria-label": t("archive.confirm.activity"),
3492
+ children: request.activity.map((entry, index) => (0, react_jsx_runtime.jsx)("li", { children: activityLine(entry, t) }, `${entry.kind}-${String(index)}`))
3493
+ }),
3494
+ archiving && (0, react_jsx_runtime.jsx)("div", {
3495
+ className: WorkspaceBrowser_module_css_default.deleteStatus,
3496
+ role: "status",
3497
+ children: t("archive.confirm.pending")
3498
+ }),
3499
+ error !== null && (0, react_jsx_runtime.jsx)("div", {
3500
+ className: WorkspaceBrowser_module_css_default.renameError,
3501
+ role: "alert",
3502
+ children: error
3503
+ })
3504
+ ]
3505
+ });
3506
+ }
3507
+ /**
3508
+ * One family's line: its count and the items' labels (ids when a family
3509
+ * carries no label). A family this dictionary does not know — a provider
3510
+ * merged into the kind map — falls through to the generic line.
3511
+ */
3512
+ function activityLine(entry, t) {
3513
+ const items = entry.items ?? [];
3514
+ const n = items.length;
3515
+ const names = items.map((item) => item.label ?? item.id).join(t("archive.confirm.listSeparator"));
3516
+ const plural = n === 1 ? "one" : "other";
3517
+ switch (entry.kind) {
3518
+ case "turn": return t("archive.confirm.turn");
3519
+ case "subagent": return t(`archive.confirm.subagents.${plural}`, {
3520
+ n,
3521
+ names
3522
+ });
3523
+ case "job": return t(`archive.confirm.jobs.${plural}`, {
3524
+ n,
3525
+ names
3526
+ });
3527
+ case "schedule": return t(`archive.confirm.schedules.${plural}`, {
3528
+ n,
3529
+ names
3530
+ });
3531
+ default: return t(`archive.confirm.other.${plural}`, {
3532
+ kind: entry.kind,
3533
+ n
3534
+ });
3535
+ }
3536
+ }
3537
+ //#endregion
3538
+ //#region lib/types/client/session-actions/derived.js
3539
+ /**
3540
+ * Project one observable into another, recomputing only when the source
3541
+ * snapshot changes identity, so consumers that select from the projection
3542
+ * (a Set lookup per row) never rebuild it per read.
3543
+ * @param source - the observable to project.
3544
+ * @param project - pure projection of one source snapshot.
3545
+ * @returns the projected observable, subscribing through the source.
3546
+ */
3547
+ function derive(source, project) {
3548
+ let seen;
3549
+ let value;
3550
+ return {
3551
+ getSnapshot: () => {
3552
+ const snapshot = source.getSnapshot();
3553
+ if (value === void 0 || snapshot !== seen) {
3554
+ seen = snapshot;
3555
+ value = project(snapshot);
3556
+ }
3557
+ return value;
3558
+ },
3559
+ subscribe: (listener) => source.subscribe(listener)
3560
+ };
3561
+ }
3562
+ //#endregion
3563
+ //#region lib/types/client/session-actions/ForkSession.js
3564
+ /** The fork action: one `sidebar.workspaces.session.menu.item` row. */
3565
+ /**
3566
+ * Menu row (order 300): fork at the Session's last completed turn; the child
3567
+ * arrives through the Host list beside its source.
3568
+ * @param props - owner share, menu open state, and the fork share.
3569
+ * @returns the row.
3570
+ */
3571
+ function ForkSessionMenuItem({ sessionId, useMenuOpenState, forkSession, t }) {
3572
+ const [, setMenuOpen] = useMenuOpenState();
3573
+ return (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MenuItemButton, {
3574
+ icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconBranchOutlineRegular, {}),
3575
+ onSelect: () => {
3576
+ setMenuOpen(false);
3577
+ forkSession(sessionId);
3578
+ },
3579
+ children: t("menu.fork")
3580
+ });
3581
+ }
3582
+ //#endregion
3583
+ //#region lib/types/client/session-actions/PinSession.js
2712
3584
  /**
2713
- * `workspace` namespace dictionaries: the browsing region (section header,
2714
- * search, tree rows, dialogs) and the pick/add flow. Runtime failure
2715
- * messages (wire error strings) pass through untranslated by policy.
3585
+ * The pin action: a `sidebar.workspaces.session.menu.item` row and a
3586
+ * `sidebar.workspaces.session.row.action` button over one injected behavior.
3587
+ * Pin and archive are mutually exclusive on the Host, so the action reads both
3588
+ * sets and does not offer itself on an archived row; what a pin does beyond
3589
+ * the Host call (fronting the Session in its saved orders, the failure
3590
+ * notice) lives in the injected callbacks, not here.
2716
3591
  */
2717
- /** Simplified Chinese dictionary (the key-set source of truth). */
2718
- const zh = {
2719
- "group.ungrouped": "未分组",
2720
- "session.new": "新会话",
2721
- "section.workspaces": "工作区",
2722
- "section.sessions": "会话",
2723
- "viewOptions.label": "视图选项",
2724
- "groupBy.label": "分组方式",
2725
- "groupBy.workspace": "按工作区",
2726
- "groupBy.workspaceTree": "按工作区树",
2727
- "groupBy.flat": "单列表",
2728
- "orderBy.label": "排序方式",
2729
- "orderBy.manual": "手动排序",
2730
- "orderBy.updated": "最近更新",
2731
- "sessions.expand": "展开其余 {n} 个会话",
2732
- "sessions.collapse": "收起",
2733
- "empty.none": "暂无会话",
2734
- "empty.noMatches": "无匹配结果",
2735
- "workspace.add": "添加工作区",
2736
- "search.sessions.aria": "搜索会话",
2737
- "search.placeholder": "搜索会话…",
2738
- "search.clear": "清除搜索",
2739
- "search.results.aria": "搜索结果",
2740
- "search.pending": "正在搜索会话历史…",
2741
- "search.unavailable": "内容搜索暂不可用,仅显示名称匹配。",
2742
- "search.noMatches": "无匹配会话",
2743
- "search.hasMore": "仅显示前 {n} 条结果,请缩小搜索范围。",
2744
- "menu.addWorkspace": "添加工作区…",
2745
- "picker.loading": "正在加载工作区…",
2746
- "conflict.named": "已存在名为“{name}”的工作区。",
2747
- "folderError.title": "无法打开文件夹",
2748
- "folderError.retry": "重新选择",
2749
- "rename": "重命名",
2750
- "rename.workspace.title": "重命名工作区",
2751
- "rename.session.title": "重命名会话",
2752
- "field.workspaceName": "工作区名称",
2753
- "field.sessionName": "会话名称",
2754
- "delete.workspace": "删除工作区",
2755
- "delete.desc": "将把“{name}”从工作区列表中移除。文件夹与会话记录会保留,其会话将显示在“未分组”下。",
2756
- "delete.pending": "正在删除工作区…",
2757
- "menu.fork": "分叉会话",
2758
- "menu.archiveSession": "归档会话",
2759
- "sessions.count.one": "{n} 个会话",
2760
- "sessions.count.other": "{n} 个会话",
2761
- "actions.workspace.aria": "工作区“{name}”的操作",
2762
- "actions.session.aria": "会话“{name}”的操作",
2763
- "actions.newSession.aria": "在“{name}”中新建会话",
2764
- "status.running": "进行中",
2765
- "status.subagentsRunning.one": "{n} 个子代理运行中",
2766
- "status.subagentsRunning.other": "{n} 个子代理运行中",
2767
- "status.idle": "空闲",
2768
- "status.waitingApproval": "等待审批",
2769
- "status.planReview": "计划待审",
2770
- "status.waitingAnswer": "等待回答",
2771
- "status.completed": "已完成",
2772
- "schedule.active": "有活动定时任务",
2773
- "hover.created": "创建于 {time}",
2774
- "hover.copied": "已复制",
2775
- "date.ymd": "{y}年{m}月{d}日",
2776
- "time.now": "刚刚",
2777
- "time.minutes": "{n}分钟",
2778
- "time.hours": "{n}小时",
2779
- "time.days": "{n}天",
2780
- "time.months": "{n}个月",
2781
- "time.years": "{n}年",
2782
- "time.ago": "{t}前"
2783
- };
2784
- /** English dictionary, checked complete against the zh key set. */
2785
- const en = {
2786
- "group.ungrouped": "Ungrouped",
2787
- "session.new": "New Session",
2788
- "section.workspaces": "Workspaces",
2789
- "section.sessions": "Sessions",
2790
- "viewOptions.label": "View options",
2791
- "groupBy.label": "Group by",
2792
- "groupBy.workspace": "WorkSpace",
2793
- "groupBy.workspaceTree": "Workspace Tree",
2794
- "groupBy.flat": "In one list",
2795
- "orderBy.label": "Order by",
2796
- "orderBy.manual": "Manual",
2797
- "orderBy.updated": "Last updated",
2798
- "sessions.expand": "Show {n} more sessions",
2799
- "sessions.collapse": "Show less",
2800
- "empty.none": "No sessions yet",
2801
- "empty.noMatches": "No matches",
2802
- "workspace.add": "Add workspace",
2803
- "search.sessions.aria": "Search sessions",
2804
- "search.placeholder": "Search sessions...",
2805
- "search.clear": "Clear search",
2806
- "search.results.aria": "Search results",
2807
- "search.pending": "Searching session history…",
2808
- "search.unavailable": "Content search is temporarily unavailable. Showing name matches.",
2809
- "search.noMatches": "No matching sessions",
2810
- "search.hasMore": "Showing the first {n} results. Narrow your search.",
2811
- "menu.addWorkspace": "Add workspace…",
2812
- "picker.loading": "Loading workspaces…",
2813
- "conflict.named": "A workspace named “{name}” already exists.",
2814
- "folderError.title": "Couldn’t open folder",
2815
- "folderError.retry": "Choose again",
2816
- "rename": "Rename",
2817
- "rename.workspace.title": "Rename workspace",
2818
- "rename.session.title": "Rename session",
2819
- "field.workspaceName": "Workspace name",
2820
- "field.sessionName": "Session name",
2821
- "delete.workspace": "Delete workspace",
2822
- "delete.desc": "This removes “{name}” from the workspace list. The folder and session logs will be kept. Its sessions will appear under Ungrouped.",
2823
- "delete.pending": "Deleting workspace…",
2824
- "menu.fork": "Fork session",
2825
- "menu.archiveSession": "Archive session",
2826
- "sessions.count.one": "{n} session",
2827
- "sessions.count.other": "{n} sessions",
2828
- "actions.workspace.aria": "Workspace actions for {name}",
2829
- "actions.session.aria": "Session actions for {name}",
2830
- "actions.newSession.aria": "New session in {name}",
2831
- "status.running": "Running",
2832
- "status.subagentsRunning.one": "{n} subagent running",
2833
- "status.subagentsRunning.other": "{n} subagents running",
2834
- "status.idle": "Idle",
2835
- "status.waitingApproval": "Waiting for approval",
2836
- "status.planReview": "Plan awaiting review",
2837
- "status.waitingAnswer": "Waiting for answer",
2838
- "status.completed": "Completed",
2839
- "schedule.active": "Has active scheduled task",
2840
- "hover.created": "Created {time}",
2841
- "hover.copied": "Copied",
2842
- "date.ymd": "{y}-{m}-{d}",
2843
- "time.now": "now",
2844
- "time.minutes": "{n}min",
2845
- "time.hours": "{n}h",
2846
- "time.days": "{n}d",
2847
- "time.months": "{n}mo",
2848
- "time.years": "{n}y",
2849
- "time.ago": "{t} ago"
2850
- };
3592
+ /** The row's pin and archive membership, one Set lookup each. */
3593
+ function usePinState({ sessionId, usePinned, useArchived }) {
3594
+ return {
3595
+ pinned: usePinned((pinned) => pinned.has(sessionId)),
3596
+ archived: useArchived((archived) => archived.has(sessionId))
3597
+ };
3598
+ }
3599
+ /**
3600
+ * Menu row (order 100): pin or unpin by the row's current state; absent on archived rows.
3601
+ * @param props - owner share, the pin share, and the menu open state.
3602
+ * @returns the row, or null for an archived Session.
3603
+ */
3604
+ function PinSessionMenuItem(props) {
3605
+ const { sessionId, useMenuOpenState, pinSession, unpinSession, t } = props;
3606
+ const [, setMenuOpen] = useMenuOpenState();
3607
+ const { pinned, archived } = usePinState(props);
3608
+ if (archived) return null;
3609
+ return (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MenuItemButton, {
3610
+ icon: pinned ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPinFillRegular, {}) : (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPinOutlineRegular, {}),
3611
+ onSelect: () => {
3612
+ setMenuOpen(false);
3613
+ (pinned ? unpinSession : pinSession)(sessionId);
3614
+ },
3615
+ children: t(pinned ? "menu.unpinSession" : "menu.pinSession")
3616
+ });
3617
+ }
3618
+ /**
3619
+ * Hover button (order 200, rightmost: it lands where the rest-state pin marker sits); absent on archived rows.
3620
+ * @param props - owner share and the pin share.
3621
+ * @returns the button, or null for an archived Session.
3622
+ */
3623
+ function PinSessionRowButton(props) {
3624
+ const { sessionId, pinSession, unpinSession, t } = props;
3625
+ const { pinned, archived } = usePinState(props);
3626
+ if (archived) return null;
3627
+ return (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
3628
+ label: t(pinned ? "actions.unpin" : "actions.pin"),
3629
+ side: "bottom",
3630
+ align: "end",
3631
+ delayMs: 500,
3632
+ children: (0, react_jsx_runtime.jsx)("button", {
3633
+ type: "button",
3634
+ className: Rows_module_css_default.iconButton,
3635
+ "aria-label": t(pinned ? "menu.unpinSession" : "menu.pinSession"),
3636
+ onClick: () => {
3637
+ (pinned ? unpinSession : pinSession)(sessionId);
3638
+ },
3639
+ children: pinned ? (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPinFillRegular, { size: 14 }) : (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconPinOutlineRegular, { size: 14 })
3640
+ })
3641
+ });
3642
+ }
3643
+ //#endregion
3644
+ //#region lib/types/client/session-actions/RenameSession.js
3645
+ /**
3646
+ * The rename action: a `sidebar.workspaces.session.menu.item` row that raises
3647
+ * the rename request, and the `shell.overlay` dialog entry that answers it.
3648
+ * The dialog lives outside the row menu because the row unmounts with the
3649
+ * menu; the browser raises the same request from a title double-click.
3650
+ */
3651
+ /**
3652
+ * Menu row (order 200): ask for the rename dialog, seeded with the row's current title.
3653
+ * @param props - owner share, menu open state, and the rename share.
3654
+ * @returns the row.
3655
+ */
3656
+ function RenameSessionMenuItem({ sessionId, displayTitle, useMenuOpenState, requestSessionRename, t }) {
3657
+ const [, setMenuOpen] = useMenuOpenState();
3658
+ return (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MenuItemButton, {
3659
+ icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutlineRegular, {}),
3660
+ onSelect: () => {
3661
+ setMenuOpen(false);
3662
+ requestSessionRename(sessionId, displayTitle);
3663
+ },
3664
+ children: t("rename")
3665
+ });
3666
+ }
3667
+ /**
3668
+ * The `shell.overlay` entry: nothing while no rename is requested, otherwise
3669
+ * one dialog per request (keyed by the Session, so a new request starts a
3670
+ * fresh draft). Sessions have no client-side name-conflict rule (the host
3671
+ * normalizes), and unlike Workspace rename an unchanged title is NOT
3672
+ * blocked: confirming the current automatic title is the gesture that pins it.
3673
+ * @param props - the request hook, its settlement, the rename hop, and the locale seat.
3674
+ * @returns the open dialog, or null.
3675
+ */
3676
+ function SessionRenameDialog({ useRenameRequest, settleSessionRename, renameSession, t }) {
3677
+ const request = useRenameRequest((pending) => pending);
3678
+ if (request === null) return null;
3679
+ return (0, react_jsx_runtime.jsx)(RenameForm, {
3680
+ request,
3681
+ renameSession,
3682
+ onSettle: settleSessionRename,
3683
+ t
3684
+ }, request.sessionId);
3685
+ }
3686
+ /** One request's dialog: the draft seeds from the request on mount; in-flight and error state die with it. */
3687
+ function RenameForm({ request, renameSession, onSettle, t }) {
3688
+ const [draft, setDraft] = (0, react.useState)(request.currentTitle);
3689
+ const [renaming, setRenaming] = (0, react.useState)(false);
3690
+ const [error, setError] = (0, react.useState)(null);
3691
+ const composingRef = (0, react.useRef)(false);
3692
+ const trimmed = draft.trim();
3693
+ const blocked = renaming || trimmed === "";
3694
+ const close = () => {
3695
+ if (renaming) return;
3696
+ onSettle();
3697
+ };
3698
+ const confirm = () => {
3699
+ if (blocked) return;
3700
+ setRenaming(true);
3701
+ setError(null);
3702
+ renameSession(request.sessionId, trimmed).then(() => {
3703
+ setRenaming(false);
3704
+ onSettle();
3705
+ }).catch((reason) => {
3706
+ setRenaming(false);
3707
+ setError(reason instanceof Error ? reason.message : String(reason));
3708
+ });
3709
+ };
3710
+ return (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
3711
+ open: true,
3712
+ onClose: close,
3713
+ closeLabel: t("close"),
3714
+ title: t("rename.session.title"),
3715
+ footer: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
3716
+ variant: "outline",
3717
+ disabled: renaming,
3718
+ onClick: close,
3719
+ children: t("cancel")
3720
+ }), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
3721
+ variant: "primary",
3722
+ disabled: blocked,
3723
+ onClick: confirm,
3724
+ children: t("rename")
3725
+ })] }),
3726
+ children: [(0, react_jsx_runtime.jsx)("input", {
3727
+ className: WorkspaceBrowser_module_css_default.renameInput,
3728
+ value: draft,
3729
+ "aria-label": t("field.sessionName"),
3730
+ autoFocus: true,
3731
+ disabled: renaming,
3732
+ onFocus: (e) => {
3733
+ e.target.select();
3734
+ },
3735
+ onChange: (e) => {
3736
+ setDraft(e.target.value);
3737
+ setError(null);
3738
+ },
3739
+ onCompositionStart: () => {
3740
+ composingRef.current = true;
3741
+ },
3742
+ onCompositionEnd: () => {
3743
+ composingRef.current = false;
3744
+ },
3745
+ onKeyDown: (e) => {
3746
+ if (e.key === "Enter" && !composingRef.current) {
3747
+ e.preventDefault();
3748
+ confirm();
3749
+ }
3750
+ }
3751
+ }), error !== null && (0, react_jsx_runtime.jsx)("div", {
3752
+ className: WorkspaceBrowser_module_css_default.renameError,
3753
+ role: "alert",
3754
+ children: error
3755
+ })]
3756
+ });
3757
+ }
3758
+ //#endregion
3759
+ //#region lib/types/client/session-actions/RowActionToast.js
3760
+ /**
3761
+ * The `shell.overlay` entry for Workspace and Session notices.
3762
+ * One notice is visible at a time; a parent rerender does not extend its hold.
3763
+ */
3764
+ /**
3765
+ * Hold for the notices that take longer to read than a one-line warning: the
3766
+ * actionable archived notice (two buttons to react to) and a refused Session
3767
+ * creation, which quotes the Host's reason.
3768
+ */
3769
+ const LONG_TOAST_HOLD_MS = 6e3;
3770
+ /**
3771
+ * Render the current notice: the archived and stopped-and-archived notices
3772
+ * with their undo and show-archived actions on a 6 s hold, a refused Session
3773
+ * creation with the Host's reason on the same hold, or a plain warning for a
3774
+ * failed pin, an archived row that was clicked, or default Workspace creation.
3775
+ * @param props - the notice hook, its dismissal, the two archived-notice actions, and the locale seat.
3776
+ * @returns the notice on display, or null.
3777
+ */
3778
+ function RowActionToast({ useToast, dismissToast, undoArchive, showArchived, t }) {
3779
+ const toast = useToast((current) => current);
3780
+ if (toast === null) return null;
3781
+ if (toast.kind === "archived" || toast.kind === "stoppedAndArchived") {
3782
+ const { sessionId } = toast;
3783
+ return (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
3784
+ text: t(toast.kind === "archived" ? "toast.archived" : "toast.stoppedAndArchived"),
3785
+ tone: "success",
3786
+ holdMs: LONG_TOAST_HOLD_MS,
3787
+ actions: [{
3788
+ label: t("toast.archivedUndo"),
3789
+ onClick: () => {
3790
+ dismissToast();
3791
+ undoArchive(sessionId);
3792
+ }
3793
+ }, {
3794
+ prefix: t("toast.archivedOr"),
3795
+ label: t("toast.archivedFilter"),
3796
+ onClick: () => {
3797
+ dismissToast();
3798
+ showArchived();
3799
+ }
3800
+ }],
3801
+ onDone: dismissToast
3802
+ }, `toast-${String(toast.seq)}`);
3803
+ }
3804
+ if (toast.kind === "createFailed") return (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
3805
+ text: t("toast.createFailed", { message: toast.message }),
3806
+ icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutlineRegular, {}),
3807
+ holdMs: LONG_TOAST_HOLD_MS,
3808
+ onDone: dismissToast
3809
+ }, `toast-${String(toast.seq)}`);
3810
+ return (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Toast, {
3811
+ text: plainNoticeText(toast, t),
3812
+ icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconWarningOutlineRegular, {}),
3813
+ onDone: dismissToast
3814
+ }, `toast-${String(toast.seq)}`);
3815
+ }
3816
+ /** The copy of one plain warning, keyed by the notice kind the union closes over. */
3817
+ function plainNoticeText(toast, t) {
3818
+ switch (toast.kind) {
3819
+ case "pinFailed": return t("toast.pinFailed");
3820
+ case "unpinFailed": return t("toast.unpinFailed");
3821
+ case "defaultWorkspaceFailed": return t("defaultWorkspace.failed");
3822
+ case "archivedNotOpenable": return t("toast.archivedNotOpenable");
3823
+ /* v8 ignore next 2 -- closed-union backstop; only reached if a notice kind is forged */
3824
+ default: return assertNever$1(toast);
3825
+ }
3826
+ }
2851
3827
  //#endregion
2852
3828
  //#region lib/types/client/index.js
2853
3829
  /** Dictionary namespace owned by this plugin. */
@@ -2878,7 +3854,21 @@ window.__ModuleLoader__.load({
2878
3854
  function apply(ctx) {
2879
3855
  const sessions = ctx.get("sessions");
2880
3856
  const workspaces = ctx.get("workspaces");
2881
- const uiWorkspace = new UiWorkspaceService(ctx, ctx.remote.directoryPicker, workspaces, sessions);
3857
+ const viewHandle = createWorkspaceViewStore();
3858
+ const viewInstance = viewHandle.create();
3859
+ const viewStore = {
3860
+ ...viewHandle,
3861
+ create: () => viewInstance
3862
+ };
3863
+ const rowToast = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)(null);
3864
+ let toastSeq = 0;
3865
+ const notify = (toast) => {
3866
+ rowToast.set({
3867
+ ...toast,
3868
+ seq: ++toastSeq
3869
+ });
3870
+ };
3871
+ const uiWorkspace = new UiWorkspaceService(ctx, ctx.remote.directoryPicker, workspaces, sessions, viewInstance.actions, notify);
2882
3872
  ctx.slots.provideRoot({ hooks: { workspaces: workspaces.list } });
2883
3873
  ctx.effect(() => ctx.locale.register(NS, {
2884
3874
  zh,
@@ -2902,6 +3892,99 @@ window.__ModuleLoader__.load({
2902
3892
  const openSession = (sessionId) => {
2903
3893
  uiWorkspace.openSession(sessionId);
2904
3894
  };
3895
+ const pinnedSet = derive(workspaces.list, (snapshot) => new Set(snapshot.pinnedSessionIds));
3896
+ const archivedSet = derive(workspaces.list, (snapshot) => new Set(snapshot.archivedSessionIds));
3897
+ const renameRequest = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)(null);
3898
+ const archiveRequest = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)(null);
3899
+ const requestSessionRename = (sessionId, currentTitle) => {
3900
+ renameRequest.set({
3901
+ sessionId,
3902
+ currentTitle
3903
+ });
3904
+ };
3905
+ const unarchiveSession = (sessionId) => {
3906
+ uiWorkspace.unarchiveSession(sessionId).catch((reason) => {
3907
+ console.warn("session unarchive rejected:", reason);
3908
+ });
3909
+ };
3910
+ const renameSession = async (sessionId, title) => {
3911
+ const result = await sessions.using(sessionId, { source: "workspaceOperation" }, (reference) => reference.binding.session.rename(title));
3912
+ if (!result.ok) throw new Error(result.error.message);
3913
+ };
3914
+ const pinInjected = () => ({
3915
+ hooks: {
3916
+ pinned: pinnedSet,
3917
+ archived: archivedSet
3918
+ },
3919
+ pinSession: (sessionId) => {
3920
+ uiWorkspace.pinSession(sessionId).catch(() => {
3921
+ notify({ kind: "pinFailed" });
3922
+ });
3923
+ },
3924
+ unpinSession: (sessionId) => {
3925
+ uiWorkspace.unpinSession(sessionId).catch(() => {
3926
+ notify({ kind: "unpinFailed" });
3927
+ });
3928
+ }
3929
+ });
3930
+ const archiveInjected = () => ({
3931
+ hooks: { archived: archivedSet },
3932
+ archiveSession: (sessionId) => {
3933
+ uiWorkspace.archiveSession(sessionId).then(() => {
3934
+ notify({
3935
+ kind: "archived",
3936
+ sessionId
3937
+ });
3938
+ }).catch((reason) => {
3939
+ const activity = activeSessionRefusal(reason);
3940
+ if (activity === void 0) {
3941
+ console.warn("session archive rejected:", reason);
3942
+ return;
3943
+ }
3944
+ const displayTitle = sessions.list.getSnapshot().byId[sessionId]?.displayTitle ?? sessionId;
3945
+ archiveRequest.set({
3946
+ sessionId,
3947
+ displayTitle,
3948
+ activity
3949
+ });
3950
+ });
3951
+ },
3952
+ unarchiveSession
3953
+ });
3954
+ const archiveConfirmInjected = () => ({
3955
+ hooks: { archiveRequest },
3956
+ settleSessionArchive: () => {
3957
+ archiveRequest.set(null);
3958
+ },
3959
+ stopAndArchiveSession: async (sessionId) => {
3960
+ await uiWorkspace.archiveSession(sessionId, { stopActivity: true });
3961
+ notify({
3962
+ kind: "stoppedAndArchived",
3963
+ sessionId
3964
+ });
3965
+ }
3966
+ });
3967
+ const forkInjected = () => ({ forkSession: (sessionId) => {
3968
+ uiWorkspace.forkSession(sessionId).catch(() => {});
3969
+ } });
3970
+ const renameInjected = () => ({ requestSessionRename });
3971
+ const renameDialogInjected = () => ({
3972
+ hooks: { renameRequest },
3973
+ settleSessionRename: () => {
3974
+ renameRequest.set(null);
3975
+ },
3976
+ renameSession
3977
+ });
3978
+ const rowToastInjected = () => ({
3979
+ hooks: { toast: rowToast },
3980
+ dismissToast: () => {
3981
+ rowToast.set(null);
3982
+ },
3983
+ undoArchive: unarchiveSession,
3984
+ showArchived: () => {
3985
+ viewInstance.actions.setArchivedFilter("show");
3986
+ }
3987
+ });
2905
3988
  const browserInjected = () => ({
2906
3989
  startSession: (workspaceId) => {
2907
3990
  uiWorkspace.startSession(workspaceId);
@@ -2909,12 +3992,9 @@ window.__ModuleLoader__.load({
2909
3992
  open: openSession,
2910
3993
  searchSessions,
2911
3994
  searchResultLimit: sessions.searchResultLimit,
2912
- renameSession: async (sessionId, title) => {
2913
- const result = await sessions.using(sessionId, { source: "workspaceOperation" }, (reference) => reference.binding.session.rename(title));
2914
- if (!result.ok) throw new Error(result.error.message);
2915
- },
2916
- forkSession: (sessionId) => {
2917
- uiWorkspace.forkSession(sessionId).catch(() => {});
3995
+ requestSessionRename,
3996
+ notifyArchivedNotOpenable: () => {
3997
+ notify({ kind: "archivedNotOpenable" });
2918
3998
  },
2919
3999
  renameWorkspace: async (workspaceId, title) => {
2920
4000
  await workspaces.rename(workspaceId, title);
@@ -2925,8 +4005,8 @@ window.__ModuleLoader__.load({
2925
4005
  insertWorkspaceBefore: async (workspaceId, beforeWorkspaceId) => {
2926
4006
  await workspaces.insertBefore(workspaceId, beforeWorkspaceId);
2927
4007
  },
2928
- archiveSession: async (sessionId) => {
2929
- await uiWorkspace.archiveSession(sessionId);
4008
+ unarchiveSession: async (sessionId) => {
4009
+ await uiWorkspace.unarchiveSession(sessionId);
2930
4010
  },
2931
4011
  createWorkspace: (input) => workspaces.create(input),
2932
4012
  hooks: {
@@ -2940,14 +4020,91 @@ window.__ModuleLoader__.load({
2940
4020
  });
2941
4021
  ctx.slots.inject("sidebar.workspaces", () => ctx.slots.register({
2942
4022
  name: "sidebar.workspaces",
2943
- children: { "sidebar.workspaces.directoryFlow": {
2944
- kind: "single",
2945
- scope: "root"
2946
- } },
2947
- store: createWorkspaceViewStore(),
4023
+ children: {
4024
+ "sidebar.workspaces.directoryFlow": {
4025
+ kind: "single",
4026
+ scope: "root"
4027
+ },
4028
+ "sidebar.workspaces.session.menu.item": {
4029
+ kind: "list",
4030
+ scope: "root",
4031
+ inject: { hooks: { menuOpenState: menuOpenStateFactory } }
4032
+ },
4033
+ "sidebar.workspaces.session.row.action": {
4034
+ kind: "list",
4035
+ scope: "root"
4036
+ }
4037
+ },
4038
+ store: viewStore,
2948
4039
  inject: browserInjected,
2949
4040
  locale: NS
2950
4041
  }, WorkspaceBrowser));
4042
+ ctx.slots.inject("sidebar.workspaces.session.menu.item", function* () {
4043
+ yield ctx.slots.register({
4044
+ name: "sidebar.workspaces.session.menu.item",
4045
+ id: "pin",
4046
+ order: 100,
4047
+ locale: NS,
4048
+ inject: pinInjected
4049
+ }, PinSessionMenuItem);
4050
+ yield ctx.slots.register({
4051
+ name: "sidebar.workspaces.session.menu.item",
4052
+ id: "rename",
4053
+ order: 200,
4054
+ locale: NS,
4055
+ inject: renameInjected
4056
+ }, RenameSessionMenuItem);
4057
+ yield ctx.slots.register({
4058
+ name: "sidebar.workspaces.session.menu.item",
4059
+ id: "fork",
4060
+ order: 300,
4061
+ locale: NS,
4062
+ inject: forkInjected
4063
+ }, ForkSessionMenuItem);
4064
+ yield ctx.slots.register({
4065
+ name: "sidebar.workspaces.session.menu.item",
4066
+ id: "archive",
4067
+ order: 400,
4068
+ locale: NS,
4069
+ inject: archiveInjected
4070
+ }, ArchiveSessionMenuItem);
4071
+ });
4072
+ ctx.slots.inject("sidebar.workspaces.session.row.action", function* () {
4073
+ yield ctx.slots.register({
4074
+ name: "sidebar.workspaces.session.row.action",
4075
+ id: "archive",
4076
+ order: 100,
4077
+ locale: NS,
4078
+ inject: archiveInjected
4079
+ }, ArchiveSessionRowButton);
4080
+ yield ctx.slots.register({
4081
+ name: "sidebar.workspaces.session.row.action",
4082
+ id: "pin",
4083
+ order: 200,
4084
+ locale: NS,
4085
+ inject: pinInjected
4086
+ }, PinSessionRowButton);
4087
+ });
4088
+ ctx.slots.inject("shell.overlay", function* () {
4089
+ yield ctx.slots.register({
4090
+ name: "shell.overlay",
4091
+ id: "workspace.session-rename",
4092
+ locale: NS,
4093
+ inject: renameDialogInjected
4094
+ }, SessionRenameDialog);
4095
+ yield ctx.slots.register({
4096
+ name: "shell.overlay",
4097
+ id: "workspace.session-archive",
4098
+ locale: NS,
4099
+ inject: archiveConfirmInjected
4100
+ }, SessionArchiveConfirmDialog);
4101
+ yield ctx.slots.register({
4102
+ name: "shell.overlay",
4103
+ id: "workspace.row-toast",
4104
+ locale: NS,
4105
+ inject: rowToastInjected
4106
+ }, RowActionToast);
4107
+ });
2951
4108
  ctx.slots.inject("conversation.hero.workspace", () => ctx.slots.register({
2952
4109
  name: "conversation.hero.workspace",
2953
4110
  children: { "conversation.hero.workspace.directoryFlow": {
@@ -2958,6 +4115,16 @@ window.__ModuleLoader__.load({
2958
4115
  locale: NS
2959
4116
  }, WorkspacePicker));
2960
4117
  }
4118
+ /**
4119
+ * The activity a Host `workspace/session-active` refusal reported, or nothing
4120
+ * for any other failure. The class identity check goes by name: client plugin
4121
+ * bundles do not share error-class identity.
4122
+ */
4123
+ function activeSessionRefusal(reason) {
4124
+ if (!(reason instanceof Error) || reason.name !== "WorkspaceArchiveError") return void 0;
4125
+ const { rpcError } = reason;
4126
+ return rpcError.code === "workspace/session-active" ? rpcError.details.activity : void 0;
4127
+ }
2961
4128
  //#endregion
2962
4129
  exports.apply = apply;
2963
4130
  exports.inject = inject;