dsh-rewind-plugin 0.6.3 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -319,6 +319,250 @@ var STYLE = `
319
319
  color: var(--dsw-alias-label-primary);
320
320
  pointer-events: none;
321
321
  }
322
+
323
+ /* ---- Snapshot-cleanup settings card (mirrors the harness PluginCard look) ---- */
324
+ .dsh-rewind-cleanup-card {
325
+ list-style: none;
326
+ border: 1px solid var(--dsw-alias-border-l2);
327
+ border-radius: 12px;
328
+ background: var(--dsw-alias-bg-layer-3);
329
+ transition: border-color .16s, background .16s;
330
+ }
331
+ .dsh-rewind-cleanup-card:hover {
332
+ border-color: var(--dsw-alias-label-dimmed);
333
+ }
334
+ .dsh-rewind-cleanup-card-open {
335
+ background: var(--dsw-alias-bg-layer-2);
336
+ border-color: var(--dsw-alias-label-dimmed);
337
+ }
338
+ .dsh-rewind-cleanup-header {
339
+ width: 100%;
340
+ appearance: none;
341
+ border: 0;
342
+ background: none;
343
+ font: inherit;
344
+ color: inherit;
345
+ text-align: left;
346
+ cursor: pointer;
347
+ display: flex;
348
+ align-items: center;
349
+ gap: 12px;
350
+ padding: 14px 16px;
351
+ border-radius: 12px;
352
+ }
353
+ .dsh-rewind-cleanup-header:focus-visible {
354
+ outline: 2px solid var(--dsw-alias-brand-primary);
355
+ outline-offset: -2px;
356
+ }
357
+ .dsh-rewind-cleanup-head-text {
358
+ flex: 1;
359
+ min-width: 0;
360
+ display: flex;
361
+ flex-direction: column;
362
+ gap: 4px;
363
+ }
364
+ .dsh-rewind-cleanup-name {
365
+ font-size: 15px;
366
+ font-weight: 600;
367
+ line-height: 1.4;
368
+ color: var(--dsw-alias-label-primary);
369
+ }
370
+ .dsh-rewind-cleanup-desc {
371
+ font-size: 13px;
372
+ line-height: 1.5;
373
+ color: var(--dsw-alias-label-tertiary);
374
+ }
375
+ .dsh-rewind-cleanup-chevron {
376
+ flex: none;
377
+ color: var(--dsw-alias-label-tertiary);
378
+ transition: transform .16s;
379
+ }
380
+ .dsh-rewind-cleanup-chevron-open {
381
+ transform: rotate(180deg);
382
+ }
383
+ .dsh-rewind-cleanup-pending {
384
+ flex: none;
385
+ border-radius: 999px;
386
+ padding: 1px 8px;
387
+ font-size: 11px;
388
+ line-height: 17px;
389
+ font-weight: 500;
390
+ white-space: nowrap;
391
+ background: var(--dsw-alias-bg-module-platform);
392
+ color: var(--dsw-alias-label-secondary);
393
+ }
394
+ .dsh-rewind-cleanup-body {
395
+ border-top: 1px solid var(--dsw-alias-border-l2);
396
+ margin: 0 16px;
397
+ padding: 4px 0 8px;
398
+ }
399
+ .dsh-rewind-cleanup-readonly {
400
+ margin: 12px 0 0;
401
+ font-size: 12px;
402
+ line-height: 1.5;
403
+ color: var(--dsw-alias-label-tertiary);
404
+ }
405
+ .dsh-rewind-cleanup-permission {
406
+ display: grid;
407
+ gap: 6px;
408
+ padding: 12px 0;
409
+ }
410
+ .dsh-rewind-cleanup-field {
411
+ display: flex;
412
+ flex-direction: column;
413
+ gap: 6px;
414
+ padding: 12px 0;
415
+ }
416
+ .dsh-rewind-cleanup-field + .dsh-rewind-cleanup-field {
417
+ border-top: 1px solid var(--dsw-alias-border-l2);
418
+ }
419
+ .dsh-rewind-cleanup-head {
420
+ display: flex;
421
+ align-items: center;
422
+ gap: 8px;
423
+ }
424
+ .dsh-rewind-cleanup-label {
425
+ flex: 1;
426
+ min-width: 0;
427
+ font-size: 13px;
428
+ font-weight: 500;
429
+ line-height: 1.5;
430
+ color: var(--dsw-alias-label-primary);
431
+ }
432
+ .dsh-rewind-cleanup-hint {
433
+ margin: 0;
434
+ font-size: 12px;
435
+ line-height: 1.5;
436
+ color: var(--dsw-alias-label-tertiary);
437
+ }
438
+ .dsh-rewind-cleanup-error {
439
+ margin: 0;
440
+ font-size: 12px;
441
+ line-height: 1.5;
442
+ color: var(--dsw-alias-label-error);
443
+ }
444
+ /* Switch row: label left, role=switch button right, hint below (Subagent module). */
445
+ .dsh-rewind-cleanup-toggle-row {
446
+ display: flex;
447
+ align-items: flex-start;
448
+ justify-content: space-between;
449
+ gap: 16px;
450
+ font-size: 13px;
451
+ line-height: 1.5;
452
+ color: var(--dsw-alias-label-primary);
453
+ }
454
+ .dsh-rewind-cleanup-toggle-label {
455
+ flex: 1;
456
+ min-width: 0;
457
+ }
458
+ .dsh-rewind-cleanup-switch {
459
+ box-sizing: border-box;
460
+ position: relative;
461
+ flex: 0 0 auto;
462
+ width: 36px;
463
+ height: 20px;
464
+ padding: 2px;
465
+ border: 0;
466
+ border-radius: 10px;
467
+ background: var(--dsw-alias-border-l3);
468
+ cursor: pointer;
469
+ }
470
+ .dsh-rewind-cleanup-switch-on {
471
+ background: var(--dsw-alias-brand-primary);
472
+ }
473
+ .dsh-rewind-cleanup-switch:disabled {
474
+ cursor: default;
475
+ opacity: 0.5;
476
+ }
477
+ .dsh-rewind-cleanup-switch:focus-visible {
478
+ outline: 2px solid var(--dsw-alias-brand-primary);
479
+ outline-offset: 2px;
480
+ }
481
+ .dsh-rewind-cleanup-thumb {
482
+ display: block;
483
+ width: 16px;
484
+ height: 16px;
485
+ border-radius: 50%;
486
+ corner-shape: round;
487
+ background: var(--dsw-alias-label-primary-foreground);
488
+ transition: transform 120ms ease;
489
+ }
490
+ .dsh-rewind-cleanup-switch-on .dsh-rewind-cleanup-thumb {
491
+ transform: translateX(16px);
492
+ }
493
+ .dsh-rewind-cleanup-input {
494
+ box-sizing: border-box;
495
+ height: 34px;
496
+ padding: 0 12px;
497
+ border: 1px solid var(--dsw-alias-border-l2);
498
+ border-radius: 8px;
499
+ background: var(--dsw-alias-bg-layer-3);
500
+ font: inherit;
501
+ font-size: 13px;
502
+ line-height: 1.5;
503
+ color: var(--dsw-alias-label-primary);
504
+ }
505
+ .dsh-rewind-cleanup-input:focus-visible {
506
+ outline: none;
507
+ border-color: var(--dsw-alias-brand-primary);
508
+ }
509
+ .dsh-rewind-cleanup-input:disabled {
510
+ color: var(--dsw-alias-label-tertiary);
511
+ cursor: default;
512
+ }
513
+ .dsh-rewind-cleanup-input-invalid {
514
+ border-color: var(--dsw-alias-label-error);
515
+ }
516
+ .dsh-rewind-cleanup-footer {
517
+ display: flex;
518
+ align-items: center;
519
+ justify-content: flex-end;
520
+ gap: 8px;
521
+ padding: 12px 0 4px;
522
+ border-top: 1px solid var(--dsw-alias-border-l2);
523
+ }
524
+ .dsh-rewind-cleanup-failed {
525
+ flex: 1;
526
+ min-width: 0;
527
+ margin: 0;
528
+ font-size: 12px;
529
+ line-height: 1.5;
530
+ color: var(--dsw-alias-label-error);
531
+ }
532
+ .dsh-rewind-cleanup-discard,
533
+ .dsh-rewind-cleanup-save {
534
+ appearance: none;
535
+ border: 1px solid transparent;
536
+ border-radius: 8px;
537
+ padding: 5px 14px;
538
+ font: inherit;
539
+ font-size: 13px;
540
+ line-height: 1.5;
541
+ cursor: pointer;
542
+ }
543
+ .dsh-rewind-cleanup-discard {
544
+ border-color: var(--dsw-alias-border-l2);
545
+ background: none;
546
+ color: var(--dsw-alias-label-secondary);
547
+ }
548
+ .dsh-rewind-cleanup-discard:hover:not(:disabled) {
549
+ color: var(--dsw-alias-label-primary);
550
+ border-color: var(--dsw-alias-label-dimmed);
551
+ }
552
+ .dsh-rewind-cleanup-save {
553
+ background: var(--dsw-alias-label-primary);
554
+ color: var(--dsw-alias-bg-layer-3);
555
+ }
556
+ .dsh-rewind-cleanup-discard:disabled,
557
+ .dsh-rewind-cleanup-save:disabled {
558
+ opacity: 0.4;
559
+ cursor: default;
560
+ }
561
+ .dsh-rewind-cleanup-discard:focus-visible,
562
+ .dsh-rewind-cleanup-save:focus-visible {
563
+ outline: 2px solid var(--dsw-alias-brand-primary);
564
+ outline-offset: 1px;
565
+ }
322
566
  `;
