dsh-rewind-plugin 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -181
- package/README.zh.md +73 -168
- package/docs/harness-reference.md +23 -0
- package/docs/release.md +90 -0
- package/docs/troubleshooting.md +39 -0
- package/docs/troubleshooting.zh.md +31 -0
- package/lib/client.js +219 -148
- package/lib/types/client/index.d.ts +14 -18
- package/lib/types/client/portals.d.ts +66 -0
- package/lib/types/client/styles.d.ts +0 -2
- package/package.json +7 -2
package/lib/client.js
CHANGED
|
@@ -32,6 +32,10 @@ __export(index_exports, {
|
|
|
32
32
|
});
|
|
33
33
|
module.exports = __toCommonJS(index_exports);
|
|
34
34
|
|
|
35
|
+
// src/client/portals.tsx
|
|
36
|
+
var import_react = require("react");
|
|
37
|
+
var import_react_dom = require("react-dom");
|
|
38
|
+
|
|
35
39
|
// src/client/hidden.ts
|
|
36
40
|
function targetOfOutcome(text) {
|
|
37
41
|
if (text === void 0) return void 0;
|
|
@@ -85,48 +89,7 @@ function hiddenSeqsOf(snap) {
|
|
|
85
89
|
return hidden;
|
|
86
90
|
}
|
|
87
91
|
|
|
88
|
-
// src/client/locales.ts
|
|
89
|
-
var zh = {
|
|
90
|
-
"button.aria": "\u56DE\u9000\u5230\u6B64\u6D88\u606F",
|
|
91
|
-
"button.title": "\u56DE\u9000",
|
|
92
|
-
"popover.title": "\u56DE\u9000\u5230\u8FD9\u6761\u6D88\u606F",
|
|
93
|
-
"popover.noText": "\uFF08\u65E0\u6587\u672C\uFF09",
|
|
94
|
-
"popover.chat": "\u4EC5\u56DE\u9000\u5BF9\u8BDD",
|
|
95
|
-
"popover.chat.hint": "\u53EA\u56DE\u9000\u6A21\u578B\u4E0A\u4E0B\u6587\uFF0C\u4E0D\u52A8\u5DE5\u4F5C\u533A\u6587\u4EF6",
|
|
96
|
-
"popover.both": "\u56DE\u9000\u5BF9\u8BDD\u548C\u4EE3\u7801",
|
|
97
|
-
"popover.both.hint": "\u5BF9\u8BDD\u56DE\u9000\u5E76\u8FD8\u539F\u5DE5\u4F5C\u533A\u6587\u4EF6",
|
|
98
|
-
"popover.checking": "\u6B63\u5728\u68C0\u67E5\u6587\u4EF6\u53D8\u66F4\u2026",
|
|
99
|
-
"popover.noChanges": "\u6B64\u6D88\u606F\u4E4B\u540E\u6CA1\u6709\u53EF\u8FD8\u539F\u7684\u6587\u4EF6\u53D8\u66F4\uFF0C\u4EC5\u53EF\u56DE\u9000\u5BF9\u8BDD",
|
|
100
|
-
"popover.cancel": "\u53D6\u6D88",
|
|
101
|
-
"popover.impact.loading": "\u6B63\u5728\u83B7\u53D6\u5F71\u54CD\u6E05\u5355\u2026",
|
|
102
|
-
"popover.impact.failed": "\u65E0\u6CD5\u83B7\u53D6\u5F71\u54CD\u6E05\u5355\uFF1A{message}",
|
|
103
|
-
"popover.impact.none": "\u76EE\u6807\u4E4B\u540E\u6CA1\u6709\u8DDF\u8E2A\u5230\u7684\u5199\u7C7B\u53D8\u66F4\uFF0C\u65E0\u9700\u8FD8\u539F\u6587\u4EF6\u3002",
|
|
104
|
-
"popover.confirm": "\u786E\u8BA4\u56DE\u9000",
|
|
105
|
-
"popover.back": "\u8FD4\u56DE",
|
|
106
|
-
"guard.hint": "/rewind \u547D\u4EE4\u4E0D\u652F\u6301\u624B\u52A8\u8F93\u5165\uFF0C\u8BF7\u4F7F\u7528\u6D88\u606F\u65C1\u7684 \u21B6 \u56DE\u9000\u6309\u94AE"
|
|
107
|
-
};
|
|
108
|
-
var en = {
|
|
109
|
-
"button.aria": "Rewind to this message",
|
|
110
|
-
"button.title": "Rewind",
|
|
111
|
-
"popover.title": "Rewind to this message",
|
|
112
|
-
"popover.noText": "(no text)",
|
|
113
|
-
"popover.chat": "Rewind conversation only",
|
|
114
|
-
"popover.chat.hint": "Cut the model context only; workspace files stay untouched",
|
|
115
|
-
"popover.both": "Rewind conversation and code",
|
|
116
|
-
"popover.both.hint": "Cut the context and restore workspace files",
|
|
117
|
-
"popover.checking": "Checking for file changes\u2026",
|
|
118
|
-
"popover.noChanges": "No tracked file changes after this message; conversation-only rewind",
|
|
119
|
-
"popover.cancel": "Cancel",
|
|
120
|
-
"popover.impact.loading": "Fetching impact list\u2026",
|
|
121
|
-
"popover.impact.failed": "Could not fetch the impact list: {message}",
|
|
122
|
-
"popover.impact.none": "No tracked file changes after the target; nothing to restore.",
|
|
123
|
-
"popover.confirm": "Confirm rewind",
|
|
124
|
-
"popover.back": "Back",
|
|
125
|
-
"guard.hint": "/rewind cannot be typed manually \u2014 use the \u21B6 button on a message to rewind."
|
|
126
|
-
};
|
|
127
|
-
|
|
128
92
|
// src/client/styles.ts
|
|
129
|
-
var REWIND_ATTACHED = "data-dsh-rewind-attached";
|
|
130
93
|
var CLASS = {
|
|
131
94
|
button: "dsh-rewind-btn",
|
|
132
95
|
popover: "dsh-rewind-popover",
|
|
@@ -497,14 +460,8 @@ function openPopover(opts) {
|
|
|
497
460
|
};
|
|
498
461
|
}
|
|
499
462
|
|
|
500
|
-
// src/client/
|
|
501
|
-
var
|
|
502
|
-
var inject = ["sessions", "locale"];
|
|
503
|
-
var NS = "rewind";
|
|
504
|
-
var USER_SEAT_SELECTOR = '[data-chat-flow-kind="user"], [data-chat-flow-kind="steering"]';
|
|
505
|
-
var CHAT_SEAT_SELECTOR = "[data-chat-anchor-key]";
|
|
506
|
-
var ACTIONS_ROOT_SELECTOR = "[data-time-hover-root]";
|
|
507
|
-
var COMPOSER_SELECTOR = "[data-input-scroll] textarea, textarea[data-phase]";
|
|
463
|
+
// src/client/portals.tsx
|
|
464
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
508
465
|
function messagePreviewOf(node) {
|
|
509
466
|
const text = node.content.map((block) => block.type === "text" && typeof block.text === "string" ? block.text : "").join("").replace(/\s+/g, " ").trim();
|
|
510
467
|
return text.length <= 80 ? text : `${text.slice(0, 79)}\u2026`;
|
|
@@ -530,90 +487,84 @@ function fillComposer(text) {
|
|
|
530
487
|
textarea.focus();
|
|
531
488
|
return true;
|
|
532
489
|
}
|
|
533
|
-
function
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
});
|
|
596
|
-
actions.appendChild(button);
|
|
597
|
-
buttons.set(key, button);
|
|
598
|
-
seat.setAttribute(REWIND_ATTACHED, "");
|
|
599
|
-
};
|
|
600
|
-
const scan = () => {
|
|
601
|
-
for (const [key, button] of buttons) {
|
|
602
|
-
if (!button.isConnected) buttons.delete(key);
|
|
490
|
+
function userNodeOf(session, key) {
|
|
491
|
+
const node = session.getSnapshot().chat.nodes.get(key);
|
|
492
|
+
if (node === void 0 || node.kind !== "user" && node.kind !== "steering") return void 0;
|
|
493
|
+
return node.data;
|
|
494
|
+
}
|
|
495
|
+
async function runRewindAndFill(session, seq, mode, currentSessionId) {
|
|
496
|
+
const known = knownCommandSeqs(session, (node) => isExecutedRewindCommand(node, seq));
|
|
497
|
+
const result = await session.command(`/rewind @${seq} ${mode}`);
|
|
498
|
+
if (!result.ok || result.value?.matched !== true) return;
|
|
499
|
+
const outcome = await waitForCommand(session, (node) => isExecutedRewindCommand(node, seq) && !known.has(node.seq), 2e4);
|
|
500
|
+
if (outcome === null || outcome.kind !== "success") return;
|
|
501
|
+
if (currentSessionId() !== session.sessionId) return;
|
|
502
|
+
const text = userTextAt(session, seq);
|
|
503
|
+
if (text === void 0 || text === "") return;
|
|
504
|
+
fillComposer(text);
|
|
505
|
+
}
|
|
506
|
+
var COMPOSER_SELECTOR = "[data-input-scroll] textarea, textarea[data-phase]";
|
|
507
|
+
var USER_SEAT_SELECTOR = '[data-chat-flow-kind="user"][data-chat-anchor-key], [data-chat-flow-kind="steering"][data-chat-anchor-key]';
|
|
508
|
+
var CHAT_SEAT_SELECTOR = "[data-chat-anchor-key]";
|
|
509
|
+
var ACTIONS_ROOT_SELECTOR = "[data-time-hover-root]";
|
|
510
|
+
function collectTargets(chat, hiddenSeqs) {
|
|
511
|
+
const rows = /* @__PURE__ */ new Map();
|
|
512
|
+
for (const element of document.querySelectorAll(USER_SEAT_SELECTOR)) {
|
|
513
|
+
const key = element.dataset.chatAnchorKey;
|
|
514
|
+
if (key !== void 0) rows.set(key, element);
|
|
515
|
+
}
|
|
516
|
+
const targets = [];
|
|
517
|
+
for (const key of chat.order) {
|
|
518
|
+
const node = chat.nodes.get(key);
|
|
519
|
+
if (node === void 0 || node.kind !== "user" && node.kind !== "steering") continue;
|
|
520
|
+
const user = node.data;
|
|
521
|
+
if (hiddenSeqs.has(node.anchorSeq ?? user.seq)) continue;
|
|
522
|
+
const row = rows.get(key);
|
|
523
|
+
const messageRoot = row?.querySelector(ACTIONS_ROOT_SELECTOR);
|
|
524
|
+
const actions = messageRoot?.lastElementChild;
|
|
525
|
+
if (!(actions instanceof HTMLElement) || actions.querySelector("button") === null) continue;
|
|
526
|
+
targets.push({ key, container: actions, seq: user.seq, time: user.time, preview: messagePreviewOf(user) });
|
|
527
|
+
}
|
|
528
|
+
return targets;
|
|
529
|
+
}
|
|
530
|
+
function sameTargets(left, right) {
|
|
531
|
+
return left.length === right.length && left.every((target, index) => {
|
|
532
|
+
const other = right[index];
|
|
533
|
+
return other !== void 0 && target.key === other.key && target.container === other.container && target.seq === other.seq;
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
function RewindPortals({ sessionId, sessionOf, currentSessionId, t, subscribeLocale }) {
|
|
537
|
+
const [targets, setTargets] = (0, import_react.useState)([]);
|
|
538
|
+
const hidden = (0, import_react.useRef)(/* @__PURE__ */ new WeakSet());
|
|
539
|
+
const [, forceRender] = (0, import_react.useReducer)((count) => count + 1, 0);
|
|
540
|
+
(0, import_react.useEffect)(() => subscribeLocale(() => {
|
|
541
|
+
forceRender();
|
|
542
|
+
}), [subscribeLocale]);
|
|
543
|
+
(0, import_react.useLayoutEffect)(() => {
|
|
544
|
+
let active = true;
|
|
545
|
+
let queued = false;
|
|
546
|
+
const refresh = () => {
|
|
547
|
+
if (!active) return;
|
|
548
|
+
const session = sessionOf(sessionId);
|
|
549
|
+
if (session === void 0) {
|
|
550
|
+
setTargets([]);
|
|
551
|
+
return;
|
|
603
552
|
}
|
|
604
|
-
const
|
|
605
|
-
const hiddenSeqs =
|
|
553
|
+
const chat = session.getSnapshot().chat;
|
|
554
|
+
const hiddenSeqs = hiddenSeqsOf(chat);
|
|
606
555
|
let hiddenCount = 0;
|
|
607
556
|
for (const seat of document.querySelectorAll(CHAT_SEAT_SELECTOR)) {
|
|
608
557
|
const key = seat.dataset.chatAnchorKey;
|
|
609
|
-
const anchor = key !== void 0
|
|
558
|
+
const anchor = key !== void 0 ? chat.nodes.get(key)?.anchorSeq : void 0;
|
|
610
559
|
if (anchor !== void 0 && hiddenSeqs.has(anchor)) {
|
|
611
560
|
seat.style.display = "none";
|
|
612
|
-
|
|
561
|
+
seat.dataset.dshRewindHidden = "true";
|
|
562
|
+
hidden.current.add(seat);
|
|
613
563
|
hiddenCount += 1;
|
|
614
|
-
} else if (hidden.has(seat)) {
|
|
564
|
+
} else if (hidden.current.has(seat)) {
|
|
615
565
|
seat.style.display = "";
|
|
616
|
-
|
|
566
|
+
delete seat.dataset.dshRewindHidden;
|
|
567
|
+
hidden.current.delete(seat);
|
|
617
568
|
}
|
|
618
569
|
}
|
|
619
570
|
if (hiddenSeqs.size > 0 || hiddenCount > 0) {
|
|
@@ -621,23 +572,151 @@ function apply(ctx) {
|
|
|
621
572
|
`[dsh-rewind] hiding: ${hiddenCount} rows, seqs [${[...hiddenSeqs].slice(0, 20).join(", ")}${hiddenSeqs.size > 20 ? "\u2026" : ""}]`
|
|
622
573
|
);
|
|
623
574
|
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
}
|
|
575
|
+
const next = collectTargets(chat, hiddenSeqs);
|
|
576
|
+
setTargets((current) => sameTargets(current, next) ? current : next);
|
|
627
577
|
};
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
const text = userTextAt(session, seq);
|
|
636
|
-
if (text === void 0 || text === "") return;
|
|
637
|
-
fillComposer(text);
|
|
578
|
+
const queueRefresh = () => {
|
|
579
|
+
if (queued || !active) return;
|
|
580
|
+
queued = true;
|
|
581
|
+
queueMicrotask(() => {
|
|
582
|
+
queued = false;
|
|
583
|
+
refresh();
|
|
584
|
+
});
|
|
638
585
|
};
|
|
586
|
+
refresh();
|
|
587
|
+
const observer = new MutationObserver(queueRefresh);
|
|
588
|
+
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["style"] });
|
|
589
|
+
return () => {
|
|
590
|
+
active = false;
|
|
591
|
+
observer.disconnect();
|
|
592
|
+
};
|
|
593
|
+
}, [sessionId, sessionOf]);
|
|
594
|
+
return targets.map((target) => (0, import_react_dom.createPortal)(
|
|
595
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
596
|
+
RewindButton,
|
|
597
|
+
{
|
|
598
|
+
target,
|
|
599
|
+
sessionId,
|
|
600
|
+
sessionOf,
|
|
601
|
+
currentSessionId,
|
|
602
|
+
t
|
|
603
|
+
},
|
|
604
|
+
target.key
|
|
605
|
+
),
|
|
606
|
+
target.container,
|
|
607
|
+
target.key
|
|
608
|
+
));
|
|
609
|
+
}
|
|
610
|
+
function RewindButton({ target, sessionId, sessionOf, currentSessionId, t }) {
|
|
611
|
+
const onClick = (event) => {
|
|
612
|
+
event.stopPropagation();
|
|
613
|
+
const session = sessionOf(sessionId);
|
|
614
|
+
if (session === void 0) {
|
|
615
|
+
console.warn("[dsh-rewind] rewind button clicked with no session binding");
|
|
616
|
+
return;
|
|
617
|
+
}
|
|
618
|
+
const node = userNodeOf(session, target.key);
|
|
619
|
+
if (node === void 0) return;
|
|
620
|
+
openPopover({
|
|
621
|
+
session,
|
|
622
|
+
seq: node.seq,
|
|
623
|
+
time: node.time,
|
|
624
|
+
preview: messagePreviewOf(node),
|
|
625
|
+
anchor: event.currentTarget,
|
|
626
|
+
t,
|
|
627
|
+
onRewind: (mode) => {
|
|
628
|
+
void runRewindAndFill(session, node.seq, mode, currentSessionId);
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
};
|
|
632
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
633
|
+
"button",
|
|
634
|
+
{
|
|
635
|
+
type: "button",
|
|
636
|
+
className: CLASS.button,
|
|
637
|
+
"aria-label": t("button.aria"),
|
|
638
|
+
title: t("button.title"),
|
|
639
|
+
onClick,
|
|
640
|
+
dangerouslySetInnerHTML: { __html: REWIND_ICON_SVG }
|
|
641
|
+
}
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
function createRewindBridge(deps) {
|
|
645
|
+
return function RewindBridge({ sessionId }) {
|
|
646
|
+
return (0, import_react.createElement)(RewindPortals, { sessionId, ...deps });
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// src/client/locales.ts
|
|
651
|
+
var zh = {
|
|
652
|
+
"button.aria": "\u56DE\u9000\u5230\u6B64\u6D88\u606F",
|
|
653
|
+
"button.title": "\u56DE\u9000",
|
|
654
|
+
"popover.title": "\u56DE\u9000\u5230\u8FD9\u6761\u6D88\u606F",
|
|
655
|
+
"popover.noText": "\uFF08\u65E0\u6587\u672C\uFF09",
|
|
656
|
+
"popover.chat": "\u4EC5\u56DE\u9000\u5BF9\u8BDD",
|
|
657
|
+
"popover.chat.hint": "\u53EA\u56DE\u9000\u6A21\u578B\u4E0A\u4E0B\u6587\uFF0C\u4E0D\u52A8\u5DE5\u4F5C\u533A\u6587\u4EF6",
|
|
658
|
+
"popover.both": "\u56DE\u9000\u5BF9\u8BDD\u548C\u4EE3\u7801",
|
|
659
|
+
"popover.both.hint": "\u5BF9\u8BDD\u56DE\u9000\u5E76\u8FD8\u539F\u5DE5\u4F5C\u533A\u6587\u4EF6",
|
|
660
|
+
"popover.checking": "\u6B63\u5728\u68C0\u67E5\u6587\u4EF6\u53D8\u66F4\u2026",
|
|
661
|
+
"popover.noChanges": "\u6B64\u6D88\u606F\u4E4B\u540E\u6CA1\u6709\u53EF\u8FD8\u539F\u7684\u6587\u4EF6\u53D8\u66F4\uFF0C\u4EC5\u53EF\u56DE\u9000\u5BF9\u8BDD",
|
|
662
|
+
"popover.cancel": "\u53D6\u6D88",
|
|
663
|
+
"popover.impact.loading": "\u6B63\u5728\u83B7\u53D6\u5F71\u54CD\u6E05\u5355\u2026",
|
|
664
|
+
"popover.impact.failed": "\u65E0\u6CD5\u83B7\u53D6\u5F71\u54CD\u6E05\u5355\uFF1A{message}",
|
|
665
|
+
"popover.impact.none": "\u76EE\u6807\u4E4B\u540E\u6CA1\u6709\u8DDF\u8E2A\u5230\u7684\u5199\u7C7B\u53D8\u66F4\uFF0C\u65E0\u9700\u8FD8\u539F\u6587\u4EF6\u3002",
|
|
666
|
+
"popover.confirm": "\u786E\u8BA4\u56DE\u9000",
|
|
667
|
+
"popover.back": "\u8FD4\u56DE",
|
|
668
|
+
"guard.hint": "/rewind \u547D\u4EE4\u4E0D\u652F\u6301\u624B\u52A8\u8F93\u5165\uFF0C\u8BF7\u4F7F\u7528\u6D88\u606F\u65C1\u7684 \u21B6 \u56DE\u9000\u6309\u94AE"
|
|
669
|
+
};
|
|
670
|
+
var en = {
|
|
671
|
+
"button.aria": "Rewind to this message",
|
|
672
|
+
"button.title": "Rewind",
|
|
673
|
+
"popover.title": "Rewind to this message",
|
|
674
|
+
"popover.noText": "(no text)",
|
|
675
|
+
"popover.chat": "Rewind conversation only",
|
|
676
|
+
"popover.chat.hint": "Cut the model context only; workspace files stay untouched",
|
|
677
|
+
"popover.both": "Rewind conversation and code",
|
|
678
|
+
"popover.both.hint": "Cut the context and restore workspace files",
|
|
679
|
+
"popover.checking": "Checking for file changes\u2026",
|
|
680
|
+
"popover.noChanges": "No tracked file changes after this message; conversation-only rewind",
|
|
681
|
+
"popover.cancel": "Cancel",
|
|
682
|
+
"popover.impact.loading": "Fetching impact list\u2026",
|
|
683
|
+
"popover.impact.failed": "Could not fetch the impact list: {message}",
|
|
684
|
+
"popover.impact.none": "No tracked file changes after the target; nothing to restore.",
|
|
685
|
+
"popover.confirm": "Confirm rewind",
|
|
686
|
+
"popover.back": "Back",
|
|
687
|
+
"guard.hint": "/rewind cannot be typed manually \u2014 use the \u21B6 button on a message to rewind."
|
|
688
|
+
};
|
|
689
|
+
|
|
690
|
+
// src/client/index.ts
|
|
691
|
+
var name = "dsh-rewind";
|
|
692
|
+
var inject = ["slots", "sessions", "locale"];
|
|
693
|
+
var NS = "rewind";
|
|
694
|
+
var HEADER_ACTIONS_SLOT = "conversation.session.header.actions";
|
|
695
|
+
var COMPOSER_SELECTOR2 = "[data-input-scroll] textarea, textarea[data-phase]";
|
|
696
|
+
function apply(ctx) {
|
|
697
|
+
ctx.effect(function* () {
|
|
698
|
+
yield ctx.locale.register(NS, { zh, en });
|
|
699
|
+
const t = ctx.locale.bind(NS);
|
|
700
|
+
const style = document.createElement("style");
|
|
701
|
+
style.dataset.plugin = "dsh-rewind";
|
|
702
|
+
style.textContent = STYLE;
|
|
703
|
+
document.head.appendChild(style);
|
|
704
|
+
const sessionOf = (sessionId) => ctx.sessions.binding(sessionId)?.session;
|
|
705
|
+
const currentSessionId = () => ctx.sessions.list.getSnapshot().current;
|
|
706
|
+
const subscribeLocale = (cb) => ctx.locale.subscribe(cb);
|
|
707
|
+
const slots = ctx.slots;
|
|
708
|
+
yield slots.inject(HEADER_ACTIONS_SLOT, () => slots.register(
|
|
709
|
+
{
|
|
710
|
+
name: HEADER_ACTIONS_SLOT,
|
|
711
|
+
// A distinct list-entry id keeps the bridge from shadowing any other
|
|
712
|
+
// header action; the entry renders portals only, never header UI.
|
|
713
|
+
id: "dsh-rewind-portals",
|
|
714
|
+
order: 1e3
|
|
715
|
+
},
|
|
716
|
+
createRewindBridge({ sessionOf, currentSessionId, t, subscribeLocale })
|
|
717
|
+
));
|
|
639
718
|
const MANUAL_REWIND = /^\s*\/rewind(?:\s|$)/i;
|
|
640
|
-
const composerTextarea = () => document.querySelector(
|
|
719
|
+
const composerTextarea = () => document.querySelector(COMPOSER_SELECTOR2);
|
|
641
720
|
const hasBlockedRewindDraft = () => {
|
|
642
721
|
const textarea = composerTextarea();
|
|
643
722
|
return textarea !== null && MANUAL_REWIND.test(textarea.value);
|
|
@@ -651,7 +730,7 @@ function apply(ctx) {
|
|
|
651
730
|
if (textarea === null) return;
|
|
652
731
|
const card = textarea.closest("[data-composer-card]");
|
|
653
732
|
const hint = document.createElement("div");
|
|
654
|
-
hint.className =
|
|
733
|
+
hint.className = "dsh-rewind-guard-hint";
|
|
655
734
|
hint.setAttribute("role", "status");
|
|
656
735
|
hint.textContent = t("guard.hint");
|
|
657
736
|
document.body.appendChild(hint);
|
|
@@ -689,19 +768,11 @@ function apply(ctx) {
|
|
|
689
768
|
};
|
|
690
769
|
document.addEventListener("keydown", onKeyDownGuard, true);
|
|
691
770
|
document.addEventListener("click", onClickGuard, true);
|
|
692
|
-
observer = new MutationObserver(scan);
|
|
693
|
-
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["style"] });
|
|
694
|
-
scan();
|
|
695
771
|
yield () => {
|
|
696
|
-
unsubscribeLocale();
|
|
697
772
|
document.removeEventListener("keydown", onKeyDownGuard, true);
|
|
698
773
|
document.removeEventListener("click", onClickGuard, true);
|
|
699
774
|
if (guardHintEl !== null) guardHintEl.remove();
|
|
700
775
|
if (guardHintTimer !== void 0) window.clearTimeout(guardHintTimer);
|
|
701
|
-
observer?.disconnect();
|
|
702
|
-
for (const button of buttons.values()) button.remove();
|
|
703
|
-
buttons.clear();
|
|
704
|
-
closePopover();
|
|
705
776
|
style.remove();
|
|
706
777
|
};
|
|
707
778
|
}, "dsh-rewind client lifecycle");
|
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-rewind client half: the
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* dsh-rewind client half: the manual `/rewind` composer guard, the locale
|
|
3
|
+
* registration, and the session-scoped portal bridge that renders the
|
|
4
|
+
* per-message ↶ rewind button (see `portals.tsx` for the button itself).
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* `
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* popover offers the two modes (step two); "rewind conversation and code"
|
|
15
|
-
* first fetches the impact list via `/rewind preview @seq both` and confirms
|
|
16
|
-
* before executing. Execution always goes through `session.command(...)`, the
|
|
17
|
-
* same host path the `/rewind` command uses.
|
|
6
|
+
* The button is NOT injected by hand into the DOM anymore: the plugin
|
|
7
|
+
* registers a bridge into the harness's `conversation.session.header.actions`
|
|
8
|
+
* list slot, and that bridge portals a React button into every user message's
|
|
9
|
+
* IconActions row — the same rendering family as the copy button (a React
|
|
10
|
+
* child of the actions row), without touching any harness source. The
|
|
11
|
+
* registration is typed structurally (see `SlotsLike` in portals.tsx), so the
|
|
12
|
+
* plugin never imports conversation UI types and survives harness version
|
|
13
|
+
* drift.
|
|
18
14
|
*
|
|
19
15
|
* Manual composer input of `/rewind` is deliberately blocked (the guard
|
|
20
|
-
* below): the command exists only as the per-message button's internal
|
|
16
|
+
* below): the command exists only as the per-message ↶ button's internal
|
|
21
17
|
* channel, so any `/rewind` line typed by hand — bare or with arguments — is
|
|
22
18
|
* stopped with a hint pointing at the button.
|
|
23
19
|
*
|
|
@@ -27,7 +23,7 @@ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
|
|
|
27
23
|
export declare const name = "dsh-rewind";
|
|
28
24
|
export declare const inject: string[];
|
|
29
25
|
/**
|
|
30
|
-
* Client plugin body:
|
|
31
|
-
* @param ctx - client root context carrying `sessions` and `locale`.
|
|
26
|
+
* Client plugin body: composer guard + locale + the portal bridge.
|
|
27
|
+
* @param ctx - client root context carrying `slots`, `sessions` and `locale`.
|
|
32
28
|
*/
|
|
33
29
|
export declare function apply(ctx: ClientContext): void;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-rewind portal half: the per-user-message ↶ rewind button, rendered as a
|
|
3
|
+
* React portal inside the message's `MessageIconActions` row.
|
|
4
|
+
*
|
|
5
|
+
* Why portals (aligned with the copy button's own rendering): the copy button
|
|
6
|
+
* is a React child of the actions row, painted in the same commit as the
|
|
7
|
+
* bubble. A pure-DOM `appendChild` (the earlier approach) lands one microtask
|
|
8
|
+
* later and re-runs a full-transcript scan on EVERY mutation, which can push
|
|
9
|
+
* the paint of a newly sent bubble — the "occasional hiccup before the bubble
|
|
10
|
+
* shows". Portals let React own the button lifecycle (mount/unmount with the
|
|
11
|
+
* row, no orphaned buttons, no manual re-attach after harness re-renders),
|
|
12
|
+
* and the target collection is coalesced (one refresh per mutation batch) and
|
|
13
|
+
* diffed (no setState churn when nothing changed).
|
|
14
|
+
*
|
|
15
|
+
* Mount point: the plugin registers a session-scoped bridge into the harness's
|
|
16
|
+
* `conversation.session.header.actions` list slot. The bridge renders NO
|
|
17
|
+
* header UI — it only portals buttons into the user rows of the session the
|
|
18
|
+
* harness mounts it for. That slot is the harness-native way to get a
|
|
19
|
+
* per-session React mount without touching any source; the registration is
|
|
20
|
+
* typed structurally (see `SlotsLike`) so the plugin never imports the
|
|
21
|
+
* conversation UI package's types and survives its version drift.
|
|
22
|
+
*
|
|
23
|
+
* @module dsh-rewind/client/portals
|
|
24
|
+
*/
|
|
25
|
+
import { type ReactNode } from 'react';
|
|
26
|
+
import type { SessionFace } from '@deepseek-ai/dsh-client-runtime/client';
|
|
27
|
+
import type { RewindKey } from './locales.ts';
|
|
28
|
+
type Translate = (key: RewindKey, params?: Record<string, unknown>) => string;
|
|
29
|
+
/** Capabilities the session-scoped bridge receives from the plugin apply(). */
|
|
30
|
+
export interface RewindBridgeDeps {
|
|
31
|
+
readonly sessionOf: (sessionId: string) => SessionFace | undefined;
|
|
32
|
+
readonly currentSessionId: () => string | undefined;
|
|
33
|
+
readonly t: Translate;
|
|
34
|
+
readonly subscribeLocale: (cb: () => void) => () => void;
|
|
35
|
+
}
|
|
36
|
+
/** Structural face of the runtime slot service (see the module doc). */
|
|
37
|
+
export interface SlotsLike {
|
|
38
|
+
inject(key: string, install: () => () => void): () => void;
|
|
39
|
+
register(entry: {
|
|
40
|
+
readonly name: string;
|
|
41
|
+
readonly id: string;
|
|
42
|
+
readonly order: number;
|
|
43
|
+
}, component: (props: {
|
|
44
|
+
readonly sessionId: string;
|
|
45
|
+
}) => ReactNode): () => void;
|
|
46
|
+
}
|
|
47
|
+
interface RewindPortalsProps extends RewindBridgeDeps {
|
|
48
|
+
readonly sessionId: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Session-scoped portal bridge: renders the ↶ button of every user message
|
|
52
|
+
* row of the session the harness mounts it for. The refresh is coalesced
|
|
53
|
+
* (one pass per mutation batch via queueMicrotask) and diffed (setState is
|
|
54
|
+
* skipped when the target set is unchanged), so the plugin never runs a
|
|
55
|
+
* synchronous full-transcript scan inside a commit microtask.
|
|
56
|
+
*/
|
|
57
|
+
export declare function RewindPortals({ sessionId, sessionOf, currentSessionId, t, subscribeLocale }: RewindPortalsProps): ReactNode;
|
|
58
|
+
/**
|
|
59
|
+
* Build the slot-entry component for the plugin apply(): a tiny bridge that
|
|
60
|
+
* injects the apply-time capabilities (session resolution, locale, rewind
|
|
61
|
+
* runner) into the module-level `RewindPortals`.
|
|
62
|
+
*/
|
|
63
|
+
export declare function createRewindBridge(deps: RewindBridgeDeps): (props: {
|
|
64
|
+
readonly sessionId: string;
|
|
65
|
+
}) => ReactNode;
|
|
66
|
+
export {};
|
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @module dsh-rewind/client/styles
|
|
7
7
|
*/
|
|
8
|
-
/** Marker attribute set on a seat row once its rewind button is attached. */
|
|
9
|
-
export declare const REWIND_ATTACHED = "data-dsh-rewind-attached";
|
|
10
8
|
/** Class names shared between the injected DOM and the stylesheet. */
|
|
11
9
|
export declare const CLASS: {
|
|
12
10
|
readonly button: 'dsh-rewind-btn';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-rewind-plugin",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
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",
|
|
@@ -54,7 +54,8 @@
|
|
|
54
54
|
"client": {
|
|
55
55
|
"inject": [
|
|
56
56
|
"@deepseek-ai/dsh-client-locale",
|
|
57
|
-
"@deepseek-ai/dsh-client-runtime"
|
|
57
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
58
|
+
"@deepseek-ai/dsh-client-ui-conversation"
|
|
58
59
|
],
|
|
59
60
|
"platform": "web"
|
|
60
61
|
}
|
|
@@ -126,7 +127,11 @@
|
|
|
126
127
|
"@deepseek-ai/dsh-session": "^0.1.0-rc.7",
|
|
127
128
|
"@deepseek-ai/dsh-tools": "^0.1.0-rc.7",
|
|
128
129
|
"@types/node": "^24.0.0",
|
|
130
|
+
"@types/react": "^18.3.31",
|
|
131
|
+
"@types/react-dom": "^18.3.7",
|
|
129
132
|
"esbuild": "^0.28.2",
|
|
133
|
+
"react": "^18.3.1",
|
|
134
|
+
"react-dom": "^18.3.1",
|
|
130
135
|
"typescript": "^7.0.2",
|
|
131
136
|
"vitest": "^4.1.10"
|
|
132
137
|
}
|