dsh-session-curator 0.1.0 → 0.1.2

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/lib/client.js CHANGED
@@ -1,1011 +1,1348 @@
1
- /**
2
- * dsh-session-curator — client 半区(免构建手写束)。
3
- *
4
- * 三个贡献:
5
- * 1. `sidebar.workspaces.session.menu.item`(order 500,排在官方 archive 之后):
6
- * 一行「删除会话」,点击后弹确认框,确认即软删除到回收站。
7
- * 2. `shell.overlay`:全局确认弹窗宿主(菜单项与设置面板共用)。
8
- * 3. `settings.section`(order 45):会话管理面板 —— 已归档 / 已置顶 / 回收站。
9
- *
10
- * 与宿主通信走 Connection 的共享 `/api` 通道:官方调用器
11
- * (`ctx.get('connection').rpc.call`,若该服务在客户端存在)或逐字复刻的
12
- * `fetch` 兜底,目标都是 `/api/session-curator/<endpoint>`。信任栅栏与会话鉴权
13
- * 仍由宿主 Connection 的 `/api` 前缀路由把关,插件自身无需任何鉴权代码。
14
- */
1
+ // 本文件由 `node scripts/build.mjs` 从 src/client/**/*.ts 生成,不要手改。
15
2
  window.__ModuleLoader__.load({
16
3
  id: "dsh-session-curator",
4
+ // 浏览器只认这种「工厂 + 局部 require」的束:factory 收到的 require 就是 DSH 的模块表,
5
+ // esbuild 把 external 的 import 编译成了对这个参数的调用。
17
6
  factory: (require) => {
18
- const bundleModule = { exports: {} };
19
- Object.defineProperty(bundleModule.exports, Symbol.toStringTag, { value: "Module" });
7
+ const module = { exports: {} };
8
+ (function (module, exports, require) {
9
+ var __create = Object.create;
10
+ var __defProp = Object.defineProperty;
11
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
12
+ var __getOwnPropNames = Object.getOwnPropertyNames;
13
+ var __getProtoOf = Object.getPrototypeOf;
14
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
15
+ var __export = (target2, all) => {
16
+ for (var name in all)
17
+ __defProp(target2, name, { get: all[name], enumerable: true });
18
+ };
19
+ var __copyProps = (to, from, except, desc) => {
20
+ if (from && typeof from === "object" || typeof from === "function") {
21
+ for (let key of __getOwnPropNames(from))
22
+ if (!__hasOwnProp.call(to, key) && key !== except)
23
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
24
+ }
25
+ return to;
26
+ };
27
+ var __toESM = (mod, isNodeMode, target2) => (target2 = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
28
+ // If the importer is in node compatibility mode or this is not an ESM
29
+ // file that has been converted to a CommonJS file using a Babel-
30
+ // compatible transform (i.e. "__esModule" has not been set), then set
31
+ // "default" to the CommonJS "module.exports" for node compatibility.
32
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target2, "default", { value: mod, enumerable: true }) : target2,
33
+ mod
34
+ ));
35
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
36
 
21
- const React = require("react");
22
- const {
23
- Button,
24
- Menu,
25
- MenuItemButton,
26
- Modal,
27
- IconChevronDownOutlineRegular,
28
- IconChevronRightOutlineRegular,
29
- IconEllipsisOutlineRegular,
30
- IconFolderCloseRegular,
31
- IconFolderOpenRegular,
32
- IconPinOutlineRegular,
33
- IconRefreshOutlineRegular,
34
- IconTrashOutlineRegular,
35
- IconUnarchiveOutlineRegular
36
- } = require("@deepseek-ai/dsh-client-ui-primitives");
37
- const h = React.createElement;
38
-
39
- const NS = "session-curator";
40
- /** 官方共享通道;本插件占 `/api/session-curator/<endpoint>`。 */
41
- const API_CHANNEL = "/api";
42
- const ROUTE_PREFIX = "session-curator";
43
- const MENU_ID = "session-curator.delete";
44
- const OVERLAY_ID = "session-curator.dialog";
45
- /** 本插件自己的设置页 id(也是"没得借"时的兜底)。 */
46
- const SECTION_ID = "session-curator";
47
- /**
48
- * 设置页导航栏的图标**不是注册时给的** —— 设置外壳
49
- * `@deepseek-ai/dsh-client-ui-settings-general` 的 `navIcon(id)` 是按 **section id 查表**:
50
- *
51
- * ```js
52
- * account → 人像 / models → 数据 / agent-presets → 预设 / plugins → 个性化
53
- * archived-sessions → 归档盒子 / 其它一律 → 齿轮(默认)
54
- * ```
55
- *
56
- * 我们的 `session-curator` 不在表里,所以一直是"默认齿轮"(`settings.section` 的
57
- * options 只读 id / order / label,外壳不看 icon 字段,插件没法自带导航图标)。
58
- * 表里**唯一没人用**的 id 是 `archived-sessions`,它映射到归档盒子图标 —— 语义刚好贴合
59
- * 本插件(归档 / 回收站管理),所以借用它。
60
- */
61
- const NAV_SECTION_ID = "archived-sessions";
62
-
63
- /**
64
- * 借 id 之前先看槽位账本:**一旦将来 DSH 自己注册了 `archived-sessions`,
65
- * 我们就退回自己的 id**(拿默认齿轮),绝不会跟官方撞车。
66
- */
67
- function sectionIdFor(ctx) {
68
- try {
69
- const taken = ctx.slots.entries("settings.section").some((entry) => entry?.options?.id === NAV_SECTION_ID);
70
- return taken ? SECTION_ID : NAV_SECTION_ID;
71
- } catch {
72
- // 拿不到账本(没有 entries)就退回自己的 id。
73
- return SECTION_ID;
74
- }
75
- }
76
-
77
- // ── 文案 ────────────────────────────────────────────────────────────
78
- const zh = {
79
- sectionLabel: "会话管理",
80
- menuDelete: "删除会话",
81
- titleArchived: "已归档",
82
- titlePinned: "已置顶",
83
- titleTrash: "回收站",
84
- refresh: "刷新",
85
- loading: "读取中…",
86
- retry: "重试",
87
- emptyArchived: "还没有归档的会话。在会话行的「⋯」菜单里归档后会出现在这里。",
88
- emptyPinned: "没有置顶的会话。",
89
- emptyTrash: "回收站是空的。",
90
- unarchive: "取消归档",
91
- pin: "置顶",
92
- unpin: "取消置顶",
93
- restore: "恢复",
94
- purge: "永久删除",
95
- moveToTrash: "移入回收站",
96
- rowActions: "更多操作",
97
- selectAll: "全选",
98
- selectNone: "取消选择",
99
- selected: "已选 {n} 项",
100
- emptyTrashAll: "清空回收站",
101
- total: "合计 {n} 个会话 · {size}",
102
- working: "处理中…",
103
- sessionRunning: "会话正在运行,先停止它再删除。",
104
- confirmTrashTitle: "删除会话",
105
- confirmTrashBody: "「{title}」会被移到回收站,之后可以在「设置 → 会话管理」里恢复。",
106
- confirmTrashAction: "移到回收站",
107
- confirmPurgeTitle: "永久删除",
108
- confirmPurgeBody: "「{title}」将从回收站永久删除,日志文件会被彻底移除,无法恢复。",
109
- confirmPurgeManyBody: "选中的 {n} 个会话将从回收站永久删除,无法恢复。",
110
- confirmPurgeAction: "永久删除",
111
- ackPurge: "我明白这无法撤销",
112
- cancel: "取消",
113
- close: "关闭",
114
- noProject: "(未知目录)",
115
- files: "{n} 个文件",
116
- groupingOn: "分组:工作区",
117
- groupingOff: "分组:平铺",
118
- groupHint: "按会话原来的工作区文件夹归类;点分组标题折叠,左边的勾选框选中整个工作区",
119
- sessions: "{n} 个会话",
120
- workspaces: "{n} 个工作区",
121
- selectGroup: "全选该工作区",
122
- clearGroup: "取消选中该工作区",
123
- selectAllShort: "全选",
124
- clearShort: "取消",
125
- collapse: "折叠",
126
- expand: "展开"
127
- };
128
- const en = {
129
- sectionLabel: "Session manager",
130
- menuDelete: "Delete session",
131
- titleArchived: "Archived",
132
- titlePinned: "Pinned",
133
- titleTrash: "Trash",
134
- refresh: "Refresh",
135
- loading: "Loading…",
136
- retry: "Retry",
137
- emptyArchived: "No archived sessions yet. Archive one from its “⋯” menu and it shows up here.",
138
- emptyPinned: "No pinned sessions.",
139
- emptyTrash: "The trash is empty.",
140
- unarchive: "Unarchive",
141
- pin: "Pin",
142
- unpin: "Unpin",
143
- restore: "Restore",
144
- purge: "Delete permanently",
145
- moveToTrash: "Move to trash",
146
- rowActions: "More actions",
147
- selectAll: "Select all",
148
- selectNone: "Clear selection",
149
- selected: "{n} selected",
150
- emptyTrashAll: "Empty trash",
151
- total: "{n} sessions · {size}",
152
- working: "Working…",
153
- sessionRunning: "This session is running; stop it before deleting.",
154
- confirmTrashTitle: "Delete session",
155
- confirmTrashBody: "“{title}” moves to the trash. You can restore it later under Settings → Session manager.",
156
- confirmTrashAction: "Move to trash",
157
- confirmPurgeTitle: "Delete permanently",
158
- confirmPurgeBody: "“{title}” is removed from the trash for good; its log files are gone and cannot be recovered.",
159
- confirmPurgeManyBody: "{n} selected sessions are removed from the trash for good and cannot be recovered.",
160
- confirmPurgeAction: "Delete permanently",
161
- ackPurge: "I understand this cannot be undone",
162
- cancel: "Cancel",
163
- close: "Close",
164
- noProject: "(unknown directory)",
165
- files: "{n} files",
166
- groupingOn: "Grouped: workspace",
167
- groupingOff: "Grouped: off",
168
- groupHint: "Group sessions under their original workspace folder; click a header to collapse it, the checkbox selects the whole workspace",
169
- sessions: "{n} sessions",
170
- workspaces: "{n} workspaces",
171
- selectGroup: "Select this workspace",
172
- clearGroup: "Clear this workspace",
173
- selectAllShort: "Select all",
174
- clearShort: "Clear",
175
- collapse: "Collapse",
176
- expand: "Expand"
177
- };
178
-
179
- // ── 两个模块级小 store(弹窗宿主与设置面板共享) ─────────────────────
180
- function createStore(initial) {
181
- let value = initial;
182
- const listeners = new Set();
183
- return {
184
- get: () => value,
185
- set: (next) => {
186
- value = next;
187
- for (const listener of listeners) listener();
188
- },
189
- subscribe: (listener) => {
190
- listeners.add(listener);
191
- return () => listeners.delete(listener);
192
- }
193
- };
194
- }
195
- function useStore(store) {
196
- const [value, setValue] = React.useState(store.get);
197
- React.useEffect(() => store.subscribe(() => setValue(store.get())), [store]);
198
- return value;
199
- }
200
-
201
- /** 待确认的请求;null 表示没有弹窗。 */
202
- const dialogStore = createStore(null);
203
- /** 每次写操作成功后自增,让设置面板重新拉快照。 */
204
- const mutationStore = createStore(0);
205
- /** "按工作区分组"开关;模块级,切走设置页再回来仍然记得。 */
206
- const groupingStore = createStore(true);
207
-
208
- function requestTrash(ids, title) {
209
- dialogStore.set({ kind: "trash", ids, title });
210
- }
211
- function requestPurge(ids, title) {
212
- dialogStore.set({ kind: "purge", ids, title });
213
- }
214
-
215
- // ── 宿主调用 ────────────────────────────────────────────────────────
216
- let ctxRef = null;
217
-
218
- /** RFC 4122 v4;浏览器在非安全上下文也有 crypto.getRandomValues。 */
219
- function randomUuid() {
220
- const bytes = new Uint8Array(16);
221
- const crypto = globalThis.crypto;
222
- if (crypto && typeof crypto.getRandomValues === "function") crypto.getRandomValues(bytes);
223
- else for (let index = 0; index < bytes.length; index += 1) bytes[index] = Math.floor(Math.random() * 256);
224
- const view = new DataView(bytes.buffer);
225
- view.setUint8(6, (view.getUint8(6) & 15) | 64);
226
- view.setUint8(8, (view.getUint8(8) & 63) | 128);
227
- const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
228
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
229
- }
230
-
231
- /** 端点相对共享通道的目标:`/api/session-curator/<endpoint>`。 */
232
- function target(endpoint) {
233
- return `${ROUTE_PREFIX}/${endpoint}`;
234
- }
235
-
236
- /**
237
- * 手写 Connection 线协议:逐字复刻 `@deepseek-ai/dsh-client-connection/lib/client.js`
238
- * 的 `createWebConnectionRpc().call` —— POST `${channel}/${endpoint}`(去掉前导 `/`,
239
- * 即文档相对路径),body 是 `{type:'client-request', rpcId, method, payload}`,
240
- * 其中 `method` 是**相对通道**的端点(宿主按它核对路由),响应信封是
241
- * `{type:'server-response', rpcId, result:{ok,value|error}}`。
242
- * 这样客户端**不依赖任何 Cordis 服务**;这条 HTTP 路由的信任栅栏与会话鉴权
243
- * 仍然由宿主 Connection 的 `/api` 前缀路由把关。
244
- */
245
- async function rawCall(endpoint, payload) {
246
- const rpcId = randomUuid();
247
- const url = `${API_CHANNEL}/${ROUTE_PREFIX}/${endpoint}`;
248
- const response = await fetch(url.slice(1), {
249
- method: "POST",
250
- headers: { "content-type": "application/json" },
251
- body: JSON.stringify({ type: "client-request", rpcId, method: target(endpoint), payload: payload || {} })
252
- });
253
- if (!response.ok) throw new Error(`HTTP ${response.status}(${url})`);
254
- let envelope;
255
- try {
256
- envelope = await response.json();
257
- } catch {
258
- throw new Error(`响应不是 JSON(${url})`);
259
- }
260
- if (!envelope || envelope.type !== "server-response" || typeof envelope.rpcId !== "string") {
261
- throw new Error("无效的 server-response 信封");
262
- }
263
- if (envelope.rpcId !== rpcId) throw new Error(`rpcId 不匹配(${endpoint})`);
264
- const result = envelope.result;
265
- if (!result || (result.ok !== true && result.ok !== false)) throw new Error("无效的 server-response 结果");
266
- if (!result.ok) throw new Error(`${result.error ? result.error.code : "error"}: ${result.error ? result.error.message : "未知错误"}`);
267
- return result.value;
268
- }
269
-
270
- /** 首选宿主自带的调用器(若客户端真有这个服务),否则退回手写 fetch。 */
271
- async function call(endpoint, payload) {
272
- const connection = ctxRef === null ? null : ctxRef.get("connection");
273
- const rpc = connection ? connection.rpc : null;
274
- if (rpc && typeof rpc.call === "function") {
275
- const result = await rpc.call(API_CHANNEL, target(endpoint), payload || {});
276
- if (!result.ok) throw new Error(`${result.error.code}: ${result.error.message}`);
277
- return result.value;
278
- }
279
- return rawCall(endpoint, payload);
280
- }
281
- function reportError(error) {
282
- const message = error instanceof Error ? error.message : String(error);
283
- if (ctxRef !== null && ctxRef.logger) ctxRef.logger.warn(`[session-curator] ${message}`);
284
- return message;
285
- }
286
- /** 逐项结果里第一条失败原因,用于把宿主拒绝转成一句人话。 */
287
- function firstFailure(results) {
288
- for (const row of results || []) {
289
- if (!row.ok) return row.reason || "操作失败";
290
- }
291
- return null;
292
- }
293
-
294
- // ── 展示辅助 ────────────────────────────────────────────────────────
295
- function formatBytes(bytes) {
296
- if (typeof bytes !== "number" || !Number.isFinite(bytes) || bytes <= 0) return "0 B";
297
- const units = ["B", "KB", "MB", "GB"];
298
- let value = bytes;
299
- let unit = 0;
300
- while (value >= 1024 && unit < units.length - 1) {
301
- value /= 1024;
302
- unit += 1;
303
- }
304
- return `${value >= 10 || unit === 0 ? Math.round(value) : value.toFixed(1)} ${units[unit]}`;
305
- }
306
- function formatTime(ms) {
307
- if (typeof ms !== "number" || ms <= 0) return "";
308
- try {
309
- return new Date(ms).toLocaleString();
310
- } catch {
311
- return "";
312
- }
313
- }
314
- function text(t, key, params) {
315
- let value = t(key);
316
- if (params) {
317
- for (const name of Object.keys(params)) value = value.split(`{${name}}`).join(String(params[name]));
318
- }
319
- return value;
320
- }
321
- function baseName(path) {
322
- if (typeof path !== "string" || path === "") return "";
323
- const parts = path.split(/[\\/]/).filter((part) => part !== "");
324
- return parts.length > 0 ? parts[parts.length - 1] : path;
325
- }
326
-
327
- // ── 按工作区文件夹分组 ──────────────────────────────────────────────
328
- /**
329
- * 分组键 = 会话**原来的工作区文件夹**。
330
- * 优先 cwd(Windows 大小写不敏感、末尾分隔符无关,所以归一化后再比),
331
- * 没有 cwd 时退到 workspaceId,两者都没有就落进"未知目录"那一组。
332
- */
333
- function groupKeyOf(row) {
334
- if (typeof row.cwd === "string" && row.cwd.trim() !== "") {
335
- return `cwd:${row.cwd.trim().replace(/[\\/]+$/, "").replace(/\\/g, "/").toLowerCase()}`;
336
- }
337
- if (typeof row.workspaceId === "string" && row.workspaceId !== "") return `id:${row.workspaceId}`;
338
- return "unknown";
339
- }
340
-
341
- /**
342
- * 分成 `[{ key, title, cwd, bytes, rows[] }]`。组内保持宿主给的顺序
343
- * (回收站本来就是"最近删除在前");组间按文件夹名排序,"未知目录"永远在最后 ——
344
- * 归档 / 取消归档之后列表不会整片跳位。
345
- */
346
- function groupRows(rows, t) {
347
- const groups = new Map();
348
- for (const row of rows) {
349
- const key = groupKeyOf(row);
350
- let group = groups.get(key);
351
- if (group === undefined) {
352
- const cwd = typeof row.cwd === "string" && row.cwd !== "" ? row.cwd : null;
353
- const title = (typeof row.workspaceTitle === "string" && row.workspaceTitle !== "" ? row.workspaceTitle : null)
354
- ?? (cwd === null ? null : baseName(cwd) || cwd)
355
- ?? t("noProject");
356
- group = { key, title, cwd, bytes: 0, rows: [] };
357
- groups.set(key, group);
358
- }
359
- group.rows.push(row);
360
- if (typeof row.bytes === "number" && Number.isFinite(row.bytes)) group.bytes += row.bytes;
361
- }
362
- const list = [...groups.values()];
363
- list.sort((left, right) => {
364
- if (left.key === "unknown" || right.key === "unknown") {
365
- return left.key === right.key ? 0 : left.key === "unknown" ? 1 : -1;
366
- }
367
- return left.title.localeCompare(right.title);
368
- });
369
- return list;
370
- }
371
-
372
- // ── 内联样式(全部走主题 token,不依赖 styles 服务) ────────────────
373
- const S = {
374
- column: { display: "flex", flexDirection: "column", gap: "10px", padding: "4px 0", minWidth: 0 },
375
- rowLine: { display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" },
376
- tabBar: { display: "flex", alignItems: "center", gap: "6px" },
377
- card: {
378
- border: "1px solid var(--dsw-alias-border-l1)",
379
- borderRadius: "10px",
380
- overflow: "hidden"
381
- },
382
- list: { display: "flex", flexDirection: "column" },
383
- item: {
384
- display: "flex",
385
- alignItems: "center",
386
- gap: "10px",
387
- padding: "8px 10px",
388
- borderTop: "1px solid var(--dsw-alias-border-l1)"
389
- },
390
- itemMain: { display: "flex", flexDirection: "column", gap: "2px", minWidth: 0, flex: "1 1 auto" },
391
- itemTitle: {
392
- color: "var(--dsw-alias-label-primary)",
393
- fontSize: "13px",
394
- overflow: "hidden",
395
- textOverflow: "ellipsis",
396
- whiteSpace: "nowrap"
397
- },
398
- itemMeta: {
399
- color: "var(--dsw-alias-label-secondary)",
400
- fontSize: "11px",
401
- overflow: "hidden",
402
- textOverflow: "ellipsis",
403
- whiteSpace: "nowrap"
404
- },
405
- itemActions: { display: "flex", alignItems: "center", gap: "6px", flex: "0 0 auto" },
406
- /** 「⋯」触发器:图标按钮,弱化成次要前景色(跟侧边栏行的图标按钮一致)。 */
407
- rowMenuButton: { color: "var(--dsw-alias-label-secondary)", padding: "0 6px" },
408
- /**
409
- * 文件夹那一行的底色。**不能用 `--dsw-alias-bg-layer-*`**:浅色主题里
410
- * base / layer-1 / layer-2 / layer-3 都指向同一个颜色(`neutral-bluish-00`),
411
- * 铺上去等于没铺(这就是之前"看不出区别"的原因)。
412
- * `interactive-bg-active` 是带 alpha 的叠加色(#2631481a,约 10%),
413
- * 深浅主题都有效;比行 hover 用的 `interactive-bg-hover`(约 6%)略重一点,
414
- * 这样"文件夹条"和"鼠标划过的会话行"不会糊在一起。
415
- */
416
- groupHead: {
417
- display: "flex",
418
- alignItems: "center",
419
- gap: "10px",
420
- padding: "6px 10px",
421
- background: "var(--dsw-alias-interactive-bg-active)",
422
- borderTop: "1px solid var(--dsw-alias-border-l1)"
423
- },
424
- groupToggle: {
425
- display: "flex",
426
- alignItems: "center",
427
- gap: "8px",
428
- minWidth: 0,
429
- flex: "1 1 auto",
430
- background: "transparent",
431
- border: "none",
432
- padding: 0,
433
- margin: 0,
434
- color: "inherit",
435
- font: "inherit",
436
- textAlign: "left",
437
- cursor: "pointer"
438
- },
439
- /** 图标只吃 `size` / `className`,不接 `style`:颜色靠外层 span 的 currentColor 传下去。 */
440
- groupCaret: { color: "var(--dsw-alias-label-tertiary)", flex: "0 0 auto", display: "flex", alignItems: "center" },
441
- groupIcon: { color: "var(--dsw-alias-label-secondary)", flex: "0 0 auto", display: "flex", alignItems: "center" },
442
- groupTitle: {
443
- color: "var(--dsw-alias-label-primary)",
444
- fontSize: "12px",
445
- fontWeight: 600,
446
- overflow: "hidden",
447
- textOverflow: "ellipsis",
448
- whiteSpace: "nowrap",
449
- flex: "0 1 auto"
450
- },
451
- groupMeta: { color: "var(--dsw-alias-label-tertiary)", fontSize: "11px", flex: "0 0 auto" },
452
- groupPath: {
453
- color: "var(--dsw-alias-label-tertiary)",
454
- fontSize: "11px",
455
- overflow: "hidden",
456
- textOverflow: "ellipsis",
457
- whiteSpace: "nowrap",
458
- minWidth: 0,
459
- flex: "0 1 auto"
460
- },
461
- empty: { color: "var(--dsw-alias-label-secondary)", fontSize: "12px", padding: "14px 10px", textAlign: "center" },
462
- error: { color: "var(--dsw-alias-state-error-primary)", fontSize: "12px" },
463
- footer: { color: "var(--dsw-alias-label-secondary)", fontSize: "11px", display: "flex", gap: "10px", flexWrap: "wrap" },
464
- mono: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" },
465
- ack: { display: "flex", alignItems: "center", gap: "8px", fontSize: "12px", color: "var(--dsw-alias-label-primary)" }
466
- };
467
-
468
- // ── 会话行 ──────────────────────────────────────────────────────────
469
- /**
470
- * 一行会话。分组开着时 `showCwd` 为 false —— 文件夹已经写在组标题上,
471
- * 行里不必再重复一遍;平铺时才把它显示回来。
472
- *
473
- * 右侧原来并排 2–3 个文字按钮("看着有点乱"),现在统一成**一个「⋯」菜单**
474
- * (`Menu` + `IconEllipsisOutlineRegular`,与侧边栏会话行的 ⋯ 完全同一套写法):
475
- * 每行的形状一致,动作收进菜单,危险项用 `danger` 标红。
476
- */
477
- function SessionRow(props) {
478
- const row = props.row;
479
- const t = props.t;
480
- const meta = [];
481
- if (props.showCwd) meta.push(row.cwd ? row.cwd : t("noProject"));
482
- if (row.bytes) meta.push(formatBytes(row.bytes));
483
- if (row.files) meta.push(text(t, "files", { n: row.files }));
484
- const time = props.showDeletedAt ? formatTime(row.deletedAt) : formatTime(row.createdAt);
485
- if (time !== "") meta.push(time);
486
-
487
- const menu = props.menu;
488
- return h(
489
- "div",
490
- { style: props.first ? { ...S.item, borderTop: "none" } : S.item },
491
- h("input", {
492
- type: "checkbox",
493
- checked: props.checked,
494
- disabled: props.busy,
495
- "aria-label": row.title,
496
- onChange: () => props.onToggle()
497
- }),
498
- h(
499
- "div",
500
- { style: S.itemMain },
501
- h("div", { style: S.itemTitle, title: row.title }, row.title),
502
- h("div", { style: S.itemMeta, title: row.cwd || "" }, meta.join(" · "))
503
- ),
504
- h(
505
- "div",
506
- { style: S.itemActions },
507
- h(Menu, {
508
- open: props.menuOpen,
509
- portal: true,
510
- side: "bottom",
511
- align: "end",
512
- closeOnPointerLeave: true,
513
- items: menu.items,
514
- onSelect: (id) => menu.run(id),
515
- onClose: () => props.onCloseMenu(),
516
- anchor: h(Button, {
517
- size: "sm",
518
- variant: "ghost",
519
- disabled: props.busy,
520
- style: S.rowMenuButton,
521
- icon: h(IconEllipsisOutlineRegular, {}),
522
- "aria-label": `${row.title} — ${t("rowActions")}`,
523
- onClick: (event) => {
524
- event.stopPropagation();
525
- props.onToggleMenu();
526
- }
527
- })
528
- })
529
- )
530
- );
531
- }
532
-
533
- /** 一个工作区分组的标题行:左边(箭头 + 文件夹图标 + 名字)折叠 / 展开,右边一个小按钮选整组。 */
534
- function GroupHeader(props) {
535
- const group = props.group;
536
- const t = props.t;
537
- const total = group.rows.length;
538
- const allSelected = total > 0 && group.rows.every((row) => props.selected.includes(row.sessionId));
539
- const action = allSelected ? t("clearShort") : t("selectAllShort");
540
- return h(
541
- "div",
542
- { style: props.first ? { ...S.groupHead, borderTop: "none" } : S.groupHead },
543
- h(
544
- "button",
545
- {
546
- type: "button",
547
- style: S.groupToggle,
548
- title: props.collapsed ? t("expand") : t("collapse"),
549
- onClick: () => props.onToggleCollapse()
550
- },
551
- // 箭头与文件夹图标都用 DSH 自带图标(跟侧边栏的项目行同款,展开 = 打开的文件夹)。
552
- h("span", { style: S.groupCaret }, props.collapsed
553
- ? h(IconChevronRightOutlineRegular, { size: 12 })
554
- : h(IconChevronDownOutlineRegular, { size: 12 })),
555
- h("span", { style: S.groupIcon }, props.collapsed
556
- ? h(IconFolderCloseRegular, { size: 14 })
557
- : h(IconFolderOpenRegular, { size: 14 })),
558
- h("span", { style: S.groupTitle, title: group.cwd || group.title }, group.title),
559
- h("span", { style: S.groupMeta }, text(t, "sessions", { n: total })),
560
- group.bytes > 0 ? h("span", { style: S.groupMeta }, formatBytes(group.bytes)) : null,
561
- group.cwd !== null ? h("span", { style: { ...S.groupPath, ...S.mono }, title: group.cwd }, group.cwd) : null
562
- ),
563
- h(Button, {
564
- size: "sm",
565
- variant: "ghost",
566
- disabled: props.busy,
567
- title: `${group.title} — ${allSelected ? t("clearGroup") : t("selectGroup")}`,
568
- onClick: () => props.onToggleGroup()
569
- }, action)
570
- );
571
- }
572
-
573
- // ── 设置面板 ────────────────────────────────────────────────────────
574
- function SessionManagerPanel(props) {
575
- const t = props.t;
576
- const [tab, setTab] = React.useState("archived");
577
- const [snapshot, setSnapshot] = React.useState(null);
578
- const [error, setError] = React.useState(null);
579
- const [loading, setLoading] = React.useState(true);
580
- const [busy, setBusy] = React.useState(false);
581
- const [selected, setSelected] = React.useState([]);
582
- // 折叠的分组键(`<tab>:<groupKey>`),按页签各记各的。
583
- const [collapsed, setCollapsed] = React.useState([]);
584
- // 同一时刻只允许一个行的「⋯」菜单开着:存 sessionId。
585
- const [menuFor, setMenuFor] = React.useState(null);
586
- const grouping = useStore(groupingStore);
587
- const revision = useStore(mutationStore);
588
-
589
- const applyResult = (value) => {
590
- if (value && value.archived) setSnapshot(value);
591
- };
592
-
593
- const refresh = React.useCallback(async () => {
594
- setLoading(true);
595
- try {
596
- applyResult(await call("snapshot", {}));
597
- setError(null);
598
- } catch (failure) {
599
- setError(reportError(failure));
600
- } finally {
601
- setLoading(false);
602
- }
603
- }, []);
604
-
605
- React.useEffect(() => {
606
- void refresh();
607
- }, [refresh, revision]);
608
-
609
- // 换页签时清空选择,避免把上一个页签的 id 带到下一个页签的操作里。
610
- React.useEffect(() => {
611
- setSelected([]);
612
- setMenuFor(null);
613
- }, [tab]);
614
-
615
- const run = async (endpoint, ids, payload) => {
616
- if (ids.length === 0) return;
617
- setBusy(true);
618
- try {
619
- const value = await call(endpoint, { ids, ...(payload || {}) });
620
- applyResult(value);
621
- setError(firstFailure(value.results));
622
- setSelected([]);
623
- } catch (failure) {
624
- setError(reportError(failure));
625
- } finally {
626
- setBusy(false);
627
- }
628
- };
629
-
630
- const toggle = (sessionId) => {
631
- setSelected((current) => current.includes(sessionId) ? current.filter((id) => id !== sessionId) : current.concat([sessionId]));
632
- };
633
-
634
- /** 整组一起选 / 取消选:组内全选就整组取消,否则补齐。 */
635
- const toggleGroup = (group) => {
636
- const ids = group.rows.map((row) => row.sessionId);
637
- const all = ids.every((id) => selected.includes(id));
638
- setSelected((current) => all
639
- ? current.filter((id) => !ids.includes(id))
640
- : [...new Set(current.concat(ids))]);
641
- };
642
-
643
- const archived = snapshot ? snapshot.archived : [];
644
- const pinned = snapshot ? snapshot.pinned : [];
645
- const trashed = snapshot ? snapshot.trashed : [];
646
- const rows = tab === "archived" ? archived : tab === "pinned" ? pinned : trashed;
647
- const selectedRows = rows.filter((row) => selected.includes(row.sessionId));
648
-
649
- // 分组只是显示层:选择、批量操作、计数仍然按整个页签的行来算。
650
- const groups = grouping ? groupRows(rows, t) : [];
651
- const blockOf = (group) => {
652
- const key = `${tab}:${group.key}`;
653
- return { key, group, rows: collapsed.includes(key) ? [] : group.rows };
654
- };
655
- const blocks = grouping ? groups.map(blockOf) : [{ key: `${tab}:flat`, group: null, rows }];
656
-
657
- const tabs = [
658
- { id: "archived", label: t("titleArchived"), count: archived.length },
659
- { id: "pinned", label: t("titlePinned"), count: pinned.length },
660
- { id: "trash", label: t("titleTrash"), count: trashed.length }
661
- ];
662
-
663
- /**
664
- * 一行的「⋯」菜单内容。每个页签的动作本来就不同,但**控件形状统一**:
665
- * 都是一行会话 + 一个 ⋯。
666
- *
667
- * 注意已归档那一组的第三项:以前是"永久删除",可它走的是 `purge`,而 `purge`
668
- * 只认回收站里的目录 —— 归档会话不在回收站,所以那个按钮**从来只会报
669
- * "回收站里没有这个会话"**。现在改成真正可用的"移入回收站"(`trash`:先取消归档
670
- * 再搬走,随时能恢复)。
671
- */
672
- const rowMenu = (row) => {
673
- const moveToTrash = {
674
- id: "trash",
675
- label: t("moveToTrash"),
676
- icon: h(IconTrashOutlineRegular, { size: 14 }),
677
- danger: true
678
- };
679
- if (tab === "archived") {
680
- return {
681
- items: [
682
- { id: "unarchive", label: t("unarchive"), icon: h(IconUnarchiveOutlineRegular, { size: 14 }) },
683
- { id: "pin", label: t("pin"), icon: h(IconPinOutlineRegular, { size: 14 }) },
684
- moveToTrash
685
- ],
686
- run: (id) => {
687
- setMenuFor(null);
688
- if (id === "trash") requestTrash([row.sessionId], row.title);
689
- else void run(id, [row.sessionId]);
690
- }
691
- };
692
- }
693
- if (tab === "pinned") {
694
- return {
695
- items: [
696
- { id: "unpin", label: t("unpin"), icon: h(IconPinOutlineRegular, { size: 14 }) },
697
- moveToTrash
698
- ],
699
- run: (id) => {
700
- setMenuFor(null);
701
- if (id === "trash") requestTrash([row.sessionId], row.title);
702
- else void run(id, [row.sessionId]);
703
- }
704
- };
705
- }
706
- return {
707
- items: [
708
- { id: "restore", label: t("restore"), icon: h(IconRefreshOutlineRegular, { size: 14 }) },
709
- { id: "purge", label: t("purge"), icon: h(IconTrashOutlineRegular, { size: 14 }), danger: true }
710
- ],
711
- run: (id) => {
712
- setMenuFor(null);
713
- if (id === "purge") requestPurge([row.sessionId], row.title);
714
- else void run(id, [row.sessionId]);
715
- }
716
- };
717
- };
718
-
719
- const batch = [];
720
- if (tab === "archived" && selected.length > 0) {
721
- batch.push({ label: t("unarchive"), run: () => void run("unarchive", selected) });
722
- batch.push({ label: t("moveToTrash"), variant: "outline", run: () => requestTrash(selected, selectedRows[0] ? selectedRows[0].title : "") });
723
- }
724
- if (tab === "pinned" && selected.length > 0) {
725
- batch.push({ label: t("unpin"), run: () => void run("unpin", selected) });
726
- batch.push({ label: t("moveToTrash"), variant: "outline", run: () => requestTrash(selected, selectedRows[0] ? selectedRows[0].title : "") });
727
- }
728
- if (tab === "trash" && selected.length > 0) {
729
- batch.push({ label: t("restore"), run: () => void run("restore", selected) });
730
- batch.push({ label: t("purge"), variant: "outline", run: () => requestPurge(selected, selectedRows[0] ? selectedRows[0].title : "") });
731
- }
732
-
733
- const totals = snapshot ? snapshot.totals : null;
734
- const totalBytes = tab === "archived"
735
- ? (totals ? totals.archivedBytes : 0)
736
- : tab === "trash"
737
- ? (totals ? totals.trashBytes : 0)
738
- : 0;
739
-
740
- // 卡片里的显示节点:分组标题 + 行。第一个节点不要上边框(卡片自己有)。
741
- const nodes = [];
742
- let first = true;
743
- for (const block of blocks) {
744
- if (block.group !== null) {
745
- const group = block.group;
746
- const isCollapsed = collapsed.includes(block.key);
747
- nodes.push(h(GroupHeader, {
748
- key: `group:${block.key}`,
749
- group,
750
- t,
751
- first,
752
- busy,
753
- selected,
754
- collapsed: isCollapsed,
755
- onToggleGroup: () => toggleGroup(group),
756
- onToggleCollapse: () => setCollapsed((current) => current.includes(block.key)
757
- ? current.filter((item) => item !== block.key)
758
- : current.concat([block.key]))
759
- }));
760
- first = false;
761
- }
762
- for (const row of block.rows) {
763
- nodes.push(h(SessionRow, {
764
- key: row.sessionId,
765
- row,
766
- t,
767
- first,
768
- busy,
769
- showCwd: block.group === null,
770
- showDeletedAt: tab === "trash",
771
- checked: selected.includes(row.sessionId),
772
- onToggle: () => toggle(row.sessionId),
773
- menu: rowMenu(row),
774
- menuOpen: menuFor === row.sessionId,
775
- onToggleMenu: () => setMenuFor(menuFor === row.sessionId ? null : row.sessionId),
776
- onCloseMenu: () => setMenuFor(null)
777
- }));
778
- first = false;
779
- }
780
- }
781
-
782
- return h(
783
- "div",
784
- { style: S.column },
785
- h(
786
- "div",
787
- { style: S.rowLine },
788
- h(
789
- "div",
790
- { style: S.tabBar },
791
- tabs.map((entry) => h(Button, {
792
- key: entry.id,
793
- size: "sm",
794
- variant: tab === entry.id ? "primary" : "ghost",
795
- onClick: () => setTab(entry.id)
796
- }, `${entry.label}${entry.count > 0 ? ` (${entry.count})` : ""}`))
797
- ),
798
- h(Button, {
799
- size: "sm",
800
- variant: "ghost",
801
- title: t("groupHint"),
802
- onClick: () => groupingStore.set(!grouping)
803
- }, grouping ? t("groupingOn") : t("groupingOff")),
804
- h(Button, { size: "sm", variant: "ghost", disabled: loading || busy, onClick: () => void refresh() }, t("refresh"))
805
- ),
806
-
807
- error ? h("div", { style: S.error }, error) : null,
37
+ // src/client/index.ts
38
+ var index_exports = {};
39
+ __export(index_exports, {
40
+ GroupHeader: () => GroupHeader,
41
+ NS: () => NS,
42
+ SessionRow: () => SessionRow,
43
+ apply: () => apply,
44
+ collapseKey: () => collapseKey,
45
+ groupKeyOf: () => groupKeyOf,
46
+ groupRows: () => groupRows,
47
+ inject: () => inject,
48
+ isGroupCollapsed: () => isGroupCollapsed,
49
+ sectionIdFor: () => sectionIdFor,
50
+ toggledCollapse: () => toggledCollapse
51
+ });
52
+ module.exports = __toCommonJS(index_exports);
53
+ var React6 = __toESM(require("react"), 1);
54
+ var import_dsh_client_ui_primitives5 = require("@deepseek-ai/dsh-client-ui-primitives");
808
55
 
809
- h(
810
- "div",
811
- { style: S.card },
812
- loading && !snapshot
813
- ? h("div", { style: S.empty }, t("loading"))
814
- : rows.length === 0
815
- ? h("div", { style: S.empty }, tab === "archived" ? t("emptyArchived") : tab === "pinned" ? t("emptyPinned") : t("emptyTrash"))
816
- : h("div", { style: S.list }, nodes)
817
- ),
56
+ // src/client/components/dialog-host.ts
57
+ var React2 = __toESM(require("react"), 1);
58
+ var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
818
59
 
819
- h(
820
- "div",
821
- { style: S.rowLine },
822
- h(Button, {
823
- size: "sm",
824
- variant: "ghost",
825
- disabled: busy || rows.length === 0,
826
- onClick: () => setSelected(selected.length === rows.length ? [] : rows.map((row) => row.sessionId))
827
- }, selected.length === rows.length && rows.length > 0 ? t("selectNone") : t("selectAll")),
828
- batch.map((action) => h(Button, {
829
- key: action.label,
830
- size: "sm",
831
- variant: action.variant || "ghost",
832
- disabled: busy,
833
- onClick: action.run
834
- }, action.label)),
835
- tab === "trash" && trashed.length > 0
836
- ? h(Button, {
837
- size: "sm",
838
- variant: "outline",
839
- disabled: busy,
840
- onClick: () => requestPurge(trashed.map((row) => row.sessionId), "")
841
- }, t("emptyTrashAll"))
842
- : null,
843
- busy ? h("span", { style: S.footer }, t("working")) : null
844
- ),
60
+ // src/client/runtime.ts
61
+ var current = null;
62
+ function setContext(ctx) {
63
+ current = ctx;
64
+ }
65
+ function connectionRpc() {
66
+ const ctx = current;
67
+ if (ctx === null) return null;
68
+ const connection = ctx.get("connection");
69
+ const rpc = connection === null || connection === void 0 ? null : connection.rpc;
70
+ return rpc && typeof rpc.call === "function" ? rpc : null;
71
+ }
72
+ function ctxLogger() {
73
+ const ctx = current;
74
+ return ctx !== null && ctx.logger ? ctx.logger : null;
75
+ }
76
+ function reportError(error) {
77
+ const message = error instanceof Error ? error.message : String(error);
78
+ const logger = ctxLogger();
79
+ if (logger) logger.warn(`[session-curator] ${message}`);
80
+ return message;
81
+ }
845
82
 
846
- h(
847
- "div",
848
- { style: S.footer },
849
- h("span", null, text(t, "total", { n: rows.length, size: formatBytes(totalBytes) })),
850
- grouping && groups.length > 0 ? h("span", null, text(t, "workspaces", { n: groups.length })) : null,
851
- selected.length > 0 ? h("span", null, text(t, "selected", { n: selected.length })) : null,
852
- snapshot ? h("span", { style: S.mono, title: snapshot.trashDir }, snapshot.trashDir) : null
853
- )
854
- );
855
- }
83
+ // src/client/api.ts
84
+ var API_CHANNEL = "/api";
85
+ var ROUTE_PREFIX = "session-curator";
86
+ function randomUuid() {
87
+ const bytes = new Uint8Array(16);
88
+ const crypto = globalThis.crypto;
89
+ if (crypto && typeof crypto.getRandomValues === "function") crypto.getRandomValues(bytes);
90
+ else for (let index = 0; index < bytes.length; index += 1) bytes[index] = Math.floor(Math.random() * 256);
91
+ const view = new DataView(bytes.buffer);
92
+ view.setUint8(6, view.getUint8(6) & 15 | 64);
93
+ view.setUint8(8, view.getUint8(8) & 63 | 128);
94
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
95
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
96
+ }
97
+ function target(endpoint) {
98
+ return `${ROUTE_PREFIX}/${endpoint}`;
99
+ }
100
+ async function rawCall(endpoint, payload) {
101
+ const rpcId = randomUuid();
102
+ const url = `${API_CHANNEL}/${ROUTE_PREFIX}/${endpoint}`;
103
+ const response = await fetch(url.slice(1), {
104
+ method: "POST",
105
+ headers: { "content-type": "application/json" },
106
+ body: JSON.stringify({ type: "client-request", rpcId, method: target(endpoint), payload: payload || {} })
107
+ });
108
+ if (!response.ok) throw new Error(`HTTP ${response.status}(${url})`);
109
+ let envelope;
110
+ try {
111
+ envelope = await response.json();
112
+ } catch {
113
+ throw new Error(`响应不是 JSON(${url})`);
114
+ }
115
+ const message = envelope;
116
+ if (!message || message.type !== "server-response" || typeof message.rpcId !== "string") {
117
+ throw new Error("无效的 server-response 信封");
118
+ }
119
+ if (message.rpcId !== rpcId) throw new Error(`rpcId 不匹配(${endpoint})`);
120
+ const result = message.result;
121
+ if (!result || result.ok !== true && result.ok !== false) throw new Error("无效的 server-response 结果");
122
+ if (result.ok !== true) {
123
+ throw new Error(`${result.error ? result.error.code : "error"}: ${result.error ? result.error.message : "未知错误"}`);
124
+ }
125
+ return result.value;
126
+ }
127
+ async function call(endpoint, payload) {
128
+ const rpc = connectionRpc();
129
+ if (rpc !== null) {
130
+ const result = await rpc.call(API_CHANNEL, target(endpoint), payload || {});
131
+ if (!result.ok) throw new Error(`${result.error.code}: ${result.error.message}`);
132
+ return result.value;
133
+ }
134
+ return await rawCall(endpoint, payload);
135
+ }
136
+ function firstFailure(results) {
137
+ for (const row of results || []) {
138
+ if (!row.ok) return row.reason || "操作失败";
139
+ }
140
+ return null;
141
+ }
856
142
 
857
- // ── 全局确认弹窗宿主(挂在 shell.overlay 上) ────────────────────────
858
- function DialogHost(props) {
859
- const t = props.t;
860
- const request = useStore(dialogStore);
861
- const [busy, setBusy] = React.useState(false);
862
- const [ack, setAck] = React.useState(false);
863
- const [error, setError] = React.useState(null);
143
+ // src/client/stores.ts
144
+ var React = __toESM(require("react"), 1);
145
+ function createStore(initial) {
146
+ let value = initial;
147
+ const listeners = /* @__PURE__ */ new Set();
148
+ return {
149
+ get: () => value,
150
+ set: (next) => {
151
+ value = next;
152
+ for (const listener of listeners) listener();
153
+ },
154
+ subscribe: (listener) => {
155
+ listeners.add(listener);
156
+ return () => {
157
+ listeners.delete(listener);
158
+ };
159
+ }
160
+ };
161
+ }
162
+ function useStore(store) {
163
+ const [value, setValue] = React.useState(store.get);
164
+ React.useEffect(() => store.subscribe(() => setValue(store.get())), [store]);
165
+ return value;
166
+ }
864
167
 
865
- // 每次新请求都重置勾选与错误(hooks 先于条件渲染,保持调用顺序稳定)。
866
- const requestKey = request === null ? "" : `${request.kind}:${request.ids.join(",")}:${request.title}`;
867
- React.useEffect(() => {
868
- setAck(false);
869
- setError(null);
870
- }, [requestKey]);
168
+ // src/client/dialog.ts
169
+ var dialogStore = createStore(null);
170
+ var mutationStore = createStore(0);
171
+ function requestTrash(ids, title) {
172
+ dialogStore.set({ kind: "trash", ids, title });
173
+ }
174
+ function requestPurge(ids, title) {
175
+ dialogStore.set({ kind: "purge", ids, title });
176
+ }
871
177
 
872
- if (request === null) return null;
178
+ // src/client/i18n.ts
179
+ var NS = "session-curator";
180
+ var zh = {
181
+ sectionLabel: "会话管理",
182
+ menuDelete: "删除会话",
183
+ titleArchived: "已归档",
184
+ titlePinned: "已置顶",
185
+ titleTrash: "回收站",
186
+ refresh: "刷新",
187
+ loading: "读取中…",
188
+ retry: "重试",
189
+ emptyArchived: "还没有归档的会话。在会话行的「⋯」菜单里归档后会出现在这里。",
190
+ emptyPinned: "没有置顶的会话。",
191
+ emptyTrash: "回收站是空的。",
192
+ unarchive: "取消归档",
193
+ pin: "置顶",
194
+ unpin: "取消置顶",
195
+ restore: "恢复",
196
+ purge: "永久删除",
197
+ moveToTrash: "移入回收站",
198
+ rowActions: "更多操作",
199
+ selectAll: "全选",
200
+ selectNone: "取消选择",
201
+ selected: "已选 {n} 项",
202
+ emptyTrashAll: "清空回收站",
203
+ total: "合计 {n} 个会话 · {size}",
204
+ working: "处理中…",
205
+ sessionRunning: "会话正在运行,先停止它再删除。",
206
+ confirmTrashTitle: "删除会话",
207
+ confirmTrashBody: "「{title}」会被移到回收站,之后可以在「设置 → 会话管理」里恢复。",
208
+ confirmTrashAction: "移到回收站",
209
+ confirmPurgeTitle: "永久删除",
210
+ confirmPurgeBody: "「{title}」将从回收站永久删除,日志文件会被彻底移除,无法恢复。",
211
+ confirmPurgeManyBody: "选中的 {n} 个会话将从回收站永久删除,无法恢复。",
212
+ confirmPurgeAction: "永久删除",
213
+ ackPurge: "我明白这无法撤销",
214
+ cancel: "取消",
215
+ close: "关闭",
216
+ noProject: "(未知目录)",
217
+ files: "{n} 个文件",
218
+ groupingOn: "分组:工作区",
219
+ groupingOff: "分组:平铺",
220
+ groupHint: "按会话原来的工作区文件夹归类;点分组标题折叠,左边的勾选框选中整个工作区",
221
+ sessions: "{n} 个会话",
222
+ workspaces: "{n} 个工作区",
223
+ selectGroup: "全选该工作区",
224
+ clearGroup: "取消选中该工作区",
225
+ selectAllShort: "全选",
226
+ clearShort: "取消",
227
+ collapse: "折叠",
228
+ expand: "展开",
229
+ titleConfig: "配置",
230
+ configTitle: "基础设置",
231
+ configTab: "默认页签",
232
+ configTabHint: "打开这里时先显示哪个页签",
233
+ configGrouping: "按工作区分组",
234
+ configGroupingHint: "关掉就是平铺列表;顶部的「分组」按钮改的是同一个值",
235
+ configGroupDefault: "分组默认状态",
236
+ configGroupDefaultHint: "以后打开的文件夹默认是折叠还是展开;手动点过的文件夹保留手动状态",
237
+ groupExpanded: "展开",
238
+ groupCollapsed: "折叠",
239
+ configSidebarArchived: "侧边栏显示已归档会话",
240
+ configSidebarArchivedHint: "对应侧边栏「视图选项」里的「隐藏已归档 / 全部对话(显示已归档)」;存的是官方那份设置,改完要刷新页面才生效",
241
+ configSidebarRefreshHint: "已写入官方设置,但侧边栏要刷新页面才会跟着变。",
242
+ refreshPage: "刷新页面",
243
+ configMeta: "行内显示体积与文件数",
244
+ configMetaHint: "关掉后每行只留标题(平铺时仍会显示文件夹)",
245
+ configOrder: "排序",
246
+ configOrderHint: "原顺序就是宿主给的顺序(回收站里是最近删除在前)",
247
+ orderHost: "原顺序",
248
+ orderTitle: "标题",
249
+ orderBytes: "体积",
250
+ configAutoUpdate: "自动检查更新",
251
+ configAutoUpdateHint: "打开这个页面时问一次 npm registry(10 分钟内有缓存)",
252
+ configReset: "恢复默认",
253
+ configResetDone: "已恢复默认设置",
254
+ configSaved: "已保存",
255
+ aboutTitle: "关于",
256
+ versionLabel: "版本",
257
+ updateChecking: "正在检查更新…",
258
+ updateLatest: "已是最新版本",
259
+ updateAvailable: "有新版本 {latest}(当前 {current})",
260
+ updateFailed: "检查更新失败:{reason}",
261
+ updateRetry: "重新检查",
262
+ updateCheckNow: "检查更新",
263
+ updateStepsTitle: "升级步骤",
264
+ updateStepOne: "在终端里升级包(按你自己的 profile 名替换 desktop):",
265
+ updateStepTwo: "重启 DSH 让宿主半区生效;只改了客户端半区的话刷新页面就够。",
266
+ updateOpen: "打开 npm 页面"
267
+ };
268
+ var en = {
269
+ sectionLabel: "Session manager",
270
+ menuDelete: "Delete session",
271
+ titleArchived: "Archived",
272
+ titlePinned: "Pinned",
273
+ titleTrash: "Trash",
274
+ refresh: "Refresh",
275
+ loading: "Loading…",
276
+ retry: "Retry",
277
+ emptyArchived: "No archived sessions yet. Archive one from its “⋯” menu and it shows up here.",
278
+ emptyPinned: "No pinned sessions.",
279
+ emptyTrash: "The trash is empty.",
280
+ unarchive: "Unarchive",
281
+ pin: "Pin",
282
+ unpin: "Unpin",
283
+ restore: "Restore",
284
+ purge: "Delete permanently",
285
+ moveToTrash: "Move to trash",
286
+ rowActions: "More actions",
287
+ selectAll: "Select all",
288
+ selectNone: "Clear selection",
289
+ selected: "{n} selected",
290
+ emptyTrashAll: "Empty trash",
291
+ total: "{n} sessions · {size}",
292
+ working: "Working…",
293
+ sessionRunning: "This session is running; stop it before deleting.",
294
+ confirmTrashTitle: "Delete session",
295
+ confirmTrashBody: "“{title}” moves to the trash. You can restore it later under Settings → Session manager.",
296
+ confirmTrashAction: "Move to trash",
297
+ confirmPurgeTitle: "Delete permanently",
298
+ confirmPurgeBody: "“{title}” is removed from the trash for good; its log files are gone and cannot be recovered.",
299
+ confirmPurgeManyBody: "{n} selected sessions are removed from the trash for good and cannot be recovered.",
300
+ confirmPurgeAction: "Delete permanently",
301
+ ackPurge: "I understand this cannot be undone",
302
+ cancel: "Cancel",
303
+ close: "Close",
304
+ noProject: "(unknown directory)",
305
+ files: "{n} files",
306
+ groupingOn: "Grouped: workspace",
307
+ groupingOff: "Grouped: off",
308
+ groupHint: "Group sessions under their original workspace folder; click a header to collapse it, the checkbox selects the whole workspace",
309
+ sessions: "{n} sessions",
310
+ workspaces: "{n} workspaces",
311
+ selectGroup: "Select this workspace",
312
+ clearGroup: "Clear this workspace",
313
+ selectAllShort: "Select all",
314
+ clearShort: "Clear",
315
+ collapse: "Collapse",
316
+ expand: "Expand",
317
+ titleConfig: "Settings",
318
+ configTitle: "Basics",
319
+ configTab: "Default tab",
320
+ configTabHint: "Which tab the panel opens on",
321
+ configGrouping: "Group by workspace",
322
+ configGroupingHint: "Off renders a flat list; the top “Grouped” button edits the same value",
323
+ configGroupDefault: "Default folder state",
324
+ configGroupDefaultHint: "Whether folders start collapsed or expanded; folders you toggled keep their manual state",
325
+ groupExpanded: "Expanded",
326
+ groupCollapsed: "Collapsed",
327
+ configSidebarArchived: "Show archived sessions in the sidebar",
328
+ configSidebarArchivedHint: "Mirrors “Hide archived / All conversations” in the sidebar's view options; it is the official setting, so a page refresh applies it",
329
+ configSidebarRefreshHint: "Written to the official setting — the sidebar only follows it after a page refresh.",
330
+ refreshPage: "Refresh page",
331
+ configMeta: "Show size and file count per row",
332
+ configMetaHint: "Off leaves only the title (the folder still shows in the flat list)",
333
+ configOrder: "Order",
334
+ configOrderHint: "Host order is what the host sent (the trash is most-recently-deleted first)",
335
+ orderHost: "Host",
336
+ orderTitle: "Title",
337
+ orderBytes: "Size",
338
+ configAutoUpdate: "Check for updates automatically",
339
+ configAutoUpdateHint: "Asks the npm registry when this page opens (cached for 10 minutes)",
340
+ configReset: "Reset to defaults",
341
+ configResetDone: "Defaults restored",
342
+ configSaved: "Saved",
343
+ aboutTitle: "About",
344
+ versionLabel: "Version",
345
+ updateChecking: "Checking for updates…",
346
+ updateLatest: "You are on the latest version",
347
+ updateAvailable: "Version {latest} is available (you have {current})",
348
+ updateFailed: "Update check failed: {reason}",
349
+ updateRetry: "Check again",
350
+ updateCheckNow: "Check for updates",
351
+ updateStepsTitle: "How to upgrade",
352
+ updateStepOne: "Upgrade the package in a terminal (replace desktop with your profile name):",
353
+ updateStepTwo: "Restart DSH so the host half reloads; a client-half change only needs a page refresh.",
354
+ updateOpen: "Open the npm page"
355
+ };
356
+ var dictionaries = { zh, en };
357
+ function text(t, key, params) {
358
+ let value = t(key);
359
+ if (params) {
360
+ for (const name of Object.keys(params)) value = value.split(`{${name}}`).join(String(params[name]));
361
+ }
362
+ return value;
363
+ }
364
+ var TAB_LABEL = {
365
+ archived: "titleArchived",
366
+ pinned: "titlePinned",
367
+ trash: "titleTrash"
368
+ };
873
369
 
874
- const purge = request.kind === "purge";
875
- const many = request.ids.length > 1;
876
- const title = purge ? t("confirmPurgeTitle") : t("confirmTrashTitle");
877
- const body = purge
878
- ? (many ? text(t, "confirmPurgeManyBody", { n: request.ids.length }) : text(t, "confirmPurgeBody", { title: request.title }))
879
- : text(t, "confirmTrashBody", { title: request.title });
370
+ // src/client/styles.ts
371
+ var S = {
372
+ column: { display: "flex", flexDirection: "column", gap: "10px", padding: "4px 0", minWidth: 0 },
373
+ rowLine: { display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" },
374
+ tabBar: { display: "flex", alignItems: "center", gap: "6px" },
375
+ card: {
376
+ border: "1px solid var(--dsw-alias-border-l1)",
377
+ borderRadius: "10px",
378
+ overflow: "hidden"
379
+ },
380
+ list: { display: "flex", flexDirection: "column" },
381
+ item: {
382
+ display: "flex",
383
+ alignItems: "center",
384
+ gap: "10px",
385
+ padding: "8px 10px",
386
+ borderTop: "1px solid var(--dsw-alias-border-l1)"
387
+ },
388
+ itemMain: { display: "flex", flexDirection: "column", gap: "2px", minWidth: 0, flex: "1 1 auto" },
389
+ itemTitle: {
390
+ color: "var(--dsw-alias-label-primary)",
391
+ fontSize: "13px",
392
+ overflow: "hidden",
393
+ textOverflow: "ellipsis",
394
+ whiteSpace: "nowrap"
395
+ },
396
+ itemMeta: {
397
+ color: "var(--dsw-alias-label-secondary)",
398
+ fontSize: "11px",
399
+ overflow: "hidden",
400
+ textOverflow: "ellipsis",
401
+ whiteSpace: "nowrap"
402
+ },
403
+ itemActions: { display: "flex", alignItems: "center", gap: "6px", flex: "0 0 auto" },
404
+ /**
405
+ * 「⋯」触发器:图标按钮,弱化成次要前景色(跟侧边栏行的图标按钮一致)。
406
+ * `flex: 0 0 auto` 防"长标题挤坏按钮":行内标题再长也只能挤 `itemMain`
407
+ *(那边有 minWidth: 0 + 省略号),别让收缩落到这个按钮上。
408
+ */
409
+ rowMenuButton: { color: "var(--dsw-alias-label-secondary)", padding: "0 6px", flex: "0 0 auto" },
410
+ /**
411
+ * 文件夹那一行的底色。**不能用 `--dsw-alias-bg-layer-*`**:浅色主题里
412
+ * base / layer-1 / layer-2 / layer-3 都指向同一个颜色(`neutral-bluish-00`),
413
+ * 铺上去等于没铺(这就是之前"看不出区别"的原因)。
414
+ * `interactive-bg-active` 是带 alpha 的叠加色(#2631481a,约 10%),
415
+ * 深浅主题都有效;比行 hover 用的 `interactive-bg-hover`(约 6%)略重一点,
416
+ * 这样"文件夹条"和"鼠标划过的会话行"不会糊在一起。
417
+ */
418
+ groupHead: {
419
+ display: "flex",
420
+ alignItems: "center",
421
+ gap: "10px",
422
+ padding: "6px 10px",
423
+ background: "var(--dsw-alias-interactive-bg-active)",
424
+ borderTop: "1px solid var(--dsw-alias-border-l1)"
425
+ },
426
+ groupToggle: {
427
+ display: "flex",
428
+ alignItems: "center",
429
+ gap: "8px",
430
+ minWidth: 0,
431
+ flex: "1 1 auto",
432
+ background: "transparent",
433
+ border: "none",
434
+ padding: 0,
435
+ margin: 0,
436
+ color: "inherit",
437
+ font: "inherit",
438
+ textAlign: "left",
439
+ cursor: "pointer"
440
+ },
441
+ groupCaret: { color: "var(--dsw-alias-label-tertiary)", flex: "0 0 auto", display: "flex", alignItems: "center" },
442
+ groupIcon: { color: "var(--dsw-alias-label-secondary)", flex: "0 0 auto", display: "flex", alignItems: "center" },
443
+ groupTitle: {
444
+ color: "var(--dsw-alias-label-primary)",
445
+ fontSize: "12px",
446
+ fontWeight: 600,
447
+ overflow: "hidden",
448
+ textOverflow: "ellipsis",
449
+ whiteSpace: "nowrap",
450
+ flex: "0 1 auto"
451
+ },
452
+ groupMeta: { color: "var(--dsw-alias-label-tertiary)", fontSize: "11px", flex: "0 0 auto" },
453
+ /**
454
+ * 分组标题右侧的「全选 / 取消」。
455
+ *
456
+ * 为什么必须写这两条:路径一长,`groupHead` 这行必然溢出,浏览器按 flex 基准**按比例**
457
+ * 分配收缩量 —— 右侧按钮也会被挤窄。而 DSH 的 `Button` 是 `display: inline-flex`
458
+ * 且**没有 `white-space: nowrap`**,窄到一定程度「全选」两个字就在按钮里折成两行。
459
+ * 让它完全不参与收缩(`0 0 auto`)、再兜一条 `nowrap`,收缩就只剩左边的折叠按钮承担:
460
+ * 标题 / 路径各自 `minWidth: 0` + 省略号,该截断的地方截断。
461
+ */
462
+ groupAction: { flex: "0 0 auto", whiteSpace: "nowrap" },
463
+ groupPath: {
464
+ color: "var(--dsw-alias-label-tertiary)",
465
+ fontSize: "11px",
466
+ overflow: "hidden",
467
+ textOverflow: "ellipsis",
468
+ whiteSpace: "nowrap",
469
+ minWidth: 0,
470
+ flex: "0 1 auto"
471
+ },
472
+ empty: { color: "var(--dsw-alias-label-secondary)", fontSize: "12px", padding: "14px 10px", textAlign: "center" },
473
+ error: { color: "var(--dsw-alias-state-error-primary)", fontSize: "12px" },
474
+ footer: { color: "var(--dsw-alias-label-secondary)", fontSize: "11px", display: "flex", gap: "10px", flexWrap: "wrap" },
475
+ mono: { fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" },
476
+ ack: { display: "flex", alignItems: "center", gap: "8px", fontSize: "12px", color: "var(--dsw-alias-label-primary)" },
477
+ /** 顶部条:页签在左,动作靠 `marginInlineStart: auto` 推到右边。 */
478
+ header: { display: "flex", alignItems: "center", gap: "8px", flexWrap: "wrap" },
479
+ headerActions: { display: "flex", alignItems: "center", gap: "8px", marginInlineStart: "auto" },
480
+ /** 配置页的一行:左边标签 + 说明,右边控件。 */
481
+ configRow: {
482
+ display: "flex",
483
+ alignItems: "center",
484
+ justifyContent: "space-between",
485
+ gap: "12px",
486
+ padding: "10px 12px",
487
+ borderTop: "1px solid var(--dsw-alias-border-l1)"
488
+ },
489
+ configMain: { display: "flex", flexDirection: "column", gap: "2px", minWidth: 0 },
490
+ configLabel: { color: "var(--dsw-alias-label-primary)", fontSize: "13px" },
491
+ configHint: { color: "var(--dsw-alias-label-tertiary)", fontSize: "11px" },
492
+ configControl: { display: "flex", alignItems: "center", gap: "8px", flex: "0 0 auto" },
493
+ configFoot: {
494
+ display: "flex",
495
+ alignItems: "center",
496
+ gap: "8px",
497
+ flexWrap: "wrap",
498
+ padding: "10px 12px",
499
+ borderTop: "1px solid var(--dsw-alias-border-l1)"
500
+ },
501
+ /** 版本号:浅色小字。 */
502
+ version: { color: "var(--dsw-alias-label-tertiary)", fontSize: "11px" },
503
+ /**
504
+ * 升级提示块。底色用带 alpha 的 `interactive-bg-active` —— 浅色主题里
505
+ * base / layer-1 / layer-2 / layer-3 是同一个颜色,铺上去等于没铺。
506
+ */
507
+ updateBox: {
508
+ display: "flex",
509
+ flexDirection: "column",
510
+ gap: "6px",
511
+ padding: "10px 12px",
512
+ borderTop: "1px solid var(--dsw-alias-border-l1)",
513
+ background: "var(--dsw-alias-interactive-bg-active)"
514
+ },
515
+ updateTitle: { color: "var(--dsw-alias-label-primary)", fontSize: "12px", fontWeight: 600 },
516
+ updateSteps: { color: "var(--dsw-alias-label-secondary)", fontSize: "11px", display: "flex", flexDirection: "column", gap: "4px" },
517
+ code: {
518
+ display: "block",
519
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace",
520
+ fontSize: "11px",
521
+ color: "var(--dsw-alias-label-primary)",
522
+ background: "var(--dsw-alias-interactive-bg-active)",
523
+ borderRadius: "6px",
524
+ padding: "6px 8px",
525
+ overflowX: "auto",
526
+ whiteSpace: "pre"
527
+ },
528
+ link: { color: "var(--dsw-alias-label-primary)", fontSize: "11px" }
529
+ };
880
530
 
881
- const close = () => {
882
- if (busy) return;
883
- dialogStore.set(null);
884
- };
531
+ // src/client/components/dialog-host.ts
532
+ function DialogHost(props) {
533
+ const t = props.t;
534
+ const request = useStore(dialogStore);
535
+ const [busy, setBusy] = React2.useState(false);
536
+ const [ack, setAck] = React2.useState(false);
537
+ const [error, setError] = React2.useState(null);
538
+ const requestKey = request === null ? "" : `${request.kind}:${request.ids.join(",")}:${request.title}`;
539
+ React2.useEffect(() => {
540
+ setAck(false);
541
+ setError(null);
542
+ }, [requestKey]);
543
+ if (request === null) return null;
544
+ const purge = request.kind === "purge";
545
+ const many = request.ids.length > 1;
546
+ const title = purge ? t("confirmPurgeTitle") : t("confirmTrashTitle");
547
+ const body = purge ? many ? text(t, "confirmPurgeManyBody", { n: request.ids.length }) : text(t, "confirmPurgeBody", { title: request.title }) : text(t, "confirmTrashBody", { title: request.title });
548
+ const close = () => {
549
+ if (busy) return;
550
+ dialogStore.set(null);
551
+ };
552
+ const confirm = async () => {
553
+ setBusy(true);
554
+ try {
555
+ const value = await call(purge ? "purge" : "trash", { ids: request.ids });
556
+ const failure = firstFailure(value.results);
557
+ if (failure === null) {
558
+ dialogStore.set(null);
559
+ mutationStore.set(mutationStore.get() + 1);
560
+ } else {
561
+ setError(failure);
562
+ }
563
+ } catch (thrown) {
564
+ setError(reportError(thrown));
565
+ } finally {
566
+ setBusy(false);
567
+ }
568
+ };
569
+ return React2.createElement(
570
+ import_dsh_client_ui_primitives.Modal,
571
+ {
572
+ open: true,
573
+ onClose: close,
574
+ title,
575
+ closeLabel: t("close"),
576
+ description: body,
577
+ footer: React2.createElement(
578
+ "div",
579
+ { style: S.rowLine },
580
+ React2.createElement(import_dsh_client_ui_primitives.Button, { size: "md", variant: "ghost", disabled: busy, onClick: close }, t("cancel")),
581
+ React2.createElement(import_dsh_client_ui_primitives.Button, {
582
+ size: "md",
583
+ variant: "primary",
584
+ disabled: busy || purge && !ack,
585
+ onClick: () => void confirm()
586
+ }, purge ? t("confirmPurgeAction") : t("confirmTrashAction"))
587
+ )
588
+ },
589
+ purge ? React2.createElement(
590
+ "div",
591
+ { style: S.column },
592
+ React2.createElement(
593
+ "label",
594
+ { style: S.ack },
595
+ React2.createElement("input", {
596
+ type: "checkbox",
597
+ checked: ack,
598
+ disabled: busy,
599
+ onChange: (event) => setAck(Boolean(event.target.checked))
600
+ }),
601
+ t("ackPurge")
602
+ ),
603
+ error ? React2.createElement("div", { style: S.error }, error) : null
604
+ ) : error ? React2.createElement("div", { style: S.error }, error) : null
605
+ );
606
+ }
885
607
 
886
- const confirm = async () => {
887
- setBusy(true);
888
- try {
889
- const value = await call(purge ? "purge" : "trash", { ids: request.ids });
890
- const failure = firstFailure(value.results);
891
- if (failure === null) {
892
- dialogStore.set(null);
893
- mutationStore.set(mutationStore.get() + 1);
894
- } else {
895
- setError(failure);
896
- }
897
- } catch (thrown) {
898
- setError(reportError(thrown));
899
- } finally {
900
- setBusy(false);
901
- }
902
- };
608
+ // src/client/components/settings-panel.ts
609
+ var React5 = __toESM(require("react"), 1);
610
+ var import_dsh_client_ui_primitives4 = require("@deepseek-ai/dsh-client-ui-primitives");
903
611
 
904
- return h(
905
- Modal,
906
- {
907
- open: true,
908
- onClose: close,
909
- title,
910
- closeLabel: t("close"),
911
- description: body,
912
- footer: h(
913
- "div",
914
- { style: S.rowLine },
915
- h(Button, { size: "md", variant: "ghost", disabled: busy, onClick: close }, t("cancel")),
916
- h(Button, {
917
- size: "md",
918
- variant: "primary",
919
- disabled: busy || (purge && !ack),
920
- onClick: () => void confirm()
921
- }, purge ? t("confirmPurgeAction") : t("confirmTrashAction"))
922
- )
923
- },
924
- purge
925
- ? h(
926
- "div",
927
- { style: S.column },
928
- h(
929
- "label",
930
- { style: S.ack },
931
- h("input", {
932
- type: "checkbox",
933
- checked: ack,
934
- disabled: busy,
935
- onChange: (event) => setAck(Boolean(event.target.checked))
936
- }),
937
- t("ackPurge")
938
- ),
939
- error ? h("div", { style: S.error }, error) : null
940
- )
941
- : (error ? h("div", { style: S.error }, error) : null)
942
- );
943
- }
612
+ // src/client/format.ts
613
+ function formatBytes(bytes) {
614
+ if (typeof bytes !== "number" || !Number.isFinite(bytes) || bytes <= 0) return "0 B";
615
+ const units = ["B", "KB", "MB", "GB"];
616
+ let value = bytes;
617
+ let unit = 0;
618
+ while (value >= 1024 && unit < units.length - 1) {
619
+ value /= 1024;
620
+ unit += 1;
621
+ }
622
+ return `${value >= 10 || unit === 0 ? Math.round(value) : value.toFixed(1)} ${units[unit]}`;
623
+ }
624
+ function formatTime(ms) {
625
+ if (typeof ms !== "number" || ms <= 0) return "";
626
+ try {
627
+ return new Date(ms).toLocaleString();
628
+ } catch {
629
+ return "";
630
+ }
631
+ }
632
+ function baseName(path) {
633
+ if (typeof path !== "string" || path === "") return "";
634
+ const parts = path.split(/[\\/]/).filter((part) => part !== "");
635
+ return parts.length > 0 ? parts[parts.length - 1] : path;
636
+ }
944
637
 
945
- // ── 插件装配 ────────────────────────────────────────────────────────
946
- const inject = ["slots", "locale"];
638
+ // src/client/groups.ts
639
+ function groupKeyOf(row) {
640
+ if (typeof row.cwd === "string" && row.cwd.trim() !== "") {
641
+ return `cwd:${row.cwd.trim().replace(/[\\/]+$/, "").replace(/\\/g, "/").toLowerCase()}`;
642
+ }
643
+ if (typeof row.workspaceId === "string" && row.workspaceId !== "") return `id:${row.workspaceId}`;
644
+ return "unknown";
645
+ }
646
+ function groupRows(rows, t) {
647
+ const groups = /* @__PURE__ */ new Map();
648
+ for (const row of rows) {
649
+ const key = groupKeyOf(row);
650
+ let group = groups.get(key);
651
+ if (group === void 0) {
652
+ const cwd = typeof row.cwd === "string" && row.cwd !== "" ? row.cwd : null;
653
+ const title = (typeof row.workspaceTitle === "string" && row.workspaceTitle !== "" ? row.workspaceTitle : null) ?? (cwd === null ? null : baseName(cwd) || cwd) ?? t("noProject");
654
+ group = { key, title, cwd, bytes: 0, rows: [] };
655
+ groups.set(key, group);
656
+ }
657
+ group.rows.push(row);
658
+ if (typeof row.bytes === "number" && Number.isFinite(row.bytes)) group.bytes += row.bytes;
659
+ }
660
+ const list = [...groups.values()];
661
+ list.sort((left, right) => {
662
+ if (left.key === "unknown" || right.key === "unknown") {
663
+ return left.key === right.key ? 0 : left.key === "unknown" ? 1 : -1;
664
+ }
665
+ return left.title.localeCompare(right.title);
666
+ });
667
+ return list;
668
+ }
669
+ function sortRows(rows, order) {
670
+ if (order === "title") {
671
+ return [...rows].sort((left, right) => String(left.title ?? "").localeCompare(String(right.title ?? "")));
672
+ }
673
+ if (order === "bytes") {
674
+ return [...rows].sort((left, right) => (right.bytes ?? 0) - (left.bytes ?? 0));
675
+ }
676
+ return rows;
677
+ }
678
+ function collapseKey(tab, groupKey) {
679
+ return `${tab}:${groupKey}`;
680
+ }
681
+ function isGroupCollapsed(key, overrides, fallback) {
682
+ const manual = overrides[key];
683
+ return manual === void 0 ? fallback === "collapsed" : manual;
684
+ }
685
+ function toggledCollapse(key, overrides, fallback) {
686
+ const next = { ...overrides };
687
+ const value = !isGroupCollapsed(key, overrides, fallback);
688
+ if (value === (fallback === "collapsed")) delete next[key];
689
+ else next[key] = value;
690
+ return next;
691
+ }
947
692
 
948
- function apply(ctx) {
949
- ctxRef = ctx;
950
- ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-session-curator: dictionaries");
951
- const t = ctx.locale.bind(NS);
693
+ // src/client/settings.ts
694
+ var DEFAULT_SETTINGS = {
695
+ tab: "archived",
696
+ grouping: true,
697
+ // 与 0.1.1 的行为一致:分组默认是**展开**的,老用户升级后界面不变。
698
+ groupDefault: "expanded",
699
+ showMeta: true,
700
+ order: "host",
701
+ autoUpdate: true
702
+ };
703
+ var SETTINGS_KEY = "dsh-session-curator.settings";
704
+ function readSettings() {
705
+ try {
706
+ const raw = globalThis.localStorage ? globalThis.localStorage.getItem(SETTINGS_KEY) : null;
707
+ if (typeof raw !== "string" || raw === "") return { ...DEFAULT_SETTINGS };
708
+ const parsed = JSON.parse(raw);
709
+ if (parsed === null || typeof parsed !== "object") return { ...DEFAULT_SETTINGS };
710
+ const next = { ...DEFAULT_SETTINGS };
711
+ const pick = (key, validate) => {
712
+ const value = validate(parsed[key]);
713
+ if (value !== void 0) next[key] = value;
714
+ };
715
+ pick("tab", (value) => value === "archived" || value === "pinned" || value === "trash" ? value : void 0);
716
+ pick("grouping", (value) => typeof value === "boolean" ? value : void 0);
717
+ pick("groupDefault", (value) => value === "expanded" || value === "collapsed" ? value : void 0);
718
+ pick("showMeta", (value) => typeof value === "boolean" ? value : void 0);
719
+ pick("order", (value) => value === "host" || value === "title" || value === "bytes" ? value : void 0);
720
+ pick("autoUpdate", (value) => typeof value === "boolean" ? value : void 0);
721
+ return next;
722
+ } catch {
723
+ return { ...DEFAULT_SETTINGS };
724
+ }
725
+ }
726
+ var settingsStore = createStore(readSettings());
727
+ function updateSettings(patch) {
728
+ const next = { ...settingsStore.get(), ...patch };
729
+ settingsStore.set(next);
730
+ try {
731
+ if (globalThis.localStorage) globalThis.localStorage.setItem(SETTINGS_KEY, JSON.stringify(next));
732
+ } catch {
733
+ }
734
+ return next;
735
+ }
736
+ var groupingStore = createStore(settingsStore.get().grouping);
737
+ var SIDEBAR_VIEW_KEY = "dsh.workspace.view.v5";
738
+ function readSidebarArchived() {
739
+ try {
740
+ const raw = globalThis.localStorage ? globalThis.localStorage.getItem(SIDEBAR_VIEW_KEY) : null;
741
+ if (typeof raw !== "string" || raw === "") return "default";
742
+ const parsed = JSON.parse(raw);
743
+ const value = parsed !== null && typeof parsed === "object" ? parsed.archivedFilter : null;
744
+ return value === "show" || value === "only" ? value : "default";
745
+ } catch {
746
+ return "default";
747
+ }
748
+ }
749
+ function writeSidebarArchived(show) {
750
+ try {
751
+ if (!globalThis.localStorage) return false;
752
+ const raw = globalThis.localStorage.getItem(SIDEBAR_VIEW_KEY);
753
+ const parsed = raw === null || raw === "" ? null : JSON.parse(raw);
754
+ const target2 = parsed === null || typeof parsed !== "object" || Array.isArray(parsed) ? {} : parsed;
755
+ target2.archivedFilter = show ? "show" : "default";
756
+ globalThis.localStorage.setItem(SIDEBAR_VIEW_KEY, JSON.stringify(target2));
757
+ return true;
758
+ } catch {
759
+ return false;
760
+ }
761
+ }
952
762
 
953
- // 逐个贡献:某个槽位声明缺失时只丢这一个贡献,不连累其余的。
954
- const contribute = (slot, options, component, label) => {
955
- try {
956
- ctx.slots.inject(slot, () => ctx.slots.register(options, component));
957
- } catch (error) {
958
- reportError(error);
959
- if (ctx.logger) ctx.logger.warn(`[session-curator] slot ${slot} 注册失败(${label})`);
960
- }
961
- };
763
+ // src/client/update.ts
764
+ var updateStore = createStore({ state: "idle", value: null, error: null });
765
+ async function checkUpdate(force) {
766
+ updateStore.set({ state: "checking", value: updateStore.get().value, error: null });
767
+ try {
768
+ const value = await call("check-update", force === true ? { force: true } : {});
769
+ updateStore.set({ state: "done", value, error: null });
770
+ } catch (error) {
771
+ updateStore.set({ state: "failed", value: updateStore.get().value, error: reportError(error) });
772
+ }
773
+ }
962
774
 
963
- // 1) 会话行「⋯」菜单里的删除项,排在官方 archive(400)之后。
964
- contribute("sidebar.workspaces.session.menu.item", {
965
- name: "sidebar.workspaces.session.menu.item",
966
- id: MENU_ID,
967
- order: 500,
968
- label: () => t("menuDelete"),
969
- locale: NS
970
- }, ({ sessionId, displayTitle, useMenuOpenState }) => {
971
- const [, setMenuOpen] = useMenuOpenState();
972
- return h(MenuItemButton, {
973
- danger: true,
974
- separatorBefore: true,
975
- onSelect: () => {
976
- setMenuOpen(false);
977
- requestTrash([sessionId], displayTitle || sessionId);
978
- }
979
- }, t("menuDelete"));
980
- }, "menu item");
775
+ // src/client/components/group-header.ts
776
+ var React3 = __toESM(require("react"), 1);
777
+ var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives");
778
+ function GroupHeader(props) {
779
+ const group = props.group;
780
+ const t = props.t;
781
+ const total = group.rows.length;
782
+ const allSelected = total > 0 && group.rows.every((row) => props.selected.includes(row.sessionId));
783
+ const action = allSelected ? t("clearShort") : t("selectAllShort");
784
+ return React3.createElement(
785
+ "div",
786
+ { style: props.first ? { ...S.groupHead, borderTop: "none" } : S.groupHead },
787
+ React3.createElement(
788
+ "button",
789
+ {
790
+ type: "button",
791
+ style: S.groupToggle,
792
+ title: props.collapsed ? t("expand") : t("collapse"),
793
+ onClick: () => props.onToggleCollapse()
794
+ },
795
+ React3.createElement("span", { style: S.groupCaret }, props.collapsed ? React3.createElement(import_dsh_client_ui_primitives2.IconChevronRightOutlineRegular, { size: 12 }) : React3.createElement(import_dsh_client_ui_primitives2.IconChevronDownOutlineRegular, { size: 12 })),
796
+ React3.createElement("span", { style: S.groupIcon }, props.collapsed ? React3.createElement(import_dsh_client_ui_primitives2.IconFolderCloseRegular, { size: 14 }) : React3.createElement(import_dsh_client_ui_primitives2.IconFolderOpenRegular, { size: 14 })),
797
+ React3.createElement("span", { style: S.groupTitle, title: group.cwd || group.title }, group.title),
798
+ React3.createElement("span", { style: S.groupMeta }, text(t, "sessions", { n: total })),
799
+ group.bytes > 0 ? React3.createElement("span", { style: S.groupMeta }, formatBytes(group.bytes)) : null,
800
+ group.cwd !== null ? React3.createElement("span", { style: { ...S.groupPath, ...S.mono }, title: group.cwd }, group.cwd) : null
801
+ ),
802
+ React3.createElement(import_dsh_client_ui_primitives2.Button, {
803
+ size: "sm",
804
+ variant: "ghost",
805
+ disabled: props.busy,
806
+ // 长路径不许把这个按钮挤窄(否则中文标签折行),见 styles.ts 的 groupAction。
807
+ style: S.groupAction,
808
+ title: `${group.title} — ${allSelected ? t("clearGroup") : t("selectGroup")}`,
809
+ onClick: () => props.onToggleGroup()
810
+ }, action)
811
+ );
812
+ }
981
813
 
982
- // 2) 全局确认弹窗宿主。
983
- contribute("shell.overlay", {
984
- name: "shell.overlay",
985
- id: OVERLAY_ID,
986
- order: 60,
987
- locale: NS
988
- }, DialogHost, "dialog host");
814
+ // src/client/components/session-row.ts
815
+ var React4 = __toESM(require("react"), 1);
816
+ var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives");
817
+ function SessionRow(props) {
818
+ const row = props.row;
819
+ const t = props.t;
820
+ const meta = [];
821
+ if (props.showCwd) meta.push(row.cwd ? row.cwd : t("noProject"));
822
+ if (props.showMeta !== false) {
823
+ if (row.bytes) meta.push(formatBytes(row.bytes));
824
+ if (row.files) meta.push(text(t, "files", { n: row.files }));
825
+ }
826
+ const time = props.showDeletedAt ? formatTime(row.deletedAt) : formatTime(row.createdAt);
827
+ if (time !== "") meta.push(time);
828
+ const menu = props.menu;
829
+ return React4.createElement(
830
+ "div",
831
+ { style: props.first ? { ...S.item, borderTop: "none" } : S.item },
832
+ React4.createElement("input", {
833
+ type: "checkbox",
834
+ checked: props.checked,
835
+ disabled: props.busy,
836
+ "aria-label": row.title,
837
+ onChange: () => props.onToggle()
838
+ }),
839
+ React4.createElement(
840
+ "div",
841
+ { style: S.itemMain },
842
+ React4.createElement("div", { style: S.itemTitle, title: row.title }, row.title),
843
+ React4.createElement("div", { style: S.itemMeta, title: row.cwd || "" }, meta.join(" · "))
844
+ ),
845
+ React4.createElement(
846
+ "div",
847
+ { style: S.itemActions },
848
+ React4.createElement(import_dsh_client_ui_primitives3.Menu, {
849
+ open: props.menuOpen,
850
+ portal: true,
851
+ side: "bottom",
852
+ align: "end",
853
+ closeOnPointerLeave: true,
854
+ items: menu.items,
855
+ onSelect: (id) => menu.run(id),
856
+ onClose: () => props.onCloseMenu(),
857
+ anchor: React4.createElement(import_dsh_client_ui_primitives3.Button, {
858
+ size: "sm",
859
+ variant: "ghost",
860
+ disabled: props.busy,
861
+ style: S.rowMenuButton,
862
+ icon: React4.createElement(import_dsh_client_ui_primitives3.IconEllipsisOutlineRegular, {}),
863
+ "aria-label": `${row.title} — ${t("rowActions")}`,
864
+ onClick: (event) => {
865
+ event.stopPropagation();
866
+ props.onToggleMenu();
867
+ }
868
+ })
869
+ })
870
+ )
871
+ );
872
+ }
989
873
 
990
- // 3) 设置页:已归档 / 已置顶 / 回收站(id 见 sectionIdFor:为了导航栏的归档图标)。
991
- contribute("settings.section", {
992
- name: "settings.section",
993
- id: sectionIdFor(ctx),
994
- order: 45,
995
- label: () => t("sectionLabel"),
996
- locale: NS
997
- }, SessionManagerPanel, "settings section");
998
- }
874
+ // src/client/components/settings-panel.ts
875
+ function SessionManagerPanel(props) {
876
+ const t = props.t;
877
+ const settings = useStore(settingsStore);
878
+ const [tab, setTab] = React5.useState(settings.tab);
879
+ const [snapshot, setSnapshot] = React5.useState(null);
880
+ const [error, setError] = React5.useState(null);
881
+ const [loading, setLoading] = React5.useState(true);
882
+ const [busy, setBusy] = React5.useState(false);
883
+ const [selected, setSelected] = React5.useState([]);
884
+ const [collapsedOverrides, setCollapsedOverrides] = React5.useState({});
885
+ const [menuFor, setMenuFor] = React5.useState(null);
886
+ const grouping = useStore(groupingStore);
887
+ const revision = useStore(mutationStore);
888
+ const update = useStore(updateStore);
889
+ const [sidebarArchived, setSidebarArchived] = React5.useState(readSidebarArchived);
890
+ const [sidebarAtMount] = React5.useState(readSidebarArchived);
891
+ const applyResult = (value) => {
892
+ if (value && value.archived) setSnapshot(value);
893
+ };
894
+ const refresh = React5.useCallback(async () => {
895
+ setLoading(true);
896
+ try {
897
+ applyResult(await call("snapshot", {}));
898
+ setError(null);
899
+ } catch (failure) {
900
+ setError(reportError(failure));
901
+ } finally {
902
+ setLoading(false);
903
+ }
904
+ }, []);
905
+ React5.useEffect(() => {
906
+ void refresh();
907
+ }, [refresh, revision]);
908
+ React5.useEffect(() => {
909
+ if (tab !== "config" || !settings.autoUpdate || update.state !== "idle") return;
910
+ void checkUpdate(false);
911
+ }, [tab, settings.autoUpdate, update.state]);
912
+ React5.useEffect(() => {
913
+ setSelected([]);
914
+ setMenuFor(null);
915
+ }, [tab]);
916
+ const run = async (endpoint, ids) => {
917
+ if (ids.length === 0) return;
918
+ setBusy(true);
919
+ try {
920
+ const value = await call(endpoint, { ids });
921
+ applyResult(value);
922
+ setError(firstFailure(value.results));
923
+ setSelected([]);
924
+ } catch (failure) {
925
+ setError(reportError(failure));
926
+ } finally {
927
+ setBusy(false);
928
+ }
929
+ };
930
+ const toggle = (sessionId) => {
931
+ setSelected((current2) => current2.includes(sessionId) ? current2.filter((id) => id !== sessionId) : current2.concat([sessionId]));
932
+ };
933
+ const toggleGroup = (group) => {
934
+ const ids = group.rows.map((row) => row.sessionId);
935
+ const all = ids.every((id) => selected.includes(id));
936
+ setSelected((current2) => all ? current2.filter((id) => !ids.includes(id)) : [...new Set(current2.concat(ids))]);
937
+ };
938
+ const toggleCollapse = (key) => {
939
+ setCollapsedOverrides((current2) => toggledCollapse(key, current2, settings.groupDefault));
940
+ };
941
+ const archived = snapshot ? snapshot.archived : [];
942
+ const pinned = snapshot ? snapshot.pinned : [];
943
+ const trashed = snapshot ? snapshot.trashed : [];
944
+ const rows = sortRows(tab === "archived" ? archived : tab === "pinned" ? pinned : tab === "trash" ? trashed : [], settings.order);
945
+ const selectedRows = rows.filter((row) => selected.includes(row.sessionId));
946
+ const groups = grouping ? groupRows(rows, t) : [];
947
+ const blockOf = (group) => {
948
+ const key = collapseKey(tab, group.key);
949
+ return { key, group, rows: isGroupCollapsed(key, collapsedOverrides, settings.groupDefault) ? [] : group.rows };
950
+ };
951
+ const blocks = grouping ? groups.map(blockOf) : [{ key: `${tab}:flat`, group: null, rows }];
952
+ const tabs = [
953
+ { id: "archived", label: t(TAB_LABEL.archived), count: archived.length },
954
+ { id: "pinned", label: t(TAB_LABEL.pinned), count: pinned.length },
955
+ { id: "trash", label: t(TAB_LABEL.trash), count: trashed.length },
956
+ { id: "config", label: t("titleConfig"), count: 0 }
957
+ ];
958
+ const rowMenu = (row) => {
959
+ const moveToTrash = {
960
+ id: "trash",
961
+ label: t("moveToTrash"),
962
+ icon: React5.createElement(import_dsh_client_ui_primitives4.IconTrashOutlineRegular, { size: 14 }),
963
+ danger: true
964
+ };
965
+ if (tab === "archived") {
966
+ return {
967
+ items: [
968
+ { id: "unarchive", label: t("unarchive"), icon: React5.createElement(import_dsh_client_ui_primitives4.IconUnarchiveOutlineRegular, { size: 14 }) },
969
+ { id: "pin", label: t("pin"), icon: React5.createElement(import_dsh_client_ui_primitives4.IconPinOutlineRegular, { size: 14 }) },
970
+ moveToTrash
971
+ ],
972
+ run: (id) => {
973
+ setMenuFor(null);
974
+ if (id === "trash") requestTrash([row.sessionId], row.title);
975
+ else void run(id, [row.sessionId]);
976
+ }
977
+ };
978
+ }
979
+ if (tab === "pinned") {
980
+ return {
981
+ items: [
982
+ { id: "unpin", label: t("unpin"), icon: React5.createElement(import_dsh_client_ui_primitives4.IconPinOutlineRegular, { size: 14 }) },
983
+ moveToTrash
984
+ ],
985
+ run: (id) => {
986
+ setMenuFor(null);
987
+ if (id === "trash") requestTrash([row.sessionId], row.title);
988
+ else void run(id, [row.sessionId]);
989
+ }
990
+ };
991
+ }
992
+ return {
993
+ items: [
994
+ { id: "restore", label: t("restore"), icon: React5.createElement(import_dsh_client_ui_primitives4.IconRefreshOutlineRegular, { size: 14 }) },
995
+ { id: "purge", label: t("purge"), icon: React5.createElement(import_dsh_client_ui_primitives4.IconTrashOutlineRegular, { size: 14 }), danger: true }
996
+ ],
997
+ run: (id) => {
998
+ setMenuFor(null);
999
+ if (id === "purge") requestPurge([row.sessionId], row.title);
1000
+ else void run(id, [row.sessionId]);
1001
+ }
1002
+ };
1003
+ };
1004
+ const batch = [];
1005
+ if (tab === "archived" && selected.length > 0) {
1006
+ batch.push({ label: t("unarchive"), run: () => void run("unarchive", selected) });
1007
+ batch.push({ label: t("moveToTrash"), variant: "outline", run: () => requestTrash(selected, selectedRows[0] ? selectedRows[0].title : "") });
1008
+ }
1009
+ if (tab === "pinned" && selected.length > 0) {
1010
+ batch.push({ label: t("unpin"), run: () => void run("unpin", selected) });
1011
+ batch.push({ label: t("moveToTrash"), variant: "outline", run: () => requestTrash(selected, selectedRows[0] ? selectedRows[0].title : "") });
1012
+ }
1013
+ if (tab === "trash" && selected.length > 0) {
1014
+ batch.push({ label: t("restore"), run: () => void run("restore", selected) });
1015
+ batch.push({ label: t("purge"), variant: "outline", run: () => requestPurge(selected, selectedRows[0] ? selectedRows[0].title : "") });
1016
+ }
1017
+ const totals = snapshot ? snapshot.totals : null;
1018
+ const totalBytes = tab === "archived" ? totals ? totals.archivedBytes : 0 : tab === "trash" ? totals ? totals.trashBytes : 0 : 0;
1019
+ const nodes = [];
1020
+ let first = true;
1021
+ for (const block of blocks) {
1022
+ if (block.group !== null) {
1023
+ const group = block.group;
1024
+ const key = block.key;
1025
+ nodes.push(React5.createElement(GroupHeader, {
1026
+ key: `group:${key}`,
1027
+ group,
1028
+ t,
1029
+ first,
1030
+ busy,
1031
+ selected,
1032
+ collapsed: isGroupCollapsed(key, collapsedOverrides, settings.groupDefault),
1033
+ onToggleGroup: () => toggleGroup(group),
1034
+ onToggleCollapse: () => toggleCollapse(key)
1035
+ }));
1036
+ first = false;
1037
+ }
1038
+ for (const row of block.rows) {
1039
+ nodes.push(React5.createElement(SessionRow, {
1040
+ key: row.sessionId,
1041
+ row,
1042
+ t,
1043
+ first,
1044
+ busy,
1045
+ showCwd: block.group === null,
1046
+ showMeta: settings.showMeta,
1047
+ showDeletedAt: tab === "trash",
1048
+ checked: selected.includes(row.sessionId),
1049
+ onToggle: () => toggle(row.sessionId),
1050
+ menu: rowMenu(row),
1051
+ menuOpen: menuFor === row.sessionId,
1052
+ onToggleMenu: () => setMenuFor(menuFor === row.sessionId ? null : row.sessionId),
1053
+ onCloseMenu: () => setMenuFor(null)
1054
+ }));
1055
+ first = false;
1056
+ }
1057
+ }
1058
+ const isConfig = tab === "config";
1059
+ const configRow = (key, label, hint, control, firstRow) => React5.createElement(
1060
+ "div",
1061
+ { key, style: firstRow ? { ...S.configRow, borderTop: "none" } : S.configRow },
1062
+ React5.createElement(
1063
+ "div",
1064
+ { style: S.configMain },
1065
+ React5.createElement("span", { style: S.configLabel }, label),
1066
+ hint === null ? null : React5.createElement("span", { style: S.configHint }, hint)
1067
+ ),
1068
+ React5.createElement("div", { style: S.configControl }, control)
1069
+ );
1070
+ const version = snapshot !== null && typeof snapshot.version === "string" ? snapshot.version : update.value !== null && typeof update.value.current === "string" ? update.value.current : null;
1071
+ const outdated = update.state === "done" && update.value !== null && update.value.outdated === true;
1072
+ const updateLine = update.state === "checking" ? React5.createElement("span", { style: S.configHint }, t("updateChecking")) : update.state === "failed" ? React5.createElement("span", { style: S.error }, text(t, "updateFailed", { reason: update.error === null ? "?" : update.error })) : update.value !== null && typeof update.value.error === "string" ? React5.createElement("span", { style: S.configHint }, text(t, "updateFailed", { reason: update.value.error })) : update.state === "done" && !outdated ? React5.createElement("span", { style: S.configHint }, t("updateLatest")) : null;
1073
+ const configPage = React5.createElement(
1074
+ "div",
1075
+ { style: S.column },
1076
+ React5.createElement(
1077
+ "div",
1078
+ { style: S.card },
1079
+ configRow("config:tab", t("configTab"), t("configTabHint"), React5.createElement(import_dsh_client_ui_primitives4.SegmentedControl, {
1080
+ id: "session-curator-default-tab",
1081
+ value: settings.tab,
1082
+ options: [
1083
+ { value: "archived", label: t(TAB_LABEL.archived) },
1084
+ { value: "pinned", label: t(TAB_LABEL.pinned) },
1085
+ { value: "trash", label: t(TAB_LABEL.trash) }
1086
+ ],
1087
+ label: t("configTab"),
1088
+ onChange: (next) => void updateSettings({ tab: next })
1089
+ }), true),
1090
+ configRow("config:grouping", t("configGrouping"), t("configGroupingHint"), React5.createElement(import_dsh_client_ui_primitives4.Switch, {
1091
+ checked: settings.grouping,
1092
+ label: t("configGrouping"),
1093
+ onChange: (next) => {
1094
+ updateSettings({ grouping: next });
1095
+ groupingStore.set(next);
1096
+ }
1097
+ })),
1098
+ configRow("config:group-default", t("configGroupDefault"), t("configGroupDefaultHint"), React5.createElement(import_dsh_client_ui_primitives4.SegmentedControl, {
1099
+ id: "session-curator-group-default",
1100
+ value: settings.groupDefault,
1101
+ options: [
1102
+ { value: "expanded", label: t("groupExpanded") },
1103
+ { value: "collapsed", label: t("groupCollapsed") }
1104
+ ],
1105
+ label: t("configGroupDefault"),
1106
+ onChange: (next) => void updateSettings({ groupDefault: next })
1107
+ })),
1108
+ configRow("config:sidebar-archived", t("configSidebarArchived"), t("configSidebarArchivedHint"), React5.createElement(import_dsh_client_ui_primitives4.Switch, {
1109
+ checked: sidebarArchived !== "default",
1110
+ label: t("configSidebarArchived"),
1111
+ onChange: (next) => {
1112
+ writeSidebarArchived(next);
1113
+ setSidebarArchived(next ? "show" : "default");
1114
+ }
1115
+ })),
1116
+ // 官方 store 只在创建时读一次 localStorage,所以改完必须刷新页面。
1117
+ sidebarArchived === sidebarAtMount ? null : React5.createElement(
1118
+ "div",
1119
+ { key: "config:sidebar-refresh", style: S.configFoot },
1120
+ React5.createElement("span", { style: S.configHint }, t("configSidebarRefreshHint")),
1121
+ React5.createElement(import_dsh_client_ui_primitives4.Button, {
1122
+ size: "sm",
1123
+ variant: "outline",
1124
+ onClick: () => {
1125
+ try {
1126
+ globalThis.location.reload();
1127
+ } catch {
1128
+ }
1129
+ }
1130
+ }, t("refreshPage"))
1131
+ ),
1132
+ configRow("config:meta", t("configMeta"), t("configMetaHint"), React5.createElement(import_dsh_client_ui_primitives4.Switch, {
1133
+ checked: settings.showMeta,
1134
+ label: t("configMeta"),
1135
+ onChange: (next) => void updateSettings({ showMeta: next })
1136
+ })),
1137
+ configRow("config:order", t("configOrder"), t("configOrderHint"), React5.createElement(import_dsh_client_ui_primitives4.SegmentedControl, {
1138
+ id: "session-curator-order",
1139
+ value: settings.order,
1140
+ options: [
1141
+ { value: "host", label: t("orderHost") },
1142
+ { value: "title", label: t("orderTitle") },
1143
+ { value: "bytes", label: t("orderBytes") }
1144
+ ],
1145
+ label: t("configOrder"),
1146
+ onChange: (next) => void updateSettings({ order: next })
1147
+ })),
1148
+ configRow("config:auto", t("configAutoUpdate"), t("configAutoUpdateHint"), React5.createElement(import_dsh_client_ui_primitives4.Switch, {
1149
+ checked: settings.autoUpdate,
1150
+ label: t("configAutoUpdate"),
1151
+ onChange: (next) => void updateSettings({ autoUpdate: next })
1152
+ })),
1153
+ React5.createElement(
1154
+ "div",
1155
+ { key: "config:reset", style: { ...S.configFoot, justifyContent: "flex-end" } },
1156
+ React5.createElement(import_dsh_client_ui_primitives4.Button, {
1157
+ size: "sm",
1158
+ variant: "outline",
1159
+ onClick: () => {
1160
+ updateSettings({ ...DEFAULT_SETTINGS });
1161
+ groupingStore.set(DEFAULT_SETTINGS.grouping);
1162
+ }
1163
+ }, t("configReset"))
1164
+ )
1165
+ ),
1166
+ React5.createElement(
1167
+ "div",
1168
+ { style: S.card },
1169
+ React5.createElement(
1170
+ "div",
1171
+ { style: { ...S.configRow, borderTop: "none" } },
1172
+ React5.createElement(
1173
+ "div",
1174
+ { style: S.configMain },
1175
+ React5.createElement("span", { style: S.configLabel }, t("aboutTitle")),
1176
+ React5.createElement("span", { style: S.configHint }, "dsh-session-curator")
1177
+ ),
1178
+ React5.createElement(
1179
+ "div",
1180
+ { style: S.configControl },
1181
+ // 版本号:浅色小字。
1182
+ React5.createElement("span", { style: S.version }, `${t("versionLabel")} ${version === null ? "—" : `v${version}`}`),
1183
+ React5.createElement(import_dsh_client_ui_primitives4.Button, {
1184
+ size: "sm",
1185
+ variant: "outline",
1186
+ disabled: update.state === "checking",
1187
+ onClick: () => void checkUpdate(true)
1188
+ }, t("updateCheckNow"))
1189
+ )
1190
+ ),
1191
+ updateLine === null ? null : React5.createElement("div", { key: "config:update", style: S.configFoot }, updateLine),
1192
+ outdated && update.value !== null ? React5.createElement(
1193
+ "div",
1194
+ { key: "config:upgrade", style: S.updateBox },
1195
+ React5.createElement("span", { style: S.updateTitle }, text(t, "updateAvailable", { latest: update.value.latest ?? "?", current: version === null ? "?" : version })),
1196
+ React5.createElement(
1197
+ "span",
1198
+ { style: S.updateSteps },
1199
+ React5.createElement("span", null, t("updateStepsTitle")),
1200
+ React5.createElement("span", null, t("updateStepOne")),
1201
+ React5.createElement("code", { style: S.code }, "dsh plugin --profile desktop add dsh-session-curator@latest"),
1202
+ React5.createElement("span", null, t("updateStepTwo"))
1203
+ ),
1204
+ React5.createElement("a", {
1205
+ style: S.link,
1206
+ href: "https://www.npmjs.com/package/dsh-session-curator",
1207
+ target: "_blank",
1208
+ rel: "noreferrer"
1209
+ }, t("updateOpen"))
1210
+ ) : null
1211
+ )
1212
+ );
1213
+ return React5.createElement(
1214
+ "div",
1215
+ { style: S.column },
1216
+ React5.createElement(
1217
+ "div",
1218
+ { style: S.header },
1219
+ React5.createElement(
1220
+ "div",
1221
+ { style: S.tabBar },
1222
+ tabs.map((entry) => React5.createElement(import_dsh_client_ui_primitives4.Button, {
1223
+ key: entry.id,
1224
+ size: "sm",
1225
+ variant: tab === entry.id ? "primary" : "ghost",
1226
+ onClick: () => setTab(entry.id)
1227
+ }, `${entry.label}${entry.count > 0 ? ` (${entry.count})` : ""}`))
1228
+ ),
1229
+ // 分组与刷新推到右边;配置页上没有它们。
1230
+ isConfig ? null : React5.createElement(
1231
+ "div",
1232
+ { style: S.headerActions },
1233
+ React5.createElement(import_dsh_client_ui_primitives4.Button, {
1234
+ size: "sm",
1235
+ variant: "outline",
1236
+ title: t("groupHint"),
1237
+ onClick: () => {
1238
+ const next = !grouping;
1239
+ groupingStore.set(next);
1240
+ updateSettings({ grouping: next });
1241
+ }
1242
+ }, grouping ? t("groupingOn") : t("groupingOff")),
1243
+ React5.createElement(import_dsh_client_ui_primitives4.Button, { size: "sm", variant: "outline", disabled: loading || busy, onClick: () => void refresh() }, t("refresh"))
1244
+ )
1245
+ ),
1246
+ error ? React5.createElement("div", { style: S.error }, error) : null,
1247
+ isConfig ? null : React5.createElement(
1248
+ "div",
1249
+ { style: S.card },
1250
+ loading && !snapshot ? React5.createElement("div", { style: S.empty }, t("loading")) : rows.length === 0 ? React5.createElement("div", { style: S.empty }, tab === "archived" ? t("emptyArchived") : tab === "pinned" ? t("emptyPinned") : t("emptyTrash")) : React5.createElement("div", { style: S.list }, nodes)
1251
+ ),
1252
+ isConfig ? null : React5.createElement(
1253
+ "div",
1254
+ { style: S.rowLine },
1255
+ React5.createElement(import_dsh_client_ui_primitives4.Button, {
1256
+ size: "sm",
1257
+ variant: "ghost",
1258
+ disabled: busy || rows.length === 0,
1259
+ onClick: () => setSelected(selected.length === rows.length ? [] : rows.map((row) => row.sessionId))
1260
+ }, selected.length === rows.length && rows.length > 0 ? t("selectNone") : t("selectAll")),
1261
+ batch.map((action) => React5.createElement(import_dsh_client_ui_primitives4.Button, {
1262
+ key: action.label,
1263
+ size: "sm",
1264
+ variant: action.variant || "ghost",
1265
+ disabled: busy,
1266
+ onClick: action.run
1267
+ }, action.label)),
1268
+ tab === "trash" && trashed.length > 0 ? React5.createElement(import_dsh_client_ui_primitives4.Button, {
1269
+ size: "sm",
1270
+ variant: "outline",
1271
+ disabled: busy,
1272
+ onClick: () => requestPurge(trashed.map((row) => row.sessionId), "")
1273
+ }, t("emptyTrashAll")) : null,
1274
+ busy ? React5.createElement("span", { style: S.footer }, t("working")) : null
1275
+ ),
1276
+ isConfig ? configPage : null,
1277
+ isConfig ? null : React5.createElement(
1278
+ "div",
1279
+ { style: S.footer },
1280
+ React5.createElement("span", null, text(t, "total", { n: rows.length, size: formatBytes(totalBytes) })),
1281
+ grouping && groups.length > 0 ? React5.createElement("span", null, text(t, "workspaces", { n: groups.length })) : null,
1282
+ selected.length > 0 ? React5.createElement("span", null, text(t, "selected", { n: selected.length })) : null,
1283
+ snapshot ? React5.createElement("span", { style: S.mono, title: snapshot.trashDir }, snapshot.trashDir) : null
1284
+ )
1285
+ );
1286
+ }
999
1287
 
1000
- bundleModule.exports.NS = NS;
1001
- bundleModule.exports.apply = apply;
1002
- bundleModule.exports.inject = inject;
1003
- // 纯函数 / 纯组件,给 scripts/client.mjs 直接单测(浏览器里用不到这几个导出)。
1004
- bundleModule.exports.groupRows = groupRows;
1005
- bundleModule.exports.groupKeyOf = groupKeyOf;
1006
- bundleModule.exports.SessionRow = SessionRow;
1007
- bundleModule.exports.GroupHeader = GroupHeader;
1008
- bundleModule.exports.sectionIdFor = sectionIdFor;
1009
- return bundleModule.exports;
1288
+ // src/client/index.ts
1289
+ var MENU_ID = "session-curator.delete";
1290
+ var OVERLAY_ID = "session-curator.dialog";
1291
+ var SECTION_ID = "session-curator";
1292
+ var NAV_SECTION_ID = "archived-sessions";
1293
+ function sectionIdFor(ctx) {
1294
+ try {
1295
+ const taken = ctx.slots.entries("settings.section").some((entry) => entry?.options?.id === NAV_SECTION_ID);
1296
+ return taken ? SECTION_ID : NAV_SECTION_ID;
1297
+ } catch {
1298
+ return SECTION_ID;
1299
+ }
1300
+ }
1301
+ var inject = ["slots", "locale"];
1302
+ function apply(ctx) {
1303
+ setContext(ctx);
1304
+ ctx.effect(() => ctx.locale.register(NS, dictionaries), "dsh-session-curator: dictionaries");
1305
+ const t = ctx.locale.bind(NS);
1306
+ const contribute = (slot, options, component, label) => {
1307
+ try {
1308
+ ctx.slots.inject(slot, () => ctx.slots.register(options, component));
1309
+ } catch (error) {
1310
+ reportError(error);
1311
+ if (ctx.logger) ctx.logger.warn(`[session-curator] slot ${slot} 注册失败(${label})`);
1312
+ }
1313
+ };
1314
+ contribute("sidebar.workspaces.session.menu.item", {
1315
+ name: "sidebar.workspaces.session.menu.item",
1316
+ id: MENU_ID,
1317
+ order: 500,
1318
+ label: () => t("menuDelete"),
1319
+ locale: NS
1320
+ }, (props) => {
1321
+ const [, setMenuOpen] = props.useMenuOpenState();
1322
+ return React6.createElement(import_dsh_client_ui_primitives5.MenuItemButton, {
1323
+ danger: true,
1324
+ separatorBefore: true,
1325
+ onSelect: () => {
1326
+ setMenuOpen(false);
1327
+ requestTrash([props.sessionId], props.displayTitle || props.sessionId);
1328
+ }
1329
+ }, t("menuDelete"));
1330
+ }, "menu item");
1331
+ contribute("shell.overlay", {
1332
+ name: "shell.overlay",
1333
+ id: OVERLAY_ID,
1334
+ order: 60,
1335
+ locale: NS
1336
+ }, DialogHost, "dialog host");
1337
+ contribute("settings.section", {
1338
+ name: "settings.section",
1339
+ id: sectionIdFor(ctx),
1340
+ order: 45,
1341
+ label: () => t("sectionLabel"),
1342
+ locale: NS
1343
+ }, SessionManagerPanel, "settings section");
1344
+ }
1345
+ })(module, module.exports, require);
1346
+ return module.exports;
1010
1347
  }
1011
1348
  });