323
567
 
324
568
  // src/client/popover.ts
@@ -777,7 +1021,16 @@ var COMPOSER_EDITABLE_SELECTOR = "[data-composer-input]";
777
1021
  var USER_SEAT_SELECTOR = '[data-chat-flow-kind="user"][data-chat-anchor-key], [data-chat-flow-kind="steering"][data-chat-anchor-key]';
778
1022
  var CHAT_SEAT_SELECTOR = "[data-chat-anchor-key]";
779
1023
  var ACTIONS_ROOT_SELECTOR = "[data-time-hover-root], [data-actions-reveal]";
780
- var PENDING_SEAT_SELECTOR = "[data-pending-steering][data-time-hover-root], [data-pending-steering][data-actions-reveal]";
1024
+ var PENDING_SEAT_SELECTOR = "[data-pending-steering]";
1025
+ function actionsContainerOf(row) {
1026
+ const root = row?.matches(ACTIONS_ROOT_SELECTOR) ? row : row?.querySelector(ACTIONS_ROOT_SELECTOR);
1027
+ const actions = root?.lastElementChild;
1028
+ if (actions instanceof HTMLElement && actions.querySelector("button") !== null) return actions;
1029
+ const copy = row?.querySelector("button");
1030
+ const structural = copy?.parentElement;
1031
+ if (structural instanceof HTMLElement && structural.querySelector("button") !== null) return structural;
1032
+ return void 0;
1033
+ }
781
1034
  function collectTargets(chat, hiddenSeqs) {
782
1035
  const rows = /* @__PURE__ */ new Map();
783
1036
  for (const element of document.querySelectorAll(USER_SEAT_SELECTOR)) {
@@ -791,9 +1044,8 @@ function collectTargets(chat, hiddenSeqs) {
791
1044
  const user = node.data;
792
1045
  if (hiddenSeqs.has(node.anchorSeq ?? user.seq)) continue;
793
1046
  const row = rows.get(key);
794
- const messageRoot = row?.querySelector(ACTIONS_ROOT_SELECTOR);
795
- const actions = messageRoot?.lastElementChild;
796
- if (!(actions instanceof HTMLElement) || actions.querySelector("button") === null) continue;
1047
+ const actions = actionsContainerOf(row);
1048
+ if (actions === void 0) continue;
797
1049
  targets.push({ kind: "durable", key, container: actions, seq: user.seq, time: user.time, preview: messagePreviewOf(user) });
798
1050
  }
799
1051
  return targets;
@@ -818,9 +1070,8 @@ function collectPendingTargets(snapshot) {
818
1070
  if (itemId === null) continue;
819
1071
  const row = rows[i];
820
1072
  if (row === void 0) continue;
821
- const messageRoot = row.matches(ACTIONS_ROOT_SELECTOR) ? row : row.querySelector(ACTIONS_ROOT_SELECTOR);
822
- const actions = messageRoot?.lastElementChild;
823
- if (!(actions instanceof HTMLElement) || actions.querySelector("button") === null) continue;
1073
+ const actions = actionsContainerOf(row);
1074
+ if (actions === void 0) continue;
824
1075
  const item = steering[i];
825
1076
  targets.push({
826
1077
  kind: "pending",
@@ -841,6 +1092,40 @@ function sameTargets(left, right) {
841
1092
  return other.kind === "pending" && target.itemId === other.itemId;
842
1093
  });
843
1094
  }
1095
+ function hasExecutedRewindCommand(chat) {
1096
+ for (const key of chat.order) {
1097
+ const node = chat.nodes.get(key);
1098
+ if (node === void 0 || node.kind !== "command") continue;
1099
+ const command = node.data;
1100
+ if (command.name !== "rewind") continue;
1101
+ const args = command.args ?? "";
1102
+ if (args.includes("preview") || args.includes("__candidates")) continue;
1103
+ return true;
1104
+ }
1105
+ return false;
1106
+ }
1107
+ function describeHiding(chat, hiddenSeqs) {
1108
+ const commands = [];
1109
+ for (const key of chat.order) {
1110
+ const node = chat.nodes.get(key);
1111
+ if (node === void 0 || node.kind !== "command") continue;
1112
+ const c = node.data;
1113
+ commands.push(`#${c.seq} name=${c.name} outcome=${c.outcome?.kind ?? "-"} marker=${c.outcome?.sourceEventSeq ?? "-"} args=${JSON.stringify(c.args)}`);
1114
+ }
1115
+ const scoped = hiddenSeqs.size > 0 ? `hidden=[${[...hiddenSeqs].slice(0, 20).join(",")}${hiddenSeqs.size > 20 ? "\u2026" : ""}]` : "";
1116
+ let seats = 0;
1117
+ let resolved = 0;
1118
+ let inHidden = 0;
1119
+ for (const seat of document.querySelectorAll(CHAT_SEAT_SELECTOR)) {
1120
+ seats += 1;
1121
+ const key = seat.dataset.chatAnchorKey;
1122
+ const node = key === void 0 ? void 0 : chat.nodes.get(key);
1123
+ if (node === void 0) continue;
1124
+ resolved += 1;
1125
+ if (hiddenSeqs.has(node.anchorSeq)) inHidden += 1;
1126
+ }
1127
+ return `commands=[${commands.join(" | ")}] ${scoped} seats=${seats} resolved=${resolved} inHidden=${inHidden}`;
1128
+ }
844
1129
  function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, t, subscribeLocale, setComposerText }) {
845
1130
  const [targets, setTargets] = (0, import_react.useState)([]);
846
1131
  const hidden = (0, import_react.useRef)(/* @__PURE__ */ new WeakSet());
@@ -881,6 +1166,9 @@ function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, t, subs
881
1166
  `[dsh-rewind] hiding: ${hiddenCount} rows, seqs [${[...hiddenSeqs].slice(0, 20).join(", ")}${hiddenSeqs.size > 20 ? "\u2026" : ""}]`
882
1167
  );
883
1168
  }
