dsh-rewind-plugin 0.3.2 → 0.3.3
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 +236 -47
- package/lib/index.js +7 -1
- package/lib/types/client/locales.d.ts +12 -0
- package/lib/types/client/pending.d.ts +54 -0
- package/lib/types/client/popover.d.ts +12 -3
- package/lib/types/client/portals.d.ts +22 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -467,9 +467,96 @@ function renderImpactStep(root, opts, back, cached) {
|
|
|
467
467
|
impact.textContent = t("popover.impact.failed", { message: "unexpected error" });
|
|
468
468
|
});
|
|
469
469
|
}
|
|
470
|
+
function mountShell(root, anchor, onKeyDown) {
|
|
471
|
+
const position = () => {
|
|
472
|
+
const rect = anchor.getBoundingClientRect();
|
|
473
|
+
const gap = 4;
|
|
474
|
+
const height = root.offsetHeight;
|
|
475
|
+
const top = rect.bottom + gap + height <= window.innerHeight - 8 ? rect.bottom + gap : Math.max(8, rect.top - gap - height);
|
|
476
|
+
root.style.top = `${Math.round(top)}px`;
|
|
477
|
+
root.style.left = `${Math.round(Math.min(rect.right, window.innerWidth - 8 - root.offsetWidth))}px`;
|
|
478
|
+
};
|
|
479
|
+
const onPointerDown = (event) => {
|
|
480
|
+
const target = event.target;
|
|
481
|
+
if (root.contains(target) || anchor.contains(target)) return;
|
|
482
|
+
closePopover();
|
|
483
|
+
};
|
|
484
|
+
const deferred = setTimeout(() => {
|
|
485
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
486
|
+
document.addEventListener("keydown", onKeyDown, true);
|
|
487
|
+
}, 0);
|
|
488
|
+
const dispose = () => {
|
|
489
|
+
clearTimeout(deferred);
|
|
490
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
491
|
+
document.removeEventListener("keydown", onKeyDown, true);
|
|
492
|
+
};
|
|
493
|
+
document.body.append(root);
|
|
494
|
+
position();
|
|
495
|
+
return { position, dispose };
|
|
496
|
+
}
|
|
497
|
+
function openRetractPopover(opts) {
|
|
498
|
+
closePopover();
|
|
499
|
+
const { preview, anchor, t, retract, onRetract } = opts;
|
|
500
|
+
if (retract === void 0 || onRetract === void 0) return;
|
|
501
|
+
const root = el("div", CLASS.popover);
|
|
502
|
+
root.setAttribute("role", "dialog");
|
|
503
|
+
root.setAttribute("aria-label", t("popover.retract.title"));
|
|
504
|
+
const onKeyDown = (event) => {
|
|
505
|
+
if (event.key === "ArrowDown") {
|
|
506
|
+
event.preventDefault();
|
|
507
|
+
event.stopPropagation();
|
|
508
|
+
moveFocus(root, 1);
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
if (event.key === "ArrowUp") {
|
|
512
|
+
event.preventDefault();
|
|
513
|
+
event.stopPropagation();
|
|
514
|
+
moveFocus(root, -1);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (event.key === "Escape") {
|
|
518
|
+
event.preventDefault();
|
|
519
|
+
event.stopPropagation();
|
|
520
|
+
closePopover();
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
const previewText = preview.length > 0 ? preview : t("popover.noText");
|
|
524
|
+
const actions = el("div", CLASS.popoverActions);
|
|
525
|
+
const confirm = document.createElement("button");
|
|
526
|
+
confirm.type = "button";
|
|
527
|
+
confirm.className = CLASS.popoverPrimary;
|
|
528
|
+
confirm.textContent = t("popover.retract.confirm");
|
|
529
|
+
confirm.addEventListener("click", () => {
|
|
530
|
+
closePopover();
|
|
531
|
+
onRetract();
|
|
532
|
+
});
|
|
533
|
+
const cancel = document.createElement("button");
|
|
534
|
+
cancel.type = "button";
|
|
535
|
+
cancel.className = CLASS.popoverGhost;
|
|
536
|
+
cancel.textContent = t("popover.cancel");
|
|
537
|
+
cancel.addEventListener("click", closePopover);
|
|
538
|
+
actions.append(confirm, cancel);
|
|
539
|
+
root.replaceChildren(
|
|
540
|
+
el("div", CLASS.popoverTitle, t("popover.retract.title")),
|
|
541
|
+
el("div", CLASS.popoverTarget, t("popover.retract.target", { preview: previewText })),
|
|
542
|
+
el("div", CLASS.popoverImpact, t("popover.retract.hint")),
|
|
543
|
+
actions
|
|
544
|
+
);
|
|
545
|
+
const shell = mountShell(root, anchor, onKeyDown);
|
|
546
|
+
popoverEl = root;
|
|
547
|
+
disposeOutside = shell.dispose;
|
|
548
|
+
focusFirst(root);
|
|
549
|
+
}
|
|
470
550
|
function openPopover(opts) {
|
|
471
551
|
closePopover();
|
|
552
|
+
if (opts.retract !== void 0) {
|
|
553
|
+
openRetractPopover(opts);
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
472
556
|
const { session, seq, time, preview, anchor, t } = opts;
|
|
557
|
+
const onRewind = opts.onRewind;
|
|
558
|
+
if (seq === void 0 || time === void 0 || onRewind === void 0) return;
|
|
559
|
+
const durableOpts = { session, seq, time, preview, anchor, t, onRewind };
|
|
473
560
|
const root = el("div", CLASS.popover);
|
|
474
561
|
root.setAttribute("role", "dialog");
|
|
475
562
|
root.setAttribute("aria-label", t("popover.title"));
|
|
@@ -483,7 +570,7 @@ function openPopover(opts) {
|
|
|
483
570
|
el("div", CLASS.popoverTarget, formatTarget(t, seq, time, preview)),
|
|
484
571
|
modeOption(t("popover.chat"), t("popover.chat.hint"), () => {
|
|
485
572
|
closePopover();
|
|
486
|
-
|
|
573
|
+
durableOpts.onRewind("chat");
|
|
487
574
|
})
|
|
488
575
|
];
|
|
489
576
|
if (bothState.state === "noChanges") {
|
|
@@ -510,38 +597,7 @@ function openPopover(opts) {
|
|
|
510
597
|
};
|
|
511
598
|
const renderImpact = () => {
|
|
512
599
|
step = "impact";
|
|
513
|
-
renderImpactStep(root,
|
|
514
|
-
};
|
|
515
|
-
const position = () => {
|
|
516
|
-
const rect = anchor.getBoundingClientRect();
|
|
517
|
-
const gap = 4;
|
|
518
|
-
const height = root.offsetHeight;
|
|
519
|
-
const top = rect.bottom + gap + height <= window.innerHeight - 8 ? rect.bottom + gap : Math.max(8, rect.top - gap - height);
|
|
520
|
-
root.style.top = `${Math.round(top)}px`;
|
|
521
|
-
root.style.left = `${Math.round(Math.min(rect.right, window.innerWidth - 8 - root.offsetWidth))}px`;
|
|
522
|
-
};
|
|
523
|
-
renderModes();
|
|
524
|
-
document.body.append(root);
|
|
525
|
-
position();
|
|
526
|
-
focusFirst(root);
|
|
527
|
-
void (async () => {
|
|
528
|
-
const outcome = await previewImpact(session, seq);
|
|
529
|
-
impactOutcome = outcome;
|
|
530
|
-
if (outcome !== null && outcome.kind === "success") {
|
|
531
|
-
bothState = { state: hasFileImpact(outcome.text) ? "hasChanges" : "noChanges" };
|
|
532
|
-
}
|
|
533
|
-
renderModes();
|
|
534
|
-
position();
|
|
535
|
-
})().catch(() => {
|
|
536
|
-
bothState = { state: "hasChanges" };
|
|
537
|
-
renderModes();
|
|
538
|
-
position();
|
|
539
|
-
});
|
|
540
|
-
popoverEl = root;
|
|
541
|
-
const onPointerDown = (event) => {
|
|
542
|
-
const target = event.target;
|
|
543
|
-
if (root.contains(target) || anchor.contains(target)) return;
|
|
544
|
-
closePopover();
|
|
600
|
+
renderImpactStep(root, durableOpts, renderModes, impactOutcome);
|
|
545
601
|
};
|
|
546
602
|
const onKeyDown = (event) => {
|
|
547
603
|
if (event.key === "ArrowDown") {
|
|
@@ -563,20 +619,50 @@ function openPopover(opts) {
|
|
|
563
619
|
else closePopover();
|
|
564
620
|
}
|
|
565
621
|
};
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
622
|
+
renderModes();
|
|
623
|
+
const shell = mountShell(root, anchor, onKeyDown);
|
|
624
|
+
popoverEl = root;
|
|
625
|
+
disposeOutside = shell.dispose;
|
|
626
|
+
void (async () => {
|
|
627
|
+
const outcome = await previewImpact(session, seq);
|
|
628
|
+
impactOutcome = outcome;
|
|
629
|
+
if (outcome !== null && outcome.kind === "success") {
|
|
630
|
+
bothState = { state: hasFileImpact(outcome.text) ? "hasChanges" : "noChanges" };
|
|
631
|
+
}
|
|
632
|
+
renderModes();
|
|
633
|
+
shell.position();
|
|
634
|
+
})().catch(() => {
|
|
635
|
+
bothState = { state: "hasChanges" };
|
|
636
|
+
renderModes();
|
|
637
|
+
shell.position();
|
|
638
|
+
});
|
|
575
639
|
}
|
|
576
640
|
|
|
577
641
|
// src/client/portals.tsx
|
|
578
642
|
var import_react = require("react");
|
|
579
643
|
var import_react_dom = require("react-dom");
|
|
644
|
+
|
|
645
|
+
// src/client/pending.ts
|
|
646
|
+
function matchPendingRows(rows, steering) {
|
|
647
|
+
const matched = [];
|
|
648
|
+
for (let i = 0; i < rows.length; i++) {
|
|
649
|
+
const row = rows[i];
|
|
650
|
+
const item = steering[i];
|
|
651
|
+
if (item !== void 0 && row.text === (item.text ?? "")) {
|
|
652
|
+
matched.push(item.id);
|
|
653
|
+
} else {
|
|
654
|
+
matched.push(null);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return matched;
|
|
658
|
+
}
|
|
659
|
+
function retractSpan(steering, targetId) {
|
|
660
|
+
const index = steering.findIndex((item) => item.id === targetId);
|
|
661
|
+
if (index === -1) return [];
|
|
662
|
+
return steering.slice(index).map((item) => item.id);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// src/client/portals.tsx
|
|
580
666
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
581
667
|
function userTextAt(session, seq) {
|
|
582
668
|
const snap = session.getSnapshot();
|
|
@@ -619,6 +705,7 @@ var COMPOSER_SELECTOR = "[data-input-scroll] textarea, textarea[data-phase]";
|
|
|
619
705
|
var USER_SEAT_SELECTOR = '[data-chat-flow-kind="user"][data-chat-anchor-key], [data-chat-flow-kind="steering"][data-chat-anchor-key]';
|
|
620
706
|
var CHAT_SEAT_SELECTOR = "[data-chat-anchor-key]";
|
|
621
707
|
var ACTIONS_ROOT_SELECTOR = "[data-time-hover-root]";
|
|
708
|
+
var PENDING_SEAT_SELECTOR = "[data-pending-steering][data-time-hover-root]";
|
|
622
709
|
function collectTargets(chat, hiddenSeqs) {
|
|
623
710
|
const rows = /* @__PURE__ */ new Map();
|
|
624
711
|
for (const element of document.querySelectorAll(USER_SEAT_SELECTOR)) {
|
|
@@ -635,14 +722,51 @@ function collectTargets(chat, hiddenSeqs) {
|
|
|
635
722
|
const messageRoot = row?.querySelector(ACTIONS_ROOT_SELECTOR);
|
|
636
723
|
const actions = messageRoot?.lastElementChild;
|
|
637
724
|
if (!(actions instanceof HTMLElement) || actions.querySelector("button") === null) continue;
|
|
638
|
-
targets.push({ key, container: actions, seq: user.seq, time: user.time, preview: messagePreviewOf(user) });
|
|
725
|
+
targets.push({ kind: "durable", key, container: actions, seq: user.seq, time: user.time, preview: messagePreviewOf(user) });
|
|
726
|
+
}
|
|
727
|
+
return targets;
|
|
728
|
+
}
|
|
729
|
+
function bubbleTextOf(row) {
|
|
730
|
+
const clone = row.cloneNode(true);
|
|
731
|
+
clone.lastElementChild?.remove();
|
|
732
|
+
return clone.textContent ?? "";
|
|
733
|
+
}
|
|
734
|
+
function collectPendingTargets(snapshot) {
|
|
735
|
+
if (snapshot.subagent !== null) return [];
|
|
736
|
+
const steering = snapshot.queue.filter((item) => item.placement === "steering");
|
|
737
|
+
if (steering.length === 0) return [];
|
|
738
|
+
const rows = Array.from(document.querySelectorAll(PENDING_SEAT_SELECTOR));
|
|
739
|
+
const matched = matchPendingRows(
|
|
740
|
+
rows.map((row) => ({ text: bubbleTextOf(row) })),
|
|
741
|
+
steering.map((item) => ({ id: item.id, text: item.text }))
|
|
742
|
+
);
|
|
743
|
+
const targets = [];
|
|
744
|
+
for (let i = 0; i < matched.length; i++) {
|
|
745
|
+
const itemId = matched[i];
|
|
746
|
+
if (itemId === null) continue;
|
|
747
|
+
const row = rows[i];
|
|
748
|
+
if (row === void 0) continue;
|
|
749
|
+
const messageRoot = row.matches(ACTIONS_ROOT_SELECTOR) ? row : row.querySelector(ACTIONS_ROOT_SELECTOR);
|
|
750
|
+
const actions = messageRoot?.lastElementChild;
|
|
751
|
+
if (!(actions instanceof HTMLElement) || actions.querySelector("button") === null) continue;
|
|
752
|
+
const item = steering[i];
|
|
753
|
+
targets.push({
|
|
754
|
+
kind: "pending",
|
|
755
|
+
key: `pending:${itemId}`,
|
|
756
|
+
container: actions,
|
|
757
|
+
itemId,
|
|
758
|
+
text: item.text,
|
|
759
|
+
preview: item.preview
|
|
760
|
+
});
|
|
639
761
|
}
|
|
640
762
|
return targets;
|
|
641
763
|
}
|
|
642
764
|
function sameTargets(left, right) {
|
|
643
765
|
return left.length === right.length && left.every((target, index) => {
|
|
644
766
|
const other = right[index];
|
|
645
|
-
|
|
767
|
+
if (other === void 0 || target.key !== other.key || target.container !== other.container) return false;
|
|
768
|
+
if (target.kind === "durable") return other.kind === "durable" && target.seq === other.seq;
|
|
769
|
+
return other.kind === "pending" && target.itemId === other.itemId;
|
|
646
770
|
});
|
|
647
771
|
}
|
|
648
772
|
function RewindPortals({ sessionId, sessionOf, currentSessionId, t, subscribeLocale }) {
|
|
@@ -662,7 +786,8 @@ function RewindPortals({ sessionId, sessionOf, currentSessionId, t, subscribeLoc
|
|
|
662
786
|
setTargets([]);
|
|
663
787
|
return;
|
|
664
788
|
}
|
|
665
|
-
const
|
|
789
|
+
const snapshot = session.getSnapshot();
|
|
790
|
+
const chat = snapshot.chat;
|
|
666
791
|
const hiddenSeqs = hiddenSeqsOf(chat);
|
|
667
792
|
let hiddenCount = 0;
|
|
668
793
|
for (const seat of document.querySelectorAll(CHAT_SEAT_SELECTOR)) {
|
|
@@ -684,7 +809,7 @@ function RewindPortals({ sessionId, sessionOf, currentSessionId, t, subscribeLoc
|
|
|
684
809
|
`[dsh-rewind] hiding: ${hiddenCount} rows, seqs [${[...hiddenSeqs].slice(0, 20).join(", ")}${hiddenSeqs.size > 20 ? "\u2026" : ""}]`
|
|
685
810
|
);
|
|
686
811
|
}
|
|
687
|
-
const next = collectTargets(chat, hiddenSeqs);
|
|
812
|
+
const next = [...collectTargets(chat, hiddenSeqs), ...collectPendingTargets(snapshot)];
|
|
688
813
|
setTargets((current) => sameTargets(current, next) ? current : next);
|
|
689
814
|
};
|
|
690
815
|
const queueRefresh = () => {
|
|
@@ -704,7 +829,16 @@ function RewindPortals({ sessionId, sessionOf, currentSessionId, t, subscribeLoc
|
|
|
704
829
|
};
|
|
705
830
|
}, [sessionId, sessionOf]);
|
|
706
831
|
return targets.map((target) => (0, import_react_dom.createPortal)(
|
|
707
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
832
|
+
target.kind === "pending" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
833
|
+
RetractButton,
|
|
834
|
+
{
|
|
835
|
+
target,
|
|
836
|
+
sessionId,
|
|
837
|
+
sessionOf,
|
|
838
|
+
t
|
|
839
|
+
},
|
|
840
|
+
target.key
|
|
841
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
708
842
|
RewindButton,
|
|
709
843
|
{
|
|
710
844
|
target,
|
|
@@ -753,6 +887,49 @@ function RewindButton({ target, sessionId, sessionOf, currentSessionId, t }) {
|
|
|
753
887
|
}
|
|
754
888
|
);
|
|
755
889
|
}
|
|
890
|
+
function composerText() {
|
|
891
|
+
const textarea = document.querySelector(COMPOSER_SELECTOR);
|
|
892
|
+
return textarea === null ? "" : textarea.value;
|
|
893
|
+
}
|
|
894
|
+
async function retractPending(session, itemId, text) {
|
|
895
|
+
await session.cancel();
|
|
896
|
+
const queue = session.getSnapshot().queue;
|
|
897
|
+
const steering = queue.filter((item) => item.placement === "steering");
|
|
898
|
+
for (const id of retractSpan(steering, itemId)) {
|
|
899
|
+
await session.updateQueue(id, { kind: "remove" });
|
|
900
|
+
}
|
|
901
|
+
if (text !== null && text !== "" && composerText().trim() === "") {
|
|
902
|
+
fillComposer(text);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
function RetractButton({ target, sessionId, sessionOf, t }) {
|
|
906
|
+
const onClick = (event) => {
|
|
907
|
+
event.stopPropagation();
|
|
908
|
+
const session = sessionOf(sessionId);
|
|
909
|
+
if (session === void 0) return;
|
|
910
|
+
openPopover({
|
|
911
|
+
session,
|
|
912
|
+
preview: target.preview,
|
|
913
|
+
anchor: event.currentTarget,
|
|
914
|
+
t,
|
|
915
|
+
retract: { itemId: target.itemId, text: target.text },
|
|
916
|
+
onRetract: () => {
|
|
917
|
+
void retractPending(session, target.itemId, target.text);
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
};
|
|
921
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
922
|
+
"button",
|
|
923
|
+
{
|
|
924
|
+
type: "button",
|
|
925
|
+
className: CLASS.button,
|
|
926
|
+
"aria-label": t("button.retract.aria"),
|
|
927
|
+
title: t("button.retract.title"),
|
|
928
|
+
onClick,
|
|
929
|
+
dangerouslySetInnerHTML: { __html: REWIND_ICON_SVG }
|
|
930
|
+
}
|
|
931
|
+
);
|
|
932
|
+
}
|
|
756
933
|
function createRewindBridge(deps) {
|
|
757
934
|
return function RewindBridge({ sessionId }) {
|
|
758
935
|
return (0, import_react.createElement)(RewindPortals, { sessionId, ...deps });
|
|
@@ -763,8 +940,14 @@ function createRewindBridge(deps) {
|
|
|
763
940
|
var zh = {
|
|
764
941
|
"button.aria": "\u56DE\u9000\u5230\u6B64\u6D88\u606F",
|
|
765
942
|
"button.title": "\u56DE\u9000",
|
|
943
|
+
"button.retract.aria": "\u56DE\u9000\u5230\u6B64\u63D2\u8BDD\u6D88\u606F",
|
|
944
|
+
"button.retract.title": "\u56DE\u9000",
|
|
766
945
|
"popover.title": "\u56DE\u9000\u5230\u8FD9\u6761\u6D88\u606F",
|
|
767
946
|
"popover.noText": "\uFF08\u65E0\u6587\u672C\uFF09",
|
|
947
|
+
"popover.retract.title": "\u56DE\u9000\u5230\u8FD9\u6761\u63D2\u8BDD\u6D88\u606F",
|
|
948
|
+
"popover.retract.target": "\u63D2\u8BDD\u4E2D \xB7 {preview}",
|
|
949
|
+
"popover.retract.hint": "\u5C06\u505C\u6B62\u5F53\u524D\u751F\u6210\uFF0C\u5E76\u56DE\u9000\u5230\u8BE5\u6D88\u606F\u4E4B\u524D",
|
|
950
|
+
"popover.retract.confirm": "\u786E\u8BA4\u56DE\u9000",
|
|
768
951
|
"popover.chat": "\u4EC5\u56DE\u9000\u5BF9\u8BDD",
|
|
769
952
|
"popover.chat.hint": "\u53EA\u56DE\u9000\u6A21\u578B\u4E0A\u4E0B\u6587\uFF0C\u4E0D\u52A8\u5DE5\u4F5C\u533A\u6587\u4EF6",
|
|
770
953
|
"popover.both": "\u56DE\u9000\u5BF9\u8BDD\u548C\u4EE3\u7801",
|
|
@@ -784,8 +967,14 @@ var zh = {
|
|
|
784
967
|
var en = {
|
|
785
968
|
"button.aria": "Rewind to this message",
|
|
786
969
|
"button.title": "Rewind",
|
|
970
|
+
"button.retract.aria": "Rewind to this pending message",
|
|
971
|
+
"button.retract.title": "Rewind",
|
|
787
972
|
"popover.title": "Rewind to this message",
|
|
788
973
|
"popover.noText": "(no text)",
|
|
974
|
+
"popover.retract.title": "Rewind to this pending message",
|
|
975
|
+
"popover.retract.target": "Pending \xB7 {preview}",
|
|
976
|
+
"popover.retract.hint": "Stops the current run and rewinds to before this message",
|
|
977
|
+
"popover.retract.confirm": "Confirm rewind",
|
|
789
978
|
"popover.chat": "Rewind conversation only",
|
|
790
979
|
"popover.chat.hint": "Cut the model context only; workspace files stay untouched",
|
|
791
980
|
"popover.both": "Rewind conversation and code",
|
package/lib/index.js
CHANGED
|
@@ -653,6 +653,11 @@ async function waitForAgentIdle(agent, signal, timeoutMs = 15e3) {
|
|
|
653
653
|
if (onAbort !== void 0) signal.removeEventListener("abort", onAbort);
|
|
654
654
|
}
|
|
655
655
|
}
|
|
656
|
+
function dropPendingSteering(agent) {
|
|
657
|
+
for (const message of [...agent.inbox.nextStep]) {
|
|
658
|
+
agent.inbox.remove(message.id);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
656
661
|
async function executeRewind(ctx, store, fs, invocation, rawTarget, mode, inflight) {
|
|
657
662
|
const { agent } = invocation;
|
|
658
663
|
const sessionId = agent.session.id;
|
|
@@ -662,12 +667,13 @@ async function executeRewind(ctx, store, fs, invocation, rawTarget, mode, inflig
|
|
|
662
667
|
inflight.add(sessionId);
|
|
663
668
|
try {
|
|
664
669
|
if (agent.status !== "idle") {
|
|
665
|
-
agent.cancel({ kind: "user" });
|
|
670
|
+
agent.cancel({ kind: "user" }, { keepInbox: true });
|
|
666
671
|
const stopped = await waitForAgentIdle(agent, invocation.signal);
|
|
667
672
|
if (!stopped) {
|
|
668
673
|
return { kind: "error", text: t("stopFailed") };
|
|
669
674
|
}
|
|
670
675
|
}
|
|
676
|
+
dropPendingSteering(agent);
|
|
671
677
|
if (invocation.signal.aborted) {
|
|
672
678
|
return { kind: "error", text: t("cancelled") };
|
|
673
679
|
}
|
|
@@ -3,8 +3,14 @@
|
|
|
3
3
|
export declare const zh: {
|
|
4
4
|
'button.aria': string;
|
|
5
5
|
'button.title': string;
|
|
6
|
+
'button.retract.aria': string;
|
|
7
|
+
'button.retract.title': string;
|
|
6
8
|
'popover.title': string;
|
|
7
9
|
'popover.noText': string;
|
|
10
|
+
'popover.retract.title': string;
|
|
11
|
+
'popover.retract.target': string;
|
|
12
|
+
'popover.retract.hint': string;
|
|
13
|
+
'popover.retract.confirm': string;
|
|
8
14
|
'popover.chat': string;
|
|
9
15
|
'popover.chat.hint': string;
|
|
10
16
|
'popover.both': string;
|
|
@@ -33,8 +39,14 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
|
33
39
|
export declare const en: {
|
|
34
40
|
'button.aria': string;
|
|
35
41
|
'button.title': string;
|
|
42
|
+
'button.retract.aria': string;
|
|
43
|
+
'button.retract.title': string;
|
|
36
44
|
'popover.title': string;
|
|
37
45
|
'popover.noText': string;
|
|
46
|
+
'popover.retract.title': string;
|
|
47
|
+
'popover.retract.target': string;
|
|
48
|
+
'popover.retract.hint': string;
|
|
49
|
+
'popover.retract.confirm': string;
|
|
38
50
|
'popover.chat': string;
|
|
39
51
|
'popover.chat.hint': string;
|
|
40
52
|
'popover.both': string;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure pending-message matching: pairs the rendered pending-steering bubble
|
|
3
|
+
* rows with the session's transient queue mirror rows (`placement === 'steering'`).
|
|
4
|
+
*
|
|
5
|
+
* Both sides derive from the host's next-step inbox order — the ChatView
|
|
6
|
+
* renders `pendingSteering` in array order and the queue mirror keeps the same
|
|
7
|
+
* host order — so index-primary matching is reliable. Text equality is still
|
|
8
|
+
* verified as a per-row cross-check, and a row that fails (or a row with no
|
|
9
|
+
* mirror item, or a mirror item with no row) is skipped INDIVIDUALLY: one bad
|
|
10
|
+
* row never takes down the other rows' buttons. The matching text is the
|
|
11
|
+
* bubble's message text WITHOUT its actions container — the harness copy
|
|
12
|
+
* button's Tooltip mounts a label bubble inside that container on hover, so
|
|
13
|
+
* the full row textContent would flip between "message" and "message+Copy"
|
|
14
|
+
* with the mouse, flickering the button (see `bubbleTextOf` in portals.tsx).
|
|
15
|
+
*
|
|
16
|
+
* The browser half lives in `portals.tsx`; this module stays DOM-free so the
|
|
17
|
+
* matching contract is unit-testable in a plain node environment.
|
|
18
|
+
*
|
|
19
|
+
* @module dsh-rewind/client/pending
|
|
20
|
+
*/
|
|
21
|
+
/** One rendered pending-steering bubble row (only the fields matching reads). */
|
|
22
|
+
export interface PendingRow {
|
|
23
|
+
/** The bubble's message text, excluding the actions container (see module doc). */
|
|
24
|
+
readonly text: string;
|
|
25
|
+
}
|
|
26
|
+
/** One steering occurrence from the session queue mirror. */
|
|
27
|
+
export interface PendingSteeringItem {
|
|
28
|
+
readonly id: string;
|
|
29
|
+
/** Complete editable text; null when the message contains non-text blocks. */
|
|
30
|
+
readonly text: string | null;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Pair rows to steering items by index, verifying text equality per row.
|
|
34
|
+
* @param rows - pending bubble rows in DOM order (== render order).
|
|
35
|
+
* @param steering - steering queue items in host order (== render order).
|
|
36
|
+
* @returns the item id for each row, or null for rows that cannot be matched
|
|
37
|
+
* safely (missing counterpart, text mismatch). A bad row never affects the
|
|
38
|
+
* other rows.
|
|
39
|
+
*/
|
|
40
|
+
export declare function matchPendingRows(rows: readonly PendingRow[], steering: readonly PendingSteeringItem[]): readonly (string | null)[];
|
|
41
|
+
/**
|
|
42
|
+
* The pending-steering ids a "rewind to this pre-sent message" retracts: the
|
|
43
|
+
* target occurrence and every steering message after it, in inbox (FIFO)
|
|
44
|
+
* order. Queued (next-turn) messages are deliberately NOT included — the
|
|
45
|
+
* harness QueueDock already offers the user per-item edit/remove, so a rewind
|
|
46
|
+
* must not silently drop messages the user may still want to send.
|
|
47
|
+
* @param steering - steering queue items in host order (== render order).
|
|
48
|
+
* @param targetId - the rewind target's inbox occurrence id.
|
|
49
|
+
* @returns the ids to remove, oldest-first; empty when the target is no
|
|
50
|
+
* longer pending (already claimed/consumed).
|
|
51
|
+
*/
|
|
52
|
+
export declare function retractSpan(steering: readonly {
|
|
53
|
+
readonly id: string;
|
|
54
|
+
}[], targetId: string): readonly string[];
|
|
@@ -19,8 +19,17 @@ import type { RewindKey } from './locales.ts';
|
|
|
19
19
|
type Translate = (key: RewindKey, params?: Record<string, unknown>) => string;
|
|
20
20
|
export interface PopoverOptions {
|
|
21
21
|
readonly session: SessionFace;
|
|
22
|
-
|
|
23
|
-
readonly
|
|
22
|
+
/** Durable variant: the target message seq (mode-selection flow). */
|
|
23
|
+
readonly seq?: number;
|
|
24
|
+
/** Durable variant: the target message time. */
|
|
25
|
+
readonly time?: number;
|
|
26
|
+
/** Pending variant: retract a pre-sent steering message (single-confirm flow). */
|
|
27
|
+
readonly retract?: {
|
|
28
|
+
readonly itemId: string;
|
|
29
|
+
readonly text: string | null;
|
|
30
|
+
};
|
|
31
|
+
/** Pending variant: executed after the retract confirm closes the popover. */
|
|
32
|
+
readonly onRetract?: () => void;
|
|
24
33
|
readonly preview: string;
|
|
25
34
|
/** The button that opened the popover (outside-click ignore target). */
|
|
26
35
|
readonly anchor: HTMLElement;
|
|
@@ -30,7 +39,7 @@ export interface PopoverOptions {
|
|
|
30
39
|
* the callback owns the command + composer-refill lifecycle (see
|
|
31
40
|
* runRewindAndFill in index.ts).
|
|
32
41
|
*/
|
|
33
|
-
readonly onRewind
|
|
42
|
+
readonly onRewind?: (mode: 'chat' | 'both') => void;
|
|
34
43
|
}
|
|
35
44
|
/** Close the current popover, if any. */
|
|
36
45
|
export declare function closePopover(): void;
|
|
@@ -26,6 +26,28 @@ import { type ReactNode } from 'react';
|
|
|
26
26
|
import type { SessionFace } from '@deepseek-ai/dsh-client-runtime/client';
|
|
27
27
|
import type { RewindKey } from './locales.ts';
|
|
28
28
|
type Translate = (key: RewindKey, params?: Record<string, unknown>) => string;
|
|
29
|
+
/** One portal target: the actions row of a user/steering seat + its durable node. */
|
|
30
|
+
export type PortalTarget = {
|
|
31
|
+
readonly kind: 'durable';
|
|
32
|
+
/** The seat's chat node key (React reconciliation + diff identity). */
|
|
33
|
+
readonly key: string;
|
|
34
|
+
/** The row's actions container (React portal target). */
|
|
35
|
+
readonly container: HTMLElement;
|
|
36
|
+
readonly seq: number;
|
|
37
|
+
readonly time: number;
|
|
38
|
+
readonly preview: string;
|
|
39
|
+
} | {
|
|
40
|
+
readonly kind: 'pending';
|
|
41
|
+
/** `pending:${itemId}` — stable per inbox occurrence. */
|
|
42
|
+
readonly key: string;
|
|
43
|
+
/** The row's actions container (React portal target). */
|
|
44
|
+
readonly container: HTMLElement;
|
|
45
|
+
/** The host inbox occurrence the retract button addresses. */
|
|
46
|
+
readonly itemId: string;
|
|
47
|
+
/** Complete editable text; null when the message contains non-text blocks. */
|
|
48
|
+
readonly text: string | null;
|
|
49
|
+
readonly preview: string;
|
|
50
|
+
};
|
|
29
51
|
/** Capabilities the session-scoped bridge receives from the plugin apply(). */
|
|
30
52
|
export interface RewindBridgeDeps {
|
|
31
53
|
readonly sessionOf: (sessionId: string) => SessionFace | undefined;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-rewind-plugin",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "DeepSeek Harness plugin: in-place conversation rewind in the same session window (Claude Code /rewind semantics) with optional workspace file restore",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|