@michengai/dsh-archive-manager 0.1.6 → 0.1.7

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
@@ -1,5 +1,5 @@
1
1
  window.__ModuleLoader__.load({
2
- id: "@michengai/dsh-archive-manager",
2
+ id: "@michengai/dsh-archive-manager",
3
3
  factory: (require) => {
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
@@ -36,14 +36,14 @@ window.__ModuleLoader__.load({
36
36
  }
37
37
  };
38
38
  /**
39
- * 客户端通过 `ctx.remote.$mount` 注册 workspaceRegistry 的远程方法。
40
- * 调用走 typert gateway SRC 路径,不影响既有 `/api/workspace.*` 网关。
39
+ * 客户端通过 `ctx.remote.$mount` 注册 workspaceRegistry 的远程方法。
40
+ * 调用走 typert gateway SRC 路径,不影响既有 `/api/workspace.*` 网关。
41
41
  */
42
42
  const ARCHIVE_MANAGER_REMOTE = {
43
- package: "@michengai/dsh-archive-manager",
43
+ package: "@michengai/dsh-archive-manager",
44
44
  descriptors: [
45
45
  {
46
- id: "@michengai/dsh-archive-manager#workspaceRegistry/unarchiveSession",
46
+ id: "@michengai/dsh-archive-manager#workspaceRegistry/unarchiveSession",
47
47
  service: "workspaceRegistry",
48
48
  namespace: "workspaceRegistry",
49
49
  method: "unarchiveSession",
@@ -56,13 +56,13 @@ window.__ModuleLoader__.load({
56
56
  }],
57
57
  result: {
58
58
  mode: "strict",
59
- typeSymbol: "@michengai/dsh-archive-manager/types#ArchivedSessionIds",
59
+ typeSymbol: "@michengai/dsh-archive-manager/types#ArchivedSessionIds",
60
60
  schema: archivedSetSchema
61
61
  },
62
- sourceLocation: { file: "@michengai/dsh-archive-manager/lib/workspace.js", line: 1, column: 1 }
62
+ sourceLocation: { file: "@michengai/dsh-archive-manager/lib/workspace.js", line: 1, column: 1 }
63
63
  },
64
64
  {
65
- id: "@michengai/dsh-archive-manager#workspaceRegistry/deleteSession",
65
+ id: "@michengai/dsh-archive-manager#workspaceRegistry/deleteSession",
66
66
  service: "workspaceRegistry",
67
67
  namespace: "workspaceRegistry",
68
68
  method: "deleteSession",
@@ -75,28 +75,28 @@ window.__ModuleLoader__.load({
75
75
  }],
76
76
  result: {
77
77
  mode: "strict",
78
- typeSymbol: "@michengai/dsh-archive-manager/types#Deleted",
78
+ typeSymbol: "@michengai/dsh-archive-manager/types#Deleted",
79
79
  schema: deletedSchema
80
80
  },
81
- sourceLocation: { file: "@michengai/dsh-archive-manager/lib/workspace.js", line: 1, column: 1 }
81
+ sourceLocation: { file: "@michengai/dsh-archive-manager/lib/workspace.js", line: 1, column: 1 }
82
82
  }
83
83
  ]
84
84
  };
85
85
  /** Archived-session row presentation classes (theme-token driven). */
86
- const ARCHIVED_CLASSES = {
87
- row: "dsham_archivedRow",
88
- title: "dsham_archivedTitle",
89
- badge: "dsham_archivedBadge",
90
- content: "dsham_archiveCardContent",
91
- meta: "dsham_archiveCardMeta",
92
- actions: "dsham_archiveCardActions",
93
- unarchive: "dsham_archiveCardUnarchive",
94
- delete: "dsham_archiveCardDelete"
95
- };
96
- const ARCHIVED_CSS = ".YDXeBa_sessionRow.dsham_archivedRow{box-sizing:border-box;cursor:default;min-height:64px;height:auto;background:var(--dsw-alias-button-elevated-fill);border:1px solid var(--dsw-alias-border-l2);border-radius:16px;gap:12px;margin:8px 0;padding:10px 16px}.YDXeBa_sessionRow.dsham_archivedRow:hover{background:var(--dsw-alias-button-elevated-fill);border-color:var(--dsw-alias-border-l3)}.YDXeBa_searchResultRow.dsham_archivedRow{background:var(--dsw-alias-button-elevated-fill);border:1px solid var(--dsw-alias-border-l2);border-radius:16px;margin:8px 0;padding:10px 16px}.dsham_archivedTitle{color:var(--dsw-alias-label-primary);font-weight:600}.dsham_archivedBadge{display:none}.dsham_archiveCardContent{min-width:0;flex:1;flex-direction:column;gap:2px;display:flex}.dsham_archiveCardMeta{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.YDXeBa_sessionRow.dsham_archivedRow>.YDXeBa_time,.YDXeBa_sessionRow.dsham_archivedRow>.YDXeBa_rowActions{display:none}.dsham_archiveCardActions{align-items:center;gap:12px;display:inline-flex}.dsham_archiveCardActions button{cursor:pointer;border:none;flex:none}.dsham_archiveCardDelete{width:28px;height:28px;color:var(--dsw-alias-label-tertiary);background:transparent;border-radius:8px;align-items:center;justify-content:center;display:inline-flex}.dsham_archiveCardDelete:hover{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover)}.dsham_archiveCardUnarchive{height:32px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-button-elevated-fill);border:1px solid var(--dsw-alias-border-l2)!important;border-radius:10px;padding:0 12px;font-size:13px;font-weight:600;line-height:20px}.dsham_archiveCardUnarchive:hover{background:var(--dsw-alias-interactive-bg-hover)}";
86
+ const ARCHIVED_CLASSES = {
87
+ row: "dsham_archivedRow",
88
+ title: "dsham_archivedTitle",
89
+ badge: "dsham_archivedBadge",
90
+ content: "dsham_archiveCardContent",
91
+ meta: "dsham_archiveCardMeta",
92
+ actions: "dsham_archiveCardActions",
93
+ unarchive: "dsham_archiveCardUnarchive",
94
+ delete: "dsham_archiveCardDelete"
95
+ };
96
+ const ARCHIVED_CSS = ".YDXeBa_sessionRow.dsham_archivedRow{box-sizing:border-box;cursor:default;min-height:64px;height:auto;background:var(--dsw-alias-button-elevated-fill);border:1px solid var(--dsw-alias-border-l2);border-radius:16px;gap:12px;margin:8px 0;padding:10px 16px}.YDXeBa_sessionRow.dsham_archivedRow:hover{background:var(--dsw-alias-button-elevated-fill);border-color:var(--dsw-alias-border-l3)}.YDXeBa_searchResultRow.dsham_archivedRow{background:var(--dsw-alias-button-elevated-fill);border:1px solid var(--dsw-alias-border-l2);border-radius:16px;margin:8px 0;padding:10px 16px}.dsham_archivedTitle{color:var(--dsw-alias-label-primary);font-weight:600}.dsham_archivedBadge{display:none}.dsham_archiveCardContent{min-width:0;flex:1;flex-direction:column;gap:2px;display:flex}.dsham_archiveCardMeta{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.YDXeBa_sessionRow.dsham_archivedRow>.YDXeBa_time,.YDXeBa_sessionRow.dsham_archivedRow>.YDXeBa_rowActions{display:none}.dsham_archiveCardActions{align-items:center;gap:12px;display:inline-flex}.dsham_archiveCardActions button{cursor:pointer;border:none;flex:none}.dsham_archiveCardDelete{width:28px;height:28px;color:var(--dsw-alias-label-tertiary);background:transparent;border-radius:8px;align-items:center;justify-content:center;display:inline-flex}.dsham_archiveCardDelete:hover{color:var(--dsw-alias-state-error-primary);background:var(--dsw-alias-interactive-bg-hover)}.dsham_archiveCardUnarchive{height:32px;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-button-elevated-fill);border:1px solid var(--dsw-alias-border-l2)!important;border-radius:10px;padding:0 12px;font-size:13px;font-weight:600;line-height:20px}.dsham_archiveCardUnarchive:hover{background:var(--dsw-alias-interactive-bg-hover)}";
97
97
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify("dsh-archive-manager/Archived.module.css") + "]") === null) {
98
98
  const tag = document.createElement("style");
99
- tag.dataset.plugin = "@michengai/dsh-archive-manager";
99
+ tag.dataset.plugin = "@michengai/dsh-archive-manager";
100
100
  tag.dataset.pluginCss = "dsh-archive-manager/Archived.module.css";
101
101
  tag.textContent = ARCHIVED_CSS;
102
102
  document.head.appendChild(tag);
@@ -200,6 +200,83 @@ window.__ModuleLoader__.load({
200
200
  return session.origin !== "subagent" && (!archived.has(session.id) || showArchived === true) && (!session.blank || session.id === current);
201
201
  }
202
202
  /**
203
+ * 单个删除按钮的目标:先子代理(含多层),再本会话。fork 分支不是 subagent,不会被收进来。
204
+ */
205
+ function collectSessionAndDescendantIds(sessionId, byId) {
206
+ if (typeof sessionId !== "string" || sessionId.length === 0) return [];
207
+ const catalog = byId ?? {};
208
+ const childrenByParent = /* @__PURE__ */ new Map();
209
+ for (const session of Object.values(catalog)) {
210
+ if (session === void 0 || session.origin !== "subagent" || typeof session.id !== "string" || session.id.length === 0) continue;
211
+ const parentId = session.parentId;
212
+ if (typeof parentId !== "string") continue;
213
+ const list = childrenByParent.get(parentId) ?? [];
214
+ list.push(session.id);
215
+ childrenByParent.set(parentId, list);
216
+ }
217
+ const ordered = [];
218
+ const seen = /* @__PURE__ */ new Set();
219
+ const visit = (id) => {
220
+ for (const childId of childrenByParent.get(id) ?? []) {
221
+ if (seen.has(childId)) continue;
222
+ seen.add(childId);
223
+ visit(childId);
224
+ ordered.push(childId);
225
+ }
226
+ };
227
+ visit(sessionId);
228
+ if (!seen.has(sessionId)) ordered.push(sessionId);
229
+ return ordered;
230
+ }
231
+ /**
232
+ * 归档设置页“删除全部”的目标:全部已归档 id,加上这些父会话下仍在目录中的 subagent 子会话。
233
+ * 每棵树都是先子后父,避免级联删除后再删子会话时被当成未知。
234
+ */
235
+ function collectArchivedDeleteAllIds(archivedSessionIds, byId) {
236
+ const catalog = byId ?? {};
237
+ const ids = [];
238
+ const seenArchived = /* @__PURE__ */ new Set();
239
+ for (const id of archivedSessionIds ?? []) {
240
+ if (typeof id !== "string" || id.length === 0 || seenArchived.has(id)) continue;
241
+ seenArchived.add(id);
242
+ ids.push(id);
243
+ }
244
+ const result = [];
245
+ const seen = /* @__PURE__ */ new Set();
246
+ const addTree = (id) => {
247
+ for (const item of collectSessionAndDescendantIds(id, catalog)) {
248
+ if (seen.has(item)) continue;
249
+ seen.add(item);
250
+ result.push(item);
251
+ }
252
+ };
253
+ for (const id of ids) {
254
+ if (catalog[id]?.origin === "subagent") continue;
255
+ addTree(id);
256
+ }
257
+ for (const id of ids) {
258
+ if (seen.has(id)) continue;
259
+ addTree(id);
260
+ }
261
+ return result;
262
+ }
263
+ /** 识别删除路径上“会话已不存在”的稳定标记;保留旧文案作兜底。 */
264
+ function isUnknownSessionError(reason) {
265
+ const message = reason instanceof Error ? reason.message : String(reason);
266
+ return message.includes("UNKNOWN_SESSION") || message.includes("no such session");
267
+ }
268
+ /** 把删除失败转成当前语言的用户文案。 */
269
+ function formatDeleteError(reason, t) {
270
+ if (isUnknownSessionError(reason)) return t("deleteSession.unknown");
271
+ const detail = reason instanceof Error ? reason.message : String(reason);
272
+ return t("deleteSession.failed", { detail });
273
+ }
274
+ function formatUnarchiveError(reason, t) {
275
+ if (isUnknownSessionError(reason)) return t("archives.unarchiveUnknown");
276
+ const detail = reason instanceof Error ? reason.message : String(reason);
277
+ return t("archives.unarchiveFailed", { detail });
278
+ }
279
+ /**
203
280
  * A blank session is the selected Workspace's provisional New Session row;
204
281
  * its canonical title never enters search (blank rows are query-excluded)
205
282
  * and the renderer localizes its display label.
@@ -442,7 +519,7 @@ window.__ModuleLoader__.load({
442
519
  const tagId$2 = "@deepseek-ai/dsh-client-ui-workspace/Rows.module.css";
443
520
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$2) + "]") === null) {
444
521
  const tag = document.createElement("style");
445
- tag.dataset.plugin = "@michengai/dsh-archive-manager";
522
+ tag.dataset.plugin = "@michengai/dsh-archive-manager";
446
523
  tag.dataset.pluginCss = tagId$2;
447
524
  tag.textContent = css$2;
448
525
  document.head.appendChild(tag);
@@ -492,21 +569,21 @@ window.__ModuleLoader__.load({
492
569
  * except workspace Rename/Delete and session Rename/Fork/Archive; the session
493
570
  * and workspace hover cards are suppressed while a menu is open.
494
571
  */
495
- /** Row display title: blank rows show the localized New Session label. */
496
- function displayTitle(node, t) {
497
- return node.blank ? t("session.new") : node.title ?? node.displayTitle ?? "";
572
+ /** Row display title: blank rows show the localized New Session label. */
573
+ function displayTitle(node, t) {
574
+ return node.blank ? t("session.new") : node.title ?? node.displayTitle ?? "";
498
575
  }
499
576
  /** Localized compact relative time ("刚刚"/"5分钟" in zh, "now"/"5min" in en). */
500
- function timeLabel(updatedAt, now, t) {
501
- const { unit, n } = relativeTime(updatedAt, now);
502
- return unit === "now" ? t("time.now") : t(`time.${unit}`, { n });
503
- }
504
- /** 归档页使用绝对时间,方便在历史记录中准确辨识会话。 */
505
- function archiveTimeLabel(updatedAt, t) {
506
- const date = new Date(updatedAt);
507
- const time = `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
508
- return t("archives.timestamp", { date: t("date.ymd", { y: date.getFullYear(), m: date.getMonth() + 1, d: date.getDate() }), time });
509
- }
577
+ function timeLabel(updatedAt, now, t) {
578
+ const { unit, n } = relativeTime(updatedAt, now);
579
+ return unit === "now" ? t("time.now") : t(`time.${unit}`, { n });
580
+ }
581
+ /** 归档页使用绝对时间,方便在历史记录中准确辨识会话。 */
582
+ function archiveTimeLabel(updatedAt, t) {
583
+ const date = new Date(updatedAt);
584
+ const time = `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
585
+ return t("archives.timestamp", { date: t("date.ymd", { y: date.getFullYear(), m: date.getMonth() + 1, d: date.getDate() }), time });
586
+ }
510
587
  /** Hover-card variant: distances wrap in the ago template; the now bucket stays bare (no "now ago"). */
511
588
  function hoverTimeLabel(updatedAt, now, t) {
512
589
  const { unit, n } = relativeTime(updatedAt, now);
@@ -890,42 +967,42 @@ window.__ModuleLoader__.load({
890
967
  className: ARCHIVED_CLASSES.badge,
891
968
  children: t("archived.badge")
892
969
  }),
893
- archived ? (0, react_jsx_runtime.jsxs)("span", {
894
- className: ARCHIVED_CLASSES.content,
895
- children: [(0, react_jsx_runtime.jsx)("span", {
896
- className: clsx(Rows_module_css_default.title, ARCHIVED_CLASSES.title),
897
- children: title
898
- }), !row.blank && (0, react_jsx_runtime.jsx)("span", {
899
- className: ARCHIVED_CLASSES.meta,
900
- children: timeLabel(row.updatedAt, now, t)
901
- })]
902
- }) : (0, react_jsx_runtime.jsx)("span", {
903
- className: Rows_module_css_default.title,
904
- children: title
905
- }),
906
- !row.blank && (0, react_jsx_runtime.jsx)("span", {
907
- className: Rows_module_css_default.time,
908
- children: timeLabel(row.updatedAt, now, t)
909
- }),
910
- archived && !row.blank && (0, react_jsx_runtime.jsxs)("span", {
911
- className: ARCHIVED_CLASSES.actions,
912
- onClick: (e) => {
913
- e.stopPropagation();
914
- },
915
- children: [(0, react_jsx_runtime.jsx)("button", {
916
- type: "button",
917
- className: ARCHIVED_CLASSES.delete,
918
- "aria-label": t("menu.deleteSession"),
919
- onClick: () => onDeleteSession(node.id, row.title),
920
- children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, {})
921
- }), (0, react_jsx_runtime.jsx)("button", {
922
- type: "button",
923
- className: ARCHIVED_CLASSES.unarchive,
924
- onClick: () => onUnarchive(node.id),
925
- children: t("menu.unarchive")
926
- })]
927
- }),
928
- !row.blank && (0, react_jsx_runtime.jsx)("span", {
970
+ archived ? (0, react_jsx_runtime.jsxs)("span", {
971
+ className: ARCHIVED_CLASSES.content,
972
+ children: [(0, react_jsx_runtime.jsx)("span", {
973
+ className: clsx(Rows_module_css_default.title, ARCHIVED_CLASSES.title),
974
+ children: title
975
+ }), !row.blank && (0, react_jsx_runtime.jsx)("span", {
976
+ className: ARCHIVED_CLASSES.meta,
977
+ children: timeLabel(row.updatedAt, now, t)
978
+ })]
979
+ }) : (0, react_jsx_runtime.jsx)("span", {
980
+ className: Rows_module_css_default.title,
981
+ children: title
982
+ }),
983
+ !row.blank && (0, react_jsx_runtime.jsx)("span", {
984
+ className: Rows_module_css_default.time,
985
+ children: timeLabel(row.updatedAt, now, t)
986
+ }),
987
+ archived && !row.blank && (0, react_jsx_runtime.jsxs)("span", {
988
+ className: ARCHIVED_CLASSES.actions,
989
+ onClick: (e) => {
990
+ e.stopPropagation();
991
+ },
992
+ children: [(0, react_jsx_runtime.jsx)("button", {
993
+ type: "button",
994
+ className: ARCHIVED_CLASSES.delete,
995
+ "aria-label": t("menu.deleteSession"),
996
+ onClick: () => onDeleteSession(node.id, row.title),
997
+ children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, {})
998
+ }), (0, react_jsx_runtime.jsx)("button", {
999
+ type: "button",
1000
+ className: ARCHIVED_CLASSES.unarchive,
1001
+ onClick: () => onUnarchive(node.id),
1002
+ children: t("menu.unarchive")
1003
+ })]
1004
+ }),
1005
+ !row.blank && (0, react_jsx_runtime.jsx)("span", {
929
1006
  className: Rows_module_css_default.rowActions,
930
1007
  children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
931
1008
  open: menuOpen,
@@ -974,7 +1051,7 @@ window.__ModuleLoader__.load({
974
1051
  const tagId$1 = "@deepseek-ai/dsh-client-ui-workspace/WorkspacePicker.module.css";
975
1052
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId$1) + "]") === null) {
976
1053
  const tag = document.createElement("style");
977
- tag.dataset.plugin = "@michengai/dsh-archive-manager";
1054
+ tag.dataset.plugin = "@michengai/dsh-archive-manager";
978
1055
  tag.dataset.pluginCss = tagId$1;
979
1056
  tag.textContent = css$1;
980
1057
  document.head.appendChild(tag);
@@ -1144,7 +1221,7 @@ window.__ModuleLoader__.load({
1144
1221
  const tagId = "@deepseek-ai/dsh-client-ui-workspace/WorkspaceBrowser.module.css";
1145
1222
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
1146
1223
  const tag = document.createElement("style");
1147
- tag.dataset.plugin = "@michengai/dsh-archive-manager";
1224
+ tag.dataset.plugin = "@michengai/dsh-archive-manager";
1148
1225
  tag.dataset.pluginCss = tagId;
1149
1226
  tag.textContent = css;
1150
1227
  document.head.appendChild(tag);
@@ -1300,8 +1377,8 @@ window.__ModuleLoader__.load({
1300
1377
  changed: orderChanged || timestampsChanged
1301
1378
  };
1302
1379
  }
1303
- /** Grouping and ordering controls for the workspace browser. */
1304
- function ViewOptionsMenu({ groupBy, orderBy, onGroupPick, onOrderPick, t }) {
1380
+ /** Grouping and ordering controls for the workspace browser. */
1381
+ function ViewOptionsMenu({ groupBy, orderBy, onGroupPick, onOrderPick, t }) {
1305
1382
  const [open, setOpen] = (0, react.useState)(false);
1306
1383
  return (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
1307
1384
  open,
@@ -1339,12 +1416,12 @@ window.__ModuleLoader__.load({
1339
1416
  id: "updated",
1340
1417
  label: t("orderBy.updated")
1341
1418
  },
1342
- ],
1343
- selectedIds: [groupBy, orderBy],
1344
- onSelect: (id) => {
1345
- if (id === "workspace" || id === "flat") onGroupPick(id);
1346
- else if (id === "manual" || id === "updated") onOrderPick(id);
1347
- setOpen(false);
1419
+ ],
1420
+ selectedIds: [groupBy, orderBy],
1421
+ onSelect: (id) => {
1422
+ if (id === "workspace" || id === "flat") onGroupPick(id);
1423
+ else if (id === "manual" || id === "updated") onOrderPick(id);
1424
+ setOpen(false);
1348
1425
  },
1349
1426
  align: "end",
1350
1427
  dense: true,
@@ -1830,6 +1907,7 @@ window.__ModuleLoader__.load({
1830
1907
  const workspaces = useWorkspaces((state) => state.items);
1831
1908
  const workspacePhase = useWorkspaces((state) => state.phase);
1832
1909
  const archivedSessionIds = useWorkspaces((state) => state.archivedSessionIds);
1910
+ const sessionSnapshot = useSessions((s) => s);
1833
1911
  const directoryFlowAvailable = useDirectoryFlow((occupied) => occupied);
1834
1912
  const groupBy = useStore((s) => s.groupBy);
1835
1913
  const orderBy = useStore((s) => s.orderBy);
@@ -2069,13 +2147,25 @@ window.__ModuleLoader__.load({
2069
2147
  if (deletingSession || deleteSessionTarget === null) return;
2070
2148
  setDeletingSession(true);
2071
2149
  setDeleteSessionError(null);
2072
- deleteSession(deleteSessionTarget.sessionId).then(() => {
2150
+ const rootId = deleteSessionTarget.sessionId;
2151
+ const ids = collectSessionAndDescendantIds(rootId, sessionSnapshot.byId);
2152
+ (async () => {
2153
+ for (const id of ids) {
2154
+ try {
2155
+ await deleteSession(id);
2156
+ } catch (reason) {
2157
+ const message = reason instanceof Error ? reason.message : String(reason);
2158
+ if (id !== rootId && isUnknownSessionError(reason)) continue;
2159
+ throw reason;
2160
+ }
2161
+ }
2162
+ })().then(() => {
2073
2163
  setDeletingSession(false);
2074
2164
  setDeleteSessionTarget(null);
2075
- setDeleteSessionCommittedId(deleteSessionTarget.sessionId);
2165
+ setDeleteSessionCommittedId(rootId);
2076
2166
  }).catch((reason) => {
2077
2167
  setDeletingSession(false);
2078
- setDeleteSessionError(reason instanceof Error ? reason.message : String(reason));
2168
+ setDeleteSessionError(formatDeleteError(reason, t));
2079
2169
  });
2080
2170
  };
2081
2171
  return (0, react_jsx_runtime.jsxs)("div", {
@@ -2149,17 +2239,17 @@ window.__ModuleLoader__.load({
2149
2239
  }),
2150
2240
  (0, react_jsx_runtime.jsxs)("div", {
2151
2241
  className: clsx(WorkspaceBrowser_module_css_default.headerActions, wide && searchExpanded && WorkspaceBrowser_module_css_default.headerActionsHidden),
2152
- children: [wide && (0, react_jsx_runtime.jsx)(ViewOptionsMenu, {
2153
- groupBy,
2154
- orderBy,
2155
- onGroupPick: (mode) => {
2156
- actions.setGroupBy(mode);
2157
- },
2158
- onOrderPick: (mode) => {
2159
- actions.setOrderBy(mode);
2160
- },
2161
- t
2162
- }), directoryFlowAvailable && (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
2242
+ children: [wide && (0, react_jsx_runtime.jsx)(ViewOptionsMenu, {
2243
+ groupBy,
2244
+ orderBy,
2245
+ onGroupPick: (mode) => {
2246
+ actions.setGroupBy(mode);
2247
+ },
2248
+ onOrderPick: (mode) => {
2249
+ actions.setOrderBy(mode);
2250
+ },
2251
+ t
2252
+ }), directoryFlowAvailable && (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tooltip, {
2163
2253
  label: t("workspace.add"),
2164
2254
  side: "bottom",
2165
2255
  delayMs: 500,
@@ -2451,104 +2541,114 @@ window.__ModuleLoader__.load({
2451
2541
  }
2452
2542
  })
2453
2543
  ]
2454
- });
2455
- }
2456
- //#endregion
2457
- //#region dsh-archive-manager: settings section
2458
- 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{min-width:132px;height:32px;padding:0 28px 0 11px;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}.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}}";
2459
- const ARCHIVE_SETTINGS_LAYOUT_OVERRIDE = ".dsham_settings{margin:0 auto!important}@media(max-width:720px){.dsham_settings{margin:0 auto!important}}";
2460
- if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify("dsh-archive-manager/ArchiveSettings.layout.css") + "]") === null) {
2461
- const tag = document.createElement("style");
2462
- tag.dataset.plugin = "@michengai/dsh-archive-manager";
2463
- tag.dataset.pluginCss = "dsh-archive-manager/ArchiveSettings.layout.css";
2464
- tag.textContent = ARCHIVE_SETTINGS_LAYOUT_OVERRIDE;
2465
- document.head.appendChild(tag);
2466
- }
2467
- /** 管理设置页中的归档会话,数据直接订阅 DSH 的会话与工作区投影。 */
2468
- function ArchivedSessionsSection({ sessionStore, workspaceStore, unarchiveSession, deleteSession, t }) {
2469
- const sessions = (0, react.useSyncExternalStore)(sessionStore.subscribe, sessionStore.getSnapshot);
2470
- const workspaceState = (0, react.useSyncExternalStore)(workspaceStore.subscribe, workspaceStore.getSnapshot);
2471
- const [deleteTarget, setDeleteTarget] = (0, react.useState)(null);
2472
- const [busy, setBusy] = (0, react.useState)(false);
2473
- const [error, setError] = (0, react.useState)(null);
2474
- const [query, setQuery] = (0, react.useState)("");
2475
- const [project, setProject] = (0, react.useState)("all");
2476
- const groups = (0, react.useMemo)(() => {
2477
- const archived = new Set(workspaceState.archivedSessionIds);
2478
- const byWorkspace = workspaceState.items.map((workspace) => ({
2479
- title: workspace.title,
2480
- ids: new Set(workspace.sessionIds),
2481
- sessions: []
2482
- }));
2483
- const ungrouped = [];
2484
- for (const id of workspaceState.archivedSessionIds) {
2485
- const session = sessions.byId[id];
2486
- if (session === void 0 || session.origin === "subagent" || !archived.has(id)) continue;
2487
- const group = byWorkspace.find((workspace) => workspace.ids.has(id));
2488
- (group === void 0 ? ungrouped : group.sessions).push(session);
2489
- }
2490
- const result = byWorkspace.filter((group) => group.sessions.length > 0);
2491
- if (ungrouped.length > 0) result.push({ title: t("group.ungrouped"), sessions: ungrouped });
2492
- return result;
2493
- }, [sessions, workspaceState, t]);
2494
- const filteredGroups = (0, react.useMemo)(() => {
2495
- const normalizedQuery = query.trim().toLocaleLowerCase();
2496
- return groups.filter((group) => project === "all" || project === group.title).map((group) => ({
2497
- ...group,
2498
- sessions: group.sessions.filter((session) => normalizedQuery === "" || displayTitle(session, t).toLocaleLowerCase().includes(normalizedQuery))
2499
- })).filter((group) => group.sessions.length > 0);
2500
- }, [groups, project, query, t]);
2501
- const archivedSessionIds = (0, react.useMemo)(() => groups.flatMap((group) => group.sessions.map((session) => session.id)), [groups]);
2502
- const onUnarchive = (sessionId) => {
2503
- setError(null);
2504
- unarchiveSession(sessionId).catch((reason) => {
2505
- setError(reason instanceof Error ? reason.message : String(reason));
2506
- });
2507
- };
2508
- const closeDelete = () => {
2509
- if (!busy) setDeleteTarget(null);
2510
- };
2511
- const confirmDelete = async () => {
2512
- if (busy || deleteTarget === null) return;
2513
- setBusy(true);
2514
- setError(null);
2515
- try {
2516
- for (const id of deleteTarget.all ? archivedSessionIds : [deleteTarget.id]) await deleteSession(id);
2517
- setDeleteTarget(null);
2518
- } catch (reason) {
2519
- setError(reason instanceof Error ? reason.message : String(reason));
2520
- } finally {
2521
- setBusy(false);
2522
- }
2523
- };
2524
- return (0, react_jsx_runtime.jsxs)("section", {
2525
- className: "dsham_settings",
2526
- "aria-label": t("archives.title"),
2527
- children: [(0, react_jsx_runtime.jsx)("style", { children: ARCHIVE_SETTINGS_CSS }), (0, react_jsx_runtime.jsxs)("header", { className: "dsham_settingsHeader", children: [(0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsx)("h2", { children: t("archives.title") }), (0, react_jsx_runtime.jsx)("p", { className: "dsham_settingsIntro", children: t("archives.description") })] }), (0, react_jsx_runtime.jsxs)("button", { type: "button", className: "dsham_settingsDanger", disabled: busy || archivedSessionIds.length === 0, onClick: () => setDeleteTarget({ all: true }), children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, {}), t("archives.deleteAll")] })] }), (0, react_jsx_runtime.jsxs)("div", { className: "dsham_settingsToolbar", children: [(0, react_jsx_runtime.jsxs)("label", { className: "dsham_settingsSearch", 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") })] }), (0, react_jsx_runtime.jsx)("select", { className: "dsham_settingsFilter", value: project, onChange: (event) => setProject(event.target.value), "aria-label": t("archives.projectFilter"), children: [(0, react_jsx_runtime.jsx)("option", { value: "all", children: t("archives.allProjects") }), groups.map((group) => (0, react_jsx_runtime.jsx)("option", { value: group.title, children: group.title }, group.title))] })] }), 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) => (0, react_jsx_runtime.jsxs)("section", {
2528
- className: "dsham_settingsGroup",
2529
- children: [(0, react_jsx_runtime.jsxs)("div", { className: "dsham_settingsGroupHeading", 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.jsx)("span", { className: "dsham_settingsCount", children: t("archives.sessionCount", { n: group.sessions.length }) })] }), (0, react_jsx_runtime.jsx)("div", {
2530
- className: "dsham_settingsList",
2531
- children: group.sessions.map((session) => (0, react_jsx_runtime.jsxs)("article", {
2532
- className: "dsham_settingsRow",
2533
- children: [(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", {
2534
- className: "dsham_settingsActions",
2535
- children: [(0, react_jsx_runtime.jsx)("button", { type: "button", className: "dsham_settingsAction", disabled: busy, onClick: () => onUnarchive(session.id), children: t("menu.unarchive") }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: "dsham_settingsDelete", disabled: busy, "aria-label": t("menu.deleteSession"), onClick: () => setDeleteTarget(session), children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, {}) })]
2536
- })]
2537
- }, session.id))
2538
- })]
2539
- }, group.title)), error !== null && (0, react_jsx_runtime.jsx)("div", { className: "dsham_settingsError", role: "alert", children: error }), (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
2540
- open: deleteTarget !== null,
2541
- onClose: closeDelete,
2542
- closeLabel: t("close"),
2543
- title: deleteTarget?.all ? t("archives.deleteAllTitle") : t("deleteSession.title"),
2544
- ...deleteTarget === null ? {} : { description: deleteTarget.all ? t("archives.deleteAllDesc", { n: archivedSessionIds.length }) : t("deleteSession.desc", { name: displayTitle(deleteTarget, t) }) },
2545
- 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: deleteTarget?.all ? t("archives.deleteAll") : t("deleteSession.title") })] }),
2546
- children: busy && (0, react_jsx_runtime.jsx)("div", { role: "status", children: deleteTarget?.all ? t("archives.deleteAllPending") : t("deleteSession.pending") })
2547
- })]
2548
- });
2549
- }
2550
- //#endregion
2551
- //#region lib/types/client/locales.js
2544
+ });
2545
+ }
2546
+ //#endregion
2547
+ //#region dsh-archive-manager: settings section
2548
+ 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{min-width:132px;height:32px;padding:0 28px 0 11px;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}.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}}";
2549
+ const ARCHIVE_SETTINGS_LAYOUT_OVERRIDE = ".dsham_settings{margin:0 auto!important}@media(max-width:720px){.dsham_settings{margin:0 auto!important}}";
2550
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify("dsh-archive-manager/ArchiveSettings.layout.css") + "]") === null) {
2551
+ const tag = document.createElement("style");
2552
+ tag.dataset.plugin = "@michengai/dsh-archive-manager";
2553
+ tag.dataset.pluginCss = "dsh-archive-manager/ArchiveSettings.layout.css";
2554
+ tag.textContent = ARCHIVE_SETTINGS_LAYOUT_OVERRIDE;
2555
+ document.head.appendChild(tag);
2556
+ }
2557
+ /** 管理设置页中的归档会话,数据直接订阅 DSH 的会话与工作区投影。 */
2558
+ function ArchivedSessionsSection({ sessionStore, workspaceStore, unarchiveSession, deleteSession, t }) {
2559
+ const sessions = (0, react.useSyncExternalStore)(sessionStore.subscribe, sessionStore.getSnapshot);
2560
+ const workspaceState = (0, react.useSyncExternalStore)(workspaceStore.subscribe, workspaceStore.getSnapshot);
2561
+ const [deleteTarget, setDeleteTarget] = (0, react.useState)(null);
2562
+ const [busy, setBusy] = (0, react.useState)(false);
2563
+ const [error, setError] = (0, react.useState)(null);
2564
+ const [query, setQuery] = (0, react.useState)("");
2565
+ const [project, setProject] = (0, react.useState)("all");
2566
+ const groups = (0, react.useMemo)(() => {
2567
+ // 列表仍隐藏 subagent;“删除全部”另用 collectArchivedDeleteAllIds 收父会话和子会话。
2568
+ const archived = new Set(workspaceState.archivedSessionIds);
2569
+ const byWorkspace = workspaceState.items.map((workspace) => ({
2570
+ title: workspace.title,
2571
+ ids: new Set(workspace.sessionIds),
2572
+ sessions: []
2573
+ }));
2574
+ const ungrouped = [];
2575
+ for (const id of workspaceState.archivedSessionIds) {
2576
+ const session = sessions.byId[id];
2577
+ if (session === void 0 || session.origin === "subagent" || !archived.has(id)) continue;
2578
+ const group = byWorkspace.find((workspace) => workspace.ids.has(id));
2579
+ (group === void 0 ? ungrouped : group.sessions).push(session);
2580
+ }
2581
+ const result = byWorkspace.filter((group) => group.sessions.length > 0);
2582
+ if (ungrouped.length > 0) result.push({ title: t("group.ungrouped"), sessions: ungrouped });
2583
+ return result;
2584
+ }, [sessions, workspaceState, t]);
2585
+ const filteredGroups = (0, react.useMemo)(() => {
2586
+ const normalizedQuery = query.trim().toLocaleLowerCase();
2587
+ return groups.filter((group) => project === "all" || project === group.title).map((group) => ({
2588
+ ...group,
2589
+ sessions: group.sessions.filter((session) => normalizedQuery === "" || displayTitle(session, t).toLocaleLowerCase().includes(normalizedQuery))
2590
+ })).filter((group) => group.sessions.length > 0);
2591
+ }, [groups, project, query, t]);
2592
+ const archivedSessionIds = (0, react.useMemo)(() => groups.flatMap((group) => group.sessions.map((session) => session.id)), [groups]);
2593
+ const deleteAllSessionIds = (0, react.useMemo)(() => collectArchivedDeleteAllIds(workspaceState.archivedSessionIds, sessions.byId), [workspaceState.archivedSessionIds, sessions]);
2594
+ const onUnarchive = (sessionId) => {
2595
+ setError(null);
2596
+ unarchiveSession(sessionId).catch((reason) => {
2597
+ setError(formatUnarchiveError(reason, t));
2598
+ });
2599
+ };
2600
+ const closeDelete = () => {
2601
+ if (!busy) setDeleteTarget(null);
2602
+ };
2603
+ const confirmDelete = async () => {
2604
+ if (busy || deleteTarget === null) return;
2605
+ setBusy(true);
2606
+ setError(null);
2607
+ try {
2608
+ for (const id of deleteTarget.all ? deleteAllSessionIds : collectSessionAndDescendantIds(deleteTarget.id, sessions.byId)) {
2609
+ try {
2610
+ await deleteSession(id);
2611
+ } catch (reason) {
2612
+ const message = reason instanceof Error ? reason.message : String(reason);
2613
+ if ((deleteTarget.all || id !== deleteTarget.id) && isUnknownSessionError(reason)) continue;
2614
+ throw reason;
2615
+ }
2616
+ }
2617
+ setDeleteTarget(null);
2618
+ } catch (reason) {
2619
+ setError(formatDeleteError(reason, t));
2620
+ } finally {
2621
+ setBusy(false);
2622
+ }
2623
+ };
2624
+ return (0, react_jsx_runtime.jsxs)("section", {
2625
+ className: "dsham_settings",
2626
+ "aria-label": t("archives.title"),
2627
+ children: [(0, react_jsx_runtime.jsx)("style", { children: ARCHIVE_SETTINGS_CSS }), (0, react_jsx_runtime.jsxs)("header", { className: "dsham_settingsHeader", children: [(0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsx)("h2", { children: t("archives.title") }), (0, react_jsx_runtime.jsx)("p", { className: "dsham_settingsIntro", children: t("archives.description") })] }), (0, react_jsx_runtime.jsxs)("button", { type: "button", className: "dsham_settingsDanger", disabled: busy || deleteAllSessionIds.length === 0, onClick: () => setDeleteTarget({ all: true }), children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, {}), t("archives.deleteAll")] })] }), (0, react_jsx_runtime.jsxs)("div", { className: "dsham_settingsToolbar", children: [(0, react_jsx_runtime.jsxs)("label", { className: "dsham_settingsSearch", 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") })] }), (0, react_jsx_runtime.jsx)("select", { className: "dsham_settingsFilter", value: project, onChange: (event) => setProject(event.target.value), "aria-label": t("archives.projectFilter"), children: [(0, react_jsx_runtime.jsx)("option", { value: "all", children: t("archives.allProjects") }), groups.map((group) => (0, react_jsx_runtime.jsx)("option", { value: group.title, children: group.title }, group.title))] })] }), 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) => (0, react_jsx_runtime.jsxs)("section", {
2628
+ className: "dsham_settingsGroup",
2629
+ children: [(0, react_jsx_runtime.jsxs)("div", { className: "dsham_settingsGroupHeading", 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.jsx)("span", { className: "dsham_settingsCount", children: t("archives.sessionCount", { n: group.sessions.length }) })] }), (0, react_jsx_runtime.jsx)("div", {
2630
+ className: "dsham_settingsList",
2631
+ children: group.sessions.map((session) => (0, react_jsx_runtime.jsxs)("article", {
2632
+ className: "dsham_settingsRow",
2633
+ children: [(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", {
2634
+ className: "dsham_settingsActions",
2635
+ children: [(0, react_jsx_runtime.jsx)("button", { type: "button", className: "dsham_settingsAction", disabled: busy, onClick: () => onUnarchive(session.id), children: t("menu.unarchive") }), (0, react_jsx_runtime.jsx)("button", { type: "button", className: "dsham_settingsDelete", disabled: busy, "aria-label": t("menu.deleteSession"), onClick: () => setDeleteTarget(session), children: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, {}) })]
2636
+ })]
2637
+ }, session.id))
2638
+ })]
2639
+ }, group.title)), error !== null && (0, react_jsx_runtime.jsx)("div", { className: "dsham_settingsError", role: "alert", children: error }), (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
2640
+ open: deleteTarget !== null,
2641
+ onClose: closeDelete,
2642
+ closeLabel: t("close"),
2643
+ title: deleteTarget?.all ? t("archives.deleteAllTitle") : t("deleteSession.title"),
2644
+ ...deleteTarget === null ? {} : { description: deleteTarget.all ? t("archives.deleteAllDesc", { n: deleteAllSessionIds.length }) : t("deleteSession.desc", { name: displayTitle(deleteTarget, t) }) },
2645
+ 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: deleteTarget?.all ? t("archives.deleteAll") : t("deleteSession.title") })] }),
2646
+ children: busy && (0, react_jsx_runtime.jsx)("div", { role: "status", children: deleteTarget?.all ? t("archives.deleteAllPending") : t("deleteSession.pending") })
2647
+ })]
2648
+ });
2649
+ }
2650
+ //#endregion
2651
+ //#region lib/types/client/locales.js
2552
2652
  /**
2553
2653
  * `workspace` namespace dictionaries: the browsing region (section header,
2554
2654
  * search, tree rows, dialogs) and the pick/add flow. Runtime failure
@@ -2561,7 +2661,7 @@ window.__ModuleLoader__.load({
2561
2661
  "section.workspaces": "工作区",
2562
2662
  "section.sessions": "会话",
2563
2663
  "viewOptions.label": "视图选项",
2564
- "groupBy.label": "分组方式",
2664
+ "groupBy.label": "分组方式",
2565
2665
  "groupBy.workspace": "按工作区",
2566
2666
  "groupBy.flat": "单列表",
2567
2667
  "orderBy.label": "排序方式",
@@ -2584,23 +2684,27 @@ window.__ModuleLoader__.load({
2584
2684
  "menu.unarchive": "取消归档",
2585
2685
  "menu.deleteSession": "删除会话",
2586
2686
  "archived.badge": "已归档",
2587
- "archived.notOpenable": "已归档,取消归档后可继续对话",
2588
- "archives.title": "已归档的聊天",
2589
- "archives.description": "管理已归档的会话。",
2590
- "archives.empty": "暂无已归档会话",
2591
- "archives.emptyFiltered": "没有匹配的已归档聊天",
2592
- "archives.searchPlaceholder": "搜索已归档聊天",
2593
- "archives.projectFilter": "按项目筛选",
2594
- "archives.allProjects": "所有项目",
2595
- "archives.sessionCount": "{n} 个聊天",
2596
- "archives.timestamp": "{date},{time}",
2597
- "archives.deleteAll": "全部删除",
2598
- "archives.deleteAllTitle": "删除全部已归档聊天",
2599
- "archives.deleteAllDesc": "将永久删除全部 {n} 个已归档聊天及其记录,此操作不可恢复。",
2687
+ "archived.notOpenable": "已归档,取消归档后可继续对话",
2688
+ "archives.title": "已归档的聊天",
2689
+ "archives.description": "管理已归档的会话。",
2690
+ "archives.empty": "暂无已归档会话",
2691
+ "archives.emptyFiltered": "没有匹配的已归档聊天",
2692
+ "archives.searchPlaceholder": "搜索已归档聊天",
2693
+ "archives.projectFilter": "按项目筛选",
2694
+ "archives.allProjects": "所有项目",
2695
+ "archives.sessionCount": "{n} 个聊天",
2696
+ "archives.timestamp": "{date},{time}",
2697
+ "archives.deleteAll": "全部删除",
2698
+ "archives.deleteAllTitle": "删除全部已归档聊天",
2699
+ "archives.deleteAllDesc": "将永久删除全部 {n} 个已归档聊天及其子代理和记录,此操作不可恢复。",
2600
2700
  "archives.deleteAllPending": "正在删除已归档聊天…",
2601
- "deleteSession.title": "删除会话",
2602
- "deleteSession.desc": "将永久删除会话“{name}”及其全部记录(对话内容、统计、缓存),此操作不可恢复。",
2603
- "deleteSession.pending": "正在删除会话…",
2701
+ "archives.unarchiveUnknown": "会话已不存在,无法取消归档。",
2702
+ "archives.unarchiveFailed": "取消归档失败:{detail}",
2703
+ "deleteSession.title": "删除会话",
2704
+ "deleteSession.desc": "将永久删除会话“{name}”及其子代理和全部记录(对话内容、统计、缓存),此操作不可恢复。",
2705
+ "deleteSession.pending": "正在删除会话…",
2706
+ "deleteSession.unknown": "会话已不存在或已被删除。",
2707
+ "deleteSession.failed": "删除会话失败:{detail}",
2604
2708
  "picker.loading": "正在加载工作区…",
2605
2709
  "conflict.named": "已存在名为“{name}”的工作区。",
2606
2710
  "folderError.title": "无法打开文件夹",
@@ -2646,7 +2750,7 @@ window.__ModuleLoader__.load({
2646
2750
  "section.workspaces": "Workspaces",
2647
2751
  "section.sessions": "Sessions",
2648
2752
  "viewOptions.label": "View options",
2649
- "groupBy.label": "Group by",
2753
+ "groupBy.label": "Group by",
2650
2754
  "groupBy.workspace": "WorkSpace",
2651
2755
  "groupBy.flat": "In one list",
2652
2756
  "orderBy.label": "Order by",
@@ -2669,23 +2773,27 @@ window.__ModuleLoader__.load({
2669
2773
  "menu.unarchive": "Unarchive",
2670
2774
  "menu.deleteSession": "Delete session",
2671
2775
  "archived.badge": "Archived",
2672
- "archived.notOpenable": "This session is archived. Unarchive it to continue the conversation.",
2673
- "archives.title": "Archived chats",
2674
- "archives.description": "Manage archived sessions.",
2675
- "archives.empty": "No archived sessions.",
2676
- "archives.emptyFiltered": "No archived chats match your filters.",
2677
- "archives.searchPlaceholder": "Search archived chats",
2678
- "archives.projectFilter": "Filter by project",
2679
- "archives.allProjects": "All projects",
2680
- "archives.sessionCount": "{n} chats",
2681
- "archives.timestamp": "{date}, {time}",
2682
- "archives.deleteAll": "Delete all",
2683
- "archives.deleteAllTitle": "Delete all archived chats",
2684
- "archives.deleteAllDesc": "This permanently deletes all {n} archived chats and their records. This cannot be undone.",
2776
+ "archived.notOpenable": "This session is archived. Unarchive it to continue the conversation.",
2777
+ "archives.title": "Archived chats",
2778
+ "archives.description": "Manage archived sessions.",
2779
+ "archives.empty": "No archived sessions.",
2780
+ "archives.emptyFiltered": "No archived chats match your filters.",
2781
+ "archives.searchPlaceholder": "Search archived chats",
2782
+ "archives.projectFilter": "Filter by project",
2783
+ "archives.allProjects": "All projects",
2784
+ "archives.sessionCount": "{n} chats",
2785
+ "archives.timestamp": "{date}, {time}",
2786
+ "archives.deleteAll": "Delete all",
2787
+ "archives.deleteAllTitle": "Delete all archived chats",
2788
+ "archives.deleteAllDesc": "This permanently deletes all {n} archived chats, their child agents, and their records. This cannot be undone.",
2685
2789
  "archives.deleteAllPending": "Deleting archived chats…",
2686
- "deleteSession.title": "Delete session",
2687
- "deleteSession.desc": "This permanently deletes session “{name}” and all of its records (conversation, stats, cache). This cannot be undone.",
2688
- "deleteSession.pending": "Deleting session…",
2790
+ "archives.unarchiveUnknown": "This session no longer exists, so it cannot be unarchived.",
2791
+ "archives.unarchiveFailed": "Could not unarchive the session: {detail}",
2792
+ "deleteSession.title": "Delete session",
2793
+ "deleteSession.desc": "This permanently deletes session “{name}”, its child agents, and all of its records (conversation, stats, cache). This cannot be undone.",
2794
+ "deleteSession.pending": "Deleting session…",
2795
+ "deleteSession.unknown": "This session no longer exists or was already deleted.",
2796
+ "deleteSession.failed": "Could not delete the session: {detail}",
2689
2797
  "picker.loading": "Loading workspaces…",
2690
2798
  "conflict.named": "A workspace named “{name}” already exists.",
2691
2799
  "folderError.title": "Couldn’t open folder",
@@ -2768,7 +2876,7 @@ window.__ModuleLoader__.load({
2768
2876
  * framework's global hooks.
2769
2877
  * @param ctx - client root context.
2770
2878
  */
