@michengai/dsh-archive-manager 0.1.21 → 0.1.23

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/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@
4
4
 
5
5
  Published release notes are retained below; new versions are added without removing earlier entries.
6
6
 
7
+ ## 0.1.23 — 2026-09-01
8
+
9
+ - Fixed missing sidebar pending-state indicators on DSH `v0.1.2-alpha.2`: workspace rows now read pending UI interactions from the `ui-session` `pendingInteractions` Map.
10
+ - Whitelisted the official visible pending states (`question`, `approval`, and `plan-review`) before passing them to row rendering, so unknown future states are ignored safely.
11
+ - Kept the legacy `SessionSummary.pendingInteraction` fallback and added regression coverage for grouped, flat, and search result rows.
12
+
13
+ Published package: [`@michengai/dsh-archive-manager@0.1.23`](https://www.npmjs.com/package/@michengai/dsh-archive-manager/v/0.1.23).
14
+
15
+ ## 0.1.22 — 2026-09-01
16
+
17
+ - Added arbitrary archived-chat selection in **Settings → Archived**, including per-row checkboxes, select-all for the current filtered results, cross-project selection, batch restore, and confirmed batch permanent deletion.
18
+ - Kept selected operations on the existing authoritative host batch path, so stale records, partial deletion failures, and retry behavior match existing project-wide and all-chat actions.
19
+ - Refreshed the client session projection after restoring chats so restored sessions immediately reappear in their original workspace.
20
+
21
+ Published package: [`@michengai/dsh-archive-manager@0.1.22`](https://www.npmjs.com/package/@michengai/dsh-archive-manager/v/0.1.22).
22
+
7
23
  ## 0.1.21 — 2026-08-31
8
24
 
9
25
  - Preserved the projection-cache `put()` Promise contract when a tombstone suppresses a deleted Session write, so DSH `v0.1.2-alpha.2` cold-read write-back remains fail-soft instead of throwing a synchronous `TypeError`.
@@ -4,6 +4,22 @@
4
4
 
5
5
  以下发布说明会持续保留;新增版本时不再删除较早记录。
6
6
 
7
+ ## 0.1.23 — 2026-09-01
8
+
9
+ - 修复 DSH `v0.1.2-alpha.2` 后会话侧栏“等待回答 / 等待审批”状态点不显示的问题:侧栏行现在从 `ui-session` 的 `pendingInteractions` Map 读取待处理交互状态。
10
+ - 对 `question`、`approval`、`plan-review` 三种官方可见状态做白名单透传,未知状态会被忽略,避免触发行渲染断言。
11
+ - 保留旧版 `SessionSummary.pendingInteraction` 兜底,并补充分组、扁平列表和搜索结果三条路径的回归测试。
12
+
13
+ 已发布包:[`@michengai/dsh-archive-manager@0.1.23`](https://www.npmjs.com/package/@michengai/dsh-archive-manager/v/0.1.23)。
14
+
15
+ ## 0.1.22 — 2026-09-01
16
+
17
+ - 在「设置 → 已归档」新增任意多选:每行复选框、全选当前筛选结果、跨项目保留选择、批量恢复和确认后的批量永久删除。
18
+ - 所选操作复用宿主权威批量路径,陈旧记录清理、部分删除失败和重试行为与项目级及全部操作保持一致。
19
+ - 恢复聊天后刷新客户端会话投影,使其立刻重新出现在原工作区。
20
+
21
+ 已发布包:[`@michengai/dsh-archive-manager@0.1.22`](https://www.npmjs.com/package/@michengai/dsh-archive-manager/v/0.1.22)。
22
+
7
23
  ## 0.1.21 — 2026-08-31
8
24
 
9
25
  - 墓碑拦截已删除会话的投影缓存写入时仍保持 `put()` 的 Promise 契约,使 DSH `v0.1.2-alpha.2` 冷读写回继续按 fail-soft 方式处理,而不会同步抛出 `TypeError`。
package/lib/client.js CHANGED
@@ -49,7 +49,8 @@ window.__ModuleLoader__.load({
49
49
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("target must be an object");
50
50
  if (value.scope === "all" || value.scope === "ungrouped") return value;
51
51
  if (value.scope === "workspace" && typeof value.workspaceId === "string" && value.workspaceId.length > 0) return value;
52
- throw new TypeError("target.scope must be all, ungrouped, or workspace with a non-empty workspaceId");
52
+ if (value.scope === "sessions" && Array.isArray(value.sessionIds) && value.sessionIds.length > 0 && value.sessionIds.every((id) => typeof id === "string" && id.length > 0)) return value;
53
+ throw new TypeError("target.scope must be all, ungrouped, workspace with a non-empty workspaceId, or sessions with non-empty sessionIds");
53
54
  }
54
55
  };
55
56
  const unarchivedBatchSchema = {
@@ -283,6 +284,195 @@ window.__ModuleLoader__.load({
283
284
  }
284
285
  });
285
286
  }
287
+ /**
288
+ * 归档多选原型。保留既有设置页作为兼容回退,原型直接使用相同宿主
289
+ * 批量接口,因此其恢复、删除和并发安全语义与原有项目级操作一致。
290
+ */
291
+ function ArchivedSessionsSectionPrototype({ sessionStore, workspaceStore, unarchiveSession, deleteSession, unarchiveSessions, deleteArchivedSessions, archivedSessionMetadata, t }) {
292
+ const sessions = (0, react.useSyncExternalStore)(sessionStore.subscribe, sessionStore.getSnapshot);
293
+ const workspaceState = (0, react.useSyncExternalStore)(workspaceStore.subscribe, workspaceStore.getSnapshot);
294
+ const [deleteTarget, setDeleteTarget] = (0, react.useState)(null);
295
+ const [busy, setBusy] = (0, react.useState)(false);
296
+ const [error, setError] = (0, react.useState)(null);
297
+ const [notice, setNotice] = (0, react.useState)(null);
298
+ const [query, setQuery] = (0, react.useState)("");
299
+ const [project, setProject] = (0, react.useState)("all");
300
+ const [sortBy, setSortBy] = (0, react.useState)("updated");
301
+ const [createdAtById, setCreatedAtById] = (0, react.useState)({});
302
+ const [unarchivingSessionIds, setUnarchivingSessionIds] = (0, react.useState)(() => /* @__PURE__ */ new Set());
303
+ const unarchivingSessionIdsRef = (0, react.useRef)(/* @__PURE__ */ new Set());
304
+ const [selectedSessionIds, setSelectedSessionIds] = (0, react.useState)([]);
305
+ const groups = (0, react.useMemo)(() => deriveArchivedGroups(sessions.byId, workspaceState.items, workspaceState.archivedSessionIds, t("group.ungrouped")), [sessions.byId, workspaceState, t]);
306
+ const sortedGroups = (0, react.useMemo)(() => sortArchivedGroups(groups, sortBy, createdAtById, t), [groups, sortBy, createdAtById, t]);
307
+ (0, react.useEffect)(() => {
308
+ let cancelled = false;
309
+ archivedSessionMetadata().then((result) => {
310
+ if (!cancelled) setCreatedAtById(Object.fromEntries(result.items.map((item) => [item.sessionId, item.createdAt])));
311
+ }).catch(() => {
312
+ if (!cancelled) setCreatedAtById({});
313
+ });
314
+ return () => {
315
+ cancelled = true;
316
+ };
317
+ }, [archivedSessionMetadata, workspaceState.archivedSessionIds]);
318
+ (0, react.useEffect)(() => {
319
+ if (project !== "all" && !groups.some((group) => group.key === project)) setProject("all");
320
+ }, [groups, project]);
321
+ const filteredGroups = (0, react.useMemo)(() => {
322
+ const normalizedQuery = query.trim().toLocaleLowerCase();
323
+ return sortedGroups.filter((group) => project === "all" || project === group.key).map((group) => ({
324
+ ...group,
325
+ sessions: group.sessions.filter((session) => normalizedQuery === "" || displayTitle(session, t).toLocaleLowerCase().includes(normalizedQuery))
326
+ })).filter((group) => group.sessions.length > 0);
327
+ }, [sortedGroups, project, query, t]);
328
+ const visibleSessionIds = (0, react.useMemo)(() => archivedSessionIdsInGroups(filteredGroups), [filteredGroups]);
329
+ const selectedSessionIdSet = (0, react.useMemo)(() => new Set(selectedSessionIds), [selectedSessionIds]);
330
+ const selectedVisibleCount = visibleSessionIds.filter((sessionId) => selectedSessionIdSet.has(sessionId)).length;
331
+ const allVisibleSelected = visibleSessionIds.length > 0 && selectedVisibleCount === visibleSessionIds.length;
332
+ const allBatchTarget = { scope: "all" };
333
+ const allBatchSessionIds = (0, react.useMemo)(() => deriveArchivedBatchIds(workspaceState.archivedSessionIds, workspaceState.items, allBatchTarget), [workspaceState.archivedSessionIds, workspaceState.items]);
334
+ (0, react.useEffect)(() => {
335
+ setSelectedSessionIds((current) => pruneArchivedSelection(current, workspaceState.archivedSessionIds));
336
+ }, [workspaceState.archivedSessionIds]);
337
+ const toggleSessionSelection = (sessionId, checked) => {
338
+ setSelectedSessionIds((current) => toggleArchivedSelection(current, [sessionId], checked));
339
+ };
340
+ const toggleVisibleSelection = (checked) => {
341
+ setSelectedSessionIds((current) => toggleArchivedSelection(current, visibleSessionIds, checked));
342
+ };
343
+ const onUnarchive = (sessionId) => {
344
+ if (busy || unarchivingSessionIdsRef.current.has(sessionId)) return;
345
+ unarchivingSessionIdsRef.current.add(sessionId);
346
+ setUnarchivingSessionIds(new Set(unarchivingSessionIdsRef.current));
347
+ setError(null);
348
+ setNotice(null);
349
+ unarchiveSession(sessionId).catch((reason) => {
350
+ setError(formatUnarchiveError(reason, t));
351
+ }).finally(() => {
352
+ unarchivingSessionIdsRef.current.delete(sessionId);
353
+ setUnarchivingSessionIds(new Set(unarchivingSessionIdsRef.current));
354
+ });
355
+ };
356
+ const onBatchUnarchive = async (target) => {
357
+ if (busy) return;
358
+ setBusy(true);
359
+ setError(null);
360
+ setNotice(null);
361
+ try {
362
+ const result = await unarchiveSessions(target);
363
+ setNotice(t("archives.restoreSuccess", { n: result.unarchivedSessionIds.length }));
364
+ return result;
365
+ } catch (reason) {
366
+ setError(t("archives.restoreBatchFailed", { detail: reason instanceof Error ? reason.message : String(reason) }));
367
+ } finally {
368
+ setBusy(false);
369
+ }
370
+ };
371
+ const onSelectedUnarchive = async () => {
372
+ if (selectedSessionIds.length === 0) return;
373
+ const result = await onBatchUnarchive({ scope: "sessions", sessionIds: selectedSessionIds });
374
+ if (result !== void 0) {
375
+ const restored = new Set(result.unarchivedSessionIds);
376
+ setSelectedSessionIds((current) => current.filter((sessionId) => !restored.has(sessionId)));
377
+ }
378
+ };
379
+ const closeDelete = () => {
380
+ if (!busy) setDeleteTarget(null);
381
+ };
382
+ (0, react.useEffect)(() => {
383
+ if (deleteTarget === null) return;
384
+ const onKeyDown = (event) => {
385
+ if (event.key !== "Escape") return;
386
+ event.preventDefault();
387
+ event.stopPropagation();
388
+ if (typeof event.stopImmediatePropagation === "function") event.stopImmediatePropagation();
389
+ if (!busy) setDeleteTarget(null);
390
+ };
391
+ window.addEventListener("keydown", onKeyDown, true);
392
+ return () => window.removeEventListener("keydown", onKeyDown, true);
393
+ }, [deleteTarget, busy]);
394
+ const confirmDelete = async () => {
395
+ if (busy || deleteTarget === null) return;
396
+ setBusy(true);
397
+ setError(null);
398
+ setNotice(null);
399
+ try {
400
+ if (deleteTarget.kind === "batch") {
401
+ const result = await deleteArchivedSessions(deleteTarget.target);
402
+ const feedback = archivedDeleteFeedback(result, t);
403
+ if (feedback.kind === "error") setError(feedback.message);
404
+ else setNotice(feedback.message);
405
+ if (deleteTarget.target.scope === "sessions") {
406
+ const completed = new Set([...result.deletedSessionIds, ...result.skippedSessionIds]);
407
+ setSelectedSessionIds((current) => current.filter((sessionId) => !completed.has(sessionId)));
408
+ }
409
+ } else {
410
+ await deleteSession(deleteTarget.session.id);
411
+ }
412
+ setDeleteTarget(null);
413
+ } catch (reason) {
414
+ setError(formatDeleteError(reason, t));
415
+ } finally {
416
+ setBusy(false);
417
+ }
418
+ };
419
+ const batchScope = deleteTarget?.kind === "batch" ? deleteTarget.target.scope : null;
420
+ const deleteDialogTitle = batchScope === "all" ? t("archives.deleteAllTitle") : batchScope === "ungrouped" ? t("archives.deleteUngroupedTitle") : batchScope === "workspace" ? t("archives.deleteProjectTitle", { name: deleteTarget.title }) : batchScope === "sessions" ? t("archives.deleteSelectedTitle") : t("deleteSession.title");
421
+ const deleteDialogDescription = deleteTarget === null ? void 0 : batchScope === "all" ? t("archives.deleteAllDesc", { n: deleteTarget.count }) : batchScope === "ungrouped" ? t("archives.deleteUngroupedDesc", { n: deleteTarget.count }) : batchScope === "workspace" ? t("archives.deleteProjectDesc", { name: deleteTarget.title, n: deleteTarget.count }) : batchScope === "sessions" ? t("archives.deleteSelectedDesc", { n: deleteTarget.count }) : t("deleteSession.desc", { name: displayTitle(deleteTarget.session, t) });
422
+ const deleteConfirmLabel = batchScope === "all" ? t("archives.deleteAll") : batchScope === "ungrouped" ? t("archives.deleteUngroupedConfirm") : batchScope === "workspace" ? t("archives.deleteProjectConfirm") : batchScope === "sessions" ? t("archives.deleteSelectedConfirm") : t("deleteSession.title");
423
+ return (0, react_jsx_runtime.jsxs)("section", {
424
+ className: "dsham_settings",
425
+ "aria-label": t("archives.title"),
426
+ children: [(0, react_jsx_runtime.jsx)("style", { children: ARCHIVE_SETTINGS_CSS + ARCHIVE_SETTINGS_BATCH_CSS + ARCHIVE_SETTINGS_SELECTION_CSS }), (0, react_jsx_runtime.jsxs)("header", {
427
+ className: "dsham_settingsHeader",
428
+ children: [(0, react_jsx_runtime.jsxs)("div", {
429
+ children: [(0, react_jsx_runtime.jsx)("h2", { children: t("archives.title") }), (0, react_jsx_runtime.jsx)("p", { className: "dsham_settingsIntro", children: t("archives.description") })]
430
+ }), (0, react_jsx_runtime.jsxs)("div", {
431
+ className: "dsham_settingsHeaderActions",
432
+ children: [(0, react_jsx_runtime.jsx)("button", { type: "button", className: "dsham_settingsRestoreAll", disabled: busy || allBatchSessionIds.length === 0, onClick: () => onBatchUnarchive(allBatchTarget), children: t("archives.restoreAll") }), (0, react_jsx_runtime.jsxs)("button", { type: "button", className: "dsham_settingsDanger", disabled: busy || allBatchSessionIds.length === 0, onClick: () => setDeleteTarget({ kind: "batch", target: allBatchTarget, title: t("archives.allProjects"), count: allBatchSessionIds.length }), children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, {}), t("archives.deleteAll")] })]
433
+ })]
434
+ }), (0, react_jsx_runtime.jsxs)("div", {
435
+ className: "dsham_settingsToolbar",
436
+ children: [(0, react_jsx_runtime.jsxs)("label", {
437
+ className: "dsham_settingsSearch",
438
+ children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16, {}), (0, react_jsx_runtime.jsx)("input", { type: "search", value: query, onChange: (event) => setQuery(event.target.value), placeholder: t("archives.searchPlaceholder"), "aria-label": t("archives.searchPlaceholder") })]
439
+ }), (0, react_jsx_runtime.jsx)(ArchiveProjectSelect, { id: "dsham-sort-filter", value: sortBy, options: [{ value: "updated", label: t("archives.sortUpdated") }, { value: "created", label: t("archives.sortCreated") }, { value: "alphabetical", label: t("archives.sortAlphabetical") }], onChange: setSortBy, "aria-label": t("archives.sortBy") }), (0, react_jsx_runtime.jsx)(ArchiveProjectSelect, { id: "dsham-project-filter", value: project, options: [{ value: "all", label: t("archives.allProjects") }, ...sortedGroups.map((group) => ({ value: group.key, label: group.title }))], onChange: setProject, "aria-label": t("archives.projectFilter") })]
440
+ }), groups.length > 0 && (0, react_jsx_runtime.jsxs)("div", {
441
+ className: "dsham_settingsSelection",
442
+ children: [(0, react_jsx_runtime.jsxs)("label", {
443
+ className: "dsham_settingsSelectionToggle",
444
+ children: [(0, react_jsx_runtime.jsx)(ArchiveSelectionCheckbox, { checked: allVisibleSelected, indeterminate: selectedVisibleCount > 0 && !allVisibleSelected, disabled: busy || visibleSessionIds.length === 0, label: t("archives.selectAllFiltered"), onChange: (event) => toggleVisibleSelection(event.target.checked) }), t("archives.selectAllFiltered")]
445
+ }), (0, react_jsx_runtime.jsx)("span", { className: "dsham_settingsSelectionCount", children: t("archives.selectedCount", { n: selectedSessionIds.length }) }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: "dsham_settingsSelectionAction", disabled: busy || selectedSessionIds.length === 0, onClick: onSelectedUnarchive, children: t("archives.restoreSelected") }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: "dsham_settingsSelectionAction dsham_settingsSelectionDelete", disabled: busy || selectedSessionIds.length === 0, onClick: () => setDeleteTarget({ kind: "batch", target: { scope: "sessions", sessionIds: selectedSessionIds }, count: selectedSessionIds.length }), children: t("archives.deleteSelected") })]
446
+ }), groups.length === 0 ? (0, react_jsx_runtime.jsx)("div", { className: "dsham_settingsEmpty", children: t("archives.empty") }) : filteredGroups.length === 0 ? (0, react_jsx_runtime.jsx)("div", { className: "dsham_settingsEmpty", children: t("archives.emptyFiltered") }) : filteredGroups.map((group) => {
447
+ const target = archivedBatchTargetForGroup(group.key);
448
+ const count = deriveArchivedBatchIds(workspaceState.archivedSessionIds, workspaceState.items, target).length;
449
+ return (0, react_jsx_runtime.jsxs)("section", {
450
+ className: "dsham_settingsGroup",
451
+ children: [(0, react_jsx_runtime.jsxs)("div", {
452
+ className: "dsham_settingsGroupHeading",
453
+ children: [(0, react_jsx_runtime.jsxs)("h3", { className: "dsham_settingsGroupTitle", children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconFolderOpenOutline16, {}), group.title] }), (0, react_jsx_runtime.jsxs)("div", { className: "dsham_settingsGroupMeta", children: [(0, react_jsx_runtime.jsx)("span", { className: "dsham_settingsCount", children: t("archives.sessionCount", { n: count }) }), (0, react_jsx_runtime.jsx)(ArchivedGroupActions, { group, busy, onRestore: () => onBatchUnarchive(target), onDelete: () => setDeleteTarget({ kind: "batch", target, title: group.title, count }), t })] })]
454
+ }), (0, react_jsx_runtime.jsx)("div", {
455
+ className: "dsham_settingsList",
456
+ children: group.sessions.map((session) => (0, react_jsx_runtime.jsxs)("article", {
457
+ className: "dsham_settingsRow",
458
+ children: [(0, react_jsx_runtime.jsx)(ArchiveSelectionCheckbox, { checked: selectedSessionIdSet.has(session.id), disabled: busy, label: t("archives.selectSession", { name: displayTitle(session, t) }), onChange: (event) => toggleSessionSelection(session.id, event.target.checked) }), (0, react_jsx_runtime.jsxs)("div", { className: "dsham_settingsContent", children: [(0, react_jsx_runtime.jsx)("div", { className: "dsham_settingsTitle", children: displayTitle(session, t) }), (0, react_jsx_runtime.jsx)("div", { className: "dsham_settingsMeta", children: archiveTimeLabel(session.updatedAt, t) })] }), (0, react_jsx_runtime.jsxs)("div", {
459
+ className: "dsham_settingsActions",
460
+ children: [(0, react_jsx_runtime.jsx)("button", { type: "button", className: "dsham_settingsAction", disabled: busy || unarchivingSessionIds.has(session.id), onClick: () => onUnarchive(session.id), children: t("menu.unarchive") }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: "dsham_settingsDelete", disabled: busy || unarchivingSessionIds.has(session.id), "aria-label": t("menu.deleteSession"), onClick: () => setDeleteTarget({ kind: "session", session }), children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, {}) })]
461
+ })]
462
+ }, session.id))
463
+ })]
464
+ }, group.key);
465
+ }), error !== null && (0, react_jsx_runtime.jsx)("div", { className: "dsham_settingsError", role: "alert", children: error }), notice !== null && (0, react_jsx_runtime.jsx)("div", { className: "dsham_settingsStatus", role: "status", children: notice }), (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
466
+ open: deleteTarget !== null,
467
+ onClose: closeDelete,
468
+ closeLabel: t("close"),
469
+ title: deleteDialogTitle,
470
+ ...deleteDialogDescription === void 0 ? {} : { description: deleteDialogDescription },
471
+ footer: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, { variant: "outline", disabled: busy, onClick: closeDelete, children: t("cancel") }), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, { variant: "outline", disabled: busy, onClick: confirmDelete, children: deleteConfirmLabel })] }),
472
+ children: busy && (0, react_jsx_runtime.jsx)("div", { role: "status", children: deleteTarget?.kind === "batch" ? t("archives.deleteBatchPending") : t("deleteSession.pending") })
473
+ })]
474
+ });
475
+ }
286
476
  //#endregion
