@duke-dsh-plugins/dsh-archive-manager 1.3.6 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,6 +18,12 @@
18
18
 
19
19
  归档后消失在侧栏的会话,在这里统一呈现——**按项目分组、按更新时间倒序、支持搜索**,并且每个归档都能**一键还原**或**永久删除**。
20
20
 
21
+ > **版本适配(自动切换,无需配置)**
22
+ > - **DSH < 0.1.7**:完整归档管理面板(侧栏底部"归档"按钮 → 分组列表 / 搜索 / 还原 / 删除)。
23
+ > - **DSH ≥ 0.1.7**:DSH 自带归档管理(侧栏筛选"仅显示已归档"、行内取消归档),本插件**只补上它缺少的删除功能**——归档会话的 "…" 菜单里多一行"删除会话…"、行尾悬停多一个 🗑 按钮,点击弹出永久删除二次确认框;不再显示本插件自己的面板按钮。
24
+ > - 识别方式是**特性检测**(0.1.7 新增的两个行操作槽位是否声明),不是版本号,因此任何中间版本都会落到正确的一侧。
25
+ > - **支持 DSH 0.1.x 与 0.2.x**。0.2.0 起 DSH 会校验插件声明的 `@deepseek-ai/dsh*` peer 版本范围,不匹配就**整个 bundle 静默跳过**(插件直接消失,控制台只多一行 `skipping profile bundle ...`);v1.5.0 已放行 0.2.x 并同时保留 0.1.x。
26
+
21
27
  ## 功能
22
28
 
23
29
  | 功能 | 说明 |
@@ -46,12 +52,14 @@ dsh plugin --profile web add /path/to/dsh-archive-manager
46
52
  dsh plugin --profile web add https://github.com/MoonlitDropOfBlood/dsh-archive-manager/releases/download/v1.3.0/dsh-archive-manager-1.3.0.tgz
47
53
  ```
48
54
 
49
- 重启 DSH 后,侧栏底部(Cordis Plugin 下方、设置上方)会出现"归档"按钮。
55
+ 重启 DSH 后生效:**DSH < 0.1.7** 会在侧栏底部(Cordis Plugin 下方、设置上方)出现"归档"按钮;**DSH ≥ 0.1.7** 则在归档会话行上出现删除入口(见"使用")。
50
56
 
51
57
  > `dsh plugin add` 把插件装成 profile 的 npm 依赖并追加到 `dsh.profile.bundles`,启动时 DSH 自动应用包内的 `cordis.patch.yml` 挂载插件。卸载:`dsh plugin --profile web remove dsh-archive-manager`。
52
58
 
53
59
  ## 使用
54
60
 
61
+ **DSH < 0.1.7:**
62
+
55
63
  1. 点侧栏底部的 **🗂 归档** 按钮(带归档数量)。
56
64
  2. 面板按项目列出所有归档会话:
57
65
  - **还原**:会话回到侧栏对应项目下。
@@ -59,6 +67,14 @@ dsh plugin --profile web add https://github.com/MoonlitDropOfBlood/dsh-archive-m
59
67
  - **无效的归档记录**:日志已不存在的残留,点删除清理记录。
60
68
  3. 顶部搜索框可实时过滤。
61
69
 
70
+ **DSH ≥ 0.1.7(自带归档管理,本插件只加删除):**
71
+
72
+ 1. 侧栏工作区筛选 **仅显示已归档**(DSH 自带功能)。
73
+ 2. 归档会话行上任选其一发起删除:
74
+ - 行尾悬停的 **🗑 按钮**;
75
+ - "…" 菜单里的 **删除会话…**(危险红行,位于取消归档之后)。
76
+ 3. 弹出确认框后点 **永久删除**;运行中的智能体会被拦截,失败原因显示在框内;已删除但驻留内存的会话会提示重启后清除。
77
+
62
78
  ## 工作原理
63
79
 
64
80
  ```
@@ -79,6 +95,8 @@ dsh-archive-manager/
79
95
  ├── client.js # Client 半:归档管理 UI bundle
80
96
  ├── typert.host.js # Typert Host manifest(Remote 方法描述)
81
97
  ├── cordis.patch.yml # dsh bundle patch(挂载行)
98
+ ├── verify-*.mjs # 四个验证脚本(构造 / typert 线上契约 / client apply / 真实安装)
99
+ ├── scripts/ # check-profile-bundle.mjs:DSH 会不会跳过本插件的自检
82
100
  ├── .github/workflows/ # GitHub Actions 发布
83
101
  ├── AGENTS.md # 面向 AI agent 的开发指南(含踩坑)
84
102
  └── LICENSE # MIT
@@ -88,9 +106,17 @@ dsh-archive-manager/
88
106
 
89
107
  ```bash
90
108
  npm run check # node --check index.js client.js typert.host.js
109
+ npm run verify # 四个验证脚本:构造 / 双版本 typert 线上契约 / client apply 冒烟 / 真实 DSH 安装
110
+ node scripts/check-profile-bundle.mjs --sandbox # 用真实 dsh-app-boot 确认本插件不会被跳过
91
111
  dsh plugin --profile web add /path/to/dsh-archive-manager # 安装/重装到本机 DSH profile
92
112
  ```
93
113
 
114
+ `verify-typert-compat.mjs` 会用**真实 DSH 安装**里的 `validateTypertManifest` 与 `TypertRegistry` 校验本插件的线格式(同时覆盖 0.1.5 与 0.1.7 两套规则):默认探测本机 DSH 安装与 `.tmppkg/v017/`(npm 拉取的 0.1.7 包),也可用 `DSH_INSTALL_015` / `DSH_INSTALL_017` 环境变量指定。
115
+
116
+ `verify-real-install.mjs` 补的是另一个洞——前三个脚本跑在仓库自带的旧版依赖上,在"根本不加载插件"的 DSH 上也会全绿。它直接拿本机 DSH 安装的 `dsh-app-boot.evaluatePluginCompatibility` 跑 0.2.0 的 peer 闸门、断言 0.1.5/0.1.7/当前运行时三条版本线都满足、把 `index.js` 重新绑定到该安装的 cordis/typert-protocol 上构造服务,并在 0.1.x 与 0.2.x 两种磁盘布局上各真删一次会话。
117
+
118
+ `scripts/check-profile-bundle.mjs --sandbox` 会建一个临时 profile、用软链把**本工作树**挂进去,再调用真实 dsh-app-boot 的 profile 加载器,直接回答"dsh 会不会跳过我"。它不碰任何 DSH home,所以随时可跑。
119
+
94
120
  详见 [AGENTS.md](AGENTS.md)——记录了 DSH 正式插件(Host/Client/Typert 三件套)的完整机制和踩坑。
95
121
 
96
122
  ## License
