dsh-cool-session 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +24 -2
  3. package/README.zh.md +25 -0
  4. package/cordis.patch.yml +7 -0
  5. package/lib/client.js +1602 -0
  6. package/lib/index.js +690 -0
  7. package/lib/types/client/api.d.ts +30 -0
  8. package/lib/types/client/api.d.ts.map +1 -0
  9. package/lib/types/client/archive.d.ts +43 -0
  10. package/lib/types/client/archive.d.ts.map +1 -0
  11. package/lib/types/client/icons.d.ts +32 -0
  12. package/lib/types/client/icons.d.ts.map +1 -0
  13. package/lib/types/client/index.d.ts +28 -0
  14. package/lib/types/client/index.d.ts.map +1 -0
  15. package/lib/types/client/locales.d.ts +143 -0
  16. package/lib/types/client/locales.d.ts.map +1 -0
  17. package/lib/types/client/nav-icon.d.ts +30 -0
  18. package/lib/types/client/nav-icon.d.ts.map +1 -0
  19. package/lib/types/client/reason.d.ts +24 -0
  20. package/lib/types/client/reason.d.ts.map +1 -0
  21. package/lib/types/client/session-menu.d.ts +65 -0
  22. package/lib/types/client/session-menu.d.ts.map +1 -0
  23. package/lib/types/client/settings-section.d.ts +43 -0
  24. package/lib/types/client/settings-section.d.ts.map +1 -0
  25. package/lib/types/client/styles.d.ts +26 -0
  26. package/lib/types/client/styles.d.ts.map +1 -0
  27. package/lib/types/host/deletion.d.ts +26 -0
  28. package/lib/types/host/deletion.d.ts.map +1 -0
  29. package/lib/types/host/index.d.ts +20 -0
  30. package/lib/types/host/index.d.ts.map +1 -0
  31. package/lib/types/host/refusal.d.ts +25 -0
  32. package/lib/types/host/refusal.d.ts.map +1 -0
  33. package/lib/types/host/routes.d.ts +28 -0
  34. package/lib/types/host/routes.d.ts.map +1 -0
  35. package/lib/types/shared/contract.d.ts +61 -0
  36. package/lib/types/shared/contract.d.ts.map +1 -0
  37. package/package.json +69 -3
