dsh-session-conductor 0.0.0-stage → 1.0.9

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.
Files changed (68) hide show
  1. package/README.md +165 -2
  2. package/cordis.patch.yml +48 -0
  3. package/lib/apply.js +1361 -0
  4. package/lib/client-parts/apply.js +43 -0
  5. package/lib/client-parts/components/compaction.js +124 -0
  6. package/lib/client-parts/components/conductor-settings.js +62 -0
  7. package/lib/client-parts/components/group.js +115 -0
  8. package/lib/client-parts/components/panel.js +1265 -0
  9. package/lib/client-parts/components/processing.js +98 -0
  10. package/lib/client-parts/components/session-icon.js +20 -0
  11. package/lib/client-parts/components/settings.js +415 -0
  12. package/lib/client-parts/foundation/bootstrap.js +48 -0
  13. package/lib/client-parts/foundation/i18n.js +74 -0
  14. package/lib/client-parts/foundation/list-cache.js +28 -0
  15. package/lib/client-parts/foundation/styles.js +236 -0
  16. package/lib/client.js +2534 -0
  17. package/lib/core/config.js +159 -0
  18. package/lib/core/domain.js +157 -0
  19. package/lib/core/lazy.js +20 -0
  20. package/lib/core/state.js +106 -0
  21. package/lib/core.js +82 -0
  22. package/lib/eio-repair.js +204 -0
  23. package/lib/features/context-usage/index.js +219 -0
  24. package/lib/features/continue/eligibility.js +49 -0
  25. package/lib/features/continue/scan.js +134 -0
  26. package/lib/features/continue/session.js +235 -0
  27. package/lib/features/inject/preset.js +20 -0
  28. package/lib/features/member-model/index.js +14 -0
  29. package/lib/features/member-model/locate.js +157 -0
  30. package/lib/features/member-model/override.js +76 -0
  31. package/lib/features/member-model/setup.js +55 -0
  32. package/lib/features/member-model/switch.js +186 -0
  33. package/lib/features/rename/analysis.js +303 -0
  34. package/lib/features/rename/title.js +158 -0
  35. package/lib/features/rename/value.js +77 -0
  36. package/lib/features/search.js +119 -0
  37. package/lib/features/session-ops-routes.js +218 -0
  38. package/lib/features/session-ops.js +414 -0
  39. package/lib/group.js +303 -0
  40. package/lib/guards.js +28 -0
  41. package/lib/http.js +24 -0
  42. package/lib/i18n/en.json +204 -0
  43. package/lib/i18n/zh.json +204 -0
  44. package/lib/index.js +78 -0
  45. package/lib/repair.js +398 -0
  46. package/lib/seq-gap-repair.js +224 -0
  47. package/lib/session-codec.js +37 -0
  48. package/lib/session-log.js +46 -0
  49. package/lib/sessions/detach.js +113 -0
  50. package/lib/sessions/events.js +73 -0
  51. package/lib/sessions/interruption.js +73 -0
  52. package/lib/sessions/list-cache.js +175 -0
  53. package/lib/sessions/list.js +187 -0
  54. package/lib/sessions/route.js +60 -0
  55. package/lib/sessions/turn.js +21 -0
  56. package/lib/shared/constants.js +20 -0
  57. package/lib/shared/log.js +17 -0
  58. package/lib/shared/settings.js +14 -0
  59. package/lib/shared/test-hooks.js +93 -0
  60. package/lib/shared/util.js +6 -0
  61. package/lib/template-inject.js +289 -0
  62. package/lib/value.js +430 -0
  63. package/lib/zstd-frames.js +177 -0
  64. package/package.json +65 -3
  65. package/skills/dsh-session-conductor-functions.md +197 -0
  66. package/skills/dsh-session-conductor.md +151 -0
  67. package/skills/dsh-session-official-doc.md +245 -0
  68. package/skills/task-completion-report.md +56 -0