287
477
  //#region ../../../node_modules/.pnpm/clsx@2.1.1/node_modules/clsx/dist/clsx.mjs
288
478
  function r(e) {
@@ -413,7 +603,26 @@ window.__ModuleLoader__.load({
413
603
  if (stray.length > 0) groups.push(buildGroup("", void 0, void 0, void 0, UNGROUPED_LABEL, ungroupedOrder === void 0 ? stray : orderedUngrouped(stray, ungroupedOrder), ungroupedOrder === void 0 ? "recency" : "account"));
414
604
  return groups;
415
605
  }
416
- function sessionNode(s, descendants, archived) {
606
+ /** 只把侧栏已支持的待处理交互类型交给行渲染,避免未知类型触发断言。 */
607
+ function visiblePendingKind(kind) {
608
+ switch (kind) {
609
+ case "approval":
610
+ case "plan-review":
611
+ case "question": return kind;
612
+ default: return;
613
+ }
614
+ }
615
+ function pendingInteractionForSession(session, pendingInteractions) {
616
+ const entry = pendingInteractions.get(session.id);
617
+ const kind = entry === void 0 ? session.pendingInteraction : entry.kind;
618
+ return visiblePendingKind(kind);
619
+ }
620
+ const EMPTY_PENDING_INTERACTIONS = /* @__PURE__ */ new Map();
621
+ function useEmptySessionPendingInteraction(selector) {
622
+ return selector(EMPTY_PENDING_INTERACTIONS);
623
+ }
624
+ function sessionNode(s, descendants, archived, pendingInteractions) {
625
+ const pendingInteraction = pendingInteractionForSession(s, pendingInteractions);
417
626
  return {
418
627
  id: s.id,
419
628
  title: sessionTitle(s),
@@ -423,7 +632,7 @@ window.__ModuleLoader__.load({
423
632
  completed: s.completed === true,
424
633
  updatedAt: s.updatedAt,
425
634
  archived: archived.has(s.id),
426
- ...s.pendingInteraction === void 0 ? {} : { pendingInteraction: s.pendingInteraction }
635
+ ...pendingInteraction === void 0 ? {} : { pendingInteraction }
427
636
  };
428
637
  }
429
638
  /**
@@ -438,10 +647,11 @@ window.__ModuleLoader__.load({
438
647
  * @param list - sessions list snapshot (`current` feeds containsCurrent).
439
648
  * @param workspaces - real workspaces in stable Host order.
440
649
  * @param archivedSessionIds - registry-global archive set.
650
+ * @param pendingInteractions - pending UI interactions by Session.
441
651
  * @param view - local expansion arrays and the show-archived toggle.
442
652
  * @returns group sections in render order.
443
653
  */
444
- function deriveGroups(list, workspaces, archivedSessionIds, view) {
654
+ function deriveGroups(list, workspaces, archivedSessionIds, pendingInteractions, view) {
445
655
  const archived = new Set(archivedSessionIds);
446
656
  const expandedGroups = new Set(view.expandedGroups);
447
657
  const descendants = indexSubagentDescendants(list.byId);
@@ -458,7 +668,7 @@ window.__ModuleLoader__.load({
458
668
  sessionCount: g.sessions.length,
459
669
  expanded,
460
670
  containsCurrent: g.key === currentGroup,
461
- sessions: expanded ? g.sessions.map((session) => sessionNode(session, descendants, archived)) : []
671
+ sessions: expanded ? g.sessions.map((session) => sessionNode(session, descendants, archived, pendingInteractions)) : []
462
672
  });
463
673
  }
464
674
  return groups;
@@ -472,10 +682,11 @@ window.__ModuleLoader__.load({
472
682
  * (see {@link deriveSearchResults}).
473
683
  * @param list - sessions list snapshot.
474
684
  * @param archivedSessionIds - registry-global archive set.
685
+ * @param pendingInteractions - pending UI interactions by Session.
475
686
  * @param showArchived - "show archived" view toggle.
476
687
  * @returns flat rows in render order.
477
688
  */
478
- function deriveFlat(list, archivedSessionIds, showArchived) {
689
+ function deriveFlat(list, archivedSessionIds, pendingInteractions, showArchived) {
479
690
  const archived = new Set(archivedSessionIds);
480
691
  const descendants = indexSubagentDescendants(list.byId);
481
692
  const rows = [];
@@ -485,7 +696,7 @@ window.__ModuleLoader__.load({
485
696
  rows.push(s);
486
697
  }
487
698
  rows.sort(byRecency);
488
- return rows.map((session) => sessionNode(session, descendants, archived));
699
+ return rows.map((session) => sessionNode(session, descendants, archived, pendingInteractions));
489
700
  }
490
701
  /**
491
702
  * Merge immediate title/Workspace substring matches with ranked Host content
@@ -496,12 +707,13 @@ window.__ModuleLoader__.load({
496
707
  * @param workspaces - Workspace membership and display labels.
497
708
  * @param query - caller text; surrounding whitespace is ignored.
498
709
  * @param archivedSessionIds - registry-global archive set.
710
+ * @param pendingInteractions - pending UI interactions by Session.
499
711
  * @param content - ranked Host content-search page.
500
712
  * @param limit - protocol-owned maximum merged row count.
501
713
  * @param showArchived - "show archived" view toggle.
502
714
  * @returns bounded deduplicated flat rows and a refine-query hint bit.
503
715
  */
504
- function deriveSearchResults(list, workspaces, query, archivedSessionIds, content, limit, showArchived) {
716
+ function deriveSearchResults(list, workspaces, query, archivedSessionIds, pendingInteractions, content, limit, showArchived) {
505
717
  const q = query.trim().toLowerCase();
506
718
  if (q === "") return {
507
719
  items: [],
@@ -536,6 +748,7 @@ window.__ModuleLoader__.load({
536
748
  return {
537
749
  items: ordered.slice(0, limit).map((summary) => {
538
750
  const match = contentBySession.get(summary.id);
751
+ const pendingInteraction = pendingInteractionForSession(summary, pendingInteractions);
539
752
  return {
540
753
  id: summary.id,
541
754
  title: sessionTitle(summary),
@@ -543,7 +756,7 @@ window.__ModuleLoader__.load({
543
756
  running: summary.running,
544
757
  runningSubagentCount: descendants.get(summary.id)?.runningCount ?? 0,
545
758
  archived: archived.has(summary.id),
546
- ...summary.pendingInteraction === void 0 ? {} : { pendingInteraction: summary.pendingInteraction },
759
+ ...pendingInteraction === void 0 ? {} : { pendingInteraction },
547
760
  completed: summary.completed === true,
548
761
  ...match === void 0 ? {} : { snippet: match.snippet }
549
762
  };
@@ -1523,8 +1736,9 @@ window.__ModuleLoader__.load({
1523
1736
  return e.clientY < rect.top + rect.height / 2 ? "before" : "after";
1524
1737
  }
1525
1738
  /** The scrolling session tree; unmounting drops the sessions subscription and expand-all state. */
1526
- function SessionTree({ useSessions, startSession, open, forkSession, workspaces, archivedSessionIds, showArchived, onRenameRequest, onDeleteRequest, onSessionRename, onSessionArchive, onSessionUnarchive, onSessionDelete, insertWorkspaceBefore, insertSessionBefore, orderBy, groupExpansion, setGroupExpanded, sessionOrderByAccount, sessionUpdatedAtByAccount, syncSessionOrderAccount, setSessionOrder, t }) {
1739
+ function SessionTree({ useSessions, useSessionPendingInteraction, startSession, open, forkSession, workspaces, archivedSessionIds, showArchived, onRenameRequest, onDeleteRequest, onSessionRename, onSessionArchive, onSessionUnarchive, onSessionDelete, insertWorkspaceBefore, insertSessionBefore, orderBy, groupExpansion, setGroupExpanded, sessionOrderByAccount, sessionUpdatedAtByAccount, syncSessionOrderAccount, setSessionOrder, t }) {
1527
1740
  const list = useSessions((s) => s);
1741
+ const pendingInteractions = useSessionPendingInteraction((s) => s);
1528
1742
  const current = list.current;
1529
1743
  const [expandedSessionGroups, setExpandedSessionGroups] = (0, react.useState)([]);
1530
1744
  const [drag, setDrag] = (0, react.useState)(null);
@@ -1591,7 +1805,7 @@ window.__ModuleLoader__.load({
1591
1805
  });
1592
1806
  }, [sessionOrderByAccount, workspaces]);
1593
1807
  const orderedUngroupedSessionIds = (0, react.useMemo)(() => reconciledSessionOrder(ungroupedSessionIds, sessionOrderByAccount[""]), [sessionOrderByAccount, ungroupedSessionIds]);
1594
- const groups = (0, react.useMemo)(() => deriveGroups(list, orderedWorkspaces, archivedSessionIds, {
1808
+ const groups = (0, react.useMemo)(() => deriveGroups(list, orderedWorkspaces, archivedSessionIds, pendingInteractions, {
1595
1809
  expandedGroups,
1596
1810
  showArchived,
1597
1811
  ...sessionOrderByAccount[""] === void 0 ? {} : { ungroupedOrder: sessionOrderByAccount[""] }
@@ -1599,6 +1813,7 @@ window.__ModuleLoader__.load({
1599
1813
  list,
1600
1814
  orderedWorkspaces,
1601
1815
  archivedSessionIds,
1816
+ pendingInteractions,
1602
1817
  showArchived,
1603
1818
  expandedGroups,
1604
1819
  sessionOrderByAccount
@@ -1800,9 +2015,15 @@ window.__ModuleLoader__.load({
1800
2015
  });
1801
2016
  }
1802
2017
  /** The flat "In one list" body: every session is one draggable top-level row. */
1803
- function FlatList({ useSessions, open, forkSession, onSessionRename, onSessionArchive, onSessionUnarchive, onSessionDelete, archivedSessionIds, showArchived, orderBy, sessionOrderByAccount, sessionUpdatedAtByAccount, syncSessionOrderAccount, setSessionOrder, t }) {
2018
+ function FlatList({ useSessions, useSessionPendingInteraction, open, forkSession, onSessionRename, onSessionArchive, onSessionUnarchive, onSessionDelete, archivedSessionIds, showArchived, orderBy, sessionOrderByAccount, sessionUpdatedAtByAccount, syncSessionOrderAccount, setSessionOrder, t }) {
1804
2019
  const list = useSessions((s) => s);
1805
- const baseRows = (0, react.useMemo)(() => deriveFlat(list, archivedSessionIds, showArchived), [list, archivedSessionIds, showArchived]);
2020
+ const pendingInteractions = useSessionPendingInteraction((s) => s);
2021
+ const baseRows = (0, react.useMemo)(() => deriveFlat(list, archivedSessionIds, pendingInteractions, showArchived), [
2022
+ list,
2023
+ archivedSessionIds,
2024
+ pendingInteractions,
2025
+ showArchived
2026
+ ]);
1806
2027
  const sessionIds = (0, react.useMemo)(() => baseRows.map((row) => row.id), [baseRows]);
1807
2028
  const previousOrderBy = (0, react.useRef)(orderBy);
1808
2029
  (0, react.useEffect)(() => {
@@ -1924,19 +2145,21 @@ window.__ModuleLoader__.load({
1924
2145
  });
1925
2146
  }
1926
2147
  /** Flat search body: local metadata matches plus the current Host result page. */
1927
- function SearchResults({ useSessions, open, workspaces, archivedSessionIds, showArchived, query, remote, resultLimit, t }) {
2148
+ function SearchResults({ useSessions, useSessionPendingInteraction, open, workspaces, archivedSessionIds, showArchived, query, remote, resultLimit, t }) {
1928
2149
  const list = useSessions((s) => s);
2150
+ const pendingInteractions = useSessionPendingInteraction((s) => s);
1929
2151
  const currentRemote = remote.query === query ? remote : {
1930
2152
  query,
1931
2153
  status: "loading",
1932
2154
  items: [],
1933
2155
  hasMore: false
1934
2156
  };
1935
- const results = (0, react.useMemo)(() => deriveSearchResults(list, workspaces, query, archivedSessionIds, currentRemote, resultLimit, showArchived), [
2157
+ const results = (0, react.useMemo)(() => deriveSearchResults(list, workspaces, query, archivedSessionIds, pendingInteractions, currentRemote, resultLimit, showArchived), [
1936
2158
  list,
1937
2159
  workspaces,
1938
2160
  query,
1939
2161
  archivedSessionIds,
2162
+ pendingInteractions,
1940
2163
  currentRemote,
1941
2164
  resultLimit,
1942
2165
  showArchived
@@ -1986,7 +2209,8 @@ window.__ModuleLoader__.load({
1986
2209
  * @param props - composed slot props (shell owner share + store + injected actions).
1987
2210
  * @returns the region element tree.
1988
2211
  */
1989
- function WorkspaceBrowser({ wide, expandSidebar, useSessions, useWorkspaces, useStore, actions, startSession, open, renameSession, forkSession, renameWorkspace, deleteWorkspace, insertWorkspaceBefore, archiveSession, unarchiveSession, deleteSession, insertSessionBefore, createWorkspace, searchSessions, searchResultLimit, useDirectoryFlow, renderSlot, t }) {
2212
+ function WorkspaceBrowser({ wide, expandSidebar, useSessions, useSessionPendingInteraction, useWorkspaces, useStore, actions, startSession, open, renameSession, forkSession, renameWorkspace, deleteWorkspace, insertWorkspaceBefore, archiveSession, unarchiveSession, deleteSession, insertSessionBefore, createWorkspace, searchSessions, searchResultLimit, useDirectoryFlow, renderSlot, t }) {
2213
+ const useEffectiveSessionPendingInteraction = useSessionPendingInteraction ?? useEmptySessionPendingInteraction;
1990
2214
  const workspaces = useWorkspaces((state) => state.items);
1991
2215
  const workspacePhase = useWorkspaces((state) => state.phase);
1992
2216
  const archivedSessionIds = useWorkspaces((state) => state.archivedSessionIds);
@@ -2381,6 +2605,7 @@ window.__ModuleLoader__.load({
2381
2605
  className: WorkspaceBrowser_module_css_default.listArea,
2382
2606
  children: wide && (normalizedQuery !== "" ? (0, react_jsx_runtime.jsx)(SearchResults, {
2383
2607
  useSessions,
2608
+ useSessionPendingInteraction: useEffectiveSessionPendingInteraction,
2384
2609
  open: guardedOpen,
2385
2610
  workspaces,
2386
2611
  archivedSessionIds,
@@ -2391,6 +2616,7 @@ window.__ModuleLoader__.load({
2391
2616
  t
2392
2617
  }) : groupBy === "flat" ? (0, react_jsx_runtime.jsx)(FlatList, {
2393
2618
  useSessions,
2619
+ useSessionPendingInteraction: useEffectiveSessionPendingInteraction,
2394
2620
  open: guardedOpen,
2395
2621
  forkSession,
2396
2622
  onSessionRename,
@@ -2410,6 +2636,7 @@ window.__ModuleLoader__.load({
2410
2636
  t
2411
2637
  }) : (0, react_jsx_runtime.jsx)(SessionTree, {
2412
2638
  useSessions,
2639
+ useSessionPendingInteraction: useEffectiveSessionPendingInteraction,
2413
2640
  onSessionRename,
2414
2641
  onSessionArchive,
2415
2642
  onSessionUnarchive,
@@ -2622,6 +2849,7 @@ window.__ModuleLoader__.load({
2622
2849
  //#region dsh-archive-manager: settings section
2623
2850
  const ARCHIVE_SETTINGS_CSS = ".dsham_settings{box-sizing:border-box;width:min(100%,760px);margin:0 auto;padding:0 0 32px;color:var(--dsw-alias-label-primary)}.dsham_settingsHeader{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:12px}.dsham_settings h2{margin:0;font-size:20px;font-weight:650;letter-spacing:-.2px;line-height:28px}.dsham_settingsIntro{margin:4px 0 0;max-width:42em;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}.dsham_settingsDanger{display:inline-flex;align-items:center;gap:6px;min-height:32px;padding:0 12px;color:var(--dsw-alias-state-error-primary);background:transparent;border:1px solid var(--dsw-alias-state-error-primary);border-radius:8px;cursor:pointer;font:inherit;font-size:13px;font-weight:500}.dsham_settingsDanger:hover{background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 20%,transparent)}.dsham_settingsToolbar{display:flex;gap:8px;margin-bottom:16px}.dsham_settingsSearch{display:flex;align-items:center;gap:8px;min-width:0;flex:1;height:32px;padding:0 12px;color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-button-elevated-fill));border:1px solid var(--dsw-alias-border-l2);border-radius:8px}.dsham_settingsSearch:focus-within{border-color:var(--dsw-alias-label-tertiary)}.dsham_settingsSearch input{width:100%;min-width:0;padding:0;color:var(--dsw-alias-label-primary);background:transparent;border:0;outline:0;font:inherit;font-size:12px}.dsham_settingsSearch input::placeholder{color:var(--dsw-alias-label-tertiary)}.dsham_settingsFilter{position:relative;min-width:168px;flex:none}.dsham_selectTrigger{box-sizing:border-box;display:flex;align-items:center;justify-content:space-between;gap:8px;width:100%;min-height:32px;padding:0 10px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3,var(--dsw-alias-button-elevated-fill));border:1px solid var(--dsw-alias-border-l2);border-radius:8px;cursor:pointer;font:inherit;font-size:13px;line-height:20px;text-align:left}.dsham_selectTrigger:hover{background:var(--dsw-alias-interactive-bg-hover)}.dsham_selectTrigger:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}.dsham_selectTrigger[aria-expanded='true']{border-color:var(--dsw-alias-state-success-primary)}.dsham_selectValue{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dsham_selectCaret{flex:none;width:12px;height:12px;color:var(--dsw-alias-label-tertiary)}.dsham_selectMenu{position:absolute;top:calc(100% + 4px);left:0;right:0;z-index:30;box-sizing:border-box;min-width:100%;max-height:280px;overflow:auto;padding:4px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-specific-menu,var(--dsw-alias-bg-layer-2));box-shadow:var(--dsw-shadow-lv3)}.dsham_selectOption{box-sizing:border-box;display:flex;align-items:center;width:100%;min-height:32px;padding:0 10px;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:20px;text-align:left;cursor:pointer}.dsham_selectOption:hover,.dsham_selectOption[data-active='true']{background:var(--dsw-alias-interactive-bg-hover)}.dsham_selectOption[aria-selected='true']{color:var(--dsw-alias-state-success-primary)}.dsham_settingsGroup{margin:0 0 20px}.dsham_settingsGroupHeading{display:flex;align-items:center;justify-content:space-between;gap:12px;margin:0 0 14px}.dsham_settingsGroupTitle{display:flex;align-items:center;gap:8px;min-width:0;margin:0;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600}.dsham_settingsGroupTitle svg{flex:none;color:var(--dsw-alias-label-secondary)}.dsham_settingsCount{flex:none;color:var(--dsw-alias-label-tertiary);font-size:12px}.dsham_settingsList{overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-button-elevated-fill))}.dsham_settingsRow{display:flex;align-items:center;gap:12px;min-height:60px;padding:10px 16px;border-bottom:1px solid var(--dsw-alias-border-l2)}.dsham_settingsRow:last-child{border-bottom:0}.dsham_settingsContent{min-width:0;flex:1}.dsham_settingsTitle{overflow:hidden;color:var(--dsw-alias-label-primary);font-size:13px;font-weight:600;line-height:18px;text-overflow:ellipsis;white-space:nowrap}.dsham_settingsMeta{margin-top:2px;color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:16px}.dsham_settingsActions{display:flex;align-items:center;gap:8px}.dsham_settingsAction{min-height:32px;padding:0 12px;color:var(--dsw-alias-label-primary);background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;cursor:pointer;font:inherit;font-size:13px;font-weight:500}.dsham_settingsAction:hover{filter:brightness(1.12)}.dsham_settingsDelete{display:flex;align-items:center;justify-content:center;width:28px;height:28px;color:var(--dsw-alias-label-tertiary);background:transparent;border:0;border-radius:8px;cursor:pointer}.dsham_settingsDelete:hover{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover)}.dsham_settingsEmpty{padding:28px 8px;color:var(--dsw-alias-label-secondary);text-align:center}.dsham_settingsError{margin-top:10px;color:var(--dsw-alias-state-error-primary);font-size:12px}@media(max-width:720px){.dsham_settings{width:100%;margin:28px auto 48px;padding:0 16px}.dsham_settingsHeader{margin-bottom:28px}.dsham_settingsToolbar{flex-wrap:wrap;margin-bottom:28px}.dsham_settingsSearch{flex-basis:100%}.dsham_settingsFilter{flex:1;min-width:0}.dsham_settingsGroup{margin-bottom:32px}.dsham_settingsRow{padding:10px 12px}.dsham_settingsActions{gap:4px}}";
2624
2851
  const ARCHIVE_SETTINGS_BATCH_CSS = ".dsham_settingsHeaderActions,.dsham_settingsGroupMeta{display:flex;align-items:center;gap:8px;flex:none}.dsham_settingsRestoreAll{display:inline-flex;align-items:center;min-height:32px;padding:0 12px;color:var(--dsw-alias-label-primary);background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;cursor:pointer;font:inherit;font-size:13px;font-weight:500}.dsham_settingsRestoreAll:hover{background:var(--dsw-alias-interactive-bg-hover)}.dsham_settingsRestoreAll:disabled,.dsham_settingsDanger:disabled,.dsham_settingsGroupMenu:disabled{cursor:not-allowed;opacity:.5}.dsham_settingsGroupMenu{display:flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;color:var(--dsw-alias-label-tertiary);background:transparent;border:0;border-radius:8px;cursor:pointer}.dsham_settingsGroupMenu:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.dsham_settingsStatus{margin-top:10px;color:var(--dsw-alias-label-secondary);font-size:12px}@media(max-width:720px){.dsham_settingsHeader{flex-direction:column}.dsham_settingsHeaderActions{align-self:flex-end}}";
2852
+ const ARCHIVE_SETTINGS_SELECTION_CSS = ".dsham_settingsSelection{display:flex;align-items:center;gap:10px;min-height:40px;margin:-8px 0 16px;padding:4px 10px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-button-elevated-fill))}.dsham_settingsSelectionToggle{display:inline-flex;align-items:center;gap:8px;min-width:0;color:var(--dsw-alias-label-primary);font-size:13px;cursor:pointer}.dsham_settingsCheckbox{width:16px;height:16px;margin:0;accent-color:#3b82f6}.dsham_settingsSelectionCount{margin-right:auto;color:var(--dsw-alias-label-tertiary);font-size:12px}.dsham_settingsSelectionAction{min-height:28px;padding:0 10px;color:var(--dsw-alias-label-primary);background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;cursor:pointer;font:inherit;font-size:12px;font-weight:500}.dsham_settingsSelectionAction:hover{background:var(--dsw-alias-interactive-bg-hover)}.dsham_settingsSelectionDelete{color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary)}.dsham_settingsSelectionAction:disabled{cursor:not-allowed;opacity:.5}@media(max-width:720px){.dsham_settingsSelection{flex-wrap:wrap}.dsham_settingsSelectionCount{margin-right:0}.dsham_settingsSelectionAction{margin-left:auto}.dsham_settingsSelectionAction+.dsham_settingsSelectionAction{margin-left:-2px}}";
2625
2853
  const ARCHIVE_SETTINGS_LAYOUT_OVERRIDE = ".dsham_settings{margin:0 auto!important}@media(max-width:720px){.dsham_settings{margin:0 auto!important}}";
2626
2854
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify("dsh-archive-manager/ArchiveSettings.layout.css") + "]") === null) {
2627
2855
  const tag = document.createElement("style");
@@ -2762,6 +2990,22 @@ window.__ModuleLoader__.load({
2762
2990
  }) : null]
2763
2991
  });
2764
2992
  }
2993
+ /** 原生 checkbox 保留浏览器的正确键盘语义,并显式同步三态显示。 */
2994
+ function ArchiveSelectionCheckbox({ checked, indeterminate = false, disabled = false, label, onChange }) {
2995
+ const inputRef = (0, react.useRef)(null);
2996
+ (0, react.useEffect)(() => {
2997
+ if (inputRef.current !== null) inputRef.current.indeterminate = indeterminate;
2998
+ }, [indeterminate]);
2999
+ return (0, react_jsx_runtime.jsx)("input", {
3000
+ ref: inputRef,
3001
+ type: "checkbox",
3002
+ className: "dsham_settingsCheckbox",
3003
+ checked,
3004
+ disabled,
3005
+ "aria-label": label,
3006
+ onChange
3007
+ });
3008
+ }
2765
3009
  /** 项目标题右侧的批量恢复/删除菜单,复用宿主菜单组件的键盘和焦点行为。 */
2766
3010
  function ArchivedGroupActions({ group, busy, onRestore, onDelete, t }) {
2767
3011
  const [open, setOpen] = (0, react.useState)(false);
@@ -2805,6 +3049,10 @@ window.__ModuleLoader__.load({
2805
3049
  function deriveArchivedBatchIds(archivedSessionIds, items, target) {
2806
3050
  const ids = [...new Set(archivedSessionIds ?? [])];
2807
3051
  if (target.scope === "all") return ids;
3052
+ if (target.scope === "sessions") {
3053
+ const selected = new Set(target.sessionIds ?? []);
3054
+ return ids.filter((id) => selected.has(id));
3055
+ }
2808
3056
  if (target.scope === "workspace") {
2809
3057
  const accounted = new Set(items.find((workspace) => workspace.workspaceId === target.workspaceId)?.sessionIds ?? []);
2810
3058
  return ids.filter((id) => accounted.has(id));
@@ -2812,6 +3060,24 @@ window.__ModuleLoader__.load({
2812
3060
  const accounted = new Set(items.flatMap((workspace) => workspace.sessionIds));
2813
3061
  return ids.filter((id) => !accounted.has(id));
2814
3062
  }
3063
+ /** 当前筛选结果中的可选会话;按列表显示顺序去重。 */
3064
+ function archivedSessionIdsInGroups(groups) {
3065
+ return [...new Set(groups.flatMap((group) => group.sessions.map((session) => session.id)))];
3066
+ }
3067
+ /** 只保留仍属于权威归档集合的选中项,避免对已变化的列表执行旧操作。 */
3068
+ function pruneArchivedSelection(selectedSessionIds, archivedSessionIds) {
3069
+ const archived = new Set(archivedSessionIds);
3070
+ return [...new Set(selectedSessionIds)].filter((id) => archived.has(id));
3071
+ }
3072
+ /** 向当前选择中加入或移除一组可见会话,供“全选筛选结果”复用。 */
3073
+ function toggleArchivedSelection(selectedSessionIds, sessionIds, checked) {
3074
+ const selected = new Set(selectedSessionIds);
3075
+ for (const sessionId of sessionIds) {
3076
+ if (checked) selected.add(sessionId);
3077
+ else selected.delete(sessionId);
3078
+ }
3079
+ return [...selected];
3080
+ }
2815
3081
  /** 把批量删除结果转换为准确区分“已删除”与“陈旧标记已清理”的反馈。 */
2816
3082
  function archivedDeleteFeedback(result, t) {
2817
3083
  const deleted = result.deletedSessionIds.length;
@@ -2898,6 +3164,7 @@ window.__ModuleLoader__.load({
2898
3164
  const [createdAtById, setCreatedAtById] = (0, react.useState)({});
2899
3165
  const [unarchivingSessionIds, setUnarchivingSessionIds] = (0, react.useState)(() => /* @__PURE__ */ new Set());
2900
3166
  const unarchivingSessionIdsRef = (0, react.useRef)(/* @__PURE__ */ new Set());
3167
+ const [selectedSessionIds, setSelectedSessionIds] = (0, react.useState)([]);
2901
3168
  const groups = (0, react.useMemo)(() => deriveArchivedGroups(sessions.byId, workspaceState.items, workspaceState.archivedSessionIds, t("group.ungrouped")), [sessions.byId, workspaceState, t]);
2902
3169
  const sortedGroups = (0, react.useMemo)(() => sortArchivedGroups(groups, sortBy, createdAtById, t), [groups, sortBy, createdAtById, t]);
2903
3170
  (0, react.useEffect)(() => {
@@ -2926,8 +3193,21 @@ window.__ModuleLoader__.load({
2926
3193
  sessions: group.sessions.filter((session) => normalizedQuery === "" || displayTitle(session, t).toLocaleLowerCase().includes(normalizedQuery))
2927
3194
  })).filter((group) => group.sessions.length > 0);
2928
3195
  }, [sortedGroups, project, query, t]);
3196
+ const visibleSessionIds = (0, react.useMemo)(() => archivedSessionIdsInGroups(filteredGroups), [filteredGroups]);
3197
+ const selectedSessionIdSet = (0, react.useMemo)(() => new Set(selectedSessionIds), [selectedSessionIds]);
3198
+ const selectedVisibleCount = visibleSessionIds.filter((sessionId) => selectedSessionIdSet.has(sessionId)).length;
3199
+ const allVisibleSelected = visibleSessionIds.length > 0 && selectedVisibleCount === visibleSessionIds.length;
2929
3200
  const allBatchTarget = { scope: "all" };
2930
3201
  const allBatchSessionIds = (0, react.useMemo)(() => deriveArchivedBatchIds(workspaceState.archivedSessionIds, workspaceState.items, allBatchTarget), [workspaceState.archivedSessionIds, workspaceState.items]);
3202
+ (0, react.useEffect)(() => {
3203
+ setSelectedSessionIds((current) => pruneArchivedSelection(current, workspaceState.archivedSessionIds));
3204
+ }, [workspaceState.archivedSessionIds]);
3205
+ const toggleSessionSelection = (sessionId, checked) => {
3206
+ setSelectedSessionIds((current) => toggleArchivedSelection(current, [sessionId], checked));
3207
+ };
3208
+ const toggleVisibleSelection = (checked) => {
3209
+ setSelectedSessionIds((current) => toggleArchivedSelection(current, visibleSessionIds, checked));
3210
+ };
2931
3211
  const onUnarchive = (sessionId) => {
2932
3212
  if (busy || unarchivingSessionIdsRef.current.has(sessionId)) return;
2933
3213
  unarchivingSessionIdsRef.current.add(sessionId);
@@ -2949,12 +3229,21 @@ window.__ModuleLoader__.load({
2949
3229
  try {
2950
3230
  const result = await unarchiveSessions(target);
2951
3231
  setNotice(t("archives.restoreSuccess", { n: result.unarchivedSessionIds.length }));
3232
+ return result;
2952
3233
  } catch (reason) {
2953
3234
  setError(t("archives.restoreBatchFailed", { detail: reason instanceof Error ? reason.message : String(reason) }));
2954
3235
  } finally {
2955
3236
  setBusy(false);
2956
3237
  }
2957
3238
  };
3239
+ const onSelectedUnarchive = async () => {
3240
+ if (selectedSessionIds.length === 0) return;
3241
+ const result = await onBatchUnarchive({ scope: "sessions", sessionIds: selectedSessionIds });
3242
+ if (result !== void 0) {
3243
+ const restored = new Set(result.unarchivedSessionIds);
3244
+ setSelectedSessionIds((current) => current.filter((sessionId) => !restored.has(sessionId)));
3245
+ }
3246
+ };
2958
3247
  const closeDelete = () => {
2959
3248
  if (!busy) setDeleteTarget(null);
2960
3249
  };
@@ -2981,6 +3270,10 @@ window.__ModuleLoader__.load({
2981
3270
  const feedback = archivedDeleteFeedback(result, t);
2982
3271
  if (feedback.kind === "error") setError(feedback.message);
2983
3272
  else setNotice(feedback.message);
3273
+ if (deleteTarget.target.scope === "sessions") {
3274
+ const completed = new Set([...result.deletedSessionIds, ...result.skippedSessionIds]);
3275
+ setSelectedSessionIds((current) => current.filter((sessionId) => !completed.has(sessionId)));
3276
+ }
2984
3277
  } else {
2985
3278
  await deleteSession(deleteTarget.session.id);
2986
3279
  }
@@ -2992,9 +3285,9 @@ window.__ModuleLoader__.load({
2992
3285
  }
2993
3286
  };
2994
3287
  const batchScope = deleteTarget?.kind === "batch" ? deleteTarget.target.scope : null;
2995
- const deleteDialogTitle = batchScope === "all" ? t("archives.deleteAllTitle") : batchScope === "ungrouped" ? t("archives.deleteUngroupedTitle") : batchScope === "workspace" ? t("archives.deleteProjectTitle", { name: deleteTarget.title }) : t("deleteSession.title");
2996
- const deleteDialogDescription = deleteTarget === null ? void 0 : batchScope === "all" ? t("archives.deleteAllDesc", { n: deleteTarget.count }) : batchScope === "ungrouped" ? t("archives.deleteUngroupedDesc", { n: deleteTarget.count }) : batchScope === "workspace" ? t("archives.deleteProjectDesc", { name: deleteTarget.title, n: deleteTarget.count }) : t("deleteSession.desc", { name: displayTitle(deleteTarget.session, t) });
2997
- const deleteConfirmLabel = batchScope === "all" ? t("archives.deleteAll") : batchScope === "ungrouped" ? t("archives.deleteUngroupedConfirm") : batchScope === "workspace" ? t("archives.deleteProjectConfirm") : t("deleteSession.title");
3288
+ const deleteDialogTitle = batchScope === "all" ? t("archives.deleteAllTitle") : batchScope === "ungrouped" ? t("archives.deleteUngroupedTitle") : batchScope === "workspace" ? t("archives.deleteProjectTitle", { name: deleteTarget.title }) : batchScope === "sessions" ? t("archives.deleteSelectedTitle") : t("deleteSession.title");
3289
+ const deleteDialogDescription = deleteTarget === null ? void 0 : batchScope === "all" ? t("archives.deleteAllDesc", { n: deleteTarget.count }) : batchScope === "ungrouped" ? t("archives.deleteUngroupedDesc", { n: deleteTarget.count }) : batchScope === "workspace" ? t("archives.deleteProjectDesc", { name: deleteTarget.title, n: deleteTarget.count }) : batchScope === "sessions" ? t("archives.deleteSelectedDesc", { n: deleteTarget.count }) : t("deleteSession.desc", { name: displayTitle(deleteTarget.session, t) });
3290
+ const deleteConfirmLabel = batchScope === "all" ? t("archives.deleteAll") : batchScope === "ungrouped" ? t("archives.deleteUngroupedConfirm") : batchScope === "workspace" ? t("archives.deleteProjectConfirm") : batchScope === "sessions" ? t("archives.deleteSelectedConfirm") : t("deleteSession.title");
2998
3291
  return (0, react_jsx_runtime.jsxs)("section", {
2999
3292
  className: "dsham_settings",
3000
3293
  "aria-label": t("archives.title"),
@@ -3075,6 +3368,11 @@ window.__ModuleLoader__.load({
3075
3368
  "archives.projectFilter": "按项目筛选",
3076
3369
  "archives.allProjects": "所有项目",
3077
3370
  "archives.sessionCount": "{n} 个聊天",
3371
+ "archives.selectAllFiltered": "全选当前筛选结果",
3372
+ "archives.selectSession": "选择会话“{name}”",
3373
+ "archives.selectedCount": "已选 {n} 个",
3374
+ "archives.restoreSelected": "恢复所选",
3375
+ "archives.deleteSelected": "删除所选",
3078
3376
  "archives.timestamp": "{date},{time}",
3079
3377
  "archives.restoreAll": "全部恢复",
3080
3378
  "archives.restoreProject": "恢复该项目的全部聊天",
@@ -3088,6 +3386,9 @@ window.__ModuleLoader__.load({
3088
3386
  "archives.deleteAll": "全部删除",
3089
3387
  "archives.deleteAllTitle": "删除全部已归档聊天",
3090
3388
  "archives.deleteAllDesc": "将永久删除全部 {n} 个已归档聊天及其子代理(含正在运行的)和记录,此操作不可恢复。",
3389
+ "archives.deleteSelectedTitle": "删除所选已归档聊天",
3390
+ "archives.deleteSelectedDesc": "将永久删除所选的 {n} 个已归档聊天及其子代理(含正在运行的)和记录。其他聊天不会受影响,此操作不可恢复。",
3391
+ "archives.deleteSelectedConfirm": "删除所选聊天",
3091
3392
  "archives.deleteAllPending": "正在删除已归档聊天…",
3092
3393
  "archives.deleteProjectTitle": "删除“{name}”中的已归档聊天",
3093
3394
  "archives.deleteProjectDesc": "将永久删除“{name}”中的 {n} 个已归档聊天及其子代理和记录。项目目录和未归档聊天不会受影响,此操作不可恢复。",
@@ -3191,6 +3492,11 @@ window.__ModuleLoader__.load({
3191
3492
  "archives.projectFilter": "Filter by project",
3192
3493
  "archives.allProjects": "All projects",
3193
3494
  "archives.sessionCount": "{n} chats",
3495
+ "archives.selectAllFiltered": "Select all results",
3496
+ "archives.selectSession": "Select chat {name}",
3497
+ "archives.selectedCount": "{n} selected",
3498
+ "archives.restoreSelected": "Restore selected",
3499
+ "archives.deleteSelected": "Delete selected",
3194
3500
  "archives.timestamp": "{date}, {time}",
3195
3501
  "archives.restoreAll": "Restore all",
3196
3502
  "archives.restoreProject": "Restore all chats in this project",
@@ -3204,6 +3510,9 @@ window.__ModuleLoader__.load({
3204
3510
  "archives.deleteAll": "Delete all",
3205
3511
  "archives.deleteAllTitle": "Delete all archived chats",
3206
3512
  "archives.deleteAllDesc": "This permanently deletes all {n} archived chats, their child agents (including any that are still running), and their records. This cannot be undone.",
3513
+ "archives.deleteSelectedTitle": "Delete selected archived chats",
3514
+ "archives.deleteSelectedDesc": "This permanently deletes the selected {n} archived chats, their child agents (including any that are still running), and their records. Other chats are not affected. This cannot be undone.",
3515
+ "archives.deleteSelectedConfirm": "Delete selected chats",
3207
3516
  "archives.deleteAllPending": "Deleting archived chats…",
3208
3517
  "archives.deleteProjectTitle": "Delete archived chats in {name}",
3209
3518
  "archives.deleteProjectDesc": "This permanently deletes the {n} archived chats in {name}, their child agents, and their records. The project directory and unarchived chats are not affected. This cannot be undone.",
@@ -3482,11 +3791,20 @@ window.__ModuleLoader__.load({
3482
3791
  });
3483
3792
  const browserFlowSource = flowSource("sidebar.workspaces.directoryFlow");
3484
3793
  const pickerFlowSource = flowSource("conversation.hero.workspace.directoryFlow");
3794
+ const refreshSessionList = async () => {
3795
+ if (typeof ctx.sessions.refresh !== "function") return;
3796
+ try {
3797
+ await ctx.sessions.refresh();
3798
+ } catch (error) {
3799
+ console.warn("archive-manager: restored archived sessions but session list refresh failed:", error);
3800
+ }
3801
+ };
3485
3802
  const unarchiveSession = async (sessionId) => {
3486
3803
  const registry = ctx.get("remote.workspaceRegistry");
3487
3804
  if (registry === void 0) throw new Error("archive-manager remote service is unavailable");
3488
3805
  const result = await registry.unarchiveSession(sessionId);
3489
3806
  if (!result.ok) throw new Error(result.error.message);
3807
+ await refreshSessionList();
3490
3808
  };
3491
3809
  const deleteSession = async (sessionId) => {
3492
3810
  const registry = ctx.get("remote.workspaceRegistry");
@@ -3499,6 +3817,7 @@ window.__ModuleLoader__.load({
3499
3817
  if (registry === void 0) throw new Error("archive-manager remote service is unavailable");
3500
3818
  const result = await registry.unarchiveSessions(target);
3501
3819
  if (!result.ok) throw new Error(result.error.message);
3820
+ await refreshSessionList();
3502
3821
  return result.value;
3503
3822
  };
3504
3823
  const deleteArchivedSessions = async (target) => {
@@ -3605,7 +3924,7 @@ window.__ModuleLoader__.load({
3605
3924
  archivedSessionMetadata,
3606
3925
  t: ctx.locale.bind(NS)
3607
3926
  })
3608
- }, ArchivedSessionsSection));
3927
+ }, ArchivedSessionsSectionPrototype));
3609
3928
  }
3610
3929
  //#endregion
3611
3930
  /** Pure derivation surface for dsh-archive-manager self-tests (no-op for the runtime). */
@@ -3620,6 +3939,9 @@ window.__ModuleLoader__.load({
3620
3939
  deriveArchivedGroups,
3621
3940
  sortArchivedGroups,
3622
3941
  deriveArchivedBatchIds,
3942
+ archivedSessionIdsInGroups,
3943
+ pruneArchivedSelection,
3944
+ toggleArchivedSelection,
3623
3945
  archivedDeleteFeedback,
3624
3946
  createWorkspaceViewStore,
3625
3947
  bindObservable,
package/lib/workspace.js CHANGED
@@ -84,7 +84,8 @@ const archivedBatchTargetSchema = {
84
84
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("target must be an object");
85
85
  if (value.scope === "all" || value.scope === "ungrouped") return value;
86
86
  if (value.scope === "workspace" && typeof value.workspaceId === "string" && value.workspaceId.length > 0) return value;
87
- throw new TypeError("target.scope must be all, ungrouped, or workspace with a non-empty workspaceId");
87
+ if (value.scope === "sessions" && Array.isArray(value.sessionIds) && value.sessionIds.length > 0 && value.sessionIds.every((id) => typeof id === "string" && id.length > 0)) return value;
88
+ throw new TypeError("target.scope must be all, ungrouped, workspace with a non-empty workspaceId, or sessions with non-empty sessionIds");
88
89
  }
89
90
  };
90
91
  const unarchivedBatchSchema = {
@@ -400,6 +401,10 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
400
401
  const state = this.requireState();
401
402
  const archivedSessionIds = [...new Set(state.archivedSessionIds)];
402
403
  if (target.scope === "all") return archivedSessionIds;
404
+ if (target.scope === "sessions") {
405
+ const selected = new Set(target.sessionIds);
406
+ return archivedSessionIds.filter((id) => selected.has(id));
407
+ }
403
408
  if (target.scope === "workspace") {
404
409
  const workspace = this.requireTable().get(target.workspaceId);
405
410
  if (workspace === void 0) throw new Error(`unknown workspace "${target.workspaceId}"`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michengai/dsh-archive-manager",
3
- "version": "0.1.21",
3
+ "version": "0.1.23",
4
4
  "description": "NPM-installable DSH Web plugin for managing archived sessions.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.21.0",