1169
+ if (chat !== void 0 && hiddenCount === 0 && hasExecutedRewindCommand(chat)) {
1170
+ console.warn(`[dsh-rewind] rewind not hidden: ${describeHiding(chat, hiddenSeqs)}`);
1171
+ }
884
1172
  const durable = chat === void 0 ? [] : collectTargets(chat, hiddenSeqs);
885
1173
  const next = [...durable, ...collectPendingTargets(snapshot)];
886
1174
  setTargets((current) => sameTargets(current, next) ? current : next);
@@ -1042,7 +1330,24 @@ var zh = {
1042
1330
  "popover.impact.restore": "\u8FD8\u539F {path}",
1043
1331
  "popover.impact.delete": "\u5220\u9664 {path}",
1044
1332
  "popover.confirm": "\u786E\u8BA4\u56DE\u9000",
1045
- "popover.back": "\u8FD4\u56DE"
1333
+ "popover.back": "\u8FD4\u56DE",
1334
+ "cleanup.title": "\u5FEB\u7167\u6E05\u7406",
1335
+ "cleanup.desc": "dsh-rewind \xB7 \u7BA1\u7406\u4F1A\u8BDD\u5FEB\u7167\u5907\u4EFD\u7684\u81EA\u52A8\u6E05\u7406\u7B56\u7565",
1336
+ "cleanup.expand": "\u5C55\u5F00",
1337
+ "cleanup.collapse": "\u6536\u8D77",
1338
+ "cleanup.unsaved": "\u672A\u4FDD\u5B58\u4FEE\u6539",
1339
+ "cleanup.auto": "\u81EA\u52A8\u6E05\u7406",
1340
+ "cleanup.auto.on": "\u5F00\u542F\u540E\u6309\u4E0D\u6D3B\u8DC3\u5929\u6570\u81EA\u52A8\u6E05\u7406\u8FC7\u671F\u4F1A\u8BDD\u5FEB\u7167",
1341
+ "cleanup.auto.off": "\u5173\u95ED\u540E\u4FDD\u7559\u5168\u90E8\u5FEB\u7167\uFF0C\u4E0D\u81EA\u52A8\u6E05\u7406",
1342
+ "cleanup.maxAge": "\u4E0D\u6D3B\u8DC3\u65F6\u95F4\uFF08\u5929\uFF09",
1343
+ "cleanup.maxAge.hint": "\u8D85\u8FC7\u8BE5\u5929\u6570\u672A\u6D3B\u52A8\u7684\u4F1A\u8BDD\u5FEB\u7167\u4F1A\u88AB\u6E05\u7406",
1344
+ "cleanup.invalid": "\u4EC5\u63A5\u53D7\u6B63\u6574\u6570",
1345
+ "cleanup.discard": "\u653E\u5F03\u4FEE\u6539",
1346
+ "cleanup.save": "\u4FDD\u5B58",
1347
+ "cleanup.saving": "\u4FDD\u5B58\u4E2D\u2026",
1348
+ "cleanup.saved": "\u5DF2\u4FDD\u5B58\u5E76\u751F\u6548",
1349
+ "cleanup.saveFailed": "\u4FDD\u5B58\u5931\u8D25\uFF1A{message}",
1350
+ "cleanup.readonly": "\u8BBE\u7F6E\u6E90\u53EA\u8BFB"
1046
1351
  };
