@michengai/dsh-archive-manager 0.1.23 → 0.1.25

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,19 @@
4
4
 
5
5
  Published release notes are retained below; new versions are added without removing earlier entries.
6
6
 
7
+ ## 0.1.25 — 2026-09-03
8
+
9
+ - Added **Archive all chats** to each workspace action menu. The entry is hidden when no active chats remain, and the client refreshes its session list after a successful batch archive.
10
+ - Added a red outlined confirmation style for **Archive all** with hover and keyboard-focus states.
11
+ - Kept the path-safe `session_projcache_archive_manager_v2` domain as the only write target. Startup now imports missing entries from the short-lived old-name v2 domain, old-name v1 domain, and supported DSH cache formats without overwriting newer records.
12
+ - Added RC2-to-current isolated compatibility coverage and made it a release gate: CI and `prepublishOnly` now run `test:compat`; CI no longer assumes pnpm exists before Corepack enables it.
13
+
14
+ ## 0.1.24 — 2026-09-02
15
+
16
+ - Upgraded the development test stack to DSH `0.1.2-alpha.5` and `@deepseek-ai/cordis` `4.0.2`, while retaining the existing `>=0.1.0-rc.5 <0.2.0` DSH runtime peer declarations.
17
+ - Switched primary client integration tests to the current `dsh-client-store` contract and retained a tested optional `dsh-client-runtime` fallback for older hosts.
18
+ - Added a reproducible pnpm alpha-resolution policy that disables automatic peer installation and records the approved newly published packages.
19
+
7
20
  ## 0.1.23 — 2026-09-01
8
21
 
9
22
  - 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.
@@ -4,6 +4,19 @@
4
4
 
5
5
  以下发布说明会持续保留;新增版本时不再删除较早记录。
6
6
 
7
+ ## 0.1.25 — 2026-09-03
8
+
9
+ - 每个工作区操作菜单新增「归档全部聊天」;没有可归档聊天时不显示入口,批量归档成功后立即刷新客户端会话列表。
10
+ - 「全部归档」确认按钮使用红色描边,并提供悬停和键盘焦点状态。
11
+ - 路径安全的 `session_projcache_archive_manager_v2` 域继续作为唯一写入目标;启动时从短暂使用过的旧名 v2 域、旧名 v1 域及受支持的 DSH 缓存格式只读补缺导入,不覆盖更新的记录。
12
+ - 新增从 RC2 到当前版本的隔离兼容回归,并将其纳入发布门禁:CI 与 `prepublishOnly` 均执行 `test:compat`,CI 不再假定 Corepack 启用前系统已存在 pnpm。
13
+
14
+ ## 0.1.24 — 2026-09-02
15
+
16
+ - 将开发测试依赖升级至 DSH `0.1.2-alpha.5` 与 `@deepseek-ai/cordis` `4.0.2`,同时保留原有的 DSH 运行时 peer 声明 `>=0.1.0-rc.5 <0.2.0`。
17
+ - 客户端集成测试以当前 `dsh-client-store` 为主路径,并保留经过测试的可选 `dsh-client-runtime` 回退,兼容旧版宿主。
18
+ - 新增可复现的 pnpm alpha 解析策略:关闭自动安装 peer,并记录已批准的刚发布依赖。
19
+
7
20
  ## 0.1.23 — 2026-09-01
8
21
 
9
22
  - 修复 DSH `v0.1.2-alpha.2` 后会话侧栏“等待回答 / 等待审批”状态点不显示的问题:侧栏行现在从 `ui-session` 的 `pendingInteractions` Map 读取待处理交互状态。
package/README.md CHANGED
@@ -22,6 +22,7 @@
22
22
  ## Features
23
23
 
24
24
  - Archive a session from the sidebar session menu.
25
+ - Archive every active chat in a workspace from its sidebar action menu.
25
26
  - Search archived chats, sort them by update time, creation time, or title, and filter them by workspace in **Settings → Archived**.
26
27
  - Restore a session to its original workspace with **Unarchive**.
27
28
  - Restore or permanently delete every archived chat in a project group.
package/README.zh-CN.md CHANGED
@@ -22,6 +22,7 @@
22
22
  ## 功能概览
23
23
 
24
24
  - 在侧栏会话菜单中选择「归档会话」。
25
+ - 在工作区侧栏操作菜单中一键归档该工作区全部未归档聊天。
25
26
  - 在「设置 → 已归档」按工作区查看归档会话,并支持搜索、按更新时间/创建时间/标题排序和按项目筛选。
26
27
  - 安全取消归档,将会话恢复到原工作区位置。
27
28
  - 在项目分组中批量恢复或永久删除该项目的全部已归档聊天。
package/lib/client.js CHANGED
@@ -30,6 +30,12 @@ window.__ModuleLoader__.load({
30
30
  return value;
31
31
  }
32
32
  };
