dsh-rewind-plugin 0.17.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +191 -159
- package/lib/index.js +67 -71
- package/lib/types/client/candidates.d.ts +2 -9
- package/lib/types/client/hidden.d.ts +8 -24
- package/lib/types/client/host-rpc.d.ts +45 -0
- package/lib/types/client/index.d.ts +3 -3
- package/lib/types/client/popover.d.ts +8 -18
- package/lib/types/client/portals.d.ts +5 -4
- package/lib/types/client/styles.d.ts +1 -1
- package/lib/types/locales.d.ts +2 -10
- package/lib/types/rewind.d.ts +0 -17
- package/lib/types/snapshot.d.ts +1 -1
- package/package.json +6 -1
package/lib/client.js
CHANGED
|
@@ -36,7 +36,8 @@ module.exports = __toCommonJS(index_exports);
|
|
|
36
36
|
|
|
37
37
|
// src/client/hidden.ts
|
|
38
38
|
var ENGINE_COMMAND = "rewind-plugin";
|
|
39
|
-
var
|
|
39
|
+
var LEGACY_ENGINE_COMMAND = "rewind";
|
|
40
|
+
var ENGINE_COMMANDS = /* @__PURE__ */ new Set([ENGINE_COMMAND, LEGACY_ENGINE_COMMAND]);
|
|
40
41
|
function resolveChatWatch(resolveView, sessionId, cb) {
|
|
41
42
|
const view = resolveView(sessionId);
|
|
42
43
|
return view?.subscribe?.(cb) ?? (() => {
|
|
@@ -93,15 +94,6 @@ function isExecutedRewindCommand(node, seq) {
|
|
|
93
94
|
const args = node.args ?? "";
|
|
94
95
|
return new RegExp(`(?:^|\\s)@${seq}(?:\\s|$)`).test(args);
|
|
95
96
|
}
|
|
96
|
-
function hasFileImpact(text) {
|
|
97
|
-
if (text === void 0) return true;
|
|
98
|
-
const match = text.match(/impact=(\d+)/);
|
|
99
|
-
if (match !== null) return Number(match[1]) > 0;
|
|
100
|
-
return false;
|
|
101
|
-
}
|
|
102
|
-
function hasBackupDisabled(text) {
|
|
103
|
-
return text !== void 0 && /(?:^|\n)backup=off(?:\n|$)/.test(text);
|
|
104
|
-
}
|
|
105
97
|
function isPreviewCommand(command) {
|
|
106
98
|
return (command.args ?? "").includes("preview");
|
|
107
99
|
}
|
|
@@ -167,7 +159,10 @@ function addWithdrawnTurnChrome(snap, cuts, hidden) {
|
|
|
167
159
|
hidden.add(chrome.anchorSeq);
|
|
168
160
|
}
|
|
169
161
|
}
|
|
162
|
+
var hiddenSeqsMemo = /* @__PURE__ */ new WeakMap();
|
|
170
163
|
function hiddenSeqsOf(snap) {
|
|
164
|
+
const memo = hiddenSeqsMemo.get(snap);
|
|
165
|
+
if (memo !== void 0) return new Set(memo);
|
|
171
166
|
const hidden = /* @__PURE__ */ new Set();
|
|
172
167
|
const spans = [];
|
|
173
168
|
for (const key of snap.order) {
|
|
@@ -198,6 +193,7 @@ function hiddenSeqsOf(snap) {
|
|
|
198
193
|
addWithdrawnTurnChrome(snap, cuts, hidden);
|
|
199
194
|
} catch {
|
|
200
195
|
}
|
|
196
|
+
hiddenSeqsMemo.set(snap, new Set(hidden));
|
|
201
197
|
return hidden;
|
|
202
198
|
}
|
|
203
199
|
|
|
@@ -233,23 +229,6 @@ function rewindCandidatesOf(snap, hidden, limit = DEFAULT_CANDIDATE_LIMIT) {
|
|
|
233
229
|
function rewindCandidatesOfChat(snap) {
|
|
234
230
|
return rewindCandidatesOf(snap, hiddenSeqsOf(snap));
|
|
235
231
|
}
|
|
236
|
-
var CANDIDATE_LIST_HEADER = "candidates=";
|
|
237
|
-
function rewindCandidatesFromHostText(text) {
|
|
238
|
-
if (!text.startsWith(CANDIDATE_LIST_HEADER)) return [];
|
|
239
|
-
const lines = text.split("\n").slice(1);
|
|
240
|
-
const candidates = [];
|
|
241
|
-
for (const line of lines) {
|
|
242
|
-
if (line === "") continue;
|
|
243
|
-
const parts = line.split(" ");
|
|
244
|
-
if (parts.length !== 3) continue;
|
|
245
|
-
const seq = Number(parts[0]);
|
|
246
|
-
const time = Number(parts[1]);
|
|
247
|
-
const preview = parts[2] ?? "";
|
|
248
|
-
if (!Number.isSafeInteger(seq) || !Number.isFinite(time)) continue;
|
|
249
|
-
candidates.push({ seq, time, preview });
|
|
250
|
-
}
|
|
251
|
-
return candidates;
|
|
252
|
-
}
|
|
253
232
|
function rewindOptionsFromCandidates(candidates, t) {
|
|
254
233
|
return candidates.map((candidate) => ({
|
|
255
234
|
id: String(candidate.seq),
|
|
@@ -258,48 +237,6 @@ function rewindOptionsFromCandidates(candidates, t) {
|
|
|
258
237
|
}));
|
|
259
238
|
}
|
|
260
239
|
|
|
261
|
-
// src/client/log.ts
|
|
262
|
-
var NS = "dsh-rewind";
|
|
263
|
-
var DEBUG_KEY = "dsh-rewind.debug";
|
|
264
|
-
var ALWAYS_ON = /* @__PURE__ */ new Set(["error", "warn"]);
|
|
265
|
-
function switchValue() {
|
|
266
|
-
try {
|
|
267
|
-
return window.localStorage.getItem(DEBUG_KEY) ?? "";
|
|
268
|
-
} catch {
|
|
269
|
-
return "";
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
function matches(value, ns) {
|
|
273
|
-
for (const entry of value.split(",")) {
|
|
274
|
-
const part = entry.trim();
|
|
275
|
-
if (part === "") continue;
|
|
276
|
-
if (part === "*" || part === `${NS}*`) return true;
|
|
277
|
-
if (part.endsWith("*")) {
|
|
278
|
-
if (ns.startsWith(part.slice(0, -1))) return true;
|
|
279
|
-
} else if (ns === part) {
|
|
280
|
-
return true;
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
return false;
|
|
284
|
-
}
|
|
285
|
-
function tag(scope) {
|
|
286
|
-
return `[${NS}:${scope}]`;
|
|
287
|
-
}
|
|
288
|
-
function log(level, scope, message, data) {
|
|
289
|
-
if (ALWAYS_ON.has(level)) {
|
|
290
|
-
console[level](tag(scope), message, data);
|
|
291
|
-
return;
|
|
292
|
-
}
|
|
293
|
-
if (!matches(switchValue(), `${NS}:${scope}`)) return;
|
|
294
|
-
console.info(tag(scope), message, data);
|
|
295
|
-
}
|
|
296
|
-
var rewindLog = {
|
|
297
|
-
error: (scope, message, data) => log("error", scope, message, data),
|
|
298
|
-
warn: (scope, message, data) => log("warn", scope, message, data),
|
|
299
|
-
info: (scope, message, data) => log("info", scope, message, data),
|
|
300
|
-
debug: (scope, message, data) => log("debug", scope, message, data)
|
|
301
|
-
};
|
|
302
|
-
|
|
303
240
|
// src/client/styles.ts
|
|
304
241
|
var CLASS = {
|
|
305
242
|
button: "dsh-rewind-btn",
|
|
@@ -351,10 +288,13 @@ var STYLE = `
|
|
|
351
288
|
z-index: 1000;
|
|
352
289
|
width: 288px;
|
|
353
290
|
padding: 12px;
|
|
354
|
-
border:
|
|
355
|
-
border-radius:
|
|
291
|
+
border: 0;
|
|
292
|
+
border-radius: var(--dsw-radius-md);
|
|
356
293
|
background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));
|
|
357
|
-
|
|
294
|
+
backdrop-filter: var(--dsw-menu-backdrop-filter);
|
|
295
|
+
isolation: isolate;
|
|
296
|
+
--dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
|
|
297
|
+
box-shadow: var(--dsw-elevation-prominent);
|
|
358
298
|
font-size: 14px;
|
|
359
299
|
line-height: 20px;
|
|
360
300
|
color: var(--dsw-alias-label-primary);
|
|
@@ -466,10 +406,13 @@ var STYLE = `
|
|
|
466
406
|
z-index: 1000;
|
|
467
407
|
max-width: min(440px, calc(100vw - 24px));
|
|
468
408
|
padding: 8px 12px;
|
|
469
|
-
border:
|
|
470
|
-
border-radius:
|
|
409
|
+
border: 0;
|
|
410
|
+
border-radius: var(--dsw-radius-md);
|
|
471
411
|
background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));
|
|
472
|
-
|
|
412
|
+
backdrop-filter: var(--dsw-menu-backdrop-filter);
|
|
413
|
+
isolation: isolate;
|
|
414
|
+
--dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
|
|
415
|
+
box-shadow: var(--dsw-elevation-panel);
|
|
473
416
|
font-size: 13px;
|
|
474
417
|
line-height: 18px;
|
|
475
418
|
color: var(--dsw-alias-label-primary);
|
|
@@ -540,15 +483,6 @@ function formatTarget(t, seq, time, preview) {
|
|
|
540
483
|
const previewText = preview.length > 0 ? preview : t("popover.noText");
|
|
541
484
|
return `seq ${seq} \xB7 ${hh}:${mm} \xB7 ${previewText}`;
|
|
542
485
|
}
|
|
543
|
-
function parseImpactList(text) {
|
|
544
|
-
const restores = [];
|
|
545
|
-
const deletes = [];
|
|
546
|
-
for (const line of text.split("\n")) {
|
|
547
|
-
if (line.startsWith("restore:")) restores.push(line.slice("restore:".length));
|
|
548
|
-
else if (line.startsWith("delete:")) deletes.push(line.slice("delete:".length));
|
|
549
|
-
}
|
|
550
|
-
return { restores, deletes };
|
|
551
|
-
}
|
|
552
486
|
function findCommand(chat, match) {
|
|
553
487
|
if (chat === void 0) return void 0;
|
|
554
488
|
let found;
|
|
@@ -595,28 +529,12 @@ function waitForCommand(session, chatOf, match, timeoutMs = 8e3, watch) {
|
|
|
595
529
|
check();
|
|
596
530
|
});
|
|
597
531
|
}
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
532
|
+
var previewCall = null;
|
|
533
|
+
function registerPreviewCall(call) {
|
|
534
|
+
previewCall = call;
|
|
601
535
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
let result;
|
|
605
|
-
try {
|
|
606
|
-
result = await session.command(`/${ENGINE_COMMAND} preview @${seq} both`);
|
|
607
|
-
} catch (error) {
|
|
608
|
-
rewindLog.warn("preview", `preview command threw for @${seq}`, error);
|
|
609
|
-
return { kind: "error", text: error instanceof Error ? error.message : String(error) };
|
|
610
|
-
}
|
|
611
|
-
if (!result.ok) {
|
|
612
|
-
rewindLog.warn("preview", `preview command rejected for @${seq}`, result.error);
|
|
613
|
-
return { kind: "error", text: `${result.error.code}: ${result.error.message}` };
|
|
614
|
-
}
|
|
615
|
-
if (result.value?.matched !== true) {
|
|
616
|
-
rewindLog.warn("preview", `preview command was not matched for @${seq}`);
|
|
617
|
-
return { kind: "error", text: "the rewind command is not registered on this host" };
|
|
618
|
-
}
|
|
619
|
-
return waitForCommand(session, chatOf, (node) => isPreviewFor(node, seq) && !known.has(node.seq), 8e3, watch);
|
|
536
|
+
function previewImpact(sessionId, seq) {
|
|
537
|
+
return previewCall?.(sessionId, seq) ?? Promise.resolve({ kind: "error", text: "host RPC is unavailable" });
|
|
620
538
|
}
|
|
621
539
|
function el(tag2, className, text) {
|
|
622
540
|
const node = document.createElement(tag2);
|
|
@@ -667,33 +585,30 @@ function renderImpactStep(root, opts, back, cached) {
|
|
|
667
585
|
root.replaceChildren(impact, actions);
|
|
668
586
|
focusFirst(root);
|
|
669
587
|
void (async () => {
|
|
670
|
-
const outcome = cached ?? await previewImpact(session
|
|
588
|
+
const outcome = cached ?? await previewImpact(session.sessionId, seq);
|
|
671
589
|
if (outcome === null) {
|
|
672
|
-
impact.textContent = t("popover.impact.failed", { message: "preview
|
|
590
|
+
impact.textContent = t("popover.impact.failed", { message: "preview request timed out" });
|
|
673
591
|
return;
|
|
674
592
|
}
|
|
675
593
|
if (outcome.kind === "error") {
|
|
676
|
-
impact.textContent = t("popover.impact.failed", { message: outcome.text
|
|
594
|
+
impact.textContent = t("popover.impact.failed", { message: outcome.text });
|
|
677
595
|
return;
|
|
678
596
|
}
|
|
679
|
-
|
|
597
|
+
const restores = outcome.files.filter((file) => file.action === "restore").map((file) => file.path);
|
|
598
|
+
const deletes = outcome.files.filter((file) => file.action === "delete").map((file) => file.path);
|
|
599
|
+
if (restores.length === 0 && deletes.length === 0) {
|
|
680
600
|
impact.textContent = t("popover.impact.none");
|
|
681
601
|
} else {
|
|
682
|
-
const
|
|
683
|
-
|
|
684
|
-
|
|
602
|
+
const lines = [
|
|
603
|
+
...restores.map((path) => t("popover.impact.restore", { path })),
|
|
604
|
+
...deletes.map((path) => t("popover.impact.delete", { path }))
|
|
605
|
+
];
|
|
606
|
+
if (outcome.backupOff) {
|
|
607
|
+
const list = document.createElement("div");
|
|
608
|
+
list.textContent = lines.join("\n");
|
|
609
|
+
impact.replaceChildren(list, el("div", CLASS.popoverNote, t("popover.backupDisabled")));
|
|
685
610
|
} else {
|
|
686
|
-
|
|
687
|
-
...restores.map((path) => t("popover.impact.restore", { path })),
|
|
688
|
-
...deletes.map((path) => t("popover.impact.delete", { path }))
|
|
689
|
-
];
|
|
690
|
-
if (hasBackupDisabled(outcome.text)) {
|
|
691
|
-
const list = document.createElement("div");
|
|
692
|
-
list.textContent = lines.join("\n");
|
|
693
|
-
impact.replaceChildren(list, el("div", CLASS.popoverNote, t("popover.backupDisabled")));
|
|
694
|
-
} else {
|
|
695
|
-
impact.textContent = lines.join("\n");
|
|
696
|
-
}
|
|
611
|
+
impact.textContent = lines.join("\n");
|
|
697
612
|
}
|
|
698
613
|
}
|
|
699
614
|
confirm.disabled = false;
|
|
@@ -793,10 +708,10 @@ function openPopover(opts) {
|
|
|
793
708
|
openRetractPopover(opts);
|
|
794
709
|
return;
|
|
795
710
|
}
|
|
796
|
-
const { session, seq, time, preview, anchor, t
|
|
711
|
+
const { session, seq, time, preview, anchor, t } = opts;
|
|
797
712
|
const onRewind = opts.onRewind;
|
|
798
713
|
if (seq === void 0 || time === void 0 || onRewind === void 0) return;
|
|
799
|
-
const durableOpts = { session, seq, time, preview, anchor, t,
|
|
714
|
+
const durableOpts = { session, seq, time, preview, anchor, t, onRewind };
|
|
800
715
|
const root = el("div", CLASS.popover);
|
|
801
716
|
root.setAttribute("role", "dialog");
|
|
802
717
|
root.setAttribute("aria-label", t("popover.title"));
|
|
@@ -866,12 +781,12 @@ function openPopover(opts) {
|
|
|
866
781
|
popoverEl = root;
|
|
867
782
|
disposeOutside = shell.dispose;
|
|
868
783
|
void (async () => {
|
|
869
|
-
const outcome = await previewImpact(session
|
|
784
|
+
const outcome = await previewImpact(session.sessionId, seq);
|
|
870
785
|
impactOutcome = outcome;
|
|
871
786
|
if (outcome !== null && outcome.kind === "success") {
|
|
872
|
-
bothState = { state:
|
|
787
|
+
bothState = { state: outcome.files.length > 0 ? "hasChanges" : "noChanges" };
|
|
873
788
|
} else {
|
|
874
|
-
bothState = { state: "error", message: outcome?.text
|
|
789
|
+
bothState = { state: "error", message: outcome?.kind === "error" ? outcome.text : "preview request timed out" };
|
|
875
790
|
}
|
|
876
791
|
renderModes();
|
|
877
792
|
shell.position();
|
|
@@ -916,11 +831,11 @@ function matchPendingRows(rows, steering) {
|
|
|
916
831
|
for (let i = 0; i < rows.length; i++) {
|
|
917
832
|
const row = rows[i];
|
|
918
833
|
const item = steering[i];
|
|
919
|
-
matched.push(item !== void 0 &&
|
|
834
|
+
matched.push(item !== void 0 && matches(item, row) ? item.id : null);
|
|
920
835
|
}
|
|
921
836
|
return matched;
|
|
922
837
|
}
|
|
923
|
-
function
|
|
838
|
+
function matches(item, row) {
|
|
924
839
|
if (item.attachments > 0) return row.attachments === item.attachments;
|
|
925
840
|
return row.text === (item.text ?? "");
|
|
926
841
|
}
|
|
@@ -930,6 +845,48 @@ function retractSpan(steering, targetId) {
|
|
|
930
845
|
return steering.slice(index).map((item) => item.id);
|
|
931
846
|
}
|
|
932
847
|
|
|
848
|
+
// src/client/log.ts
|
|
849
|
+
var NS = "dsh-rewind";
|
|
850
|
+
var DEBUG_KEY = "dsh-rewind.debug";
|
|
851
|
+
var ALWAYS_ON = /* @__PURE__ */ new Set(["error", "warn"]);
|
|
852
|
+
function switchValue() {
|
|
853
|
+
try {
|
|
854
|
+
return window.localStorage.getItem(DEBUG_KEY) ?? "";
|
|
855
|
+
} catch {
|
|
856
|
+
return "";
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
function matches2(value, ns) {
|
|
860
|
+
for (const entry of value.split(",")) {
|
|
861
|
+
const part = entry.trim();
|
|
862
|
+
if (part === "") continue;
|
|
863
|
+
if (part === "*" || part === `${NS}*`) return true;
|
|
864
|
+
if (part.endsWith("*")) {
|
|
865
|
+
if (ns.startsWith(part.slice(0, -1))) return true;
|
|
866
|
+
} else if (ns === part) {
|
|
867
|
+
return true;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return false;
|
|
871
|
+
}
|
|
872
|
+
function tag(scope) {
|
|
873
|
+
return `[${NS}:${scope}]`;
|
|
874
|
+
}
|
|
875
|
+
function log(level, scope, message, data) {
|
|
876
|
+
if (ALWAYS_ON.has(level)) {
|
|
877
|
+
console[level](tag(scope), message, data);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
if (!matches2(switchValue(), `${NS}:${scope}`)) return;
|
|
881
|
+
console.info(tag(scope), message, data);
|
|
882
|
+
}
|
|
883
|
+
var rewindLog = {
|
|
884
|
+
error: (scope, message, data) => log("error", scope, message, data),
|
|
885
|
+
warn: (scope, message, data) => log("warn", scope, message, data),
|
|
886
|
+
info: (scope, message, data) => log("info", scope, message, data),
|
|
887
|
+
debug: (scope, message, data) => log("debug", scope, message, data)
|
|
888
|
+
};
|
|
889
|
+
|
|
933
890
|
// src/client/portals.tsx
|
|
934
891
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
935
892
|
function fillComposerEditable(text) {
|
|
@@ -1062,6 +1019,13 @@ function showHint(text) {
|
|
|
1062
1019
|
var COMPOSER_EDITABLE_SELECTOR = "[data-composer-input]";
|
|
1063
1020
|
var USER_SEAT_SELECTOR = '[data-chat-flow-kind="user"][data-chat-anchor-key], [data-chat-flow-kind="steering"][data-chat-anchor-key]';
|
|
1064
1021
|
var CHAT_SEAT_SELECTOR = "[data-chat-anchor-key]";
|
|
1022
|
+
var IGNORED_PORTAL_SCOPE = [
|
|
1023
|
+
"[data-composer-seat]",
|
|
1024
|
+
"[data-composer-input]",
|
|
1025
|
+
"[data-sidebar-right-panel]",
|
|
1026
|
+
"[data-sidebar-chat]",
|
|
1027
|
+
"[data-plugin-panel]"
|
|
1028
|
+
].join(", ");
|
|
1065
1029
|
var GROUP_SHELL_SELECTOR = '[data-chat-anchor-key^="group:"]';
|
|
1066
1030
|
function resolveSeatAnchorSeq(element, chat) {
|
|
1067
1031
|
if (chat === void 0) return void 0;
|
|
@@ -1214,12 +1178,14 @@ function RewindPortals({ sessionId, sessionOf, chatOf, isMainViewSession, watchC
|
|
|
1214
1178
|
const chat = chatOf(session);
|
|
1215
1179
|
const hiddenSeqs = chat === void 0 ? /* @__PURE__ */ new Set() : hiddenSeqsOf(chat);
|
|
1216
1180
|
for (const element of hidden.current) if (!element.isConnected) hidden.current.delete(element);
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1181
|
+
if (hiddenSeqs.size > 0 || hidden.current.size > 0) {
|
|
1182
|
+
hideWithdrawnSeats(
|
|
1183
|
+
chat,
|
|
1184
|
+
chat === void 0 ? [] : document.querySelectorAll(CHAT_SEAT_SELECTOR),
|
|
1185
|
+
hiddenSeqs,
|
|
1186
|
+
hidden.current
|
|
1187
|
+
);
|
|
1188
|
+
}
|
|
1223
1189
|
const durable = collectDurableTargets(snapshot, chat, hiddenSeqs);
|
|
1224
1190
|
const next = [...durable, ...collectPendingTargets(snapshot, steeringItemsOf(inboxOf(session)?.["next-step"]))];
|
|
1225
1191
|
setTargets((current) => sameTargets(current, next) ? current : next);
|
|
@@ -1245,17 +1211,34 @@ function RewindPortals({ sessionId, sessionOf, chatOf, isMainViewSession, watchC
|
|
|
1245
1211
|
}
|
|
1246
1212
|
};
|
|
1247
1213
|
refresh();
|
|
1248
|
-
const
|
|
1214
|
+
const unsubscribeChat = watchChat(sessionId, queueRefresh);
|
|
1215
|
+
const observer = new MutationObserver((records) => {
|
|
1216
|
+
for (const record of records) {
|
|
1217
|
+
if (record.type === "attributes") {
|
|
1218
|
+
const target = record.target;
|
|
1219
|
+
if (target instanceof HTMLElement && hidden.current.has(target) && target.style.display !== "none") {
|
|
1220
|
+
queueRefresh();
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1225
|
+
const node = record.target instanceof Element ? record.target : record.target.parentElement;
|
|
1226
|
+
if (node !== null && node.closest(IGNORED_PORTAL_SCOPE) !== null) continue;
|
|
1227
|
+
queueRefresh();
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
});
|
|
1249
1231
|
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["style"] });
|
|
1250
1232
|
return () => {
|
|
1251
1233
|
active = false;
|
|
1252
1234
|
observer.disconnect();
|
|
1235
|
+
unsubscribeChat();
|
|
1253
1236
|
showHiddenSeats(hidden.current);
|
|
1254
1237
|
unsubscribeInbox?.();
|
|
1255
1238
|
unsubscribeInbox = void 0;
|
|
1256
1239
|
inboxOwner = void 0;
|
|
1257
1240
|
};
|
|
1258
|
-
}, [sessionId, sessionOf]);
|
|
1241
|
+
}, [sessionId, sessionOf, watchChat]);
|
|
1259
1242
|
return targets.map((target) => (0, import_react_dom.createPortal)(
|
|
1260
1243
|
target.kind === "pending" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1261
1244
|
RetractButton,
|
|
@@ -1263,8 +1246,6 @@ function RewindPortals({ sessionId, sessionOf, chatOf, isMainViewSession, watchC
|
|
|
1263
1246
|
target,
|
|
1264
1247
|
sessionId,
|
|
1265
1248
|
sessionOf,
|
|
1266
|
-
chatOf,
|
|
1267
|
-
watchChat,
|
|
1268
1249
|
setComposerText,
|
|
1269
1250
|
t
|
|
1270
1251
|
},
|
|
@@ -1300,8 +1281,6 @@ function RewindButton({ target, sessionId, sessionOf, chatOf, watchChat, isMainV
|
|
|
1300
1281
|
if (node === void 0) return;
|
|
1301
1282
|
openPopover({
|
|
1302
1283
|
session,
|
|
1303
|
-
chatOf,
|
|
1304
|
-
watchChat,
|
|
1305
1284
|
seq: node.seq,
|
|
1306
1285
|
time: node.time,
|
|
1307
1286
|
preview: messagePreviewOf(node),
|
|
@@ -1348,15 +1327,13 @@ async function retractPending(session, itemId, text, setComposerText) {
|
|
|
1348
1327
|
setComposerText(session.sessionId, text);
|
|
1349
1328
|
}
|
|
1350
1329
|
}
|
|
1351
|
-
function RetractButton({ target, sessionId, sessionOf,
|
|
1330
|
+
function RetractButton({ target, sessionId, sessionOf, setComposerText, t }) {
|
|
1352
1331
|
const onClick = (event) => {
|
|
1353
1332
|
event.stopPropagation();
|
|
1354
1333
|
const session = sessionOf(sessionId);
|
|
1355
1334
|
if (session === void 0) return;
|
|
1356
1335
|
openPopover({
|
|
1357
1336
|
session,
|
|
1358
|
-
chatOf,
|
|
1359
|
-
watchChat,
|
|
1360
1337
|
preview: target.preview,
|
|
1361
1338
|
anchor: event.currentTarget,
|
|
1362
1339
|
t,
|
|
@@ -1384,10 +1361,69 @@ function createRewindBridge(deps) {
|
|
|
1384
1361
|
};
|
|
1385
1362
|
}
|
|
1386
1363
|
|
|
1364
|
+
// src/client/host-rpc.ts
|
|
1365
|
+
var CHANNEL = "/dsh-rewind";
|
|
1366
|
+
var TIMEOUT_MS = 8e3;
|
|
1367
|
+
function isRecord(value) {
|
|
1368
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1369
|
+
}
|
|
1370
|
+
function isImpact(value) {
|
|
1371
|
+
return isRecord(value) && typeof value["path"] === "string" && (value["action"] === "restore" || value["action"] === "delete");
|
|
1372
|
+
}
|
|
1373
|
+
function isCandidate(value) {
|
|
1374
|
+
return isRecord(value) && Number.isSafeInteger(value["seq"]) && typeof value["time"] === "number" && Number.isFinite(value["time"]) && typeof value["preview"] === "string";
|
|
1375
|
+
}
|
|
1376
|
+
function isAbort(error) {
|
|
1377
|
+
const name2 = error?.name;
|
|
1378
|
+
return name2 === "AbortError" || name2 === "TimeoutError";
|
|
1379
|
+
}
|
|
1380
|
+
async function previewOverRpc(connection, sessionId, seq) {
|
|
1381
|
+
if (connection === void 0) return { kind: "error", text: "host RPC is unavailable" };
|
|
1382
|
+
try {
|
|
1383
|
+
const result = await connection.rpc.call(CHANNEL, "preview", { sessionId, seq }, AbortSignal.timeout(TIMEOUT_MS));
|
|
1384
|
+
if (!result.ok) {
|
|
1385
|
+
rewindLog.warn("preview", `RPC preview failed for @${seq}`, result.error);
|
|
1386
|
+
return { kind: "error", text: result.error.message };
|
|
1387
|
+
}
|
|
1388
|
+
const value = result.value;
|
|
1389
|
+
if (!isRecord(value) || typeof value["backupOff"] !== "boolean" || !Array.isArray(value["files"]) || !value["files"].every(isImpact)) {
|
|
1390
|
+
rewindLog.warn("preview", `RPC preview returned an invalid result for @${seq}`);
|
|
1391
|
+
return { kind: "error", text: "invalid preview response" };
|
|
1392
|
+
}
|
|
1393
|
+
return { kind: "success", files: value["files"], backupOff: value["backupOff"] };
|
|
1394
|
+
} catch (error) {
|
|
1395
|
+
if (isAbort(error)) {
|
|
1396
|
+
rewindLog.warn("preview", `RPC preview timed out for @${seq}`);
|
|
1397
|
+
return null;
|
|
1398
|
+
}
|
|
1399
|
+
rewindLog.warn("preview", `RPC preview threw for @${seq}`, error);
|
|
1400
|
+
return { kind: "error", text: error instanceof Error ? error.message : String(error) };
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
async function candidatesOverRpc(connection, sessionId) {
|
|
1404
|
+
if (connection === void 0) return void 0;
|
|
1405
|
+
try {
|
|
1406
|
+
const result = await connection.rpc.call(CHANNEL, "candidates", { sessionId }, AbortSignal.timeout(TIMEOUT_MS));
|
|
1407
|
+
if (!result.ok) {
|
|
1408
|
+
rewindLog.warn("preview", "RPC candidates failed", result.error);
|
|
1409
|
+
return void 0;
|
|
1410
|
+
}
|
|
1411
|
+
const value = result.value;
|
|
1412
|
+
if (!isRecord(value) || !Array.isArray(value["candidates"]) || !value["candidates"].every(isCandidate)) {
|
|
1413
|
+
rewindLog.warn("preview", "RPC candidates returned an invalid result");
|
|
1414
|
+
return void 0;
|
|
1415
|
+
}
|
|
1416
|
+
return value["candidates"];
|
|
1417
|
+
} catch (error) {
|
|
1418
|
+
rewindLog.warn("preview", "RPC candidates threw", error);
|
|
1419
|
+
return void 0;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1387
1423
|
// src/client/build-info.ts
|
|
1388
|
-
var PLUGIN_VERSION = true ? "0.
|
|
1424
|
+
var PLUGIN_VERSION = true ? "0.18.0" : "dev";
|
|
1389
1425
|
var PLUGIN_PACKAGE = "dsh-rewind-plugin";
|
|
1390
|
-
var BUILD_HASH = true ? "
|
|
1426
|
+
var BUILD_HASH = true ? "18b65dfa" : "dev";
|
|
1391
1427
|
|
|
1392
1428
|
// src/client/locales.ts
|
|
1393
1429
|
var zh = {
|
|
@@ -1599,7 +1635,7 @@ function SettingsCard(props) {
|
|
|
1599
1635
|
|
|
1600
1636
|
// src/client/index.ts
|
|
1601
1637
|
var name = "dsh-rewind";
|
|
1602
|
-
var inject = ["slots", "sessions", "locale", "commandUi", "configForms"];
|
|
1638
|
+
var inject = ["slots", "sessions", "locale", "commandUi", "configForms", "connection"];
|
|
1603
1639
|
var NS2 = "rewind";
|
|
1604
1640
|
var HEADER_ACTIONS_SLOT = "conversation.session.header.actions";
|
|
1605
1641
|
var COMPOSER_EDITABLE_SELECTOR2 = "[data-composer-input]";
|
|
@@ -1608,6 +1644,11 @@ function apply(ctx) {
|
|
|
1608
1644
|
ctx.effect(function* () {
|
|
1609
1645
|
yield ctx.locale.register(NS2, { zh, en });
|
|
1610
1646
|
const t = ctx.locale.bind(NS2);
|
|
1647
|
+
const connection = ctx.get("connection");
|
|
1648
|
+
registerPreviewCall((sessionId, seq) => previewOverRpc(connection, sessionId, seq));
|
|
1649
|
+
yield () => {
|
|
1650
|
+
registerPreviewCall(null);
|
|
1651
|
+
};
|
|
1611
1652
|
const style = document.createElement("style");
|
|
1612
1653
|
style.dataset.plugin = PLUGIN_PACKAGE;
|
|
1613
1654
|
style.textContent = STYLE;
|
|
@@ -1746,14 +1787,7 @@ function apply(ctx) {
|
|
|
1746
1787
|
const chat = chatOf(face);
|
|
1747
1788
|
return chat !== void 0 && rewindCandidatesOfChat(chat).length > 0;
|
|
1748
1789
|
};
|
|
1749
|
-
const fetchHostCandidates = async (face
|
|
1750
|
-
const known = knownCommandSeqs(face, chatOf2, (node) => isCandidateCommand(node));
|
|
1751
|
-
const result = await face.command(`/${ENGINE_COMMAND} __candidates`);
|
|
1752
|
-
if (!result.ok || result.value?.matched !== true) return void 0;
|
|
1753
|
-
const outcome = await waitForCommand(face, chatOf2, (node) => isCandidateCommand(node) && !known.has(node.seq), 8e3, (cb) => watchChat(face.sessionId, cb));
|
|
1754
|
-
if (outcome === null || outcome.kind !== "success" || outcome.text === void 0) return void 0;
|
|
1755
|
-
return rewindCandidatesFromHostText(outcome.text);
|
|
1756
|
-
};
|
|
1790
|
+
const fetchHostCandidates = async (face) => candidatesOverRpc(connection, face.sessionId);
|
|
1757
1791
|
const hostCandidatesCache = /* @__PURE__ */ new Map();
|
|
1758
1792
|
const composerAnchor = () => {
|
|
1759
1793
|
const surface = composerSurface2();
|
|
@@ -1771,7 +1805,7 @@ function apply(ctx) {
|
|
|
1771
1805
|
options: async (session) => {
|
|
1772
1806
|
const face = sessionOf(session.sessionId);
|
|
1773
1807
|
if (face === void 0) return [];
|
|
1774
|
-
const candidates = await fetchHostCandidates(face
|
|
1808
|
+
const candidates = await fetchHostCandidates(face);
|
|
1775
1809
|
if (candidates !== void 0) hostCandidatesCache.set(session.sessionId, candidates);
|
|
1776
1810
|
return candidates === void 0 ? [] : rewindOptionsFromCandidates(candidates, t);
|
|
1777
1811
|
},
|
|
@@ -1784,8 +1818,6 @@ function apply(ctx) {
|
|
|
1784
1818
|
if (candidate === void 0) return;
|
|
1785
1819
|
openPopover({
|
|
1786
1820
|
session: face,
|
|
1787
|
-
chatOf,
|
|
1788
|
-
watchChat,
|
|
1789
1821
|
seq: candidate.seq,
|
|
1790
1822
|
time: candidate.time,
|
|
1791
1823
|
preview: candidate.preview,
|
package/lib/index.js
CHANGED
|
@@ -9,15 +9,8 @@ function preferredLocale(preference) {
|
|
|
9
9
|
return preference.toLowerCase().split("-")[0] === "zh" ? "zh" : "en";
|
|
10
10
|
}
|
|
11
11
|
var en = {
|
|
12
|
-
"plan.rewinding": "Rewind to seq {targetSeq}, removing {count} node(s) from the model context (conversation log kept).",
|
|
13
|
-
"plan.affects": "Affects {count} file(s):",
|
|
14
|
-
"plan.restore": "restore {path}",
|
|
15
|
-
"plan.delete": "delete {path}",
|
|
16
|
-
"plan.noChanges": "No restorable changes after the target.",
|
|
17
|
-
"plan.backupOff": "Snapshot backups are off; this restore may be incomplete.",
|
|
18
12
|
"plan.skip": " not restored: {path} ({reason})",
|
|
19
13
|
"skipReason.oversized": "over the {limit} backup limit, so the recorded state predates untracked edits",
|
|
20
|
-
"uncovered.header": "{count} tracked file(s) exceed the {limit} backup limit and are not covered:",
|
|
21
14
|
"error.invalidTarget": 'Cannot parse target "{raw}" (expected <index> or @<seq>)',
|
|
22
15
|
"sep.list": ", ",
|
|
23
16
|
"sep.clause": "; ",
|
|
@@ -62,15 +55,8 @@ var en = {
|
|
|
62
55
|
"cleanup.usage": "Usage:\n /snapshot-auto-cleanup show status\n /snapshot-auto-cleanup on|off enable/disable auto-cleanup\n /snapshot-auto-cleanup max-age <days> set the idle cutoff\n /snapshot-auto-cleanup run [--apply] dry-run, or execute with --apply\n /snapshot-auto-cleanup run --current [--apply] dry-run/clear this session's snapshots"
|
|
63
56
|
};
|
|
64
57
|
var zh = {
|
|
65
|
-
"plan.rewinding": "\u5C06\u56DE\u9000\u5230 seq {targetSeq}\uFF0C\u4ECE\u6A21\u578B\u4E0A\u4E0B\u6587\u79FB\u9664 {count} \u4E2A\u8282\u70B9\uFF08\u5BF9\u8BDD\u65E5\u5FD7\u4FDD\u7559\uFF09\u3002",
|
|
66
|
-
"plan.affects": "\u5C06\u5F71\u54CD {count} \u4E2A\u6587\u4EF6\uFF1A",
|
|
67
|
-
"plan.restore": "\u8FD8\u539F {path}",
|
|
68
|
-
"plan.delete": "\u5220\u9664 {path}",
|
|
69
|
-
"plan.noChanges": "\u76EE\u6807\u4E4B\u540E\u6CA1\u6709\u9700\u8981\u8FD8\u539F\u7684\u53D8\u66F4\u3002",
|
|
70
|
-
"plan.backupOff": "\u5FEB\u7167\u5907\u4EFD\u5DF2\u5173\u95ED\uFF1B\u672C\u6B21\u8FD8\u539F\u53EF\u80FD\u4E0D\u5145\u5206\u3002",
|
|
71
58
|
"plan.skip": " \u672A\u8FD8\u539F\uFF1A{path}\uFF08{reason}\uFF09",
|
|
72
59
|
"skipReason.oversized": "\u5DF2\u8D85\u8FC7 {limit} \u7684\u5907\u4EFD\u4E0A\u9650\uFF0C\u8BB0\u5F55\u7684\u72B6\u6001\u65E9\u4E8E\u672A\u88AB\u8DDF\u8E2A\u7684\u4FEE\u6539",
|
|
73
|
-
"uncovered.header": "{count} \u4E2A\u5DF2\u8DDF\u8E2A\u6587\u4EF6\u8D85\u8FC7 {limit} \u7684\u5907\u4EFD\u4E0A\u9650\uFF0C\u672A\u7EB3\u5165\u8BB0\u5F55\uFF1A",
|
|
74
60
|
"error.invalidTarget": '\u65E0\u6CD5\u89E3\u6790\u76EE\u6807 "{raw}"\uFF08\u5E94\u4E3A <\u5E8F\u53F7> \u6216 @<seq>\uFF09',
|
|
75
61
|
"sep.list": "\u3001",
|
|
76
62
|
"sep.clause": "\uFF1B",
|
|
@@ -182,14 +168,6 @@ function listRewindCandidates(events, surface, limit = DEFAULT_CANDIDATE_LIMIT)
|
|
|
182
168
|
}
|
|
183
169
|
return candidates;
|
|
184
170
|
}
|
|
185
|
-
var CANDIDATE_LIST_HEADER = "candidates=";
|
|
186
|
-
function formatCandidateList(candidates) {
|
|
187
|
-
const lines = [`${CANDIDATE_LIST_HEADER}${candidates.length}`];
|
|
188
|
-
for (const candidate of candidates) {
|
|
189
|
-
lines.push(`${candidate.seq} ${candidate.time} ${candidate.preview}`);
|
|
190
|
-
}
|
|
191
|
-
return lines.join("\n");
|
|
192
|
-
}
|
|
193
171
|
function planRewind(events, surface, target) {
|
|
194
172
|
let targetSeq;
|
|
195
173
|
if (target.kind === "seq") {
|
|
@@ -2319,6 +2297,7 @@ function shouldRunAutoSweep(lastAtMs, nowMs, intervalMs = AUTO_SWEEP_INTERVAL_MS
|
|
|
2319
2297
|
var name = "dsh-rewind";
|
|
2320
2298
|
var inject = ["commands", "tools", "settings"];
|
|
2321
2299
|
var ENGINE_COMMAND = "rewind-plugin";
|
|
2300
|
+
var REWIND_RPC_CHANNEL = "/dsh-rewind";
|
|
2322
2301
|
var DEFAULT_FILE_CHECKPOINTING_ENABLED = true;
|
|
2323
2302
|
var Config = z.object({
|
|
2324
2303
|
snapshotDir: z.string().required(false),
|
|
@@ -2357,7 +2336,7 @@ function t(key, params) {
|
|
|
2357
2336
|
function warnCommandUnavailable(ctx, name2, error) {
|
|
2358
2337
|
ctx.logger.warn(`[dsh-rewind] command "${name2}" was not registered: ${error instanceof Error ? error.message : String(error)}`);
|
|
2359
2338
|
if (name2 === ENGINE_COMMAND) {
|
|
2360
|
-
ctx.logger.warn(`[dsh-rewind] the internal "${ENGINE_COMMAND}" engine channel is unavailable;
|
|
2339
|
+
ctx.logger.warn(`[dsh-rewind] the internal "${ENGINE_COMMAND}" engine channel is unavailable; rewind execution will not work.`);
|
|
2361
2340
|
}
|
|
2362
2341
|
}
|
|
2363
2342
|
async function discardCapture(capture) {
|
|
@@ -2489,31 +2468,6 @@ function formatBytes(bytes) {
|
|
|
2489
2468
|
if (bytes >= 1024) return `${Math.round(bytes / 1024)} KiB`;
|
|
2490
2469
|
return `${bytes} B`;
|
|
2491
2470
|
}
|
|
2492
|
-
function formatPlan(plan, files, uncovered = [], maxFileBytes = 0, checkpointingEnabled = true) {
|
|
2493
|
-
const lines = [
|
|
2494
|
-
t("plan.rewinding", { targetSeq: plan.targetSeq, count: plan.shadowedSeqs.length })
|
|
2495
|
-
];
|
|
2496
|
-
if (files.length > 0) {
|
|
2497
|
-
lines.push(t("plan.affects", { count: files.length }));
|
|
2498
|
-
for (const file of files) {
|
|
2499
|
-
lines.push(` ${file.action === "restore" ? t("plan.restore", { path: file.path }) : t("plan.delete", { path: file.path })}`);
|
|
2500
|
-
}
|
|
2501
|
-
} else {
|
|
2502
|
-
lines.push(t("plan.noChanges"));
|
|
2503
|
-
}
|
|
2504
|
-
if (uncovered.length > 0) {
|
|
2505
|
-
lines.push(t("uncovered.header", { count: uncovered.length, limit: formatBytes(maxFileBytes) }));
|
|
2506
|
-
for (const file of uncovered.slice(0, 5)) lines.push(` ${file.path}`);
|
|
2507
|
-
if (uncovered.length > 5) lines.push(" \u2026");
|
|
2508
|
-
}
|
|
2509
|
-
if (!checkpointingEnabled) lines.push(t("plan.backupOff"));
|
|
2510
|
-
lines.push(`impact=${files.length}`);
|
|
2511
|
-
for (const file of files) {
|
|
2512
|
-
lines.push(`${file.action}:${file.path}`);
|
|
2513
|
-
}
|
|
2514
|
-
if (!checkpointingEnabled) lines.push("backup=off");
|
|
2515
|
-
return lines.join("\n");
|
|
2516
|
-
}
|
|
2517
2471
|
function resolveOrError(events, surface, raw) {
|
|
2518
2472
|
const target = parseRewindTarget(raw);
|
|
2519
2473
|
if (target === void 0) {
|
|
@@ -2521,6 +2475,54 @@ function resolveOrError(events, surface, raw) {
|
|
|
2521
2475
|
}
|
|
2522
2476
|
return planRewind(events, surface, target);
|
|
2523
2477
|
}
|
|
2478
|
+
async function previewImpact(store, session, seq, checkpointingEnabled) {
|
|
2479
|
+
const plan = resolveOrError(session.snapshotEvents(), session.surface.nodes, `@${seq}`);
|
|
2480
|
+
let impacts;
|
|
2481
|
+
try {
|
|
2482
|
+
impacts = await store.impactsAfter(session.id, plan.targetSeq);
|
|
2483
|
+
} catch (error) {
|
|
2484
|
+
if (!(error instanceof UnknownStoreVersionError)) throw error;
|
|
2485
|
+
throw new Error(t("storeUnsupported", { version: await store.readStoreVersion(session.id) ?? 0 }));
|
|
2486
|
+
}
|
|
2487
|
+
const uncovered = await store.uncoveredPaths(session.id).catch(() => []);
|
|
2488
|
+
return { files: impacts, uncovered, backupOff: !checkpointingEnabled };
|
|
2489
|
+
}
|
|
2490
|
+
function isRecord(value) {
|
|
2491
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2492
|
+
}
|
|
2493
|
+
function rpcFailure(code, message) {
|
|
2494
|
+
return { ok: false, error: { code, message, details: {} } };
|
|
2495
|
+
}
|
|
2496
|
+
function createRewindRpcHandler(store, agentsOf, checkpointingEnabled) {
|
|
2497
|
+
return async (endpoint, payload) => {
|
|
2498
|
+
try {
|
|
2499
|
+
if (!isRecord(payload)) return rpcFailure("rewind/bad-request", "invalid payload");
|
|
2500
|
+
const sessionId = payload["sessionId"];
|
|
2501
|
+
if (typeof sessionId !== "string" || sessionId.length === 0) {
|
|
2502
|
+
return rpcFailure("rewind/bad-request", "sessionId must be a non-empty string");
|
|
2503
|
+
}
|
|
2504
|
+
const agents = agentsOf();
|
|
2505
|
+
if (agents === void 0) return rpcFailure("rewind/agents-unavailable", "agent registry is unavailable");
|
|
2506
|
+
const agent = agents.get(sessionId);
|
|
2507
|
+
if (agent === void 0) return rpcFailure("rewind/session-not-found", `session ${sessionId} is not attached`);
|
|
2508
|
+
const session = agent.session;
|
|
2509
|
+
if (endpoint === "preview") {
|
|
2510
|
+
const seq = payload["seq"];
|
|
2511
|
+
if (!Number.isSafeInteger(seq)) return rpcFailure("rewind/bad-request", "seq must be an integer");
|
|
2512
|
+
return { ok: true, value: await previewImpact(store, session, seq, checkpointingEnabled()) };
|
|
2513
|
+
}
|
|
2514
|
+
if (endpoint === "candidates") {
|
|
2515
|
+
return {
|
|
2516
|
+
ok: true,
|
|
2517
|
+
value: { candidates: listRewindCandidates(session.snapshotEvents(), session.surface.nodes) }
|
|
2518
|
+
};
|
|
2519
|
+
}
|
|
2520
|
+
return rpcFailure("rewind/unknown-endpoint", `unknown endpoint: ${endpoint}`);
|
|
2521
|
+
} catch (error) {
|
|
2522
|
+
return rpcFailure("rewind/failed", error instanceof Error ? error.message : String(error));
|
|
2523
|
+
}
|
|
2524
|
+
};
|
|
2525
|
+
}
|
|
2524
2526
|
function renderFailures(failed) {
|
|
2525
2527
|
if (failed.length === 0) return "";
|
|
2526
2528
|
return t("failures.suffix", {
|
|
@@ -2678,29 +2680,6 @@ async function handleRewind(ctx, store, fs, invocation, inflight, checkpointingE
|
|
|
2678
2680
|
return executeRewind(ctx, store, fs, invocation, `@${candidates[0].seq}`, "chat", inflight, checkpointingEnabled);
|
|
2679
2681
|
}
|
|
2680
2682
|
const parts = input.split(/\s+/);
|
|
2681
|
-
if (parts[0] === "preview") {
|
|
2682
|
-
const target2 = parts[1];
|
|
2683
|
-
if (target2 === void 0) return { kind: "error", text: t("engine.rejected") };
|
|
2684
|
-
let plan;
|
|
2685
|
-
try {
|
|
2686
|
-
plan = resolveOrError(session.snapshotEvents(), session.surface.nodes, target2);
|
|
2687
|
-
} catch (error) {
|
|
2688
|
-
return rewindErrorResult(error);
|
|
2689
|
-
}
|
|
2690
|
-
const impacts = await store.impactsAfter(session.id, plan.targetSeq).catch((error) => {
|
|
2691
|
-
if (error instanceof UnknownStoreVersionError) return void 0;
|
|
2692
|
-
throw error;
|
|
2693
|
-
});
|
|
2694
|
-
if (impacts === void 0) {
|
|
2695
|
-
return { kind: "error", text: t("storeUnsupported", { version: await store.readStoreVersion(session.id) ?? 0 }) };
|
|
2696
|
-
}
|
|
2697
|
-
const uncovered = await store.uncoveredPaths(session.id).catch(() => []);
|
|
2698
|
-
return { kind: "success", text: formatPlan(plan, impacts, uncovered, store.fileCapBytes, checkpointingEnabled) };
|
|
2699
|
-
}
|
|
2700
|
-
if (parts[0] === "__candidates") {
|
|
2701
|
-
const candidates = listRewindCandidates(session.snapshotEvents(), session.surface.nodes);
|
|
2702
|
-
return { kind: "success", text: formatCandidateList(candidates) };
|
|
2703
|
-
}
|
|
2704
2683
|
const target = parts[0];
|
|
2705
2684
|
const mode = parts[1];
|
|
2706
2685
|
if (mode !== void 0 && mode !== "chat" && mode !== "both") {
|
|
@@ -2978,6 +2957,23 @@ function apply(ctx, config) {
|
|
|
2978
2957
|
}
|
|
2979
2958
|
})();
|
|
2980
2959
|
}, { global: true });
|
|
2960
|
+
ctx.inject(["connection"], (scope) => {
|
|
2961
|
+
try {
|
|
2962
|
+
const connection = scope.root.connection;
|
|
2963
|
+
if (connection === void 0) return;
|
|
2964
|
+
const dispose = connection.rpc.handle(
|
|
2965
|
+
REWIND_RPC_CHANNEL,
|
|
2966
|
+
createRewindRpcHandler(
|
|
2967
|
+
store,
|
|
2968
|
+
() => scope.get("agents"),
|
|
2969
|
+
() => isFileCheckpointingEnabled(config)
|
|
2970
|
+
)
|
|
2971
|
+
);
|
|
2972
|
+
scope.effect(() => dispose, "dsh-rewind RPC channel");
|
|
2973
|
+
} catch (error) {
|
|
2974
|
+
ctx.logger.warn(`[dsh-rewind] RPC channel registration failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
2975
|
+
}
|
|
2976
|
+
});
|
|
2981
2977
|
ctx.inject(["fs", "workingDirectory"], (scope) => {
|
|
2982
2978
|
const fs = scope.fs;
|
|
2983
2979
|
const workingDirectory = scope.workingDirectory;
|
|
@@ -75,15 +75,8 @@ export declare function rewindCandidatesOf(snap: CandidateChat, hidden: Readonly
|
|
|
75
75
|
/** The candidates of a live chat snapshot, withdrawn rows already excluded. */
|
|
76
76
|
export declare function rewindCandidatesOfChat(snap: CandidateChat): RewindCandidate[];
|
|
77
77
|
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* missing/zero header yields an empty list.
|
|
81
|
-
*/
|
|
82
|
-
export declare function rewindCandidatesFromHostText(text: string): RewindCandidate[];
|
|
83
|
-
/**
|
|
84
|
-
* Map typed candidates to popupSelect rows (the host-derived path). The
|
|
85
|
-
* popupSelect sources its options from the FULL host surface via the
|
|
86
|
-
* `__candidates` channel instead of the windowed chat snapshot.
|
|
78
|
+
* Map typed candidates to popupSelect rows. The list comes from the host over
|
|
79
|
+
* RPC, so it covers the FULL host surface rather than the windowed chat snapshot.
|
|
87
80
|
*/
|
|
88
81
|
export declare function rewindOptionsFromCandidates(candidates: readonly RewindCandidate[], t: Translate): SelectOption[];
|
|
89
82
|
export {};
|
|
@@ -11,11 +11,9 @@ import type { CommandNode } from '@deepseek-ai/dsh-client-ui-conversation/client
|
|
|
11
11
|
* `ENGINE_COMMAND` in `src/index.ts` — the two bundles share no module.
|
|
12
12
|
*/
|
|
13
13
|
export declare const ENGINE_COMMAND = "rewind-plugin";
|
|
14
|
-
/**
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
* spans (forward compatibility only: an older client cannot know the new name).
|
|
18
|
-
*/
|
|
14
|
+
/** Legacy engine command name. */
|
|
15
|
+
export declare const LEGACY_ENGINE_COMMAND = "rewind";
|
|
16
|
+
/** Engine command names the client recognizes. */
|
|
19
17
|
export declare const ENGINE_COMMANDS: ReadonlySet<string>;
|
|
20
18
|
/** A chat-snapshot view node as the hiding / composer-refill logic reads it.
|
|
21
19
|
* The harness's view node shape plus the `anchorSeq` the chat snapshot carries
|
|
@@ -121,7 +119,7 @@ export interface MessageImageRef {
|
|
|
121
119
|
export declare function messageAttachmentsAt(chat: HiddenChat | undefined, seq: number): readonly MessageImageRef[] | undefined;
|
|
122
120
|
/**
|
|
123
121
|
* Extract the rewind target seq from a `/rewind` command's structured `args`
|
|
124
|
-
* (e.g. `@5 chat
|
|
122
|
+
* (e.g. `@5 chat`). Locale-independent — never parses the
|
|
125
123
|
* host's human outcome copy.
|
|
126
124
|
*/
|
|
127
125
|
export declare function targetSeqOfArgs(args: string | null | undefined): number | undefined;
|
|
@@ -134,28 +132,14 @@ export declare function targetSeqOfArgs(args: string | null | undefined): number
|
|
|
134
132
|
*/
|
|
135
133
|
export declare function isExecutedRewindCommand(node: CommandNode, seq: number): boolean;
|
|
136
134
|
/**
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* Reads ONLY the machine-readable `impact=<n>` trailer the host appends to
|
|
142
|
-
* preview text. Older host output without the trailer is treated as having no
|
|
143
|
-
* changes (never guesses from human copy). Absent text (undefined) degrades to
|
|
144
|
-
* always-show so a working option is never hidden on a failed probe.
|
|
145
|
-
*/
|
|
146
|
-
export declare function hasFileImpact(text: string | undefined): boolean;
|
|
147
|
-
/** Whether the preview reports that new file checkpoints are disabled. */
|
|
148
|
-
export declare function hasBackupDisabled(text: string | undefined): boolean;
|
|
149
|
-
/**
|
|
150
|
-
* True when an engine command node is the internal candidate-list probe
|
|
151
|
-
* (`__candidates`) the popupSelect runs to fetch the FULL candidate
|
|
152
|
-
* list from the host. Like previews, its flow node never surfaces in the
|
|
153
|
-
* transcript — it only feeds the popup — so it is hidden in every state.
|
|
135
|
+
* True when an engine command node is a legacy candidate-list probe
|
|
136
|
+
* (`__candidates`). New candidate lists travel over RPC; this only keeps
|
|
137
|
+
* probe rows in historical sessions hidden.
|
|
154
138
|
*/
|
|
155
139
|
export declare function isCandidateCommand(command: CommandNode): boolean;
|
|
156
140
|
/**
|
|
157
141
|
* Anchor seqs that must be hidden from the rendered transcript so the user sees
|
|
158
|
-
* the conversation as the agent sees it: every
|
|
142
|
+
* the conversation as the agent sees it: every legacy probe flow node (it only
|
|
159
143
|
* exists to feed the popover) and every SUCCESSFUL executed `/rewind` command
|
|
160
144
|
* row, plus every message a rewind withdrew — the target, everything after it,
|
|
161
145
|
* and the (unrendered) marker.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browser half of the rewind RPC channel: the mode popover's impact
|
|
3
|
+
* preview and the `/rewind` candidate list. Both are read-only host queries,
|
|
4
|
+
* so they travel over Connection RPC instead of the human command channel.
|
|
5
|
+
*
|
|
6
|
+
* @module dsh-rewind/client/host-rpc
|
|
7
|
+
*/
|
|
8
|
+
import type { RewindCandidate } from './candidates.ts';
|
|
9
|
+
/** One file affected by a rewind. */
|
|
10
|
+
export interface PreviewFileImpact {
|
|
11
|
+
readonly path: string;
|
|
12
|
+
readonly action: 'restore' | 'delete';
|
|
13
|
+
}
|
|
14
|
+
/** One impact probe resolved, failed, or timed out (`null`). */
|
|
15
|
+
export type PreviewOutcome = {
|
|
16
|
+
readonly kind: 'success';
|
|
17
|
+
readonly files: readonly PreviewFileImpact[];
|
|
18
|
+
readonly backupOff: boolean;
|
|
19
|
+
} | {
|
|
20
|
+
readonly kind: 'error';
|
|
21
|
+
readonly text: string;
|
|
22
|
+
} | null;
|
|
23
|
+
/** Fetch one target's impact through the registered RPC call. */
|
|
24
|
+
export type PreviewCall = (sessionId: string, seq: number) => Promise<PreviewOutcome>;
|
|
25
|
+
type RpcResult = {
|
|
26
|
+
readonly ok: true;
|
|
27
|
+
readonly value: unknown;
|
|
28
|
+
} | {
|
|
29
|
+
readonly ok: false;
|
|
30
|
+
readonly error: {
|
|
31
|
+
readonly code: string;
|
|
32
|
+
readonly message: string;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
35
|
+
/** The slice of the client `connection` service this module reads. */
|
|
36
|
+
export interface ConnectionLike {
|
|
37
|
+
readonly rpc: {
|
|
38
|
+
call(channel: string, endpoint: string, payload: unknown, signal?: AbortSignal): Promise<RpcResult>;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** Resolve one target's file impact over RPC. */
|
|
42
|
+
export declare function previewOverRpc(connection: ConnectionLike | undefined, sessionId: string, seq: number): Promise<PreviewOutcome>;
|
|
43
|
+
/** Fetch the full rewind candidate list over RPC. */
|
|
44
|
+
export declare function candidatesOverRpc(connection: ConnectionLike | undefined, sessionId: string): Promise<readonly RewindCandidate[] | undefined>;
|
|
45
|
+
export {};
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
* so the plugin imports no conversation UI types and survives harness drift.
|
|
9
9
|
* The command decoration (`ctx.commandUi.decorate`) makes a bare `/rewind` (or
|
|
10
10
|
* its alias `/undo`) open the harness's own popupSelect instead of executing;
|
|
11
|
-
* the parameterized `@<seq>`
|
|
12
|
-
*
|
|
13
|
-
* `docs/contract/client-contract.md`.
|
|
11
|
+
* the parameterized `@<seq>` form stays the execution channel, while the
|
|
12
|
+
* popover's impact and candidate reads go over the host RPC channel. The
|
|
13
|
+
* exported contract is specified in `docs/contract/client-contract.md`.
|
|
14
14
|
*
|
|
15
15
|
* @module dsh-rewind/client
|
|
16
16
|
*/
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The rewind mode-selection popover (plain DOM, no React). Step two of the
|
|
3
3
|
* interaction: the target is already fixed (the clicked message); the popover
|
|
4
|
-
* offers the two modes. Choosing "both"
|
|
5
|
-
*
|
|
4
|
+
* offers the two modes. Choosing "both" fetches the impact list over the host
|
|
5
|
+
* RPC channel and shows it before confirming.
|
|
6
6
|
*
|
|
7
7
|
* Keyboard: ↑/↓ move focus across the step's ACTION buttons only (the two
|
|
8
8
|
* modes, or the confirm button on the impact step), Enter activates the
|
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import type { SessionFace } from '@deepseek-ai/dsh-api-session-controller/client';
|
|
17
17
|
import type { CommandNode } from '@deepseek-ai/dsh-client-ui-conversation/client';
|
|
18
|
-
import { type ChatOf
|
|
18
|
+
import { type ChatOf } from './hidden.ts';
|
|
19
|
+
import type { PreviewCall } from './host-rpc.ts';
|
|
19
20
|
import type { RewindKey } from './locales.ts';
|
|
20
21
|
type Translate = (key: RewindKey, params?: Record<string, unknown>) => string;
|
|
21
22
|
export interface PopoverOptions {
|
|
@@ -32,18 +33,6 @@ export interface PopoverOptions {
|
|
|
32
33
|
/** Pending variant: executed after the retract confirm closes the popover. */
|
|
33
34
|
readonly onRetract?: () => void;
|
|
34
35
|
readonly preview: string;
|
|
35
|
-
/**
|
|
36
|
-
* Chat reader on the `uiConversation` "chat" view: the durable
|
|
37
|
-
* variant's command probes scan the chat through it. Unused by the
|
|
38
|
-
* pending-retract variant.
|
|
39
|
-
*/
|
|
40
|
-
readonly chatOf: ChatOf;
|
|
41
|
-
/**
|
|
42
|
-
* Subscribe to one session's live chat-update signal, so a probe waiting on
|
|
43
|
-
* a command's chat node can be woken when the chat snapshot changes. Passed
|
|
44
|
-
* straight through to `waitForCommand`.
|
|
45
|
-
*/
|
|
46
|
-
readonly watchChat: ChatWatch;
|
|
47
36
|
/** The button that opened the popover (outside-click ignore target). */
|
|
48
37
|
readonly anchor: HTMLElement;
|
|
49
38
|
readonly t: Translate;
|
|
@@ -64,9 +53,8 @@ export declare function closePopover(): void;
|
|
|
64
53
|
/**
|
|
65
54
|
* Seqs of the command nodes currently matching `match`. Sample BEFORE issuing
|
|
66
55
|
* a new command of the same shape so the subsequent wait can exclude them: a
|
|
67
|
-
* repeated
|
|
68
|
-
*
|
|
69
|
-
* after those changes were already restored).
|
|
56
|
+
* repeated rewind of the same target must not settle on the previous command's
|
|
57
|
+
* stale outcome.
|
|
70
58
|
*/
|
|
71
59
|
export declare function knownCommandSeqs(session: SessionFace, chatOf: ChatOf, match: (node: CommandNode) => boolean): Set<number>;
|
|
72
60
|
/**
|
|
@@ -78,6 +66,8 @@ export declare function waitForCommand(session: SessionFace, chatOf: ChatOf, mat
|
|
|
78
66
|
kind: 'success' | 'error';
|
|
79
67
|
text?: string;
|
|
80
68
|
} | null>;
|
|
69
|
+
/** Register the host RPC preview call (null clears it on unload). */
|
|
70
|
+
export declare function registerPreviewCall(call: PreviewCall | null): void;
|
|
81
71
|
/** Open the mode-selection popover anchored near the given button. */
|
|
82
72
|
export declare function openPopover(opts: PopoverOptions): void;
|
|
83
73
|
export {};
|
|
@@ -315,13 +315,14 @@ interface RewindPortalsProps extends RewindBridgeDeps {
|
|
|
315
315
|
/**
|
|
316
316
|
* Session-scoped portal bridge: renders the ↶ button of every user message
|
|
317
317
|
* row of the session the harness mounts it for. The refresh is coalesced
|
|
318
|
-
* (one pass per
|
|
318
|
+
* (one pass per microtask window via queueMicrotask) and diffed (setState is
|
|
319
319
|
* skipped when the target set is unchanged), so the plugin never runs a
|
|
320
320
|
* synchronous full-transcript scan inside a commit microtask.
|
|
321
321
|
*
|
|
322
|
-
* The
|
|
323
|
-
*
|
|
324
|
-
* occurrences — see `syncInboxSubscription`)
|
|
322
|
+
* The refresh is driven by the three inputs its targets are made of: the chat
|
|
323
|
+
* snapshot (`hiddenSeqsOf`), the session's `inbox` projection (the pending
|
|
324
|
+
* occurrences — see `syncInboxSubscription`), and the DOM (row/actions mounts,
|
|
325
|
+
* via the scoped observer).
|
|
325
326
|
*/
|
|
326
327
|
export declare function RewindPortals({ sessionId, sessionOf, chatOf, isMainViewSession, watchChat, t, subscribeLocale, setComposerText, restoreImages }: RewindPortalsProps): ReactNode;
|
|
327
328
|
/** The current composer draft: the contenteditable `textContent`.
|
|
@@ -32,4 +32,4 @@ export declare const CLASS: {
|
|
|
32
32
|
*/
|
|
33
33
|
export declare const REWIND_ICON_SVG: string;
|
|
34
34
|
/** One injected stylesheet (scoped under `.dsh-rewind-*`). */
|
|
35
|
-
export declare const STYLE = "\n.dsh-rewind-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n /* The neighbouring action buttons scale by this delta too. */\n width: calc(28px + var(--dsh-content-font-delta, 0px));\n height: calc(28px + var(--dsh-content-font-delta, 0px));\n padding: 6px;\n border: none;\n border-radius: var(--dsw-radius-sm, 8px);\n background: transparent;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n}\n.dsh-rewind-btn svg {\n width: calc(15px + var(--dsh-content-font-delta, 0px));\n height: calc(15px + var(--dsh-content-font-delta, 0px));\n}\n.dsh-rewind-btn:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n\n.dsh-rewind-popover {\n position: fixed;\n z-index: 1000;\n width: 288px;\n padding: 12px;\n border:
|
|
35
|
+
export declare const STYLE = "\n.dsh-rewind-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n /* The neighbouring action buttons scale by this delta too. */\n width: calc(28px + var(--dsh-content-font-delta, 0px));\n height: calc(28px + var(--dsh-content-font-delta, 0px));\n padding: 6px;\n border: none;\n border-radius: var(--dsw-radius-sm, 8px);\n background: transparent;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n}\n.dsh-rewind-btn svg {\n width: calc(15px + var(--dsh-content-font-delta, 0px));\n height: calc(15px + var(--dsh-content-font-delta, 0px));\n}\n.dsh-rewind-btn:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n\n.dsh-rewind-popover {\n position: fixed;\n z-index: 1000;\n width: 288px;\n padding: 12px;\n border: 0;\n border-radius: var(--dsw-radius-md);\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n backdrop-filter: var(--dsw-menu-backdrop-filter);\n isolation: isolate;\n --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);\n box-shadow: var(--dsw-elevation-prominent);\n font-size: 14px;\n line-height: 20px;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-popover-title {\n font-size: 14px;\n font-weight: 600;\n line-height: 20px;\n}\n.dsh-rewind-popover-target {\n margin: 4px 0 10px;\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-all;\n}\n.dsh-rewind-popover-option {\n display: flex;\n flex-direction: column;\n gap: 2px;\n width: 100%;\n margin: 0 0 6px;\n padding: 8px 10px;\n border: 1px solid transparent;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n.dsh-rewind-popover-option:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n/* Arrow navigation moves real focus, so the focused row carries the pointer's\n fill and the browser ring is suppressed: in dark mode the UA two-tone ring\n draws its dark half against the filled row and reads as a residue edge (the\n harness Menu suppresses it the same way). */\n.dsh-rewind-popover-option:focus-visible:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n outline: none;\n}\n.dsh-rewind-popover-option:disabled {\n opacity: 0.5;\n cursor: default;\n}\n.dsh-rewind-popover-option-label {\n font-weight: 500;\n}\n.dsh-rewind-popover-option-hint {\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-popover-impact {\n margin: 4px 0 10px;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-interactive-bg-hover);\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-secondary);\n white-space: pre-wrap;\n max-height: 160px;\n overflow: auto;\n}\n.dsh-rewind-popover-note {\n margin-top: 6px;\n font-size: 11px;\n line-height: 15px;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-popover-actions {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n}\n.dsh-rewind-popover-primary,\n.dsh-rewind-popover-ghost {\n padding: 5px 12px;\n border: none;\n border-radius: 8px;\n font: inherit;\n font-size: 13px;\n line-height: 18px;\n cursor: pointer;\n}\n.dsh-rewind-popover-primary {\n background: var(--dsw-alias-button-primary-fill);\n color: var(--dsw-alias-label-primary-foreground);\n}\n.dsh-rewind-popover-primary:hover:not(:disabled) {\n background: var(--dsw-alias-button-primary-hover);\n}\n.dsh-rewind-popover-primary:disabled {\n opacity: 0.5;\n cursor: default;\n}\n.dsh-rewind-popover-ghost {\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-popover-ghost:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dsh-rewind-guard-hint {\n position: fixed;\n z-index: 1000;\n max-width: min(440px, calc(100vw - 24px));\n padding: 8px 12px;\n border: 0;\n border-radius: var(--dsw-radius-md);\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n backdrop-filter: var(--dsw-menu-backdrop-filter);\n isolation: isolate;\n --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);\n box-shadow: var(--dsw-elevation-panel);\n font-size: 13px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n pointer-events: none;\n}\n\n/* ---- Plugin configuration form ---- */\n/* Only the card's own controls: the two switch rows, their hints, and the divider. */\n.dsh-rewind-settings-divider {\n border-top: 1px solid var(--dsw-alias-border-l2);\n}\n.dsh-rewind-settings-section {\n display: grid;\n gap: 6px;\n padding: 12px 0;\n}\n.dsh-rewind-settings-toggle-row {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 16px;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-settings-toggle-label {\n flex: 1;\n min-width: 0;\n}\n.dsh-rewind-settings-hint {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n";
|
package/lib/types/locales.d.ts
CHANGED
|
@@ -15,9 +15,8 @@
|
|
|
15
15
|
* public read the settings UI and the client's own locale runtime perform.
|
|
16
16
|
*
|
|
17
17
|
* The client half (`src/client/locales.ts`) owns all interactive UI copy via
|
|
18
|
-
* `ctx.locale` + `t()`; the
|
|
19
|
-
*
|
|
20
|
-
* host prose.
|
|
18
|
+
* `ctx.locale` + `t()`; the client never parses host prose (the popover reads
|
|
19
|
+
* travel over the RPC channel).
|
|
21
20
|
*
|
|
22
21
|
* DSH localizes only its OWN command descriptions via the client locale — a
|
|
23
22
|
* closed `HOST_FACES` allowlist; third-party plugin descriptions are
|
|
@@ -45,15 +44,8 @@ export type HostLocaleId = 'zh' | 'en';
|
|
|
45
44
|
export declare function preferredLocale(preference: unknown): HostLocaleId;
|
|
46
45
|
/** English dictionary — the key-set source of truth (neutral default). */
|
|
47
46
|
export declare const en: {
|
|
48
|
-
'plan.rewinding': string;
|
|
49
|
-
'plan.affects': string;
|
|
50
|
-
'plan.restore': string;
|
|
51
|
-
'plan.delete': string;
|
|
52
|
-
'plan.noChanges': string;
|
|
53
|
-
'plan.backupOff': string;
|
|
54
47
|
'plan.skip': string;
|
|
55
48
|
'skipReason.oversized': string;
|
|
56
|
-
'uncovered.header': string;
|
|
57
49
|
'error.invalidTarget': string;
|
|
58
50
|
'sep.list': string;
|
|
59
51
|
'sep.clause': string;
|
package/lib/types/rewind.d.ts
CHANGED
|
@@ -106,23 +106,6 @@ export declare function parseRewindTarget(raw: string): RewindTarget | undefined
|
|
|
106
106
|
* @returns candidates numbered 1..N by recency.
|
|
107
107
|
*/
|
|
108
108
|
export declare function listRewindCandidates(events: readonly SessionEvent[], surface: readonly number[], limit?: number): RewindCandidate[];
|
|
109
|
-
/** Header line of the machine-readable candidate list (locale-independent). */
|
|
110
|
-
export declare const CANDIDATE_LIST_HEADER = "candidates=";
|
|
111
|
-
/**
|
|
112
|
-
* Encode a candidate list as the host→client machine channel (the same
|
|
113
|
-
* trailer pattern `formatPlan` uses for `impact=`). The client popupSelect
|
|
114
|
-
* parses this instead of reading the windowed chat snapshot, so the candidate
|
|
115
|
-
* list reflects the FULL host surface — not just the already-loaded history.
|
|
116
|
-
*
|
|
117
|
-
* Lines (each preview is already whitespace-collapsed and tab-free by
|
|
118
|
-
* `messagePreview`):
|
|
119
|
-
* candidates=<n>
|
|
120
|
-
* <seq>\t<time>\t<preview>
|
|
121
|
-
* … (one line per candidate, newest first, matching `listRewindCandidates`)
|
|
122
|
-
*
|
|
123
|
-
* A list with no candidates is just `candidates=0`.
|
|
124
|
-
*/
|
|
125
|
-
export declare function formatCandidateList(candidates: readonly RewindCandidate[]): string;
|
|
126
109
|
/**
|
|
127
110
|
* Resolve a target against the session log and surface into a validated plan.
|
|
128
111
|
* @param events - the full session event log.
|
package/lib/types/snapshot.d.ts
CHANGED
|
@@ -179,7 +179,7 @@ export interface LinkEntry {
|
|
|
179
179
|
export type StoredEntry = CheckpointEntry | LinkEntry;
|
|
180
180
|
/** True when an entry is a dedup link (carries `ref`, not `before`). */
|
|
181
181
|
export declare function isLinkEntry(entry: StoredEntry): entry is LinkEntry;
|
|
182
|
-
/** Per-file restore impact preview (
|
|
182
|
+
/** Per-file restore impact preview (served by the host RPC channel). */
|
|
183
183
|
export interface FileImpact {
|
|
184
184
|
readonly path: string;
|
|
185
185
|
/** `restore` = write the before content back; `delete` = remove the file. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-rewind-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "In-window conversation rewind with workspace file restore · 同窗口内对话回退并可还原工作区文件",
|
|
5
5
|
"icon": "./assets/icon.svg",
|
|
6
6
|
"keywords": [
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"client": {
|
|
55
55
|
"inject": [
|
|
56
56
|
"@deepseek-ai/dsh-client-locale",
|
|
57
|
+
"@deepseek-ai/dsh-client-connection",
|
|
57
58
|
"@deepseek-ai/dsh-client-ui-commands",
|
|
58
59
|
"@deepseek-ai/dsh-client-ui-conversation",
|
|
59
60
|
"@deepseek-ai/dsh-client-ui-settings"
|
|
@@ -74,6 +75,7 @@
|
|
|
74
75
|
},
|
|
75
76
|
"peerDependencies": {
|
|
76
77
|
"@deepseek-ai/cordis": "^4.0.5",
|
|
78
|
+
"@deepseek-ai/dsh-client-connection": "^0.2.1-rc.1",
|
|
77
79
|
"@deepseek-ai/dsh-client-locale": "^0.2.1-rc.1",
|
|
78
80
|
"@deepseek-ai/dsh-client-ui-commands": "^0.2.1-rc.1",
|
|
79
81
|
"@deepseek-ai/dsh-client-ui-plugin-manager": "^0.2.1-rc.1",
|
|
@@ -95,6 +97,9 @@
|
|
|
95
97
|
"@deepseek-ai/cordis": {
|
|
96
98
|
"optional": true
|
|
97
99
|
},
|
|
100
|
+
"@deepseek-ai/dsh-client-connection": {
|
|
101
|
+
"optional": true
|
|
102
|
+
},
|
|
98
103
|
"@deepseek-ai/dsh-client-locale": {
|
|
99
104
|
"optional": true
|
|
100
105
|
},
|