@morlay/ui-conversation-manager 0.0.2-alpha.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,1257 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@morlay/ui-conversation-manager",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let react = require("react");
8
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
+ let _morlay_dsh_client_ui_primitives_client = require("@morlay/dsh-client-ui-primitives/client");
10
+ let react_jsx_runtime = require("react/jsx-runtime");
11
+ //#region src/client/controller.ts
12
+ /** host 侧已存在的会话路由。 */
13
+ const SESSION_DELETE_PATH = "/api/session.delete";
14
+ const SESSION_IMPORT_PATH = "/api/session.import";
15
+ const SESSION_EXPORT_PATH = "/api/session.export";
16
+ const SESSION_GC_PATH = "/api/session.gc";
17
+ const SESSION_USAGE_PATH = "/api/session.usage";
18
+ /** 带 host 错误码的请求失败:页面据此选本地化文案。 */
19
+ var ConversationManagerRequestError = class extends Error {
20
+ code;
21
+ constructor(message, code) {
22
+ super(message);
23
+ this.code = code;
24
+ this.name = "ConversationManagerRequestError";
25
+ }
26
+ };
27
+ async function postJson(path, body) {
28
+ const response = await fetch(path, {
29
+ method: "POST",
30
+ headers: {
31
+ accept: "application/json",
32
+ "content-type": "application/json"
33
+ },
34
+ body: JSON.stringify(body)
35
+ });
36
+ const value = await response.json().catch(() => ({}));
37
+ if (!response.ok) throw new ConversationManagerRequestError(typeof value.error === "string" ? value.error : `请求失败:HTTP ${response.status}`, typeof value.code === "string" ? value.code : void 0);
38
+ return value;
39
+ }
40
+ async function zipBase64(file) {
41
+ const dataUrl = await new Promise((resolve, reject) => {
42
+ const reader = new FileReader();
43
+ reader.onload = () => {
44
+ resolve(typeof reader.result === "string" ? reader.result : "");
45
+ };
46
+ reader.onerror = () => {
47
+ reject(reader.error ?? /* @__PURE__ */ new Error("failed to read the selected file"));
48
+ };
49
+ reader.readAsDataURL(file);
50
+ });
51
+ const comma = dataUrl.indexOf(",");
52
+ return comma < 0 ? dataUrl : dataUrl.slice(comma + 1);
53
+ }
54
+ /** 页面的动作:host 交互收在这里,页面只见数据与回调。 */
55
+ var ConversationManagerController = class {
56
+ ports;
57
+ face;
58
+ constructor(ports) {
59
+ this.ports = ports;
60
+ this.face = {
61
+ archive: (sessionId) => this.ports.archiveSession(sessionId),
62
+ unarchive: (sessionId) => this.ports.unarchiveSession(sessionId),
63
+ remove: (sessionId) => this.remove(sessionId),
64
+ exportZip: (sessionId) => this.exportZip(sessionId),
65
+ importZip: (file) => this.importZip(file),
66
+ collectGarbage: () => this.collectGarbage(),
67
+ loadUsage: (range) => this.loadUsage(range)
68
+ };
69
+ }
70
+ async remove(sessionId) {
71
+ await postJson(SESSION_DELETE_PATH, { sessionId });
72
+ await this.ports.refresh();
73
+ }
74
+ async exportZip(sessionId) {
75
+ const response = await fetch(SESSION_EXPORT_PATH, {
76
+ method: "POST",
77
+ headers: {
78
+ accept: "application/zip",
79
+ "content-type": "application/json"
80
+ },
81
+ body: JSON.stringify({ sessionId })
82
+ });
83
+ if (!response.ok) {
84
+ const value = await response.json().catch(() => ({}));
85
+ throw new ConversationManagerRequestError(typeof value.error === "string" ? value.error : `请求失败:HTTP ${response.status}`, typeof value.code === "string" ? value.code : void 0);
86
+ }
87
+ downloadBlob(await response.blob(), filenameOf(response.headers.get("content-disposition"), String(sessionId)));
88
+ }
89
+ async importZip(file) {
90
+ const zip = await zipBase64(file);
91
+ const value = await postJson(SESSION_IMPORT_PATH, { zip });
92
+ await this.ports.refresh();
93
+ return value["sessionId"];
94
+ }
95
+ async collectGarbage() {
96
+ const value = await postJson(SESSION_GC_PATH, {});
97
+ await this.ports.refresh();
98
+ return {
99
+ orphanSessions: typeof value["orphanSessions"] === "number" ? value["orphanSessions"] : 0,
100
+ orphanEvents: typeof value["orphanEvents"] === "number" ? value["orphanEvents"] : 0,
101
+ stoppedAgents: typeof value["stoppedAgents"] === "number" ? value["stoppedAgents"] : 0
102
+ };
103
+ }
104
+ /**
105
+ * 用量统计:host 侧聚合,前端各维度本地折叠。
106
+ * @param range - 时间范围语义键(`all` 不限、`day`/`week` 自然日/周、其余最近 N 天)。
107
+ */
108
+ async loadUsage(range) {
109
+ const report = await postJson(SESSION_USAGE_PATH, { range });
110
+ if (report.totals === void 0 || !Array.isArray(report.buckets) || !Array.isArray(report.sessions)) throw new ConversationManagerRequestError("用量统计响应不可用", void 0);
111
+ return report;
112
+ }
113
+ };
114
+ /** 导出文件名优先取 host 给的 Content-Disposition。 */
115
+ function filenameOf(disposition, sessionId) {
116
+ return (disposition === null ? null : /filename="([^"]+)"/u.exec(disposition))?.[1] ?? `${sessionId}.zip`;
117
+ }
118
+ /** 浏览器下载:blob URL + 一次性 anchor;URL 在下一轮事件循环回收。 */
119
+ function downloadBlob(blob, filename) {
120
+ const url = URL.createObjectURL(blob);
121
+ const anchor = document.createElement("a");
122
+ anchor.href = url;
123
+ anchor.download = filename;
124
+ anchor.rel = "noopener";
125
+ document.body.append(anchor);
126
+ anchor.click();
127
+ anchor.remove();
128
+ window.setTimeout(() => {
129
+ URL.revokeObjectURL(url);
130
+ }, 0);
131
+ }
132
+ //#endregion
133
+ //#region src/client/format.ts
134
+ /** 一位小数;三位数以上不留小数。 */
135
+ function trim(value) {
136
+ return value >= 100 ? String(Math.round(value)) : value.toFixed(1).replace(/\.0$/u, "");
137
+ }
138
+ /** @param value - token 数(非负整数)。 @returns 紧凑文本,例如 `1.2K`、`44.2B`。 */
139
+ function formatTokens(value) {
140
+ if (!Number.isFinite(value) || value <= 0) return "0";
141
+ if (value < 1e3) return String(Math.round(value));
142
+ if (value < 1e6) return `${trim(value / 1e3)}K`;
143
+ if (value < 1e9) return `${trim(value / 1e6)}M`;
144
+ return `${trim(value / 1e9)}B`;
145
+ }
146
+ /** @param value - 百分点(0~100)。 @returns 紧凑百分比,例如 `98.4%`、`100%`。 */
147
+ function formatPercent(value) {
148
+ if (!Number.isFinite(value) || value <= 0) return "0%";
149
+ return `${value >= 100 ? Math.round(value) : Math.round(value * 10) / 10}%`;
150
+ }
151
+ /** 「对话管理」页面的样式表(官方 --dsw-* 变量 + 我们的 css-in-js 层)。
152
+ * 页面根是纵向 flex + 整页滚动:除列表外的项都 `flex: none`,否则内容变长时
153
+ * flex 会把这些项压到 min-content(官方 Input 的 32px 高会被压成一行文字高)。 */
154
+ const styles = {
155
+ page: {
156
+ display: "flex",
157
+ flexDirection: "column",
158
+ gap: "12px",
159
+ boxSizing: "border-box",
160
+ width: "100%",
161
+ height: "100%",
162
+ padding: "24px 28px",
163
+ overflow: "auto",
164
+ color: "var(--dsw-alias-label-primary)"
165
+ },
166
+ header: {
167
+ display: "flex",
168
+ alignItems: "center",
169
+ justifyContent: "space-between",
170
+ gap: "12px",
171
+ flex: "none"
172
+ },
173
+ headerActions: {
174
+ display: "flex",
175
+ alignItems: "center",
176
+ gap: "8px",
177
+ flex: "none"
178
+ },
179
+ title: {
180
+ margin: "0",
181
+ fontSize: "16px",
182
+ lineHeight: "24px",
183
+ fontWeight: "500"
184
+ },
185
+ tabs: {
186
+ position: "relative",
187
+ zIndex: "1",
188
+ display: "flex",
189
+ gap: "36px",
190
+ marginTop: "10px",
191
+ paddingLeft: "8px",
192
+ flex: "none"
193
+ },
194
+ tab: {
195
+ position: "relative",
196
+ padding: "0 0 9px",
197
+ border: "none",
198
+ background: "transparent",
199
+ fontSize: "13px",
200
+ lineHeight: "16px",
201
+ fontWeight: "500",
202
+ color: "var(--dsw-alias-label-tertiary)",
203
+ cursor: "pointer",
204
+ "&::after": {
205
+ content: "''",
206
+ position: "absolute",
207
+ right: "0",
208
+ bottom: "-1px",
209
+ left: "0",
210
+ height: "2px",
211
+ borderRadius: "2px",
212
+ background: "transparent"
213
+ }
214
+ },
215
+ tabActive: {
216
+ color: "var(--dsw-alias-state-business-primary)",
217
+ "&::after": { background: "var(--dsw-alias-state-business-primary)" }
218
+ },
219
+ usage: {
220
+ display: "flex",
221
+ flexDirection: "column",
222
+ gap: "12px",
223
+ flex: "none"
224
+ },
225
+ usageRow: {
226
+ display: "flex",
227
+ flexDirection: "column",
228
+ gap: "6px",
229
+ padding: "10px 12px",
230
+ border: "1px solid var(--dsw-alias-border-l1)",
231
+ borderRadius: "10px",
232
+ background: "var(--dsw-alias-bg-base)",
233
+ flex: "none"
234
+ },
235
+ usageRowLabel: {
236
+ overflow: "hidden",
237
+ fontSize: "12px",
238
+ lineHeight: "18px",
239
+ color: "var(--dsw-alias-label-tertiary)",
240
+ textOverflow: "ellipsis",
241
+ whiteSpace: "nowrap"
242
+ },
243
+ usageMetrics: {
244
+ display: "flex",
245
+ flexDirection: "row",
246
+ flexWrap: "wrap",
247
+ gap: "6px 20px"
248
+ },
249
+ usageMetric: {
250
+ display: "flex",
251
+ flexDirection: "column",
252
+ gap: "2px"
253
+ },
254
+ usageMetricLabel: {
255
+ fontSize: "12px",
256
+ lineHeight: "18px",
257
+ color: "var(--dsw-alias-label-tertiary)"
258
+ },
259
+ usageMetricValue: {
260
+ fontSize: "16px",
261
+ lineHeight: "24px",
262
+ fontWeight: "500",
263
+ fontVariantNumeric: "tabular-nums"
264
+ },
265
+ usageList: {
266
+ display: "flex",
267
+ flexDirection: "column",
268
+ gap: "4px",
269
+ margin: "0",
270
+ padding: "0",
271
+ listStyle: "none",
272
+ flex: "none"
273
+ },
274
+ filters: {
275
+ display: "flex",
276
+ alignItems: "center",
277
+ gap: "16px",
278
+ flex: "none"
279
+ },
280
+ search: {
281
+ width: "360px",
282
+ maxWidth: "100%",
283
+ flex: "none"
284
+ },
285
+ list: {
286
+ display: "flex",
287
+ flexDirection: "column",
288
+ gap: "4px",
289
+ margin: "0",
290
+ padding: "0",
291
+ listStyle: "none",
292
+ flex: "none"
293
+ },
294
+ row: {
295
+ display: "flex",
296
+ alignItems: "center",
297
+ justifyContent: "space-between",
298
+ gap: "16px",
299
+ padding: "10px 12px",
300
+ border: "1px solid var(--dsw-alias-border-l1)",
301
+ borderRadius: "10px",
302
+ background: "var(--dsw-alias-bg-base)",
303
+ flex: "none"
304
+ },
305
+ identity: {
306
+ display: "flex",
307
+ flexDirection: "column",
308
+ gap: "2px",
309
+ minWidth: "0"
310
+ },
311
+ titleLine: {
312
+ display: "flex",
313
+ alignItems: "center",
314
+ gap: "8px",
315
+ minWidth: "0"
316
+ },
317
+ rowTitle: {
318
+ overflow: "hidden",
319
+ fontSize: "13px",
320
+ lineHeight: "20px",
321
+ textOverflow: "ellipsis",
322
+ whiteSpace: "nowrap"
323
+ },
324
+ meta: {
325
+ fontSize: "12px",
326
+ lineHeight: "18px",
327
+ color: "var(--dsw-alias-label-tertiary)"
328
+ },
329
+ actions: {
330
+ display: "flex",
331
+ alignItems: "center",
332
+ gap: "8px",
333
+ flex: "none"
334
+ },
335
+ pagination: {
336
+ display: "flex",
337
+ alignItems: "center",
338
+ justifyContent: "flex-end",
339
+ gap: "8px",
340
+ marginTop: "4px",
341
+ flex: "none"
342
+ },
343
+ paginationLabel: {
344
+ fontSize: "12px",
345
+ lineHeight: "18px",
346
+ color: "var(--dsw-alias-label-tertiary)",
347
+ fontVariantNumeric: "tabular-nums"
348
+ },
349
+ status: {
350
+ margin: "0",
351
+ fontSize: "13px",
352
+ lineHeight: "20px",
353
+ color: "var(--dsw-alias-label-tertiary)",
354
+ flex: "none"
355
+ },
356
+ failure: {
357
+ margin: "0",
358
+ fontSize: "13px",
359
+ lineHeight: "20px",
360
+ color: "var(--dsw-alias-label-error, var(--dsw-alias-label-primary))",
361
+ flex: "none"
362
+ },
363
+ blocking: {
364
+ display: "flex",
365
+ flexDirection: "column",
366
+ alignItems: "center",
367
+ gap: "14px",
368
+ padding: "32px 40px",
369
+ color: "var(--dsw-alias-label-primary)"
370
+ },
371
+ spinner: {
372
+ width: "28px",
373
+ height: "28px",
374
+ border: "2px solid var(--dsw-alias-border-l2)",
375
+ borderTopColor: "var(--dsw-alias-label-primary)",
376
+ borderRadius: "50%",
377
+ animation: `${_morlay_dsh_client_ui_primitives_client.styling.keyframes({
378
+ from: { transform: "rotate(0deg)" },
379
+ to: { transform: "rotate(360deg)" }
380
+ })} 900ms linear infinite`
381
+ },
382
+ blockingText: {
383
+ margin: "0",
384
+ fontSize: "13px",
385
+ lineHeight: "20px"
386
+ }
387
+ };
388
+ //#endregion
389
+ //#region src/client/ConversationManagerPage.tsx
390
+ /** 一页的行数(会话列表)。 */
391
+ const PAGE_SIZE = 20;
392
+ /** 统计视图按会话列出时的行数上限。 */
393
+ const USAGE_SESSION_ROWS = 20;
394
+ /** 行上显示的紧凑相对时间。 */
395
+ function timeLabel(updatedAt, now, t) {
396
+ const { unit, n } = (0, _deepseek_ai_dsh_client_ui_primitives.relativeTime)(updatedAt, now);
397
+ return unit === "now" ? t("time.now") : t(`time.${unit}`, { n });
398
+ }
399
+ /** 标题或工作区名命中归一化后的查询。 */
400
+ function matches(row, normalizedQuery) {
401
+ return normalizedQuery.length === 0 || row.title.toLowerCase().includes(normalizedQuery) || row.workspace.toLowerCase().includes(normalizedQuery);
402
+ }
403
+ /** host 错误码 → 可读文案;没有码时保留原文。 */
404
+ function failureText(error, t) {
405
+ const code = error instanceof ConversationManagerRequestError ? error.code : void 0;
406
+ if (code === "SESSION_NOT_ARCHIVED") return t("failure.notArchived");
407
+ if (code === "SESSION_LIVE") return t("failure.live");
408
+ if (code === "SESSION_NOT_FOUND") return t("failure.missing");
409
+ return t("failure.other", { reason: error instanceof Error ? error.message : String(error) });
410
+ }
411
+ function ConversationManagerPage({ t, useSessions, useWorkspaces, archive, unarchive, remove, exportZip, importZip, collectGarbage, loadUsage }) {
412
+ const sessions = useSessions((state) => state);
413
+ const workspaces = useWorkspaces((state) => state);
414
+ const [query, setQuery] = (0, react.useState)("");
415
+ const [page, setPage] = (0, react.useState)(1);
416
+ const [showSubagents, setShowSubagents] = (0, react.useState)(false);
417
+ const [view, setView] = (0, react.useState)("sessions");
418
+ const [usageTab, setUsageTab] = (0, react.useState)("overview");
419
+ const [usageRange, setUsageRange] = (0, react.useState)("day");
420
+ const [usage, setUsage] = (0, react.useState)(null);
421
+ const [usageLoading, setUsageLoading] = (0, react.useState)(false);
422
+ const [usageError, setUsageError] = (0, react.useState)(null);
423
+ const [confirming, setConfirming] = (0, react.useState)(null);
424
+ const [gcPhase, setGcPhase] = (0, react.useState)("idle");
425
+ const [importing, setImporting] = (0, react.useState)(false);
426
+ const [notice, setNotice] = (0, react.useState)(null);
427
+ const [failure, setFailure] = (0, react.useState)(null);
428
+ const fileRef = (0, react.useRef)(null);
429
+ const ungrouped = t("ungrouped");
430
+ const rows = (0, react.useMemo)(() => {
431
+ const owners = /* @__PURE__ */ new Map();
432
+ for (const workspace of workspaces.items) for (const id of workspace.sessionIds) owners.set(id, workspace.title);
433
+ const archivedIds = new Set(workspaces.archivedSessionIds);
434
+ return [.../* @__PURE__ */ new Set([...sessions.ids, ...workspaces.archivedSessionIds])].flatMap((id) => {
435
+ const summary = sessions.byId[id];
436
+ if (summary === void 0) return [];
437
+ return [{
438
+ id,
439
+ title: summary.displayTitle,
440
+ workspace: owners.get(id) ?? ungrouped,
441
+ archived: archivedIds.has(id),
442
+ subagent: summary.origin === "subagent",
443
+ updatedAt: summary.updatedAt
444
+ }];
445
+ }).sort((left, right) => right.updatedAt - left.updatedAt);
446
+ }, [
447
+ workspaces,
448
+ sessions.ids,
449
+ sessions.byId,
450
+ ungrouped
451
+ ]);
452
+ const run = (action, settle) => {
453
+ setFailure(null);
454
+ setNotice(null);
455
+ action.then(() => {
456
+ settle?.();
457
+ }, (error) => {
458
+ settle?.();
459
+ setFailure(failureText(error, t));
460
+ });
461
+ };
462
+ const startGc = () => {
463
+ setGcPhase("running");
464
+ setFailure(null);
465
+ setNotice(null);
466
+ collectGarbage().then((result) => {
467
+ setGcPhase("idle");
468
+ setNotice(t("gc.done", {
469
+ sessions: result.orphanSessions,
470
+ events: result.orphanEvents
471
+ }));
472
+ }, (error) => {
473
+ setGcPhase("idle");
474
+ setFailure(failureText(error, t));
475
+ });
476
+ };
477
+ const requestUsage = (range) => {
478
+ setUsageRange(range);
479
+ setUsageLoading(true);
480
+ setUsageError(null);
481
+ loadUsage(range).then((report) => {
482
+ setUsage(report);
483
+ }, (error) => {
484
+ setUsageError(failureText(error, t));
485
+ }).finally(() => {
486
+ setUsageLoading(false);
487
+ });
488
+ };
489
+ const openUsage = () => {
490
+ setView("usage");
491
+ if (usage === null && !usageLoading) requestUsage(usageRange);
492
+ };
493
+ if (sessions.phase !== "ready") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
494
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.status),
495
+ children: t("loading")
496
+ });
497
+ const now = Date.now();
498
+ const listed = showSubagents ? rows : rows.filter((row) => !row.subagent);
499
+ const matched = listed.filter((row) => matches(row, query.trim().toLowerCase()));
500
+ const pageCount = Math.max(1, Math.ceil(matched.length / PAGE_SIZE));
501
+ const currentPage = Math.min(page, pageCount);
502
+ const visible = matched.slice((currentPage - 1) * PAGE_SIZE, currentPage * PAGE_SIZE);
503
+ const confirmed = confirming;
504
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
505
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.page),
506
+ "data-view": view,
507
+ children: [
508
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
509
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.header),
510
+ children: [
511
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
512
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.title),
513
+ children: t("title")
514
+ }),
515
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
516
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.tabs),
517
+ role: "tablist",
518
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
519
+ type: "button",
520
+ role: "tab",
521
+ "data-tab": "sessions",
522
+ "aria-selected": view === "sessions",
523
+ className: _morlay_dsh_client_ui_primitives_client.styling.className(styles.tab, view === "sessions" && styles.tabActive),
524
+ onClick: () => {
525
+ setView("sessions");
526
+ },
527
+ children: t("view.sessions")
528
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
529
+ type: "button",
530
+ role: "tab",
531
+ "data-tab": "usage",
532
+ "aria-selected": view === "usage",
533
+ className: _morlay_dsh_client_ui_primitives_client.styling.className(styles.tab, view === "usage" && styles.tabActive),
534
+ onClick: openUsage,
535
+ children: t("view.usage")
536
+ })]
537
+ }),
538
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
539
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.headerActions),
540
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
541
+ variant: "outline",
542
+ size: "sm",
543
+ "data-action": "import",
544
+ disabled: importing,
545
+ "aria-busy": importing,
546
+ "aria-label": importing ? t("importing") : t("import"),
547
+ onClick: () => {
548
+ fileRef.current?.click();
549
+ },
550
+ children: importing ? t("importing") : t("import")
551
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
552
+ variant: "outline",
553
+ size: "sm",
554
+ "data-action": "gc",
555
+ disabled: gcPhase !== "idle",
556
+ "aria-label": t("gc.button"),
557
+ onClick: () => {
558
+ setFailure(null);
559
+ setNotice(null);
560
+ setGcPhase("confirm");
561
+ },
562
+ children: t("gc.button")
563
+ })]
564
+ }),
565
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
566
+ ref: fileRef,
567
+ type: "file",
568
+ accept: ".zip,application/zip",
569
+ hidden: true,
570
+ onChange: (event) => {
571
+ const file = event.currentTarget.files?.[0];
572
+ event.currentTarget.value = "";
573
+ if (file === void 0) return;
574
+ setImporting(true);
575
+ setFailure(null);
576
+ setNotice(null);
577
+ importZip(file).then(() => {
578
+ setNotice(t("imported"));
579
+ }, (error) => {
580
+ setFailure(failureText(error, t));
581
+ }).finally(() => {
582
+ setImporting(false);
583
+ });
584
+ }
585
+ })
586
+ ]
587
+ }),
588
+ view === "usage" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageView, {
589
+ report: usage,
590
+ loading: usageLoading,
591
+ error: usageError,
592
+ tab: usageTab,
593
+ onTab: setUsageTab,
594
+ range: usageRange,
595
+ onRange: requestUsage,
596
+ t
597
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
598
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
599
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.filters),
600
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
601
+ className: _morlay_dsh_client_ui_primitives_client.styling.className(styles.search),
602
+ "data-filter": "search",
603
+ type: "search",
604
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconSearchOutline16, {}),
605
+ value: query,
606
+ placeholder: t("search"),
607
+ "aria-label": t("search"),
608
+ onChange: (event) => {
609
+ setQuery(event.currentTarget.value);
610
+ setPage(1);
611
+ }
612
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
613
+ "data-filter": "subagents",
614
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Checkbox, {
615
+ checked: showSubagents,
616
+ label: t("showSubagents"),
617
+ onChange: (next) => {
618
+ setShowSubagents(next);
619
+ setPage(1);
620
+ }
621
+ })
622
+ })]
623
+ }),
624
+ notice === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
625
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.status),
626
+ "data-notice": "result",
627
+ children: notice
628
+ }),
629
+ failure === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
630
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.failure),
631
+ "data-failure": "result",
632
+ role: "alert",
633
+ children: failure
634
+ }),
635
+ listed.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
636
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.status),
637
+ "data-status": "empty",
638
+ children: t("empty")
639
+ }) : null,
640
+ listed.length > 0 && matched.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
641
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.status),
642
+ "data-status": "empty-search",
643
+ children: t("emptySearch")
644
+ }) : null,
645
+ visible.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
646
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.list),
647
+ children: visible.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
648
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.row),
649
+ "data-session-id": String(row.id),
650
+ "data-archived": row.archived ? "true" : "false",
651
+ "data-subagent": row.subagent ? "true" : "false",
652
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
653
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.identity),
654
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
655
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.titleLine),
656
+ children: [
657
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
658
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.rowTitle),
659
+ children: row.title
660
+ }),
661
+ row.archived ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
662
+ tone: "neutral",
663
+ children: t("archived")
664
+ }) : null,
665
+ row.subagent ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Tag, {
666
+ tone: "quiet",
667
+ children: t("subagent")
668
+ }) : null
669
+ ]
670
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
671
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.meta),
672
+ children: [row.workspace, timeLabel(row.updatedAt, now, t)].join(" · ")
673
+ })]
674
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
675
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.actions),
676
+ children: [
677
+ row.archived ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
678
+ variant: "outline",
679
+ size: "sm",
680
+ "data-action": "unarchive",
681
+ "aria-label": t("unarchiveNamed", { title: row.title }),
682
+ onClick: () => {
683
+ run(unarchive(row.id));
684
+ },
685
+ children: t("unarchive")
686
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
687
+ variant: "outline",
688
+ size: "sm",
689
+ "data-action": "archive",
690
+ "aria-label": t("archiveNamed", { title: row.title }),
691
+ onClick: () => {
692
+ run(archive(row.id));
693
+ },
694
+ children: t("archive")
695
+ }),
696
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
697
+ variant: "outline",
698
+ size: "sm",
699
+ "data-action": "export",
700
+ "aria-label": t("exportNamed", { title: row.title }),
701
+ onClick: () => {
702
+ run(exportZip(row.id));
703
+ },
704
+ children: t("export")
705
+ }),
706
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
707
+ variant: "outline",
708
+ size: "sm",
709
+ disabled: !row.archived,
710
+ "data-action": "remove",
711
+ "aria-label": t("removeNamed", { title: row.title }),
712
+ onClick: () => {
713
+ setFailure(null);
714
+ setNotice(null);
715
+ setConfirming(row);
716
+ },
717
+ children: t("remove")
718
+ })
719
+ ]
720
+ })]
721
+ }, row.id))
722
+ }) : null,
723
+ matched.length > 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
724
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.pagination),
725
+ "data-pagination": "",
726
+ "data-page-current": currentPage,
727
+ "data-page-total": pageCount,
728
+ children: [
729
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
730
+ variant: "ghost",
731
+ size: "sm",
732
+ disabled: currentPage <= 1,
733
+ onClick: () => {
734
+ setPage(currentPage - 1);
735
+ },
736
+ children: t("page.previous")
737
+ }),
738
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
739
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.paginationLabel),
740
+ children: t("page.label", {
741
+ page: currentPage,
742
+ total: pageCount
743
+ })
744
+ }),
745
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
746
+ variant: "ghost",
747
+ size: "sm",
748
+ disabled: currentPage >= pageCount,
749
+ onClick: () => {
750
+ setPage(currentPage + 1);
751
+ },
752
+ children: t("page.next")
753
+ })
754
+ ]
755
+ }) : null
756
+ ] }),
757
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
758
+ open: confirmed !== null,
759
+ onClose: () => {
760
+ setConfirming(null);
761
+ },
762
+ title: t("confirmTitle"),
763
+ closeLabel: t("close"),
764
+ description: t("confirmDescription"),
765
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
766
+ variant: "ghost",
767
+ onClick: () => {
768
+ setConfirming(null);
769
+ },
770
+ children: t("confirmCancel")
771
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
772
+ variant: "primary",
773
+ onClick: () => {
774
+ if (confirmed === null) return;
775
+ run(remove(confirmed.id), () => {
776
+ setConfirming(null);
777
+ });
778
+ },
779
+ children: t("confirmAccept")
780
+ })] })
781
+ }),
782
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
783
+ open: gcPhase === "confirm",
784
+ onClose: () => {
785
+ setGcPhase("idle");
786
+ },
787
+ title: t("gc.title"),
788
+ closeLabel: t("close"),
789
+ description: t("gc.description"),
790
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
791
+ variant: "ghost",
792
+ onClick: () => {
793
+ setGcPhase("idle");
794
+ },
795
+ children: t("gc.cancel")
796
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
797
+ variant: "primary",
798
+ onClick: startGc,
799
+ children: t("gc.confirm")
800
+ })] })
801
+ }),
802
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
803
+ open: gcPhase === "running",
804
+ onClose: () => {},
805
+ headless: true,
806
+ title: t("gc.title"),
807
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
808
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.blocking),
809
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
810
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.spinner),
811
+ "aria-hidden": "true"
812
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
813
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.blockingText),
814
+ children: t("gc.running")
815
+ })]
816
+ })
817
+ })
818
+ ]
819
+ });
820
+ }
821
+ /** 时间范围选项:默认本日,其后是本周(周一起算)与最近 N 天,「全部」放在最后。 */
822
+ const USAGE_RANGES = [
823
+ "day",
824
+ "week",
825
+ "7d",
826
+ "30d",
827
+ "90d",
828
+ "all"
829
+ ];
830
+ /** 范围按钮的文案。 */
831
+ function rangeLabel(range, t) {
832
+ switch (range) {
833
+ case "all": return t("usage.range.all");
834
+ case "day": return t("usage.range.day");
835
+ case "week": return t("usage.range.week");
836
+ case "7d": return t("usage.range.days", { n: 7 });
837
+ case "30d": return t("usage.range.days", { n: 30 });
838
+ case "90d": return t("usage.range.days", { n: 90 });
839
+ }
840
+ }
841
+ /** 缓存命中率(百分点):缓存输入占总输入(含缓存)的比例。 */
842
+ function cacheHitPercent(totals) {
843
+ const total = totals.inputTokens + totals.cacheReadTokens;
844
+ if (total <= 0) return 0;
845
+ return totals.cacheReadTokens / total * 100;
846
+ }
847
+ /** 显示口径的单项:输入(含缓存输入)、缓存输入、缓存命中率、输出、推理、事件——没有合计项。 */
848
+ function usageMetrics(totals, t) {
849
+ return [
850
+ {
851
+ key: "input",
852
+ label: t("usage.inputWithCache"),
853
+ value: totals.inputTokens + totals.cacheReadTokens,
854
+ kind: "tokens"
855
+ },
856
+ {
857
+ key: "cacheInput",
858
+ label: t("usage.cacheInput"),
859
+ value: totals.cacheReadTokens,
860
+ kind: "tokens"
861
+ },
862
+ {
863
+ key: "cacheRate",
864
+ label: t("usage.cacheRate"),
865
+ value: cacheHitPercent(totals),
866
+ kind: "percent"
867
+ },
868
+ {
869
+ key: "output",
870
+ label: t("usage.output"),
871
+ value: totals.outputTokens,
872
+ kind: "tokens"
873
+ },
874
+ {
875
+ key: "reasoning",
876
+ label: t("usage.reasoning"),
877
+ value: totals.reasoningTokens,
878
+ kind: "tokens"
879
+ },
880
+ {
881
+ key: "events",
882
+ label: t("usage.events"),
883
+ value: totals.events,
884
+ kind: "tokens"
885
+ }
886
+ ];
887
+ }
888
+ /** 折叠行的排序口径(不显示):输入(含缓存)+ 输出。 */
889
+ function sortWeight(totals) {
890
+ return totals.inputTokens + totals.cacheReadTokens + totals.outputTokens;
891
+ }
892
+ function emptyTotals() {
893
+ return {
894
+ events: 0,
895
+ inputTokens: 0,
896
+ outputTokens: 0,
897
+ cacheReadTokens: 0,
898
+ reasoningTokens: 0,
899
+ totalTokens: 0
900
+ };
901
+ }
902
+ /** 把桶按一个键折叠成行并按用量降序(按天 / 按模型都用它)。 */
903
+ function foldBuckets(buckets, keyOf) {
904
+ const folded = /* @__PURE__ */ new Map();
905
+ for (const bucket of buckets) {
906
+ const key = keyOf(bucket);
907
+ const row = folded.get(key) ?? {
908
+ key,
909
+ label: key,
910
+ totals: emptyTotals()
911
+ };
912
+ row.totals.events += bucket.events;
913
+ row.totals.inputTokens += bucket.inputTokens;
914
+ row.totals.outputTokens += bucket.outputTokens;
915
+ row.totals.cacheReadTokens += bucket.cacheReadTokens;
916
+ row.totals.reasoningTokens += bucket.reasoningTokens;
917
+ row.totals.totalTokens += bucket.totalTokens;
918
+ folded.set(key, row);
919
+ }
920
+ return [...folded.values()].sort((left, right) => sortWeight(right.totals) - sortWeight(left.totals));
921
+ }
922
+ /** 一行用量:label 在上,下面是横向排布的单项。 */
923
+ function UsageRow({ rowKey, label, totals, t }) {
924
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("li", {
925
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.usageRow),
926
+ "data-usage-key": rowKey,
927
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
928
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.usageRowLabel),
929
+ children: label
930
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
931
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.usageMetrics),
932
+ children: usageMetrics(totals, t).map((metric) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
933
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.usageMetric),
934
+ "data-usage-cell": metric.key,
935
+ "data-usage-value": metric.value,
936
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
937
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.usageMetricLabel),
938
+ children: metric.label
939
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
940
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.usageMetricValue),
941
+ children: metric.kind === "percent" ? formatPercent(metric.value) : formatTokens(metric.value)
942
+ })]
943
+ }, metric.key))
944
+ })]
945
+ });
946
+ }
947
+ function UsageList({ rows, t }) {
948
+ if (rows.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
949
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.status),
950
+ "data-usage-status": "empty",
951
+ children: t("usage.empty")
952
+ });
953
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
954
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.usageList),
955
+ children: rows.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageRow, {
956
+ rowKey: row.key,
957
+ label: row.label,
958
+ totals: row.totals,
959
+ t
960
+ }, row.key))
961
+ });
962
+ }
963
+ /** 总览:全部与「其中子代理」两行,与列表行同形。 */
964
+ function UsageOverview({ report, t }) {
965
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ul", {
966
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.usageList),
967
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageRow, {
968
+ rowKey: "all",
969
+ label: t("usage.all"),
970
+ totals: report.totals,
971
+ t
972
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageRow, {
973
+ rowKey: "subagent",
974
+ label: t("usage.subagentOnly"),
975
+ totals: report.subagent,
976
+ t
977
+ })]
978
+ });
979
+ }
980
+ /** 统计视图:时间范围过滤 + 二层维度切换(总览 / 按模型 / 按会话)。 */
981
+ function UsageView({ report, loading, error, tab, onTab, range, onRange, t }) {
982
+ const items = [
983
+ "overview",
984
+ "models",
985
+ "sessions"
986
+ ];
987
+ const labels = {
988
+ overview: t("usage.overview"),
989
+ models: t("usage.models"),
990
+ sessions: t("usage.sessions")
991
+ };
992
+ const rows = report === null || tab === "overview" ? [] : tab === "models" ? foldBuckets(report.buckets, (bucket) => `${bucket.provider ?? t("usage.unknownModel")} / ${bucket.model ?? t("usage.unknownModel")}`) : report.sessions.map((row) => ({
993
+ key: row.sessionId,
994
+ label: row.title ?? row.sessionId,
995
+ totals: row
996
+ })).sort((left, right) => sortWeight(right.totals) - sortWeight(left.totals)).slice(0, USAGE_SESSION_ROWS);
997
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
998
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.usage),
999
+ "data-usage-view": tab,
1000
+ "data-usage-range": range,
1001
+ children: [
1002
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1003
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.tabs),
1004
+ role: "radiogroup",
1005
+ "aria-label": t("usage.range"),
1006
+ children: USAGE_RANGES.map((option) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1007
+ type: "button",
1008
+ role: "radio",
1009
+ "aria-checked": range === option,
1010
+ "data-range": option,
1011
+ className: _morlay_dsh_client_ui_primitives_client.styling.className(styles.tab, range === option && styles.tabActive),
1012
+ onClick: () => {
1013
+ onRange(option);
1014
+ },
1015
+ children: rangeLabel(option, t)
1016
+ }, option))
1017
+ }),
1018
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1019
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.tabs),
1020
+ role: "tablist",
1021
+ children: items.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1022
+ type: "button",
1023
+ role: "tab",
1024
+ "data-usage-tab": item,
1025
+ "aria-selected": tab === item,
1026
+ className: _morlay_dsh_client_ui_primitives_client.styling.className(styles.tab, tab === item && styles.tabActive),
1027
+ onClick: () => {
1028
+ onTab(item);
1029
+ },
1030
+ children: labels[item]
1031
+ }, item))
1032
+ }),
1033
+ loading ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1034
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.status),
1035
+ "data-usage-status": "loading",
1036
+ children: t("usage.loading")
1037
+ }) : null,
1038
+ error === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1039
+ ..._morlay_dsh_client_ui_primitives_client.styling.props(styles.failure),
1040
+ "data-usage-status": "error",
1041
+ role: "alert",
1042
+ children: error
1043
+ }),
1044
+ report === null ? null : tab === "overview" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageOverview, {
1045
+ report,
1046
+ t
1047
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UsageList, {
1048
+ rows,
1049
+ t
1050
+ })
1051
+ ]
1052
+ });
1053
+ }
1054
+ //#endregion
1055
+ //#region src/client/ConversationManagerIcon.tsx
1056
+ function ConversationManagerIcon({ size }) {
1057
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconArchiveOutline20, { size });
1058
+ }
1059
+ //#endregion
1060
+ //#region src/client/locales.ts
1061
+ /** 「对话管理」页面的文案字典。 */
1062
+ /** 简体中文是 key 真源。 */
1063
+ const zh = {
1064
+ panel: "对话管理",
1065
+ title: "对话管理",
1066
+ search: "搜索会话",
1067
+ loading: "正在读取会话…",
1068
+ empty: "暂无会话。",
1069
+ emptySearch: "没有匹配的会话。",
1070
+ archived: "已归档",
1071
+ subagent: "子代理",
1072
+ showSubagents: "显示子代理会话",
1073
+ "view.sessions": "会话",
1074
+ "view.usage": "统计",
1075
+ "usage.overview": "总览",
1076
+ "usage.all": "全部会话",
1077
+ "usage.range": "时间范围",
1078
+ "usage.range.all": "全部",
1079
+ "usage.range.day": "本日",
1080
+ "usage.range.week": "本周",
1081
+ "usage.range.days": "近 {n} 天",
1082
+ "usage.models": "按模型",
1083
+ "usage.sessions": "按会话",
1084
+ "usage.loading": "正在统计…",
1085
+ "usage.empty": "暂无用量数据。",
1086
+ "usage.input": "输入",
1087
+ "usage.inputWithCache": "输入(含缓存)",
1088
+ "usage.output": "输出",
1089
+ "usage.cacheInput": "缓存输入",
1090
+ "usage.cacheRate": "缓存命中率",
1091
+ "usage.reasoning": "推理",
1092
+ "usage.total": "合计",
1093
+ "usage.events": "事件",
1094
+ "usage.subagentOnly": "其中子代理",
1095
+ "usage.unknownModel": "未知模型",
1096
+ archive: "归档",
1097
+ archiveNamed: "归档 {title}",
1098
+ unarchive: "取消归档",
1099
+ unarchiveNamed: "取消归档 {title}",
1100
+ remove: "删除",
1101
+ removeNamed: "删除 {title}",
1102
+ export: "导出",
1103
+ exportNamed: "导出 {title}",
1104
+ ungrouped: "未分组",
1105
+ "page.previous": "上一页",
1106
+ "page.next": "下一页",
1107
+ "page.label": "第 {page} / {total} 页",
1108
+ "gc.button": "清理孤儿数据",
1109
+ "gc.title": "清理孤儿数据",
1110
+ "gc.description": "会先停止所有运行中的 Agent,期间界面不可操作;随后回收孤儿 subagent 会话与孤儿数据,并执行 VACUUM。",
1111
+ "gc.confirm": "开始清理",
1112
+ "gc.cancel": "取消",
1113
+ "gc.running": "正在清理,请稍候…",
1114
+ "gc.done": "已回收 {sessions} 条孤儿会话、{events} 行孤儿数据。",
1115
+ import: "导入对话",
1116
+ importing: "导入中…",
1117
+ imported: "已导入为新会话。",
1118
+ confirmTitle: "删除会话",
1119
+ confirmDescription: "删除后无法恢复:该会话的内容会从存储中移除。",
1120
+ confirmAccept: "删除",
1121
+ confirmCancel: "取消",
1122
+ close: "关闭",
1123
+ "failure.notArchived": "只有已归档的会话可以删除。",
1124
+ "failure.live": "会话正在使用中,无法删除。",
1125
+ "failure.missing": "会话不存在或已被删除。",
1126
+ "failure.other": "操作失败:{reason}",
1127
+ "time.now": "刚刚",
1128
+ "time.minutes": "{n}分钟",
1129
+ "time.hours": "{n}小时",
1130
+ "time.days": "{n}天",
1131
+ "time.months": "{n}个月",
1132
+ "time.years": "{n}年"
1133
+ };
1134
+ /** 英文对照表。 */
1135
+ const en = {
1136
+ panel: "Conversations",
1137
+ title: "Conversations",
1138
+ search: "Search sessions",
1139
+ loading: "Reading sessions…",
1140
+ empty: "No sessions.",
1141
+ emptySearch: "No matching sessions.",
1142
+ archived: "Archived",
1143
+ subagent: "Subagent",
1144
+ showSubagents: "Show subagent sessions",
1145
+ "view.sessions": "Sessions",
1146
+ "view.usage": "Usage",
1147
+ "usage.overview": "Overview",
1148
+ "usage.all": "All sessions",
1149
+ "usage.range": "Time range",
1150
+ "usage.range.all": "All",
1151
+ "usage.range.day": "Today",
1152
+ "usage.range.week": "This week",
1153
+ "usage.range.days": "Last {n}d",
1154
+ "usage.models": "By model",
1155
+ "usage.sessions": "By session",
1156
+ "usage.loading": "Reading usage…",
1157
+ "usage.empty": "No usage recorded.",
1158
+ "usage.input": "Input",
1159
+ "usage.inputWithCache": "Input (incl. cache)",
1160
+ "usage.output": "Output",
1161
+ "usage.cacheInput": "Cache input",
1162
+ "usage.cacheRate": "Cache hit rate",
1163
+ "usage.reasoning": "Reasoning",
1164
+ "usage.total": "Total",
1165
+ "usage.events": "Events",
1166
+ "usage.subagentOnly": "Subagent share",
1167
+ "usage.unknownModel": "Unknown model",
1168
+ archive: "Archive",
1169
+ archiveNamed: "Archive {title}",
1170
+ unarchive: "Unarchive",
1171
+ unarchiveNamed: "Unarchive {title}",
1172
+ remove: "Delete",
1173
+ removeNamed: "Delete {title}",
1174
+ export: "Export",
1175
+ exportNamed: "Export {title}",
1176
+ ungrouped: "Ungrouped",
1177
+ "page.previous": "Previous",
1178
+ "page.next": "Next",
1179
+ "page.label": "Page {page} / {total}",
1180
+ "gc.button": "Clean orphan data",
1181
+ "gc.title": "Clean orphan data",
1182
+ "gc.description": "Stops every running Agent first — the UI is blocked meanwhile — then reclaims orphan subagent sessions and orphan rows, and runs VACUUM.",
1183
+ "gc.confirm": "Start",
1184
+ "gc.cancel": "Cancel",
1185
+ "gc.running": "Cleaning, please wait…",
1186
+ "gc.done": "Reclaimed {sessions} orphan sessions and {events} orphan rows.",
1187
+ import: "Import conversation",
1188
+ importing: "Importing…",
1189
+ imported: "Imported as a new session.",
1190
+ confirmTitle: "Delete session",
1191
+ confirmDescription: "This cannot be undone: the session's content is removed from storage.",
1192
+ confirmAccept: "Delete",
1193
+ confirmCancel: "Cancel",
1194
+ close: "Close",
1195
+ "failure.notArchived": "Only archived sessions can be deleted.",
1196
+ "failure.live": "The session is in use and cannot be deleted.",
1197
+ "failure.missing": "The session does not exist or was already deleted.",
1198
+ "failure.other": "Action failed: {reason}",
1199
+ "time.now": "now",
1200
+ "time.minutes": "{n}min",
1201
+ "time.hours": "{n}h",
1202
+ "time.days": "{n}d",
1203
+ "time.months": "{n}mo",
1204
+ "time.years": "{n}y"
1205
+ };
1206
+ //#endregion
1207
+ //#region src/client/index.ts
1208
+ /** 本包字典的 namespace。 */
1209
+ const NS = "conversationManager";
1210
+ /** nav 行与主面板共用的 id。 */
1211
+ const PANEL_ID = "conversations";
1212
+ /** nav 行的位置:紧邻官方 Plugins 行(order 0)。 */
1213
+ const PANEL_ORDER = 1;
1214
+ /** 页面用到的服务。 */
1215
+ const inject = [
1216
+ "slots",
1217
+ "locale",
1218
+ "uiWorkspace",
1219
+ "sessions"
1220
+ ];
1221
+ function apply(ctx) {
1222
+ ctx.effect(() => ctx.locale.register(NS, {
1223
+ zh,
1224
+ en
1225
+ }), "ui-conversation-manager: dictionaries");
1226
+ const t = ctx.locale.bind(NS);
1227
+ const sessions = ctx.get("sessions");
1228
+ const controller = new ConversationManagerController({
1229
+ archiveSession: (sessionId) => ctx.uiWorkspace.archiveSession(sessionId),
1230
+ unarchiveSession: (sessionId) => ctx.uiWorkspace.unarchiveSession(sessionId),
1231
+ refresh: () => sessions.refresh()
1232
+ });
1233
+ ctx.slots.inject("main", () => ctx.slots.register({
1234
+ name: "main",
1235
+ key: PANEL_ID,
1236
+ locale: NS,
1237
+ inject: () => controller.face
1238
+ }, ConversationManagerPage));
1239
+ ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
1240
+ name: "sidebar.panellist",
1241
+ id: PANEL_ID,
1242
+ order: 1,
1243
+ label: () => t("panel"),
1244
+ locale: NS
1245
+ }, ConversationManagerIcon));
1246
+ }
1247
+ //#endregion
1248
+ exports.ConversationManagerController = ConversationManagerController;
1249
+ exports.ConversationManagerRequestError = ConversationManagerRequestError;
1250
+ exports.NS = NS;
1251
+ exports.PANEL_ID = PANEL_ID;
1252
+ exports.PANEL_ORDER = PANEL_ORDER;
1253
+ exports.apply = apply;
1254
+ exports.inject = inject;
1255
+ return module.exports;
1256
+ }
1257
+ });