33
+ const workspaceIdSchema = {
34
+ parse(value) {
35
+ if (typeof value !== "string" || value.length === 0) throw new TypeError(`workspaceId must be a non-empty string, got ${String(value)}`);
36
+ return value;
37
+ }
38
+ };
33
39
  const archivedSetSchema = {
34
40
  parse(value) {
35
41
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("result must be an object");
@@ -61,6 +67,14 @@ window.__ModuleLoader__.load({
61
67
  return value;
62
68
  }
63
69
  };
70
+ const archivedWorkspaceBatchSchema = {
71
+ parse(value) {
72
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("result must be an object");
73
+ if (!Array.isArray(value.archivedSessionIds) || value.archivedSessionIds.some((id) => typeof id !== "string")) throw new TypeError("archivedSessionIds must be a string array");
74
+ if (!Array.isArray(value.archivedSessionIdsAdded) || value.archivedSessionIdsAdded.some((id) => typeof id !== "string")) throw new TypeError("archivedSessionIdsAdded must be a string array");
75
+ return value;
76
+ }
77
+ };
64
78
  const deletedBatchSchema = {
65
79
  parse(value) {
66
80
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("result must be an object");
@@ -143,6 +157,25 @@ window.__ModuleLoader__.load({
143
157
  },
144
158
  sourceLocation: { file: "@michengai/dsh-archive-manager/lib/workspace.js", line: 1, column: 1 }
145
159
  },
160
+ {
161
+ id: "@michengai/dsh-archive-manager#workspaceRegistry/archiveWorkspaceSessions",
162
+ service: "workspaceRegistry",
163
+ namespace: "workspaceRegistry",
164
+ method: "archiveWorkspaceSessions",
165
+ invocation: { kind: "direct" },
166
+ parameters: [{
167
+ name: "workspaceId",
168
+ wire: "workspaceId",
169
+ source: "json",
170
+ codec: { mode: "strict", typeSymbol: "@deepseek-ai/dsh-workspace/types#WorkspaceId", schema: workspaceIdSchema }
171
+ }],
172
+ result: {
173
+ mode: "strict",
174
+ typeSymbol: "@michengai/dsh-archive-manager/types#ArchivedWorkspaceBatch",
175
+ schema: archivedWorkspaceBatchSchema
176
+ },
177
+ sourceLocation: { file: "@michengai/dsh-archive-manager/lib/workspace.js", line: 1, column: 1 }
178
+ },
146
179
  {
147
180
  id: "@michengai/dsh-archive-manager#workspaceRegistry/deleteArchivedSessions",
148
181
  service: "workspaceRegistry",
@@ -284,6 +317,10 @@ window.__ModuleLoader__.load({
284
317
  }
285
318
  });
286
319
  }
320
+ /** GitHub 品牌标识未由宿主图标库提供,内联以保持离线可用和主题适配。 */
321
+ function GithubMark16() {
322
+ return (0, react_jsx_runtime.jsx)("svg", { viewBox: "0 0 16 16", width: 16, height: 16, "aria-hidden": true, focusable: "false", children: (0, react_jsx_runtime.jsx)("path", { fill: "currentColor", d: "M8 0a8 8 0 0 0-2.53 15.59c.4.074.547-.173.547-.385 0-.19-.007-.693-.01-1.36-2.226.484-2.695-1.073-2.695-1.073-.364-.924-.89-1.17-.89-1.17-.726-.496.055-.486.055-.486.803.056 1.225.824 1.225.824.714 1.223 1.872.87 2.328.665.072-.517.28-.87.508-1.07-1.777-.202-3.645-.888-3.645-3.956 0-.874.31-1.588.823-2.148-.083-.202-.357-1.017.078-2.12 0 0 .672-.215 2.2.82A7.65 7.65 0 0 1 8 4.8c.68.003 1.365.092 2.004.27 1.527-1.035 2.197-.82 2.197-.82.437 1.103.162 1.918.08 2.12.513.56.822 1.274.822 2.148 0 3.076-1.872 3.752-3.654 3.95.288.248.544.735.544 1.482 0 1.07-.01 1.932-.01 2.195 0 .214.144.463.55.384A8.001 8.001 0 0 0 8 0Z" }) });
323
+ }
287
324
  /**
288
325
  * 归档多选原型。保留既有设置页作为兼容回退,原型直接使用相同宿主
289
326
  * 批量接口,因此其恢复、删除和并发安全语义与原有项目级操作一致。
@@ -423,10 +460,10 @@ window.__ModuleLoader__.load({
423
460
  return (0, react_jsx_runtime.jsxs)("section", {
424
461
  className: "dsham_settings",
425
462
  "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", {
463
+ children: [(0, react_jsx_runtime.jsx)("style", { children: ARCHIVE_SETTINGS_CSS + ARCHIVE_SETTINGS_BATCH_CSS + ARCHIVE_SETTINGS_EXTERNAL_LINK_CSS + ARCHIVE_SETTINGS_SELECTION_CSS + ARCHIVE_SETTINGS_DELETE_CONFIRM_CSS }), (0, react_jsx_runtime.jsxs)("header", {
427
464
  className: "dsham_settingsHeader",
428
465
  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") })]
466
+ children: [(0, react_jsx_runtime.jsxs)("div", { className: "dsham_settingsTitleRow", children: [(0, react_jsx_runtime.jsx)("h2", { children: t("archives.title") }), (0, react_jsx_runtime.jsxs)("div", { className: "dsham_settingsLinks", children: [(0, react_jsx_runtime.jsxs)("a", { className: "dsham_settingsExternalLink", href: "https://github.com/MichengAI/dsh-archive-manager", target: "_blank", rel: "noreferrer", "aria-label": t("archives.viewProject"), children: [(0, react_jsx_runtime.jsx)(GithubMark16, {}), t("archives.viewProject")] }), (0, react_jsx_runtime.jsxs)("a", { className: "dsham_settingsExternalLink", href: "https://github.com/MichengAI/dsh-archive-manager/issues", target: "_blank", rel: "noreferrer", "aria-label": t("archives.feedback"), children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconListPenOutline16, {}), t("archives.feedback")] })] })] }), (0, react_jsx_runtime.jsx)("p", { className: "dsham_settingsIntro", children: t("archives.description") })]
430
467
  }), (0, react_jsx_runtime.jsxs)("div", {
431
468
  className: "dsham_settingsHeaderActions",
432
469
  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")] })]
@@ -468,7 +505,7 @@ window.__ModuleLoader__.load({
468
505
  closeLabel: t("close"),
469
506
  title: deleteDialogTitle,
470
507
  ...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 })] }),
508
+ 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", className: "dsham_settingsDeleteConfirm", disabled: busy, onClick: confirmDelete, children: deleteConfirmLabel })] }),
472
509
  children: busy && (0, react_jsx_runtime.jsx)("div", { role: "status", children: deleteTarget?.kind === "batch" ? t("archives.deleteBatchPending") : t("deleteSession.pending") })
473
510
  })]
474
511
  });
@@ -496,8 +533,8 @@ window.__ModuleLoader__.load({
496
533
  * @param cwd - directory path, or undefined for the ungrouped bucket.
497
534
  * @returns basename, the raw cwd when it has no basename, or the ungrouped label.
498
535
  */
499
- function workspaceLabel(cwd) {
500
- if (cwd === void 0 || cwd === "") return UNGROUPED_LABEL;
536
+ function workspaceLabel(cwd, ungroupedLabel = UNGROUPED_LABEL) {
537
+ if (cwd === void 0 || cwd === "") return ungroupedLabel;
501
538
  const base = cwd.replace(/[/\\]+$/, "").split(/[/\\]/).pop();
502
539
  return base !== void 0 && base !== "" ? base : cwd;
503
540
  }
@@ -603,6 +640,22 @@ window.__ModuleLoader__.load({
603
640
  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"));
604
641
  return groups;
605
642
  }
643
+ /** 返回工作区中尚未归档的唯一会话数量。 */
644
+ function archiveableWorkspaceSessionCount(workspace, archivedSessionIds) {
645
+ const archived = archivedSessionIds instanceof Set ? archivedSessionIds : new Set(archivedSessionIds);
646
+ return [...new Set(workspace.sessionIds)].filter((sessionId) => !archived.has(sessionId)).length;
647
+ }
648
+ /** 仅在工作区仍有活跃会话时创建批量归档确认目标。 */
649
+ function archiveWorkspaceDialogTarget(workspaces, workspaceId, title, archivedSessionIds) {
650
+ const workspace = workspaces.find((item) => item.workspaceId === workspaceId);
651
+ if (workspace === void 0) return null;
652
+ const count = archiveableWorkspaceSessionCount(workspace, archivedSessionIds);
653
+ return count === 0 ? null : { workspaceId, title, count };
654
+ }
655
+ /** 请求失败时保留确认目标,方便用户查看错误后重试。 */
656
+ function archiveWorkspaceDialogFailureState(target, reason, t) {
657
+ return { target, archiving: false, error: formatArchiveError(reason, t) };
658
+ }
606
659
  /** 只把侧栏已支持的待处理交互类型交给行渲染,避免未知类型触发断言。 */
607
660
  function visiblePendingKind(kind) {
608
661
  switch (kind) {
@@ -711,9 +764,10 @@ window.__ModuleLoader__.load({
711
764
  * @param content - ranked Host content-search page.
712
765
  * @param limit - protocol-owned maximum merged row count.
713
766
  * @param showArchived - "show archived" view toggle.
767
+ * @param ungroupedLabel - 未归属工作区会话的本地化回退标签。
714
768
  * @returns bounded deduplicated flat rows and a refine-query hint bit.
715
769
  */
716
- function deriveSearchResults(list, workspaces, query, archivedSessionIds, pendingInteractions, content, limit, showArchived) {
770
+ function deriveSearchResults(list, workspaces, query, archivedSessionIds, pendingInteractions, content, limit, showArchived, ungroupedLabel = UNGROUPED_LABEL) {
717
771
  const q = query.trim().toLowerCase();
718
772
  if (q === "") return {
719
773
  items: [],
@@ -723,7 +777,7 @@ window.__ModuleLoader__.load({
723
777
  const descendants = indexSubagentDescendants(list.byId);
724
778
  const workspaceBySession = /* @__PURE__ */ new Map();
725
779
  for (const workspace of workspaces) for (const sessionId of workspace.sessionIds) if (!workspaceBySession.has(sessionId)) workspaceBySession.set(sessionId, workspace.title);
726
- const labelOf = (summary) => workspaceBySession.get(summary.id) ?? workspaceLabel(summary.cwd);
780
+ const labelOf = (summary) => workspaceBySession.get(summary.id) ?? workspaceLabel(summary.cwd, ungroupedLabel);
727
781
  const contentBySession = /* @__PURE__ */ new Map();
728
782
  for (const item of content.items) if (!contentBySession.has(item.sessionId)) contentBySession.set(item.sessionId, item);
729
783
  const local = [];
@@ -937,7 +991,11 @@ window.__ModuleLoader__.load({
937
991
  id: "rename",
938
992
  label: t("rename"),
939
993
  icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconEditOutline16, {})
940
- }, {
994
+ }, ...(actions?.canArchive === true ? [{
995
+ id: "archive-workspace",
996
+ label: t("menu.archiveWorkspace"),
997
+ icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconArchiveOutline20, { size: 16 })
998
+ }] : []), {
941
999
  id: "delete",
942
1000
  label: t("delete.workspace"),
943
1001
  icon: (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutline16, {}),
@@ -981,9 +1039,10 @@ window.__ModuleLoader__.load({
981
1039
  items: workspaceMenuItems,
982
1040
  onSelect: (id) => {
983
1041
  setMenuOpen(false);
984
- /* v8 ignore next -- workspaceMenuItems carries exactly these two rows today. */
985
- if (id !== "rename" && id !== "delete") return;
1042
+ /* v8 ignore next -- workspaceMenuItems carries exactly these three rows today. */
1043
+ if (id !== "rename" && id !== "archive-workspace" && id !== "delete") return;
986
1044
  if (id === "rename") actions.rename();
1045
+ else if (id === "archive-workspace") actions.archive();
987
1046
  else actions.delete();
988
1047
  },
989
1048
  portal: true,
@@ -1736,7 +1795,7 @@ window.__ModuleLoader__.load({
1736
1795
  return e.clientY < rect.top + rect.height / 2 ? "before" : "after";
1737
1796
  }
1738
1797
  /** The scrolling session tree; unmounting drops the sessions subscription and expand-all state. */
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 }) {
1798
+ function SessionTree({ useSessions, useSessionPendingInteraction, startSession, open, forkSession, workspaces, archivedSessionIds, showArchived, onRenameRequest, onArchiveRequest, onDeleteRequest, onSessionRename, onSessionArchive, onSessionUnarchive, onSessionDelete, insertWorkspaceBefore, insertSessionBefore, orderBy, groupExpansion, setGroupExpanded, sessionOrderByAccount, sessionUpdatedAtByAccount, syncSessionOrderAccount, setSessionOrder, t }) {
1740
1799
  const list = useSessions((s) => s);
1741
1800
  const pendingInteractions = useSessionPendingInteraction((s) => s);
1742
1801
  const current = list.current;
@@ -1932,16 +1991,24 @@ window.__ModuleLoader__.load({
1932
1991
  }
1933
1992
  },
1934
1993
  drag: workspaceDragProps,
1935
- actions: group.workspaceId === void 0 ? void 0 : {
1936
- rename: () => {
1937
- /* v8 ignore next -- narrowing guard: the actions object exists only for real-workspace groups. */
1938
- if (group.workspaceId !== void 0) onRenameRequest(group.workspaceId, group.label);
1939
- },
1940
- delete: () => {
1941
- /* v8 ignore next -- narrowing guard: the actions object exists only for real-workspace groups. */
1942
- if (group.workspaceId !== void 0) onDeleteRequest(group.workspaceId, group.label);
1994
+ actions: group.workspaceId === void 0 ? void 0 : {
1995
+ canArchive: (() => {
1996
+ const workspace = workspaces.find((item) => item.workspaceId === group.workspaceId);
1997
+ return workspace !== void 0 && archiveableWorkspaceSessionCount(workspace, archivedSessionIds) > 0;
1998
+ })(),
1999
+ rename: () => {
2000
+ /* v8 ignore next -- narrowing guard: the actions object exists only for real-workspace groups. */
2001
+ if (group.workspaceId !== void 0) onRenameRequest(group.workspaceId, group.label);
2002
+ },
2003
+ archive: () => {
2004
+ /* v8 ignore next -- narrowing guard: the actions object exists only for real-workspace groups. */
2005
+ if (group.workspaceId !== void 0) onArchiveRequest(group.workspaceId, group.label);
2006
+ },
2007
+ delete: () => {
2008
+ /* v8 ignore next -- narrowing guard: the actions object exists only for real-workspace groups. */
2009
+ if (group.workspaceId !== void 0) onDeleteRequest(group.workspaceId, group.label);
2010
+ }
1943
2011
  }
1944
- }
1945
2012
  }),
1946
2013
  (expandedSessionGroups.includes(group.key) ? group.sessions : group.sessions.slice(0, COLLAPSED_SESSION_LIMIT)).map((node) => {
1947
2014
  const sameGroupDrag = drag !== null && drag.accountKey === group.key;
@@ -2148,13 +2215,14 @@ window.__ModuleLoader__.load({
2148
2215
  function SearchResults({ useSessions, useSessionPendingInteraction, open, workspaces, archivedSessionIds, showArchived, query, remote, resultLimit, t }) {
2149
2216
  const list = useSessions((s) => s);
2150
2217
  const pendingInteractions = useSessionPendingInteraction((s) => s);
2218
+ const ungroupedLabel = t("group.ungrouped");
2151
2219
  const currentRemote = remote.query === query ? remote : {
2152
2220
  query,
2153
2221
  status: "loading",
2154
2222
  items: [],
2155
2223
  hasMore: false
2156
2224
  };
2157
- const results = (0, react.useMemo)(() => deriveSearchResults(list, workspaces, query, archivedSessionIds, pendingInteractions, currentRemote, resultLimit, showArchived), [
2225
+ const results = (0, react.useMemo)(() => deriveSearchResults(list, workspaces, query, archivedSessionIds, pendingInteractions, currentRemote, resultLimit, showArchived, ungroupedLabel), [
2158
2226
  list,
2159
2227
  workspaces,
2160
2228
  query,
@@ -2162,7 +2230,8 @@ window.__ModuleLoader__.load({
2162
2230
  pendingInteractions,
2163
2231
  currentRemote,
2164
2232
  resultLimit,
2165
- showArchived
2233
+ showArchived,
2234
+ ungroupedLabel
2166
2235
  ]);
2167
2236
  const pending = currentRemote.status === "loading";
2168
2237
  const failed = currentRemote.status === "error";
@@ -2209,7 +2278,7 @@ window.__ModuleLoader__.load({
2209
2278
  * @param props - composed slot props (shell owner share + store + injected actions).
2210
2279
  * @returns the region element tree.
2211
2280
  */
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 }) {
2281
+ function WorkspaceBrowser({ wide, expandSidebar, useSessions, useSessionPendingInteraction, useWorkspaces, useStore, actions, startSession, open, renameSession, forkSession, renameWorkspace, deleteWorkspace, insertWorkspaceBefore, archiveSession, archiveWorkspaceSessions, unarchiveSession, deleteSession, insertSessionBefore, createWorkspace, searchSessions, searchResultLimit, useDirectoryFlow, renderSlot, t }) {
2213
2282
  const useEffectiveSessionPendingInteraction = useSessionPendingInteraction ?? useEmptySessionPendingInteraction;
2214
2283
  const workspaces = useWorkspaces((state) => state.items);
2215
2284
  const workspacePhase = useWorkspaces((state) => state.phase);
@@ -2404,6 +2473,34 @@ window.__ModuleLoader__.load({
2404
2473
  showArchivedToast(formatArchiveError(reason, t));
2405
2474
  });
2406
2475
  };
2476
+ const [archiveWorkspaceTarget, setArchiveWorkspaceTarget] = (0, react.useState)(null);
2477
+ const [archivingWorkspace, setArchivingWorkspace] = (0, react.useState)(false);
2478
+ const [archiveWorkspaceError, setArchiveWorkspaceError] = (0, react.useState)(null);
2479
+ const closeArchiveWorkspace = () => {
2480
+ if (archivingWorkspace) return;
2481
+ setArchiveWorkspaceTarget(null);
2482
+ setArchiveWorkspaceError(null);
2483
+ };
2484
+ const onArchiveWorkspaceRequest = (workspaceId, title) => {
2485
+ const target = archiveWorkspaceDialogTarget(workspaces, workspaceId, title, archivedSet);
2486
+ if (target === null) return;
2487
+ setArchiveWorkspaceTarget(target);
2488
+ setArchiveWorkspaceError(null);
2489
+ };
2490
+ const confirmArchiveWorkspace = () => {
2491
+ /* v8 ignore next -- the Modal is absent without a target and its button is disabled while archiving. */
2492
+ if (archivingWorkspace || archiveWorkspaceTarget === null) return;
2493
+ setArchivingWorkspace(true);
2494
+ setArchiveWorkspaceError(null);
2495
+ archiveWorkspaceSessions(archiveWorkspaceTarget.workspaceId).then(() => {
2496
+ setArchivingWorkspace(false);
2497
+ setArchiveWorkspaceTarget(null);
2498
+ }).catch((reason) => {
2499
+ const state = archiveWorkspaceDialogFailureState(archiveWorkspaceTarget, reason, t);
2500
+ setArchivingWorkspace(state.archiving);
2501
+ setArchiveWorkspaceError(state.error);
2502
+ });
2503
+ };
2407
2504
  const [deleteTarget, setDeleteTarget] = (0, react.useState)(null);
2408
2505
  const [deleting, setDeleting] = (0, react.useState)(false);
2409
2506
  const [deleteCommittedId, setDeleteCommittedId] = (0, react.useState)(null);
@@ -2668,6 +2765,7 @@ window.__ModuleLoader__.load({
2668
2765
  setRenameDraft(currentTitle);
2669
2766
  setRenameError(null);
2670
2767
  },
2768
+ onArchiveRequest: onArchiveWorkspaceRequest,
2671
2769
  onDeleteRequest: (workspaceId, title) => {
2672
2770
  setDeleteTarget({
2673
2771
  workspaceId,
@@ -2807,6 +2905,34 @@ window.__ModuleLoader__.load({
2807
2905
  children: deleteError
2808
2906
  })]
2809
2907
  }),
2908
+ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
2909
+ open: archiveWorkspaceTarget !== null,
2910
+ onClose: closeArchiveWorkspace,
2911
+ closeLabel: t("close"),
2912
+ title: archiveWorkspaceTarget === null ? "" : t("archiveWorkspace.title", { n: archiveWorkspaceTarget.count }),
2913
+ ...archiveWorkspaceTarget === null ? {} : { description: t("archiveWorkspace.desc", { name: archiveWorkspaceTarget.title }) },
2914
+ footer: (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2915
+ variant: "outline",
2916
+ disabled: archivingWorkspace,
2917
+ onClick: closeArchiveWorkspace,
2918
+ children: t("cancel")
2919
+ }), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
2920
+ variant: "outline",
2921
+ className: "dsham_archiveWorkspaceConfirm",
2922
+ disabled: archivingWorkspace,
2923
+ onClick: confirmArchiveWorkspace,
2924
+ children: t("archiveWorkspace.confirm")
2925
+ })] }),
2926
+ children: [archivingWorkspace && (0, react_jsx_runtime.jsx)("div", {
2927
+ className: WorkspaceBrowser_module_css_default.deleteStatus,
2928
+ role: "status",
2929
+ children: t("archiveWorkspace.pending")
2930
+ }), archiveWorkspaceError !== null && (0, react_jsx_runtime.jsx)("div", {
2931
+ className: WorkspaceBrowser_module_css_default.renameError,
2932
+ role: "alert",
2933
+ children: archiveWorkspaceError
2934
+ })]
2935
+ }),
2810
2936
  (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
2811
2937
  open: deleteSessionTarget !== null,
2812
2938
  onClose: closeDeleteSession,
@@ -2849,13 +2975,16 @@ window.__ModuleLoader__.load({
2849
2975
  //#region dsh-archive-manager: settings section
2850
2976
  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}}";
2851
2977
  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}}";
2978
+ const ARCHIVE_SETTINGS_EXTERNAL_LINK_CSS = ".dsham_settingsTitleRow{display:flex;align-items:center;gap:8px;min-width:0}.dsham_settingsLinks{display:flex;align-items:center;gap:4px}.dsham_settingsExternalLink{display:inline-flex;align-items:center;gap:5px;min-height:28px;padding:0 8px;color:var(--dsw-alias-label-secondary);background:transparent;border:1px solid var(--dsw-alias-border-l2);border-radius:7px;font-size:12px;font-weight:500;line-height:18px;text-decoration:none;white-space:nowrap}.dsham_settingsExternalLink:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.dsham_settingsExternalLink:focus-visible{outline:2px solid var(--dsw-alias-state-success-primary);outline-offset:2px}.dsham_settingsExternalLink svg{flex:none}@media(max-width:720px){.dsham_settingsTitleRow{flex-wrap:wrap}}";
2979
+ const ARCHIVE_SETTINGS_DELETE_CONFIRM_CSS = ".dsham_settingsDeleteConfirm{color:var(--dsw-alias-state-error-primary)!important;background:transparent!important;border-color:var(--dsw-alias-state-error-primary)!important}.dsham_settingsDeleteConfirm:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 20%,transparent)!important}.dsham_settingsDeleteConfirm:focus-visible{outline:2px solid var(--dsw-alias-state-error-secondary);outline-offset:2px}.dsham_settingsDeleteConfirm:disabled{cursor:not-allowed;opacity:.5}";
2980
+ const ARCHIVE_WORKSPACE_CONFIRM_CSS = ".dsham_archiveWorkspaceConfirm{color:var(--dsw-alias-state-error-primary)!important;background:transparent!important;border-color:var(--dsw-alias-state-error-primary)!important}.dsham_archiveWorkspaceConfirm:hover:not(:disabled){background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 20%,transparent)!important}.dsham_archiveWorkspaceConfirm:focus-visible{outline:2px solid var(--dsw-alias-state-error-secondary);outline-offset:2px}.dsham_archiveWorkspaceConfirm:disabled{cursor:not-allowed;opacity:.5}";
2852
2981
  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}}";
2853
2982
  const ARCHIVE_SETTINGS_LAYOUT_OVERRIDE = ".dsham_settings{margin:0 auto!important}@media(max-width:720px){.dsham_settings{margin:0 auto!important}}";
2854
2983
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify("dsh-archive-manager/ArchiveSettings.layout.css") + "]") === null) {
2855
2984
  const tag = document.createElement("style");
2856
2985
  tag.dataset.plugin = "@michengai/dsh-archive-manager";
2857
2986
  tag.dataset.pluginCss = "dsh-archive-manager/ArchiveSettings.layout.css";
2858
- tag.textContent = ARCHIVE_SETTINGS_LAYOUT_OVERRIDE;
2987
+ tag.textContent = ARCHIVE_SETTINGS_LAYOUT_OVERRIDE + ARCHIVE_SETTINGS_DELETE_CONFIRM_CSS + ARCHIVE_WORKSPACE_CONFIRM_CSS;
2859
2988
  document.head.appendChild(tag);
2860
2989
  }
2861
2990
  /** 设置页筛选/排序:自定义菜单,避免原生 select 弹出系统浅色下拉。 */
@@ -3291,7 +3420,7 @@ window.__ModuleLoader__.load({
3291
3420
  return (0, react_jsx_runtime.jsxs)("section", {
3292
3421
  className: "dsham_settings",
3293
3422
  "aria-label": t("archives.title"),
3294
- children: [(0, react_jsx_runtime.jsx)("style", { children: ARCHIVE_SETTINGS_CSS + ARCHIVE_SETTINGS_BATCH_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)("div", { className: "dsham_settingsHeaderActions", 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")] })] })] }), (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)(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") })] }), 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) => {
3423
+ children: [(0, react_jsx_runtime.jsx)("style", { children: ARCHIVE_SETTINGS_CSS + ARCHIVE_SETTINGS_BATCH_CSS + ARCHIVE_SETTINGS_EXTERNAL_LINK_CSS }), (0, react_jsx_runtime.jsxs)("header", { className: "dsham_settingsHeader", children: [(0, react_jsx_runtime.jsxs)("div", { children: [(0, react_jsx_runtime.jsxs)("div", { className: "dsham_settingsTitleRow", children: [(0, react_jsx_runtime.jsx)("h2", { children: t("archives.title") }), (0, react_jsx_runtime.jsxs)("div", { className: "dsham_settingsLinks", children: [(0, react_jsx_runtime.jsxs)("a", { className: "dsham_settingsExternalLink", href: "https://github.com/MichengAI/dsh-archive-manager", target: "_blank", rel: "noreferrer", "aria-label": t("archives.viewProject"), children: [(0, react_jsx_runtime.jsx)(GithubMark16, {}), t("archives.viewProject")] }), (0, react_jsx_runtime.jsxs)("a", { className: "dsham_settingsExternalLink", href: "https://github.com/MichengAI/dsh-archive-manager/issues", target: "_blank", rel: "noreferrer", "aria-label": t("archives.feedback"), children: [(0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconListPenOutline16, {}), t("archives.feedback")] })] })] }), (0, react_jsx_runtime.jsx)("p", { className: "dsham_settingsIntro", children: t("archives.description") })] }), (0, react_jsx_runtime.jsxs)("div", { className: "dsham_settingsHeaderActions", 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")] })] })] }), (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)(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") })] }), 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) => {
3295
3424
  const target = archivedBatchTargetForGroup(group.key);
3296
3425
  const count = deriveArchivedBatchIds(workspaceState.archivedSessionIds, workspaceState.items, target).length;
3297
3426
  return (0, react_jsx_runtime.jsxs)("section", {
@@ -3313,7 +3442,7 @@ window.__ModuleLoader__.load({
3313
3442
  closeLabel: t("close"),
3314
3443
  title: deleteDialogTitle,
3315
3444
  ...deleteDialogDescription === void 0 ? {} : { description: deleteDialogDescription },
3316
- 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 })] }),
3445
+ 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", className: "dsham_settingsDeleteConfirm", disabled: busy, onClick: confirmDelete, children: deleteConfirmLabel })] }),
3317
3446
  children: busy && (0, react_jsx_runtime.jsx)("div", { role: "status", children: deleteTarget?.kind === "batch" ? t("archives.deleteBatchPending") : t("deleteSession.pending") })
3318
3447
  })]
3319
3448
  });
@@ -3358,6 +3487,8 @@ window.__ModuleLoader__.load({
3358
3487
  "archived.notOpenable": "已归档,取消归档后可继续对话",
3359
3488
  "archives.title": "已归档的聊天",
3360
3489
  "archives.description": "管理已归档的会话。",
3490
+ "archives.viewProject": "GitHub",
3491
+ "archives.feedback": "问题反馈",
3361
3492
  "archives.empty": "暂无已归档会话",
3362
3493
  "archives.emptyFiltered": "没有匹配的已归档聊天",
3363
3494
  "archives.searchPlaceholder": "搜索已归档聊天",
@@ -3425,6 +3556,11 @@ window.__ModuleLoader__.load({
3425
3556
  "delete.pending": "正在删除工作区…",
3426
3557
  "menu.fork": "分叉会话",
3427
3558
  "menu.archiveSession": "归档会话",
3559
+ "menu.archiveWorkspace": "归档全部聊天",
3560
+ "archiveWorkspace.title": "归档 {n} 个聊天?",
3561
+ "archiveWorkspace.desc": "这会将“{name}”中的聊天归档。之后你可以在已归档的聊天中找到它们。",
3562
+ "archiveWorkspace.confirm": "全部归档",
3563
+ "archiveWorkspace.pending": "正在归档聊天…",
3428
3564
  "sessions.count.one": "{n} 个会话",
3429
3565
  "sessions.count.other": "{n} 个会话",
3430
3566
  "actions.workspace.aria": "工作区“{name}”的操作",
@@ -3482,6 +3618,8 @@ window.__ModuleLoader__.load({
3482
3618
  "archived.notOpenable": "This session is archived. Unarchive it to continue the conversation.",
3483
3619
  "archives.title": "Archived chats",
3484
3620
  "archives.description": "Manage archived sessions.",
3621
+ "archives.viewProject": "GitHub",
3622
+ "archives.feedback": "Issues",
3485
3623
  "archives.empty": "No archived sessions.",
3486
3624
  "archives.emptyFiltered": "No archived chats match your filters.",
3487
3625
  "archives.searchPlaceholder": "Search archived chats",
@@ -3549,6 +3687,11 @@ window.__ModuleLoader__.load({
3549
3687
  "delete.pending": "Deleting workspace…",
3550
3688
  "menu.fork": "Fork session",
3551
3689
  "menu.archiveSession": "Archive session",
3690
+ "menu.archiveWorkspace": "Archive all chats",
3691
+ "archiveWorkspace.title": "Archive {n} chats?",
3692
+ "archiveWorkspace.desc": "This archives the chats in “{name}”. You can find them later in Archived chats.",
3693
+ "archiveWorkspace.confirm": "Archive all",
3694
+ "archiveWorkspace.pending": "Archiving chats…",
3552
3695
  "sessions.count.one": "{n} session",
3553
3696
  "sessions.count.other": "{n} sessions",
3554
3697
  "actions.workspace.aria": "Workspace actions for {name}",
@@ -3754,6 +3897,12 @@ window.__ModuleLoader__.load({
3754
3897
  * effect-scoped inside `applyWorkspaceBrowser`).
3755
3898
  * @param ctx - client root context.
3756
3899
  */
3900
+ async function archiveWorkspaceSessionsAndRefresh(registry, workspaceId, refresh) {
3901
+ const result = await registry.archiveWorkspaceSessions(workspaceId);
3902
+ if (!result.ok) throw new Error(result.error.message);
3903
+ await refresh();
3904
+ return result.value;
3905
+ }
3757
3906
  async function apply(ctx) {
3758
3907
  const remote = ctx.get("remote");
3759
3908
  let disposeRemote = () => {};
@@ -3806,6 +3955,11 @@ window.__ModuleLoader__.load({
3806
3955
  if (!result.ok) throw new Error(result.error.message);
3807
3956
  await refreshSessionList();
3808
3957
  };
3958
+ const archiveWorkspaceSessions = async (workspaceId) => {
3959
+ const registry = ctx.get("remote.workspaceRegistry");
3960
+ if (registry === void 0) throw new Error("archive-manager remote service is unavailable");
3961
+ return archiveWorkspaceSessionsAndRefresh(registry, workspaceId, refreshSessionList);
3962
+ };
3809
3963
  const deleteSession = async (sessionId) => {
3810
3964
  const registry = ctx.get("remote.workspaceRegistry");
3811
3965
  if (registry === void 0) throw new Error("archive-manager remote service is unavailable");
@@ -3876,6 +4030,7 @@ window.__ModuleLoader__.load({
3876
4030
  archiveSession: async (sessionId) => {
3877
4031
  await ctx.workspaces.archiveSession(sessionId);
3878
4032
  },
4033
+ archiveWorkspaceSessions,
3879
4034
  unarchiveSession,
3880
4035
  deleteSession,
3881
4036
  insertSessionBefore: async (workspaceId, sessionId, beforeSessionId) => {
@@ -3943,6 +4098,10 @@ window.__ModuleLoader__.load({
3943
4098
  pruneArchivedSelection,
3944
4099
  toggleArchivedSelection,
3945
4100
  archivedDeleteFeedback,
4101
+ archiveableWorkspaceSessionCount,
4102
+ archiveWorkspaceDialogTarget,
4103
+ archiveWorkspaceDialogFailureState,
4104
+ archiveWorkspaceSessionsAndRefresh,
3946
4105
  createWorkspaceViewStore,
3947
4106
  bindObservable,
3948
4107
  hasSplitClientStore,
package/lib/projcache.js CHANGED
@@ -10,32 +10,54 @@ import { defineDomain, domainTable } from "@deepseek-ai/dsh-storage-domain";
10
10
  import { trackTombstone } from "./tombstone.js";
11
11
 
12
12
  /**
13
- * Archive Manager owns a separate cache domain so DSH alpha.2 never asks the
14
- * JSON backend to turn a logical IM id such as `im:qq:dm:...` into a Windows
15
- * filename. The public Session id remains unchanged; only the storage key is
16
- * a fixed-length SHA-256 token and the original id is carried in the value.
17
- *
18
- * On rc.2 the runtime has no per-record layout, so the same domain remains a
19
- * single JSON file. After that user upgrades to alpha.2, every already-safe
20
- * hashed key can be migrated by the official backend without hitting the
21
- * legacy colon bug.
13
+ * 当前投影缓存写入独立的 v2 域,避免 DSH alpha.2 将包含冒号的逻辑会话 ID
14
+ * 直接映射为 Windows 文件名。旧名 v1 与旧名 v2 域只在启动时读取并补缺迁入,
15
+ * 迁移不删除原始缓存,任意中断都可在后续启动继续。
22
16
  */
23
17
  const storedCheckpointRecord = checkpointRecord.extend({
24
18
  sessionId: checkpointIdentity.shape.cwd.unwrap()
25
19
  });
26
20
  const safeDomainInput = {
27
- name: "session_projcache_archive_manager",
28
- version: 1,
21
+ // 旧版 DSH 不支持同名域的版本迁移。v2 改用新域名,确保升级时不会在
22
+ // 打开目标域之前因旧 v1 元数据而终止启动。
23
+ name: "session_projcache_archive_manager_v2",
24
+ version: 2,
29
25
  tables: { sessions: domainTable(storedCheckpointRecord) },
30
26
  ...(projectionCacheDomainSpec.layout === "per-record" ? { layout: "per-record" } : {})
31
27
  };
32
28
  const safeProjectionCacheDomainSpec = defineDomain(safeDomainInput);
33
29
 
34
30
  /** The last official whole-file cache format used by DSH rc.2. */
31
+ const legacyOptionalIdentityFields = {};
32
+ for (const field of ["isSeeded", "inheritedEventCount"]) {
33
+ // rc.2 的身份 schema 尚未定义这些字段;向 Zod 传入不存在的字段会在
34
+ // 首次解析历史记录时抛错,因此只能按当前运行时实际的 schema 构造掩码。
35
+ if (Object.hasOwn(checkpointIdentity.shape, field)) legacyOptionalIdentityFields[field] = true;
36
+ }
37
+ const legacyCheckpointRecord = checkpointRecord.extend({
38
+ identity: checkpointIdentity.partial(legacyOptionalIdentityFields)
39
+ });
40
+ const legacyStoredCheckpointRecord = legacyCheckpointRecord.extend({
41
+ sessionId: checkpointIdentity.shape.cwd.unwrap()
42
+ });
43
+ /** v0.1.24 短暂使用过的旧名 v2 安全缓存域。 */
44
+ const legacySafeV2ProjectionCacheDomainSpec = defineDomain({
45
+ name: "session_projcache_archive_manager",
46
+ version: 2,
47
+ tables: { sessions: domainTable(storedCheckpointRecord) },
48
+ ...(projectionCacheDomainSpec.layout === "per-record" ? { layout: "per-record" } : {})
49
+ });
50
+ /** v0.1.23 及以前归档管理器自身使用的安全缓存域。 */
51
+ const legacySafeProjectionCacheDomainSpec = defineDomain({
52
+ name: "session_projcache_archive_manager",
53
+ version: 1,
54
+ tables: { sessions: domainTable(legacyStoredCheckpointRecord) },
55
+ ...(projectionCacheDomainSpec.layout === "per-record" ? { layout: "per-record" } : {})
56
+ });
35
57
  const legacyProjectionCacheDomainSpec = defineDomain({
36
58
  name: "session_projcache",
37
59
  version: 3,
38
- tables: { sessions: domainTable(checkpointRecord) }
60
+ tables: { sessions: domainTable(legacyCheckpointRecord) }
39
61
  });
40
62
 
41
63
  function projectionCacheStorageKey(sessionId) {
@@ -107,12 +129,59 @@ async function readSourceRecords(ctx, spec, label) {
107
129
  }
108
130
  }
109
131
 
132
+ async function readLegacySafeRecords(ctx, spec, label) {
133
+ const source = await readSourceRecords(ctx, spec, label);
134
+ const records = [];
135
+ for (const [physicalKey, stored] of source.records) {
136
+ if (stored === null || typeof stored !== "object" || typeof stored.sessionId !== "string") {
137
+ ctx.logger.warn(`archive-manager projcache: ${label} row "${physicalKey}" import skipped because its session id is invalid`);
138
+ continue;
139
+ }
140
+ if (projectionCacheStorageKey(stored.sessionId) !== physicalKey) {
141
+ ctx.logger.warn(`archive-manager projcache: ${label} row "${physicalKey}" import skipped because its storage key does not match the session id`);
142
+ continue;
143
+ }
144
+ records.push([stored.sessionId, unwrapStoredCheckpoint(stored)]);
145
+ }
146
+ return { ...source, records };
147
+ }
148
+
149
+ /**
150
+ * 旧版记录没有完整身份时,只能安全补齐未播种会话的零继承切点。
151
+ * 播种会话无法从头部反推出精确切点,宁可跳过并按需重建。
152
+ */
153
+ function normalizeLegacyRecord(record) {
154
+ if (record === null || typeof record !== "object" || record.identity === null || typeof record.identity !== "object") return void 0;
155
+ const identity = record.identity;
156
+ const inheritedEventCount = identity.inheritedEventCount;
157
+ const validOffset = Number.isSafeInteger(inheritedEventCount) && inheritedEventCount >= 0;
158
+ if (identity.isSeeded === true) {
159
+ if (!validOffset) return void 0;
160
+ return record;
161
+ }
162
+ if (identity.isSeeded !== void 0 && identity.isSeeded !== false) return void 0;
163
+ if (inheritedEventCount !== void 0 && inheritedEventCount !== 0) return void 0;
164
+ return {
165
+ ...record,
166
+ identity: {
167
+ ...identity,
168
+ isSeeded: false,
169
+ inheritedEventCount: 0
170
+ }
171
+ };
172
+ }
173
+
110
174
  async function importMissingRecords(ctx, target, records, label) {
111
175
  let imported = 0;
112
176
  for (const [sessionId, record] of records) {
113
177
  if (target.has(sessionId)) continue;
114
178
  try {
115
- await target.put(sessionId, record);
179
+ const normalized = normalizeLegacyRecord(record);
180
+ if (normalized === void 0) {
181
+ ctx.logger.warn(`archive-manager projcache: ${label} row "${sessionId}" import skipped because its lifecycle identity is incomplete`);
182
+ continue;
183
+ }
184
+ await target.put(sessionId, normalized);
116
185
  imported += 1;
117
186
  } catch (error) {
118
187
  ctx.logger.warn(`archive-manager projcache: ${label} row "${sessionId}" import failed: ${String(error)}`);
@@ -122,30 +191,33 @@ async function importMissingRecords(ctx, target, records, label) {
122
191
  }
123
192
 
124
193
  /**
125
- * Seed the compatibility domain without opening alpha.2's unsafe per-record
126
- * source while the v3 whole-file cache contains rows. An empty v3 source may
127
- * fall through to the current source; source-open failures stay fail-soft.
128
- * Imports are add-only: a newer compatibility-domain checkpoint always wins,
129
- * and a partially completed import resumes on the next startup.
194
+ * 合并归档管理器旧名 v2、旧名 v1、官方新版逐会话与旧版整文件缓存。目标域记录
195
+ * 最高优先,随后按新旧顺序只补缺失记录;任一来源失败时仍继续尝试另一个来源。
130
196
  */
131
197
  async function importPreviousProjectionCache(ctx, target) {
198
+ const legacySafeV2 = await readLegacySafeRecords(ctx, legacySafeV2ProjectionCacheDomainSpec, "archive-manager v2");
199
+ const legacySafe = await readLegacySafeRecords(ctx, legacySafeProjectionCacheDomainSpec, "archive-manager v1");
132
200
  const legacy = await readSourceRecords(ctx, legacyProjectionCacheDomainSpec, "legacy v3");
133
- if (!legacy.opened) return 0;
134
- if (legacy.records.length > 0) return importMissingRecords(ctx, target, legacy.records, "legacy v3");
135
201
  const sameSpec = projectionCacheDomainSpec.version === legacyProjectionCacheDomainSpec.version
136
202
  && projectionCacheDomainSpec.layout === legacyProjectionCacheDomainSpec.layout;
137
- if (sameSpec) return 0;
138
- const current = await readSourceRecords(ctx, projectionCacheDomainSpec, "current");
139
- if (!current.opened) return 0;
140
- return importMissingRecords(ctx, target, current.records, "current");
203
+ let imported = 0;
204
+ if (legacySafeV2.opened) imported += await importMissingRecords(ctx, target, legacySafeV2.records, "archive-manager v2");
205
+ if (legacySafe.opened) imported += await importMissingRecords(ctx, target, legacySafe.records, "archive-manager v1");
206
+ if (!sameSpec) {
207
+ const current = await readSourceRecords(ctx, projectionCacheDomainSpec, "current");
208
+ if (current.opened) imported += await importMissingRecords(ctx, target, current.records, "current");
209
+ }
210
+ if (legacy.opened) imported += await importMissingRecords(ctx, target, legacy.records, "legacy v3");
211
+ return imported;
141
212
  }
142
213
  //#region lib/types/index.js
143
214
  /**
144
215
  * dsh-archive-manager projcache 半边。
145
216
  *
146
217
  * `ArchiveProjectionCache` 继承上游 `SessionProjectionCache`,保留服务名
147
- * `sessionProjectionCache` 与 fail-soft 写路径,但使用独立、路径安全的
148
- * `session_projcache_archive_manager` 兼容域,为归档管理的
218
+ * `sessionProjectionCache` 与 fail-soft 写路径,但写入独立、路径安全的
219
+ * `session_projcache_archive_manager_v2` 域。旧名 v1 与 v2 域只在启动时
220
+ * 读取并补缺迁入;归档管理的
149
221
  * `deleteSession` 增加三处守护:
150
222
  *
151
223
  * - `delete(id)` - 永久移除兼容域中该会话经 SHA-256 映射的缓存投影行,
@@ -255,6 +327,8 @@ export {
255
327
  SafeSessionTable,
256
328
  importPreviousProjectionCache,
257
329
  legacyProjectionCacheDomainSpec,
330
+ legacySafeProjectionCacheDomainSpec,
331
+ legacySafeV2ProjectionCacheDomainSpec,
258
332
  projectionCacheStorageKey,
259
333
  safeProjectionCacheDomainSpec
260
334
  };
package/lib/workspace.js CHANGED
@@ -65,6 +65,12 @@ const sessionIdSchema = {
65
65
  return value;
66
66
  }
67
67
  };
68
+ const workspaceIdSchema = {
69
+ parse(value) {
70
+ if (typeof value !== "string" || value.length === 0) throw new TypeError(`workspaceId must be a non-empty string, got ${String(value)}`);
71
+ return value;
72
+ }
73
+ };
68
74
  const archivedSetSchema = {
69
75
  parse(value) {
70
76
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("result must be an object");
@@ -96,6 +102,14 @@ const unarchivedBatchSchema = {
96
102
  return value;
97
103
  }
98
104
  };
105
+ const archivedWorkspaceBatchSchema = {
106
+ parse(value) {
107
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("result must be an object");
108
+ if (!Array.isArray(value.archivedSessionIds) || value.archivedSessionIds.some((id) => typeof id !== "string")) throw new TypeError("archivedSessionIds must be a string array");
109
+ if (!Array.isArray(value.archivedSessionIdsAdded) || value.archivedSessionIdsAdded.some((id) => typeof id !== "string")) throw new TypeError("archivedSessionIdsAdded must be a string array");
110
+ return value;
111
+ }
112
+ };
99
113
  const deletedBatchSchema = {
100
114
  parse(value) {
101
115
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError("result must be an object");
@@ -176,6 +190,25 @@ const ARCHIVE_MANAGER_INVOCATIONS = [
176
190
  },
177
191
  sourceLocation: { file: "@michengai/dsh-archive-manager/lib/workspace.js", line: 1, column: 1 }
178
192
  },
193
+ {
194
+ id: "@michengai/dsh-archive-manager#workspaceRegistry/archiveWorkspaceSessions",
195
+ service: "workspaceRegistry",
196
+ namespace: "workspaceRegistry",
197
+ method: "archiveWorkspaceSessions",
198
+ invocation: { kind: "direct" },
199
+ parameters: [{
200
+ name: "workspaceId",
201
+ wire: "workspaceId",
202
+ source: "json",
203
+ codec: { mode: "strict", typeSymbol: "@deepseek-ai/dsh-workspace/types#WorkspaceId", schema: workspaceIdSchema }
204
+ }],
205
+ result: {
206
+ mode: "strict",
207
+ typeSymbol: "@michengai/dsh-archive-manager/types#ArchivedWorkspaceBatch",
208
+ schema: archivedWorkspaceBatchSchema
209
+ },
210
+ sourceLocation: { file: "@michengai/dsh-archive-manager/lib/workspace.js", line: 1, column: 1 }
211
+ },
179
212
  {
180
213
  id: "@michengai/dsh-archive-manager#workspaceRegistry/deleteArchivedSessions",
181
214
  service: "workspaceRegistry",
@@ -248,6 +281,7 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
248
281
  markRemoteMethod(this, "unarchiveSession");
249
282
  markRemoteMethod(this, "deleteSession");
250
283
  markRemoteMethod(this, "unarchiveSessions");
284
+ markRemoteMethod(this, "archiveWorkspaceSessions");
251
285
  markRemoteMethod(this, "deleteArchivedSessions");
252
286
  markRemoteMethod(this, "archivedSessionMetadata");
253
287
  registerHostRemote(this.ctx);
@@ -279,14 +313,27 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
279
313
  if (cache === void 0 || typeof cache.cachedSnapshot !== "function" || typeof cache.put !== "function") return false;
280
314
  if (persistence === void 0 || typeof persistence.readFrom !== "function" || projections === void 0 || typeof projections.restore !== "function") return false;
281
315
  try {
282
- if (cache.cachedSnapshot(header) !== void 0) return false;
316
+ // 未播种会话的继承事件数恒为零,先查缓存可避免读取完整会话原文。
317
+ if (!header.isSeeded && cache.cachedSnapshot(header, 0) !== void 0) return false;
283
318
  const stored = await persistence.readFrom(header.id, 0);
284
319
  const meta = stored.meta ?? header;
285
- const restored = projections.restore({}, stored.events, 0, meta);
320
+ if (meta.isSeeded === true && stored.inheritedEventCount === void 0) {
321
+ this.ctx.logger.warn(`archive-manager: projection repair for seeded archived session "${header.id}" skipped because its inherited event count is unavailable`);
322
+ return false;
323
+ }
324
+ const inheritedEventCount = stored.inheritedEventCount ?? 0;
325
+ if (meta.isSeeded !== true && inheritedEventCount !== 0) {
326
+ this.ctx.logger.warn(`archive-manager: projection repair for unseeded archived session "${header.id}" skipped because its inherited event count is not zero`);
327
+ return false;
328
+ }
329
+ if (cache.cachedSnapshot(meta, inheritedEventCount) !== void 0) return false;
330
+ const restored = projections.restore({}, stored.events, 0, meta, inheritedEventCount);
286
331
  if (restored === void 0 || typeof restored !== "object" || restored.checkpoint === void 0) return false;
287
332
  await cache.put(header.id, {
288
333
  createdAt: meta.createdAt,
289
- ...meta.cwd === void 0 ? {} : { cwd: meta.cwd }
334
+ ...(meta.cwd === void 0 ? {} : { cwd: meta.cwd }),
335
+ isSeeded: meta.isSeeded ?? false,
336
+ inheritedEventCount
290
337
  }, restored.checkpoint);
291
338
  return true;
292
339
  } catch (error) {
@@ -315,6 +362,36 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
315
362
  });
316
363
  }
317
364
  /**
365
+ * 将一个工作区内所有会话加入归档集合。先确认所有待归档会话仍存在,
366
+ * 再执行单次状态写入,因此未知会话不会导致项目只归档一部分。
367
+ */
368
+ async archiveWorkspaceSessions(workspaceId) {
369
+ return this.enqueueOperation(async () => {
370
+ workspaceId = workspaceIdSchema.parse(workspaceId);
371
+ const workspace = this.requireTable().get(workspaceId);
372
+ if (workspace === void 0) throw new Error(`unknown workspace "${workspaceId}"`);
373
+ const state = this.requireState();
374
+ const archived = new Set(state.archivedSessionIds);
375
+ const archivedSessionIdsAdded = [...new Set(workspace.sessionIds)].filter((sessionId) => !archived.has(sessionId));
376
+ for (const sessionId of archivedSessionIdsAdded) {
377
+ if (!await this.sessionKnown(sessionId)) throw new ArchiveUnknownSessionError(sessionId);
378
+ }
379
+ if (archivedSessionIdsAdded.length === 0) return {
380
+ archivedSessionIds: [...state.archivedSessionIds],
381
+ archivedSessionIdsAdded
382
+ };
383
+ const next = {
384
+ ...state,
385
+ archivedSessionIds: [...state.archivedSessionIds, ...archivedSessionIdsAdded]
386
+ };
387
+ await this.setState(next);
388
+ return {
389
+ archivedSessionIds: [...next.archivedSessionIds],
390
+ archivedSessionIdsAdded
391
+ };
392
+ });
393
+ }
394
+ /**
318
395
  * 按宿主权威归档集合一次恢复全部、一个工作区或未分组的归档会话。
319
396
  * 目标全部来自已归档集合,因此即使日志已被外部移除,也会清掉陈旧归档标记。
320
397
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michengai/dsh-archive-manager",
3
- "version": "0.1.23",
3
+ "version": "0.1.25",
4
4
  "description": "NPM-installable DSH Web plugin for managing archived sessions.",
5
5
  "type": "module",
6
6
  "packageManager": "pnpm@11.21.0",
@@ -28,22 +28,24 @@
28
28
  "scripts": {
29
29
  "build": "node scripts/check-package.mjs",
30
30
  "test": "node --test test/*.test.mjs",
31
+ "test:compat": "node scripts/test-legacy-compatibility.mjs",
31
32
  "pack:check": "pnpm pack --dry-run",
32
- "prepublishOnly": "pnpm build && pnpm test"
33
+ "prepublishOnly": "pnpm build && pnpm test && pnpm test:compat"
33
34
  },
34
35
  "peerDependencies": {
35
36
  "@deepseek-ai/cordis": "^4.0.1",
36
37
  "@deepseek-ai/dsh-client-connection": ">=0.1.0-rc.5 <0.2.0",
37
38
  "@deepseek-ai/dsh-client-locale": ">=0.1.0-rc.5 <0.2.0",
38
39
  "@deepseek-ai/dsh-client-runtime": ">=0.1.0-rc.5 <0.2.0",
40
+ "@deepseek-ai/dsh-client-store": ">=0.1.0-rc.5 <0.2.0",
39
41
  "@deepseek-ai/dsh-client-ui-conversation": ">=0.1.0-rc.5 <0.2.0",
40
42
  "@deepseek-ai/dsh-client-ui-primitives": ">=0.1.0-rc.5 <0.2.0",
41
43
  "@deepseek-ai/dsh-client-ui-sidebar": ">=0.1.0-rc.5 <0.2.0",
42
44
  "@deepseek-ai/dsh-client-ui-workspace": ">=0.1.0-rc.5 <0.2.0",
43
45
  "@deepseek-ai/dsh-session": ">=0.1.0-rc.5 <0.2.0",
44
46
  "@deepseek-ai/dsh-session-projection-cache": ">=0.1.0-rc.5 <0.2.0",
45
- "@deepseek-ai/dsh-storage-domain": ">=0.1.0-rc.5 <0.2.0",
46
47
  "@deepseek-ai/dsh-spill-local": ">=0.1.0-rc.5 <0.2.0",
48
+ "@deepseek-ai/dsh-storage-domain": ">=0.1.0-rc.5 <0.2.0",
47
49
  "@deepseek-ai/dsh-typert-protocol": ">=0.1.0-rc.5 <0.2.0",
48
50
  "@deepseek-ai/dsh-typert-registry": ">=0.1.0-rc.5 <0.2.0",
49
51
  "@deepseek-ai/dsh-workspace": ">=0.1.0-rc.5 <0.2.0"
@@ -51,20 +53,35 @@
51
53
  "peerDependenciesMeta": {
52
54
  "@deepseek-ai/dsh-client-runtime": {
53
55
  "optional": true
56
+ },
57
+ "@deepseek-ai/dsh-client-store": {
58
+ "optional": true
54
59
  }
55
60
  },
56
61
  "devDependencies": {
57
- "@deepseek-ai/cordis": "4.0.1",
58
- "@deepseek-ai/dsh-api-gateway": "0.1.0-rc.6",
59
- "@deepseek-ai/dsh-client-runtime": "0.1.0-rc.6",
60
- "@deepseek-ai/dsh-client-ui-slots": "0.1.0-rc.6",
61
- "@deepseek-ai/dsh-client-web-react": "0.1.0-rc.6",
62
- "@deepseek-ai/dsh-session-projection-cache": "0.1.0-rc.6",
63
- "@deepseek-ai/dsh-storage-domain": "0.1.0-rc.6",
64
- "@deepseek-ai/dsh-spill-local": "0.1.0-rc.6",
65
- "@deepseek-ai/dsh-typert-protocol": "0.1.0-rc.6",
66
- "@deepseek-ai/dsh-typert-registry": "0.1.0-rc.6",
67
- "@deepseek-ai/dsh-workspace": "0.1.0-rc.6",
62
+ "@deepseek-ai/cordis": "4.0.2",
63
+ "@deepseek-ai/dsh-api-gateway": "0.1.2-alpha.5",
64
+ "@deepseek-ai/dsh-client-connection": "0.1.2-alpha.5",
65
+ "@deepseek-ai/dsh-client-locale": "0.1.2-alpha.5",
66
+ "@deepseek-ai/dsh-client-store": "0.1.2-alpha.5",
67
+ "@deepseek-ai/dsh-client-ui-conversation": "0.1.2-alpha.5",
68
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.2-alpha.5",
69
+ "@deepseek-ai/dsh-client-ui-sidebar": "0.1.2-alpha.5",
70
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.2-alpha.5",
71
+ "@deepseek-ai/dsh-client-ui-workspace": "0.1.2-alpha.5",
72
+ "@deepseek-ai/dsh-invariants": "0.1.2-alpha.5",
73
+ "@deepseek-ai/dsh-scope": "0.1.2-alpha.5",
74
+ "@deepseek-ai/dsh-session": "0.1.2-alpha.5",
75
+ "@deepseek-ai/dsh-session-persistence": "0.1.2-alpha.5",
76
+ "@deepseek-ai/dsh-session-projection": "0.1.2-alpha.5",
77
+ "@deepseek-ai/dsh-session-projection-cache": "0.1.2-alpha.5",
78
+ "@deepseek-ai/dsh-spill": "0.1.2-alpha.5",
79
+ "@deepseek-ai/dsh-spill-local": "0.1.2-alpha.5",
80
+ "@deepseek-ai/dsh-storage": "0.1.2-alpha.5",
81
+ "@deepseek-ai/dsh-storage-domain": "0.1.2-alpha.5",
82
+ "@deepseek-ai/dsh-typert-protocol": "0.1.2-alpha.5",
83
+ "@deepseek-ai/dsh-typert-registry": "0.1.2-alpha.5",
84
+ "@deepseek-ai/dsh-workspace": "0.1.2-alpha.5",
68
85
  "react": "18.3.1",
69
86
  "react-dom": "18.3.1"
70
87
  },