1047
1352
  var en = {
1048
1353
  "button.aria": "Rewind to this message",
@@ -1068,9 +1373,189 @@ var en = {
1068
1373
  "popover.impact.restore": "Restore {path}",
1069
1374
  "popover.impact.delete": "Delete {path}",
1070
1375
  "popover.confirm": "Confirm rewind",
1071
- "popover.back": "Back"
1376
+ "popover.back": "Back",
1377
+ "cleanup.title": "Snapshot cleanup",
1378
+ "cleanup.desc": "dsh-rewind \xB7 Manage the auto-cleanup policy of session snapshot backups",
1379
+ "cleanup.expand": "Expand",
1380
+ "cleanup.collapse": "Collapse",
1381
+ "cleanup.unsaved": "Unsaved changes",
1382
+ "cleanup.auto": "Auto cleanup",
1383
+ "cleanup.auto.on": "When enabled, session snapshots idle past the cutoff are cleaned automatically",
1384
+ "cleanup.auto.off": "When disabled, all snapshot backups are kept; nothing is cleaned automatically.",
1385
+ "cleanup.maxAge": "Idle time (days)",
1386
+ "cleanup.maxAge.hint": "Snapshot backups of sessions idle longer than this many days are cleaned",
1387
+ "cleanup.invalid": "A positive integer only",
1388
+ "cleanup.discard": "Discard changes",
1389
+ "cleanup.save": "Save",
1390
+ "cleanup.saving": "Saving\u2026",
1391
+ "cleanup.saved": "Saved and applied",
1392
+ "cleanup.saveFailed": "Save failed: {message}",
1393
+ "cleanup.readonly": "Read-only settings source"
1072
1394
  };
1073
1395
 
1396
+ // src/client/settings-card.tsx
1397
+ var import_react2 = require("react");
1398
+ var import_jsx_runtime2 = require("react/jsx-runtime");
1399
+ var CLEANUP_SETTINGS_NAMESPACE = "dsh-rewind-snapshot-cleanup";
1400
+ var DEFAULT_MAX_AGE_DAYS = 30;
1401
+ function draftFrom(policy) {
1402
+ return { enabled: policy?.enabled ?? false, maxAgeDays: String(policy?.maxAgeDays ?? "") };
1403
+ }
1404
+ function maxAgeOf(text) {
1405
+ const trimmed = text.trim();
1406
+ if (!/^\d+$/.test(trimmed)) return null;
1407
+ const days = Number(trimmed);
1408
+ return Number.isSafeInteger(days) && days > 0 ? days : null;
1409
+ }
1410
+ function configOf(draft) {
1411
+ const days = maxAgeOf(draft.maxAgeDays);
1412
+ if (days === null) return null;
1413
+ return { enabled: draft.enabled, maxAgeDays: days };
1414
+ }
1415
+ function dirtyOf(base, draft) {
1416
+ return base.enabled !== draft.enabled || base.maxAgeDays !== draft.maxAgeDays;
1417
+ }
1418
+ function SettingsCleanupCard({ api, t }) {
1419
+ const [open, setOpen] = (0, import_react2.useState)(false);
1420
+ const [baseline, setBaseline] = (0, import_react2.useState)(() => draftFrom(api.read()));
1421
+ const [draft, setDraft] = (0, import_react2.useState)(() => draftFrom(api.read()));
1422
+ const [busy, setBusy] = (0, import_react2.useState)(false);
1423
+ const [error, setError] = (0, import_react2.useState)(null);
1424
+ const writable = api.writable();
1425
+ (0, import_react2.useEffect)(() => api.subscribe(() => {
1426
+ const next = draftFrom(api.read());
1427
+ setBaseline((base) => {
1428
+ setDraft((cur) => dirtyOf(base, cur) ? cur : next);
1429
+ return next;
1430
+ });
1431
+ }), [api]);
1432
+ const dirty = dirtyOf(baseline, draft);
1433
+ const days = maxAgeOf(draft.maxAgeDays);
1434
+ const invalid = days === null;
1435
+ const disabled = busy || !writable;
1436
+ const edit = (patch) => {
1437
+ setDraft((cur) => ({ ...cur, ...patch }));
1438
+ setError(null);
1439
+ };
1440
+ const save = async () => {
1441
+ if (busy || !writable || !dirty) return;
1442
+ const next = configOf(draft);
1443
+ if (next === null) {
1444
+ setError(t("cleanup.invalid"));
1445
+ return;
1446
+ }
1447
+ setBusy(true);
1448
+ setError(null);
1449
+ try {
1450
+ await api.save(next);
1451
+ setBaseline(draft);
1452
+ setError(null);
1453
+ } catch (e) {
1454
+ setError(t("cleanup.saveFailed", { message: e instanceof Error ? e.message : String(e) }));
1455
+ } finally {
1456
+ setBusy(false);
1457
+ }
1458
+ };
1459
+ const discard = () => {
1460
+ if (busy) return;
1461
+ setDraft(baseline);
1462
+ setError(null);
1463
+ };
1464
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("li", { className: `dsh-rewind-cleanup-card${open ? " dsh-rewind-cleanup-card-open" : ""}`, children: [
1465
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1466
+ "button",
1467
+ {
1468
+ type: "button",
1469
+ className: "dsh-rewind-cleanup-header",
1470
+ "aria-expanded": open,
1471
+ "aria-label": `${t(open ? "cleanup.collapse" : "cleanup.expand")}: ${t("cleanup.title")}`,
1472
+ onClick: () => setOpen(!open),
1473
+ children: [
1474
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "dsh-rewind-cleanup-head-text", children: [
1475
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-rewind-cleanup-name", children: t("cleanup.title") }),
1476
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-rewind-cleanup-desc", children: t("cleanup.desc") })
1477
+ ] }),
1478
+ dirty ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-rewind-cleanup-pending", children: t("cleanup.unsaved") }) : null,
1479
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1480
+ "svg",
1481
+ {
1482
+ className: `dsh-rewind-cleanup-chevron${open ? " dsh-rewind-cleanup-chevron-open" : ""}`,
1483
+ width: "14",
1484
+ height: "14",
1485
+ viewBox: "0 0 16 16",
1486
+ "aria-hidden": "true",
1487
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("path", { d: "M4 6l4 4 4-4", fill: "none", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" })
1488
+ }
1489
+ )
1490
+ ]
1491
+ }
1492
+ ),
1493
+ open ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-rewind-cleanup-body", children: [
1494
+ !writable ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "dsh-rewind-cleanup-readonly", role: "status", children: t("cleanup.readonly") }) : null,
1495
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-rewind-cleanup-permission", children: [
1496
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-rewind-cleanup-toggle-row", children: [
1497
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-rewind-cleanup-toggle-label", id: "dsh-rewind-cleanup-enabled-label", children: t("cleanup.auto") }),
1498
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1499
+ "button",
1500
+ {
1501
+ type: "button",
1502
+ role: "switch",
1503
+ className: `dsh-rewind-cleanup-switch${draft.enabled ? " dsh-rewind-cleanup-switch-on" : ""}`,
1504
+ "aria-checked": draft.enabled,
1505
+ "aria-labelledby": "dsh-rewind-cleanup-enabled-label",
1506
+ disabled,
1507
+ onClick: () => edit({ enabled: !draft.enabled }),
1508
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-rewind-cleanup-thumb" })
1509
+ }
1510
+ )
1511
+ ] }),
1512
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "dsh-rewind-cleanup-hint", children: t(draft.enabled ? "cleanup.auto.on" : "cleanup.auto.off") })
1513
+ ] }),
1514
+ draft.enabled ? /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-rewind-cleanup-field", children: [
1515
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dsh-rewind-cleanup-head", children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("label", { className: "dsh-rewind-cleanup-label", htmlFor: "dsh-rewind-cleanup-maxage", children: t("cleanup.maxAge") }) }),
1516
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1517
+ "input",
1518
+ {
1519
+ className: `dsh-rewind-cleanup-input${invalid ? " dsh-rewind-cleanup-input-invalid" : ""}`,
1520
+ type: "text",
1521
+ inputMode: "numeric",
1522
+ id: "dsh-rewind-cleanup-maxage",
1523
+ value: draft.maxAgeDays,
1524
+ disabled,
1525
+ "aria-invalid": invalid || void 0,
1526
+ placeholder: String(DEFAULT_MAX_AGE_DAYS),
1527
+ onChange: (e) => edit({ maxAgeDays: e.target.value })
1528
+ }
1529
+ ),
1530
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: invalid ? "dsh-rewind-cleanup-error" : "dsh-rewind-cleanup-hint", children: invalid ? t("cleanup.invalid") : t("cleanup.maxAge.hint") })
1531
+ ] }) : null,
1532
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-rewind-cleanup-footer", children: [
1533
+ error ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "dsh-rewind-cleanup-failed", role: "status", children: error }) : null,
1534
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1535
+ "button",
1536
+ {
1537
+ type: "button",
1538
+ className: "dsh-rewind-cleanup-discard",
1539
+ disabled: !dirty || busy || !writable,
1540
+ onClick: discard,
1541
+ children: t("cleanup.discard")
1542
+ }
1543
+ ),
1544
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1545
+ "button",
1546
+ {
1547
+ type: "button",
1548
+ className: "dsh-rewind-cleanup-save",
1549
+ disabled: !dirty || busy || !writable || invalid,
1550
+ onClick: save,
1551
+ children: busy ? t("cleanup.saving") : t("cleanup.save")
1552
+ }
1553
+ )
1554
+ ] })
1555
+ ] }) : null
1556
+ ] });
1557
+ }
1558
+
1074
1559
  // src/client/index.ts