@@ -0,0 +1,1265 @@
1
+ function SessionManagerPanel({ wide, t, onOpenSession }) {
2
+ const [open, setOpen] = react.useState(false);
3
+ const [tab, setTab] = react.useState("all");
4
+ const [sessions, setSessions] = react.useState(() => readListCache()); // 启动即从长效缓存加载
5
+ const [groups, setGroups] = react.useState(null); // workspaces[](group/list);null = host 无分组 API
6
+ const [collapsed, setCollapsed] = react.useState(() => new Set()); // 折叠的分组(含 "__ungrouped__")
7
+ const [error, setError] = react.useState(null);
8
+ const [busy, setBusy] = react.useState(null);
9
+ // —— 搜索 + 批量/按条件删除 ——
10
+ const [query, setQuery] = react.useState(""); // 列表过滤关键词
11
+ const [selected, setSelected] = react.useState(() => new Set()); // 批量删除勾选
12
+ const [fullResults, setFullResults] = react.useState(null); // 全文搜索结果(null=未搜索)
13
+ const [fullSearching, setFullSearching] = react.useState(false);
14
+ const [ruleOpen, setRuleOpen] = react.useState(false); // 按条件删除表单展开
15
+ const [ruleArchivedOnly, setRuleArchivedOnly] = react.useState(true);
16
+ const [ruleDays, setRuleDays] = react.useState(7);
17
+ const [rulePrefix, setRulePrefix] = react.useState("");
18
+ const [ruleLowValue, setRuleLowValue] = react.useState(false); // 低价值筛选(复用价值分析)
19
+ const [rulePreview, setRulePreview] = react.useState(null); // dryRun 预览(null=未预览)
20
+
21
+ // list 流式——NDJSON 每行一个会话,读到立即追加显示(每获取一个追加一个,
22
+ // 不等全部);按 updatedAt 倒序插入保持列表顺序;旧一次性 JSON(预览页离线垫片/
23
+ // 缓存路径)走 setSessions 全量。
24
+ const insertSorted = (cur, item) => {
25
+ // 2026-10-12 修复:cur 兜底为数组。历史版本 readListCache() 无缓存时返回 null,
26
+ // 使 sessions 初值为 null,流式追加走到这里就炸(见 foundation/list-cache.js 同款修复)。
27
+ // const next = cur.filter((x) => x.id !== item.id);
28
+ const next = (Array.isArray(cur) ? cur : []).filter((x) => x.id !== item.id);
29
+ let i = 0;
30
+ while (i < next.length && (next[i].updatedAt ?? 0) > (item.updatedAt ?? 0)) i += 1;
31
+ next.splice(i, 0, item);
32
+ return next;
33
+ };
34
+ const refresh = react.useCallback(async () => {
35
+ try {
36
+ const listPromise = window.__scFetch("/api/session-conductor/list", { headers: { Accept: "application/x-ndjson" } });
37
+ const groupPromise = window.__scFetch("/api/session-conductor/group/list")
38
+ .then((r) => (r.ok ? r.json() : { ok: false })).catch(() => ({ ok: false }));
39
+ const listRes = await listPromise;
40
+ if (!listRes.ok) throw new Error("list failed: HTTP " + listRes.status);
41
+ let lastBatch = null;
42
+ if (typeof listRes.body?.getReader === "function") {
43
+ // 流式 NDJSON:逐行解析追加
44
+ const reader = listRes.body.getReader();
45
+ const decoder = new TextDecoder();
46
+ let buf = "";
47
+ const batch = [];
48
+ for (;;) {
49
+ const { done, value } = await reader.read();
50
+ if (done) break;
51
+ buf += decoder.decode(value, { stream: true });
52
+ let nl;
53
+ while ((nl = buf.indexOf("\n")) >= 0) {
54
+ const line = buf.slice(0, nl).trim();
55
+ buf = buf.slice(nl + 1);
56
+ if (!line) continue;
57
+ let parsedLine;
58
+ try { parsedLine = JSON.parse(line); } catch { continue; } // 半行/头行跳过
59
+ if (parsedLine && parsedLine.id) {
60
+ batch.push(parsedLine);
61
+ setSessions((cur) => insertSorted(cur, parsedLine)); // 每获取一个追加一个
62
+ }
63
+ }
64
+ }
65
+ lastBatch = batch;
66
+ } else {
67
+ const body = await listRes.json();
68
+ if (!body.ok) throw new Error(body.error?.message ?? "list failed");
69
+ setSessions(body.sessions ?? []);
70
+ lastBatch = body.sessions ?? [];
71
+ }
72
+ if (lastBatch !== null) writeListCache(lastBatch); // 长效保存:刷新结果落 localStorage
73
+ const groupRes = await groupPromise;
74
+ setGroups(groupRes?.ok === true ? groupRes.workspaces ?? [] : null);
75
+ setError(null);
76
+ } catch (cause) {
77
+ setError(cause instanceof Error ? cause.message : String(cause));
78
+ }
79
+ }, []);
80
+
81
+ // 启动即自动加载(插件挂载就预取一次,不依赖面板打开);打开面板时再刷新一次保证最新
82
+ react.useEffect(() => {
83
+ refresh();
84
+ }, [refresh]);
85
+ react.useEffect(() => {
86
+ if (open) refresh();
87
+ }, [open, refresh]);
88
+
89
+ // 通用会话操作(archive/unarchive/delete):乐观更新——点了立即改 UI,后端异步处理,
90
+ // 失败回滚 + 提示;不阻塞其他会话的操作(不设全局 busy)。
91
+ const act = async (sessionId, action) => {
92
+ let prev = null;
93
+ setSessions((cur) => {
94
+ prev = cur.find((s) => s.id === sessionId) ?? null;
95
+ if (action === "delete") return cur.filter((s) => s.id !== sessionId);
96
+ if (action === "archive" || action === "unarchive") {
97
+ return cur.map((s) => (s.id === sessionId ? { ...s, archived: action === "archive" } : s));
98
+ }
99
+ return cur;
100
+ });
101
+ try {
102
+ const res = await window.__scFetch(`/api/session-conductor/${action}`, {
103
+ method: "POST",
104
+ headers: { "Content-Type": CONTENT_TYPE_JSON },
105
+ body: JSON.stringify({ sessionId }),
106
+ });
107
+ const body = await res.json();
108
+ if (!body.ok) throw new Error(body.error?.message ?? MSG_ACTION_FAILED);
109
+ await refresh();
110
+ } catch (cause) {
111
+ // 回滚乐观更新
112
+ if (prev) {
113
+ setSessions((cur) => {
114
+ if (action === "delete") return cur.some((s) => s.id === sessionId) ? cur : [...cur, prev];
115
+ return cur.map((s) => (s.id === sessionId ? prev : s));
116
+ });
117
+ }
118
+ const message = cause instanceof Error ? cause.message : String(cause);
119
+ window.alert(t("error.action", { message }));
120
+ }
121
+ };
122
+
123
+ const confirmDelete = (session) => {
124
+ const label = session.title || session.cwd || session.id;
125
+ const yes = window.confirm(`${t("confirm.delete.title", { title: label })}\n\n${t("confirm.delete.body")}`);
126
+ if (yes) act(session.id, "delete");
127
+ };
128
+
129
+ // 撤回最后一条消息:dryRun 先预览 → 二次确认 → 执行 → 刷新
130
+ const undoLast = async (session) => {
131
+ setBusy(session.id);
132
+ try {
133
+ const res = await window.__scFetch("/api/session-conductor/undo-message", {
134
+ method: "POST",
135
+ headers: { "Content-Type": CONTENT_TYPE_JSON },
136
+ body: JSON.stringify({ sessionId: session.id, dryRun: true }),
137
+ });
138
+ const body = await res.json();
139
+ if (!body.ok) throw new Error(body.error?.message ?? "preview failed");
140
+ const preview = body.preview || t("undo.preview.empty");
141
+ const yes = window.confirm(`${t("confirm.undo.title")}\n\n${t("confirm.undo.body", { preview })}`);
142
+ if (!yes) return;
143
+ const res2 = await window.__scFetch("/api/session-conductor/undo-message", {
144
+ method: "POST",
145
+ headers: { "Content-Type": CONTENT_TYPE_JSON },
146
+ body: JSON.stringify({ sessionId: session.id }),
147
+ });
148
+ const body2 = await res2.json();
149
+ if (!body2.ok) throw new Error(body2.error?.message ?? "undo failed");
150
+ window.alert(t("undo.done", { preview }));
151
+ await refresh();
152
+ } catch (cause) {
153
+ const message = cause instanceof Error ? cause.message : String(cause);
154
+ window.alert(t("error.action", { message }));
155
+ } finally {
156
+ setBusy(null);
157
+ }
158
+ };
159
+
160
+ // 自动重命名开关:乐观翻转(点了立即变),后端异步处理,失败回滚;不阻塞其他操作。
161
+ const toggleAutoRename = async (sessionId, enabled) => {
162
+ setSessions((cur) => cur.map((s) => (s.id === sessionId ? { ...s, autoRename: enabled } : s)));
163
+ try {
164
+ const res = await window.__scFetch("/api/session-conductor/auto-rename", {
165
+ method: "POST",
166
+ headers: { "Content-Type": CONTENT_TYPE_JSON },
167
+ body: JSON.stringify({ sessionId, enabled }),
168
+ });
169
+ const body = await res.json();
170
+ if (!body.ok) throw new Error(body.error?.message ?? MSG_ACTION_FAILED);
171
+ // 开启时后端立即分析,若已改名(analyzed.after)→ 马上用新标题重显示,
172
+ // 不等 refresh 全量冷扫描(可能数秒~数十秒)
173
+ if (enabled && body.analyzed?.ok && typeof body.analyzed?.after === "string" && body.analyzed.after !== "") {
174
+ setSessions((cur) => cur.map((s) => (s.id === sessionId ? { ...s, title: body.analyzed.after } : s)));
175
+ }
176
+ await refresh();
177
+ } catch (cause) {
178
+ setSessions((cur) => cur.map((s) => (s.id === sessionId ? { ...s, autoRename: !enabled } : s)));
179
+ const message = cause instanceof Error ? cause.message : String(cause);
180
+ window.alert(t("error.action", { message }));
181
+ }
182
+ };
183
+
184
+ const analyzeNow = async (sessionId) => {
185
+ setBusy(sessionId);
186
+ try {
187
+ const res = await window.__scFetch("/api/session-conductor/analyze", {
188
+ method: "POST",
189
+ headers: { "Content-Type": CONTENT_TYPE_JSON },
190
+ body: JSON.stringify({ sessionId }),
191
+ });
192
+ const body = await res.json();
193
+ if (!body.ok) {
194
+ const message = body.error?.message ?? MSG_ACTION_FAILED;
195
+ window.alert(`${t("action.analyze")}: ${message}`);
196
+ } else {
197
+ window.alert(body.renamed ? `${t("action.analyze")}: ${body.reason} → ${body.title}` : `${t("action.analyze")}: ${body.reason}`);
198
+ }
199
+ await refresh();
200
+ } catch (cause) {
201
+ const message = cause instanceof Error ? cause.message : String(cause);
202
+ window.alert(t("error.action", { message }));
203
+ } finally {
204
+ setBusy(null);
205
+ }
206
+ };
207
+
208
+ /** 扫描并修复损坏会话日志(先 dry-run 扫描,确认后真正修复)。 */
209
+ const scanAndRepair = async () => {
210
+ if (busy) return;
211
+ setBusy("__repair__");
212
+ try {
213
+ const scanRes = await window.__scFetch("/api/session-conductor/repair-sessions", {
214
+ method: "POST",
215
+ headers: { "Content-Type": CONTENT_TYPE_JSON },
216
+ body: JSON.stringify({ dryRun: true }),
217
+ });
218
+ const scan = await scanRes.json();
219
+ if (!scan.ok) throw new Error(scan.error?.message ?? "scan failed");
220
+ const corrupt = scan.report?.corrupt ?? 0;
221
+ const msg = corrupt === 0
222
+ ? t("repair.scanClean")
223
+ : t("repair.confirm", { n: corrupt });
224
+ if (corrupt > 0 && !window.confirm(msg)) { setBusy(null); return; }
225
+ if (corrupt === 0) { window.alert(msg); setBusy(null); return; }
226
+ const fixRes = await window.__scFetch("/api/session-conductor/repair-sessions", {
227
+ method: "POST",
228
+ headers: { "Content-Type": CONTENT_TYPE_JSON },
229
+ body: JSON.stringify({ dryRun: false }),
230
+ });
231
+ const fix = await fixRes.json();
232
+ if (!fix.ok) throw new Error(fix.error?.message ?? "repair failed");
233
+ window.alert(t("repair.done", { n: fix.report?.fixed ?? 0 }));
234
+ await refresh();
235
+ } catch (cause) {
236
+ const message = cause instanceof Error ? cause.message : String(cause);
237
+ window.alert(t("error.action", { message }));
238
+ } finally {
239
+ setBusy(null);
240
+ }
241
+ };
242
+
243
+ /** 扫描并修复 EIO 坏块会话(先 dry-run 扫描,确认后真正修复)。
244
+ * 需求来源:「failed to observe session xxx: EIO: i/o error, read」+
245
+ * 「把这个功能写进会话管理插件…新增修复会话」。
246
+ * 思路:与 scanAndRepair 同构(scan → confirm → fix),但走 /repair-eio 端点,
247
+ * 该端点做块级 EIO 探测与截断修复(见 lib/eio-repair.js)。 */
248
+ const scanAndRepairEio = async () => {
249
+ if (busy) return;
250
+ setBusy("__repair_eio__");
251
+ try {
252
+ const scanRes = await window.__scFetch("/api/session-conductor/repair-eio", {
253
+ method: "POST",
254
+ headers: { "Content-Type": CONTENT_TYPE_JSON },
255
+ body: JSON.stringify({ dryRun: true }),
256
+ });
257
+ const scan = await scanRes.json();
258
+ if (!scan.ok) throw new Error(scan.error?.message ?? "eio scan failed");
259
+ const eioCount = (scan.eio ?? []).length;
260
+ const msg = eioCount === 0
261
+ ? t("repairEio.scanClean")
262
+ : t("repairEio.confirm", { n: eioCount });
263
+ if (eioCount === 0) { window.alert(msg); setBusy(null); return; }
264
+ if (!window.confirm(msg)) { setBusy(null); return; }
265
+ const fixRes = await window.__scFetch("/api/session-conductor/repair-eio", {
266
+ method: "POST",
267
+ headers: { "Content-Type": CONTENT_TYPE_JSON },
268
+ body: JSON.stringify({ dryRun: false }),
269
+ });
270
+ const fix = await fixRes.json();
271
+ if (!fix.ok) throw new Error(fix.error?.message ?? "eio repair failed");
272
+ window.alert(t("repairEio.done", { n: fix.report?.fixed ?? 0 }));
273
+ await refresh();
274
+ } catch (cause) {
275
+ const message = cause instanceof Error ? cause.message : String(cause);
276
+ window.alert(t("error.action", { message }));
277
+ } finally {
278
+ setBusy(null);
279
+ }
280
+ };
281
+
282
+ /** 扫描并修复双格式会话(先 dry-run 扫描,确认后真正修复)。
283
+ * 需求来源:会话目录同时存在 session.jsonl + session.jsonl.zstd 时官方
284
+ * listArtifacts() 抛 encodingMismatch → 会话列表全失败(侧边栏会话消失)。
285
+ * 思路:与 scanAndRepairEio 同构(scan → confirm → fix),但走
286
+ * /repair-dual-format 端点(纯磁盘扫描,旁路 persistence,见 lib/repair.js)。 */
287
+ const scanAndRepairDual = async () => {
288
+ if (busy) return;
289
+ setBusy("__repair_dual__");
290
+ try {
291
+ const scanRes = await window.__scFetch("/api/session-conductor/repair-dual-format", {
292
+ method: "POST",
293
+ headers: { "Content-Type": CONTENT_TYPE_JSON },
294
+ body: JSON.stringify({ dryRun: true }),
295
+ });
296
+ const scan = await scanRes.json();
297
+ if (!scan.ok) throw new Error(scan.error?.message ?? "dual-format scan failed");
298
+ const dualCount = scan.report?.dual ?? 0;
299
+ const msg = dualCount === 0
300
+ ? t("repairDual.scanClean")
301
+ : t("repairDual.confirm", { n: dualCount });
302
+ if (dualCount === 0) { window.alert(msg); setBusy(null); return; }
303
+ if (!window.confirm(msg)) { setBusy(null); return; }
304
+ const fixRes = await window.__scFetch("/api/session-conductor/repair-dual-format", {
305
+ method: "POST",
306
+ headers: { "Content-Type": CONTENT_TYPE_JSON },
307
+ body: JSON.stringify({ dryRun: false }),
308
+ });
309
+ const fix = await fixRes.json();
310
+ if (!fix.ok) throw new Error(fix.error?.message ?? "dual-format repair failed");
311
+ window.alert(t("repairDual.done", { n: fix.report?.fixed ?? 0 }));
312
+ await refresh();
313
+ } catch (cause) {
314
+ const message = cause instanceof Error ? cause.message : String(cause);
315
+ window.alert(t("error.action", { message }));
316
+ } finally {
317
+ setBusy(null);
318
+ }
319
+ };
320
+
321
+ // 自动续跑开关:乐观翻转(点了立即变),后端异步处理,失败回滚;不阻塞其他操作。
322
+ const toggleAutoContinue = async (sessionId, enabled) => {
323
+ setSessions((cur) => cur.map((s) => (s.id === sessionId ? { ...s, autoContinue: enabled } : s)));
324
+ try {
325
+ const res = await window.__scFetch("/api/session-conductor/auto-continue", {
326
+ method: "POST",
327
+ headers: { "Content-Type": CONTENT_TYPE_JSON },
328
+ body: JSON.stringify({ sessionId, enabled }),
329
+ });
330
+ const body = await res.json();
331
+ if (!body.ok) throw new Error(body.error?.message ?? MSG_ACTION_FAILED);
332
+ await refresh();
333
+ } catch (cause) {
334
+ setSessions((cur) => cur.map((s) => (s.id === sessionId ? { ...s, autoContinue: !enabled } : s)));
335
+ const message = cause instanceof Error ? cause.message : String(cause);
336
+ window.alert(t("error.action", { message }));
337
+ }
338
+ };
339
+
340
+ const releaseNow = async (sessionId) => {
341
+ setBusy(sessionId);
342
+ try {
343
+ const res = await window.__scFetch("/api/session-conductor/detach", {
344
+ method: "POST",
345
+ headers: { "Content-Type": CONTENT_TYPE_JSON },
346
+ body: JSON.stringify({ sessionId }),
347
+ });
348
+ const body = await res.json();
349
+ if (!body.ok) {
350
+ window.alert(t("error.action", { message: body.error?.message ?? "release failed" }));
351
+ }
352
+ await refresh();
353
+ } catch (cause) {
354
+ const message = cause instanceof Error ? cause.message : String(cause);
355
+ window.alert(t("error.action", { message }));
356
+ } finally {
357
+ setBusy(null);
358
+ }
359
+ };
360
+
361
+ const releaseAll = async () => {
362
+ if (busy) return;
363
+ const label = t("confirm.releaseAll.title");
364
+ const yes = window.confirm(`${label}\n\n${t("confirm.releaseAll.body")}`);
365
+ if (!yes) return;
366
+ setBusy("__all__");
367
+ try {
368
+ const res = await window.__scFetch("/api/session-conductor/detach-all", {
369
+ method: "POST",
370
+ headers: { "Content-Type": CONTENT_TYPE_JSON },
371
+ body: JSON.stringify({}),
372
+ });
373
+ const body = await res.json();
374
+ if (!body.ok) {
375
+ window.alert(t("error.action", { message: body.error?.message ?? "release all failed" }));
376
+ } else if (body.released?.length > 0) {
377
+ window.alert(`${t("action.releaseAll")}: ${body.released.length}`);
378
+ }
379
+ await refresh();
380
+ } catch (cause) {
381
+ const message = cause instanceof Error ? cause.message : String(cause);
382
+ window.alert(t("error.action", { message }));
383
+ } finally {
384
+ setBusy(null);
385
+ }
386
+ };
387
+
388
+ const continueNow = async (sessionId) => {
389
+ setBusy(sessionId);
390
+ try {
391
+ const res = await window.__scFetch("/api/session-conductor/continue", {
392
+ method: "POST",
393
+ headers: { "Content-Type": CONTENT_TYPE_JSON },
394
+ body: JSON.stringify({ sessionId }),
395
+ });
396
+ const body = await res.json();
397
+ if (!body.ok) {
398
+ window.alert(t("error.action", { message: body.error?.message ?? "continue failed" }));
399
+ }
400
+ await refresh();
401
+ // 续跑进行中:轮询刷新几次以反映 continueRunning / running 状态
402
+ const POLL_TIMES = 6; // 续跑后轮询刷新次数
403
+ const POLL_INTERVAL_MS = 3000; // 轮询间隔
404
+ for (let i = 0; i < POLL_TIMES; i++) {
405
+ await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
406
+ await refresh();
407
+ }
408
+ } catch (cause) {
409
+ const message = cause instanceof Error ? cause.message : String(cause);
410
+ window.alert(t("error.action", { message }));
411
+ } finally {
412
+ setBusy(null);
413
+ }
414
+ };
415
+
416
+ // —— 全文搜索(官方 FTS5 优先,不可用时降级内置扫描)——
417
+ const SEARCH_LIMIT = 50; // 单次搜索最大返回条数
418
+ const runFullSearch = async (keyword) => {
419
+ const keywordText = String(keyword ?? query ?? "").trim();
420
+ if (keywordText.length < 2) return;
421
+ setFullSearching(true);
422
+ setFullResults(null);
423
+ try {
424
+ let res = await window.__scFetch("/api/session-search/query", {
425
+ method: "POST",
426
+ headers: { "Content-Type": CONTENT_TYPE_JSON },
427
+ body: JSON.stringify({ query: keywordText, limit: SEARCH_LIMIT }),
428
+ });
429
+ let body = res.ok ? await res.json() : null;
430
+ let usedFallback = false;
431
+ if (!body?.ok || !Array.isArray(body?.items)) {
432
+ // 官方搜索不可用 → 降级到本插件内置扫描
433
+ res = await window.__scFetch("/api/session-conductor/search", {
434
+ method: "POST",
435
+ headers: { "Content-Type": CONTENT_TYPE_JSON },
436
+ body: JSON.stringify({ query: keywordText, scope: "all" }),
437
+ });
438
+ body = res.ok ? await res.json() : null;
439
+ usedFallback = true;
440
+ }
441
+ if (!body?.ok) throw new Error(body?.error?.message ?? "search failed");
442
+ const hits = (body.items ?? []).map((h) => ({
443
+ sessionId: h.sessionId,
444
+ title: h.title ?? null,
445
+ cwd: h.cwd ?? null,
446
+ archived: h.archived === true,
447
+ running: h.running === true,
448
+ updatedAt: h.updatedAt ?? null,
449
+ matches: Array.isArray(h.matches) ? h.matches : (h.match ? [h.match] : []),
450
+ }));
451
+ setFullResults({ query: keywordText, hits, usedFallback });
452
+ } catch (cause) {
453
+ const message = cause instanceof Error ? cause.message : String(cause);
454
+ window.alert(t("error.action", { message }));
455
+ } finally {
456
+ setFullSearching(false);
457
+ }
458
+ };
459
+
460
+ // —— 批量删除选中 ——
461
+ const deleteSelected = async () => {
462
+ const ids = [...selected];
463
+ if (ids.length === 0) return;
464
+ const yes = window.confirm(`${t("deleteBatch.confirm.title", { n: ids.length })}\n\n${t("deleteBatch.confirm.body")}`);
465
+ if (!yes) return;
466
+ setBusy("__batch__");
467
+ try {
468
+ const res = await window.__scFetch("/api/session-conductor/delete-batch", {
469
+ method: "POST",
470
+ headers: { "Content-Type": CONTENT_TYPE_JSON },
471
+ body: JSON.stringify({ sessionIds: ids }),
472
+ });
473
+ const body = await res.json();
474
+ if (!body.ok) throw new Error(body.error?.message ?? "delete-batch failed");
475
+ const skippedText = body.skipped?.length > 0 ? t("deleteBatch.skipped", { n: body.skipped.length }) : "";
476
+ window.alert(t("deleteBatch.done", { deleted: body.deleted?.length ?? 0, skippedText }));
477
+ setSelected(new Set());
478
+ await refresh();
479
+ } catch (cause) {
480
+ const message = cause instanceof Error ? cause.message : String(cause);
481
+ window.alert(t("error.action", { message }));
482
+ } finally {
483
+ setBusy(null);
484
+ }
485
+ };
486
+
487
+ // —— 按条件删除(预览 + 执行)——
488
+ // 预览页离线模式(window.__BACKEND__ === ""):按条件删除在快照数据上本地计算预览
489
+ // (真实 GUI 走后端 API;预览页点预览可立即看到命中,无需后端)
490
+ const previewLocally = () => {
491
+ const ep = window.__ENDPOINTS__ || {};
492
+ const listBody = ep["/api/session-conductor/list"];
493
+ const vaBody = ep["/api/session-conductor/value-analysis"];
494
+ const rows = (listBody?.sessions ?? []).map((s) => ({
495
+ sessionId: s.id,
496
+ title: s.title,
497
+ cwd: s.cwd,
498
+ archived: s.archived === true,
499
+ updatedAt: typeof s.updatedAt === "number" ? s.updatedAt : null,
500
+ }));
501
+ const now = Date.now();
502
+ const lowIds = new Set((vaBody?.low ?? []).map((i) => i.id));
503
+ const prefix = rulePrefix.trim().replace(/\/+$/, "");
504
+ const days = Number(ruleDays) || 0;
505
+ const matched = rows.filter((m) => {
506
+ if (ruleArchivedOnly && !m.archived) return false;
507
+ if (days > 0) {
508
+ const updated = m.updatedAt;
509
+ if (!Number.isFinite(updated) || now - updated < days * 86400000) return false;
510
+ }
511
+ if (prefix !== "") {
512
+ const cwd = String(m.cwd ?? "").replace(/\/+$/, "");
513
+ if (cwd !== prefix && !cwd.startsWith(prefix + "/")) return false;
514
+ }
515
+ if (ruleLowValue && !lowIds.has(m.sessionId)) return false;
516
+ return true;
517
+ });
518
+ return matched;
519
+ };
520
+
521
+ const previewRuleDelete = async () => {
522
+ setBusy("__rule__");
523
+ setRulePreview(null);
524
+ if (typeof window.__BACKEND__ !== "undefined" && window.__BACKEND__ === "") {
525
+ // 预览页离线:本地计算
526
+ try {
527
+ const matched = previewLocally();
528
+ setRulePreview(matched);
529
+ if (matched.length === 0) window.alert(t("deleteByRule.noMatch"));
530
+ } catch (cause) {
531
+ window.alert(t("error.action", { message: cause instanceof Error ? cause.message : String(cause) }));
532
+ } finally {
533
+ setBusy(null);
534
+ }
535
+ return;
536
+ }
537
+ try {
538
+ const res = await window.__scFetch("/api/session-conductor/delete-by-rule", {
539
+ method: "POST",
540
+ headers: { "Content-Type": CONTENT_TYPE_JSON },
541
+ body: JSON.stringify({
542
+ archivedOnly: ruleArchivedOnly,
543
+ inactiveDays: Number(ruleDays) || 0,
544
+ cwdPrefix: rulePrefix.trim(),
545
+ lowValue: ruleLowValue,
546
+ dryRun: true,
547
+ }),
548
+ });
549
+ const body = await res.json();
550
+ if (!body.ok) throw new Error(body.error?.message ?? "preview failed");
551
+ setRulePreview(body.matched ?? []);
552
+ if (!body.matched || body.matched.length === 0) window.alert(t("deleteByRule.noMatch"));
553
+ } catch (cause) {
554
+ const message = cause instanceof Error ? cause.message : String(cause);
555
+ window.alert(t("error.action", { message }));
556
+ } finally {
557
+ setBusy(null);
558
+ }
559
+ };
560
+
561
+ const runRuleDelete = async () => {
562
+ if (!rulePreview || rulePreview.length === 0) return;
563
+ // 预览页离线模式:不执行真实删除(破坏性操作需真实后端),提示到 DSH GUI
564
+ if (typeof window.__BACKEND__ !== "undefined" && window.__BACKEND__ === "") {
565
+ window.alert(t("deleteByRule.offlineHint"));
566
+ return;
567
+ }
568
+ const yes = window.confirm(t("deleteByRule.preview", { n: rulePreview.length }));
569
+ if (!yes) return;
570
+ setBusy("__rule__");
571
+ try {
572
+ const res = await window.__scFetch("/api/session-conductor/delete-by-rule", {
573
+ method: "POST",
574
+ headers: { "Content-Type": CONTENT_TYPE_JSON },
575
+ body: JSON.stringify({
576
+ archivedOnly: ruleArchivedOnly,
577
+ inactiveDays: Number(ruleDays) || 0,
578
+ cwdPrefix: rulePrefix.trim(),
579
+ lowValue: ruleLowValue,
580
+ dryRun: false,
581
+ }),
582
+ });
583
+ const body = await res.json();
584
+ if (!body.ok) throw new Error(body.error?.message ?? "delete failed");
585
+ window.alert(t("deleteBatch.done", {
586
+ deleted: body.deleted?.length ?? 0,
587
+ skippedText: body.skipped?.length > 0 ? t("deleteBatch.skipped", { n: body.skipped.length }) : "",
588
+ }));
589
+ setRuleOpen(false);
590
+ setRulePreview(null);
591
+ await refresh();
592
+ } catch (cause) {
593
+ const message = cause instanceof Error ? cause.message : String(cause);
594
+ window.alert(t("error.action", { message }));
595
+ } finally {
596
+ setBusy(null);
597
+ }
598
+ };
599
+
600
+ const rows = sessions ?? [];
601
+ // 列表过滤:标题 / cwd / id / 状态词(大小写不敏感)
602
+ const ql = query.trim().toLowerCase();
603
+ const matchesQuery = (row) => {
604
+ if (!ql) return true;
605
+ const hay = [
606
+ row.title ?? "", row.cwd ?? "", row.id ?? "",
607
+ row.archived ? "已归档 archived" : "",
608
+ row.running ? "运行中 running" : "",
609
+ row.interruption ? "已中断 interrupted" : "",
610
+ ].join(" ").toLowerCase();
611
+ return hay.includes(ql);
612
+ };
613
+ // tab=active 显示活跃会话(live 挂载中 或 running 运行中),归档的除外
614
+ const shown = tab === "archived" ? rows.filter((row) => row.archived)
615
+ : tab === "active" ? rows.filter((row) => !row.archived && (row.live === true || row.running === true))
616
+ : rows;
617
+ const filteredShown = shown.filter(matchesQuery);
618
+
619
+ /** 通用分桶(「全部会话」与「已归档」共用,避免两套分组实现各写一份):
620
+ * keyOf(session) 返回桶键(空 → 落 rest「未分组」);makeBucket(key) 造桶对象;
621
+ * 只保留有会话的桶(0 会话分组不显示)。 */
622
+ const bucketSessions = (list, keyOf, makeBucket) => {
623
+ const buckets = [];
624
+ const index = new Map();
625
+ const rest = [];
626
+ for (const s of list) {
627
+ const key = keyOf(s);
628
+ if (!key) { rest.push(s); continue; }
629
+ if (!index.has(key)) {
630
+ const b = makeBucket(key);
631
+ index.set(key, b);
632
+ buckets.push(b);
633
+ }
634
+ index.get(key).sessions.push(s);
635
+ }
636
+ return { buckets, rest };
637
+ };
638
+
639
+ /** 按工作区分组:会话归入「其 cwd 的最深祖先分组」——
640
+ * cwd === 分组 path,或 cwd 位于分组 path 目录之下(path 是 cwd 的祖先目录);
641
+ * 多个分组匹配时取 path 最深者(如 move-test 子目录会话归 move-test,其余归 workspace 根)。
642
+ * 只保留有会话的分组(0 会话分组不显示);无任何匹配的归入「未分组」。 */
643
+ const groupByWorkspace = (list) => {
644
+ if (!Array.isArray(groups)) return null; // host 无分组 API → 平铺
645
+ const norm = (p) => String(p ?? "").replace(/\/+$/, "");
646
+ /** 会话归属的分组对象(最深祖先;无匹配返回 null)。 */
647
+ const groupOfSession = (s) => {
648
+ const cwd = norm(s.cwd);
649
+ if (!cwd) return null;
650
+ let best = null;
651
+ let bestLen = -1;
652
+ for (const w of groups) {
653
+ const p = norm(w.path);
654
+ if (p !== "" && (cwd === p || cwd.startsWith(p + "/")) && p.length > bestLen) {
655
+ best = w;
656
+ bestLen = p.length;
657
+ }
658
+ }
659
+ return best;
660
+ };
661
+ return bucketSessions(
662
+ list,
663
+ (s) => norm(groupOfSession(s)?.path ?? ""),
664
+ (path) => {
665
+ const workspaceEntry = groups.find((x) => norm(x.path) === path) ?? {};
666
+ return { workspaceId: workspaceEntry.workspaceId, title: workspaceEntry.title, path: workspaceEntry.path, sessions: [] };
667
+ },
668
+ );
669
+ };
670
+
671
+ /** 已归档视图分组:**复用上面同一套工作区分组**。
672
+ * 历史实现按 `s.archiveWs` 分桶,但该字段实测**恒为空**(66 个已归档会话全部为空),
673
+ * 于是所有会话都落进「未分组」,看起来就是「已归档里没有正确分组」。
674
+ * 归档会话的 cwd 是有效的(数据层保留),故直接按 cwd 的工作区分组即可正确分节。
675
+ * host 无分组 API 时退化为平铺(与「全部会话」同口径)。 */
676
+ const groupByArchiveWs = (list) => groupByWorkspace(list);
677
+
678
+ const grouped = tab === "all" ? groupByWorkspace(filteredShown) : tab === "archived" ? groupByArchiveWs(filteredShown) : null;
679
+
680
+ const toggleCollapse = (key) => {
681
+ setCollapsed((prev) => {
682
+ const next = new Set(prev);
683
+ if (next.has(key)) next.delete(key);
684
+ else next.add(key);
685
+ return next;
686
+ });
687
+ };
688
+
689
+ const renderRow = (session) => {
690
+ // 已归档会话标题数据层带「[工作区名] 」前缀,显示层剥离(只显示原标题)
691
+ let label = session.title || basenameOf(session.cwd) || session.id;
692
+ if (session.archived && session.archiveWs && label.startsWith(`[${session.archiveWs}] `)) {
693
+ label = label.slice(session.archiveWs.length + 3); // 去掉 "[工作区名] "
694
+ }
695
+ const meta = [session.cwd ? basenameOf(session.cwd) : "", timeAgo(session.updatedAt, t)]
696
+ .filter(Boolean)
697
+ .join(" · ");
698
+ // running 由顶部「● 运行中」独立显示(含 live),archived/interrupted 走 badge
699
+ const badge = session.archived ? "archived" : session.interruption ? "interrupted" : null;
700
+ const processing = busy === session.id || session.continueRunning === true;
701
+ const interruptionTitle = session.interruption ? t("interruption.tooltip", { message: session.interruption.message ?? session.interruption.kind }) : undefined;
702
+ // 行内改为纵向卡片——顶部(批量勾选+状态徽章)/ 中间(内容)/ 下方(操作集中)
703
+ return (0, react_jsx_runtime.jsx)("div", {
704
+ key: session.id, // map 渲染列表时必需(filteredShown.map / g.sessions.map)
705
+ style: S.row,
706
+ onClick: () => {
707
+ if (onOpenSession && !processing) {
708
+ onOpenSession(session.id);
709
+ setOpen(false);
710
+ }
711
+ },
712
+ children: [
713
+ // —— 顶部:批量勾选 + 状态徽章 ——
714
+ (0, react_jsx_runtime.jsxs)("div", {
715
+ style: S.rowTop,
716
+ children: [
717
+ (0, react_jsx_runtime.jsx)("label", {
718
+ style: { display: "flex", alignItems: "center", cursor: "pointer", flex: "none" },
719
+ title: t("deleteBatch.action", { n: 1 }),
720
+ onClick: (event) => event.stopPropagation(),
721
+ children: (0, react_jsx_runtime.jsx)("input", {
722
+ type: "checkbox",
723
+ checked: selected.has(session.id),
724
+ onChange: (event) => {
725
+ setSelected((prev) => {
726
+ const next = new Set(prev);
727
+ if (event.target.checked) next.add(session.id);
728
+ else next.delete(session.id);
729
+ return next;
730
+ });
731
+ },
732
+ }),
733
+ }),
734
+ (0, react_jsx_runtime.jsxs)("div", {
735
+ style: S.rowTopBadges,
736
+ children: [
737
+ session.running === true && (0, react_jsx_runtime.jsx)("span", {
738
+ style: { ...S.badge("running"), display: "inline-flex", alignItems: "center", gap: 4 },
739
+ children: ["● ", t("badge.running")],
740
+ }),
741
+ session.live === true && !session.running && (0, react_jsx_runtime.jsx)("span", {
742
+ style: { ...S.badge("running"), display: "inline-flex", alignItems: "center", gap: 4 },
743
+ children: [t("badge.live")],
744
+ }),
745
+ badge !== null && (0, react_jsx_runtime.jsx)("span", {
746
+ style: S.badge(badge),
747
+ title: badge === "interrupted" ? interruptionTitle : undefined,
748
+ children: badge === "archived" ? t("badge.archived") : t("badge.interrupted"),
749
+ }),
750
+ ],
751
+ }),
752
+ ],
753
+ }),
754
+ // —— 中间:会话内容 ——
755
+ (0, react_jsx_runtime.jsxs)("div", {
756
+ style: S.rowMain,
757
+ children: [
758
+ (0, react_jsx_runtime.jsx)("div", {
759
+ style: { ...S.rowTitle, gap: 0 },
760
+ children: (0, react_jsx_runtime.jsx)("span", {
761
+ style: { overflow: "hidden", display: "-webkit-box", WebkitLineClamp: 3, WebkitBoxOrient: "vertical", wordBreak: "break-word" },
762
+ children: label,
763
+ }),
764
+ }),
765
+ (0, react_jsx_runtime.jsx)("div", {
766
+ style: S.rowMeta,
767
+ children: meta || session.id,
768
+ }),
769
+ (0, react_jsx_runtime.jsx)("div", {
770
+ style: S.rowSid,
771
+ title: "session id",
772
+ children: session.id,
773
+ }),
774
+ ],
775
+ }),
776
+ // —— 下方:操作集中(开关 + 按钮)——
777
+ (0, react_jsx_runtime.jsxs)("div", {
778
+ style: S.rowOps,
779
+ onClick: (event) => event.stopPropagation(),
780
+ children: [
781
+ (0, react_jsx_runtime.jsxs)("div", {
782
+ style: S.opsToggles,
783
+ children: [
784
+ (0, react_jsx_runtime.jsxs)("label", {
785
+ style: S.autoToggle,
786
+ title: t("help.autoRename.title"),
787
+ children: [
788
+ (0, react_jsx_runtime.jsx)("input", {
789
+ type: "checkbox",
790
+ checked: session.autoRename === true,
791
+ disabled: processing,
792
+ onChange: (event) => toggleAutoRename(session.id, event.target.checked),
793
+ }),
794
+ (0, react_jsx_runtime.jsx)("span", { children: t("action.autoRename") }),
795
+ ],
796
+ }),
797
+ (0, react_jsx_runtime.jsxs)("label", {
798
+ style: S.autoToggle,
799
+ title: t("help.autoContinue.title"),
800
+ children: [
801
+ (0, react_jsx_runtime.jsx)("input", {
802
+ type: "checkbox",
803
+ checked: session.autoContinue === true,
804
+ disabled: processing,
805
+ onChange: (event) => toggleAutoContinue(session.id, event.target.checked),
806
+ }),
807
+ (0, react_jsx_runtime.jsx)("span", { children: t("action.autoContinue") }),
808
+ ],
809
+ }),
810
+ ],
811
+ }),
812
+ (0, react_jsx_runtime.jsxs)("div", {
813
+ style: S.opsButtons,
814
+ children: [
815
+ session.autoRename === true &&
816
+ (0, react_jsx_runtime.jsx)("button", {
817
+ style: S.actionBtn(false),
818
+ disabled: processing,
819
+ onClick: () => analyzeNow(session.id),
820
+ children: processing ? t("action.busy") : t("action.analyze"),
821
+ }),
822
+ session.interruption !== null && session.interruption !== undefined && !session.running &&
823
+ (0, react_jsx_runtime.jsx)("button", {
824
+ style: { ...S.actionBtn(false), borderColor: "var(--dsw-state-warning-primary, #e6a23c)", color: "var(--dsw-state-warning-primary, #e6a23c)" },
825
+ disabled: processing,
826
+ onClick: () => continueNow(session.id),
827
+ children: session.continueRunning === true ? t("action.continueRunning") : t("action.continue"),
828
+ }),
829
+ session.live === true && !session.running && session.continueRunning !== true &&
830
+ (0, react_jsx_runtime.jsx)("button", {
831
+ style: S.actionBtn(false),
832
+ disabled: processing,
833
+ onClick: () => releaseNow(session.id),
834
+ children: processing ? t("action.busy") : t("action.release"),
835
+ }),
836
+ session.archived
837
+ ? (0, react_jsx_runtime.jsx)("button", {
838
+ style: S.actionBtn(false),
839
+ disabled: processing,
840
+ onClick: () => act(session.id, "unarchive"),
841
+ children: processing ? t("action.busy") : t("action.restore"),
842
+ })
843
+ : (0, react_jsx_runtime.jsx)("button", {
844
+ style: S.actionBtn(false),
845
+ disabled: processing,
846
+ onClick: () => act(session.id, "archive"),
847
+ children: processing ? t("action.busy") : t("action.archive"),
848
+ }),
849
+ !session.running && !session.continueRunning &&
850
+ (0, react_jsx_runtime.jsx)("button", {
851
+ style: S.actionBtn(false),
852
+ disabled: processing,
853
+ title: t("help.undo.title"),
854
+ onClick: () => undoLast(session),
855
+ children: processing ? t("action.busy") : t("action.undo"),
856
+ }),
857
+ (0, react_jsx_runtime.jsx)("button", {
858
+ style: S.actionBtn(true),
859
+ disabled: processing || session.running,
860
+ onClick: () => confirmDelete(session),
861
+ children: processing ? t("action.busy") : t("action.delete"),
862
+ }),
863
+ ],
864
+ }),
865
+ ],
866
+ }),
867
+ ],
868
+ });
869
+ };
870
+
871
+ const renderHelp = () => (0, react_jsx_runtime.jsxs)("div", {
872
+ children: [
873
+ (0, react_jsx_runtime.jsx)("p", {
874
+ style: S.hint,
875
+ children: t("help.tip"),
876
+ }),
877
+ (0, react_jsx_runtime.jsxs)("div", {
878
+ style: { ...S.helpBlock, display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" },
879
+ children: [
880
+ (0, react_jsx_runtime.jsx)("p", {
881
+ style: { ...S.helpDesc, margin: 0, flex: 1, minWidth: HELP_DESC_MIN_WIDTH },
882
+ children: t("help.repair.desc"),
883
+ }),
884
+ (0, react_jsx_runtime.jsx)("button", {
885
+ style: S.actionBtn(false),
886
+ disabled: busy !== null,
887
+ onClick: scanAndRepair,
888
+ children: busy !== null ? t("action.busy") : t("action.repair"),
889
+ }),
890
+ ],
891
+ }),
892
+ (0, react_jsx_runtime.jsxs)("div", {
893
+ style: { ...S.helpBlock, display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" },
894
+ children: [
895
+ (0, react_jsx_runtime.jsx)("p", {
896
+ style: { ...S.helpDesc, margin: 0, flex: 1, minWidth: HELP_DESC_MIN_WIDTH },
897
+ children: t("help.repairEio.desc"),
898
+ }),
899
+ (0, react_jsx_runtime.jsx)("button", {
900
+ style: S.actionBtn(false),
901
+ disabled: busy !== null,
902
+ onClick: scanAndRepairEio,
903
+ children: busy !== null ? t("action.busy") : t("action.repairEio"),
904
+ }),
905
+ ],
906
+ }),
907
+ (0, react_jsx_runtime.jsxs)("div", {
908
+ style: { ...S.helpBlock, display: "flex", alignItems: "center", gap: 10, flexWrap: "wrap" },
909
+ children: [
910
+ (0, react_jsx_runtime.jsx)("p", {
911
+ style: { ...S.helpDesc, margin: 0, flex: 1, minWidth: HELP_DESC_MIN_WIDTH },
912
+ children: t("help.repairDual.desc"),
913
+ }),
914
+ (0, react_jsx_runtime.jsx)("button", {
915
+ style: S.actionBtn(false),
916
+ disabled: busy !== null,
917
+ onClick: scanAndRepairDual,
918
+ children: busy !== null ? t("action.busy") : t("action.repairDual"),
919
+ }),
920
+ ],
921
+ }),
922
+ (0, react_jsx_runtime.jsx)("p", {
923
+ style: { ...S.hint, fontWeight: LABEL_FONT_WEIGHT },
924
+ children: t("help.works"),
925
+ }),
926
+ (0, react_jsx_runtime.jsxs)("div", {
927
+ style: S.helpBlock,
928
+ children: [
929
+ (0, react_jsx_runtime.jsx)("p", { style: S.helpTitle, children: t("help.autoRename.title") }),
930
+ (0, react_jsx_runtime.jsx)("p", { style: S.helpDesc, children: t("help.autoRename.desc") }),
931
+ ],
932
+ }),
933
+ (0, react_jsx_runtime.jsxs)("div", {
934
+ style: S.helpBlock,
935
+ children: [
936
+ (0, react_jsx_runtime.jsx)("p", { style: S.helpTitle, children: t("help.autoContinue.title") }),
937
+ (0, react_jsx_runtime.jsx)("p", { style: S.helpDesc, children: t("help.autoContinue.desc") }),
938
+ ],
939
+ }),
940
+ (0, react_jsx_runtime.jsxs)("div", {
941
+ style: S.helpBlock,
942
+ children: [
943
+ (0, react_jsx_runtime.jsx)("p", { style: S.helpTitle, children: t("help.release.title") }),
944
+ (0, react_jsx_runtime.jsx)("p", { style: S.helpDesc, children: t("help.release.desc") }),
945
+ ],
946
+ }),
947
+ (0, react_jsx_runtime.jsxs)("div", {
948
+ style: S.helpBlock,
949
+ children: [
950
+ (0, react_jsx_runtime.jsx)("p", { style: S.helpTitle, children: t("help.delete.title") }),
951
+ (0, react_jsx_runtime.jsx)("p", { style: S.helpDesc, children: t("help.delete.desc") }),
952
+ ],
953
+ }),
954
+ (0, react_jsx_runtime.jsxs)("div", {
955
+ style: S.helpBlock,
956
+ children: [
957
+ (0, react_jsx_runtime.jsx)("p", { style: S.helpTitle, children: t("help.grouped.title") }),
958
+ (0, react_jsx_runtime.jsx)("p", { style: S.helpDesc, children: t("help.grouped.desc") }),
959
+ ],
960
+ }),
961
+ (0, react_jsx_runtime.jsxs)("div", {
962
+ style: S.helpBlock,
963
+ children: [
964
+ (0, react_jsx_runtime.jsx)("p", { style: S.helpTitle, children: t("help.newSession.title") }),
965
+ (0, react_jsx_runtime.jsx)("p", { style: S.helpDesc, children: t("help.newSession.desc") }),
966
+ ],
967
+ }),
968
+ (0, react_jsx_runtime.jsxs)("div", {
969
+ style: S.helpBlock,
970
+ children: [
971
+ (0, react_jsx_runtime.jsx)("p", { style: S.helpTitle, children: t("help.value.title") }),
972
+ (0, react_jsx_runtime.jsx)("p", { style: S.helpDesc, children: t("help.value.desc") }),
973
+ ],
974
+ }),
975
+ ],
976
+ });
977
+
978
+ return (0, react_jsx_runtime.jsxs)(react.Fragment, {
979
+ children: [
980
+ (0, react_jsx_runtime.jsx)("button", {
981
+ title: t("trigger.aria"),
982
+ "aria-label": t("trigger.aria"),
983
+ onClick: () => setOpen(true),
984
+ style: S.trigger(wide),
985
+ children: [
986
+ (0, react_jsx_runtime.jsx)(SessionIcon, { size: 16 }),
987
+ wide ? t("trigger.label") : null,
988
+ ],
989
+ }),
990
+ open &&
991
+ (0, react_jsx_runtime.jsxs)("div", {
992
+ style: S.overlay,
993
+ children: [
994
+ (0, react_jsx_runtime.jsx)("div", {
995
+ style: S.backdrop,
996
+ onClick: () => setOpen(false),
997
+ }),
998
+ (0, react_jsx_runtime.jsxs)("div", {
999
+ style: S.panel,
1000
+ children: [
1001
+ (0, react_jsx_runtime.jsxs)("div", {
1002
+ style: S.header,
1003
+ children: [
1004
+ (0, react_jsx_runtime.jsxs)("div", {
1005
+ children: [
1006
+ (0, react_jsx_runtime.jsx)("h2", { style: S.title, children: t("panel.title") }),
1007
+ (0, react_jsx_runtime.jsx)("p", { style: S.subtitle, children: t("panel.subtitle") }),
1008
+ ],
1009
+ }),
1010
+ (0, react_jsx_runtime.jsx)("button", {
1011
+ style: S.close,
1012
+ "aria-label": "close",
1013
+ onClick: () => setOpen(false),
1014
+ children: "✕",
1015
+ }),
1016
+ ],
1017
+ }),
1018
+ (0, react_jsx_runtime.jsxs)("div", {
1019
+ style: S.tabs,
1020
+ children: [
1021
+ (0, react_jsx_runtime.jsx)("button", {
1022
+ style: S.tab(tab === "all"),
1023
+ onClick: () => setTab("all"),
1024
+ children: t("tab.all"),
1025
+ }),
1026
+ (0, react_jsx_runtime.jsx)("button", {
1027
+ style: S.tab(tab === "archived"),
1028
+ onClick: () => setTab("archived"),
1029
+ children: `${t("tab.archived")} (${rows.filter((row) => row.archived).length})`,
1030
+ }),
1031
+ (0, react_jsx_runtime.jsx)("button", {
1032
+ style: S.tab(tab === "active"),
1033
+ onClick: () => setTab("active"),
1034
+ children: `${t("tab.active")} (${rows.filter((row) => !row.archived && (row.live === true || row.running === true)).length})`,
1035
+ }),
1036
+ (0, react_jsx_runtime.jsx)("button", {
1037
+ style: S.tab(tab === "help"),
1038
+ onClick: () => setTab("help"),
1039
+ children: t("tab.help"),
1040
+ }),
1041
+ ],
1042
+ }),
1043
+ (0, react_jsx_runtime.jsx)("div", {
1044
+ style: { display: "flex", gap: 6, padding: PADDING_BAR, alignItems: "center", flexWrap: "wrap" },
1045
+ children: [
1046
+ (0, react_jsx_runtime.jsx)("input", {
1047
+ type: "text",
1048
+ value: query,
1049
+ placeholder: t("search.placeholder"),
1050
+ style: { flex: 1, minWidth: CONTROL_MIN_WIDTH, height: CONTROL_HEIGHT, borderRadius: CONTROL_RADIUS, border: CSS_BORDER_L2, background: CSS_BG_BASE, color: CSS_LABEL_PRIMARY, padding: "0 10px", fontSize: FONT_SIZE_SMALL, outline: "none" },
1051
+ onChange: (event) => { setQuery(event.target.value); setFullResults(null); },
1052
+ onKeyDown: (event) => {
1053
+ if (event.key === "Enter") runFullSearch(query);
1054
+ },
1055
+ }),
1056
+ (0, react_jsx_runtime.jsx)("button", {
1057
+ style: S.actionBtn(false),
1058
+ disabled: fullSearching || busy !== null,
1059
+ onClick: () => runFullSearch(query),
1060
+ children: fullSearching ? t("search.fullBusy") : t("search.full"),
1061
+ }),
1062
+ (0, react_jsx_runtime.jsx)("button", {
1063
+ style: { ...S.actionBtn(true), borderColor: CSS_DANGER, color: CSS_DANGER },
1064
+ disabled: selected.size === 0 || busy !== null,
1065
+ onClick: () => deleteSelected(),
1066
+ children: t("deleteBatch.action", { n: selected.size }),
1067
+ }),
1068
+ (0, react_jsx_runtime.jsx)("button", {
1069
+ style: S.actionBtn(false),
1070
+ disabled: busy !== null,
1071
+ onClick: () => { setRuleOpen((v) => !v); setRulePreview(null); },
1072
+ children: t("deleteByRule.action"),
1073
+ }),
1074
+ (0, react_jsx_runtime.jsx)("button", {
1075
+ style: { ...S.actionBtn(false), marginLeft: "auto" },
1076
+ disabled: busy !== null,
1077
+ onClick: () => releaseAll(),
1078
+ children: busy === "__all__" ? t("action.releaseAllBusy") : t("action.releaseAll"),
1079
+ }),
1080
+ ],
1081
+ }),
1082
+ // —— 按条件删除表单(展开式) ——
1083
+ ruleOpen &&
1084
+ (0, react_jsx_runtime.jsxs)("div", {
1085
+ style: { display: "flex", gap: 8, padding: PADDING_BAR, alignItems: "center", flexWrap: "wrap", fontSize: FONT_SIZE_SMALL },
1086
+ children: [
1087
+ (0, react_jsx_runtime.jsxs)("label", {
1088
+ style: { display: "flex", alignItems: "center", gap: 4, color: CSS_LABEL_SECONDARY },
1089
+ children: [
1090
+ (0, react_jsx_runtime.jsx)("input", { type: "checkbox", checked: ruleArchivedOnly, onChange: (e) => setRuleArchivedOnly(e.target.checked) }),
1091
+ t("deleteByRule.label.archivedOnly"),
1092
+ ],
1093
+ }),
1094
+ (0, react_jsx_runtime.jsxs)("label", {
1095
+ style: { display: "flex", alignItems: "center", gap: 4, color: CSS_LABEL_SECONDARY },
1096
+ title: t("deleteByRule.label.lowValue"),
1097
+ children: [
1098
+ (0, react_jsx_runtime.jsx)("input", { type: "checkbox", checked: ruleLowValue, onChange: (e) => setRuleLowValue(e.target.checked) }),
1099
+ t("deleteByRule.label.lowValue"),
1100
+ ],
1101
+ }),
1102
+ (0, react_jsx_runtime.jsxs)("label", {
1103
+ style: { display: "flex", alignItems: "center", gap: 4, color: CSS_LABEL_SECONDARY, flexWrap: "nowrap" },
1104
+ children: [
1105
+ t("deleteByRule.label.inactiveDays.pre"),
1106
+ (0, react_jsx_runtime.jsx)("input", {
1107
+ type: "number", min: 0, max: 3650, value: ruleDays,
1108
+ style: { width: RULE_DAYS_INPUT_WIDTH, height: SMALL_CONTROL_HEIGHT, borderRadius: CONTROL_RADIUS, border: CSS_BORDER_L2, background: CSS_BG_BASE, color: CSS_LABEL_PRIMARY, padding: "0 6px", fontSize: FONT_SIZE_SMALL },
1109
+ onChange: (e) => setRuleDays(e.target.value),
1110
+ }),
1111
+ t("deleteByRule.label.inactiveDays.post"),
1112
+ ],
1113
+ }),
1114
+ (0, react_jsx_runtime.jsx)("input", {
1115
+ type: "text", value: rulePrefix, placeholder: t("deleteByRule.label.cwdPrefix"),
1116
+ style: { flex: 1, minWidth: RULE_PREFIX_MIN_WIDTH, height: SMALL_CONTROL_HEIGHT, borderRadius: CONTROL_RADIUS, border: CSS_BORDER_L2, background: CSS_BG_BASE, color: CSS_LABEL_PRIMARY, padding: "0 8px", fontSize: FONT_SIZE_SMALL, outline: "none" },
1117
+ onChange: (e) => { setRulePrefix(e.target.value); setRulePreview(null); },
1118
+ }),
1119
+ (0, react_jsx_runtime.jsx)("button", {
1120
+ style: S.actionBtn(false),
1121
+ disabled: busy !== null,
1122
+ onClick: previewRuleDelete,
1123
+ children: t("deleteByRule.previewBtn"),
1124
+ }),
1125
+ rulePreview !== null && rulePreview.length > 0 &&
1126
+ (0, react_jsx_runtime.jsx)("button", {
1127
+ style: { ...S.actionBtn(true), borderColor: CSS_DANGER, color: CSS_DANGER },
1128
+ disabled: busy !== null,
1129
+ onClick: runRuleDelete,
1130
+ children: t("deleteByRule.run") + ` (${rulePreview.length})`,
1131
+ }),
1132
+ rulePreview !== null && rulePreview.length > 0 &&
1133
+ (0, react_jsx_runtime.jsx)("span", {
1134
+ style: { color: CSS_LABEL_TERTIARY, fontSize: 12 },
1135
+ children: t("search.hits", { n: rulePreview.length }),
1136
+ }),
1137
+ ],
1138
+ }),
1139
+ (0, react_jsx_runtime.jsx)("div", {
1140
+ style: S.body,
1141
+ children: tab === "help"
1142
+ ? renderHelp()
1143
+ : (0, react_jsx_runtime.jsxs)(react.Fragment, {
1144
+ children: [
1145
+ // —— 全文搜索结果区(官方 FTS5 优先,自研兜底) ——
1146
+ fullResults !== null &&
1147
+ (0, react_jsx_runtime.jsxs)("div", {
1148
+ style: { margin: PADDING_BAR, borderRadius: 10, border: "1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.3))", overflow: "hidden" },
1149
+ children: [
1150
+ (0, react_jsx_runtime.jsxs)("div", {
1151
+ style: { display: "flex", alignItems: "center", gap: 8, padding: "6px 10px", background: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.08))" },
1152
+ children: [
1153
+ (0, react_jsx_runtime.jsx)("span", { style: { fontSize: FONT_SIZE_SMALL, fontWeight: FONT_WEIGHT_SEMIBOLD, color: CSS_LABEL_PRIMARY }, children: `${t("search.hits", { n: fullResults.hits.length })}:${fullResults.query}` }),
1154
+ fullResults.usedFallback && (0, react_jsx_runtime.jsx)("span", { style: { fontSize: 11.5, color: CSS_LABEL_TERTIARY }, children: t("search.fallback") }),
1155
+ (0, react_jsx_runtime.jsx)("button", {
1156
+ style: { ...S.actionBtn(false), marginLeft: "auto" },
1157
+ onClick: () => setFullResults(null),
1158
+ children: t("search.close"),
1159
+ }),
1160
+ ],
1161
+ }),
1162
+ fullResults.hits.length === 0
1163
+ ? (0, react_jsx_runtime.jsx)("div", { style: { ...S.empty, margin: 0, padding: "14px 0" }, children: t("search.noResult") })
1164
+ : fullResults.hits.map((hit) => (0, react_jsx_runtime.jsxs)("div", {
1165
+ style: { padding: "6px 10px", borderTop: "1px solid var(--dsw-alias-border-l1, rgba(128,128,128,.15))", cursor: "pointer" },
1166
+ onClick: () => { if (onOpenSession) { onOpenSession(hit.sessionId); setOpen(false); } },
1167
+ children: [
1168
+ (0, react_jsx_runtime.jsxs)("div", {
1169
+ style: { fontSize: FONT_SIZE_SMALL, fontWeight: FONT_WEIGHT_SEMIBOLD, color: CSS_LABEL_PRIMARY, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" },
1170
+ children: [hit.title || basenameOf(hit.cwd) || hit.sessionId, hit.archived ? ` · ${t("badge.archived")}` : "", hit.running ? ` · ${t("badge.running")}` : ""],
1171
+ }),
1172
+ (0, react_jsx_runtime.jsx)("div", { style: { fontSize: 11.5, color: CSS_LABEL_TERTIARY, marginBottom: 2, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: hit.cwd || hit.sessionId }),
1173
+ (hit.matches ?? []).map((m, mi) => (0, react_jsx_runtime.jsxs)("div", {
1174
+ style: { fontSize: 12, lineHeight: 1.5, color: CSS_LABEL_SECONDARY },
1175
+ children: [
1176
+ (0, react_jsx_runtime.jsx)("span", {
1177
+ style: { display: "inline-block", marginRight: 6, padding: "0 5px", borderRadius: 4, fontSize: 11, background: "var(--dsw-alias-interactive-bg-hover, rgba(128,128,128,.12))", color: CSS_LABEL_TERTIARY },
1178
+ children: m.role === "user" ? t("search.matchRole.user") : m.role === "assistant" ? t("search.matchRole.assistant") : (m.surface ?? m.type ?? ""),
1179
+ }),
1180
+ (0, react_jsx_runtime.jsx)("span", { children: m.snippet ?? m.preview ?? "" }),
1181
+ ],
1182
+ }, `${hit.sessionId}-m${mi}`)),
1183
+ ],
1184
+ }, hit.sessionId)),
1185
+ ],
1186
+ }),
1187
+ error !== null &&
1188
+ (0, react_jsx_runtime.jsxs)("div", {
1189
+ style: S.error,
1190
+ children: [
1191
+ t("list.error", { message: error }),
1192
+ " ",
1193
+ (0, react_jsx_runtime.jsx)("button", {
1194
+ style: { ...S.actionBtn(false), marginLeft: 6 },
1195
+ onClick: refresh,
1196
+ children: t("action.retry"),
1197
+ }),
1198
+ ],
1199
+ }),
1200
+ sessions === null && error === null
1201
+ ? (0, react_jsx_runtime.jsx)("div", { style: S.empty, children: t("list.loading") })
1202
+ : filteredShown.length === 0
1203
+ ? (0, react_jsx_runtime.jsx)("div", {
1204
+ style: S.empty,
1205
+ children: tab === "archived" ? t("list.archivedEmpty") : tab === "active" ? t("list.activeEmpty") : t("list.empty"),
1206
+ })
1207
+ : grouped === null
1208
+ ? filteredShown.map(renderRow)
1209
+ : (0, react_jsx_runtime.jsxs)(react.Fragment, {
1210
+ children: [
1211
+ grouped.buckets.map((g) => (0, react_jsx_runtime.jsxs)("div", {
1212
+ key: g.workspaceId,
1213
+ children: [
1214
+ (0, react_jsx_runtime.jsxs)("div", {
1215
+ style: S.groupHeader,
1216
+ onClick: () => toggleCollapse(g.workspaceId),
1217
+ children: [
1218
+ (0, react_jsx_runtime.jsx)("span", {
1219
+ style: S.groupCaret,
1220
+ children: collapsed.has(g.workspaceId) ? "▸" : "▾",
1221
+ }),
1222
+ (0, react_jsx_runtime.jsx)("span", { style: S.groupTitle, children: (tab === "archived" ? t("badge.archived") + " · " : "") + g.title }),
1223
+ (0, react_jsx_runtime.jsx)("span", { style: S.groupCount, children: g.sessions.length }),
1224
+ ],
1225
+ }),
1226
+ !collapsed.has(g.workspaceId) && (0, react_jsx_runtime.jsxs)("div", {
1227
+ style: S.groupList,
1228
+ children: g.sessions.map(renderRow),
1229
+ }),
1230
+ ],
1231
+ })),
1232
+ grouped.rest.length > 0 && (0, react_jsx_runtime.jsxs)("div", {
1233
+ key: "__ungrouped__",
1234
+ children: [
1235
+ (0, react_jsx_runtime.jsxs)("div", {
1236
+ style: S.groupHeader,
1237
+ onClick: () => toggleCollapse("__ungrouped__"),
1238
+ children: [
1239
+ (0, react_jsx_runtime.jsx)("span", {
1240
+ style: S.groupCaret,
1241
+ children: collapsed.has("__ungrouped__") ? "▸" : "▾",
1242
+ }),
1243
+ (0, react_jsx_runtime.jsx)("span", { style: S.groupTitle, children: t("group.ungrouped") }),
1244
+ (0, react_jsx_runtime.jsx)("span", { style: S.groupCount, children: grouped.rest.length }),
1245
+ ],
1246
+ }),
1247
+ !collapsed.has("__ungrouped__") && (0, react_jsx_runtime.jsxs)("div", {
1248
+ style: S.groupList,
1249
+ children: grouped.rest.map(renderRow),
1250
+ }),
1251
+ ],
1252
+ }),
1253
+ ],
1254
+ }),
1255
+ ],
1256
+ }),
1257
+ }),
1258
+ ],
1259
+ }),
1260
+ ],
1261
+ }),
1262
+ ],
1263
+ });
1264
+ }
1265
+