dsh-rewind-plugin 0.17.1 → 0.18.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
@@ -94,15 +94,6 @@ function isExecutedRewindCommand(node, seq) {
94
94
  const args = node.args ?? "";
95
95
  return new RegExp(`(?:^|\\s)@${seq}(?:\\s|$)`).test(args);
96
96
  }
97
- function hasFileImpact(text) {
98
- if (text === void 0) return true;
99
- const match = text.match(/impact=(\d+)/);
100
- if (match !== null) return Number(match[1]) > 0;
101
- return false;
102
- }
103
- function hasBackupDisabled(text) {
104
- return text !== void 0 && /(?:^|\n)backup=off(?:\n|$)/.test(text);
105
- }
106
97
  function isPreviewCommand(command) {
107
98
  return (command.args ?? "").includes("preview");
108
99
  }
@@ -238,23 +229,6 @@ function rewindCandidatesOf(snap, hidden, limit = DEFAULT_CANDIDATE_LIMIT) {
238
229
  function rewindCandidatesOfChat(snap) {
239
230
  return rewindCandidatesOf(snap, hiddenSeqsOf(snap));
240
231
  }
241
- var CANDIDATE_LIST_HEADER = "candidates=";
242
- function rewindCandidatesFromHostText(text) {
243
- if (!text.startsWith(CANDIDATE_LIST_HEADER)) return [];
244
- const lines = text.split("\n").slice(1);
245
- const candidates = [];
246
- for (const line of lines) {
247
- if (line === "") continue;
248
- const parts = line.split(" ");
249
- if (parts.length !== 3) continue;
250
- const seq = Number(parts[0]);
251
- const time = Number(parts[1]);
252
- const preview = parts[2] ?? "";
253
- if (!Number.isSafeInteger(seq) || !Number.isFinite(time)) continue;
254
- candidates.push({ seq, time, preview });
255
- }
256
- return candidates;
257
- }
258
232
  function rewindOptionsFromCandidates(candidates, t) {
259
233
  return candidates.map((candidate) => ({
260
234
  id: String(candidate.seq),
@@ -263,48 +237,6 @@ function rewindOptionsFromCandidates(candidates, t) {
263
237
  }));
264
238
  }
265
239
 
266
- // src/client/log.ts
267
- var NS = "dsh-rewind";
268
- var DEBUG_KEY = "dsh-rewind.debug";
269
- var ALWAYS_ON = /* @__PURE__ */ new Set(["error", "warn"]);
270
- function switchValue() {
271
- try {
272
- return window.localStorage.getItem(DEBUG_KEY) ?? "";
273
- } catch {
274
- return "";
275
- }
276
- }
277
- function matches(value, ns) {
278
- for (const entry of value.split(",")) {
279
- const part = entry.trim();
280
- if (part === "") continue;
281
- if (part === "*" || part === `${NS}*`) return true;
282
- if (part.endsWith("*")) {
283
- if (ns.startsWith(part.slice(0, -1))) return true;
284
- } else if (ns === part) {
285
- return true;
286
- }
287
- }
288
- return false;
289
- }
290
- function tag(scope) {
291
- return `[${NS}:${scope}]`;
292
- }
293
- function log(level, scope, message, data) {
294
- if (ALWAYS_ON.has(level)) {
295
- console[level](tag(scope), message, data);
296
- return;
297
- }
298
- if (!matches(switchValue(), `${NS}:${scope}`)) return;
299
- console.info(tag(scope), message, data);
300
- }
301
- var rewindLog = {
302
- error: (scope, message, data) => log("error", scope, message, data),
303
- warn: (scope, message, data) => log("warn", scope, message, data),
304
- info: (scope, message, data) => log("info", scope, message, data),
305
- debug: (scope, message, data) => log("debug", scope, message, data)
306
- };
307
-
308
240
  // src/client/styles.ts
309
241
  var CLASS = {
310
242
  button: "dsh-rewind-btn",
@@ -346,44 +278,58 @@ var STYLE = `
346
278
  width: calc(15px + var(--dsh-content-font-delta, 0px));
347
279
  height: calc(15px + var(--dsh-content-font-delta, 0px));
348
280
  }
349
- .dsh-rewind-btn:hover {
350
- background: var(--dsw-alias-interactive-bg-hover);
351
- color: var(--dsw-alias-label-secondary);
281
+ @media (hover: hover) {
282
+ .dsh-rewind-btn:hover {
283
+ background: var(--dsw-alias-interactive-bg-hover);
284
+ color: var(--dsw-alias-label-secondary);
285
+ }
352
286
  }
353
287
 
354
288
  .dsh-rewind-popover {
355
289
  position: fixed;
356
- z-index: 1000;
357
- width: 288px;
358
- padding: 12px;
359
- border: 1px solid var(--dsw-alias-border-l2);
360
- border-radius: 12px;
290
+ z-index: 1100;
291
+ box-sizing: border-box;
292
+ width: min(300px, calc(100vw - 24px));
293
+ padding: 16px;
294
+ border: 0;
295
+ border-radius: var(--dsw-radius-lg);
361
296
  background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));
362
- box-shadow: var(--dsw-shadow-lv3);
363
- font-size: 14px;
364
- line-height: 20px;
297
+ backdrop-filter: var(--dsw-menu-backdrop-filter);
298
+ isolation: isolate;
299
+ --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
300
+ /* Elevated surface: match the menu cards' l2 scrollbar tokens. */
301
+ --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
302
+ --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
303
+ box-shadow: var(--dsw-elevation-prominent);
304
+ font-size: 12px;
305
+ line-height: 18px;
365
306
  color: var(--dsw-alias-label-primary);
366
307
  }
367
308
  .dsh-rewind-popover-title {
368
- font-size: 14px;
369
- font-weight: 600;
370
- line-height: 20px;
309
+ font-size: 12px;
310
+ font-weight: 500;
311
+ line-height: 18px;
371
312
  }
372
313
  .dsh-rewind-popover-target {
373
- margin: 4px 0 10px;
314
+ margin: 2px 0 10px;
374
315
  font-size: 12px;
375
- line-height: 16px;
316
+ line-height: 18px;
376
317
  color: var(--dsw-alias-label-tertiary);
377
318
  word-break: break-all;
319
+ /* The full message sits above the popover; two lines are enough context. */
320
+ display: -webkit-box;
321
+ -webkit-box-orient: vertical;
322
+ -webkit-line-clamp: 2;
323
+ overflow: hidden;
378
324
  }
379
325
  .dsh-rewind-popover-option {
380
326
  display: flex;
381
327
  flex-direction: column;
382
328
  gap: 2px;
383
329
  width: 100%;
384
- margin: 0 0 6px;
385
- padding: 8px 10px;
386
- border: 1px solid transparent;
330
+ margin: 0 0 2px;
331
+ padding: 6px 8px;
332
+ border: 0;
387
333
  border-radius: 8px;
388
334
  background: transparent;
389
335
  color: inherit;
@@ -403,7 +349,7 @@ var STYLE = `
403
349
  outline: none;
404
350
  }
405
351
  .dsh-rewind-popover-option:disabled {
406
- opacity: 0.5;
352
+ opacity: 0.4;
407
353
  cursor: default;
408
354
  }
409
355
  .dsh-rewind-popover-option-label {
@@ -436,15 +382,15 @@ var STYLE = `
436
382
  display: flex;
437
383
  justify-content: flex-end;
438
384
  gap: 8px;
385
+ margin-top: 8px;
439
386
  }
440
387
  .dsh-rewind-popover-primary,
441
388
  .dsh-rewind-popover-ghost {
442
- padding: 5px 12px;
389
+ height: 28px;
390
+ padding: 0 10px;
443
391
  border: none;
444
392
  border-radius: 8px;
445
393
  font: inherit;
446
- font-size: 13px;
447
- line-height: 18px;
448
394
  cursor: pointer;
449
395
  }
450
396
  .dsh-rewind-popover-primary {
@@ -455,7 +401,7 @@ var STYLE = `
455
401
  background: var(--dsw-alias-button-primary-hover);
456
402
  }
457
403
  .dsh-rewind-popover-primary:disabled {
458
- opacity: 0.5;
404
+ opacity: 0.4;
459
405
  cursor: default;
460
406
  }
461
407
  .dsh-rewind-popover-ghost {
@@ -465,16 +411,22 @@ var STYLE = `
465
411
  .dsh-rewind-popover-ghost:hover {
466
412
  background: var(--dsw-alias-interactive-bg-hover);
467
413
  }
414
+ .dsh-rewind-popover-ghost:active {
415
+ background: var(--dsw-alias-interactive-bg-active);
416
+ }
468
417
 
469
418
  .dsh-rewind-guard-hint {
470
419
  position: fixed;
471
420
  z-index: 1000;
472
421
  max-width: min(440px, calc(100vw - 24px));
473
422
  padding: 8px 12px;
474
- border: 1px solid var(--dsw-alias-border-l2);
475
- border-radius: 10px;
423
+ border: 0;
424
+ border-radius: var(--dsw-radius-md);
476
425
  background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));
477
- box-shadow: var(--dsw-shadow-lv3);
426
+ backdrop-filter: var(--dsw-menu-backdrop-filter);
427
+ isolation: isolate;
428
+ --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
429
+ box-shadow: var(--dsw-elevation-panel);
478
430
  font-size: 13px;
479
431
  line-height: 18px;
480
432
  color: var(--dsw-alias-label-primary);
@@ -514,12 +466,21 @@ var STYLE = `
514
466
 
515
467
  // src/client/popover.ts
516
468
  var popoverEl = null;
469
+ var popoverAnchor = null;
517
470
  var disposeOutside = null;
518
471
  var liveSessionId = null;
519
472
  var composerFocuser = null;
520
473
  function registerComposerFocuser(focus) {
521
474
  composerFocuser = focus;
522
475
  }
476
+ function markOpen(anchor) {
477
+ popoverAnchor = anchor;
478
+ anchor.setAttribute("aria-expanded", "true");
479
+ }
480
+ function markClosed() {
481
+ popoverAnchor?.setAttribute("aria-expanded", "false");
482
+ popoverAnchor = null;
483
+ }
523
484
  function closePopover() {
524
485
  if (popoverEl !== null) {
525
486
  popoverEl.remove();
@@ -529,6 +490,7 @@ function closePopover() {
529
490
  disposeOutside();
530
491
  disposeOutside = null;
531
492
  }
493
+ markClosed();
532
494
  const sessionId = liveSessionId;
533
495
  liveSessionId = null;
534
496
  if (sessionId !== null) {
@@ -538,22 +500,6 @@ function closePopover() {
538
500
  }
539
501
  }
540
502
  }
541
- function formatTarget(t, seq, time, preview) {
542
- const d = new Date(time);
543
- const hh = String(d.getHours()).padStart(2, "0");
544
- const mm = String(d.getMinutes()).padStart(2, "0");
545
- const previewText = preview.length > 0 ? preview : t("popover.noText");
546
- return `seq ${seq} \xB7 ${hh}:${mm} \xB7 ${previewText}`;
547
- }
548
- function parseImpactList(text) {
549
- const restores = [];
550
- const deletes = [];
551
- for (const line of text.split("\n")) {
552
- if (line.startsWith("restore:")) restores.push(line.slice("restore:".length));
553
- else if (line.startsWith("delete:")) deletes.push(line.slice("delete:".length));
554
- }
555
- return { restores, deletes };
556
- }
557
503
  function findCommand(chat, match) {
558
504
  if (chat === void 0) return void 0;
559
505
  let found;
@@ -600,28 +546,12 @@ function waitForCommand(session, chatOf, match, timeoutMs = 8e3, watch) {
600
546
  check();
601
547
  });
602
548
  }
603
- function isPreviewFor(node, seq) {
604
- const args = node.args ?? "";
605
- return node.name !== null && ENGINE_COMMANDS.has(node.name) && args.includes("preview") && new RegExp(`(?:^|\\s)@${seq}(?=\\s|$)`).test(args);
549
+ var previewCall = null;
550
+ function registerPreviewCall(call) {
551
+ previewCall = call;
606
552
  }
607
- async function previewImpact(session, chatOf, seq, watch) {
608
- const known = knownCommandSeqs(session, chatOf, (node) => isPreviewFor(node, seq));
609
- let result;
610
- try {
611
- result = await session.command(`/${ENGINE_COMMAND} preview @${seq} both`);
612
- } catch (error) {
613
- rewindLog.warn("preview", `preview command threw for @${seq}`, error);
614
- return { kind: "error", text: error instanceof Error ? error.message : String(error) };
615
- }
616
- if (!result.ok) {
617
- rewindLog.warn("preview", `preview command rejected for @${seq}`, result.error);
618
- return { kind: "error", text: `${result.error.code}: ${result.error.message}` };
619
- }
620
- if (result.value?.matched !== true) {
621
- rewindLog.warn("preview", `preview command was not matched for @${seq}`);
622
- return { kind: "error", text: "the rewind command is not registered on this host" };
623
- }
624
- return waitForCommand(session, chatOf, (node) => isPreviewFor(node, seq) && !known.has(node.seq), 8e3, watch);
553
+ function previewImpact(sessionId, seq) {
554
+ return previewCall?.(sessionId, seq) ?? Promise.resolve({ kind: "error", text: "host RPC is unavailable" });
625
555
  }
626
556
  function el(tag2, className, text) {
627
557
  const node = document.createElement(tag2);
@@ -653,7 +583,7 @@ function moveFocus(root, dir) {
653
583
  const next = index === -1 ? dir === 1 ? 0 : buttons.length - 1 : (index + dir + buttons.length) % buttons.length;
654
584
  buttons[next]?.focus();
655
585
  }
656
- function renderImpactStep(root, opts, back, cached) {
586
+ function renderImpactStep(root, opts, back, reposition, cached) {
657
587
  const { session, seq, t } = opts;
658
588
  const impact = el("div", CLASS.popoverImpact, t("popover.impact.loading"));
659
589
  const actions = el("div", CLASS.popoverActions);
@@ -671,34 +601,32 @@ function renderImpactStep(root, opts, back, cached) {
671
601
  actions.append(confirm);
672
602
  root.replaceChildren(impact, actions);
673
603
  focusFirst(root);
604
+ reposition();
674
605
  void (async () => {
675
- const outcome = cached ?? await previewImpact(session, opts.chatOf, seq, (cb) => opts.watchChat(session.sessionId, cb));
606
+ const outcome = cached ?? await previewImpact(session.sessionId, seq);
676
607
  if (outcome === null) {
677
- impact.textContent = t("popover.impact.failed", { message: "preview command failed or timed out" });
608
+ impact.textContent = t("popover.impact.failed", { message: "preview request timed out" });
678
609
  return;
679
610
  }
680
611
  if (outcome.kind === "error") {
681
- impact.textContent = t("popover.impact.failed", { message: outcome.text ?? "unknown error" });
612
+ impact.textContent = t("popover.impact.failed", { message: outcome.text });
682
613
  return;
683
614
  }
684
- if (outcome.text === void 0) {
615
+ const restores = outcome.files.filter((file) => file.action === "restore").map((file) => file.path);
616
+ const deletes = outcome.files.filter((file) => file.action === "delete").map((file) => file.path);
617
+ if (restores.length === 0 && deletes.length === 0) {
685
618
  impact.textContent = t("popover.impact.none");
686
619
  } else {
687
- const { restores, deletes } = parseImpactList(outcome.text);
688
- if (restores.length === 0 && deletes.length === 0) {
689
- impact.textContent = t("popover.impact.none");
620
+ const lines = [
621
+ ...restores.map((path) => t("popover.impact.restore", { path })),
622
+ ...deletes.map((path) => t("popover.impact.delete", { path }))
623
+ ];
624
+ if (outcome.backupOff) {
625
+ const list = document.createElement("div");
626
+ list.textContent = lines.join("\n");
627
+ impact.replaceChildren(list, el("div", CLASS.popoverNote, t("popover.backupDisabled")));
690
628
  } else {
691
- const lines = [
692
- ...restores.map((path) => t("popover.impact.restore", { path })),
693
- ...deletes.map((path) => t("popover.impact.delete", { path }))
694
- ];
695
- if (hasBackupDisabled(outcome.text)) {
696
- const list = document.createElement("div");
697
- list.textContent = lines.join("\n");
698
- impact.replaceChildren(list, el("div", CLASS.popoverNote, t("popover.backupDisabled")));
699
- } else {
700
- impact.textContent = lines.join("\n");
701
- }
629
+ impact.textContent = lines.join("\n");
702
630
  }
703
631
  }
704
632
  confirm.disabled = false;
@@ -709,16 +637,44 @@ function renderImpactStep(root, opts, back, cached) {
709
637
  });
710
638
  })().catch(() => {
711
639
  impact.textContent = t("popover.impact.failed", { message: "unexpected error" });
640
+ }).finally(() => {
641
+ reposition();
712
642
  });
713
643
  }
714
644
  function mountShell(root, anchor, onKeyDown) {
645
+ const margin = 12;
646
+ const gap = 4;
647
+ let flippedAbove = null;
715
648
  const position = () => {
649
+ if (!root.isConnected) return;
650
+ if (!anchor.isConnected) {
651
+ if (popoverEl === root) closePopover();
652
+ return;
653
+ }
716
654
  const rect = anchor.getBoundingClientRect();
717
- const gap = 4;
718
655
  const height = root.offsetHeight;
719
- const top = rect.bottom + gap + height <= window.innerHeight - 8 ? rect.bottom + gap : Math.max(8, rect.top - gap - height);
656
+ if (flippedAbove === null) {
657
+ flippedAbove = rect.bottom + gap + height > window.innerHeight - margin;
658
+ }
659
+ const top = flippedAbove ? Math.max(margin, rect.top - gap - height) : Math.min(Math.max(margin, rect.bottom + gap), Math.max(margin, window.innerHeight - margin - height));
720
660
  root.style.top = `${Math.round(top)}px`;
721
- root.style.left = `${Math.round(Math.min(rect.right, window.innerWidth - 8 - root.offsetWidth))}px`;
661
+ root.style.left = `${Math.round(Math.max(margin, Math.min(rect.right, window.innerWidth - margin - root.offsetWidth)))}px`;
662
+ };
663
+ let frame = 0;
664
+ const schedule = () => {
665
+ if (typeof requestAnimationFrame !== "function") {
666
+ position();
667
+ return;
668
+ }
669
+ if (frame !== 0) return;
670
+ frame = requestAnimationFrame(() => {
671
+ frame = 0;
672
+ position();
673
+ });
674
+ };
675
+ const onScroll = (event) => {
676
+ if (event.target instanceof Node && root.contains(event.target)) return;
677
+ schedule();
722
678
  };
723
679
  const onPointerDown = (event) => {
724
680
  const target = event.target;
@@ -729,19 +685,30 @@ function mountShell(root, anchor, onKeyDown) {
729
685
  document.addEventListener("pointerdown", onPointerDown);
730
686
  document.addEventListener("keydown", onKeyDown, true);
731
687
  }, 0);
688
+ document.body.append(root);
689
+ position();
690
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => {
691
+ schedule();
692
+ });
693
+ observer?.observe(root);
694
+ window.addEventListener("scroll", onScroll, true);
695
+ window.addEventListener("resize", schedule);
732
696
  const dispose = () => {
733
697
  clearTimeout(deferred);
734
698
  document.removeEventListener("pointerdown", onPointerDown);
735
699
  document.removeEventListener("keydown", onKeyDown, true);
700
+ window.removeEventListener("scroll", onScroll, true);
701
+ window.removeEventListener("resize", schedule);
702
+ observer?.disconnect();
703
+ if (frame !== 0) cancelAnimationFrame(frame);
736
704
  };
737
- document.body.append(root);
738
- position();
739
705
  return { position, dispose };
740
706
  }
741
707
  function openRetractPopover(opts) {
742
708
  closePopover();
743
709
  const { preview, anchor, t, retract, onRetract } = opts;
744
710
  if (retract === void 0 || onRetract === void 0) return;
711
+ markOpen(anchor);
745
712
  const root = el("div", CLASS.popover);
746
713
  root.setAttribute("role", "dialog");
747
714
  root.setAttribute("aria-label", t("popover.retract.title"));
@@ -798,21 +765,25 @@ function openPopover(opts) {
798
765
  openRetractPopover(opts);
799
766
  return;
800
767
  }
801
- const { session, seq, time, preview, anchor, t, chatOf } = opts;
768
+ const { session, seq, time, preview, anchor, t } = opts;
802
769
  const onRewind = opts.onRewind;
803
770
  if (seq === void 0 || time === void 0 || onRewind === void 0) return;
804
- const durableOpts = { session, seq, time, preview, anchor, t, chatOf, watchChat: opts.watchChat, onRewind };
771
+ markOpen(anchor);
772
+ const durableOpts = { session, seq, time, preview, anchor, t, onRewind };
805
773
  const root = el("div", CLASS.popover);
806
774
  root.setAttribute("role", "dialog");
807
775
  root.setAttribute("aria-label", t("popover.title"));
808
776
  let bothState = { state: "loading" };
809
777
  let impactOutcome = null;
810
778
  let step = "modes";
779
+ let reposition = () => {
780
+ };
811
781
  const renderModes = () => {
812
782
  step = "modes";
783
+ const previewText = preview.length > 0 ? preview : t("popover.noText");
813
784
  const children = [
814
785
  el("div", CLASS.popoverTitle, t("popover.title")),
815
- el("div", CLASS.popoverTarget, formatTarget(t, seq, time, preview)),
786
+ el("div", CLASS.popoverTarget, previewText),
816
787
  modeOption(t("popover.chat"), t("popover.chat.hint"), () => {
817
788
  closePopover();
818
789
  durableOpts.onRewind("chat");
@@ -831,20 +802,13 @@ function openPopover(opts) {
831
802
  if (bothState.state === "loading") option.disabled = true;
832
803
  children.push(option);
833
804
  }
834
- const actions = el("div", CLASS.popoverActions);
835
- const cancel = document.createElement("button");
836
- cancel.type = "button";
837
- cancel.className = CLASS.popoverGhost;
838
- cancel.textContent = t("popover.cancel");
839
- cancel.addEventListener("click", closePopover);
840
- actions.append(cancel);
841
- children.push(actions);
842
805
  root.replaceChildren(...children);
843
806
  focusFirst(root);
807
+ reposition();
844
808
  };
845
809
  const renderImpact = () => {
846
810
  step = "impact";
847
- renderImpactStep(root, durableOpts, renderModes, impactOutcome);
811
+ renderImpactStep(root, durableOpts, renderModes, reposition, impactOutcome);
848
812
  };
849
813
  const onKeyDown = (event) => {
850
814
  if (event.key === "ArrowDown") {
@@ -868,28 +832,28 @@ function openPopover(opts) {
868
832
  };
869
833
  renderModes();
870
834
  const shell = mountShell(root, anchor, onKeyDown);
835
+ reposition = shell.position;
871
836
  popoverEl = root;
872
837
  disposeOutside = shell.dispose;
873
838
  void (async () => {
874
- const outcome = await previewImpact(session, chatOf, seq, (cb) => opts.watchChat(session.sessionId, cb));
839
+ const outcome = await previewImpact(session.sessionId, seq);
875
840
  impactOutcome = outcome;
876
841
  if (outcome !== null && outcome.kind === "success") {
877
- bothState = { state: hasFileImpact(outcome.text) ? "hasChanges" : "noChanges" };
842
+ bothState = { state: outcome.files.length > 0 ? "hasChanges" : "noChanges" };
878
843
  } else {
879
- bothState = { state: "error", message: outcome?.text ?? "preview command timed out" };
844
+ bothState = { state: "error", message: outcome?.kind === "error" ? outcome.text : "preview request timed out" };
880
845
  }
881
846
  renderModes();
882
- shell.position();
883
847
  })().catch(() => {
884
848
  bothState = { state: "error", message: "unexpected error" };
885
849
  renderModes();
886
- shell.position();
887
850
  });
888
851
  }
889
852
 
890
853
  // src/client/portals.tsx
891
854
  var import_react = require("react");
892
855
  var import_react_dom = require("react-dom");
856
+ var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
893
857
 
894
858
  // src/client/pending.ts
895
859
  var QUEUE_PREVIEW_CHARS = 200;
@@ -921,11 +885,11 @@ function matchPendingRows(rows, steering) {
921
885
  for (let i = 0; i < rows.length; i++) {
922
886
  const row = rows[i];
923
887
  const item = steering[i];
924
- matched.push(item !== void 0 && matches2(item, row) ? item.id : null);
888
+ matched.push(item !== void 0 && matches(item, row) ? item.id : null);
925
889
  }
926
890
  return matched;
927
891
  }
928
- function matches2(item, row) {
892
+ function matches(item, row) {
929
893
  if (item.attachments > 0) return row.attachments === item.attachments;
930
894
  return row.text === (item.text ?? "");
931
895
  }
@@ -935,6 +899,48 @@ function retractSpan(steering, targetId) {
935
899
  return steering.slice(index).map((item) => item.id);
936
900
  }
937
901
 
902
+ // src/client/log.ts
903
+ var NS = "dsh-rewind";
904
+ var DEBUG_KEY = "dsh-rewind.debug";
905
+ var ALWAYS_ON = /* @__PURE__ */ new Set(["error", "warn"]);
906
+ function switchValue() {
907
+ try {
908
+ return window.localStorage.getItem(DEBUG_KEY) ?? "";
909
+ } catch {
910
+ return "";
911
+ }
912
+ }
913
+ function matches2(value, ns) {
914
+ for (const entry of value.split(",")) {
915
+ const part = entry.trim();
916
+ if (part === "") continue;
917
+ if (part === "*" || part === `${NS}*`) return true;
918
+ if (part.endsWith("*")) {
919
+ if (ns.startsWith(part.slice(0, -1))) return true;
920
+ } else if (ns === part) {
921
+ return true;
922
+ }
923
+ }
924
+ return false;
925
+ }
926
+ function tag(scope) {
927
+ return `[${NS}:${scope}]`;
928
+ }
929
+ function log(level, scope, message, data) {
930
+ if (ALWAYS_ON.has(level)) {
931
+ console[level](tag(scope), message, data);
932
+ return;
933
+ }
934
+ if (!matches2(switchValue(), `${NS}:${scope}`)) return;
935
+ console.info(tag(scope), message, data);
936
+ }
937
+ var rewindLog = {
938
+ error: (scope, message, data) => log("error", scope, message, data),
939
+ warn: (scope, message, data) => log("warn", scope, message, data),
940
+ info: (scope, message, data) => log("info", scope, message, data),
941
+ debug: (scope, message, data) => log("debug", scope, message, data)
942
+ };
943
+
938
944
  // src/client/portals.tsx
939
945
  var import_jsx_runtime = require("react/jsx-runtime");
940
946
  function fillComposerEditable(text) {
@@ -1294,8 +1300,6 @@ function RewindPortals({ sessionId, sessionOf, chatOf, isMainViewSession, watchC
1294
1300
  target,
1295
1301
  sessionId,
1296
1302
  sessionOf,
1297
- chatOf,
1298
- watchChat,
1299
1303
  setComposerText,
1300
1304
  t
1301
1305
  },
@@ -1331,8 +1335,6 @@ function RewindButton({ target, sessionId, sessionOf, chatOf, watchChat, isMainV
1331
1335
  if (node === void 0) return;
1332
1336
  openPopover({
1333
1337
  session,
1334
- chatOf,
1335
- watchChat,
1336
1338
  seq: node.seq,
1337
1339
  time: node.time,
1338
1340
  preview: messagePreviewOf(node),
@@ -1343,17 +1345,17 @@ function RewindButton({ target, sessionId, sessionOf, chatOf, watchChat, isMainV
1343
1345
  }
1344
1346
  });
1345
1347
  };
1346
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1348
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Tooltip, { label: t("button.title"), side: "bottom", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1347
1349
  "button",
1348
1350
  {
1349
1351
  type: "button",
1350
1352
  className: CLASS.button,
1351
1353
  "aria-label": t("button.aria"),
1352
- title: t("button.title"),
1354
+ "aria-haspopup": "dialog",
1353
1355
  onClick,
1354
1356
  dangerouslySetInnerHTML: { __html: REWIND_ICON_SVG }
1355
1357
  }
1356
- );
1358
+ ) });
1357
1359
  }
1358
1360
  function composerText() {
1359
1361
  const surface = composerSurface();
@@ -1379,15 +1381,13 @@ async function retractPending(session, itemId, text, setComposerText) {
1379
1381
  setComposerText(session.sessionId, text);
1380
1382
  }
1381
1383
  }
1382
- function RetractButton({ target, sessionId, sessionOf, chatOf, watchChat, setComposerText, t }) {
1384
+ function RetractButton({ target, sessionId, sessionOf, setComposerText, t }) {
1383
1385
  const onClick = (event) => {
1384
1386
  event.stopPropagation();
1385
1387
  const session = sessionOf(sessionId);
1386
1388
  if (session === void 0) return;
1387
1389
  openPopover({
1388
1390
  session,
1389
- chatOf,
1390
- watchChat,
1391
1391
  preview: target.preview,
1392
1392
  anchor: event.currentTarget,
1393
1393
  t,
@@ -1397,17 +1397,17 @@ function RetractButton({ target, sessionId, sessionOf, chatOf, watchChat, setCom
1397
1397
  }
1398
1398
  });
1399
1399
  };
1400
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1400
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Tooltip, { label: t("button.retract.title"), side: "bottom", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1401
1401
  "button",
1402
1402
  {
1403
1403
  type: "button",
1404
1404
  className: CLASS.button,
1405
1405
  "aria-label": t("button.retract.aria"),
1406
- title: t("button.retract.title"),
1406
+ "aria-haspopup": "dialog",
1407
1407
  onClick,
1408
1408
  dangerouslySetInnerHTML: { __html: REWIND_ICON_SVG }
1409
1409
  }
1410
- );
1410
+ ) });
1411
1411
  }
1412
1412
  function createRewindBridge(deps) {
1413
1413
  return function RewindBridge({ sessionId }) {
@@ -1415,10 +1415,69 @@ function createRewindBridge(deps) {
1415
1415
  };
1416
1416
  }
1417
1417
 
1418
+ // src/client/host-rpc.ts
1419
+ var CHANNEL = "/dsh-rewind";
1420
+ var TIMEOUT_MS = 8e3;
1421
+ function isRecord(value) {
1422
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1423
+ }
1424
+ function isImpact(value) {
1425
+ return isRecord(value) && typeof value["path"] === "string" && (value["action"] === "restore" || value["action"] === "delete");
1426
+ }
1427
+ function isCandidate(value) {
1428
+ return isRecord(value) && Number.isSafeInteger(value["seq"]) && typeof value["time"] === "number" && Number.isFinite(value["time"]) && typeof value["preview"] === "string";
1429
+ }
1430
+ function isAbort(error) {
1431
+ const name2 = error?.name;
1432
+ return name2 === "AbortError" || name2 === "TimeoutError";
1433
+ }
1434
+ async function previewOverRpc(connection, sessionId, seq) {
1435
+ if (connection === void 0) return { kind: "error", text: "host RPC is unavailable" };
1436
+ try {
1437
+ const result = await connection.rpc.call(CHANNEL, "preview", { sessionId, seq }, AbortSignal.timeout(TIMEOUT_MS));
1438
+ if (!result.ok) {
1439
+ rewindLog.warn("preview", `RPC preview failed for @${seq}`, result.error);
1440
+ return { kind: "error", text: result.error.message };
1441
+ }
1442
+ const value = result.value;
1443
+ if (!isRecord(value) || typeof value["backupOff"] !== "boolean" || !Array.isArray(value["files"]) || !value["files"].every(isImpact)) {
1444
+ rewindLog.warn("preview", `RPC preview returned an invalid result for @${seq}`);
1445
+ return { kind: "error", text: "invalid preview response" };
1446
+ }
1447
+ return { kind: "success", files: value["files"], backupOff: value["backupOff"] };
1448
+ } catch (error) {
1449
+ if (isAbort(error)) {
1450
+ rewindLog.warn("preview", `RPC preview timed out for @${seq}`);
1451
+ return null;
1452
+ }
1453
+ rewindLog.warn("preview", `RPC preview threw for @${seq}`, error);
1454
+ return { kind: "error", text: error instanceof Error ? error.message : String(error) };
1455
+ }
1456
+ }
1457
+ async function candidatesOverRpc(connection, sessionId) {
1458
+ if (connection === void 0) return void 0;
1459
+ try {
1460
+ const result = await connection.rpc.call(CHANNEL, "candidates", { sessionId }, AbortSignal.timeout(TIMEOUT_MS));
1461
+ if (!result.ok) {
1462
+ rewindLog.warn("preview", "RPC candidates failed", result.error);
1463
+ return void 0;
1464
+ }
1465
+ const value = result.value;
1466
+ if (!isRecord(value) || !Array.isArray(value["candidates"]) || !value["candidates"].every(isCandidate)) {
1467
+ rewindLog.warn("preview", "RPC candidates returned an invalid result");
1468
+ return void 0;
1469
+ }
1470
+ return value["candidates"];
1471
+ } catch (error) {
1472
+ rewindLog.warn("preview", "RPC candidates threw", error);
1473
+ return void 0;
1474
+ }
1475
+ }
1476
+
1418
1477
  // src/client/build-info.ts
1419
- var PLUGIN_VERSION = true ? "0.17.1" : "dev";
1478
+ var PLUGIN_VERSION = true ? "0.18.1" : "dev";
1420
1479
  var PLUGIN_PACKAGE = "dsh-rewind-plugin";
1421
- var BUILD_HASH = true ? "0d33ec37" : "dev";
1480
+ var BUILD_HASH = true ? "8a3779b4" : "dev";
1422
1481
 
1423
1482
  // src/client/locales.ts
1424
1483
  var zh = {
@@ -1433,11 +1492,11 @@ var zh = {
1433
1492
  "popover.retract.hint": "\u64A4\u56DE\u8FD9\u6761\u53CA\u4E4B\u540E\u7684\u63D2\u8BDD\u6D88\u606F\uFF0C\u4E0D\u6253\u65AD\u5F53\u524D\u751F\u6210",
1434
1493
  "popover.retract.confirm": "\u786E\u8BA4\u56DE\u9000",
1435
1494
  "popover.chat": "\u4EC5\u56DE\u9000\u5BF9\u8BDD",
1436
- "popover.chat.hint": "\u53EA\u56DE\u9000\u6A21\u578B\u4E0A\u4E0B\u6587\uFF0C\u4E0D\u52A8\u5DE5\u4F5C\u533A\u6587\u4EF6",
1437
- "popover.both": "\u56DE\u9000\u5BF9\u8BDD\u548C\u4EE3\u7801",
1438
- "popover.both.hint": "\u5BF9\u8BDD\u56DE\u9000\u5E76\u8FD8\u539F\u5DE5\u4F5C\u533A\u6587\u4EF6",
1495
+ "popover.chat.hint": "\u4EC5\u56DE\u9000\u4E0A\u4E0B\u6587\uFF0C\u4E0D\u52A8\u5DE5\u4F5C\u533A\u6587\u4EF6",
1496
+ "popover.both": "\u56DE\u9000\u5BF9\u8BDD\u548C\u6587\u4EF6",
1497
+ "popover.both.hint": "\u56DE\u9000\u4E0A\u4E0B\u6587\u5E76\u8FD8\u539F\u5DE5\u4F5C\u533A\u6587\u4EF6",
1439
1498
  "popover.checking": "\u6B63\u5728\u68C0\u67E5\u6587\u4EF6\u53D8\u66F4\u2026",
1440
- "popover.noChanges": "\u6B64\u6D88\u606F\u4E4B\u540E\u6CA1\u6709\u53EF\u8FD8\u539F\u7684\u6587\u4EF6\u53D8\u66F4\uFF0C\u4EC5\u53EF\u56DE\u9000\u5BF9\u8BDD",
1499
+ "popover.noChanges": "\u65E0\u53EF\u8FD8\u539F\u7684\u6587\u4EF6\u53D8\u66F4",
1441
1500
  "popover.backupDisabled": "\u5FEB\u7167\u5907\u4EFD\u5DF2\u5173\u95ED\uFF0C\u8FD8\u539F\u53EF\u80FD\u4E0D\u5145\u5206",
1442
1501
  "popover.cancel": "\u53D6\u6D88",
1443
1502
  "popover.impact.loading": "\u6B63\u5728\u83B7\u53D6\u5F71\u54CD\u6E05\u5355\u2026",
@@ -1476,11 +1535,11 @@ var en = {
1476
1535
  "popover.retract.hint": "Retracts this pending message and the ones after it, without interrupting the run",
1477
1536
  "popover.retract.confirm": "Confirm rewind",
1478
1537
  "popover.chat": "Rewind conversation only",
1479
- "popover.chat.hint": "Cut the model context only; workspace files stay untouched",
1480
- "popover.both": "Rewind conversation and code",
1481
- "popover.both.hint": "Cut the context and restore workspace files",
1538
+ "popover.chat.hint": "Context only; workspace untouched",
1539
+ "popover.both": "Rewind conversation and files",
1540
+ "popover.both.hint": "Context and restore workspace files",
1482
1541
  "popover.checking": "Checking for file changes\u2026",
1483
- "popover.noChanges": "No tracked file changes after this message; conversation-only rewind",
1542
+ "popover.noChanges": "No restorable file changes",
1484
1543
  "popover.backupDisabled": "Snapshots off; the restore may be incomplete",
1485
1544
  "popover.cancel": "Cancel",
1486
1545
  "popover.impact.loading": "Fetching impact list\u2026",
@@ -1509,7 +1568,7 @@ var en = {
1509
1568
  };
1510
1569
 
1511
1570
  // src/client/settings-card.tsx
1512
- var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
1571
+ var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives");
1513
1572
  var import_jsx_runtime2 = require("react/jsx-runtime");
1514
1573
  var SETTINGS_ENTRY_ID = "dsh-rewind-plugin";
1515
1574
  var DEFAULT_MAX_AGE_DAYS = 30;
@@ -1537,10 +1596,10 @@ function formLabels(t) {
1537
1596
  };
1538
1597
  }
1539
1598
  function settingsForm(scope) {
1540
- const form = new import_dsh_client_ui_primitives.SettingsFormModel(scope, [
1599
+ const form = new import_dsh_client_ui_primitives2.SettingsFormModel(scope, [
1541
1600
  fileCheckpointingEnabledField(),
1542
1601
  autoCleanupEnabledField(),
1543
- (0, import_dsh_client_ui_primitives.settingsNumberField)("autoCleanupMaxAgeDays")
1602
+ (0, import_dsh_client_ui_primitives2.settingsNumberField)("autoCleanupMaxAgeDays")
1544
1603
  ]);
1545
1604
  return {
1546
1605
  form,
@@ -1560,7 +1619,7 @@ function SettingsCard(props) {
1560
1619
  const checkpointing = state.checkpointingEnabled.text === "true";
1561
1620
  const cleanupEnabled = state.enabled.text === "true";
1562
1621
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1563
- import_dsh_client_ui_primitives.SettingsForm,
1622
+ import_dsh_client_ui_primitives2.SettingsForm,
1564
1623
  {
1565
1624
  labels: formLabels(t),
1566
1625
  state,
@@ -1571,7 +1630,7 @@ function SettingsCard(props) {
1571
1630
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-rewind-settings-toggle-row", children: [
1572
1631
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-rewind-settings-toggle-label", children: t("backup.title") }),
1573
1632
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1574
- import_dsh_client_ui_primitives.Switch,
1633
+ import_dsh_client_ui_primitives2.Switch,
1575
1634
  {
1576
1635
  checked: checkpointing,
1577
1636
  label: t("backup.title"),
@@ -1589,7 +1648,7 @@ function SettingsCard(props) {
1589
1648
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-rewind-settings-toggle-row", children: [
1590
1649
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-rewind-settings-toggle-label", children: t("cleanup.auto") }),
1591
1650
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1592
- import_dsh_client_ui_primitives.Switch,
1651
+ import_dsh_client_ui_primitives2.Switch,
1593
1652
  {
1594
1653
  checked: cleanupEnabled,
1595
1654
  label: t("cleanup.auto"),
@@ -1603,7 +1662,7 @@ function SettingsCard(props) {
1603
1662
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "dsh-rewind-settings-hint", children: t(cleanupEnabled ? "cleanup.auto.on" : "cleanup.auto.off") })
1604
1663
  ] }),
1605
1664
  cleanupEnabled ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1606
- import_dsh_client_ui_primitives.SettingsValueField,
1665
+ import_dsh_client_ui_primitives2.SettingsValueField,
1607
1666
  {
1608
1667
  id: "dsh-rewind-cleanup-maxage",
1609
1668
  label: t("cleanup.maxAge"),
@@ -1630,7 +1689,7 @@ function SettingsCard(props) {
1630
1689
 
1631
1690
  // src/client/index.ts
1632
1691
  var name = "dsh-rewind";
1633
- var inject = ["slots", "sessions", "locale", "commandUi", "configForms"];
1692
+ var inject = ["slots", "sessions", "locale", "commandUi", "configForms", "connection"];
1634
1693
  var NS2 = "rewind";
1635
1694
  var HEADER_ACTIONS_SLOT = "conversation.session.header.actions";
1636
1695
  var COMPOSER_EDITABLE_SELECTOR2 = "[data-composer-input]";
@@ -1639,6 +1698,11 @@ function apply(ctx) {
1639
1698
  ctx.effect(function* () {
1640
1699
  yield ctx.locale.register(NS2, { zh, en });
1641
1700
  const t = ctx.locale.bind(NS2);
1701
+ const connection = ctx.get("connection");
1702
+ registerPreviewCall((sessionId, seq) => previewOverRpc(connection, sessionId, seq));
1703
+ yield () => {
1704
+ registerPreviewCall(null);
1705
+ };
1642
1706
  const style = document.createElement("style");
1643
1707
  style.dataset.plugin = PLUGIN_PACKAGE;
1644
1708
  style.textContent = STYLE;
@@ -1777,14 +1841,7 @@ function apply(ctx) {
1777
1841
  const chat = chatOf(face);
1778
1842
  return chat !== void 0 && rewindCandidatesOfChat(chat).length > 0;
1779
1843
  };
1780
- const fetchHostCandidates = async (face, chatOf2) => {
1781
- const known = knownCommandSeqs(face, chatOf2, (node) => isCandidateCommand(node));
1782
- const result = await face.command(`/${ENGINE_COMMAND} __candidates`);
1783
- if (!result.ok || result.value?.matched !== true) return void 0;
1784
- const outcome = await waitForCommand(face, chatOf2, (node) => isCandidateCommand(node) && !known.has(node.seq), 8e3, (cb) => watchChat(face.sessionId, cb));
1785
- if (outcome === null || outcome.kind !== "success" || outcome.text === void 0) return void 0;
1786
- return rewindCandidatesFromHostText(outcome.text);
1787
- };
1844
+ const fetchHostCandidates = async (face) => candidatesOverRpc(connection, face.sessionId);
1788
1845
  const hostCandidatesCache = /* @__PURE__ */ new Map();
1789
1846
  const composerAnchor = () => {
1790
1847
  const surface = composerSurface2();
@@ -1802,7 +1859,7 @@ function apply(ctx) {
1802
1859
  options: async (session) => {
1803
1860
  const face = sessionOf(session.sessionId);
1804
1861
  if (face === void 0) return [];
1805
- const candidates = await fetchHostCandidates(face, chatOf);
1862
+ const candidates = await fetchHostCandidates(face);
1806
1863
  if (candidates !== void 0) hostCandidatesCache.set(session.sessionId, candidates);
1807
1864
  return candidates === void 0 ? [] : rewindOptionsFromCandidates(candidates, t);
1808
1865
  },
@@ -1815,8 +1872,6 @@ function apply(ctx) {
1815
1872
  if (candidate === void 0) return;
1816
1873
  openPopover({
1817
1874
  session: face,
1818
- chatOf,
1819
- watchChat,
1820
1875
  seq: candidate.seq,
1821
1876
  time: candidate.time,
1822
1877
  preview: candidate.preview,