2771
- function applyWorkspaceBrowser(ctx) {
2879
+ function applyWorkspaceBrowser(ctx) {
2772
2880
  ctx.effect(() => ctx.locale.register(NS, {
2773
2881
  zh,
2774
2882
  en
@@ -2784,19 +2892,19 @@ window.__ModuleLoader__.load({
2784
2892
  });
2785
2893
  const browserFlowSource = flowSource("sidebar.workspaces.directoryFlow");
2786
2894
  const pickerFlowSource = flowSource("conversation.hero.workspace.directoryFlow");
2787
- const unarchiveSession = async (sessionId) => {
2788
- const registry = ctx.get("remote.workspaceRegistry");
2789
- if (registry === void 0) throw new Error("archive-manager remote service is unavailable");
2790
- const result = await registry.unarchiveSession(sessionId);
2791
- if (!result.ok) throw new Error(result.error.message);
2792
- };
2793
- const deleteSession = async (sessionId) => {
2794
- const registry = ctx.get("remote.workspaceRegistry");
2795
- if (registry === void 0) throw new Error("archive-manager remote service is unavailable");
2796
- const result = await registry.deleteSession(sessionId);
2797
- if (!result.ok) throw new Error(result.error.message);
2798
- };
2799
- const browserInjected = () => ({
2895
+ const unarchiveSession = async (sessionId) => {
2896
+ const registry = ctx.get("remote.workspaceRegistry");
2897
+ if (registry === void 0) throw new Error("archive-manager remote service is unavailable");
2898
+ const result = await registry.unarchiveSession(sessionId);
2899
+ if (!result.ok) throw new Error(result.error.message);
2900
+ };
2901
+ const deleteSession = async (sessionId) => {
2902
+ const registry = ctx.get("remote.workspaceRegistry");
2903
+ if (registry === void 0) throw new Error("archive-manager remote service is unavailable");
2904
+ const result = await registry.deleteSession(sessionId);
2905
+ if (!result.ok) throw new Error(result.error.message);
2906
+ };
2907
+ const browserInjected = () => ({
2800
2908
  startSession: (workspaceId) => {
2801
2909
  ctx.workspaces.startSession(workspaceId);
2802
2910
  },
@@ -2831,8 +2939,8 @@ window.__ModuleLoader__.load({
2831
2939
  archiveSession: async (sessionId) => {
2832
2940
  await ctx.workspaces.archiveSession(sessionId);
2833
2941
  },
2834
- unarchiveSession,
2835
- deleteSession,
2942
+ unarchiveSession,
2943
+ deleteSession,
2836
2944
  insertSessionBefore: async (workspaceId, sessionId, beforeSessionId) => {
2837
2945
  await ctx.workspaces.insertSessionBefore(workspaceId, sessionId, beforeSessionId);
2838
2946
  },
@@ -2853,7 +2961,7 @@ window.__ModuleLoader__.load({
2853
2961
  inject: browserInjected,
2854
2962
  locale: NS
2855
2963
  }, WorkspaceBrowser));
2856
- ctx.slots.inject("conversation.hero.workspace", () => ctx.slots.register({
2964
+ ctx.slots.inject("conversation.hero.workspace", () => ctx.slots.register({
2857
2965
  name: "conversation.hero.workspace",
2858
2966
  children: { "conversation.hero.workspace.directoryFlow": {
2859
2967
  kind: "single",
@@ -2861,28 +2969,31 @@ window.__ModuleLoader__.load({
2861
2969
  } },
2862
2970
  inject: pickerInjected,
2863
2971
  locale: NS
2864
- }, WorkspacePicker));
2865
- ctx.slots.inject("settings.section", () => ctx.slots.register({
2866
- name: "settings.section",
2867
- id: "archived-sessions",
2868
- order: 18,
2869
- label: () => ctx.locale.bind(NS)("archived.badge"),
2870
- icon: "archive",
2871
- locale: NS,
2872
- inject: () => ({
2873
- sessionStore: ctx.sessions.list,
2874
- workspaceStore: ctx.workspaces.list,
2875
- unarchiveSession,
2876
- deleteSession,
2877
- t: ctx.locale.bind(NS)
2878
- })
2879
- }, ArchivedSessionsSection));
2880
- }
2972
+ }, WorkspacePicker));
2973
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
2974
+ name: "settings.section",
2975
+ id: "archived-sessions",
2976
+ order: 18,
2977
+ label: () => ctx.locale.bind(NS)("archived.badge"),
2978
+ icon: "archive",
2979
+ locale: NS,
2980
+ inject: () => ({
2981
+ sessionStore: ctx.sessions.list,
2982
+ workspaceStore: ctx.workspaces.list,
2983
+ unarchiveSession,
2984
+ deleteSession,
2985
+ t: ctx.locale.bind(NS)
2986
+ })
2987
+ }, ArchivedSessionsSection));
2988
+ }
2881
2989
  //#endregion
2882
2990
  /** Pure derivation surface for dsh-archive-manager self-tests (no-op for the runtime). */
2883
- exports.__test = {
2884
- displayTitle,
2885
- sessionVisible,
2991
+ exports.__test = {
2992
+ displayTitle,
2993
+ sessionVisible,
2994
+ collectArchivedDeleteAllIds,
2995
+ collectSessionAndDescendantIds,
2996
+ isUnknownSessionError,
2886
2997
  deriveGroups,
2887
2998
  deriveFlat,
2888
2999
  deriveSearchResults,
@@ -2895,4 +3006,4 @@ window.__ModuleLoader__.load({
2895
3006
  exports.inject = inject;
2896
3007
  return module.exports;
2897
3008
  }
2898
- });
3009
+ });
package/lib/projcache.js CHANGED
@@ -34,7 +34,11 @@ var ArchiveProjectionCache = class extends SessionProjectionCache {
34
34
  this.writeTail = Promise.allSettled([this.writeTail, task]).then(() => void 0);
35
35
  return task;
36
36
  }
37
- async writeCore(session) {
37
+ /**
38
+ * 墓碑正确性依赖:super.write(session) 一次整体写入,返回后不再有后续异步落盘。
39
+ * 若上游改成多阶段异步,(C) 补删会漏掉后续写入,deletedSessionIds 挡不住复活。
40
+ */
41
+ async writeCore(session) {
38
42
  if (this.deletedSessionIds.has(session.id)) return;
39
43
  await super.write(session);
40
44
  if (this.deletedSessionIds.has(session.id)) await this.requireTable().delete(session.id);
package/lib/workspace.js CHANGED
@@ -17,6 +17,10 @@ import { sessionDir } from "@deepseek-ai/dsh-spill-local";
17
17
  *
18
18
  * 所有可失败的持久化清理都先于不可逆的物理删除。此前任一步失败时,
19
19
  * 转录仍保留,可重试删除;物理删除失败时记录残留路径供人工排查。
20
+ * 物理删除成功后必须遗忘父类内存索引(headers / sessionPaths /
21
+ * invalidSessionPaths)并打上删除墓碑:父类 sessionKnown 以 headers.has
22
+ * 短路,indexHeaders 只增不减,stale list() 否则会把已删 id 救活,
23
+ * 进而被 archiveSession 重新写回 archivedSessionIds。
20
24
  * `unarchiveSession` 与 `deleteSession` 通过 Typert Remote 暴露给浏览器,并注册到 typert.local,避免生产环境只靠 SRC 扫描时 404。
21
25
  */
22
26
  function markRemoteMethod(instance, method) {
@@ -33,6 +37,17 @@ function markRemoteMethod(instance, method) {
33
37
  };
34
38
  Remote(method)(void 0, context);
35
39
  }
40
+ function unknownSessionMessage(sessionId) {
41
+ return `cannot delete session "${sessionId}": unknown (UNKNOWN_SESSION)`;
42
+ }
43
+ var ArchiveUnknownSessionError = class extends Error {
44
+ sessionId;
45
+ constructor(sessionId) {
46
+ super(unknownSessionMessage(sessionId));
47
+ this.sessionId = sessionId;
48
+ this.name = "ArchiveUnknownSessionError";
49
+ }
50
+ };
36
51
  const sessionIdSchema = {
37
52
  parse(value) {
38
53
  if (typeof value !== "string" || value.length === 0) throw new TypeError(`sessionId must be a non-empty string, got ${String(value)}`);
@@ -121,6 +136,12 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
121
136
  "sessionProjectionCache",
122
137
  "typert"
123
138
  ];
139
+ /** 本进程内已物理删除的会话;阻止父类把 stale list() 重新编入索引。 */
140
+ deletedSessionIds = /* @__PURE__ */ new Set();
141
+ /** 墓碑插入顺序,用于在上限处淘汰最旧项。 */
142
+ deletedSessionOrder = [];
143
+ /** 墓碑上限:足够挡住 stale list(),又避免长驻进程无限增长。 */
144
+ deletedSessionTombstoneLimit = 4096;
124
145
  constructor(ctx) {
125
146
  super(ctx);
126
147
  this.typertRemote = bindTypertRemote(this, this.name);
@@ -155,7 +176,7 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
155
176
  * directory, workspace accounting, archive marker, projection cache row).
156
177
  * @param sessionId - the session to delete.
157
178
  * @returns `{ deleted: true }` after durability.
158
- * @throws {@link WorkspaceUnknownSessionError} when the session is unknown.
179
+ * @throws {@link ArchiveUnknownSessionError} when the session is unknown.
159
180
  */
160
181
  async deleteSession(sessionId) {
161
182
  return this.enqueueOperation(() => this.deleteSessionCore(sessionId));
@@ -163,7 +184,7 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
163
184
  /** The serialized core deletion body (also used by the cascade path, which
164
185
  * already holds the operation chain — it must never re-enqueue). */
165
186
  async deleteSessionCore(sessionId) {
166
- if (!await this.sessionKnown(sessionId)) throw new WorkspaceUnknownSessionError(sessionId);
187
+ if (!await this.sessionKnown(sessionId)) throw new ArchiveUnknownSessionError(sessionId);
167
188
  const sessions = this.ctx.get("sessions");
168
189
  const live = sessions?.get(sessionId);
169
190
  if (live !== void 0) {
@@ -192,9 +213,48 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
192
213
  await this.removeFromWorkspaceAccounts(sessionId);
193
214
  if (projCache !== void 0) await projCache.delete(sessionId);
194
215
  await this.deleteDescendants(sessionId);
195
- await this.cleanSpill(sessionId);
196
- await this.removeTranscriptDirectory(sessionId);
197
- return { deleted: true };
216
+ await this.cleanSpill(sessionId);
217
+ await this.removeTranscriptDirectory(sessionId);
218
+ // 物理删除已成功:此时再清父类索引。失败时保留索引,便于重试。
219
+ this.forgetIndexedSession(sessionId);
220
+ return { deleted: true };
221
+ }
222
+ /**
223
+ * 从父类内存索引中遗忘已删除会话,并阻止后续 indexHeaders 把它加回。
224
+ * 实时会话以同 id 重新出现时(自定义 id 复用)会撤掉墓碑。
225
+ */
226
+ forgetIndexedSession(sessionId) {
227
+ if (!this.deletedSessionIds.has(sessionId)) {
228
+ this.deletedSessionIds.add(sessionId);
229
+ this.deletedSessionOrder.push(sessionId);
230
+ }
231
+ while (this.deletedSessionOrder.length > this.deletedSessionTombstoneLimit) {
232
+ const oldest = this.deletedSessionOrder.shift();
233
+ if (oldest !== void 0 && oldest !== sessionId) this.deletedSessionIds.delete(oldest);
234
+ }
235
+ this.headers.delete(sessionId);
236
+ this.sessionPaths.delete(sessionId);
237
+ this.invalidSessionPaths.delete(sessionId);
238
+ }
239
+ /**
240
+ * 已删除会话对归档/删除入口都视为未知。实时复用同一 id 时撤墓碑,
241
+ * 避免挡住新会话。
242
+ */
243
+ async sessionKnown(id) {
244
+ if (this.ctx.get("sessions")?.get(id) !== void 0) {
245
+ this.deletedSessionIds.delete(id);
246
+ return true;
247
+ }
248
+ if (this.deletedSessionIds.has(id)) return false;
249
+ return super.sessionKnown(id);
250
+ }
251
+ /**
252
+ * 父类 indexHeaders 只增不减;跳过墓碑 id,避免 stale persistence.list()
253
+ * 把已删除会话重新编入 headers。
254
+ */
255
+ async indexHeader(header) {
256
+ if (this.deletedSessionIds.has(header.id)) return;
257
+ return super.indexHeader(header);
198
258
  }
199
259
  /** 为未处于实时状态的持久化会话发布相同的移除事件。 */
200
260
  async publishColdSessionRemoval(sessionId, sessions) {
package/package.json CHANGED
@@ -1,28 +1,28 @@
1
- {
1
+ {
2
2
  "name": "@michengai/dsh-archive-manager",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "NPM-installable DSH Web plugin for managing archived sessions.",
5
- "type": "module",
6
- "packageManager": "pnpm@11.21.0",
7
- "engines": {
8
- "node": "^22.19.0 || >=24.0.0"
9
- },
10
- "main": "lib/index.js",
11
- "exports": {
12
- ".": "./lib/index.js",
5
+ "type": "module",
6
+ "packageManager": "pnpm@11.21.0",
7
+ "engines": {
8
+ "node": "^22.19.0 || >=24.0.0"
9
+ },
10
+ "main": "lib/index.js",
11
+ "exports": {
12
+ ".": "./lib/index.js",
13
13
  "./workspace": "./lib/workspace.js",
14
14
  "./projcache": "./lib/projcache.js",
15
15
  "./client": "./lib/client.js",
16
- "./package.json": "./package.json"
17
- },
16
+ "./package.json": "./package.json"
17
+ },
18
18
  "files": [
19
19
  "lib",
20
20
  "assets",
21
21
  "cordis.patch.yml",
22
22
  "README.md",
23
23
  "README.zh-CN.md",
24
- "LICENSE"
25
- ],
24
+ "LICENSE"
25
+ ],
26
26
  "scripts": {
27
27
  "build": "node scripts/check-package.mjs",
28
28
  "test": "node --test test/*.test.mjs",
@@ -44,42 +44,42 @@
44
44
  "react-dom": "18.3.1"
45
45
  },
46
46
  "license": "Apache-2.0",
47
- "dsh": {
48
- "bundle": {
49
- "patch": "./cordis.patch.yml"
50
- },
51
- "client": {
52
- "inject": [
53
- "@deepseek-ai/dsh-client-locale",
54
- "@deepseek-ai/dsh-client-runtime",
55
- "@deepseek-ai/dsh-client-ui-conversation",
56
- "@deepseek-ai/dsh-client-ui-sidebar",
57
- "@deepseek-ai/dsh-typert-registry",
58
- "@deepseek-ai/dsh-client-connection"
59
- ],
60
- "platform": "web"
61
- }
62
- },
63
- "repository": {
64
- "type": "git",
47
+ "dsh": {
48
+ "bundle": {
49
+ "patch": "./cordis.patch.yml"
50
+ },
51
+ "client": {
52
+ "inject": [
53
+ "@deepseek-ai/dsh-client-locale",
54
+ "@deepseek-ai/dsh-client-runtime",
55
+ "@deepseek-ai/dsh-client-ui-conversation",
56
+ "@deepseek-ai/dsh-client-ui-sidebar",
57
+ "@deepseek-ai/dsh-typert-registry",
58
+ "@deepseek-ai/dsh-client-connection"
59
+ ],
60
+ "platform": "web"
61
+ }
62
+ },
63
+ "repository": {
64
+ "type": "git",
65
65
  "url": "git+https://github.com/MichengAI/dsh-archive-manager.git"
66
66
  },
67
67
  "homepage": "https://github.com/MichengAI/dsh-archive-manager#readme",
68
68
  "bugs": {
69
69
  "url": "https://github.com/MichengAI/dsh-archive-manager/issues"
70
- },
71
- "keywords": [
72
- "deepseek",
73
- "dsh",
74
- "dsh-plugin",
75
- "archive",
76
- "session-management",
77
- "cordis",
78
- "typert",
79
- "workspace",
80
- "nodejs",
81
- "esm"
82
- ],
70
+ },
71
+ "keywords": [
72
+ "deepseek",
73
+ "dsh",
74
+ "dsh-plugin",
75
+ "archive",
76
+ "session-management",
77
+ "cordis",
78
+ "typert",
79
+ "workspace",
80
+ "nodejs",
81
+ "esm"
82
+ ],
83
83
  "publishConfig": {
84
84
  "access": "public",
85
85
  "registry": "https://registry.npmjs.org/"