1075
1560
  var name = "dsh-rewind";
1076
1561
  var inject = ["slots", "sessions", "locale", "commandUi"];
@@ -1122,6 +1607,40 @@ function apply(ctx) {
1122
1607
  },
1123
1608
  createRewindBridge({ sessionOf, chatOf, currentSessionId, setComposerText, t, subscribeLocale })
1124
1609
  ));
1610
+ const clientCtx = ctx;
1611
+ clientCtx.inject(["settingsScope"], (scoped) => {
1612
+ try {
1613
+ const scope = scoped.settingsScope.bind({ namespace: CLEANUP_SETTINGS_NAMESPACE });
1614
+ const cardApi = {
1615
+ read: () => {
1616
+ const value = scope.getSnapshot().value;
1617
+ return value === void 0 ? void 0 : { enabled: value.enabled, maxAgeDays: value.maxAgeDays };
1618
+ },
1619
+ writable: () => {
1620
+ return scope.getSnapshot().writable === true;
1621
+ },
1622
+ save: async (next) => {
1623
+ await scope.set("enabled", next.enabled);
1624
+ await scope.set("maxAgeDays", next.maxAgeDays);
1625
+ },
1626
+ subscribe: (cb) => scope.subscribe(cb)
1627
+ };
1628
+ scoped.slots.inject("settings.plugin.item", () => scoped.slots.register(
1629
+ {
1630
+ name: "settings.plugin.item",
1631
+ key: CLEANUP_SETTINGS_NAMESPACE,
1632
+ // Match the official cards / dsh-market: locale + inject provide
1633
+ // the card its props through the slot renderer (the keyed card owns
1634
+ // its internals, but the page feeds it locale + the bound api).
1635
+ locale: NS,
1636
+ inject: () => ({ t, api: cardApi })
1637
+ },
1638
+ SettingsCleanupCard
1639
+ ));
1640
+ } catch (error) {
1641
+ console.error("[dsh-rewind] settings card register failed:", error);
1642
+ }
1643
+ });
1125
1644
  const commandUi = ctx.get("commandUi");
1126
1645
  const hasCandidates = (sessionId) => {
1127
1646
  const face = sessionId === void 0 ? void 0 : sessionOf(sessionId);