package/lib/client.js ADDED
@@ -0,0 +1,1602 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "dsh-cool-session",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
+ let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
8
+ let react = require("react");
9
+ let react_jsx_runtime = require("react/jsx-runtime");
10
+ // src/client/api.ts
11
+ const BASE = "/dsh-cool-session/api";
12
+ /**
13
+ * POST one JSON body and read a JSON answer.
14
+ *
15
+ * @param path - route path below {@link BASE}.
16
+ * @param body - the request body.
17
+ * @returns the parsed payload, or an `{ ok: false }` result when the transport failed.
18
+ */
19
+ async function postJson(path, body) {
20
+ try {
21
+ const response = await fetch(`${BASE}${path}`, {
22
+ method: "POST",
23
+ headers: { "content-type": "application/json" },
24
+ body: JSON.stringify(body),
25
+ credentials: "same-origin"
26
+ });
27
+ const payload = await response.json().catch(() => null);
28
+ if (payload === null || typeof payload !== "object") return {
29
+ ok: false,
30
+ error: `HTTP ${response.status}`
31
+ };
32
+ return payload;
33
+ } catch (error) {
34
+ return {
35
+ ok: false,
36
+ error: error instanceof Error ? error.message : String(error)
37
+ };
38
+ }
39
+ }
40
+ /**
41
+ * Ask the Host to delete one Session and every subagent Session beneath it.
42
+ *
43
+ * The Host stops running work first, then removes the artifacts.
44
+ *
45
+ * @param sessionId - the root Session to delete.
46
+ * @returns the deletion summary, or a coded refusal.
47
+ */
48
+ function deleteSession(sessionId) {
49
+ return postJson("/delete", { sessionId });
50
+ }
51
+ /**
52
+ * Whether an answer means the Host half predates the batch route.
53
+ *
54
+ * The browser bundle reloads with the page while the Host half lives until the
55
+ * process restarts, so a freshly updated plugin can be newer than the routes it
56
+ * is talking to. A missing route is not a deletion failure: the single route is
57
+ * still there and can finish the same selection.
58
+ *
59
+ * @param payload - the answer to inspect.
60
+ * @returns true when `deleteSessions` should fall back to single requests.
61
+ */
62
+ function batchRouteMissing(payload) {
63
+ return payload.error === "HTTP 404" || payload.error === "HTTP 405";
64
+ }
65
+ /**
66
+ * Sessions per batch request.
67
+ *
68
+ * Smaller than the Host's own ceiling so a very large selection is a sequence
69
+ * of bounded requests; each batch still stops and removes its own Sessions in
70
+ * order, so the persistence writer never sees two deletions at once.
71
+ */
72
+ const DELETE_BATCH_SIZE = 100;
73
+ /**
74
+ * Ask the Host to delete several Sessions.
75
+ *
76
+ * The Host runs the same stop-then-remove sequence as {@link deleteSession} for
77
+ * each id, sequentially, and answers one result per id. When this Host half does
78
+ * not serve the batch route yet, the same work is done through the single route
79
+ * so the panel keeps working across a live plugin upgrade and a later restart.
80
+ *
81
+ * @param sessionIds - the root Sessions to delete, in request order.
82
+ * @returns the per-session summaries, or a request-level refusal.
83
+ */
84
+ async function deleteSessions(sessionIds) {
85
+ const results = [];
86
+ for (let start = 0; start < sessionIds.length; start += DELETE_BATCH_SIZE) {
87
+ const chunk = sessionIds.slice(start, start + DELETE_BATCH_SIZE);
88
+ const payload = await postJson("/delete-many", { sessionIds: chunk });
89
+ if (batchRouteMissing(payload)) {
90
+ for (const sessionId of chunk) results.push(await deleteSession(sessionId));
91
+ continue;
92
+ }
93
+ if (!Array.isArray(payload.results)) return {
94
+ ok: false,
95
+ error: typeof payload.error === "string" && payload.error !== "" ? payload.error : "the delete request failed",
96
+ results
97
+ };
98
+ results.push(...payload.results);
99
+ }
100
+ return {
101
+ ok: results.every((result) => result.ok),
102
+ results
103
+ };
104
+ }
105
+ // src/client/reason.ts
106
+ /**
107
+ * Word one refused answer.
108
+ *
109
+ * @param t - the page's translate seat.
110
+ * @param payload - the Host answer.
111
+ * @param fallback - the entry to state when the answer carried no reason.
112
+ * @returns the reason to show.
113
+ */
114
+ function reasonOf(t, payload, fallback) {
115
+ if (payload.failure !== void 0) {
116
+ const sentence = t(`error.${payload.failure.code}`, payload.failure.params);
117
+ if (payload.failure.code === "delete-partial" && typeof payload.error === "string" && payload.error !== "") return `${sentence} ${payload.error}`;
118
+ return sentence;
119
+ }
120
+ if (payload.error === "HTTP 405") return t("error.host-stale");
121
+ return typeof payload.error === "string" && payload.error !== "" ? payload.error : t(fallback);
122
+ }
123
+ // src/client/session-menu.tsx
124
+ /**
125
+ * The native Session menu's delete row and its confirmation overlay.
126
+ *
127
+ * The row is contributed to `sidebar.workspaces.session.menu.item` by builds
128
+ * that have no `contextMenus` service; the client entry registers a plain-data
129
+ * action instead wherever that service exists, because the shipped Archive row
130
+ * is a footer action and only a footer action can follow it. Menus unmount as
131
+ * soon as they close, so either row only records a request; the frame-wide
132
+ * `shell.overlay` entry owns the dialog's state and the Host call. That split is
133
+ * also what lets the dialog survive the menu's portal disappearing on click.
134
+ *
135
+ * Deletion is permanent, so the confirmation is the only path: it asks before
136
+ * the Host stops running work and removes the artifacts, and disables itself
137
+ * while that happens.
138
+ */
139
+ let pendingDelete = null;
140
+ const deleteListeners = /* @__PURE__ */ new Set();
141
+ function emitDelete() {
142
+ for (const listener of [...deleteListeners]) listener();
143
+ }
144
+ function subscribeDelete(listener) {
145
+ deleteListeners.add(listener);
146
+ return () => {
147
+ deleteListeners.delete(listener);
148
+ };
149
+ }
150
+ function deleteSnapshot() {
151
+ return pendingDelete;
152
+ }
153
+ /**
154
+ * Open the delete confirmation for one Session.
155
+ *
156
+ * @param sessionId - the Session the menu row belongs to.
157
+ */
158
+ function requestDeleteSession(sessionId) {
159
+ pendingDelete = { sessionId };
160
+ emitDelete();
161
+ }
162
+ /** Dismiss the confirmation without deleting. */
163
+ function clearDeleteRequest() {
164
+ if (pendingDelete === null) return;
165
+ pendingDelete = null;
166
+ emitDelete();
167
+ }
168
+ /** Whether the menu service has taken over the Delete row. */
169
+ let serviceOwnsRow = false;
170
+ /**
171
+ * Stop drawing the legacy Slot row.
172
+ *
173
+ * The client entry installs that row when `apply` runs before `contextMenus` is
174
+ * mounted; the service then contributes Delete as a footer action so it can
175
+ * follow Archive. This flag keeps the two paths from drawing the row twice.
176
+ */
177
+ function retireLegacyDeleteRow() {
178
+ serviceOwnsRow = true;
179
+ }
180
+ /**
181
+ * The legacy delete row for builds without `contextMenus`, placed after Copy
182
+ * Session ID and separated from the shipped group. Selecting it closes the menu
183
+ * and raises the confirmation overlay.
184
+ *
185
+ * @param props - the row identity, menu hook, and locale seat.
186
+ * @returns the menu row, or null once the menu service owns it.
187
+ */
188
+ function SessionDeleteMenuItem({ sessionId, useMenuOpenState, t }) {
189
+ const [, setMenuOpen] = useMenuOpenState();
190
+ if (serviceOwnsRow) return null;
191
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.MenuItemButton, {
192
+ separatorBefore: true,
193
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
194
+ className: "dsh-cs-menu-delete-icon",
195
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconTrashOutlineRegular, {})
196
+ }),
197
+ onSelect: () => {
198
+ setMenuOpen(false);
199
+ requestDeleteSession(sessionId);
200
+ },
201
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
202
+ className: "dsh-cs-menu-delete-label",
203
+ children: t("action.delete")
204
+ })
205
+ });
206
+ }
207
+ /**
208
+ * The frame-wide confirmation entry, drawn with DSH's own `Modal` primitive so
209
+ * its chrome, blurred mask, focus trap, and Escape handling match the rest of
210
+ * the product instead of being re-implemented here.
211
+ *
212
+ * @param props - the locale seat and the post-delete callback.
213
+ * @returns the dialog, or null while no delete is pending.
214
+ */
215
+ function SessionDeleteDialog({ t, onDeleted }) {
216
+ const request = (0, react.useSyncExternalStore)(subscribeDelete, deleteSnapshot, deleteSnapshot);
217
+ const [busy, setBusy] = (0, react.useState)(false);
218
+ const [error, setError] = (0, react.useState)(void 0);
219
+ (0, react.useEffect)(() => {
220
+ if (request === null) return;
221
+ setBusy(false);
222
+ setError(void 0);
223
+ }, [request]);
224
+ const close = () => {
225
+ if (!busy) clearDeleteRequest();
226
+ };
227
+ const confirm = async () => {
228
+ if (busy || request === null) return;
229
+ setBusy(true);
230
+ setError(void 0);
231
+ const payload = await deleteSession(request.sessionId);
232
+ if (payload.ok !== true) {
233
+ setError(reasonOf(t, payload, "error.delete-failed"));
234
+ setBusy(false);
235
+ if ((payload.deletedSessions?.length ?? 0) > 0) onDeleted?.(request.sessionId);
236
+ return;
237
+ }
238
+ if (payload.warnings !== void 0 && payload.warnings.length > 0) console.warn("[dsh-cool-session] delete cleanup warnings:", payload.warnings);
239
+ clearDeleteRequest();
240
+ onDeleted?.(request.sessionId);
241
+ };
242
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
243
+ open: request !== null,
244
+ onClose: close,
245
+ title: t("delete.title"),
246
+ description: t("delete.description"),
247
+ closeLabel: t("action.close"),
248
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
249
+ variant: "outline",
250
+ "data-modal-autofocus": true,
251
+ disabled: busy,
252
+ onClick: close,
253
+ children: t("action.cancel")
254
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
255
+ variant: "outline",
256
+ className: "dsh-cs-delete-action",
257
+ disabled: busy,
258
+ onClick: () => {
259
+ confirm();
260
+ },
261
+ children: busy ? t("delete.pending") : t("delete.confirm")
262
+ })] }),
263
+ children: error === void 0 ? void 0 : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
264
+ className: "dsh-cs-delete-error",
265
+ role: "alert",
266
+ children: error
267
+ })
268
+ });
269
+ }
270
+ // src/client/icons.ts
271
+ /**
272
+ * Parse one asset's raw SVG into the artwork the nav patch inserts.
273
+ *
274
+ * The root element is dropped rather than copied: its `width`/`height` belong to
275
+ * the asset, not to the shell's 16px icon seat, and the root's `xmlns` would be
276
+ * redundant once the markup is part of the page.
277
+ *
278
+ * @param name - the asset filename stem, e.g. `archive` for `assets/archive.svg`.
279
+ * @returns the viewBox and inner markup to render.
280
+ */
281
+ function iconArtwork(name) {
282
+ const raw = {
283
+ "archive": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1em\" height=\"1em\" viewBox=\"0 0 24 24\">\n <path d=\"M0 0h24v24H0z\" fill=\"none\" />\n <g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\">\n <rect width=\"20\" height=\"5\" x=\"2\" y=\"3\" rx=\"1\" />\n <path d=\"M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8m-10 4h4\" />\n </g>\n</svg>\n",
284
+ "arrow-down": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1em\" height=\"1em\" viewBox=\"0 0 24 24\">\n <path d=\"M0 0h24v24H0z\" fill=\"none\" />\n <path fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"m6 9l6 6l6-6\" />\n</svg>\n"
285
+ }[name];
286
+ if (raw === void 0) throw new Error(`dsh-cool-session: missing icon asset '${name}.svg'`);
287
+ return {
288
+ viewBox: /viewBox="([^"]+)"/.exec(raw)?.[1] ?? "0 0 24 24",
289
+ inner: /<svg[^>]*>([\s\S]*)<\/svg>/.exec(raw)?.[1] ?? ""
290
+ };
291
+ }
292
+ /** Complete SVG markup for one asset at a fixed square size. */
293
+ function iconMarkup(name, size) {
294
+ const artwork = iconArtwork(name);
295
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="${artwork.viewBox}" aria-hidden="true" focusable="false">${artwork.inner}</svg>`;
296
+ }
297
+ /** The Settings navigation's icon seat: the shell draws its own glyphs at 16px. */
298
+ const NAV_ICON_SIZE = 16;
299
+ /** The filter triggers' caret, sized to sit inside a 32px control. */
300
+ const CARET_SIZE = 12;
301
+ /**
302
+ * The Sessions glyph as complete markup for the shell's navigation seat.
303
+ *
304
+ * It carries its own 16px size because the asset's `1em` would resolve against
305
+ * the nav cell's 14px text, and the stroke is `currentColor`, so the glyph takes
306
+ * the label's color in both appearances.
307
+ */
308
+ const ARCHIVE_NAV_MARKUP = iconMarkup("archive", NAV_ICON_SIZE);
309
+ /**
310
+ * The caret beside the panel's two filter triggers.
311
+ *
312
+ * `assets/arrow-down.svg` strokes with `currentColor`, so the trigger's label
313
+ * color reaches it; the fixed 12px keeps it clear of the control's 13px text.
314
+ */
315
+ const ARROW_DOWN_MARKUP = iconMarkup("arrow-down", CARET_SIZE);
316
+ // src/client/locales.ts
317
+ /** The dictionary namespace this page owns. */
318
+ const NS = "coolSession";
319
+ /**
320
+ * Pick the singular or plural spelling of a count-bearing message.
321
+ *
322
+ * @param base - the message family, whose `.one` and `.other` entries exist.
323
+ * @param count - the number the sentence reports.
324
+ * @returns the key to translate.
325
+ */
326
+ function countKey(base, count) {
327
+ return `${base}.${count === 1 ? "one" : "other"}`;
328
+ }
329
+ /** The English copy, which is also the fallback every lookup ends at. */
330
+ const en = {
331
+ "action.delete": "Delete session",
332
+ "action.cancel": "Cancel",
333
+ "action.close": "Close",
334
+ "settings.nav": "Sessions",
335
+ "settings.title": "Sessions",
336
+ "settings.description": "Search, filter, archive, or delete sessions.",
337
+ "sessions.search": "Search session titles",
338
+ "sessions.searchPlaceholder": "Search...",
339
+ "sessions.allWorkspaces": "All workspaces",
340
+ "sessions.noWorkspace": "No workspace",
341
+ "sessions.allSessions": "All sessions",
342
+ "sessions.unarchived": "Unarchived",
343
+ "sessions.archived": "Archived",
344
+ "sessions.running": "Running",
345
+ "sessions.selectAll": "Select all",
346
+ "sessions.selectOne": "Select {title}",
347
+ "sessions.archiveSelected": "Archive selected",
348
+ "sessions.deleteSelected": "Delete selected",
349
+ "sessions.selectedCount": "{count} selected",
350
+ "sessions.columnTitle": "Session",
351
+ "sessions.columnStatus": "Status",
352
+ "sessions.columnActions": "Actions",
353
+ "sessions.archive": "Archive",
354
+ "sessions.unarchive": "Unarchive",
355
+ "sessions.delete": "Delete",
356
+ "sessions.loading": "Loading sessions…",
357
+ "sessions.empty": "No sessions yet",
358
+ "sessions.noMatches": "No matching sessions",
359
+ "sessions.loadFailed": "The session list could not be loaded",
360
+ "sessions.retry": "Retry",
361
+ "sessions.unavailable": "Session services are not ready, so the archive action is unavailable",
362
+ "sessions.actionFailed": "The action failed",
363
+ "sessions.bulkPartial.one": "Deleted 1 session; {failed} could not be deleted.",
364
+ "sessions.bulkPartial.other": "Deleted {deleted} sessions; {failed} could not be deleted.",
365
+ "sessions.bulkStopped.one": "Deleted 1 session; the rest could not be processed.",
366
+ "sessions.bulkStopped.other": "Deleted {deleted} sessions; the rest could not be processed.",
367
+ "sessions.bulkArchivePartial.one": "Archived 1 session; {skipped} skipped because work was still running.",
368
+ "sessions.bulkArchivePartial.other": "Archived {archived} sessions; {skipped} skipped because work was still running.",
369
+ "sessions.bulkArchiveFailed.one": "Archived 1 session; {failed} could not be archived.",
370
+ "sessions.bulkArchiveFailed.other": "Archived {archived} sessions; {failed} could not be archived.",
371
+ "sessions.deleteSelectedTitle": "Delete selected sessions",
372
+ "sessions.deleteBatchDescription": "This removes the {count} selected sessions and all of their subagent sessions from disk, including their logs. Running sessions will be terminated. This cannot be undone!",
373
+ "sessions.archiveStopTitle": "Stop and archive this session?",
374
+ "sessions.archiveStopDescription": "“{title}” still has work in progress. Archiving stops it first; the stopped work will not resume on its own.",
375
+ "sessions.archiveActivity": "Work that will be stopped",
376
+ "sessions.activityTurn": "The turn in progress",
377
+ "sessions.activitySubagents.one": "1 running subagent",
378
+ "sessions.activitySubagents.other": "{count} running subagents",
379
+ "sessions.activityJobs.one": "1 background job",
380
+ "sessions.activityJobs.other": "{count} background jobs",
381
+ "sessions.activitySchedules.one": "1 scheduled reminder",
382
+ "sessions.activitySchedules.other": "{count} scheduled reminders",
383
+ "sessions.activityOther.one": "1 other item of work ({kind})",
384
+ "sessions.activityOther.other": "{count} other items of work ({kind})",
385
+ "sessions.archiveStopAction": "Stop and archive",
386
+ "sessions.archiveStopPending": "Stopping and archiving…",
387
+ "delete.title": "Delete session",
388
+ "delete.description": "This removes this session and all of its subagent sessions from disk, including their logs. Running sessions will be terminated. This cannot be undone!",
389
+ "delete.pending": "Stopping and deleting…",
390
+ "delete.confirm": "Delete",
391
+ "error.host-stale": "The Host half is still running an older build; restart DSH and try again.",
392
+ "error.missing-session": "No session was named",
393
+ "error.unknown-session": "That session no longer exists",
394
+ "error.subagent-delete": "A subagent session cannot be deleted on its own; delete its parent conversation",
395
+ "error.delete-failed": "The session could not be deleted",
396
+ "error.delete-unsupported": "This Harness does not expose the on-disk session layout needed for deletion",
397
+ "error.delete-active": "The session is still running ({activity}) and could not be stopped, so it was not deleted",
398
+ "error.session-live": "The session is still live and could not be released",
399
+ "error.delete-partial": "Some sessions could not be deleted: {deleted} removed, {remaining} still present. Retry the remaining ones"
400
+ };
401
+ /** The Chinese copy, key for key the same page. */
402
+ const zh = {
403
+ "action.delete": "删除会话",
404
+ "action.cancel": "取消",
405
+ "action.close": "关闭",
406
+ "settings.nav": "会话管理",
407
+ "settings.title": "会话管理",
408
+ "settings.description": "搜索、筛选、归档或删除会话。",
409
+ "sessions.search": "搜索会话标题",
410
+ "sessions.searchPlaceholder": "搜索...",
411
+ "sessions.allWorkspaces": "全部工作区",
412
+ "sessions.noWorkspace": "未关联工作区",
413
+ "sessions.allSessions": "全部会话",
414
+ "sessions.unarchived": "未归档",
415
+ "sessions.archived": "已归档",
416
+ "sessions.running": "运行中",
417
+ "sessions.selectAll": "全选",
418
+ "sessions.selectOne": "选择 {title}",
419
+ "sessions.archiveSelected": "归档已选",
420
+ "sessions.deleteSelected": "删除已选",
421
+ "sessions.selectedCount": "已选择 {count} 个",
422
+ "sessions.columnTitle": "会话标题",
423
+ "sessions.columnStatus": "状态",
424
+ "sessions.columnActions": "操作",
425
+ "sessions.archive": "归档",
426
+ "sessions.unarchive": "取消归档",
427
+ "sessions.delete": "删除",
428
+ "sessions.loading": "正在载入会话…",
429
+ "sessions.empty": "暂无会话",
430
+ "sessions.noMatches": "没有匹配的会话",
431
+ "sessions.loadFailed": "无法读取会话列表",
432
+ "sessions.retry": "重试",
433
+ "sessions.unavailable": "会话服务尚未就绪,无法归档",
434
+ "sessions.actionFailed": "操作失败",
435
+ "sessions.bulkPartial.one": "已删除 1 个会话,{failed} 个删除失败。",
436
+ "sessions.bulkPartial.other": "已删除 {deleted} 个会话,{failed} 个删除失败。",
437
+ "sessions.bulkStopped.one": "已删除 1 个会话,其余未能继续处理。",
438
+ "sessions.bulkStopped.other": "已删除 {deleted} 个会话,其余未能继续处理。",
439
+ "sessions.bulkArchivePartial.one": "已归档 1 个会话,{skipped} 个运行中的会话已跳过。",
440
+ "sessions.bulkArchivePartial.other": "已归档 {archived} 个会话,{skipped} 个运行中的会话已跳过。",
441
+ "sessions.bulkArchiveFailed.one": "已归档 1 个会话,{failed} 个归档失败。",
442
+ "sessions.bulkArchiveFailed.other": "已归档 {archived} 个会话,{failed} 个归档失败。",
443
+ "sessions.deleteSelectedTitle": "删除选中的会话",
444
+ "sessions.deleteBatchDescription": "从磁盘移除选中的 {count} 个会话及其所有子会话的日志,正在运行的会话将被终止。此操作不可撤销!",
445
+ "sessions.archiveStopTitle": "停止并归档此会话?",
446
+ "sessions.archiveStopDescription": "“{title}”仍有正在进行的工作。归档会先停止这些工作,停止的工作不会自动继续。",
447
+ "sessions.archiveActivity": "将被停止的工作",
448
+ "sessions.activityTurn": "进行中的回合",
449
+ "sessions.activitySubagents.one": "1 个运行中的子智能体",
450
+ "sessions.activitySubagents.other": "{count} 个运行中的子智能体",
451
+ "sessions.activityJobs.one": "1 个后台任务",
452
+ "sessions.activityJobs.other": "{count} 个后台任务",
453
+ "sessions.activitySchedules.one": "1 条定时提醒",
454
+ "sessions.activitySchedules.other": "{count} 条定时提醒",
455
+ "sessions.activityOther.one": "1 项其他工作({kind})",
456
+ "sessions.activityOther.other": "{count} 项其他工作({kind})",
457
+ "sessions.archiveStopAction": "停止并归档",
458
+ "sessions.archiveStopPending": "正在停止并归档…",
459
+ "delete.title": "删除会话",
460
+ "delete.description": "从磁盘移除此会话及其所有子会话的日志,正在运行的会话将被终止。此操作不可撤销!",
461
+ "delete.pending": "正在停止并删除…",
462
+ "delete.confirm": "删除",
463
+ "error.host-stale": "Host 端仍在运行旧版本,请重启 DSH 后重试。",
464
+ "error.missing-session": "未指定会话",
465
+ "error.unknown-session": "该会话已不存在",
466
+ "error.subagent-delete": "子代理会话不能单独删除,请删除其父会话",
467
+ "error.delete-failed": "无法删除该会话",
468
+ "error.delete-unsupported": "当前 Harness 未暴露删除会话所需的磁盘目录信息",
469
+ "error.delete-active": "会话仍在运行({activity}),无法停止,因此未删除",
470
+ "error.session-live": "会话仍处于活动状态,无法释放",
471
+ "error.delete-partial": "已删除 {deleted} 个会话,但还有 {remaining} 个未能删除;请重试剩余项"
472
+ };
473
+ // src/client/settings-section.tsx
474
+ /**
475
+ * The Sessions page of the Settings panel: list, search, filter, selection,
476
+ * archive, and deletion.
477
+ *
478
+ * The list itself is not fetched here. DSH already keeps the Host-authoritative
479
+ * Session catalog and Workspace archive set in the two client stores the shell
480
+ * exposes to every slot (`useSessions` / `useWorkspaces`), and every archive is
481
+ * a Workspace-controller call whose reply updates those stores. Reading them
482
+ * directly means this page shares the sidebar's rows and running states instead
483
+ * of maintaining a second, drifting catalog; deletion is the one operation the
484
+ * client face does not expose, so it still crosses this package's own Host
485
+ * route.
486
+ *
487
+ * Search is deliberately title-only: the row's title is the display title the
488
+ * shell computes (durable title, then project directory name, then id), so the
489
+ * text a reader can see in the row is exactly the text the box matches.
490
+ */
491
+ /** Flatten the two stores into the rows this page renders. */
492
+ function buildRows(state, workspaces) {
493
+ const archived = new Set(workspaces.archivedSessionIds ?? []);
494
+ const workspaceBySession = /* @__PURE__ */ new Map();
495
+ for (const workspace of workspaces.items ?? []) for (const id of workspace.sessionIds ?? []) if (!workspaceBySession.has(id)) workspaceBySession.set(id, {
496
+ id: workspace.workspaceId,
497
+ title: workspace.title
498
+ });
499
+ const rows = [];
500
+ for (const id of state.ids ?? []) {
501
+ const summary = state.byId?.[id];
502
+ if (summary === void 0) continue;
503
+ if (summary.origin === "subagent") continue;
504
+ const workspace = workspaceBySession.get(id);
505
+ const durable = typeof summary.title === "string" && summary.title.trim() !== "" ? summary.title.trim() : void 0;
506
+ const display = typeof summary.displayTitle === "string" && summary.displayTitle.trim() !== "" ? summary.displayTitle.trim() : void 0;
507
+ rows.push({
508
+ id,
509
+ title: durable ?? display ?? id,
510
+ ...summary.cwd === void 0 ? {} : { cwd: summary.cwd },
511
+ ...workspace === void 0 ? {} : {
512
+ workspaceId: workspace.id,
513
+ workspaceTitle: workspace.title
514
+ },
515
+ archived: archived.has(id),
516
+ running: summary.running === true,
517
+ updatedAt: typeof summary.updatedAt === "number" && Number.isFinite(summary.updatedAt) ? summary.updatedAt : 0
518
+ });
519
+ }
520
+ rows.sort((left, right) => right.updatedAt - left.updatedAt || left.title.localeCompare(right.title));
521
+ return rows;
522
+ }
523
+ /** Read the document language the locale service keeps in sync. */
524
+ function localeTag() {
525
+ if (typeof document === "undefined") return void 0;
526
+ const language = document.documentElement.lang;
527
+ return language === "" ? void 0 : language;
528
+ }
529
+ /** Format one epoch instant, or an em dash when the row carries none. */
530
+ function formatDay(ms) {
531
+ if (!Number.isFinite(ms) || ms <= 0) return "—";
532
+ try {
533
+ return new Intl.DateTimeFormat(localeTag(), {
534
+ year: "numeric",
535
+ month: "2-digit",
536
+ day: "2-digit"
537
+ }).format(ms);
538
+ } catch {
539
+ return "—";
540
+ }
541
+ }
542
+ /** Format the same instant with its time, for the row's hover text. */
543
+ function formatFull(ms) {
544
+ if (!Number.isFinite(ms) || ms <= 0) return "";
545
+ try {
546
+ return new Intl.DateTimeFormat(localeTag(), {
547
+ dateStyle: "medium",
548
+ timeStyle: "short"
549
+ }).format(ms);
550
+ } catch {
551
+ return "";
552
+ }
553
+ }
554
+ /** Word one failure, keeping the retryable summary ahead of its detail. */
555
+ function problemText(t, problem) {
556
+ const parts = [];
557
+ if (problem.prefix !== void 0) parts.push(t(problem.prefix.key, problem.prefix.params));
558
+ if (problem.failure !== void 0) parts.push(t(`error.${problem.failure.code}`, problem.failure.params));
559
+ else if (typeof problem.error === "string" && problem.error !== "") parts.push(problem.error === "HTTP 405" ? t("error.host-stale") : problem.error);
560
+ else if (problem.prefix === void 0) parts.push(t(problem.fallback ?? "sessions.actionFailed"));
561
+ return parts.join(" ");
562
+ }
563
+ /** Word a request-level failure, translating the one code the page knows. */
564
+ function rawReason(t, error, fallback) {
565
+ if (error === "HTTP 405") return t("error.host-stale");
566
+ return typeof error === "string" && error !== "" ? error : t(fallback);
567
+ }
568
+ /** One activity family, as the stop-and-archive dialog names it. */
569
+ function activityLine(t, activity) {
570
+ switch (activity.kind) {
571
+ case "turn": return t("sessions.activityTurn");
572
+ case "subagent": return t(countKey("sessions.activitySubagents", activity.count), { count: activity.count });
573
+ case "job": return t(countKey("sessions.activityJobs", activity.count), { count: activity.count });
574
+ case "schedule": return t(countKey("sessions.activitySchedules", activity.count), { count: activity.count });
575
+ default: return t(countKey("sessions.activityOther", activity.count), {
576
+ count: activity.count,
577
+ kind: activity.kind
578
+ });
579
+ }
580
+ }
581
+ /**
582
+ * One toolbar filter: the product's Menu anchored to a control shaped like the
583
+ * search box, so both filters share its 32px height and its border.
584
+ *
585
+ * The list portals to the document body: the options column is a scroll
586
+ * container, and an in-place card would be clipped at its edge (or add a
587
+ * horizontal scrollbar) instead of floating over the panel.
588
+ *
589
+ * @param props - the current option, the rows to offer, and the change callback.
590
+ * @returns the trigger and its dropdown.
591
+ */
592
+ function FilterMenu({ label, items, selectedId, onSelect, className, align = "start", listClassName }) {
593
+ const [open, setOpen] = (0, react.useState)(false);
594
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Menu, {
595
+ open,
596
+ portal: true,
597
+ align,
598
+ items,
599
+ selectedId,
600
+ listClassName,
601
+ className: "dsh-cs-filter-wrap",
602
+ onSelect: (id) => {
603
+ setOpen(false);
604
+ onSelect(id);
605
+ },
606
+ onClose: () => {
607
+ setOpen(false);
608
+ },
609
+ anchor: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
610
+ type: "button",
611
+ className: className === void 0 ? "dsh-cs-filter" : `dsh-cs-filter ${className}`,
612
+ "aria-haspopup": "menu",
613
+ "aria-expanded": open,
614
+ onClick: () => {
615
+ setOpen((value) => !value);
616
+ },
617
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
618
+ className: "dsh-cs-filter-label",
619
+ children: label
620
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
621
+ className: "dsh-cs-caret",
622
+ "aria-hidden": "true",
623
+ dangerouslySetInnerHTML: { __html: ARROW_DOWN_MARKUP }
624
+ })]
625
+ })
626
+ });
627
+ }
628
+ /**
629
+ * Render the Sessions page.
630
+ *
631
+ * @param props - the locale seat, the two catalog hooks, and the injected actions.
632
+ * @returns the management surface and its confirmations.
633
+ */
634
+ function SessionsSection({ t, useSessions, useWorkspaces, setArchived, refresh, onDeleted }) {
635
+ const sessionState = useSessions((state) => state);
636
+ const workspaceState = useWorkspaces((state) => state);
637
+ const [removed, setRemoved] = (0, react.useState)(() => /* @__PURE__ */ new Set());
638
+ const rows = (0, react.useMemo)(() => buildRows(sessionState, workspaceState).filter((row) => !removed.has(row.id)), [
639
+ sessionState,
640
+ workspaceState,
641
+ removed
642
+ ]);
643
+ const [query, setQuery] = (0, react.useState)("");
644
+ const [workspaceFilter, setWorkspaceFilter] = (0, react.useState)("all");
645
+ const [archiveFilter, setArchiveFilter] = (0, react.useState)("all");
646
+ const [selected, setSelected] = (0, react.useState)(() => /* @__PURE__ */ new Set());
647
+ const [busyId, setBusyId] = (0, react.useState)(null);
648
+ const [bulkArchiving, setBulkArchiving] = (0, react.useState)(false);
649
+ const [problem, setProblem] = (0, react.useState)(null);
650
+ const [deleteTarget, setDeleteTarget] = (0, react.useState)(null);
651
+ const [deleting, setDeleting] = (0, react.useState)(false);
652
+ const [deleteError, setDeleteError] = (0, react.useState)(void 0);
653
+ const [archiveStop, setArchiveStop] = (0, react.useState)(null);
654
+ const [archiving, setArchiving] = (0, react.useState)(false);
655
+ const [archiveError, setArchiveError] = (0, react.useState)(void 0);
656
+ const selectAllRef = (0, react.useRef)(null);
657
+ (0, react.useEffect)(() => {
658
+ setSelected((previous) => {
659
+ if (previous.size === 0) return previous;
660
+ const existing = new Set(rows.map((row) => row.id));
661
+ let changed = false;
662
+ const next = /* @__PURE__ */ new Set();
663
+ for (const id of previous) if (existing.has(id)) next.add(id);
664
+ else changed = true;
665
+ return changed ? next : previous;
666
+ });
667
+ }, [rows]);
668
+ const workspaceOptions = (0, react.useMemo)(() => {
669
+ const titles = /* @__PURE__ */ new Map();
670
+ for (const row of rows) if (row.workspaceId !== void 0) titles.set(row.workspaceId, row.workspaceTitle ?? "");
671
+ return [...titles].map(([id, title]) => ({
672
+ id,
673
+ title
674
+ })).sort((left, right) => left.title.localeCompare(right.title));
675
+ }, [rows]);
676
+ const hasUnassigned = (0, react.useMemo)(() => rows.some((row) => row.workspaceId === void 0), [rows]);
677
+ const workspaceItems = (0, react.useMemo)(() => [
678
+ {
679
+ id: "all",
680
+ label: t("sessions.allWorkspaces")
681
+ },
682
+ ...workspaceOptions.map((option) => ({
683
+ id: option.id,
684
+ label: option.title
685
+ })),
686
+ ...hasUnassigned ? [{
687
+ id: "none",
688
+ label: t("sessions.noWorkspace")
689
+ }] : []
690
+ ], [
691
+ t,
692
+ workspaceOptions,
693
+ hasUnassigned
694
+ ]);
695
+ const archiveItems = (0, react.useMemo)(() => [
696
+ {
697
+ id: "all",
698
+ label: t("sessions.allSessions")
699
+ },
700
+ {
701
+ id: "unarchived",
702
+ label: t("sessions.unarchived")
703
+ },
704
+ {
705
+ id: "archived",
706
+ label: t("sessions.archived")
707
+ }
708
+ ], [t]);
709
+ const workspaceLabel = workspaceFilter === "all" ? t("sessions.allWorkspaces") : workspaceFilter === "none" ? t("sessions.noWorkspace") : workspaceOptions.find((option) => option.id === workspaceFilter)?.title ?? t("sessions.allWorkspaces");
710
+ const archiveLabel = archiveFilter === "all" ? t("sessions.allSessions") : archiveFilter === "unarchived" ? t("sessions.unarchived") : t("sessions.archived");
711
+ (0, react.useEffect)(() => {
712
+ if (workspaceFilter === "none") {
713
+ if (!hasUnassigned) setWorkspaceFilter("all");
714
+ return;
715
+ }
716
+ if (workspaceFilter !== "all" && !workspaceOptions.some((option) => option.id === workspaceFilter)) setWorkspaceFilter("all");
717
+ }, [
718
+ workspaceFilter,
719
+ workspaceOptions,
720
+ hasUnassigned
721
+ ]);
722
+ const filtered = (0, react.useMemo)(() => {
723
+ const needle = query.trim().toLocaleLowerCase();
724
+ return rows.filter((row) => {
725
+ if (workspaceFilter !== "all") {
726
+ if (workspaceFilter === "none") {
727
+ if (row.workspaceId !== void 0) return false;
728
+ } else if (row.workspaceId !== workspaceFilter) return false;
729
+ }
730
+ if (archiveFilter === "archived" && !row.archived) return false;
731
+ if (archiveFilter === "unarchived" && row.archived) return false;
732
+ if (needle !== "" && !row.title.toLocaleLowerCase().includes(needle)) return false;
733
+ return true;
734
+ });
735
+ }, [
736
+ rows,
737
+ query,
738
+ workspaceFilter,
739
+ archiveFilter
740
+ ]);
741
+ const visibleIds = (0, react.useMemo)(() => filtered.map((row) => row.id), [filtered]);
742
+ const allSelected = visibleIds.length > 0 && visibleIds.every((id) => selected.has(id));
743
+ const someSelected = visibleIds.some((id) => selected.has(id));
744
+ (0, react.useEffect)(() => {
745
+ const node = selectAllRef.current;
746
+ if (node !== null) node.indeterminate = someSelected && !allSelected;
747
+ }, [someSelected, allSelected]);
748
+ const clearSelection = () => {
749
+ setSelected(/* @__PURE__ */ new Set());
750
+ };
751
+ const archivableSelected = (0, react.useMemo)(() => rows.filter((row) => selected.has(row.id) && !row.archived), [rows, selected]);
752
+ const toggleAll = () => {
753
+ setSelected((previous) => {
754
+ const next = new Set(previous);
755
+ if (allSelected) for (const id of visibleIds) next.delete(id);
756
+ else for (const id of visibleIds) next.add(id);
757
+ return next;
758
+ });
759
+ };
760
+ const toggleRow = (id, checked) => {
761
+ setSelected((previous) => {
762
+ const next = new Set(previous);
763
+ if (checked) next.add(id);
764
+ else next.delete(id);
765
+ return next;
766
+ });
767
+ };
768
+ const openDelete = (ids) => {
769
+ if (ids.length === 0) return;
770
+ setDeleteError(void 0);
771
+ setDeleteTarget({ ids: [...ids] });
772
+ };
773
+ const closeDelete = () => {
774
+ if (deleting) return;
775
+ setDeleteTarget(null);
776
+ setDeleteError(void 0);
777
+ };
778
+ const confirmDelete = async () => {
779
+ const target = deleteTarget;
780
+ if (deleting || target === null) return;
781
+ setDeleting(true);
782
+ setDeleteError(void 0);
783
+ const payload = await deleteSessions(target.ids);
784
+ const results = payload.results ?? [];
785
+ const requestError = typeof payload.error === "string" && payload.error !== "" ? payload.error : void 0;
786
+ if (results.length === 0) {
787
+ setDeleting(false);
788
+ setDeleteError(rawReason(t, requestError, "sessions.actionFailed"));
789
+ return;
790
+ }
791
+ const succeeded = results.filter((result) => result.ok === true && typeof result.sessionId === "string").map((result) => result.sessionId);
792
+ const failed = results.filter((result) => result.ok !== true);
793
+ for (const id of succeeded) onDeleted?.(id);
794
+ if (succeeded.length > 0) {
795
+ setRemoved((previous) => {
796
+ const next = new Set(previous);
797
+ for (const id of succeeded) next.add(id);
798
+ return next;
799
+ });
800
+ setSelected((previous) => {
801
+ if (previous.size === 0) return previous;
802
+ const next = new Set(previous);
803
+ for (const id of succeeded) next.delete(id);
804
+ return next;
805
+ });
806
+ }
807
+ setDeleting(false);
808
+ if (succeeded.length === 0) {
809
+ const first = failed[0];
810
+ setDeleteError(first === void 0 ? rawReason(t, requestError, "sessions.actionFailed") : reasonOf(t, first, "error.delete-failed"));
811
+ return;
812
+ }
813
+ setDeleteTarget(null);
814
+ if (failed.length > 0) {
815
+ const first = failed[0];
816
+ const detail = requestError ?? first?.error;
817
+ setProblem({
818
+ prefix: {
819
+ key: countKey("sessions.bulkPartial", succeeded.length),
820
+ params: {
821
+ deleted: succeeded.length,
822
+ failed: failed.length
823
+ }
824
+ },
825
+ ...first === void 0 || first.failure === void 0 ? {} : { failure: first.failure },
826
+ ...detail === void 0 ? {} : { error: detail }
827
+ });
828
+ } else if (requestError !== void 0) setProblem({
829
+ prefix: {
830
+ key: countKey("sessions.bulkStopped", succeeded.length),
831
+ params: { deleted: succeeded.length }
832
+ },
833
+ error: requestError
834
+ });
835
+ };
836
+ const toggleArchive = async (row) => {
837
+ if (setArchived === void 0) {
838
+ setProblem({ error: t("sessions.unavailable") });
839
+ return;
840
+ }
841
+ setBusyId(row.id);
842
+ setProblem(null);
843
+ const result = await setArchived(row.id, row.archived, false);
844
+ setBusyId(null);
845
+ if (result.ok) return;
846
+ if (result.unavailable === true) {
847
+ setProblem({ error: t("sessions.unavailable") });
848
+ return;
849
+ }
850
+ if (result.active !== void 0) {
851
+ setArchiveError(void 0);
852
+ setArchiveStop({
853
+ row,
854
+ activity: result.active
855
+ });
856
+ return;
857
+ }
858
+ setProblem({
859
+ ...result.failure === void 0 ? {} : { failure: result.failure },
860
+ ...result.message === void 0 ? {} : { error: result.message }
861
+ });
862
+ };
863
+ /**
864
+ * Archive every selected row that is not archived yet.
865
+ *
866
+ * Running rows are left for the single-row stop-and-archive confirmation:
867
+ * stopping work in bulk without naming the sessions would hide what is about
868
+ * to be interrupted, so this pass skips them and reports the count instead.
869
+ */
870
+ const archiveSelected = async () => {
871
+ if (setArchived === void 0) {
872
+ setProblem({ error: t("sessions.unavailable") });
873
+ return;
874
+ }
875
+ if (bulkArchiving) return;
876
+ const targets = rows.filter((row) => selected.has(row.id) && !row.archived);
877
+ if (targets.length === 0) return;
878
+ setBulkArchiving(true);
879
+ setProblem(null);
880
+ const archivedIds = [];
881
+ let skipped = 0;
882
+ let firstFailure;
883
+ for (const row of targets) {
884
+ const result = await setArchived(row.id, false, false);
885
+ if (result.ok) archivedIds.push(row.id);
886
+ else if (result.active !== void 0) skipped += 1;
887
+ else firstFailure ??= result;
888
+ }
889
+ setBulkArchiving(false);
890
+ if (archivedIds.length > 0) setSelected((previous) => {
891
+ const next = new Set(previous);
892
+ for (const id of archivedIds) next.delete(id);
893
+ return next;
894
+ });
895
+ const failed = targets.length - archivedIds.length - skipped;
896
+ if (failed > 0) {
897
+ setProblem({
898
+ ...archivedIds.length === 0 ? {} : { prefix: {
899
+ key: countKey("sessions.bulkArchiveFailed", archivedIds.length),
900
+ params: {
901
+ archived: archivedIds.length,
902
+ failed
903
+ }
904
+ } },
905
+ ...firstFailure?.failure === void 0 ? {} : { failure: firstFailure.failure },
906
+ ...firstFailure?.message === void 0 ? {} : { error: firstFailure.message }
907
+ });
908
+ return;
909
+ }
910
+ if (skipped > 0) setProblem({ prefix: {
911
+ key: countKey("sessions.bulkArchivePartial", archivedIds.length),
912
+ params: {
913
+ archived: archivedIds.length,
914
+ skipped
915
+ }
916
+ } });
917
+ };
918
+ const closeArchiveStop = () => {
919
+ if (archiving) return;
920
+ setArchiveStop(null);
921
+ setArchiveError(void 0);
922
+ };
923
+ const confirmStopArchive = async () => {
924
+ const pending = archiveStop;
925
+ if (archiving || pending === null || setArchived === void 0) return;
926
+ setArchiving(true);
927
+ setArchiveError(void 0);
928
+ const result = await setArchived(pending.row.id, false, true);
929
+ setArchiving(false);
930
+ if (result.ok) {
931
+ setArchiveStop(null);
932
+ return;
933
+ }
934
+ if (result.failure !== void 0) {
935
+ setArchiveError(t(`error.${result.failure.code}`, result.failure.params));
936
+ return;
937
+ }
938
+ setArchiveError(typeof result.message === "string" && result.message !== "" ? result.message : t("sessions.actionFailed"));
939
+ };
940
+ const loading = sessionState.phase === "pending" && rows.length === 0;
941
+ const loadFailed = !loading && rows.length === 0 && sessionState.error !== void 0 && sessionState.error !== null;
942
+ const deleteIsBatch = (deleteTarget?.ids.length ?? 0) > 1;
943
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
944
+ className: "dsh-cs-settings",
945
+ children: [
946
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
947
+ className: "dsh-cs-settings-heading",
948
+ children: t("settings.title")
949
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
950
+ className: "dsh-cs-settings-description",
951
+ children: t("settings.description")
952
+ })] }),
953
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
954
+ className: "dsh-cs-toolbar",
955
+ children: [
956
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Input, {
957
+ className: "dsh-cs-search",
958
+ type: "search",
959
+ value: query,
960
+ placeholder: t("sessions.searchPlaceholder"),
961
+ "aria-label": t("sessions.search"),
962
+ onChange: (event) => {
963
+ setQuery(event.target.value);
964
+ clearSelection();
965
+ }
966
+ }),
967
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FilterMenu, {
968
+ label: workspaceLabel,
969
+ items: workspaceItems,
970
+ selectedId: workspaceFilter,
971
+ className: "dsh-cs-filter-workspace",
972
+ listClassName: "dsh-cs-menu",
973
+ onSelect: (id) => {
974
+ if (id === workspaceFilter) return;
975
+ setWorkspaceFilter(id);
976
+ clearSelection();
977
+ }
978
+ }),
979
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FilterMenu, {
980
+ label: archiveLabel,
981
+ items: archiveItems,
982
+ selectedId: archiveFilter,
983
+ className: "dsh-cs-filter-archive",
984
+ align: "end",
985
+ onSelect: (id) => {
986
+ if (id === archiveFilter) return;
987
+ setArchiveFilter(id);
988
+ clearSelection();
989
+ }
990
+ })
991
+ ]
992
+ }),
993
+ problem !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
994
+ className: "dsh-cs-problem",
995
+ role: "alert",
996
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: problemText(t, problem) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
997
+ type: "button",
998
+ className: "dsh-cs-problem-dismiss",
999
+ "aria-label": t("action.close"),
1000
+ onClick: () => {
1001
+ setProblem(null);
1002
+ },
1003
+ children: "×"
1004
+ })]
1005
+ }),
1006
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1007
+ className: "dsh-cs-selection-bar",
1008
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1009
+ className: "dsh-cs-selection-count",
1010
+ children: t("sessions.selectedCount", { count: selected.size })
1011
+ }), selected.size > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1012
+ className: "dsh-cs-bulk-actions",
1013
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1014
+ type: "button",
1015
+ className: "dsh-cs-link",
1016
+ disabled: archivableSelected.length === 0 || bulkArchiving,
1017
+ onClick: () => {
1018
+ archiveSelected();
1019
+ },
1020
+ children: t("sessions.archiveSelected")
1021
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1022
+ type: "button",
1023
+ className: "dsh-cs-link dsh-cs-danger",
1024
+ disabled: deleting || bulkArchiving,
1025
+ onClick: () => {
1026
+ openDelete([...selected]);
1027
+ },
1028
+ children: t("sessions.deleteSelected")
1029
+ })]
1030
+ })]
1031
+ }),
1032
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1033
+ className: "dsh-cs-table",
1034
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1035
+ className: "dsh-cs-table-body",
1036
+ role: "table",
1037
+ "aria-label": t("settings.title"),
1038
+ children: [
1039
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1040
+ className: "dsh-cs-table-head",
1041
+ role: "row",
1042
+ children: [
1043
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1044
+ role: "columnheader",
1045
+ className: "dsh-cs-head-check",
1046
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1047
+ ref: selectAllRef,
1048
+ type: "checkbox",
1049
+ checked: allSelected,
1050
+ disabled: visibleIds.length === 0 || bulkArchiving,
1051
+ "aria-label": t("sessions.selectAll"),
1052
+ title: t("sessions.selectAll"),
1053
+ onChange: toggleAll
1054
+ })
1055
+ }),
1056
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1057
+ role: "columnheader",
1058
+ children: t("sessions.columnTitle")
1059
+ }),
1060
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1061
+ role: "columnheader",
1062
+ children: t("sessions.columnStatus")
1063
+ }),
1064
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1065
+ role: "columnheader",
1066
+ className: "dsh-cs-head-actions",
1067
+ children: t("sessions.columnActions")
1068
+ })
1069
+ ]
1070
+ }),
1071
+ loading && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1072
+ className: "dsh-cs-empty",
1073
+ children: t("sessions.loading")
1074
+ }),
1075
+ loadFailed && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1076
+ className: "dsh-cs-empty",
1077
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("sessions.loadFailed") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1078
+ variant: "outline",
1079
+ size: "sm",
1080
+ onClick: () => {
1081
+ refresh?.();
1082
+ },
1083
+ children: t("sessions.retry")
1084
+ })]
1085
+ }),
1086
+ !loading && !loadFailed && filtered.map((row) => {
1087
+ const full = formatFull(row.updatedAt);
1088
+ const state = row.running ? "running" : row.archived ? "archived" : "unarchived";
1089
+ const stateLabel = row.running ? t("sessions.running") : row.archived ? t("sessions.archived") : t("sessions.unarchived");
1090
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1091
+ className: "dsh-cs-row",
1092
+ role: "row",
1093
+ "data-session-id": row.id,
1094
+ "data-archived": row.archived ? "1" : "0",
1095
+ "data-running": row.running ? "1" : "0",
1096
+ children: [
1097
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1098
+ className: "dsh-cs-row-check",
1099
+ type: "checkbox",
1100
+ checked: selected.has(row.id),
1101
+ disabled: bulkArchiving,
1102
+ "aria-label": t("sessions.selectOne", { title: row.title }),
1103
+ onChange: (event) => {
1104
+ toggleRow(row.id, event.target.checked);
1105
+ }
1106
+ }),
1107
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1108
+ className: "dsh-cs-main",
1109
+ role: "cell",
1110
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1111
+ className: "dsh-cs-title",
1112
+ title: row.title,
1113
+ children: row.title
1114
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
1115
+ className: "dsh-cs-meta",
1116
+ children: [
1117
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1118
+ className: "dsh-cs-workspace",
1119
+ title: row.cwd ?? row.workspaceTitle,
1120
+ children: row.workspaceTitle ?? t("sessions.noWorkspace")
1121
+ }),
1122
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1123
+ className: "dsh-cs-sep",
1124
+ "aria-hidden": "true",
1125
+ children: "·"
1126
+ }),
1127
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("time", {
1128
+ className: "dsh-cs-date",
1129
+ dateTime: row.updatedAt > 0 ? new Date(row.updatedAt).toISOString() : void 0,
1130
+ title: full,
1131
+ children: formatDay(row.updatedAt)
1132
+ })
1133
+ ]
1134
+ })]
1135
+ }),
1136
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
1137
+ className: `dsh-cs-status dsh-cs-status-${state}`,
1138
+ role: "cell",
1139
+ children: stateLabel
1140
+ }),
1141
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1142
+ className: "dsh-cs-actions",
1143
+ role: "cell",
1144
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1145
+ type: "button",
1146
+ className: "dsh-cs-link",
1147
+ disabled: busyId === row.id || bulkArchiving,
1148
+ onClick: () => {
1149
+ toggleArchive(row);
1150
+ },
1151
+ children: row.archived ? t("sessions.unarchive") : t("sessions.archive")
1152
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1153
+ type: "button",
1154
+ className: "dsh-cs-link dsh-cs-danger",
1155
+ disabled: deleting || busyId === row.id || bulkArchiving,
1156
+ onClick: () => {
1157
+ openDelete([row.id]);
1158
+ },
1159
+ children: t("sessions.delete")
1160
+ })]
1161
+ })
1162
+ ]
1163
+ }, row.id);
1164
+ }),
1165
+ !loading && !loadFailed && filtered.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1166
+ className: "dsh-cs-empty",
1167
+ children: rows.length === 0 ? t("sessions.empty") : t("sessions.noMatches")
1168
+ })
1169
+ ]
1170
+ })
1171
+ }),
1172
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
1173
+ open: deleteTarget !== null,
1174
+ onClose: closeDelete,
1175
+ title: deleteIsBatch ? t("sessions.deleteSelectedTitle") : t("delete.title"),
1176
+ description: deleteIsBatch ? t("sessions.deleteBatchDescription", { count: deleteTarget?.ids.length ?? 0 }) : t("delete.description"),
1177
+ closeLabel: t("action.close"),
1178
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1179
+ variant: "outline",
1180
+ "data-modal-autofocus": true,
1181
+ disabled: deleting,
1182
+ onClick: closeDelete,
1183
+ children: t("action.cancel")
1184
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1185
+ variant: "outline",
1186
+ className: "dsh-cs-delete-action",
1187
+ disabled: deleting,
1188
+ onClick: () => {
1189
+ confirmDelete();
1190
+ },
1191
+ children: deleting ? t("delete.pending") : t("delete.confirm")
1192
+ })] }),
1193
+ children: deleteError === void 0 ? void 0 : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1194
+ className: "dsh-cs-delete-error",
1195
+ role: "alert",
1196
+ children: deleteError
1197
+ })
1198
+ }),
1199
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_deepseek_ai_dsh_client_ui_primitives.Modal, {
1200
+ open: archiveStop !== null,
1201
+ onClose: closeArchiveStop,
1202
+ title: t("sessions.archiveStopTitle"),
1203
+ description: archiveStop === null ? void 0 : t("sessions.archiveStopDescription", { title: archiveStop.row.title }),
1204
+ closeLabel: t("action.close"),
1205
+ footer: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1206
+ variant: "outline",
1207
+ "data-modal-autofocus": true,
1208
+ disabled: archiving,
1209
+ onClick: closeArchiveStop,
1210
+ children: t("action.cancel")
1211
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.Button, {
1212
+ variant: "outline",
1213
+ className: "dsh-cs-delete-action",
1214
+ disabled: archiving,
1215
+ onClick: () => {
1216
+ confirmStopArchive();
1217
+ },
1218
+ children: archiving ? t("sessions.archiveStopPending") : t("sessions.archiveStopAction")
1219
+ })] }),
1220
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
1221
+ className: "dsh-cs-activity",
1222
+ "aria-label": t("sessions.archiveActivity"),
1223
+ children: (archiveStop?.activity ?? []).map((entry, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: activityLine(t, entry) }, `${entry.kind}-${String(index)}`))
1224
+ }), archiveError === void 0 ? void 0 : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1225
+ className: "dsh-cs-delete-error",
1226
+ role: "alert",
1227
+ children: archiveError
1228
+ })]
1229
+ })
1230
+ ]
1231
+ });
1232
+ }
1233
+ // src/client/archive.ts
1234
+ /** Read the activity families out of a `workspace/session-active` refusal. */
1235
+ function activityOf(details) {
1236
+ const entries = details?.activity;
1237
+ if (!Array.isArray(entries)) return [];
1238
+ const activity = [];
1239
+ for (const entry of entries) {
1240
+ if (entry === null || typeof entry !== "object") continue;
1241
+ const record = entry;
1242
+ if (typeof record.kind !== "string" || record.kind === "") continue;
1243
+ const count = Array.isArray(record.items) && record.items.length > 0 ? record.items.length : 1;
1244
+ activity.push({
1245
+ kind: record.kind,
1246
+ count
1247
+ });
1248
+ }
1249
+ return activity;
1250
+ }
1251
+ /**
1252
+ * Archive or restore one Session.
1253
+ *
1254
+ * @param workspaces - the client Workspace service, when the composition mounts one.
1255
+ * @param sessionId - the Session to move between the two sets.
1256
+ * @param archived - which set it should end in.
1257
+ * @param stopActivity - ask the Host to stop running work instead of refusing.
1258
+ * @returns the outcome the panel renders.
1259
+ */
1260
+ async function setSessionArchived(workspaces, sessionId, archived, stopActivity) {
1261
+ if (workspaces === void 0 || workspaces === null || typeof workspaces.archiveSession !== "function" || typeof workspaces.unarchiveSession !== "function") return {
1262
+ ok: false,
1263
+ unavailable: true
1264
+ };
1265
+ try {
1266
+ if (archived) await workspaces.unarchiveSession(sessionId);
1267
+ else await workspaces.archiveSession(sessionId, stopActivity ? { stopActivity: true } : void 0);
1268
+ return { ok: true };
1269
+ } catch (error) {
1270
+ const failure = error;
1271
+ const code = failure?.rpcError?.code;
1272
+ if (code === "workspace/session-active") {
1273
+ const activity = activityOf(failure.rpcError?.details);
1274
+ if (activity.length > 0) return {
1275
+ ok: false,
1276
+ active: activity
1277
+ };
1278
+ return {
1279
+ ok: false,
1280
+ message: typeof failure.rpcError?.message === "string" && failure.rpcError.message !== "" ? failure.rpcError.message : "the session is still running"
1281
+ };
1282
+ }
1283
+ if (code === "session/not-found") return {
1284
+ ok: false,
1285
+ failure: { code: "unknown-session" }
1286
+ };
1287
+ return {
1288
+ ok: false,
1289
+ message: error instanceof Error ? error.message : String(error)
1290
+ };
1291
+ }
1292
+ }
1293
+ // src/client/nav-icon.ts
1294
+ /**
1295
+ * The archive glyph on the Settings panel's Sessions entry.
1296
+ *
1297
+ * A section registration carries a label, not an icon: the shipped settings
1298
+ * shell picks its navigation glyph from the section id (`navIcon(row.id)` in
1299
+ * ui-settings-general), and every id it does not know — a plugin's id included —
1300
+ * gets the generic gear. The entry therefore owns its glyph the only way a
1301
+ * plugin can: after the cell renders, hide the shell's gear and put the parsed
1302
+ * `assets/archive.svg` artwork in the seat beside the label. dsh-cool-theme
1303
+ * patches its palette into the same seat for the same reason.
1304
+ *
1305
+ * Matching is by the rendered label rather than by class hash, because the
1306
+ * shell's CSS module hashes rotate between releases. The observer only watches
1307
+ * child-list changes: opening and closing Settings replaces the cell's children,
1308
+ * while typing elsewhere in the page usually edits text nodes and must not
1309
+ * trigger a scan.
1310
+ */
1311
+ /** Marks the span this module inserts, so re-patching stays idempotent. */
1312
+ const ICON_ATTRIBUTE = "data-dsh-cs-nav-icon";
1313
+ /** Marks the gear this module hid, so disposal can restore it. */
1314
+ const HIDDEN_ATTRIBUTE = "data-dsh-cs-nav-hidden";
1315
+ /** The nav label in every locale, plus the one this render is reading. */
1316
+ function settingsLabels(t) {
1317
+ let current = "";
1318
+ try {
1319
+ current = t("settings.nav");
1320
+ } catch {}
1321
+ return [
1322
+ zh["settings.nav"],
1323
+ en["settings.nav"],
1324
+ current
1325
+ ];
1326
+ }
1327
+ /**
1328
+ * Find the settings navigation cells, tolerating a rotated class hash.
1329
+ *
1330
+ * The panel carries `data-shortcut-modal="settings"` in the shipped shell; using
1331
+ * it as the scope keeps the observer from scanning the rest of the page while
1332
+ * Settings is closed. Inside the panel the cell class may be rehashed, so the
1333
+ * fallback reads every button under the nav column.
1334
+ *
1335
+ * @returns the cells to inspect, or every button under a nav-like element when
1336
+ * the hashed name is not present.
1337
+ */
1338
+ function navCells() {
1339
+ const panel = document.querySelector("[data-shortcut-modal=\"settings\"]");
1340
+ if (panel === null) return [];
1341
+ const cells = panel.querySelectorAll("[class*=\"navCell\"]");
1342
+ if (cells.length > 0) return [...cells];
1343
+ return [...panel.querySelectorAll("[class*=\"nav\"] button")];
1344
+ }
1345
+ /**
1346
+ * Replace the gear on the Sessions cell with the plugin's archive glyph.
1347
+ *
1348
+ * @param labels - the label spellings that identify this section's cell.
1349
+ */
1350
+ function patch(labels) {
1351
+ for (const cell of navCells()) {
1352
+ const label = cell.querySelector("[class*=\"navLabel\"]")?.textContent ?? cell.textContent ?? "";
1353
+ if (!labels.has(label.trim())) continue;
1354
+ if (cell.querySelector(`[${ICON_ATTRIBUTE}]`) !== null) continue;
1355
+ const svg = cell.querySelector("svg");
1356
+ if (svg === null) continue;
1357
+ svg.style.display = "none";
1358
+ svg.setAttribute(HIDDEN_ATTRIBUTE, "1");
1359
+ const holder = document.createElement("span");
1360
+ holder.className = "dsh-cs-nav-icon";
1361
+ holder.setAttribute(ICON_ATTRIBUTE, "1");
1362
+ holder.setAttribute("aria-hidden", "true");
1363
+ holder.innerHTML = ARCHIVE_NAV_MARKUP;
1364
+ svg.after(holder);
1365
+ }
1366
+ }
1367
+ /**
1368
+ * Keep the Sessions entry's glyph patched for the plugin's lifetime.
1369
+ *
1370
+ * Runs once for a panel that is already open and again whenever the shell mounts
1371
+ * or re-renders the navigation. The disposer removes the inserted spans and
1372
+ * restores the gears, so an HMR update does not leave two glyphs in the seat.
1373
+ *
1374
+ * @param t - the plugin's translate seat, for the current locale's label.
1375
+ * @returns the disposer that stops observation and restores the shell's DOM.
1376
+ */
1377
+ function installSessionNavIcon(t) {
1378
+ if (typeof document === "undefined" || typeof MutationObserver === "undefined") return () => {};
1379
+ const labels = new Set(settingsLabels(t).filter((label) => label.length > 0));
1380
+ const apply = () => {
1381
+ patch(labels);
1382
+ };
1383
+ apply();
1384
+ let frame;
1385
+ const schedule = () => {
1386
+ if (frame !== void 0) return;
1387
+ if (typeof requestAnimationFrame !== "function") {
1388
+ apply();
1389
+ return;
1390
+ }
1391
+ frame = requestAnimationFrame(() => {
1392
+ frame = void 0;
1393
+ apply();
1394
+ });
1395
+ };
1396
+ const observer = new MutationObserver(schedule);
1397
+ observer.observe(document.body, {
1398
+ childList: true,
1399
+ subtree: true
1400
+ });
1401
+ return () => {
1402
+ observer.disconnect();
1403
+ if (frame !== void 0) cancelAnimationFrame(frame);
1404
+ for (const holder of document.querySelectorAll(`[${ICON_ATTRIBUTE}]`)) holder.remove();
1405
+ for (const svg of document.querySelectorAll(`svg[${HIDDEN_ATTRIBUTE}]`)) {
1406
+ svg.style.display = "";
1407
+ svg.removeAttribute(HIDDEN_ATTRIBUTE);
1408
+ }
1409
+ };
1410
+ }
1411
+ // src/client/styles.ts
1412
+ /** CSS rules, one selector per line. */
1413
+ const PLUGIN_CSS = [
1414
+ ".dsh-cs-menu-delete-icon{display:inline-flex;align-items:center;justify-content:center;color:var(--dsw-alias-state-error-primary)}",
1415
+ ".dsh-cs-menu-delete-label{color:var(--dsw-alias-state-error-primary)}",
1416
+ ".dsh-cs-delete-action:not(:disabled){color:var(--dsw-alias-state-error-primary)}",
1417
+ ".dsh-cs-delete-error{padding:8px 9px;border:.5px solid color-mix(in srgb,var(--dsw-alias-state-error-primary) 35%,transparent);border-radius:var(--dsw-radius-sm,6px);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 9%,transparent);color:var(--dsw-alias-state-error-primary);font-size:11.5px;line-height:1.55;white-space:pre-wrap}",
1418
+ ".dsh-cs-settings{display:flex;flex-direction:column;box-sizing:border-box;height:100%;min-height:0;gap:12px;max-width:760px;color:var(--dsw-alias-label-primary)}",
1419
+ ".dsh-cs-settings-heading{margin:0;font-size:18px;font-weight:600;line-height:26px}",
1420
+ ".dsh-cs-settings-description{margin:2px 0 0;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px}",
1421
+ ".dsh-cs-toolbar{display:flex;align-items:center;gap:8px;min-width:0}",
1422
+ ".dsh-cs-search{flex:1;min-width:120px;box-sizing:border-box}",
1423
+ ".dsh-cs-filter-wrap{flex:none;min-width:0}",
1424
+ ".dsh-cs-filter{display:inline-flex;align-items:center;gap:6px;box-sizing:border-box;height:32px;max-width:180px;padding:0 8px 0 10px;border:.5px solid var(--dsw-alias-border-l4);border-radius:var(--dsw-radius-md);background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;line-height:20px;text-align:left;cursor:pointer}",
1425
+ ".dsh-cs-filter:hover{border-color:var(--dsw-alias-border-l3)}",
1426
+ ".dsh-cs-filter:focus-visible{outline:var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:1px}",
1427
+ ".dsh-cs-filter-label{flex:1;min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}",
1428
+ ".dsh-cs-filter-workspace{width:150px}",
1429
+ ".dsh-cs-filter-archive{width:120px}",
1430
+ ".dsh-cs-caret{display:inline-flex;flex:none;align-items:center;justify-content:center;margin-left:auto;color:var(--dsw-alias-label-tertiary)}",
1431
+ ".dsh-cs-caret svg{display:block;width:12px;height:12px}",
1432
+ ".dsh-cs-menu.dsh-cs-menu{min-width:180px;max-width:280px}",
1433
+ ".dsh-cs-selection-bar{display:flex;align-items:center;gap:10px;min-height:22px}",
1434
+ ".dsh-cs-selection-count{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}",
1435
+ ".dsh-cs-bulk-actions{display:flex;align-items:center;gap:2px;margin-left:auto}",
1436
+ ".dsh-cs-head-check{display:flex;align-items:center;justify-content:center}",
1437
+ ".dsh-cs-row-check{justify-self:center}",
1438
+ ".dsh-cs-head-check input,.dsh-cs-row-check{margin:0;accent-color:var(--dsw-alias-state-business-primary);cursor:pointer}",
1439
+ ".dsh-cs-head-check input:disabled,.dsh-cs-row-check:disabled{cursor:not-allowed;opacity:.4}",
1440
+ ".dsh-cs-problem{display:flex;align-items:flex-start;gap:8px;padding:7px 9px;border:.5px solid color-mix(in srgb,var(--dsw-alias-state-error-primary) 35%,transparent);border-radius:var(--dsw-radius-sm,6px);background:color-mix(in srgb,var(--dsw-alias-state-error-primary) 9%,transparent);color:var(--dsw-alias-state-error-primary);font-size:12px;line-height:18px}",
1441
+ ".dsh-cs-problem span{flex:1;min-width:0;white-space:pre-wrap}",
1442
+ ".dsh-cs-problem-dismiss{flex:none;border:0;background:transparent;padding:0 2px;color:inherit;font:inherit;font-size:14px;line-height:18px;cursor:pointer;opacity:.7}",
1443
+ ".dsh-cs-problem-dismiss:hover{opacity:1}",
1444
+ ".dsh-cs-table{display:flex;flex:1;flex-direction:column;min-height:0;border:.5px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-md);overflow:hidden}",
1445
+ ".dsh-cs-table-body{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain}",
1446
+ ".dsh-cs-table-head,.dsh-cs-row{display:grid;grid-template-columns:22px minmax(0,1fr) 66px 112px;align-items:center;column-gap:8px;padding:8px 10px}",
1447
+ ".dsh-cs-table-head{position:sticky;top:0;z-index:1;background:var(--dsw-alias-bg-layer-1);border-bottom:.5px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);font-size:11.5px;line-height:18px}",
1448
+ ".dsh-cs-head-actions{padding-right:5px;text-align:right}",
1449
+ ".dsh-cs-row+.dsh-cs-row{border-top:.5px solid var(--dsw-alias-border-l2)}",
1450
+ ".dsh-cs-row:hover{background:var(--dsw-alias-interactive-bg-hover)}",
1451
+ ".dsh-cs-main{display:flex;flex-direction:column;gap:1px;min-width:0}",
1452
+ ".dsh-cs-title{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary)}",
1453
+ ".dsh-cs-meta{display:flex;align-items:center;gap:4px;min-width:0;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font-size:11.5px;line-height:17px;color:var(--dsw-alias-label-tertiary)}",
1454
+ ".dsh-cs-workspace{overflow:hidden;text-overflow:ellipsis}",
1455
+ ".dsh-cs-sep{flex:none;opacity:.7}",
1456
+ ".dsh-cs-status{font-size:12px;line-height:18px;white-space:nowrap}",
1457
+ ".dsh-cs-status-running{color:var(--dsw-alias-state-success-primary)}",
1458
+ ".dsh-cs-status-unarchived{color:var(--dsw-alias-state-warn-primary)}",
1459
+ ".dsh-cs-status-archived{color:var(--dsw-alias-label-tertiary)}",
1460
+ ".dsh-cs-actions{display:flex;justify-content:flex-end;gap:2px;min-width:0}",
1461
+ ".dsh-cs-link{border:0;border-radius:var(--dsw-radius-sm);padding:2px 4px;background:transparent;color:var(--dsw-alias-label-tertiary);font:inherit;font-size:12px;line-height:18px;white-space:nowrap;cursor:pointer}",
1462
+ ".dsh-cs-link:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}",
1463
+ ".dsh-cs-link:disabled{opacity:.45;cursor:not-allowed}",
1464
+ ".dsh-cs-danger:hover:not(:disabled){color:var(--dsw-alias-state-error-primary)}",
1465
+ ".dsh-cs-date{flex:none;font-size:11.5px;line-height:17px;white-space:nowrap;color:var(--dsw-alias-label-tertiary);font-variant-numeric:tabular-nums}",
1466
+ ".dsh-cs-empty{padding:26px 12px;text-align:center;font-size:13px;line-height:20px;color:var(--dsw-alias-label-tertiary)}",
1467
+ ".dsh-cs-empty p{margin:0 0 10px}",
1468
+ ".dsh-cs-activity{display:flex;flex-direction:column;gap:4px;margin:0;padding:0;list-style:none;font-size:12.5px;line-height:19px;color:var(--dsw-alias-label-secondary)}",
1469
+ ".dsh-cs-nav-icon{display:inline-flex;flex:none;align-items:center;justify-content:center}",
1470
+ ".dsh-cs-nav-icon svg{width:16px;height:16px;display:block}"
1471
+ ].join("\n");
1472
+ /**
1473
+ * Insert the stylesheet once for the plugin lifetime.
1474
+ *
1475
+ * The tag carries the platform's style-ownership record — `data-plugin` with this
1476
+ * package's id, which is what every DSH bundle writes on the tags it injects. A
1477
+ * tag carrying no attribute is claimed by whichever module materializes next and
1478
+ * deleted with it, so naming the owner here keeps the sheet this plugin's own:
1479
+ * only a reload of `dsh-cool-session` retires it, and this effect's disposer
1480
+ * removes it then anyway.
1481
+ *
1482
+ * @returns disposer that removes exactly this element.
1483
+ */
1484
+ function insertStyles() {
1485
+ const element = document.createElement("style");
1486
+ element.setAttribute("data-plugin", "dsh-cool-session");
1487
+ element.setAttribute("data-dsh-cool-session", "styles");
1488
+ element.textContent = PLUGIN_CSS;
1489
+ document.head.appendChild(element);
1490
+ return () => {
1491
+ element.remove();
1492
+ };
1493
+ }
1494
+ // src/client/index.ts
1495
+ /**
1496
+ * Client entry for dsh-cool-session.
1497
+ *
1498
+ * Contributes one native Session-menu row, the frame-wide confirmation it
1499
+ * opens, and the Sessions section of the Settings panel. The row lives in
1500
+ * `sidebar.workspaces.session.menu.item`; the dialog lives in `shell.overlay`,
1501
+ * because the menu unmounts as soon as it closes and cannot own a dialog that
1502
+ * must outlive that click; the section lives in `settings.section`, which the
1503
+ * settings shell projects into its navigation.
1504
+ *
1505
+ * The section reads the shell's Session and Workspace stores directly and
1506
+ * archives through the Workspace controller, so only deletion — the operation
1507
+ * the client face does not expose — needs this package's own Host route.
1508
+ */
1509
+ const name = "dsh-cool-session/client";
1510
+ /**
1511
+ * The locale service is a required collaborator rather than an optional one.
1512
+ *
1513
+ * The confirmation is worded entirely from this package's dictionaries, so a
1514
+ * composition without the service could only draw the wrong language: declaring
1515
+ * the dependency is what makes the framework apply this plugin once the service
1516
+ * is there, and what makes `ctx.get('locale')` below answer. DSH mounts the
1517
+ * service with the web client tree itself (`@deepseek-ai/dsh-web-app`), so
1518
+ * nothing this menu row can run in is without it.
1519
+ */
1520
+ const inject = ["slots", "locale"];
1521
+ function apply(ctx) {
1522
+ const slots = ctx.get("slots");
1523
+ if (slots === void 0) return;
1524
+ const locale = ctx.get("locale");
1525
+ ctx.effect(() => insertStyles(), "dsh-cool-session: styles");
1526
+ ctx.effect(() => locale.register(NS, {
1527
+ zh,
1528
+ en
1529
+ }), "dsh-cool-session: dictionaries");
1530
+ const t = locale.bind(NS);
1531
+ const afterDelete = (sessionId) => {
1532
+ const uiWorkspace = ctx.get("uiWorkspace");
1533
+ try {
1534
+ if (uiWorkspace?.mainReference?.sessionId === sessionId) uiWorkspace.clearMain?.();
1535
+ } catch {}
1536
+ try {
1537
+ ctx.get("sessions")?.refresh?.();
1538
+ } catch {}
1539
+ try {
1540
+ ctx.get("workspaces")?.refresh?.();
1541
+ } catch {}
1542
+ };
1543
+ const setArchived = (sessionId, archived, stopActivity) => setSessionArchived(ctx.get("workspaces"), sessionId, archived, stopActivity);
1544
+ const refreshSessions = () => {
1545
+ try {
1546
+ ctx.get("sessions")?.refresh?.();
1547
+ } catch {}
1548
+ };
1549
+ slots.inject("settings.section", () => slots.register({
1550
+ name: "settings.section",
1551
+ id: "dsh-cool-session.sessions",
1552
+ order: 12,
1553
+ label: () => t("settings.nav"),
1554
+ locale: NS,
1555
+ inject: () => ({
1556
+ setArchived,
1557
+ refresh: refreshSessions,
1558
+ onDeleted: afterDelete
1559
+ })
1560
+ }, SessionsSection));
1561
+ ctx.effect(() => installSessionNavIcon(t), "dsh-cool-session: settings nav icon");
1562
+ slots.inject("sidebar.workspaces.session.menu.item", () => slots.register({
1563
+ name: "sidebar.workspaces.session.menu.item",
1564
+ id: "dsh-cool-session.delete",
1565
+ order: 500,
1566
+ locale: NS
1567
+ }, SessionDeleteMenuItem));
1568
+ ctx.effect(() => ctx.inject(["contextMenus"], (menuCtx) => {
1569
+ retireLegacyDeleteRow();
1570
+ menuCtx.slots.inject("contextMenu.icon", () => menuCtx.slots.register({
1571
+ name: "contextMenu.icon",
1572
+ key: "dsh-cool-session.delete"
1573
+ }, _deepseek_ai_dsh_client_ui_primitives.IconTrashOutlineRegular));
1574
+ menuCtx.effect(() => menuCtx.contextMenus.register({
1575
+ id: "dsh-cool-session.delete",
1576
+ target: "session",
1577
+ order: 700,
1578
+ observe: () => [locale],
1579
+ resolve: ({ sessionId }) => [{
1580
+ id: "delete",
1581
+ label: t("action.delete"),
1582
+ icon: { key: "dsh-cool-session.delete" },
1583
+ afterContent: true,
1584
+ separatorBefore: true,
1585
+ danger: true,
1586
+ run: () => requestDeleteSession(sessionId)
1587
+ }]
1588
+ }), "dsh-cool-session: session menu action");
1589
+ }).dispose, "dsh-cool-session: session menu service");
1590
+ slots.inject("shell.overlay", () => slots.register({
1591
+ name: "shell.overlay",
1592
+ id: "dsh-cool-session.delete-dialog",
1593
+ locale: NS,
1594
+ inject: () => ({ onDeleted: afterDelete })
1595
+ }, SessionDeleteDialog));
1596
+ }
1597
+ exports.apply = apply;
1598
+ exports.inject = inject;
1599
+ exports.name = name;
1600
+ return module.exports;
1601
+ }
1602
+ });