package/client.js CHANGED
@@ -1,17 +1,31 @@
1
1
  /**
2
2
  * dsh-archive-manager — Client half (web bundle).
3
3
  *
4
- * Rendered by the DSH web shell via `window.__ModuleLoader__.load`. Registers:
5
- * - a sidebar footer action button (archived-session entry),
6
- * - a frame-wide archive manager panel (search / group / restore / delete),
7
- * rendered together with the button from the sidebar.footer.action slot —
8
- * mirroring how ui-settings renders its modal from sidebar.settings (see
9
- * the registration comment for the stacking-context rationale).
4
+ * Rendered by the DSH web shell via `window.__ModuleLoader__.load`. Two
5
+ * operating modes, chosen by feature detection (never by version string):
10
6
  *
11
- * The panel mirrors the DSH 工作区 (workspace) browsing region: a dialog
12
- * surface using the same design tokens as the workspace browser — folder
13
- * group headers, compact 32px session rows with a leading slot icon and
14
- * relative time, and hover-revealed actions (workspace rowActions pattern).
7
+ * - DSH < 0.1.7 (legacy): registers a sidebar footer action button
8
+ * (archived-session entry) plus a frame-wide archive manager panel
9
+ * (search / group / restore / delete), rendered together from the
10
+ * sidebar.footer.action slot — mirroring how ui-settings renders its modal
11
+ * from sidebar.settings (see the registration comment for the
12
+ * stacking-context rationale).
13
+ * - DSH >= 0.1.7 (modern): the shell ships its own archive management (the
14
+ * sidebar "仅显示已归档" filter with per-row archive actions), so this
15
+ * plugin adds ONLY the missing delete affordance: a danger row in each
16
+ * archived session's "..." menu (`sidebar.workspaces.session.menu.item`)
17
+ * and a hover trash button at the row end
18
+ * (`sidebar.workspaces.session.row.action`), both opening a confirm dialog
19
+ * rendered from the sidebar foot slot. Detection = whether those slots
20
+ * ever get declared: `ctx.slots.inject()` on an undeclared key never
21
+ * fires, so pre-0.1.7 builds stay on the legacy path automatically, and
22
+ * the footer button hides itself the moment the modern slots appear.
23
+ *
24
+ * The panel/dialog mirrors the DSH 工作区 (workspace) browsing region: a
25
+ * dialog surface using the same design tokens as the workspace browser —
26
+ * folder group headers, compact 32px session rows with a leading slot icon
27
+ * and relative time, and hover-revealed actions (workspace rowActions
28
+ * pattern).
15
29
  *
16
30
  * Host communication goes through the `archiveManager` Remote namespace
17
31
  * (`ctx.remote.archiveManager.restore/delete/state`), published by the Host
@@ -26,9 +40,11 @@ window.__ModuleLoader__.load({
26
40
  const React = require("react");
27
41
 
28
42
  // ---- CSS (package-owned, mirrors the DSH workspace browser + Modal) ----
29
- const CSS = `
30
- [class*="footerActions"]{flex-direction:column;align-items:center;}
31
-
43
+ // CSS_COMMON renders in both modes; CSS_LEGACY is the footer-button
44
+ // dressing, injected only while the legacy panel exists (cleared the
45
+ // moment the 0.1.7+ slots declare, so the modern shell keeps its own
46
+ // footer layout untouched).
47
+ const CSS_COMMON = `
32
48
  /* Overlay — same mask surface as DSH Modal, same z-index tier as the DSH
33
49
  settings overlay (1000). This layer MUST be rendered from a slot that sits
34
50
  outside any stacking context (the sidebar foot, like Settings does), never
@@ -98,7 +114,37 @@ window.__ModuleLoader__.load({
98
114
  .am-error{color:var(--dsw-alias-state-error-primary,#e5484d);padding:8px 24px;font-size:12px;line-height:18px;}
99
115
  .am-ghost{color:var(--dsw-alias-label-secondary,inherit);padding:8px 24px;font-size:12px;line-height:18px;}
100
116
 
101
- /* Sidebar footer entry (unchanged) */
117
+ /* ---- 0.1.7+ modern surfaces (delete affordances on the built-in archive) ---- */
118
+
119
+ /* Danger row in an archived session's "..." menu. Rendered through
120
+ ui-primitives' MenuItemButton when the module table provides it (native
121
+ look); this class styles the defensive plain-button fallback. */
122
+ .am-menu-del{display:flex;align-items:center;gap:8px;width:100%;padding:5px 12px;border:none;background:transparent;color:var(--dsw-alias-state-error-primary,#e5484d);font-family:inherit;font-size:13px;line-height:18px;text-align:left;cursor:pointer;border-radius:var(--dsw-radius-sm,6px);}
123
+ .am-menu-del:hover{background:var(--dsw-alias-interactive-bg-hover-danger,rgba(229,72,77,.12));}
124
+ .am-menu-del[disabled]{opacity:.5;cursor:default;}
125
+
126
+ /* Hover button at the row end — same 28px metrics as ui-workspace's own
127
+ row-action iconButton so it sits in the strip without reflow. */
128
+ .am-row-del{border-radius:var(--dsw-radius-sm,6px);cursor:pointer;width:28px;height:28px;color:var(--dsw-alias-label-secondary,inherit);background:0 0;border:none;flex:none;display:inline-flex;justify-content:center;align-items:center;padding:0;}
129
+ .am-row-del:hover{color:var(--dsw-alias-state-error-primary,#e5484d);}
130
+ .am-row-del[disabled]{opacity:.5;cursor:default;}
131
+
132
+ /* Confirm dialog — same surface family as the legacy panel, compact width.
133
+ Rendered from the sidebar foot slot (fixed, root stacking context) so
134
+ dsh-better-sidebar's z-index:25 body layer cannot cover it. */
135
+ .am-confirm{position:relative;display:flex;flex-direction:column;width:440px;max-width:calc(100vw - 48px);overflow:hidden;border:1px solid var(--dsw-alias-border-inverted,rgba(128,128,128,.3));border-radius:24px;background:var(--dsw-alias-bg-layer-2,#fff);color:var(--dsw-alias-label-primary,inherit);box-shadow:var(--dsw-shadow-lv3,0 12px 40px rgba(0,0,0,.25));}
136
+ .am-confirm .am-confirm-body{padding:4px 24px 16px;font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary,inherit);}
137
+ .am-confirm .am-confirm-title{font-weight:500;font-size:16px;line-height:24px;color:var(--dsw-alias-label-primary,inherit);}
138
+ .am-confirm .am-confirm-actions{display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:0 24px 20px;}
139
+ .am-confirm .am-confirm-error{color:var(--dsw-alias-state-error-primary,#e5484d);font-size:12px;line-height:18px;padding:0 24px 12px;}
140
+ .am-confirm .am-confirm-note{color:var(--dsw-alias-label-tertiary,inherit);font-size:12px;line-height:18px;}
141
+ .am-confirm .am-confirm-btn{border:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.2));background:transparent;color:var(--dsw-alias-label-primary,inherit);font-family:inherit;font-size:13px;line-height:18px;padding:6px 14px;border-radius:10px;cursor:pointer;}
142
+ .am-confirm .am-confirm-btn:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.12));}
143
+ .am-confirm .am-confirm-btn.am-confirm-danger{border-color:transparent;background:var(--dsw-alias-state-error-primary,#e5484d);color:var(--dsw-alias-bg-base,#fff);}
144
+ .am-confirm .am-confirm-btn.am-confirm-danger:hover{background:var(--dsw-alias-state-error-primary,#e5484d);filter:brightness(1.06);}
145
+ .am-confirm .am-confirm-btn[disabled]{opacity:.5;cursor:default;}
146
+
147
+ /* Sidebar footer entry (legacy mode only) */
102
148
  .am-foot-layer{flex:none;width:100%;margin:8px 0 0;display:flex;align-items:center;}
103
149
  .am-foot-layer .am-foot{width:100%;height:49px;border-radius:12px;padding:0 8px 0 6px;display:inline-flex;align-items:center;gap:8px;background:transparent;border:none;color:var(--dsw-alias-label-primary,inherit);font-family:inherit;font-size:14px;cursor:pointer;overflow:hidden;}
104
150
  .am-foot-layer .am-foot:hover{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.1));}
@@ -108,6 +154,13 @@ window.__ModuleLoader__.load({
108
154
  .am-foot-layer .am-foot .am-badge{color:var(--dsw-alias-label-tertiary,inherit);font-variant-numeric:tabular-nums;flex:none;margin-left:auto;font-size:12px;line-height:16px;}
109
155
  .am-foot-layer.am-foot-rail{width:36px;height:36px;margin:0;}
110
156
  .am-foot-layer.am-foot-rail .am-foot{width:36px;height:36px;padding:0;justify-content:center;border-radius:10px;}
157
+ `;
158
+
159
+ // Footer-column layout: only meaningful while OUR footer button exists.
160
+ // Injected at boot, cleared permanently once the 0.1.7+ slots declare
161
+ // (markModern), so a modern DSH footer keeps its own layout rules.
162
+ const CSS_LEGACY = `
163
+ [class*="footerActions"]{flex-direction:column;align-items:center;}
111
164
  `;
112
165
 
113
166
  // ---- Client Remote contribution ----------------------------------------
@@ -117,8 +170,18 @@ window.__ModuleLoader__.load({
117
170
  // must mount its own. Mirrors the invocations in typert.host.js (ids,
118
171
  // service/namespace/method, wire fields). zod is not requirable in the
119
172
  // browser module loader, so codecs use passthrough schemas — the runtime
120
- // contract only requires typeSymbol + schema.parse().
121
- const passthrough = () => ({ parse: (v) => v });
173
+ // contract only needs typeSymbol plus a parse (DSH < 0.1.7 reads
174
+ // `codec.schema.parse`) or a create() factory (the typert registry on
175
+ // DSH >= 0.1.7 rejects a strict codec with "has no create() factory").
176
+ // Every codec therefore carries BOTH fields, like typert.host.js.
177
+ let passthroughValue;
178
+ const passthroughSchema = () => (passthroughValue ??= { parse: (v) => v });
179
+ const strictClientCodec = (typeSymbol) => ({
180
+ mode: "strict",
181
+ typeSymbol,
182
+ schema: passthroughSchema(),
183
+ create: passthroughSchema,
184
+ });
122
185
  const CLIENT_REMOTE = {
123
186
  package: "dsh-archive-manager",
124
187
  descriptors: [
@@ -133,10 +196,10 @@ window.__ModuleLoader__.load({
133
196
  name: "request",
134
197
  wire: "request",
135
198
  source: "json",
136
- codec: { mode: "strict", typeSymbol: "dsh-archive-manager#ArchiveManagerRestoreRequest", schema: passthrough() },
199
+ codec: strictClientCodec("dsh-archive-manager#ArchiveManagerRestoreRequest"),
137
200
  },
138
201
  ],
139
- result: { mode: "strict", typeSymbol: "dsh-archive-manager#ArchiveManagerRestoreResult", schema: passthrough() },
202
+ result: strictClientCodec("dsh-archive-manager#ArchiveManagerRestoreResult"),
140
203
  },
141
204
  {
142
205
  id: "dsh-archive-manager#archiveManager/delete",
@@ -149,10 +212,10 @@ window.__ModuleLoader__.load({
149
212
  name: "request",
150
213
  wire: "request",
151
214
  source: "json",
152
- codec: { mode: "strict", typeSymbol: "dsh-archive-manager#ArchiveManagerDeleteRequest", schema: passthrough() },
215
+ codec: strictClientCodec("dsh-archive-manager#ArchiveManagerDeleteRequest"),
153
216
  },
154
217
  ],
155
- result: { mode: "strict", typeSymbol: "dsh-archive-manager#ArchiveManagerDeleteResult", schema: passthrough() },
218
+ result: strictClientCodec("dsh-archive-manager#ArchiveManagerDeleteResult"),
156
219
  },
157
220
  {
158
221
  id: "dsh-archive-manager#archiveManager/state",
@@ -161,7 +224,7 @@ window.__ModuleLoader__.load({
161
224
  method: "state",
162
225
  invocation: { kind: "direct" },
163
226
  parameters: [],
164
- result: { mode: "strict", typeSymbol: "dsh-archive-manager#ArchiveManagerStateResult", schema: passthrough() },
227
+ result: strictClientCodec("dsh-archive-manager#ArchiveManagerStateResult"),
165
228
  },
166
229
  ],
167
230
  };
@@ -172,14 +235,33 @@ window.__ModuleLoader__.load({
172
235
  await ctx.remote.$mount(CLIENT_REMOTE);
173
236
 
174
237
  const styleTag = document.createElement("style");
175
- styleTag.textContent = CSS;
238
+ styleTag.textContent = CSS_COMMON;
176
239
  document.head.appendChild(styleTag);
177
240
  ctx.effect(() => () => styleTag.remove());
178
241
 
242
+ // Legacy footer-column override; cleared for good by markModern() once
243
+ // the 0.1.7+ built-in archive slots declare.
244
+ const legacyStyleTag = document.createElement("style");
245
+ legacyStyleTag.textContent = CSS_LEGACY;
246
+ document.head.appendChild(legacyStyleTag);
247
+ ctx.effect(() => () => legacyStyleTag.remove());
248
+
179
249
  // ctx.get() reads the service without the property-accessor inject
180
250
  // guard; it exists because the $mount above just created it.
181
251
  const remote = ctx.get("remote.archiveManager");
182
252
 
253
+ // ui-primitives (MenuItemButton / Tooltip / IconTrashOutlineRegular) is
254
+ // part of the 0.1.7+ module table — ui-workspace requires it from the
255
+ // same table. Loaded defensively: pre-0.1.7 builds may not ship it, and
256
+ // the modern components (its only consumers) render only when the 0.1.7
257
+ // slots exist. Fallbacks below keep every surface functional without it.
258
+ let ui = null;
259
+ try {
260
+ ui = require("@deepseek-ai/dsh-client-ui-primitives");
261
+ } catch {
262
+ ui = null;
263
+ }
264
+
183
265
  // Shared open state between the footer button and the overlay.
184
266
  let open = false;
185
267
  const listeners = new Set();
@@ -208,6 +290,62 @@ window.__ModuleLoader__.load({
208
290
  }
209
291
  }
210
292
 
293
+ // ---- Modern (DSH >= 0.1.7) mode state --------------------------------
294
+ // modernStore flips on exactly when the built-in archive's row slots
295
+ // declare (see the inject registrations at the bottom of apply). Until
296
+ // then the legacy footer button renders; afterwards it hides itself and
297
+ // the footer slot only carries the delete confirm dialog.
298
+ const modernStore = (() => {
299
+ let value = false;
300
+ const listeners = new Set();
301
+ return {
302
+ getSnapshot: () => value,
303
+ subscribe: (fn) => { listeners.add(fn); return () => listeners.delete(fn); },
304
+ set: (next) => { if (value !== next) { value = next; for (const fn of listeners) fn(); } },
305
+ };
306
+ })();
307
+ function useModern() { return React.useSyncExternalStore(modernStore.subscribe, modernStore.getSnapshot); }
308
+
309
+ // Pending delete request for the modern confirm dialog ({sessionId,
310
+ // displayTitle} | null), set by the menu row / hover button.
311
+ const deleteStore = (() => {
312
+ let value = null;
313
+ const listeners = new Set();
314
+ return {
315
+ getSnapshot: () => value,
316
+ subscribe: (fn) => { listeners.add(fn); return () => listeners.delete(fn); },
317
+ set: (next) => { if (value !== next) { value = next; for (const fn of listeners) fn(); } },
318
+ };
319
+ })();
320
+ function useDeleteRequest() { return React.useSyncExternalStore(deleteStore.subscribe, deleteStore.getSnapshot); }
321
+
322
+ // Enter modern mode: hide the legacy button (and its footer-layout CSS)
323
+ // for good, then let the caller register its slot entries.
324
+ const markModern = () => {
325
+ modernStore.set(true);
326
+ legacyStyleTag.textContent = "";
327
+ };
328
+
329
+ /**
330
+ * Whether one session is in the registry-global archive set, read from
331
+ * the framework's global `useWorkspaces` standard hook (delivered to
332
+ * every slot component on every supported DSH). Fails closed to false
333
+ * when the hook is unavailable.
334
+ *
335
+ * @param {Function} useWorkspaces - the global workspace snapshot hook.
336
+ * @param {string} sessionId - row session id.
337
+ * @returns {boolean} archived membership.
338
+ */
339
+ function useArchivedMember(useWorkspaces, sessionId) {
340
+ try {
341
+ return useWorkspaces(
342
+ (s) => Array.isArray(s.archivedSessionIds) && s.archivedSessionIds.indexOf(sessionId) !== -1
343
+ ) === true;
344
+ } catch {
345
+ return false;
346
+ }
347
+ }
348
+
211
349
  function ArchiveIcon() {
212
350
  return React.createElement(
213
351
  "svg",
@@ -244,6 +382,24 @@ window.__ModuleLoader__.load({
244
382
  );
245
383
  }
246
384
 
385
+ // Trash glyph for the modern delete surfaces: ui-primitives' shipped
386
+ // icon when available (identical to the shell's own artwork), a plain
387
+ // outline SVG otherwise.
388
+ function TrashIcon({ size }) {
389
+ const px = size || 14;
390
+ if (ui && ui.IconTrashOutlineRegular) {
391
+ return React.createElement(ui.IconTrashOutlineRegular, { size: px });
392
+ }
393
+ return React.createElement(
394
+ "svg",
395
+ { width: px, height: px, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.3, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
396
+ React.createElement("path", { d: "M2.5 4.5h11" }),
397
+ React.createElement("path", { d: "M5.5 4.5v-1a1 1 0 0 1 1-1h3a1 1 0 0 1 1 1v1" }),
398
+ React.createElement("path", { d: "M4 4.5l.6 8.1a1 1 0 0 0 1 .9h4.8a1 1 0 0 0 1-.9l.6-8.1" }),
399
+ React.createElement("path", { d: "M6.5 7v4M9.5 7v4" })
400
+ );
401
+ }
402
+
247
403
  function fmtTime(ts) {
248
404
  if (typeof ts !== "number" || !isFinite(ts) || ts <= 0) return "";
249
405
  const diff = Date.now() - ts;
@@ -475,6 +631,194 @@ window.__ModuleLoader__.load({
475
631
  );
476
632
  }
477
633
 
634
+ // ---- Modern (DSH >= 0.1.7) delete surfaces ---------------------------
635
+ // The built-in archive management owns listing/restore on 0.1.7+; these
636
+ // three components add only DELETION on top of it. Both entry points
637
+ // show for archived, non-ghost rows only and open the shared confirm
638
+ // dialog (rendered from the sidebar foot slot — see the stacking note
639
+ // on the footer registration below).
640
+
641
+ /**
642
+ * Danger row in an archived session's "..." menu
643
+ * (`sidebar.workspaces.session.menu.item`, order 450 — after the
644
+ * shipped pin/rename/fork/archive rows). Dismisses the menu through the
645
+ * slot's `useMenuOpenState` hook before raising the confirm dialog.
646
+ *
647
+ * @param props - owner share (row identity) + the slot's hook seat + the global workspace hook.
648
+ * @returns the menu row, or null when delete does not apply.
649
+ */
650
+ function DeleteMenuEntry({ sessionId, displayTitle, useMenuOpenState, useWorkspaces }) {
651
+ const archived = useArchivedMember(useWorkspaces, sessionId);
652
+ const ghostIds = useGhostIds();
653
+ const ghost = ghostIds.indexOf(sessionId) !== -1;
654
+ const menuState = typeof useMenuOpenState === "function" ? useMenuOpenState() : null;
655
+ const setMenuOpen = menuState ? menuState[1] : () => {};
656
+ // Keep the ghost list fresh while the menu offering delete is open.
657
+ React.useEffect(() => { if (archived) refreshState(); }, [archived, sessionId]);
658
+ if (!archived || ghost) return null;
659
+ const select = () => {
660
+ setMenuOpen(false);
661
+ deleteStore.set({ sessionId, displayTitle });
662
+ };
663
+ if (ui && ui.MenuItemButton) {
664
+ return React.createElement(
665
+ ui.MenuItemButton,
666
+ {
667
+ danger: true,
668
+ separatorBefore: true,
669
+ icon: React.createElement(TrashIcon, { size: 14 }),
670
+ onSelect: select,
671
+ },
672
+ "删除会话…"
673
+ );
674
+ }
675
+ return React.createElement(
676
+ "button",
677
+ { type: "button", role: "menuitem", className: "am-menu-del", onClick: select },
678
+ React.createElement(TrashIcon, { size: 14 }),
679
+ "删除会话…"
680
+ );
681
+ }
682
+
683
+ /**
684
+ * Hover trash button at the archived row's end
685
+ * (`sidebar.workspaces.session.row.action`, order 300 — after the
686
+ * shipped archive/pin buttons). Clicks inside the row-action strip stay
687
+ * in the strip, so no propagation handling is needed.
688
+ *
689
+ * @param props - owner share (row identity) + the global workspace hook.
690
+ * @returns the button, or null when delete does not apply.
691
+ */
692
+ function DeleteRowButton({ sessionId, displayTitle, useWorkspaces }) {
693
+ const archived = useArchivedMember(useWorkspaces, sessionId);
694
+ const ghostIds = useGhostIds();
695
+ const ghost = ghostIds.indexOf(sessionId) !== -1;
696
+ React.useEffect(() => { if (archived) refreshState(); }, [archived, sessionId]);
697
+ if (!archived || ghost) return null;
698
+ const button = React.createElement(
699
+ "button",
700
+ {
701
+ type: "button",
702
+ className: "am-row-del",
703
+ title: "删除会话",
704
+ "aria-label": "删除会话",
705
+ onClick: () => deleteStore.set({ sessionId, displayTitle }),
706
+ },
707
+ React.createElement(TrashIcon, { size: 14 })
708
+ );
709
+ if (ui && ui.Tooltip) {
710
+ return React.createElement(
711
+ ui.Tooltip,
712
+ { label: "删除会话", side: "bottom", align: "end", delayMs: 500 },
713
+ button
714
+ );
715
+ }
716
+ return button;
717
+ }
718
+
719
+ /**
720
+ * The modern confirm dialog: permanent deletion gets the same 二次确认
721
+ * treatment as the legacy panel (per-request state dies with the
722
+ * request; errors stay visible; a live-but-idle target resolves into a
723
+ * ghost explanation instead of closing silently).
724
+ *
725
+ * @param props - the pending request and its dismissal.
726
+ * @returns the fixed overlay + dialog.
727
+ */
728
+ function DeleteConfirmForm({ req, onClose }) {
729
+ const [busy, setBusy] = React.useState(false);
730
+ const [error, setError] = React.useState(null);
731
+ const [ghost, setGhost] = React.useState(false);
732
+ // Esc cancels: the dialog owns z-index:1000, so swallow the key to
733
+ // keep it from reaching the shell's global Escape handlers.
734
+ React.useEffect(() => {
735
+ const onKeyDown = (e) => {
736
+ if (e.key === "Escape") {
737
+ e.stopPropagation();
738
+ if (!busy) onClose();
739
+ }
740
+ };
741
+ document.addEventListener("keydown", onKeyDown, true);
742
+ return () => document.removeEventListener("keydown", onKeyDown, true);
743
+ }, [busy]);
744
+ const close = () => { if (!busy) onClose(); };
745
+ const confirm = async () => {
746
+ if (busy) return;
747
+ setBusy(true);
748
+ setError(null);
749
+ try {
750
+ const res = await remote.delete({ sessionId: req.sessionId });
751
+ if (res && res.ok) {
752
+ try {
753
+ const sessions = ctx.get("sessions");
754
+ if (sessions && typeof sessions.refresh === "function") sessions.refresh();
755
+ } catch { /* non-fatal */ }
756
+ refreshState();
757
+ if (res.value && res.value.live) {
758
+ // Live-but-idle: files are gone, the archive entry stays as a
759
+ // ghost until restart (same semantics as the legacy panel).
760
+ setGhost(true);
761
+ } else {
762
+ onClose();
763
+ }
764
+ } else {
765
+ const err = res && res.error;
766
+ setError((err && err.message) || (err && err.code) || "删除失败");
767
+ }
768
+ } catch (e) {
769
+ setError(e instanceof Error ? e.message : String(e));
770
+ } finally {
771
+ setBusy(false);
772
+ }
773
+ };
774
+ const title = req.displayTitle || req.sessionId;
775
+ return React.createElement(
776
+ "div", { className: "am-overlay", onClick: close },
777
+ React.createElement(
778
+ "div", { className: "am-confirm", onClick: (e) => e.stopPropagation() },
779
+ React.createElement(
780
+ "div", { className: "am-header" },
781
+ React.createElement("span", { className: "am-title" }, ghost ? "已删除会话" : "删除会话"),
782
+ React.createElement(
783
+ "button",
784
+ { className: "am-close", onClick: close, disabled: busy, title: "关闭", "aria-label": "关闭" },
785
+ "✕"
786
+ )
787
+ ),
788
+ React.createElement(
789
+ "div", { className: "am-confirm-body" },
790
+ ghost
791
+ ? "日志文件已删除。该会话仍驻留内存,删除条目将在重启 DSH 后自动清除。"
792
+ : "将永久删除「" + title + "」的会话日志文件,此操作不可恢复。"
793
+ ),
794
+ error && !ghost
795
+ ? React.createElement("div", { className: "am-confirm-error" }, error)
796
+ : null,
797
+ React.createElement(
798
+ "div", { className: "am-confirm-actions" },
799
+ ghost
800
+ ? React.createElement(
801
+ "button",
802
+ { type: "button", className: "am-confirm-btn", onClick: onClose },
803
+ "关闭"
804
+ )
805
+ : [
806
+ React.createElement(
807
+ "button",
808
+ { key: "cancel", type: "button", className: "am-confirm-btn", disabled: busy, onClick: close },
809
+ "取消"
810
+ ),
811
+ React.createElement(
812
+ "button",
813
+ { key: "confirm", type: "button", className: "am-confirm-btn am-confirm-danger", disabled: busy, onClick: confirm },
814
+ busy ? "删除中…" : "永久删除"
815
+ ),
816
+ ]
817
+ )
818
+ )
819
+ );
820
+ }
821
+
478
822
  // Sidebar footer action entry (Cordis Plugin 下方、设置上方;纵向排列由注入
479
823
  // CSS 保证) + frame-wide archive manager panel.
480
824
  //
@@ -498,6 +842,8 @@ window.__ModuleLoader__.load({
498
842
  const count = rawCount - (ghostIds.length > rawCount ? rawCount : ghostIds.length);
499
843
  const safeCount = count > 0 ? count : 0;
500
844
  const isOpen = useOpen();
845
+ const modern = useModern();
846
+ const deleteReq = useDeleteRequest();
501
847
  // Esc 关闭弹窗:打开期间在 document 捕获阶段监听 keydown,命中
502
848
  // Escape 即关闭并 stopPropagation——弹窗位于 z-index:1000 顶层,
503
849
  // 此时 Esc 应只作用于它,不穿透到 shell 的全局快捷键或底层输入框。
@@ -512,6 +858,18 @@ window.__ModuleLoader__.load({
512
858
  document.addEventListener("keydown", onKeyDown, true);
513
859
  return () => document.removeEventListener("keydown", onKeyDown, true);
514
860
  }, [isOpen]);
861
+ // DSH >= 0.1.7: the shell's built-in archive management owns the
862
+ // sidebar entry — no button, no panel; this slot carries only the
863
+ // delete confirm dialog that this mode exists to add.
864
+ if (modern) {
865
+ return deleteReq
866
+ ? React.createElement(DeleteConfirmForm, {
867
+ key: deleteReq.sessionId,
868
+ req: deleteReq,
869
+ onClose: () => deleteStore.set(null),
870
+ })
871
+ : null;
872
+ }
515
873
  const children = [];
516
874
  children.push(React.createElement("span", { key: "icon", className: "am-foot-icon", "aria-hidden": true }, React.createElement(ArchiveIcon)));
517
875
  if (wide) children.push(React.createElement("span", { key: "label", className: "am-foot-label" }, "归档"));
@@ -535,10 +893,36 @@ window.__ModuleLoader__.load({
535
893
  return [button, overlay];
536
894
  }
537
895
  ));
896
+
897
+ // ---- Modern-mode registrations (fired only on DSH >= 0.1.7) ----------
898
+ // ui-workspace declares these two lists when it registers the built-in
899
+ // archive browsing region (the "仅显示已归档" filter world). On earlier
900
+ // DSH builds the keys are never declared: ctx.slots.inject() waits
901
+ // silently, these callbacks never run, no entry is registered, and the
902
+ // legacy footer button stays — that IS the version switch. The first
903
+ // declaration also flips markModern(), which hides the legacy button
904
+ // and drops the footer-column CSS for good.
905
+ ctx.slots.inject("sidebar.workspaces.session.menu.item", () => {
906
+ markModern();
907
+ return ctx.slots.register(
908
+ { name: "sidebar.workspaces.session.menu.item", id: "dsh-archive-manager.delete", order: 450 },
909
+ DeleteMenuEntry
910
+ );
911
+ });
912
+ ctx.slots.inject("sidebar.workspaces.session.row.action", () => {
913
+ markModern();
914
+ return ctx.slots.register(
915
+ { name: "sidebar.workspaces.session.row.action", id: "dsh-archive-manager.delete", order: 300 },
916
+ DeleteRowButton
917
+ );
918
+ });
538
919
  }
539
920
 
540
921
  exports.apply = apply;
541
922
  exports.inject = ["slots", "remote"];
923
+ // Exposed for verify-typert-compat.mjs (the browser wiring contract has
924
+ // to satisfy both DSH typert registries; nothing else reads this).
925
+ exports.CLIENT_REMOTE = CLIENT_REMOTE;
542
926
  return module.exports;
543
927
  },
544
928
  });
package/index.js CHANGED
@@ -23,8 +23,9 @@
23
23
 
24
24
  import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
25
25
  import { Service } from "@deepseek-ai/cordis";
26
- import { existsSync } from "node:fs";
26
+ import { existsSync, statSync } from "node:fs";
27
27
  import { rm } from "node:fs/promises";
28
+ import { basename, dirname, extname } from "node:path";
28
29
 
29
30
  /**
30
31
  * Mark one instance method as a Remote export without relying on decorator
@@ -71,6 +72,36 @@ function readArchived(domain) {
71
72
  return [];
72
73
  }
73
74
 
75
+ /**
76
+ * Resolve the session directory that owns a session's log artifact.
77
+ *
78
+ * `sessionPersistence.locate(header).path` changed meaning across DSH majors:
79
+ * - DSH < 0.2.0: the session DIRECTORY (`…/<project>/<sessionId>/`), which is
80
+ * what `rm(..., { recursive: true })` was written against.
81
+ * - DSH >= 0.2.0 (`dsh-session-persistence-jsonl`): the current generation's
82
+ * log FILE inside that directory (`…/<project>/<sessionId>/session.v4.jsonl.zstd`).
83
+ * Removing only that file would leave the directory and any older generation
84
+ * (`session.v3.jsonl.zstd`) behind, so a restart could resurrect the session —
85
+ * exactly what "永久删除" must not do.
86
+ *
87
+ * So: prefer `stat` on the path itself; when the artifact is already gone, fall
88
+ * back to the name shape — a located log file always carries an extension
89
+ * (`session.v4.jsonl.zstd`), while a per-session directory never does.
90
+ *
91
+ * @param {string} locatedPath - `sessionPersistence.locate(header).path`.
92
+ * @returns {string} the directory to remove.
93
+ */
94
+ function deletionTarget(locatedPath) {
95
+ const target = String(locatedPath);
96
+ try {
97
+ if (statSync(target).isDirectory()) return target;
98
+ if (statSync(target).isFile()) return dirname(target);
99
+ } catch {
100
+ /* artifact already gone — infer from the path shape below */
101
+ }
102
+ return extname(basename(target)) === "" ? target : dirname(target);
103
+ }
104
+
74
105
  export class ArchiveManagerService extends TypertRemoteService {
75
106
  static inject = [
76
107
  "workspaceRegistry",
@@ -194,9 +225,20 @@ export class ArchiveManagerService extends TypertRemoteService {
194
225
  return { ok: false, error: { code: "agent-running", message: "该会话的智能体正在运行中,请等待其完成或停止后再删除" } };
195
226
  }
196
227
  const header = await this.findHeader(sessionId);
228
+ const sessionPersistence = this.ctx.get("sessionPersistence");
229
+ if (header && (!sessionPersistence || typeof sessionPersistence.locate !== "function")) {
230
+ // The header exists but this build cannot tell us where its log lives.
231
+ // Claiming success here would silently keep the session on disk.
232
+ return {
233
+ ok: false,
234
+ error: {
235
+ code: "locate-unavailable",
236
+ message: "当前 DSH 版本的会话存储未提供 locate(),无法定位会话日志目录,已保留归档条目",
237
+ },
238
+ };
239
+ }
197
240
  let location;
198
241
  if (header) {
199
- const sessionPersistence = this.ctx.get("sessionPersistence");
200
242
  try {
201
243
  location = sessionPersistence.locate(header);
202
244
  } catch {
@@ -206,7 +248,7 @@ export class ArchiveManagerService extends TypertRemoteService {
206
248
  let removed = false;
207
249
  let fileError;
208
250
  if (location && location.path) {
209
- const target = String(location.path);
251
+ const target = deletionTarget(location.path);
210
252
  try {
211
253
  // Delete straight from the host process via node:fs — no shell, no
212
254
  // PowerShell, no sandbox escalation, so this behaves identically on
@@ -214,7 +256,8 @@ export class ArchiveManagerService extends TypertRemoteService {
214
256
  // the host's own privileges, which makes the sandbox ACL dance the
215
257
  // previous shell-based implementation needed unnecessary. maxRetries
216
258
  // absorbs transient Windows file locks (antivirus / indexers) and is
217
- // a no-op elsewhere.
259
+ // a no-op elsewhere. The whole owning session directory goes, so older
260
+ // format generations cannot survive the delete (DSH >= 0.2.0).
218
261
  await rm(target, { recursive: true, force: true, maxRetries: 3, retryDelay: 200 });
219
262
  removed = !existsSync(target);
220
263
  if (!removed) fileError = "path-still-exists";
@@ -263,8 +306,10 @@ export class ArchiveManagerService extends TypertRemoteService {
263
306
  const session = sessions.get(id);
264
307
  if (session === undefined) continue; // not live in memory → not a ghost
265
308
  const location = sessionPersistence.locate(session.header);
266
- // Live but the log directory is gone → ghost the client hides.
267
- if (location && location.path && !existsSync(String(location.path))) ghostIds.push(id);
309
+ // Live but the log is gone → ghost the client hides. Probed against the
310
+ // owning session directory, i.e. exactly what `delete` removes: on DSH
311
+ // >= 0.2.0 `locate` names the current generation's file inside it.
312
+ if (location && location.path && !existsSync(deletionTarget(location.path))) ghostIds.push(id);
268
313
  } catch {
269
314
  /* skip */
270
315
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@duke-dsh-plugins/dsh-archive-manager",
3
- "version": "1.3.6",
4
- "description": "Archive manager for DeepSeek Harness: group archived sessions by project, newest-first, with search, restore and delete.",
3
+ "version": "1.5.0",
4
+ "description": "Archive manager for DeepSeek Harness: DSH < 0.1.7 gets the grouped search/restore/delete panel; DSH >= 0.1.7 adds the delete action missing from the built-in archive management.",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/MoonlitDropOfBlood/dsh-archive-manager.git"
@@ -36,13 +36,14 @@
36
36
  },
37
37
  "peerDependencies": {
38
38
  "@deepseek-ai/cordis": "^4.0.1",
39
- "@deepseek-ai/dsh-storage-domain": "^0.1.0-rc.6",
40
- "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.6"
39
+ "@deepseek-ai/dsh-storage-domain": "^0.1.0-rc.6 || ^0.2.0-rc.1",
40
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.0-rc.6 || ^0.2.0-rc.1"
41
41
  },
42
42
  "dependencies": {
43
43
  "zod": "^4.4.3"
44
44
  },
45
45
  "scripts": {
46
- "check": "node --check index.js && node --check client.js && node --check typert.host.js"
46
+ "check": "node --check index.js && node --check client.js && node --check typert.host.js",
47
+ "verify": "node verify-constructor.mjs && node verify-typert-compat.mjs && node verify-client-apply.mjs && node verify-real-install.mjs"
47
48
  }
48
49
  }
package/typert.host.js CHANGED
@@ -8,72 +8,131 @@
8
8
  *
9
9
  * Keep the invocation ids, service/namespace names and method names in sync
10
10
  * with `index.js` (ArchiveManagerService) and `client.js`.
11
+ *
12
+ * Cross-version note: strict codecs carry BOTH `schema` (DSH < 0.1.7 reads
13
+ * the zod instance) and `create` (DSH >= 0.1.7 requires the factory) — see
14
+ * the codec contract comment below before touching any codec.
11
15
  */
12
16
 
13
17
  import { z } from "zod";
14
18
 
15
19
  // ---- shared shapes ---------------------------------------------------------
20
+ //
21
+ // DSH-versioned codec contract (the ONE field that differs across DSH builds):
22
+ // - DSH < 0.1.7 (typert-loader / typert-registry): a strict codec must carry
23
+ // `codec.schema` — a zod v4 INSTANCE (`_zod` + `parse`) — and the runtime
24
+ // calls `codec.schema.parse(...)`.
25
+ // - DSH >= 0.1.7: a strict codec must carry `codec.create` — a FACTORY
26
+ // function returning a zod schema — and the runtime calls
27
+ // `codec.create().parse(...)`; a plain instance in `schema` fails
28
+ // validation with "strict codec has no create() factory".
29
+ // Every codec below therefore carries BOTH: `schema` (materialized instance,
30
+ // read by <0.1.7) and `create` (memoized factory, read by >=0.1.7). Each
31
+ // version's validator only inspects its own field, so the extra field is
32
+ // ignored everywhere. Same for TYPERT.schemas entries (0.1.7 requires
33
+ // `create`; our list stays empty, which satisfies both).
34
+
35
+ /**
36
+ * Memoize one zod schema behind a `create()`-style factory: repeated calls
37
+ * return the same instance (the official 0.1.7 manifests use this exact
38
+ * `value ??= build()` pattern).
39
+ *
40
+ * @param {() => object} build - constructs the zod schema once.
41
+ * @returns {() => object} the factory.
42
+ */
43
+ function lazy(build) {
44
+ let value;
45
+ return () => (value ??= build());
46
+ }
16
47
 
17
- const sessionIdSchema = z.intersection(z.string(), z.unknown()).readonly();
48
+ const sessionIdSchema = lazy(() => z.intersection(z.string(), z.unknown()).readonly());
18
49
 
19
- const restoreResultSchema = z.union([
20
- z.object({
21
- ok: z.literal(true).readonly(),
22
- value: z.object({
23
- restored: z.literal(true).readonly(),
50
+ const restoreResultSchema = lazy(() =>
51
+ z.union([
52
+ z.object({
53
+ ok: z.literal(true).readonly(),
54
+ value: z.object({
55
+ restored: z.literal(true).readonly(),
56
+ }).readonly(),
24
57
  }).readonly(),
25
- }).readonly(),
26
- z.object({
27
- ok: z.literal(false).readonly(),
28
- error: z.object({
29
- code: z.string().readonly(),
30
- message: z.string().readonly().optional(),
58
+ z.object({
59
+ ok: z.literal(false).readonly(),
60
+ error: z.object({
61
+ code: z.string().readonly(),
62
+ message: z.string().readonly().optional(),
63
+ }).readonly(),
31
64
  }).readonly(),
32
- }).readonly(),
33
- ]);
65
+ ])
66
+ );
34
67
 
35
- const deleteResultSchema = z.union([
36
- z.object({
37
- ok: z.literal(true).readonly(),
38
- value: z.object({
39
- fileRemoved: z.boolean().readonly(),
40
- live: z.boolean().readonly(),
68
+ const deleteResultSchema = lazy(() =>
69
+ z.union([
70
+ z.object({
71
+ ok: z.literal(true).readonly(),
72
+ value: z.object({
73
+ fileRemoved: z.boolean().readonly(),
74
+ live: z.boolean().readonly(),
75
+ }).readonly(),
41
76
  }).readonly(),
42
- }).readonly(),
43
- z.object({
44
- ok: z.literal(false).readonly(),
45
- error: z.object({
46
- code: z.string().readonly(),
47
- message: z.string().readonly().optional(),
77
+ z.object({
78
+ ok: z.literal(false).readonly(),
79
+ error: z.object({
80
+ code: z.string().readonly(),
81
+ message: z.string().readonly().optional(),
82
+ }).readonly(),
48
83
  }).readonly(),
49
- }).readonly(),
50
- ]);
84
+ ])
85
+ );
51
86
 
52
- const stateResultSchema = z.union([
53
- z.object({
54
- ok: z.literal(true).readonly(),
55
- value: z.object({
56
- ghostIds: z.array(sessionIdSchema).readonly(),
87
+ const stateResultSchema = lazy(() =>
88
+ z.union([
89
+ z.object({
90
+ ok: z.literal(true).readonly(),
91
+ value: z.object({
92
+ ghostIds: z.array(sessionIdSchema()).readonly(),
93
+ }).readonly(),
57
94
  }).readonly(),
58
- }).readonly(),
59
- z.object({
60
- ok: z.literal(false).readonly(),
61
- error: z.object({
62
- code: z.string().readonly(),
63
- message: z.string().readonly().optional(),
95
+ z.object({
96
+ ok: z.literal(false).readonly(),
97
+ error: z.object({
98
+ code: z.string().readonly(),
99
+ message: z.string().readonly().optional(),
100
+ }).readonly(),
64
101
  }).readonly(),
65
- }).readonly(),
66
- ]);
102
+ ])
103
+ );
67
104
 
68
105
  // ---- per-invocation parameter/result schemas -------------------------------
69
106
 
70
- const _archiveManager_restore_parameter_0$schema = z.object({
71
- sessionId: sessionIdSchema,
72
- });
107
+ const restoreRequestSchema = lazy(() =>
108
+ z.object({
109
+ sessionId: sessionIdSchema(),
110
+ })
111
+ );
112
+
113
+ const deleteRequestSchema = lazy(() =>
114
+ z.object({
115
+ sessionId: sessionIdSchema(),
116
+ })
117
+ );
73
118
 
74
- const _archiveManager_delete_parameter_0$schema = z.object({
75
- sessionId: sessionIdSchema,
76
- });
119
+ /**
120
+ * Build the dual-version strict codec: `schema` for DSH < 0.1.7 (instance
121
+ * read at validation time), `create` for DSH >= 0.1.7 (factory invoked by
122
+ * the runtime). Both point at the same memoized instance.
123
+ *
124
+ * @param {string} typeSymbol - wire type symbol.
125
+ * @param {() => object} factory - the memoized schema factory.
126
+ * @returns {object} the codec descriptor.
127
+ */
128
+ function strictCodec(typeSymbol, factory) {
129
+ return {
130
+ mode: "strict",
131
+ typeSymbol,
132
+ schema: factory(),
133
+ create: factory,
134
+ };
135
+ }
77
136
 
78
137
  export const TYPERT = {
79
138
  package: "@duke-dsh-plugins/dsh-archive-manager",
@@ -91,18 +150,16 @@ export const TYPERT = {
91
150
  name: "request",
92
151
  wire: "request",
93
152
  source: "json",
94
- codec: {
95
- mode: "strict",
96
- typeSymbol: "dsh-archive-manager#ArchiveManagerRestoreRequest",
97
- schema: _archiveManager_restore_parameter_0$schema,
98
- },
153
+ codec: strictCodec(
154
+ "dsh-archive-manager#ArchiveManagerRestoreRequest",
155
+ restoreRequestSchema
156
+ ),
99
157
  },
100
158
  ],
101
- result: {
102
- mode: "strict",
103
- typeSymbol: "dsh-archive-manager#ArchiveManagerRestoreResult",
104
- schema: restoreResultSchema,
105
- },
159
+ result: strictCodec(
160
+ "dsh-archive-manager#ArchiveManagerRestoreResult",
161
+ restoreResultSchema
162
+ ),
106
163
  sourceLocation: { file: "index.js", line: 1, column: 1 },
107
164
  },
108
165
  {
@@ -116,18 +173,16 @@ export const TYPERT = {
116
173
  name: "request",
117
174
  wire: "request",
118
175
  source: "json",
119
- codec: {
120
- mode: "strict",
121
- typeSymbol: "dsh-archive-manager#ArchiveManagerDeleteRequest",
122
- schema: _archiveManager_delete_parameter_0$schema,
123
- },
176
+ codec: strictCodec(
177
+ "dsh-archive-manager#ArchiveManagerDeleteRequest",
178
+ deleteRequestSchema
179
+ ),
124
180
  },
125
181
  ],
126
- result: {
127
- mode: "strict",
128
- typeSymbol: "dsh-archive-manager#ArchiveManagerDeleteResult",
129
- schema: deleteResultSchema,
130
- },
182
+ result: strictCodec(
183
+ "dsh-archive-manager#ArchiveManagerDeleteResult",
184
+ deleteResultSchema
185
+ ),
131
186
  sourceLocation: { file: "index.js", line: 1, column: 1 },
132
187
  },
133
188
  {
@@ -137,11 +192,10 @@ export const TYPERT = {
137
192
  method: "state",
138
193
  invocation: { kind: "direct" },
139
194
  parameters: [],
140
- result: {
141
- mode: "strict",
142
- typeSymbol: "dsh-archive-manager#ArchiveManagerStateResult",
143
- schema: stateResultSchema,
144
- },
195
+ result: strictCodec(
196
+ "dsh-archive-manager#ArchiveManagerStateResult",
197
+ stateResultSchema
198
+ ),
145
199
  sourceLocation: { file: "index.js", line: 1, column: 1 },
146
200
  },
147
201
  ],