@chaoset/session-archive 0.1.2 → 0.2.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.
@@ -0,0 +1,501 @@
1
+ import * as React from "react";
2
+ import type { TypertRemoteContribution } from "@deepseek-ai/dsh-typert-protocol";
3
+
4
+ // ── 样式(注入 style 标签,复用 DSH 设计变量)────────────────────────
5
+ var css = [
6
+ ".sa_badge{width:100%;height:49px;color:var(--dsw-alias-label-primary);cursor:pointer;background:0 0;border:none;border-radius:12px;align-items:center;gap:8px;padding:0 8px 0 6px;font-family:inherit;font-size:14px;display:inline-flex;overflow:hidden}",
7
+ ".sa_badge:hover{background:var(--dsw-alias-interactive-bg-hover)}",
8
+ ".sa_badgeIcon{flex:none;display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;color:var(--dsw-alias-label-primary)}",
9
+ ".sa_badgeIcon svg{width:18px;height:18px;display:block}",
10
+ ".sa_badgeLabel{text-overflow:ellipsis;white-space:nowrap;min-width:0;overflow:hidden}",
11
+ ".sa_badgeCount{color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums;flex:none;margin-left:auto;font-size:12px;line-height:16px}",
12
+ ".sa_badge--collapsed{width:auto;justify-content:center;padding:0}",
13
+ ".sa_badge--collapsed .sa_badgeLabel,.sa_badge--collapsed .sa_badgeCount{display:none}",
14
+ ".sa_panel{z-index:30;border:1px solid var(--dsw-alias-border-l1);background:var(--dsw-alias-bg-base);width:440px;max-width:calc(100vw - 24px);max-height:62vh;box-shadow:var(--dsw-shadow-lv2);--dsh-scrollbar-thumb:var(--dsw-alias-scrollbar-bg-l2);--dsh-scrollbar-thumb-hover:var(--dsw-alias-scrollbar-hover-l2);border-radius:12px;flex-direction:column;display:flex;position:fixed;bottom:128px;left:12px;overflow:hidden}",
15
+ ".sa_header{box-sizing:border-box;border-bottom:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);flex:none;justify-content:space-between;align-items:center;min-height:44px;padding:8px 12px;display:flex}",
16
+ ".sa_title{color:var(--dsw-alias-label-primary);font-size:13px;font-weight:500;line-height:20px}",
17
+ ".sa_iconBtn{font:inherit;cursor:pointer;border:0;border-radius:8px;width:36px;height:36px;color:var(--dsw-alias-label-secondary,#666);background:0 0;display:inline-flex;align-items:center;justify-content:center;font-size:18px}",
18
+ ".sa_refresh{font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;min-height:36px;padding:5px 14px;font-size:13px;line-height:20px;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);display:inline-flex;align-items:center;gap:5px}",
19
+ ".sa_refresh:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}",
20
+ ".sa_refresh:disabled{opacity:.4;cursor:default}",
21
+ ".sa_iconBtn:hover{background:var(--dsw-alias-interactive-bg-hover)}",
22
+ ".sa_iconBtn:disabled{opacity:.4;cursor:default}",
23
+ ".sa_toolbar{flex:none;border-bottom:1px solid var(--dsw-alias-border-l2);align-items:center;gap:8px;padding:8px 12px;display:flex;flex-wrap:wrap}",
24
+ ".sa_check{accent-color:var(--dsw-alias-label-primary);width:14px;height:14px;flex:none;cursor:pointer}",
25
+ ".sa_check:disabled{cursor:default;opacity:.45}",
26
+ ".sa_toolLabel{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;user-select:none;cursor:pointer;display:inline-flex;align-items:center;gap:6px}",
27
+ ".sa_count{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;flex:1}",
28
+ ".sa_action{font:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l1);border-radius:6px;padding:3px 10px;font-size:12px;line-height:18px;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary)}",
29
+ ".sa_action:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}",
30
+ ".sa_action:disabled{opacity:.4;cursor:default}",
31
+ ".sa_actionDanger{border-color:transparent;background:var(--dsw-alias-state-error-primary);color:#fff}",
32
+ ".sa_actionDanger:hover:not(:disabled){background:var(--dsw-alias-state-error-primary)}",
33
+ ".sa_actionDanger:disabled{opacity:.4}",
34
+ ".sa_confirm{color:var(--dsw-alias-state-error-primary);border:1px solid var(--dsw-alias-state-error-primary);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 10%,transparent)}",
35
+ ".sa_confirm:hover:not(:disabled){background:var(--dsw-alias-state-error-primary);color:#fff}",
36
+ ".sa_body{flex:1;min-height:0;padding:4px 12px 12px;overflow-y:auto}",
37
+ ".sa_empty{color:var(--dsw-alias-label-tertiary);margin:24px 0;text-align:center;font-size:12px;line-height:18px}",
38
+ ".sa_error{color:var(--dsw-alias-state-error-primary);margin:8px 0;font-size:12px;line-height:18px}",
39
+ ".sa_rows{flex-direction:column;gap:8px;margin:0;padding:0;list-style:none;display:flex}",
40
+ ".sa_row{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);border-radius:12px;flex-direction:column;gap:6px;padding:8px 10px;display:flex}",
41
+ ".sa_rowHead{align-items:center;gap:8px;display:flex}",
42
+ ".sa_rowTitle{min-width:0;color:var(--dsw-alias-label-primary);text-overflow:ellipsis;white-space:nowrap;flex:1;font-size:13px;font-weight:500;line-height:20px;overflow:hidden;cursor:pointer}",
43
+ ".sa_rowTitle:hover{text-decoration:underline}",
44
+ ".sa_live{background:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-label);height:18px;border-radius:9px;flex:none;align-items:center;padding:0 6px;font-size:11px;line-height:18px;display:inline-flex}",
45
+ ".sa_rowMeta{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;overflow-wrap:anywhere}",
46
+ ".sa_rowMeta code{font-family:var(--dsh-font-mono,monospace)}",
47
+ ".sa_rowFoot{justify-content:space-between;align-items:center;gap:8px;display:flex}",
48
+ ".sa_rowActions{flex:none;align-items:center;gap:8px;display:flex}",
49
+ ".sa_detail{border-top:1px dashed var(--dsw-alias-border-l2);padding-top:8px;flex-direction:column;gap:6px;display:flex;max-height:260px;overflow-y:auto}",
50
+ ".sa_msg{flex-direction:column;gap:2px;display:flex}",
51
+ ".sa_msgRole{color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:14px;text-transform:uppercase;letter-spacing:.04em}",
52
+ ".sa_msgText{color:var(--dsw-alias-label-primary);white-space:pre-wrap;overflow-wrap:anywhere;font-size:12px;line-height:18px}",
53
+ ".sa_msgTextUser{color:var(--dsw-alias-label-secondary)}",
54
+ ".sa_busy{opacity:.55;pointer-events:none}"
55
+ ].join("");
56
+ var tagId = "@chaoset/session-archive/client.css";
57
+ if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=\"" + tagId + "\"]") === null) {
58
+ var tag = document.createElement("style");
59
+ tag.dataset.plugin = "@chaoset/session-archive";
60
+ tag.dataset.pluginCss = tagId;
61
+ tag.textContent = css;
62
+ document.head.appendChild(tag);
63
+ }
64
+
65
+ // ── 字典 ─────────────────────────────────────────────────────────────
66
+ const NS = "sidebar.sessionArchive";
67
+ const zh = {
68
+ badge: "归档",
69
+ panelTitle: "归档会话",
70
+ refresh: "刷新",
71
+ close: "关闭",
72
+ empty: "暂无归档会话。在会话列表的更多菜单中归档会话后会出现在这里。",
73
+ loadFailed: "加载归档列表失败",
74
+ selectAll: "全选",
75
+ selected: "已选 {n} 项",
76
+ restore: "恢复所选",
77
+ restoreDone: "已恢复 {n} 个归档会话",
78
+ restoreFailed: "恢复失败",
79
+ delete: "删除所选",
80
+ deleteConfirm: "再次点击确认删除",
81
+ deleteDone: "已删除 {n} 个归档会话",
82
+ deleteFailed: "部分删除失败:{n} 个",
83
+ confirmAll: "确认删除全部 {n} 个?",
84
+ noSelection: "请先勾选会话",
85
+ view: "查看",
86
+ collapse: "收起",
87
+ live: "运行中",
88
+ detailLoadFailed: "读取会话内容失败",
89
+ noneTitle: "(无标题)",
90
+ user: "用户",
91
+ assistant: "助手",
92
+ messages: "共 {n} 条消息",
93
+ noMessages: "(无文本消息)",
94
+ sizeBytes: "{n} B",
95
+ sizeKB: "{n} KB",
96
+ sizeMB: "{n} MB",
97
+ runningHint: "运行中的会话不能删除,请先停止"
98
+ };
99
+ const en = {
100
+ badge: "Archive",
101
+ panelTitle: "Archived sessions",
102
+ refresh: "Refresh",
103
+ close: "Close",
104
+ empty: "No archived sessions. Archive a session from its menu in the session list and it will show up here.",
105
+ loadFailed: "Failed to load archive list",
106
+ selectAll: "Select all",
107
+ selected: "{n} selected",
108
+ restore: "Restore",
109
+ restoreDone: "Restored {n} archived sessions",
110
+ restoreFailed: "Restore failed",
111
+ delete: "Delete",
112
+ deleteConfirm: "Click again to confirm delete",
113
+ deleteDone: "Deleted {n} archived sessions",
114
+ deleteFailed: "Some deletions failed: {n}",
115
+ confirmAll: "Delete all {n}?",
116
+ noSelection: "Select sessions first",
117
+ view: "View",
118
+ collapse: "Collapse",
119
+ live: "Running",
120
+ detailLoadFailed: "Failed to read session content",
121
+ noneTitle: "(no title)",
122
+ user: "User",
123
+ assistant: "Assistant",
124
+ messages: "{n} messages",
125
+ noMessages: "(no text messages)",
126
+ sizeBytes: "{n} B",
127
+ sizeKB: "{n} KB",
128
+ sizeMB: "{n} MB",
129
+ runningHint: "Running sessions cannot be deleted; stop them first"
130
+ };
131
+
132
+ // ── 工具函数 ─────────────────────────────────────────────────────────
133
+ function formatBytes(bytes: any, t: any) {
134
+ if (bytes < 1024) return t("sizeBytes").replace("{n}", String(bytes));
135
+ if (bytes < 1024 * 1024) return t("sizeKB").replace("{n}", (bytes / 1024).toFixed(1));
136
+ return t("sizeMB").replace("{n}", (bytes / (1024 * 1024)).toFixed(1));
137
+ }
138
+ function formatTime(ms: any) {
139
+ try { return new Date(ms).toLocaleString(); } catch { return String(ms); }
140
+ }
141
+ function shortId(id: any) {
142
+ return id.length > 12 ? id.slice(0, 12) + "…" : id;
143
+ }
144
+
145
+ // ── 归档面板 ─────────────────────────────────────────────────────────
146
+ function ArchivePanel(props: any) {
147
+ const t = props.t;
148
+ const call = props.call;
149
+ // 侧边栏收起(icon rail)时,DSH 通过 wide=false 告知(侧边栏 footer 注入点
150
+ // 下发的是 wide,而非 collapsed)。以 wide 为准;仅当宿主未下发 wide 时,才用
151
+ // ResizeObserver 观察所在格宽度(<80px 视为收起)兜底,避免窄格把文字标签挤变形。
152
+ const wideExplicit = typeof props.wide === "boolean" ? props.wide : void 0;
153
+ const [narrow, setNarrow] = React.useState(false);
154
+ const collapsed = wideExplicit === void 0 ? narrow : !wideExplicit;
155
+ const badgeRef = React.useRef<any>(null);
156
+ React.useEffect(() => {
157
+ const el = badgeRef.current?.parentElement;
158
+ if (el === undefined || el === null || typeof (globalThis as any).ResizeObserver === "undefined") return;
159
+ const ro = new (globalThis as any).ResizeObserver((entries: any[]) => {
160
+ for (const entry of entries) {
161
+ const w = entry.contentRect?.width ?? el.clientWidth;
162
+ setNarrow(w > 0 && w < 80);
163
+ }
164
+ });
165
+ ro.observe(el);
166
+ return () => ro.disconnect();
167
+ }, []);
168
+ // 有显式 wide 时完全以宿主状态为准,避免 ResizeObserver 的窄格判断在
169
+ // 展开后仍残留,导致展开态误用 collapsed 样式(图标不居中、文字丢失)。
170
+ const iconOnly = collapsed;
171
+ const rootRef = React.useRef<any>(null);
172
+ const [open, setOpen] = React.useState(false);
173
+ const [items, setItems] = React.useState<any[]>([]);
174
+ const [loading, setLoading] = React.useState(false);
175
+ const [error, setError] = React.useState<any>(null);
176
+ const [selected, setSelected] = React.useState<Set<any>>(new Set());
177
+ const [expanded, setExpanded] = React.useState<any>(null);
178
+ const [details, setDetails] = React.useState<Map<any, any>>(new Map());
179
+ const [detailLoading, setDetailLoading] = React.useState<Set<any>>(new Set());
180
+ const [busy, setBusy] = React.useState(false);
181
+ const [confirmingDelete, setConfirmingDelete] = React.useState(false);
182
+ const [notice, setNotice] = React.useState<any>(null);
183
+
184
+ const load = React.useCallback(async () => {
185
+ setLoading(true);
186
+ setError(null);
187
+ try {
188
+ const result = await call("list");
189
+ setItems(Array.isArray(result.items) ? result.items : []);
190
+ } catch (loadError: any) {
191
+ setError(t("loadFailed") + ": " + (loadError && loadError.message || loadError));
192
+ } finally {
193
+ setLoading(false);
194
+ }
195
+ }, [call, t]);
196
+
197
+ React.useEffect(() => {
198
+ if (open) { setNotice(null); load(); }
199
+ }, [open, load]);
200
+
201
+ // 后台静默刷新归档计数:侧边栏徽标要在「聊天列表里出现归档」时就同步显示数量,
202
+ // 而不是等到打开弹窗才更新。面板打开时由上面的 load() 负责,这里只在关闭态轮询,
203
+ // 避免刷新打断用户在面板里的勾选/操作;同时监听标签页重新可见。
204
+ const refreshSilently = React.useCallback(async () => {
205
+ try {
206
+ const result = await call("list");
207
+ setItems(Array.isArray(result.items) ? result.items : []);
208
+ } catch {}
209
+ }, [call]);
210
+ React.useEffect(() => {
211
+ if (open) return;
212
+ refreshSilently();
213
+ const id = globalThis.setInterval(refreshSilently, 5000);
214
+ const onVis = () => { if (document.visibilityState === "visible") refreshSilently(); };
215
+ document.addEventListener("visibilitychange", onVis);
216
+ return () => { clearInterval(id); document.removeEventListener("visibilitychange", onVis); };
217
+ }, [open, refreshSilently]);
218
+
219
+ // 提示(如「已恢复/已删除 N 个会话」)几秒后自动消失,避免残留误导用户,
220
+ // 也避免删除全部归档后仍挂着上一条提示。
221
+ React.useEffect(() => {
222
+ if (notice === null) return;
223
+ const id = globalThis.setTimeout(() => setNotice(null), 4000);
224
+ return () => clearTimeout(id);
225
+ }, [notice]);
226
+
227
+ React.useEffect(() => {
228
+ if (!open) return;
229
+ const onPointerDown = (event: any) => {
230
+ if (rootRef.current !== null && !rootRef.current.contains(event.target)) {
231
+ setOpen(false);
232
+ }
233
+ };
234
+ document.addEventListener("pointerdown", onPointerDown);
235
+ return () => document.removeEventListener("pointerdown", onPointerDown);
236
+ }, [open]);
237
+
238
+ const selectable = items.filter((item) => !item.live);
239
+ const allSelected = selectable.length > 0 && selectable.every((item) => selected.has(item.sessionId));
240
+
241
+ const toggleAll = (checked: any) => {
242
+ if (checked) {
243
+ setSelected(new Set(selectable.map((item) => item.sessionId)));
244
+ } else {
245
+ setSelected(new Set());
246
+ }
247
+ };
248
+ const toggleOne = (sessionId: any, checked: any) => {
249
+ setSelected((current) => {
250
+ const next = new Set(current);
251
+ if (checked) next.add(sessionId); else next.delete(sessionId);
252
+ return next;
253
+ });
254
+ };
255
+
256
+ const toggleDetail = (item: any) => {
257
+ if (expanded === item.sessionId) {
258
+ setExpanded(null);
259
+ return;
260
+ }
261
+ setExpanded(item.sessionId);
262
+ if (!details.has(item.sessionId)) {
263
+ setDetailLoading((current) => new Set(current).add(item.sessionId));
264
+ call("detail", item.sessionId).then((detail: any) => {
265
+ setDetails((current) => new Map(current).set(item.sessionId, detail));
266
+ }).catch((detailError: any) => {
267
+ setDetails((current) => new Map(current).set(item.sessionId, {
268
+ error: t("detailLoadFailed") + ": " + (detailError && detailError.message || detailError)
269
+ }));
270
+ }).finally(() => {
271
+ setDetailLoading((current) => {
272
+ const next = new Set(current);
273
+ next.delete(item.sessionId);
274
+ return next;
275
+ });
276
+ });
277
+ }
278
+ };
279
+
280
+ const runBatch = async (action: any, doneKey: any, failKey: any) => {
281
+ const ids = [...selected];
282
+ if (ids.length === 0) {
283
+ setNotice({ kind: "warn", text: t("noSelection") });
284
+ return;
285
+ }
286
+ setBusy(true);
287
+ setNotice(null);
288
+ try {
289
+ const result = await call(action, ids);
290
+ const doneIds = result.deleted || result.restored || [];
291
+ const n = doneIds.length;
292
+ if (result.failed && result.failed.length > 0) {
293
+ setNotice({ kind: "error", text: t(failKey).replace("{n}", String(result.failed.length)) });
294
+ } else {
295
+ setNotice({ kind: "ok", text: t(doneKey).replace("{n}", String(n)) });
296
+ }
297
+ if (doneIds.length > 0) {
298
+ const done = new Set(doneIds);
299
+ setItems((current) => current.filter((item) => !done.has(item.sessionId)));
300
+ }
301
+ setSelected(new Set());
302
+ setConfirmingDelete(false);
303
+ await load();
304
+ } catch (actionError: any) {
305
+ setNotice({ kind: "error", text: t(failKey).replace("{n}", "?") + ": " + (actionError && actionError.message || actionError) });
306
+ } finally {
307
+ setBusy(false);
308
+ }
309
+ };
310
+ const restoreSelected = () => runBatch("unarchive", "restoreDone", "restoreFailed");
311
+ const deleteSelected = () => {
312
+ if (!confirmingDelete) {
313
+ setConfirmingDelete(true);
314
+ window.setTimeout(() => setConfirmingDelete(false), 4000);
315
+ return;
316
+ }
317
+ runBatch("delete", "deleteDone", "deleteFailed");
318
+ };
319
+
320
+ const hasSelection = selected.size > 0;
321
+
322
+ return React.createElement(
323
+ "div",
324
+ { className: "sa_root", ref: rootRef },
325
+ React.createElement(
326
+ "button",
327
+ {
328
+ ref: badgeRef,
329
+ className: "sa_badge" + (iconOnly ? " sa_badge--collapsed" : ""),
330
+ type: "button",
331
+ onClick: () => setOpen(!open),
332
+ "aria-expanded": open,
333
+ title: t("badge")
334
+ },
335
+ React.createElement("span", { className: "sa_badgeIcon", "aria-hidden": true },
336
+ React.createElement("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" },
337
+ React.createElement("rect", { x: 3, y: 4, width: 18, height: 4, rx: 1 }),
338
+ React.createElement("path", { d: "M5 8v11a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8" }),
339
+ React.createElement("path", { d: "M10 12h4" })
340
+ )
341
+ ),
342
+ iconOnly ? null : React.createElement("span", { className: "sa_badgeLabel" }, t("badge")),
343
+ iconOnly ? null : React.createElement("span", { className: "sa_badgeCount" }, String(items.length))
344
+ ),
345
+ open ? React.createElement(
346
+ "div",
347
+ { className: "sa_panel" },
348
+ React.createElement(
349
+ "div",
350
+ { className: "sa_header" },
351
+ React.createElement("span", { className: "sa_title" }, t("panelTitle")),
352
+ React.createElement(
353
+ "span",
354
+ { style: { display: "inline-flex", gap: "6px", alignItems: "center" } },
355
+ React.createElement("button", { className: "sa_refresh", type: "button", title: t("refresh"), disabled: busy || loading, onClick: load }, t("refresh")),
356
+ React.createElement("button", { className: "sa_iconBtn", type: "button", title: t("close"), disabled: busy, onClick: () => setOpen(false) }, "✕")
357
+ )
358
+ ),
359
+ React.createElement(
360
+ "div",
361
+ { className: "sa_toolbar" },
362
+ React.createElement("label", { className: "sa_toolLabel" },
363
+ React.createElement("input", {
364
+ className: "sa_check",
365
+ type: "checkbox",
366
+ checked: allSelected,
367
+ disabled: busy || selectable.length === 0,
368
+ onChange: (e) => toggleAll(e.target.checked)
369
+ }),
370
+ t("selectAll")
371
+ ),
372
+ React.createElement("span", { className: "sa_count" }, t("selected").replace("{n}", String(selected.size))),
373
+ React.createElement("button", {
374
+ className: "sa_action",
375
+ type: "button",
376
+ disabled: busy || !hasSelection,
377
+ onClick: restoreSelected
378
+ }, t("restore")),
379
+ React.createElement("button", {
380
+ className: "sa_action " + (confirmingDelete ? "sa_actionDanger sa_confirm" : "sa_actionDanger"),
381
+ type: "button",
382
+ disabled: busy || !hasSelection,
383
+ onClick: deleteSelected
384
+ }, confirmingDelete ? t("deleteConfirm") : t("delete"))
385
+ ),
386
+ React.createElement(
387
+ "div",
388
+ { className: "sa_body" },
389
+ notice !== null ? React.createElement("p", { className: "sa_error" + (notice.kind === "ok" ? "" : ""), role: "status" }, notice.text) : null,
390
+ error !== null ? React.createElement("p", { className: "sa_error", role: "alert" }, error) : null,
391
+ loading && items.length === 0 ? React.createElement("p", { className: "sa_empty" }, "…") : null,
392
+ !loading && items.length === 0 && error === null ? React.createElement("p", { className: "sa_empty" }, t("empty")) : null,
393
+ items.length > 0 ? React.createElement(
394
+ "ul",
395
+ { className: "sa_rows" },
396
+ items.map((item) => {
397
+ const isExpanded = expanded === item.sessionId;
398
+ const detail = details.get(item.sessionId);
399
+ const detailPending = detailLoading.has(item.sessionId);
400
+ return React.createElement(
401
+ "li",
402
+ { className: "sa_row" + (busy ? " sa_busy" : ""), key: item.sessionId },
403
+ React.createElement(
404
+ "div",
405
+ { className: "sa_rowHead" },
406
+ React.createElement("input", {
407
+ className: "sa_check",
408
+ type: "checkbox",
409
+ checked: selected.has(item.sessionId),
410
+ disabled: busy || item.live,
411
+ title: item.live ? t("runningHint") : void 0,
412
+ onChange: (e: any) => toggleOne(item.sessionId, e.target.checked)
413
+ }),
414
+ React.createElement(
415
+ "span",
416
+ { className: "sa_rowTitle", onClick: () => toggleDetail(item), title: t("view") },
417
+ item.title !== null && item.title !== void 0 && item.title !== "" ? item.title : t("noneTitle")
418
+ ),
419
+ item.live ? React.createElement("span", { className: "sa_live" }, t("live")) : null
420
+ ),
421
+ React.createElement("div", { className: "sa_rowMeta" },
422
+ React.createElement("span", null, formatTime(item.updatedAt)),
423
+ item.cwd !== null && item.cwd !== void 0 ? React.createElement("span", null, " · ", React.createElement("code", null, item.cwd)) : null,
424
+ React.createElement("span", null, " · ", formatBytes(item.size, t))
425
+ ),
426
+ React.createElement(
427
+ "div",
428
+ { className: "sa_rowFoot" },
429
+ React.createElement("span", { className: "sa_rowMeta" },
430
+ React.createElement("code", null, shortId(item.sessionId)),
431
+ detail !== void 0 && !detail.error && detail.messageCount !== void 0
432
+ ? " · " + t("messages").replace("{n}", String(detail.messageCount))
433
+ : null
434
+ ),
435
+ React.createElement(
436
+ "div",
437
+ { className: "sa_rowActions" },
438
+ React.createElement("button", {
439
+ className: "sa_action",
440
+ type: "button",
441
+ disabled: busy || detailPending,
442
+ onClick: () => toggleDetail(item)
443
+ }, isExpanded ? t("collapse") : t("view"))
444
+ )
445
+ ),
446
+ isExpanded ? React.createElement(
447
+ "div",
448
+ { className: "sa_detail" },
449
+ detailPending ? React.createElement("p", { className: "sa_empty" }, "…") : null,
450
+ detail === void 0 ? null :
451
+ detail.error !== void 0 ? React.createElement("p", { className: "sa_error" }, detail.error) :
452
+ detail.messages.length === 0 ? React.createElement("p", { className: "sa_empty" }, t("noMessages")) :
453
+ detail.messages.map((message: any, index: any) => React.createElement(
454
+ "div",
455
+ { className: "sa_msg", key: index },
456
+ React.createElement("span", { className: "sa_msgRole" }, message.role === "user" ? t("user") : t("assistant") + " · " + formatTime(message.time)),
457
+ React.createElement("span", { className: "sa_msgText" + (message.role === "user" ? " sa_msgTextUser" : "") }, message.text)
458
+ ))
459
+ ) : null
460
+ );
461
+ })
462
+ ) : null
463
+ )
464
+ ) : null
465
+ );
466
+ }
467
+
468
+ // ── 插件 apply ───────────────────────────────────────────────────────
469
+ // DSH 客户端的 remote.<ns> 服务不会自动生成:必须由客户端代码用
470
+ // ctx.remote.$mount(contribution) 显式挂载(官方 dsh-api-remotes 即如此)。
471
+ const inject = ["slots", "locale", "remote"];
472
+ const passthroughSchema = { parse: (value: any) => value };
473
+ const REMOTE_CONTRIBUTION: TypertRemoteContribution = {
474
+ package: "@chaoset/session-archive",
475
+ descriptors: [
476
+ { id: "@chaoset/session-archive#sessionArchive/list", service: "sessionArchive", namespace: "sessionArchive", method: "list", invocation: { kind: "direct" }, parameters: [], result: { mode: "strict", typeSymbol: "sessionArchive/list:result", schema: passthroughSchema } },
477
+ { id: "@chaoset/session-archive#sessionArchive/detail", service: "sessionArchive", namespace: "sessionArchive", method: "detail", invocation: { kind: "direct" }, parameters: [{ name: "sessionId", wire: "sessionId", source: "json", codec: { mode: "strict", typeSymbol: "sessionArchive/detail:sessionId", schema: passthroughSchema } }], result: { mode: "strict", typeSymbol: "sessionArchive/detail:result", schema: passthroughSchema } },
478
+ { id: "@chaoset/session-archive#sessionArchive/delete", service: "sessionArchive", namespace: "sessionArchive", method: "delete", invocation: { kind: "direct" }, parameters: [{ name: "sessionIds", wire: "sessionIds", source: "json", codec: { mode: "strict", typeSymbol: "sessionArchive/delete:sessionIds", schema: passthroughSchema } }], result: { mode: "strict", typeSymbol: "sessionArchive/delete:result", schema: passthroughSchema } },
479
+ { id: "@chaoset/session-archive#sessionArchive/unarchive", service: "sessionArchive", namespace: "sessionArchive", method: "unarchive", invocation: { kind: "direct" }, parameters: [{ name: "sessionIds", wire: "sessionIds", source: "json", codec: { mode: "strict", typeSymbol: "sessionArchive/unarchive:sessionIds", schema: passthroughSchema } }], result: { mode: "strict", typeSymbol: "sessionArchive/unarchive:result", schema: passthroughSchema } }
480
+ ]
481
+ };
482
+ async function apply(ctx: any) {
483
+ const t = ctx.locale.bind(NS);
484
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), "session-archive: dictionaries");
485
+ await ctx.remote.$mount(REMOTE_CONTRIBUTION);
486
+ const archiveService = ctx.get("remote.sessionArchive");
487
+ if (archiveService === void 0) throw new Error("session-archive: remote.sessionArchive did not materialize after mount");
488
+ const call = (method: any, ...args: any[]) => archiveService[method](...args).then((result: any) => {
489
+ if (!result.ok) throw new Error(method + " failed: " + result.error.code + ": " + result.error.message);
490
+ return result.value;
491
+ });
492
+ ctx.slots.inject("sidebar.footer.action", () => ctx.slots.register({
493
+ name: "sidebar.footer.action",
494
+ id: "session-archive",
495
+ order: 20,
496
+ locale: NS,
497
+ inject: () => ({ call })
498
+ }, ArchivePanel));
499
+ }
500
+
501
+ export { apply, inject };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * config-store — 插件配置持久化
3
+ *
4
+ * 配置文件位于 $DSH_HOME/plugins/<name>/config.json(默认 ~/.dsh,与
5
+ * file:// 部署模式共用)。生效顺序(后者覆盖前者):
6
+ * 1. 插件内置默认值(DEFAULT_CONFIG)
7
+ * 2. cordis.patch.yml 传入的 config(安装时生成的默认块)
8
+ * 3. config.json(设置页 UI 保存,权威)
9
+ *
10
+ * 保存后通过 onUpdate 回调立即热更新运行中的配置(包装闭包读共享 state)。
11
+ * 写入采用随机临时文件 + fsync + rename,权限 0600;损坏 JSON 只告警并回退。
12
+ */
13
+ export interface ConfigStoreOptions {
14
+ name: string;
15
+ defaults: Record<string, any>;
16
+ patchConfig: Record<string, any>;
17
+ onUpdate?: (merged: Record<string, any>, next: Record<string, any>) => void;
18
+ validate?: (partial: Record<string, any>) => void;
19
+ warn?: (message: string) => void;
20
+ }
21
+ export interface ConfigStore {
22
+ file: string;
23
+ effective(): Record<string, any>;
24
+ set(partial: Record<string, any>): Record<string, any>;
25
+ }
26
+ /**
27
+ * 创建配置存储。
28
+ * @param options - { name, defaults, patchConfig, onUpdate,
29
+ * validate?: (partial) => void, warn?: (message) => void }
30
+ */
31
+ export declare function createConfigStore(options: ConfigStoreOptions): ConfigStore;
32
+ //# sourceMappingURL=config-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-store.d.ts","sourceRoot":"","sources":["../src/config-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAOH,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACjC,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,IAAI,CAAC;IAC5E,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,IAAI,CAAC;IAClD,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,IAAI,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACjC,GAAG,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CACxD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAyE1E"}
@@ -0,0 +1,107 @@
1
+ /**
2
+ * config-store — 插件配置持久化
3
+ *
4
+ * 配置文件位于 $DSH_HOME/plugins/<name>/config.json(默认 ~/.dsh,与
5
+ * file:// 部署模式共用)。生效顺序(后者覆盖前者):
6
+ * 1. 插件内置默认值(DEFAULT_CONFIG)
7
+ * 2. cordis.patch.yml 传入的 config(安装时生成的默认块)
8
+ * 3. config.json(设置页 UI 保存,权威)
9
+ *
10
+ * 保存后通过 onUpdate 回调立即热更新运行中的配置(包装闭包读共享 state)。
11
+ * 写入采用随机临时文件 + fsync + rename,权限 0600;损坏 JSON 只告警并回退。
12
+ */
13
+ import { randomUUID } from "node:crypto";
14
+ import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
15
+ import { dirname, join, resolve } from "node:path";
16
+ import { homedir } from "node:os";
17
+ /**
18
+ * 创建配置存储。
19
+ * @param options - { name, defaults, patchConfig, onUpdate,
20
+ * validate?: (partial) => void, warn?: (message) => void }
21
+ */
22
+ export function createConfigStore(options) {
23
+ // 与 harness 的 DSH_HOME 约定保持一致:默认 ~/.dsh,可用 $DSH_HOME 覆盖。
24
+ const dshHome = process.env.DSH_HOME?.trim() ? resolve(process.env.DSH_HOME) : join(homedir(), ".dsh");
25
+ const file = join(dshHome, "plugins", options.name, "config.json");
26
+ const warn = options.warn ?? (() => { });
27
+ let readWarningShown = false;
28
+ function readJson() {
29
+ try {
30
+ if (!existsSync(file))
31
+ return {};
32
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
33
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
34
+ readWarningShown = false;
35
+ return parsed;
36
+ }
37
+ if (!readWarningShown) {
38
+ readWarningShown = true;
39
+ warn(`config file ${file} must contain a JSON object; using empty config`);
40
+ }
41
+ return {};
42
+ }
43
+ catch (error) {
44
+ if (!readWarningShown) {
45
+ readWarningShown = true;
46
+ warn(`failed to read config file ${file}: ${error?.message ?? String(error)}; using empty config`);
47
+ }
48
+ return {};
49
+ }
50
+ }
51
+ /** 当前生效配置(默认 + patch + json 合并)。 */
52
+ function effective() {
53
+ return { ...options.defaults, ...options.patchConfig, ...readJson() };
54
+ }
55
+ /** 保存部分配置到 config.json 并触发热更新,返回新的生效配置。 */
56
+ function set(partial) {
57
+ if (partial === null || typeof partial !== "object" || Array.isArray(partial)) {
58
+ throw new TypeError("set expects a plain config object");
59
+ }
60
+ options.validate?.(partial);
61
+ const merged = { ...readJson(), ...partial };
62
+ mkdirSync(dirname(file), { recursive: true });
63
+ const tmp = `${file}.${process.pid}.${randomUUID()}.tmp`;
64
+ let fd;
65
+ try {
66
+ fd = openSync(tmp, "w", 0o600);
67
+ writeFileSync(fd, JSON.stringify(merged, null, 2) + "\n", "utf8");
68
+ fsyncSync(fd);
69
+ closeSync(fd);
70
+ fd = void 0;
71
+ renameSync(tmp, file);
72
+ // POSIX 目录项持久化(Windows 上目录句柄不可打开时忽略)。
73
+ try {
74
+ const dir = openSync(dirname(file), "r");
75
+ try {
76
+ fsyncSync(dir);
77
+ }
78
+ finally {
79
+ closeSync(dir);
80
+ }
81
+ }
82
+ catch { }
83
+ }
84
+ catch (error) {
85
+ if (fd !== void 0)
86
+ try {
87
+ closeSync(fd);
88
+ }
89
+ catch { }
90
+ try {
91
+ unlinkSync(tmp);
92
+ }
93
+ catch { }
94
+ throw error;
95
+ }
96
+ const next = { ...options.defaults, ...options.patchConfig, ...merged };
97
+ try {
98
+ options.onUpdate?.(merged, next);
99
+ }
100
+ catch (error) {
101
+ warn(`onUpdate failed after config save: ${error?.message ?? String(error)}`);
102
+ }
103
+ return next;
104
+ }
105
+ return { file, effective, set };
106
+ }
107
+ //# sourceMappingURL=config-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-store.js","sourceRoot":"","sources":["../src/config-store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,QAAQ,EAAE,YAAY,EAAE,UAAU,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACrI,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAiBlC;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,OAA2B;IAC3D,yDAAyD;IACzD,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,MAAM,CAAC,CAAC;IACvG,MAAM,IAAI,GAAG,IAAI,CAAC,OAAO,EAAE,SAAS,EAAE,OAAO,CAAC,IAAI,EAAE,aAAa,CAAC,CAAC;IACnE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;IACxC,IAAI,gBAAgB,GAAG,KAAK,CAAC;IAE7B,SAAS,QAAQ;QACf,IAAI,CAAC;YACH,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC;gBAAE,OAAO,EAAE,CAAC;YACjC,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC;YACtD,IAAI,MAAM,KAAK,IAAI,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC;gBAC5E,gBAAgB,GAAG,KAAK,CAAC;gBACzB,OAAO,MAAM,CAAC;YAChB,CAAC;YACD,IAAI,CAAC,gBAAgB,EAAE,CAAC;gBACtB,gBAAgB,GAAG,IAAI,CAAC;gBACxB,IAAI,CAAC,eAAe,IAAI,iDAAiD,CAAC,CAAC;YAC7E,CAAC;YACD,OAAO,EAAE,CAAC;QACZ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,gBAAgB,EAAE,CAAC;gBACtB,gBAAgB,GAAG,IAAI,CAAC;gBACxB,IAAI,CAAC,8BAA8B,IAAI,KAAM,KAAe,EAAE,OAAO,IAAI,MAAM,CAAC,KAAK,CAAC,sBAAsB,CAAC,CAAC;YAChH,CAAC;YACD,OAAO,EAAE,CAAC;QACZ,CAAC;IACH,CAAC;IAED,oCAAoC;IACpC,SAAS,SAAS;QAChB,OAAO,EAAE,GAAG,OAAO,CAAC,QAAQ,EAAE,GAAG,OAAO,CAAC,WAAW,EAAE,GAAG,QAAQ,EAAE,EAAE,CAAC;IACxE,CAAC;IAED,2CAA2C;IAC3C,SAAS,GAAG,CAAC,OAA4B;QACvC,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,OAAO,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC;YAC9E,MAAM,IAAI,SAAS,CAAC,mCAAmC,CAAC,CAAC;QAC3D,CAAC;QACD,OAAO,CAAC,QAAQ,EAAE,CAAC,OAAO,CAAC,CAAC;QAC5B,MAAM,MAAM,GAAG,EAAE,GAAG,QAAQ,EAAE,EAAE,GAAG,OAAO,EAAE,CAAC;QAC7C,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QAE9C,MAAM,GAAG,GAAG,GAAG,IAAI,IAAI,OAAO,CAAC,GAAG,IAAI,UAAU,EAAE,MAAM,CAAC;QACzD,IAAI,EAAsB,CAAC;QAC3B,IAAI,CAAC;YACH,EAAE,GAAG,QAAQ,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;YAC/B,aAAa,CAAC,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,CAAC;YAClE,SAAS,CAAC,EAAE,CAAC,CAAC;YACd,SAAS,CAAC,EAAE,CAAC,CAAC;YACd,EAAE,GAAG,KAAK,CAAC,CAAC;YACZ,UAAU,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;YACtB,sCAAsC;YACtC,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,QAAQ,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC;gBACzC,IAAI,CAAC;oBAAC,SAAS,CAAC,GAAG,CAAC,CAAC;gBAAC,CAAC;wBAAS,CAAC;oBAAC,SAAS,CAAC,GAAG,CAAC,CAAC;gBAAC,CAAC;YACrD,CAAC;YAAC,MAAM,CAAC,CAAA,CAAC;QACZ,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,EAAE,KAAK,KAAK,CAAC;gBAAE,IAAI,CAAC;oBAAC,SAAS,CAAC,EAAE,CAAC,CAAC;gBAAC,CAAC;gBAAC,MAAM,CAAC,CAAA,CAAC;YAClD,IAAI,CAAC;gBAAC,UAAU,CAAC,GAAG,CAAC,CAAC;YAAC,CAAC;YAAC,MAAM,CAAC,CAAA,CAAC;YACjC,MAAM,KAAK,CAAC;QACd,CAAC;QAED,MAAM,IAAI,GAAG,EAAE,GAAG,OAAO,CAAC,QAAQ,EAAE,GAAG,OAAO,CAAC,WAAW,EAAE,GAAG,MAAM,EAAE,CAAC;QACxE,IAAI,CAAC;YACH,OAAO,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACnC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,sCAAuC,KAAe,EAAE,OAAO,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC3F,CAAC;QACD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC;AAClC,CAAC"}