@duke-dsh-plugins/dsh-archive-manager 1.3.6 → 1.4.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 +19 -1
- package/client.js +406 -22
- package/package.json +4 -3
- package/typert.host.js +126 -72
package/README.md
CHANGED
|
@@ -18,6 +18,11 @@
|
|
|
18
18
|
|
|
19
19
|
归档后消失在侧栏的会话,在这里统一呈现——**按项目分组、按更新时间倒序、支持搜索**,并且每个归档都能**一键还原**或**永久删除**。
|
|
20
20
|
|
|
21
|
+
> **版本适配(自动切换,无需配置)**
|
|
22
|
+
> - **DSH < 0.1.7**:完整归档管理面板(侧栏底部"归档"按钮 → 分组列表 / 搜索 / 还原 / 删除)。
|
|
23
|
+
> - **DSH ≥ 0.1.7**:DSH 自带归档管理(侧栏筛选"仅显示已归档"、行内取消归档),本插件**只补上它缺少的删除功能**——归档会话的 "…" 菜单里多一行"删除会话…"、行尾悬停多一个 🗑 按钮,点击弹出永久删除二次确认框;不再显示本插件自己的面板按钮。
|
|
24
|
+
> - 识别方式是**特性检测**(0.1.7 新增的两个行操作槽位是否声明),不是版本号,因此任何中间版本都会落到正确的一侧。
|
|
25
|
+
|
|
21
26
|
## 功能
|
|
22
27
|
|
|
23
28
|
| 功能 | 说明 |
|
|
@@ -46,12 +51,14 @@ dsh plugin --profile web add /path/to/dsh-archive-manager
|
|
|
46
51
|
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
52
|
```
|
|
48
53
|
|
|
49
|
-
重启 DSH
|
|
54
|
+
重启 DSH 后生效:**DSH < 0.1.7** 会在侧栏底部(Cordis Plugin 下方、设置上方)出现"归档"按钮;**DSH ≥ 0.1.7** 则在归档会话行上出现删除入口(见"使用")。
|
|
50
55
|
|
|
51
56
|
> `dsh plugin add` 把插件装成 profile 的 npm 依赖并追加到 `dsh.profile.bundles`,启动时 DSH 自动应用包内的 `cordis.patch.yml` 挂载插件。卸载:`dsh plugin --profile web remove dsh-archive-manager`。
|
|
52
57
|
|
|
53
58
|
## 使用
|
|
54
59
|
|
|
60
|
+
**DSH < 0.1.7:**
|
|
61
|
+
|
|
55
62
|
1. 点侧栏底部的 **🗂 归档** 按钮(带归档数量)。
|
|
56
63
|
2. 面板按项目列出所有归档会话:
|
|
57
64
|
- **还原**:会话回到侧栏对应项目下。
|
|
@@ -59,6 +66,14 @@ dsh plugin --profile web add https://github.com/MoonlitDropOfBlood/dsh-archive-m
|
|
|
59
66
|
- **无效的归档记录**:日志已不存在的残留,点删除清理记录。
|
|
60
67
|
3. 顶部搜索框可实时过滤。
|
|
61
68
|
|
|
69
|
+
**DSH ≥ 0.1.7(自带归档管理,本插件只加删除):**
|
|
70
|
+
|
|
71
|
+
1. 侧栏工作区筛选 **仅显示已归档**(DSH 自带功能)。
|
|
72
|
+
2. 归档会话行上任选其一发起删除:
|
|
73
|
+
- 行尾悬停的 **🗑 按钮**;
|
|
74
|
+
- "…" 菜单里的 **删除会话…**(危险红行,位于取消归档之后)。
|
|
75
|
+
3. 弹出确认框后点 **永久删除**;运行中的智能体会被拦截,失败原因显示在框内;已删除但驻留内存的会话会提示重启后清除。
|
|
76
|
+
|
|
62
77
|
## 工作原理
|
|
63
78
|
|
|
64
79
|
```
|
|
@@ -88,9 +103,12 @@ dsh-archive-manager/
|
|
|
88
103
|
|
|
89
104
|
```bash
|
|
90
105
|
npm run check # node --check index.js client.js typert.host.js
|
|
106
|
+
npm run verify # 三个验证脚本:构造 / 双版本 typert 线上契约 / client apply 冒烟
|
|
91
107
|
dsh plugin --profile web add /path/to/dsh-archive-manager # 安装/重装到本机 DSH profile
|
|
92
108
|
```
|
|
93
109
|
|
|
110
|
+
`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` 环境变量指定。
|
|
111
|
+
|
|
94
112
|
详见 [AGENTS.md](AGENTS.md)——记录了 DSH 正式插件(Host/Client/Typert 三件套)的完整机制和踩坑。
|
|
95
113
|
|
|
96
114
|
## 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`.
|
|
5
|
-
*
|
|
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
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* group
|
|
14
|
-
*
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
/*
|
|
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
|
|
121
|
-
|
|
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:
|
|
199
|
+
codec: strictClientCodec("dsh-archive-manager#ArchiveManagerRestoreRequest"),
|
|
137
200
|
},
|
|
138
201
|
],
|
|
139
|
-
result:
|
|
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:
|
|
215
|
+
codec: strictClientCodec("dsh-archive-manager#ArchiveManagerDeleteRequest"),
|
|
153
216
|
},
|
|
154
217
|
],
|
|
155
|
-
result:
|
|
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:
|
|
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 =
|
|
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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duke-dsh-plugins/dsh-archive-manager",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Archive manager for DeepSeek Harness:
|
|
3
|
+
"version": "1.4.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"
|
|
@@ -43,6 +43,7 @@
|
|
|
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"
|
|
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 =
|
|
20
|
-
z.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
65
|
+
])
|
|
66
|
+
);
|
|
34
67
|
|
|
35
|
-
const deleteResultSchema =
|
|
36
|
-
z.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
50
|
-
|
|
84
|
+
])
|
|
85
|
+
);
|
|
51
86
|
|
|
52
|
-
const stateResultSchema =
|
|
53
|
-
z.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
|
|
102
|
+
])
|
|
103
|
+
);
|
|
67
104
|
|
|
68
105
|
// ---- per-invocation parameter/result schemas -------------------------------
|
|
69
106
|
|
|
70
|
-
const
|
|
71
|
-
|
|
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
|
-
|
|
75
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
},
|
|
153
|
+
codec: strictCodec(
|
|
154
|
+
"dsh-archive-manager#ArchiveManagerRestoreRequest",
|
|
155
|
+
restoreRequestSchema
|
|
156
|
+
),
|
|
99
157
|
},
|
|
100
158
|
],
|
|
101
|
-
result:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
},
|
|
176
|
+
codec: strictCodec(
|
|
177
|
+
"dsh-archive-manager#ArchiveManagerDeleteRequest",
|
|
178
|
+
deleteRequestSchema
|
|
179
|
+
),
|
|
124
180
|
},
|
|
125
181
|
],
|
|
126
|
-
result:
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
],
|