dsh-rewind-plugin 0.14.0 → 0.15.0-beta.2
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/SECURITY.md +7 -7
- package/cordis.patch.yml +4 -7
- package/docs/README.md +2 -1
- package/docs/architecture.md +5 -25
- package/docs/compat/audit.md +21 -43
- package/docs/compat/diagnostics.md +6 -10
- package/docs/compat/diagnostics.zh.md +3 -4
- package/docs/compat/tracking-boundary.md +1 -4
- package/docs/compat/tracking-boundary.zh.md +2 -2
- package/docs/harness-reference.md +1 -8
- package/docs/release/release.md +8 -19
- package/docs/release/release.zh.md +2 -2
- package/docs/snapshot-auto-cleanup.md +18 -22
- package/docs/snapshot-auto-cleanup.zh.md +5 -10
- package/lib/client.js +124 -33
- package/lib/index.js +19 -9
- package/lib/types/client/candidates.d.ts +0 -11
- package/lib/types/client/hidden.d.ts +44 -22
- package/lib/types/client/index.d.ts +9 -21
- package/lib/types/client/log.d.ts +11 -19
- package/lib/types/client/pending.d.ts +19 -8
- package/lib/types/client/portals.d.ts +40 -18
- package/lib/types/client/settings-card.d.ts +0 -1
- package/lib/types/client/styles.d.ts +1 -1
- package/lib/types/index.d.ts +3 -11
- package/lib/types/locales.d.ts +2 -0
- package/lib/types/marker.d.ts +3 -21
- package/lib/types/rewind.d.ts +12 -23
- package/lib/types/snapshot.d.ts +5 -34
- package/package.json +53 -55
package/lib/client.js
CHANGED
|
@@ -55,6 +55,31 @@ function messageTextAt(chat, seq) {
|
|
|
55
55
|
}
|
|
56
56
|
return void 0;
|
|
57
57
|
}
|
|
58
|
+
function messageAttachmentsAt(chat, seq) {
|
|
59
|
+
if (chat === void 0) return void 0;
|
|
60
|
+
for (const key of chat.order) {
|
|
61
|
+
const node = chat.nodes.get(key);
|
|
62
|
+
if (node === void 0 || node.kind !== "user" && node.kind !== "steering") continue;
|
|
63
|
+
const data = node.data;
|
|
64
|
+
if (data.seq !== seq) continue;
|
|
65
|
+
const images = [];
|
|
66
|
+
for (const block of data.content ?? []) {
|
|
67
|
+
if (block.type !== "image") continue;
|
|
68
|
+
const attachment = block.attachment;
|
|
69
|
+
if (typeof attachment?.attachmentId !== "string" || typeof attachment.mediaType !== "string") continue;
|
|
70
|
+
images.push({
|
|
71
|
+
attachmentId: attachment.attachmentId,
|
|
72
|
+
mediaType: attachment.mediaType,
|
|
73
|
+
bytes: typeof attachment.bytes === "number" ? attachment.bytes : void 0,
|
|
74
|
+
width: typeof attachment.width === "number" ? attachment.width : void 0,
|
|
75
|
+
height: typeof attachment.height === "number" ? attachment.height : void 0,
|
|
76
|
+
name: typeof attachment.name === "string" ? attachment.name : void 0
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return images;
|
|
80
|
+
}
|
|
81
|
+
return void 0;
|
|
82
|
+
}
|
|
58
83
|
function targetSeqOfArgs(args) {
|
|
59
84
|
if (args === void 0 || args === null) return void 0;
|
|
60
85
|
const match = args.match(/@(\d+)/);
|
|
@@ -259,8 +284,9 @@ var STYLE = `
|
|
|
259
284
|
display: inline-flex;
|
|
260
285
|
align-items: center;
|
|
261
286
|
justify-content: center;
|
|
262
|
-
|
|
263
|
-
|
|
287
|
+
/* The neighbouring action buttons scale by this delta too. */
|
|
288
|
+
width: calc(28px + var(--dsh-content-font-delta, 0px));
|
|
289
|
+
height: calc(28px + var(--dsh-content-font-delta, 0px));
|
|
264
290
|
padding: 6px;
|
|
265
291
|
border: none;
|
|
266
292
|
border-radius: var(--dsw-radius-sm, 8px);
|
|
@@ -268,6 +294,10 @@ var STYLE = `
|
|
|
268
294
|
color: var(--dsw-alias-label-tertiary);
|
|
269
295
|
cursor: pointer;
|
|
270
296
|
}
|
|
297
|
+
.dsh-rewind-btn svg {
|
|
298
|
+
width: calc(15px + var(--dsh-content-font-delta, 0px));
|
|
299
|
+
height: calc(15px + var(--dsh-content-font-delta, 0px));
|
|
300
|
+
}
|
|
271
301
|
.dsh-rewind-btn:hover {
|
|
272
302
|
background: var(--dsw-alias-interactive-bg-hover);
|
|
273
303
|
color: var(--dsw-alias-label-secondary);
|
|
@@ -803,34 +833,41 @@ var import_react_dom = require("react-dom");
|
|
|
803
833
|
// src/client/pending.ts
|
|
804
834
|
var QUEUE_PREVIEW_CHARS = 200;
|
|
805
835
|
function textOf(content) {
|
|
806
|
-
|
|
807
|
-
|
|
836
|
+
const texts = content.filter((block) => block.type === "text");
|
|
837
|
+
if (texts.length === 0) return null;
|
|
838
|
+
return texts.map((block) => block.text ?? "").join("");
|
|
839
|
+
}
|
|
840
|
+
function attachmentsCountOf(content) {
|
|
841
|
+
return content.filter((block) => block.type !== "text").length;
|
|
808
842
|
}
|
|
809
|
-
function previewOf(
|
|
810
|
-
const
|
|
811
|
-
|
|
812
|
-
return chars.length > QUEUE_PREVIEW_CHARS ? `${chars.slice(0, QUEUE_PREVIEW_CHARS).join("")}\u2026` : flat;
|
|
843
|
+
function previewOf(text) {
|
|
844
|
+
const chars = Array.from((text ?? "").replace(/\s+/g, " ").trim());
|
|
845
|
+
return chars.length > QUEUE_PREVIEW_CHARS ? `${chars.slice(0, QUEUE_PREVIEW_CHARS).join("")}\u2026` : chars.join("");
|
|
813
846
|
}
|
|
814
847
|
function steeringItemsOf(nextStep) {
|
|
815
|
-
return (nextStep ?? []).filter((item) => item.source?.kind === "user").map((item) =>
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
848
|
+
return (nextStep ?? []).filter((item) => item.source?.kind === "user").map((item) => {
|
|
849
|
+
const text = textOf(item.content);
|
|
850
|
+
return {
|
|
851
|
+
id: item.id,
|
|
852
|
+
text,
|
|
853
|
+
attachments: attachmentsCountOf(item.content),
|
|
854
|
+
preview: previewOf(text)
|
|
855
|
+
};
|
|
856
|
+
});
|
|
820
857
|
}
|
|
821
858
|
function matchPendingRows(rows, steering) {
|
|
822
859
|
const matched = [];
|
|
823
860
|
for (let i = 0; i < rows.length; i++) {
|
|
824
861
|
const row = rows[i];
|
|
825
862
|
const item = steering[i];
|
|
826
|
-
|
|
827
|
-
matched.push(item.id);
|
|
828
|
-
} else {
|
|
829
|
-
matched.push(null);
|
|
830
|
-
}
|
|
863
|
+
matched.push(item !== void 0 && matches2(item, row) ? item.id : null);
|
|
831
864
|
}
|
|
832
865
|
return matched;
|
|
833
866
|
}
|
|
867
|
+
function matches2(item, row) {
|
|
868
|
+
if (item.attachments > 0) return row.attachments === item.attachments;
|
|
869
|
+
return row.text === (item.text ?? "");
|
|
870
|
+
}
|
|
834
871
|
function retractSpan(steering, targetId) {
|
|
835
872
|
const index = steering.findIndex((item) => item.id === targetId);
|
|
836
873
|
if (index === -1) return [];
|
|
@@ -883,7 +920,7 @@ function userNodeOf(chat, key) {
|
|
|
883
920
|
if (node === void 0 || node.kind !== "user" && node.kind !== "steering") return void 0;
|
|
884
921
|
return node.data;
|
|
885
922
|
}
|
|
886
|
-
async function runRewindAndFill(session, seq, mode, isMainViewSession, chatOf, watchChat, setComposerText) {
|
|
923
|
+
async function runRewindAndFill(session, seq, mode, isMainViewSession, chatOf, watchChat, setComposerText, restoreImages) {
|
|
887
924
|
const known = knownCommandSeqs(session, chatOf, (node) => isExecutedRewindCommand(node, seq));
|
|
888
925
|
let result;
|
|
889
926
|
try {
|
|
@@ -919,12 +956,22 @@ async function runRewindAndFill(session, seq, mode, isMainViewSession, chatOf, w
|
|
|
919
956
|
return;
|
|
920
957
|
}
|
|
921
958
|
let text;
|
|
959
|
+
let images;
|
|
922
960
|
try {
|
|
923
|
-
|
|
961
|
+
const chat = chatOf(session);
|
|
962
|
+
text = messageTextAt(chat, seq);
|
|
963
|
+
images = messageAttachmentsAt(chat, seq);
|
|
924
964
|
} catch (error) {
|
|
925
|
-
rewindLog.warn("refill", `reading target
|
|
965
|
+
rewindLog.warn("refill", `reading target content for @${seq} threw`, error);
|
|
926
966
|
return;
|
|
927
967
|
}
|
|
968
|
+
if (restoreImages !== void 0 && images !== void 0 && images.length > 0) {
|
|
969
|
+
try {
|
|
970
|
+
await restoreImages(session.sessionId, images);
|
|
971
|
+
} catch (error) {
|
|
972
|
+
rewindLog.warn("refill", `image restore for @${seq} threw`, error);
|
|
973
|
+
}
|
|
974
|
+
}
|
|
928
975
|
if (text === void 0 || text === "") {
|
|
929
976
|
return;
|
|
930
977
|
}
|
|
@@ -968,6 +1015,7 @@ function resolveSeatAnchorSeq(element, chat) {
|
|
|
968
1015
|
return node?.anchorSeq;
|
|
969
1016
|
}
|
|
970
1017
|
var PENDING_SEAT_SELECTOR = "[data-pending-steering]";
|
|
1018
|
+
var MESSAGE_ATTACHMENT_SELECTOR = "[data-message-attachments]";
|
|
971
1019
|
function actionsContainerOf(row) {
|
|
972
1020
|
const buttons = Array.from(row?.querySelectorAll("button") ?? []);
|
|
973
1021
|
const lastButton = buttons.filter((button) => !button.classList.contains(CLASS.button)).at(-1);
|
|
@@ -1041,16 +1089,16 @@ var INBOX_PROJECTION = "inbox";
|
|
|
1041
1089
|
function bubbleTextOf(row) {
|
|
1042
1090
|
const clone = row.cloneNode(true);
|
|
1043
1091
|
clone.lastElementChild?.remove();
|
|
1044
|
-
|
|
1092
|
+
const originals = row.querySelectorAll(MESSAGE_ATTACHMENT_SELECTOR);
|
|
1093
|
+
const copies = clone.querySelectorAll(MESSAGE_ATTACHMENT_SELECTOR);
|
|
1094
|
+
for (let index = 0; index < copies.length; index++) copies[index]?.remove();
|
|
1095
|
+
return { text: clone.textContent ?? "", attachments: originals.length };
|
|
1045
1096
|
}
|
|
1046
1097
|
function collectPendingTargets(snapshot, steering) {
|
|
1047
1098
|
if (snapshot.subagent !== null) return [];
|
|
1048
1099
|
if (steering.length === 0) return [];
|
|
1049
1100
|
const rows = Array.from(document.querySelectorAll(PENDING_SEAT_SELECTOR));
|
|
1050
|
-
const matched = matchPendingRows(
|
|
1051
|
-
rows.map((row) => ({ text: bubbleTextOf(row) })),
|
|
1052
|
-
steering
|
|
1053
|
-
);
|
|
1101
|
+
const matched = matchPendingRows(rows.map(bubbleTextOf), steering);
|
|
1054
1102
|
const targets = [];
|
|
1055
1103
|
for (let i = 0; i < matched.length; i++) {
|
|
1056
1104
|
const itemId = matched[i];
|
|
@@ -1079,7 +1127,7 @@ function sameTargets(left, right) {
|
|
|
1079
1127
|
return other.kind === "pending" && target.itemId === other.itemId;
|
|
1080
1128
|
});
|
|
1081
1129
|
}
|
|
1082
|
-
function RewindPortals({ sessionId, sessionOf, chatOf, isMainViewSession, watchChat, t, subscribeLocale, setComposerText }) {
|
|
1130
|
+
function RewindPortals({ sessionId, sessionOf, chatOf, isMainViewSession, watchChat, t, subscribeLocale, setComposerText, restoreImages }) {
|
|
1083
1131
|
const [targets, setTargets] = (0, import_react.useState)([]);
|
|
1084
1132
|
const hidden = (0, import_react.useRef)(/* @__PURE__ */ new WeakSet());
|
|
1085
1133
|
const [, forceRender] = (0, import_react.useReducer)((count) => count + 1, 0);
|
|
@@ -1166,6 +1214,7 @@ function RewindPortals({ sessionId, sessionOf, chatOf, isMainViewSession, watchC
|
|
|
1166
1214
|
watchChat,
|
|
1167
1215
|
isMainViewSession,
|
|
1168
1216
|
setComposerText,
|
|
1217
|
+
restoreImages,
|
|
1169
1218
|
t
|
|
1170
1219
|
},
|
|
1171
1220
|
target.key
|
|
@@ -1174,7 +1223,7 @@ function RewindPortals({ sessionId, sessionOf, chatOf, isMainViewSession, watchC
|
|
|
1174
1223
|
target.key
|
|
1175
1224
|
));
|
|
1176
1225
|
}
|
|
1177
|
-
function RewindButton({ target, sessionId, sessionOf, chatOf, watchChat, isMainViewSession, setComposerText, t }) {
|
|
1226
|
+
function RewindButton({ target, sessionId, sessionOf, chatOf, watchChat, isMainViewSession, setComposerText, restoreImages, t }) {
|
|
1178
1227
|
const onClick = (event) => {
|
|
1179
1228
|
event.stopPropagation();
|
|
1180
1229
|
const session = sessionOf(sessionId);
|
|
@@ -1194,7 +1243,7 @@ function RewindButton({ target, sessionId, sessionOf, chatOf, watchChat, isMainV
|
|
|
1194
1243
|
anchor: event.currentTarget,
|
|
1195
1244
|
t,
|
|
1196
1245
|
onRewind: (mode) => {
|
|
1197
|
-
void runRewindAndFill(session, node.seq, mode, isMainViewSession, chatOf, watchChat, setComposerText);
|
|
1246
|
+
void runRewindAndFill(session, node.seq, mode, isMainViewSession, chatOf, watchChat, setComposerText, restoreImages);
|
|
1198
1247
|
}
|
|
1199
1248
|
});
|
|
1200
1249
|
};
|
|
@@ -1271,9 +1320,9 @@ function createRewindBridge(deps) {
|
|
|
1271
1320
|
}
|
|
1272
1321
|
|
|
1273
1322
|
// src/client/build-info.ts
|
|
1274
|
-
var PLUGIN_VERSION = true ? "0.
|
|
1323
|
+
var PLUGIN_VERSION = true ? "0.15.0-beta.2" : "dev";
|
|
1275
1324
|
var PLUGIN_PACKAGE = "dsh-rewind-plugin";
|
|
1276
|
-
var BUILD_HASH = true ? "
|
|
1325
|
+
var BUILD_HASH = true ? "0582c3a6" : "dev";
|
|
1277
1326
|
|
|
1278
1327
|
// src/client/locales.ts
|
|
1279
1328
|
var zh = {
|
|
@@ -1490,6 +1539,48 @@ function apply(ctx) {
|
|
|
1490
1539
|
}
|
|
1491
1540
|
};
|
|
1492
1541
|
};
|
|
1542
|
+
const IMAGE_MEDIA_EXTENSION = {
|
|
1543
|
+
"image/png": "png",
|
|
1544
|
+
"image/jpeg": "jpg",
|
|
1545
|
+
"image/webp": "webp",
|
|
1546
|
+
"image/gif": "gif"
|
|
1547
|
+
};
|
|
1548
|
+
const restoreImages = async (sessionId, images) => {
|
|
1549
|
+
if (images.length === 0) return;
|
|
1550
|
+
const ui = uiConversation();
|
|
1551
|
+
const conversation = ctx.get("conversation");
|
|
1552
|
+
const scope = ctx.sessions.scope?.(sessionId);
|
|
1553
|
+
if (ui?.imageUrl === void 0 || conversation?.input === void 0 || conversation.createDrafts === void 0 || scope === void 0) {
|
|
1554
|
+
rewindLog.warn("refill", "image restore unavailable (conversation services not ready)");
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
const files = [];
|
|
1558
|
+
for (const [index, ref] of images.entries()) {
|
|
1559
|
+
const extension = IMAGE_MEDIA_EXTENSION[ref.mediaType];
|
|
1560
|
+
if (extension === void 0) {
|
|
1561
|
+
rewindLog.warn("refill", `skipping image with unsupported type ${ref.mediaType}`);
|
|
1562
|
+
continue;
|
|
1563
|
+
}
|
|
1564
|
+
try {
|
|
1565
|
+
const url = await ui.imageUrl(sessionId, ref);
|
|
1566
|
+
const blob = await (await fetch(url)).blob();
|
|
1567
|
+
files.push(new File([blob], ref.name ?? `image-${index + 1}.${extension}`, { type: ref.mediaType }));
|
|
1568
|
+
} catch (error) {
|
|
1569
|
+
rewindLog.warn("refill", `image fetch threw for ${ref.attachmentId}`, error);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
if (files.length === 0) return;
|
|
1573
|
+
try {
|
|
1574
|
+
const drafts = conversation.createDrafts(sessionId, files);
|
|
1575
|
+
const admitted = conversation.input.for(scope).addAttachments(drafts.map((draft) => draft.id));
|
|
1576
|
+
if (!admitted) {
|
|
1577
|
+
rewindLog.warn("refill", `composer refused ${drafts.length} restored image(s) (busy)`);
|
|
1578
|
+
conversation.releaseDraftAttachments?.(drafts);
|
|
1579
|
+
}
|
|
1580
|
+
} catch (error) {
|
|
1581
|
+
rewindLog.warn("refill", "image restore threw", error);
|
|
1582
|
+
}
|
|
1583
|
+
};
|
|
1493
1584
|
const setComposerText = (sessionId, text) => {
|
|
1494
1585
|
try {
|
|
1495
1586
|
return writeComposer(text, composerFacade(sessionId));
|
|
@@ -1525,7 +1616,7 @@ function apply(ctx) {
|
|
|
1525
1616
|
id: "dsh-rewind-portals",
|
|
1526
1617
|
order: 1e3
|
|
1527
1618
|
},
|
|
1528
|
-
createRewindBridge({ sessionOf, chatOf, isMainViewSession, watchChat, setComposerText, t, subscribeLocale })
|
|
1619
|
+
createRewindBridge({ sessionOf, chatOf, isMainViewSession, watchChat, setComposerText, restoreImages, t, subscribeLocale })
|
|
1529
1620
|
));
|
|
1530
1621
|
try {
|
|
1531
1622
|
const { store, form } = cleanupForm(
|
|
@@ -1599,7 +1690,7 @@ function apply(ctx) {
|
|
|
1599
1690
|
anchor: composerAnchor(),
|
|
1600
1691
|
t,
|
|
1601
1692
|
onRewind: (mode) => {
|
|
1602
|
-
void runRewindAndFill(face, candidate.seq, mode, isMainViewSession, chatOf, watchChat, setComposerText);
|
|
1693
|
+
void runRewindAndFill(face, candidate.seq, mode, isMainViewSession, chatOf, watchChat, setComposerText, restoreImages);
|
|
1603
1694
|
}
|
|
1604
1695
|
});
|
|
1605
1696
|
}
|
package/lib/index.js
CHANGED
|
@@ -24,6 +24,8 @@ var en = {
|
|
|
24
24
|
"skipReason.oversized": "over the {limit} backup limit, so the recorded state predates untracked edits",
|
|
25
25
|
"uncovered.header": "{count} tracked file(s) exceed the {limit} backup limit and are not covered:",
|
|
26
26
|
"error.invalidTarget": 'Cannot parse target "{raw}" (expected <index> or @<seq>)',
|
|
27
|
+
"sep.list": ", ",
|
|
28
|
+
"sep.clause": "; ",
|
|
27
29
|
"failures.suffix": "; {count} file(s) failed to restore: {list}",
|
|
28
30
|
"failures.item": "{path} ({message})",
|
|
29
31
|
"inflight": "A rewind is already running for this session; please wait.",
|
|
@@ -77,6 +79,8 @@ var zh = {
|
|
|
77
79
|
"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",
|
|
78
80
|
"uncovered.header": "{count} \u4E2A\u5DF2\u8DDF\u8E2A\u6587\u4EF6\u8D85\u8FC7 {limit} \u7684\u5907\u4EFD\u4E0A\u9650\uFF0C\u672A\u7EB3\u5165\u8BB0\u5F55\uFF1A",
|
|
79
81
|
"error.invalidTarget": '\u65E0\u6CD5\u89E3\u6790\u76EE\u6807 "{raw}"\uFF08\u5E94\u4E3A <\u5E8F\u53F7> \u6216 @<seq>\uFF09',
|
|
82
|
+
"sep.list": "\u3001",
|
|
83
|
+
"sep.clause": "\uFF1B",
|
|
80
84
|
"failures.suffix": "\uFF1B{count} \u4E2A\u6587\u4EF6\u8FD8\u539F\u5931\u8D25\uFF1A{list}",
|
|
81
85
|
"failures.item": "{path}\uFF08{message}\uFF09",
|
|
82
86
|
"inflight": "\u8BE5\u4F1A\u8BDD\u5DF2\u6709\u4E00\u4E2A\u56DE\u9000\u6B63\u5728\u6267\u884C\uFF0C\u8BF7\u7A0D\u5019\u3002",
|
|
@@ -149,15 +153,21 @@ function messagePreview(message) {
|
|
|
149
153
|
const text = message.content.map((block) => block.type === "text" && typeof block.text === "string" ? block.text : "").join("").replace(/\s+/g, " ").trim();
|
|
150
154
|
return text.length <= CANDIDATE_PREVIEW_CHARS ? text : `${text.slice(0, CANDIDATE_PREVIEW_CHARS - 1)}\u2026`;
|
|
151
155
|
}
|
|
156
|
+
var SEQ_TOKEN = /^@(\d+)$/;
|
|
157
|
+
var INDEX_TOKEN = /^(\d+)$/;
|
|
152
158
|
function parseRewindTarget(raw) {
|
|
153
159
|
const token = raw.trim();
|
|
154
|
-
|
|
155
|
-
if (
|
|
156
|
-
const
|
|
157
|
-
return Number.isSafeInteger(
|
|
160
|
+
const seq = SEQ_TOKEN.exec(token);
|
|
161
|
+
if (seq !== null) {
|
|
162
|
+
const value = Number(seq[1] ?? "");
|
|
163
|
+
return Number.isSafeInteger(value) ? { kind: "seq", seq: value } : void 0;
|
|
158
164
|
}
|
|
159
|
-
const index =
|
|
160
|
-
|
|
165
|
+
const index = INDEX_TOKEN.exec(token);
|
|
166
|
+
if (index !== null) {
|
|
167
|
+
const value = Number(index[1] ?? "");
|
|
168
|
+
return Number.isSafeInteger(value) && value >= 1 ? { kind: "index", index: value } : void 0;
|
|
169
|
+
}
|
|
170
|
+
return void 0;
|
|
161
171
|
}
|
|
162
172
|
function listRewindCandidates(events, surface, limit = DEFAULT_CANDIDATE_LIMIT) {
|
|
163
173
|
const surfaceIndexes = /* @__PURE__ */ new Map();
|
|
@@ -2517,7 +2527,7 @@ function renderFailures(failed) {
|
|
|
2517
2527
|
if (failed.length === 0) return "";
|
|
2518
2528
|
return t("failures.suffix", {
|
|
2519
2529
|
count: failed.length,
|
|
2520
|
-
list: failed.map((f) => t("failures.item", { path: f.path, message: f.message })).join("
|
|
2530
|
+
list: failed.map((f) => t("failures.item", { path: f.path, message: f.message })).join(t("sep.list"))
|
|
2521
2531
|
});
|
|
2522
2532
|
}
|
|
2523
2533
|
function renderSkipped(outcome, uncovered, maxFileBytes) {
|
|
@@ -2620,7 +2630,7 @@ async function executeRewind(ctx, store, fs, invocation, rawTarget, mode, inflig
|
|
|
2620
2630
|
} catch (error) {
|
|
2621
2631
|
if (!(error instanceof UnknownStoreVersionError)) throw error;
|
|
2622
2632
|
const version = await store.readStoreVersion(agent.session.id);
|
|
2623
|
-
restore =
|
|
2633
|
+
restore = `${t("sep.clause")}${t("storeUnsupported", { version: version ?? error.version })}`;
|
|
2624
2634
|
}
|
|
2625
2635
|
if (outcome !== void 0) {
|
|
2626
2636
|
await syncRestoreObservations(ctx, fs, agent, outcome);
|
|
@@ -2633,7 +2643,7 @@ async function executeRewind(ctx, store, fs, invocation, rawTarget, mode, inflig
|
|
|
2633
2643
|
const skippedLines = renderSkipped(outcome, capped, store.fileCapBytes);
|
|
2634
2644
|
if (skippedLines !== "") parts.push(skippedLines);
|
|
2635
2645
|
}
|
|
2636
|
-
restore = parts.length > 0 ?
|
|
2646
|
+
restore = parts.length > 0 ? `${t("sep.clause")}${parts.join(t("sep.list"))}` : t("noRestorable");
|
|
2637
2647
|
restore += renderFailures(outcome.failed);
|
|
2638
2648
|
}
|
|
2639
2649
|
}
|
|
@@ -74,12 +74,6 @@ export declare function formatCandidateTime(time: number): string;
|
|
|
74
74
|
export declare function rewindCandidatesOf(snap: CandidateChat, hidden: ReadonlySet<number>, limit?: number): RewindCandidate[];
|
|
75
75
|
/** The candidates of a live chat snapshot, withdrawn rows already excluded. */
|
|
76
76
|
export declare function rewindCandidatesOfChat(snap: CandidateChat): RewindCandidate[];
|
|
77
|
-
/**
|
|
78
|
-
* Map the candidates to popupSelect rows: the message preview as the row
|
|
79
|
-
* label (left) and the clock time as the detail (right) — the shell's native
|
|
80
|
-
* label/detail flex layout, with no recency numbers.
|
|
81
|
-
*/
|
|
82
|
-
export declare function rewindOptionsOf(snap: CandidateChat, t: Translate): SelectOption[];
|
|
83
77
|
/**
|
|
84
78
|
* Parse the host's candidate-list encoding (see `formatCandidateList` in
|
|
85
79
|
* src/rewind.ts) into typed candidates. Malformed lines are skipped; a
|
|
@@ -92,9 +86,4 @@ export declare function rewindCandidatesFromHostText(text: string): RewindCandid
|
|
|
92
86
|
* `__candidates` channel instead of the windowed chat snapshot.
|
|
93
87
|
*/
|
|
94
88
|
export declare function rewindOptionsFromCandidates(candidates: readonly RewindCandidate[], t: Translate): SelectOption[];
|
|
95
|
-
/**
|
|
96
|
-
* Parse the host's candidate-list encoding (see `formatCandidateList` in
|
|
97
|
-
* src/rewind.ts) into popupSelect rows.
|
|
98
|
-
*/
|
|
99
|
-
export declare function rewindOptionsFromHostText(text: string, t: Translate): SelectOption[];
|
|
100
89
|
export {};
|
|
@@ -60,11 +60,42 @@ export declare function chatSnapshotOf(chatView: {
|
|
|
60
60
|
* filling the composer after a withdraw. Accepts BOTH `user` and `steering`
|
|
61
61
|
* nodes: a plan-mode (`/plan <text>`) input is delivered through the agent
|
|
62
62
|
* inbox next-step and claimed, so it renders as `steering`, and its text must
|
|
63
|
-
* still return to the composer (`portals.tsx` `runRewindAndFill`)
|
|
64
|
-
*
|
|
65
|
-
*
|
|
63
|
+
* still return to the composer (`portals.tsx` `runRewindAndFill`). State absent
|
|
64
|
+
* → undefined; a message with no text blocks → ''. Same text-blocks join the
|
|
65
|
+
* candidate side uses.
|
|
66
66
|
*/
|
|
67
67
|
export declare function messageTextAt(chat: HiddenChat | undefined, seq: number): string | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* A durable image reference carried by an `image` content block in the chat
|
|
70
|
+
* snapshot, read structurally (the harness's `ImageAttachmentRef` from
|
|
71
|
+
* dsh-attachment, without importing it, so the plugin survives harness
|
|
72
|
+
* drift). Fields beyond `attachmentId`/`mediaType` pass through to the
|
|
73
|
+
* durable image URL loader unchanged.
|
|
74
|
+
*/
|
|
75
|
+
export interface MessageImageRef {
|
|
76
|
+
readonly attachmentId: string;
|
|
77
|
+
readonly mediaType: string;
|
|
78
|
+
readonly bytes?: number;
|
|
79
|
+
readonly width?: number;
|
|
80
|
+
readonly height?: number;
|
|
81
|
+
readonly name?: string;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* The durable image references of the human message at `seq` in the chat
|
|
85
|
+
* snapshot, in message order. The composer-refill counterpart of
|
|
86
|
+
* `messageTextAt` (see `portals.tsx` `runRewindAndFill`): rewinding an
|
|
87
|
+
* image-bearing message restores its pictures into the composer so the
|
|
88
|
+
* withdraw does not silently drop them.
|
|
89
|
+
*
|
|
90
|
+
* Reads the same node kinds as `messageTextAt` (`user` and `steering`).
|
|
91
|
+
* Blocks tolerate malformed or missing references (skipped, never a throw),
|
|
92
|
+
* and `image/offload` projections leave the durable reference in place —
|
|
93
|
+
* which is all the URL loader needs. `file` blocks are intentionally
|
|
94
|
+
* excluded: the harness exposes no client-side file-bytes loader (file
|
|
95
|
+
* uploads go one-way), so a file is not restorable.
|
|
96
|
+
* State absent → undefined; a message without images → [].
|
|
97
|
+
*/
|
|
98
|
+
export declare function messageAttachmentsAt(chat: HiddenChat | undefined, seq: number): readonly MessageImageRef[] | undefined;
|
|
68
99
|
/**
|
|
69
100
|
* Extract the rewind target seq from a `/rewind` command's structured `args`
|
|
70
101
|
* (e.g. `@5 chat`, `preview @5 both`). Locale-independent — never parses the
|
|
@@ -98,25 +129,16 @@ export declare function hasFileImpact(text: string | undefined): boolean;
|
|
|
98
129
|
*/
|
|
99
130
|
export declare function isCandidateCommand(command: CommandNode): boolean;
|
|
100
131
|
/**
|
|
101
|
-
* Anchor seqs that must be hidden from the rendered transcript so the user
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
* every
|
|
105
|
-
*
|
|
106
|
-
* the (unrendered) marker.
|
|
107
|
-
*
|
|
108
|
-
* Each executed rewind cuts ONE span `[target, marker]`: the target message
|
|
109
|
-
* and everything after it, up to the marker appended at rewind time. Spans are
|
|
110
|
-
* kept SEPARATE (never collapsed to a single `[min target, max marker]`)
|
|
111
|
-
* because a later rewind to a LATER point leaves a visible gap of new traffic
|
|
112
|
-
* between the earlier marker and the later target — collapsing the spans would
|
|
113
|
-
* hide that still-on-surface gap. Endpoints come from the command nodes:
|
|
114
|
-
* `sourceEventSeq` is the marker's log seq, and the outcome text carries the
|
|
115
|
-
* target seq.
|
|
132
|
+
* Anchor seqs that must be hidden from the rendered transcript so the user sees
|
|
133
|
+
* the conversation as the agent sees it: every impact-preview flow node (it only
|
|
134
|
+
* exists to feed the popover) and every SUCCESSFUL executed `/rewind` command
|
|
135
|
+
* row, plus every message a rewind withdrew — the target, everything after it,
|
|
136
|
+
* and the (unrendered) marker.
|
|
116
137
|
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
138
|
+
* Each executed rewind cuts ONE span `[target, marker]`, and callers must keep
|
|
139
|
+
* the spans SEPARATE: collapsing them to `[min target, max marker]` would hide a
|
|
140
|
+
* still-on-surface gap of new traffic between an earlier marker and a later
|
|
141
|
+
* target. Endpoints come from the command nodes (`sourceEventSeq` is the
|
|
142
|
+
* marker's log seq; the outcome text carries the target seq).
|
|
121
143
|
*/
|
|
122
144
|
export declare function hiddenSeqsOf(snap: HiddenChat): Set<number>;
|
|
@@ -1,28 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-rewind client half: the `/rewind` command decoration, the locale
|
|
3
3
|
* registration, and the session-scoped portal bridge that renders the
|
|
4
|
-
* per-message ↶ rewind button (
|
|
5
|
-
* `portals.tsx` for the button itself).
|
|
4
|
+
* per-message ↶ rewind button (`portals.tsx` owns the button and the popover).
|
|
6
5
|
*
|
|
7
|
-
* The button
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* The text-driven flow is the harness's STANDARD command decoration
|
|
17
|
-
* (`ctx.commandUi.decorate`): a bare `/rewind` (or its alias `/undo`) —
|
|
18
|
-
* picked from the slash-menu completion, or typed in full and Entered —
|
|
19
|
-
* opens the harness's own popupSelect shell (search, ↑↓/Enter, Esc) listing
|
|
20
|
-
* the rewind candidates instead of executing the command. Picking one
|
|
21
|
-
* continues the SAME flow as the ↶ button: the mode popover, both-impact
|
|
22
|
-
* confirmation, execution, row hiding and the composer refill
|
|
23
|
-
* (`runRewindAndFill`). The parameterized forms (`/rewind @<seq> chat|both`,
|
|
24
|
-
* `/rewind preview …`) stay internal channels the ↶ button and the popover
|
|
25
|
-
* drive through `session.command`.
|
|
6
|
+
* The button registers through the harness's
|
|
7
|
+
* `conversation.session.header.actions` slot — a typed structural registration,
|
|
8
|
+
* so the plugin imports no conversation UI types and survives harness drift.
|
|
9
|
+
* The command decoration (`ctx.commandUi.decorate`) makes a bare `/rewind` (or
|
|
10
|
+
* its alias `/undo`) open the harness's own popupSelect instead of executing;
|
|
11
|
+
* the parameterized `@<seq>` / `preview` forms stay internal channels the
|
|
12
|
+
* button and the popover drive. The exported contract is specified in
|
|
13
|
+
* `docs/contract/client-contract.md`.
|
|
26
14
|
*
|
|
27
15
|
* @module dsh-rewind/client
|
|
28
16
|
*/
|
|
@@ -1,25 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-rewind client logger:
|
|
3
|
-
*
|
|
2
|
+
* dsh-rewind client logger: one namespaced, level-filtered channel for every
|
|
3
|
+
* browser-side diagnostic in this plugin.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* `error` / `warn` are ALWAYS emitted (the anomaly guard: rare, cheap, and
|
|
6
|
+
* surfaced even for a user who never touched the switch). `info` / `debug` are
|
|
7
|
+
* gated by the exclusively-own, per-origin `localStorage['dsh-rewind.debug']`
|
|
8
|
+
* switch and further filtered by namespace. The switch is read on every call
|
|
9
|
+
* (never cached), so flipping it and reloading takes effect on any published
|
|
10
|
+
* build without a plugin rebuild.
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
|
-
* `
|
|
13
|
-
*
|
|
14
|
-
* exclusively-own key so it can never enable any other plugin/feature and no
|
|
15
|
-
* other feature can wake this one. Because it is read on every call (never
|
|
16
|
-
* cached), flipping it and reloading takes effect on any published build
|
|
17
|
-
* without a plugin rebuild.
|
|
18
|
-
*
|
|
19
|
-
* Values accepted by the switch (empty/unset = off):
|
|
20
|
-
* - `dsh-rewind*` — every dsh-rewind namespace.
|
|
21
|
-
* - `dsh-rewind:refill` — just one subsystem (exact match).
|
|
22
|
-
* - `dsh-rewind:refill,dsh-rewind:hiding` — several (comma-separated).
|
|
12
|
+
* Values accepted (empty/unset = off): `dsh-rewind*` — every namespace;
|
|
13
|
+
* `dsh-rewind:refill` — one subsystem (exact match);
|
|
14
|
+
* `dsh-rewind:refill,dsh-rewind:portals` — several (comma-separated).
|
|
23
15
|
*
|
|
24
16
|
* @module dsh-rewind/client/log
|
|
25
17
|
*/
|
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
* bubble rows with the session's `next-step` inbox rows.
|
|
4
4
|
*
|
|
5
5
|
* Both sides follow the same host order, so index-primary matching is reliable;
|
|
6
|
-
*
|
|
6
|
+
* the match key is still verified per row, and a row that fails (or has no
|
|
7
7
|
* counterpart) is skipped INDIVIDUALLY: one bad row never takes down the other
|
|
8
|
-
* rows' buttons. The
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
8
|
+
* rows' buttons. The key is `(text, attachment count)`: exact text for a
|
|
9
|
+
* text-only message, the count for one that carries attachments — whose
|
|
10
|
+
* rendered attachment block contributes text no inbox row reproduces.
|
|
11
|
+
*
|
|
12
|
+
* The compared text excludes the bubble's actions container and its
|
|
13
|
+
* message-attachment block (see `bubbleTextOf` in portals.tsx).
|
|
12
14
|
*
|
|
13
15
|
* The browser half lives in `portals.tsx`; this module stays DOM-free so the
|
|
14
16
|
* matching contract is unit-testable in a plain node environment.
|
|
@@ -19,6 +21,8 @@
|
|
|
19
21
|
export interface PendingRow {
|
|
20
22
|
/** The bubble's message text, excluding the actions container (see module doc). */
|
|
21
23
|
readonly text: string;
|
|
24
|
+
/** Message-attachment blocks the row renders; compared against the item's count. */
|
|
25
|
+
readonly attachments: number;
|
|
22
26
|
}
|
|
23
27
|
/** One wire content block of an inbox row (only the fields the derivation reads). */
|
|
24
28
|
export interface InboxBlockLike {
|
|
@@ -51,8 +55,10 @@ export interface InboxLike {
|
|
|
51
55
|
/** One steering occurrence derived from the session's inbox projection. */
|
|
52
56
|
export interface PendingSteeringItem {
|
|
53
57
|
readonly id: string;
|
|
54
|
-
/** Complete editable text; null when the message
|
|
58
|
+
/** Complete editable text; null when the message carries no text block at all. */
|
|
55
59
|
readonly text: string | null;
|
|
60
|
+
/** Non-text blocks (image/file/…) the message carries, in prompt order. */
|
|
61
|
+
readonly attachments: number;
|
|
56
62
|
/** Space-collapsed preview with image/file blocks excluded (the harness QueueDock rule). */
|
|
57
63
|
readonly preview: string;
|
|
58
64
|
}
|
|
@@ -72,11 +78,16 @@ export interface PendingSteeringItem {
|
|
|
72
78
|
*/
|
|
73
79
|
export declare function steeringItemsOf(nextStep: readonly InboxMessageLike[] | undefined): readonly PendingSteeringItem[];
|
|
74
80
|
/**
|
|
75
|
-
* Pair rows to steering items by index, verifying
|
|
81
|
+
* Pair rows to steering items by index, verifying the match key per row: the
|
|
82
|
+
* visible text when the message has no attachments, otherwise the attachment
|
|
83
|
+
* count. Exact text equality cannot apply to an attachment-bearing row — which
|
|
84
|
+
* is why its ↶ button never mounted — because the rendered attachment block
|
|
85
|
+
* contributes text no inbox row reproduces.
|
|
86
|
+
*
|
|
76
87
|
* @param rows - pending bubble rows in DOM order (== render order).
|
|
77
88
|
* @param steering - steering items in host order (== render order).
|
|
78
89
|
* @returns the item id for each row, or null for rows that cannot be matched
|
|
79
|
-
* safely (missing counterpart,
|
|
90
|
+
* safely (missing counterpart, key mismatch). A bad row never affects the
|
|
80
91
|
* other rows.
|
|
81
92
|
*/
|
|
82
93
|
export declare function matchPendingRows(rows: readonly PendingRow[], steering: readonly PendingSteeringItem[]): readonly (string | null)[];
|