dsh-rewind-plugin 0.15.0 → 0.15.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/README.md +1 -1
- package/SECURITY.md +1 -1
- package/lib/client.js +47 -8
- package/lib/index.js +57 -50
- package/lib/types/client/hidden.d.ts +31 -5
- package/lib/types/client/popover.d.ts +1 -1
- package/lib/types/index.d.ts +6 -0
- package/lib/types/locales.d.ts +3 -7
- package/package.json +38 -40
- package/CONTRIBUTING.md +0 -73
- package/assets/screenshots/add-plugin.png +0 -0
- package/assets/screenshots/cleanup-setting.png +0 -0
- package/assets/screenshots/impact-list.png +0 -0
- package/assets/screenshots/mode-popover.png +0 -0
- package/assets/screenshots/rewind-button.png +0 -0
- package/assets/screenshots/rewind-candidates.png +0 -0
- package/docs/README.md +0 -33
- package/docs/architecture.md +0 -145
- package/docs/compat/audit.md +0 -170
- package/docs/compat/diagnostics.md +0 -83
- package/docs/compat/diagnostics.zh.md +0 -61
- package/docs/compat/tracking-boundary.md +0 -217
- package/docs/compat/tracking-boundary.zh.md +0 -131
- package/docs/contract/client-contract.md +0 -58
- package/docs/contract/client-contract.zh.md +0 -54
- package/docs/format.md +0 -271
- package/docs/harness-reference.md +0 -75
- package/docs/release/release.md +0 -136
- package/docs/release/release.zh.md +0 -124
- package/docs/rewind-fix.md +0 -89
- package/docs/rewind-fix.zh.md +0 -89
- package/docs/snapshot-auto-cleanup.md +0 -78
- package/docs/snapshot-auto-cleanup.zh.md +0 -48
package/README.md
CHANGED
|
@@ -187,7 +187,7 @@ node scripts/verify-host.mjs # 端到端验证构建产物
|
|
|
187
187
|
npm version patch && git push origin <branch> --tags
|
|
188
188
|
```
|
|
189
189
|
|
|
190
|
-
一次性 npm 侧配置与完整流程:见 [docs/release/release.
|
|
190
|
+
一次性 npm 侧配置与完整流程:见 [docs/release/release.md](docs/release/release.md)。
|
|
191
191
|
|
|
192
192
|
## 许可
|
|
193
193
|
|
package/SECURITY.md
CHANGED
|
@@ -28,7 +28,7 @@ re-verify the host's own authority boundaries.
|
|
|
28
28
|
|
|
29
29
|
The plugin **automatically captures** before-backups of tracked mutations, but
|
|
30
30
|
**never automatically applies** one. A workspace restore happens only through
|
|
31
|
-
an explicit user-invoked `/rewind @<seq> both` (the per-message ↶ button or the
|
|
31
|
+
an explicit user-invoked `/rewind-plugin @<seq> both` (the per-message ↶ button or the
|
|
32
32
|
command channel), and only when all of the following hold:
|
|
33
33
|
|
|
34
34
|
1. **A validated target**: `planRewind` accepts only a `user/message` seq that
|
package/lib/client.js
CHANGED
|
@@ -35,6 +35,8 @@ __export(index_exports, {
|
|
|
35
35
|
module.exports = __toCommonJS(index_exports);
|
|
36
36
|
|
|
37
37
|
// src/client/hidden.ts
|
|
38
|
+
var ENGINE_COMMAND = "rewind-plugin";
|
|
39
|
+
var ENGINE_COMMANDS = /* @__PURE__ */ new Set([ENGINE_COMMAND, "rewind"]);
|
|
38
40
|
function resolveChatWatch(resolveView, sessionId, cb) {
|
|
39
41
|
const view = resolveView(sessionId);
|
|
40
42
|
return view?.subscribe?.(cb) ?? (() => {
|
|
@@ -86,7 +88,7 @@ function targetSeqOfArgs(args) {
|
|
|
86
88
|
return match !== null ? Number(match[1]) : void 0;
|
|
87
89
|
}
|
|
88
90
|
function isExecutedRewindCommand(node, seq) {
|
|
89
|
-
if (node.name
|
|
91
|
+
if (node.name === null || !ENGINE_COMMANDS.has(node.name) || node.outcome?.kind !== "success") return false;
|
|
90
92
|
if (node.outcome.sourceEventSeq === void 0) return false;
|
|
91
93
|
const args = node.args ?? "";
|
|
92
94
|
return new RegExp(`(?:^|\\s)@${seq}(?:\\s|$)`).test(args);
|
|
@@ -129,6 +131,39 @@ function cutsContain(cuts, seq) {
|
|
|
129
131
|
}
|
|
130
132
|
return false;
|
|
131
133
|
}
|
|
134
|
+
function liveAnchorKeys(snap, cuts) {
|
|
135
|
+
const live = /* @__PURE__ */ new Map();
|
|
136
|
+
for (const key of snap.order) {
|
|
137
|
+
const node = snap.nodes.get(key);
|
|
138
|
+
if (node === void 0 || cutsContain(cuts, node.anchorSeq)) continue;
|
|
139
|
+
const keys = live.get(node.anchorSeq);
|
|
140
|
+
if (keys === void 0) live.set(node.anchorSeq, [key]);
|
|
141
|
+
else keys.push(key);
|
|
142
|
+
}
|
|
143
|
+
return live;
|
|
144
|
+
}
|
|
145
|
+
var EMPTY_KEYS = [];
|
|
146
|
+
function addWithdrawnTurnChrome(snap, cuts, hidden) {
|
|
147
|
+
const locations = snap.locations;
|
|
148
|
+
if (locations === void 0 || cuts.length === 0) return;
|
|
149
|
+
let live;
|
|
150
|
+
for (const key of snap.order) {
|
|
151
|
+
const chrome = snap.nodes.get(key);
|
|
152
|
+
if (chrome === void 0 || chrome.kind !== "turn-process") continue;
|
|
153
|
+
const located = chrome.location?.turn;
|
|
154
|
+
if (located?.start === void 0) continue;
|
|
155
|
+
const turn = located.turn;
|
|
156
|
+
if (typeof turn !== "number") continue;
|
|
157
|
+
const keys = locations.getTurn(turn);
|
|
158
|
+
if (keys.length === 0) continue;
|
|
159
|
+
const rows = keys.map((member) => snap.nodes.get(member)).filter((member) => member !== void 0);
|
|
160
|
+
if (rows.length !== keys.length) continue;
|
|
161
|
+
if (!rows.every((member) => member.key === chrome.key || cutsContain(cuts, member.anchorSeq))) continue;
|
|
162
|
+
live ?? (live = liveAnchorKeys(snap, cuts));
|
|
163
|
+
if ((live.get(chrome.anchorSeq) ?? EMPTY_KEYS).some((holder) => holder !== chrome.key)) continue;
|
|
164
|
+
hidden.add(chrome.anchorSeq);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
132
167
|
function hiddenSeqsOf(snap) {
|
|
133
168
|
const hidden = /* @__PURE__ */ new Set();
|
|
134
169
|
const spans = [];
|
|
@@ -136,7 +171,7 @@ function hiddenSeqsOf(snap) {
|
|
|
136
171
|
const node = snap.nodes.get(key);
|
|
137
172
|
if (node === void 0 || node.kind !== "command") continue;
|
|
138
173
|
const command = node.data;
|
|
139
|
-
if (command.name
|
|
174
|
+
if (command.name === null || !ENGINE_COMMANDS.has(command.name)) continue;
|
|
140
175
|
if (isPreviewCommand(command) || isCandidateCommand(command)) {
|
|
141
176
|
hidden.add(command.seq);
|
|
142
177
|
continue;
|
|
@@ -156,6 +191,10 @@ function hiddenSeqsOf(snap) {
|
|
|
156
191
|
if (node === void 0) continue;
|
|
157
192
|
if (cutsContain(cuts, node.anchorSeq)) hidden.add(node.anchorSeq);
|
|
158
193
|
}
|
|
194
|
+
try {
|
|
195
|
+
addWithdrawnTurnChrome(snap, cuts, hidden);
|
|
196
|
+
} catch {
|
|
197
|
+
}
|
|
159
198
|
return hidden;
|
|
160
199
|
}
|
|
161
200
|
|
|
@@ -547,13 +586,13 @@ function waitForCommand(session, chatOf, match, timeoutMs = 8e3, watch) {
|
|
|
547
586
|
}
|
|
548
587
|
function isPreviewFor(node, seq) {
|
|
549
588
|
const args = node.args ?? "";
|
|
550
|
-
return node.name
|
|
589
|
+
return node.name !== null && ENGINE_COMMANDS.has(node.name) && args.includes("preview") && new RegExp(`(?:^|\\s)@${seq}(?=\\s|$)`).test(args);
|
|
551
590
|
}
|
|
552
591
|
async function previewImpact(session, chatOf, seq, watch) {
|
|
553
592
|
const known = knownCommandSeqs(session, chatOf, (node) => isPreviewFor(node, seq));
|
|
554
593
|
let result;
|
|
555
594
|
try {
|
|
556
|
-
result = await session.command(
|
|
595
|
+
result = await session.command(`/${ENGINE_COMMAND} preview @${seq} both`);
|
|
557
596
|
} catch (error) {
|
|
558
597
|
rewindLog.warn("preview", `preview command threw for @${seq}`, error);
|
|
559
598
|
return { kind: "error", text: error instanceof Error ? error.message : String(error) };
|
|
@@ -924,7 +963,7 @@ async function runRewindAndFill(session, seq, mode, isMainViewSession, chatOf, w
|
|
|
924
963
|
const known = knownCommandSeqs(session, chatOf, (node) => isExecutedRewindCommand(node, seq));
|
|
925
964
|
let result;
|
|
926
965
|
try {
|
|
927
|
-
result = await session.command(
|
|
966
|
+
result = await session.command(`/${ENGINE_COMMAND} @${seq} ${mode}`);
|
|
928
967
|
} catch (error) {
|
|
929
968
|
rewindLog.warn("refill", `rewind command threw, skipping refill @${seq}`, error);
|
|
930
969
|
return;
|
|
@@ -1320,9 +1359,9 @@ function createRewindBridge(deps) {
|
|
|
1320
1359
|
}
|
|
1321
1360
|
|
|
1322
1361
|
// src/client/build-info.ts
|
|
1323
|
-
var PLUGIN_VERSION = true ? "0.15.
|
|
1362
|
+
var PLUGIN_VERSION = true ? "0.15.2" : "dev";
|
|
1324
1363
|
var PLUGIN_PACKAGE = "dsh-rewind-plugin";
|
|
1325
|
-
var BUILD_HASH = true ? "
|
|
1364
|
+
var BUILD_HASH = true ? "75d20acc" : "dev";
|
|
1326
1365
|
|
|
1327
1366
|
// src/client/locales.ts
|
|
1328
1367
|
var zh = {
|
|
@@ -1646,7 +1685,7 @@ function apply(ctx) {
|
|
|
1646
1685
|
};
|
|
1647
1686
|
const fetchHostCandidates = async (face, chatOf2) => {
|
|
1648
1687
|
const known = knownCommandSeqs(face, chatOf2, (node) => isCandidateCommand(node));
|
|
1649
|
-
const result = await face.command(
|
|
1688
|
+
const result = await face.command(`/${ENGINE_COMMAND} __candidates`);
|
|
1650
1689
|
if (!result.ok || result.value?.matched !== true) return void 0;
|
|
1651
1690
|
const outcome = await waitForCommand(face, chatOf2, (node) => isCandidateCommand(node) && !known.has(node.seq), 8e3, (cb) => watchChat(face.sessionId, cb));
|
|
1652
1691
|
if (outcome === null || outcome.kind !== "success" || outcome.text === void 0) return void 0;
|
package/lib/index.js
CHANGED
|
@@ -9,12 +9,6 @@ function preferredLocale(preference) {
|
|
|
9
9
|
return preference.toLowerCase().split("-")[0] === "zh" ? "zh" : "en";
|
|
10
10
|
}
|
|
11
11
|
var en = {
|
|
12
|
-
"usage.title": "Usage:",
|
|
13
|
-
"usage.noArgs": " /rewind (no args) withdraw the most recent user message",
|
|
14
|
-
"usage.seq": " /rewind @<seq> chat|both rewind to the given message (chat = conversation only / both = conversation + files)",
|
|
15
|
-
"usage.blocked": " /rewind or /undo open the rewind picker",
|
|
16
|
-
"describeTarget.seq": "seq {seq}",
|
|
17
|
-
"describeTarget.index": "message {index}",
|
|
18
12
|
"plan.rewinding": "Rewind to seq {targetSeq}, removing {count} node(s) from the model context (conversation log kept).",
|
|
19
13
|
"plan.affects": "Affects {count} file(s):",
|
|
20
14
|
"plan.restore": "restore {path}",
|
|
@@ -39,9 +33,11 @@ var en = {
|
|
|
39
33
|
"storeUnsupported": "file restore unavailable: these snapshots use a newer store format (v{version}) than this plugin understands; nothing was changed. Update the plugin or clear this session's snapshots.",
|
|
40
34
|
"success": "Withdrawn seq {targetSeq} and everything after it (conversation returned to earlier){restore}.",
|
|
41
35
|
"noUserMessages": "This session has no rewindable user messages yet.",
|
|
42
|
-
"chooseMode": "Rewind to {target}. Choose a mode:\n /rewind {target} chat conversation only\n /rewind {target} both conversation + file restore",
|
|
43
36
|
"command.description": "Rewind the conversation and/or code to a previous point",
|
|
44
37
|
"command.undoDescription": "Alias of /rewind",
|
|
38
|
+
"engine.description": "dsh-rewind internal command",
|
|
39
|
+
"engine.rejected": "Internal command: use /rewind or /undo.",
|
|
40
|
+
"engine.inputHint": "@<seq> chat|both",
|
|
45
41
|
"cleanup.description": "Manage automatic cleanup of session snapshot backups",
|
|
46
42
|
"cleanup.inputHint": "on | off | max-age <days> | run [--apply] [--current]",
|
|
47
43
|
"cleanup.status": "Auto-cleanup: {state}. Max age: {days} day(s).",
|
|
@@ -64,12 +60,6 @@ var en = {
|
|
|
64
60
|
"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"
|
|
65
61
|
};
|
|
66
62
|
var zh = {
|
|
67
|
-
"usage.title": "\u7528\u6CD5\uFF1A",
|
|
68
|
-
"usage.noArgs": " /rewind \uFF08\u65E0\u53C2\u6570\uFF09\u64A4\u56DE\u6700\u8FD1\u4E00\u6761\u7528\u6237\u6D88\u606F",
|
|
69
|
-
"usage.seq": " /rewind @<seq> chat|both \u56DE\u9000\u5230\u6307\u5B9A\u6D88\u606F\uFF08chat \u4EC5\u5BF9\u8BDD / both \u5BF9\u8BDD+\u6587\u4EF6\uFF09",
|
|
70
|
-
"usage.blocked": " /rewind \u6216 /undo \u6253\u5F00\u56DE\u9000\u9009\u62E9\u9762\u677F",
|
|
71
|
-
"describeTarget.seq": "seq {seq}",
|
|
72
|
-
"describeTarget.index": "\u7B2C {index} \u6761\u6D88\u606F",
|
|
73
63
|
"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",
|
|
74
64
|
"plan.affects": "\u5C06\u5F71\u54CD {count} \u4E2A\u6587\u4EF6\uFF1A",
|
|
75
65
|
"plan.restore": "\u8FD8\u539F {path}",
|
|
@@ -94,9 +84,11 @@ var zh = {
|
|
|
94
84
|
"storeUnsupported": "\u6587\u4EF6\u8FD8\u539F\u4E0D\u53EF\u7528\uFF1A\u8FD9\u4E9B\u5FEB\u7167\u4F7F\u7528\u4E86\u6BD4\u672C\u63D2\u4EF6\u66F4\u65B0\u7684\u5B58\u50A8\u683C\u5F0F\uFF08v{version}\uFF09\uFF1B\u5DE5\u4F5C\u533A\u672A\u505A\u4EFB\u4F55\u6539\u52A8\u3002\u8BF7\u66F4\u65B0\u63D2\u4EF6\uFF0C\u6216\u6E05\u9664\u8BE5\u4F1A\u8BDD\u7684\u5FEB\u7167\u3002",
|
|
95
85
|
"success": "\u5DF2\u64A4\u56DE seq {targetSeq} \u53CA\u4E4B\u540E\u5185\u5BB9\uFF08\u5BF9\u8BDD\u5DF2\u56DE\u5230\u6B64\u524D\uFF09{restore}\u3002",
|
|
96
86
|
"noUserMessages": "\u5F53\u524D\u4F1A\u8BDD\u8FD8\u6CA1\u6709\u53EF\u56DE\u9000\u7684\u7528\u6237\u6D88\u606F\u3002",
|
|
97
|
-
"chooseMode": "\u5C06\u56DE\u9000\u5230 {target}\u3002\u9009\u62E9\u6A21\u5F0F\uFF1A\n /rewind {target} chat \u4EC5\u56DE\u9000\u5BF9\u8BDD\n /rewind {target} both \u56DE\u9000\u5BF9\u8BDD\u5E76\u8FD8\u539F\u6587\u4EF6",
|
|
98
87
|
"command.description": "\u56DE\u9000\u5BF9\u8BDD\u548C/\u6216\u4EE3\u7801\u5230\u4E4B\u524D\u7684\u67D0\u6761\u6D88\u606F",
|
|
99
88
|
"command.undoDescription": "/rewind \u7684\u522B\u540D",
|
|
89
|
+
"engine.description": "dsh-rewind \u5185\u90E8\u547D\u4EE4",
|
|
90
|
+
"engine.rejected": "\u5185\u90E8\u547D\u4EE4\uFF1A\u8BF7\u4F7F\u7528 /rewind \u6216 /undo\u3002",
|
|
91
|
+
"engine.inputHint": "@<seq> chat|both",
|
|
100
92
|
"cleanup.description": "\u7BA1\u7406\u4F1A\u8BDD\u5FEB\u7167\u5907\u4EFD\u7684\u81EA\u52A8\u6E05\u7406",
|
|
101
93
|
"cleanup.inputHint": "on | off | max-age <\u5929\u6570> | run [--apply] [--current]",
|
|
102
94
|
"cleanup.status": "\u81EA\u52A8\u6E05\u7406\uFF1A{state}\u3002\u6700\u5927\u4FDD\u7559\u5929\u6570\uFF1A{days} \u5929\u3002",
|
|
@@ -2332,6 +2324,7 @@ function shouldRunAutoSweep(lastAtMs, nowMs) {
|
|
|
2332
2324
|
// src/index.ts
|
|
2333
2325
|
var name = "dsh-rewind";
|
|
2334
2326
|
var inject = ["commands", "tools", "settings"];
|
|
2327
|
+
var ENGINE_COMMAND = "rewind-plugin";
|
|
2335
2328
|
var Config = z.object({
|
|
2336
2329
|
snapshotDir: z.string().required(false),
|
|
2337
2330
|
dshHome: z.string().required(false),
|
|
@@ -2359,13 +2352,11 @@ var cleanupStore;
|
|
|
2359
2352
|
function t(key, params) {
|
|
2360
2353
|
return translate(activeLocale, key, params);
|
|
2361
2354
|
}
|
|
2362
|
-
function
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
t("usage.blocked")
|
|
2368
|
-
].join("\n");
|
|
2355
|
+
function warnCommandUnavailable(ctx, name2, error) {
|
|
2356
|
+
ctx.logger.warn(`[dsh-rewind] command "${name2}" was not registered: ${error instanceof Error ? error.message : String(error)}`);
|
|
2357
|
+
if (name2 === ENGINE_COMMAND) {
|
|
2358
|
+
ctx.logger.warn(`[dsh-rewind] the internal "${ENGINE_COMMAND}" engine channel is unavailable; the rewind button and picker will not work.`);
|
|
2359
|
+
}
|
|
2369
2360
|
}
|
|
2370
2361
|
async function discardCapture(capture) {
|
|
2371
2362
|
if (capture === void 0 || capture.backup === null) return;
|
|
@@ -2485,9 +2476,6 @@ function buildMarker() {
|
|
|
2485
2476
|
source: REWIND_MARKER_SOURCE
|
|
2486
2477
|
});
|
|
2487
2478
|
}
|
|
2488
|
-
function describeTarget(target) {
|
|
2489
|
-
return target.kind === "seq" ? t("describeTarget.seq", { seq: target.seq }) : t("describeTarget.index", { index: target.index });
|
|
2490
|
-
}
|
|
2491
2479
|
function formatBytes(bytes) {
|
|
2492
2480
|
if (bytes >= 1024 * 1024) return `${Math.round(bytes / (1024 * 1024))} MiB`;
|
|
2493
2481
|
if (bytes >= 1024) return `${Math.round(bytes / 1024)} KiB`;
|
|
@@ -2681,7 +2669,7 @@ async function handleRewind(ctx, store, fs, invocation, inflight) {
|
|
|
2681
2669
|
const parts = input.split(/\s+/);
|
|
2682
2670
|
if (parts[0] === "preview") {
|
|
2683
2671
|
const target2 = parts[1];
|
|
2684
|
-
if (target2 === void 0) return { kind: "error", text:
|
|
2672
|
+
if (target2 === void 0) return { kind: "error", text: t("engine.rejected") };
|
|
2685
2673
|
let plan;
|
|
2686
2674
|
try {
|
|
2687
2675
|
plan = resolveOrError(session.snapshotEvents(), session.surface.nodes, target2);
|
|
@@ -2705,16 +2693,9 @@ async function handleRewind(ctx, store, fs, invocation, inflight) {
|
|
|
2705
2693
|
const target = parts[0];
|
|
2706
2694
|
const mode = parts[1];
|
|
2707
2695
|
if (mode !== void 0 && mode !== "chat" && mode !== "both") {
|
|
2708
|
-
return { kind: "error", text:
|
|
2709
|
-
}
|
|
2710
|
-
if (mode === void 0) {
|
|
2711
|
-
const parsed = parseRewindTarget(target);
|
|
2712
|
-
if (parsed === void 0) return { kind: "error", text: usage() };
|
|
2713
|
-
return {
|
|
2714
|
-
kind: "success",
|
|
2715
|
-
text: t("chooseMode", { target: describeTarget(parsed) })
|
|
2716
|
-
};
|
|
2696
|
+
return { kind: "error", text: t("engine.rejected") };
|
|
2717
2697
|
}
|
|
2698
|
+
if (mode === void 0) return { kind: "error", text: t("engine.rejected") };
|
|
2718
2699
|
return executeRewind(ctx, store, fs, invocation, target, mode, inflight);
|
|
2719
2700
|
}
|
|
2720
2701
|
var autoSweepChecked = false;
|
|
@@ -2892,22 +2873,47 @@ function apply(ctx, config) {
|
|
|
2892
2873
|
const rewindHandler = withHostLocale(
|
|
2893
2874
|
(invocation) => handleRewind(ctx, store, fsService, invocation, inflight)
|
|
2894
2875
|
);
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
})
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
|
|
2908
|
-
|
|
2909
|
-
|
|
2910
|
-
|
|
2876
|
+
const engineHandler = withHostLocale(
|
|
2877
|
+
(invocation) => invocation.rawInput.trim() === "" ? Promise.resolve({ kind: "error", text: t("engine.rejected") }) : handleRewind(ctx, store, fsService, invocation, inflight)
|
|
2878
|
+
);
|
|
2879
|
+
try {
|
|
2880
|
+
yield ctx.commands.register({
|
|
2881
|
+
name: "rewind",
|
|
2882
|
+
description: t("command.description"),
|
|
2883
|
+
handler: rewindHandler
|
|
2884
|
+
});
|
|
2885
|
+
} catch (error) {
|
|
2886
|
+
warnCommandUnavailable(ctx, "rewind", error);
|
|
2887
|
+
}
|
|
2888
|
+
try {
|
|
2889
|
+
yield ctx.commands.register({
|
|
2890
|
+
name: "undo",
|
|
2891
|
+
description: t("command.undoDescription"),
|
|
2892
|
+
handler: rewindHandler
|
|
2893
|
+
});
|
|
2894
|
+
} catch (error) {
|
|
2895
|
+
warnCommandUnavailable(ctx, "undo", error);
|
|
2896
|
+
}
|
|
2897
|
+
try {
|
|
2898
|
+
yield ctx.commands.register({
|
|
2899
|
+
name: ENGINE_COMMAND,
|
|
2900
|
+
description: t("engine.description"),
|
|
2901
|
+
input: { hint: t("engine.inputHint") },
|
|
2902
|
+
handler: engineHandler
|
|
2903
|
+
});
|
|
2904
|
+
} catch (error) {
|
|
2905
|
+
warnCommandUnavailable(ctx, ENGINE_COMMAND, error);
|
|
2906
|
+
}
|
|
2907
|
+
try {
|
|
2908
|
+
yield ctx.commands.register({
|
|
2909
|
+
name: "snapshot-auto-cleanup",
|
|
2910
|
+
description: t("cleanup.description"),
|
|
2911
|
+
input: { hint: t("cleanup.inputHint") },
|
|
2912
|
+
handler: withHostLocale((invocation) => handleSnapshotCleanup(store, invocation, dshHome, trackedBySession))
|
|
2913
|
+
});
|
|
2914
|
+
} catch (error) {
|
|
2915
|
+
warnCommandUnavailable(ctx, "snapshot-auto-cleanup", error);
|
|
2916
|
+
}
|
|
2911
2917
|
}, "dsh-rewind command");
|
|
2912
2918
|
ctx.on("agent/created", ({ agent }) => {
|
|
2913
2919
|
const session = agent.session;
|
|
@@ -2987,6 +2993,7 @@ function apply(ctx, config) {
|
|
|
2987
2993
|
}
|
|
2988
2994
|
export {
|
|
2989
2995
|
Config,
|
|
2996
|
+
ENGINE_COMMAND,
|
|
2990
2997
|
SnapshotStore,
|
|
2991
2998
|
apply,
|
|
2992
2999
|
cleanupConfigKey,
|
|
@@ -6,6 +6,17 @@
|
|
|
6
6
|
* @module dsh-rewind/client/hidden
|
|
7
7
|
*/
|
|
8
8
|
import type { CommandNode } from '@deepseek-ai/dsh-client-ui-conversation/client';
|
|
9
|
+
/**
|
|
10
|
+
* The internal engine channel every client call is sent to. Mirrored by
|
|
11
|
+
* `ENGINE_COMMAND` in `src/index.ts` — the two bundles share no module.
|
|
12
|
+
*/
|
|
13
|
+
export declare const ENGINE_COMMAND = "rewind-plugin";
|
|
14
|
+
/**
|
|
15
|
+
* Recognized engine command names. `rewind` is what sessions written before the
|
|
16
|
+
* engine channel existed contain, so keeping it preserves their withdrawn
|
|
17
|
+
* spans (forward compatibility only: an older client cannot know the new name).
|
|
18
|
+
*/
|
|
19
|
+
export declare const ENGINE_COMMANDS: ReadonlySet<string>;
|
|
9
20
|
/** A chat-snapshot view node as the hiding / composer-refill logic reads it.
|
|
10
21
|
* The harness's view node shape plus the `anchorSeq` the chat snapshot carries
|
|
11
22
|
* (the plugin reads `anchorSeq` off each node; it is not declared on the
|
|
@@ -15,6 +26,14 @@ export interface ChatConversationViewNode {
|
|
|
15
26
|
readonly kind?: string;
|
|
16
27
|
readonly data?: unknown;
|
|
17
28
|
readonly anchorSeq: number;
|
|
29
|
+
/** Owning Turn as the chat snapshot carries it; its `turn` and `start` are read
|
|
30
|
+
* (see `addWithdrawnTurnChrome`). */
|
|
31
|
+
readonly location?: {
|
|
32
|
+
readonly turn?: {
|
|
33
|
+
readonly turn?: unknown;
|
|
34
|
+
readonly start?: unknown;
|
|
35
|
+
};
|
|
36
|
+
};
|
|
18
37
|
}
|
|
19
38
|
/** Minimal chat snapshot reader the hiding logic needs. */
|
|
20
39
|
export interface HiddenChat {
|
|
@@ -22,6 +41,10 @@ export interface HiddenChat {
|
|
|
22
41
|
readonly nodes: {
|
|
23
42
|
get(key: string): ChatConversationViewNode | undefined;
|
|
24
43
|
};
|
|
44
|
+
/** Turn-keyed node index; absent on older snapshots, which the chrome pass tolerates. */
|
|
45
|
+
readonly locations?: {
|
|
46
|
+
getTurn(turn: number): readonly string[];
|
|
47
|
+
};
|
|
25
48
|
}
|
|
26
49
|
/**
|
|
27
50
|
* Reader for one session's live chat snapshot: the chat is served by the
|
|
@@ -103,9 +126,9 @@ export declare function messageAttachmentsAt(chat: HiddenChat | undefined, seq:
|
|
|
103
126
|
*/
|
|
104
127
|
export declare function targetSeqOfArgs(args: string | null | undefined): number | undefined;
|
|
105
128
|
/**
|
|
106
|
-
* True when
|
|
107
|
-
* admission form the popover drives (`@<seq> chat` / `both`) that
|
|
108
|
-
* with a marker-carrying success outcome. The composer refill waits for
|
|
129
|
+
* True when one of this plugin's engine command nodes is an EXECUTED rewind for
|
|
130
|
+
* `seq` — the admission form the popover drives (`@<seq> chat` / `both`) that
|
|
131
|
+
* settled with a marker-carrying success outcome. The composer refill waits for
|
|
109
132
|
* exactly this node after the user confirms, so a history-loaded command can
|
|
110
133
|
* never trigger a fill.
|
|
111
134
|
*/
|
|
@@ -122,8 +145,8 @@ export declare function isExecutedRewindCommand(node: CommandNode, seq: number):
|
|
|
122
145
|
*/
|
|
123
146
|
export declare function hasFileImpact(text: string | undefined): boolean;
|
|
124
147
|
/**
|
|
125
|
-
* True when
|
|
126
|
-
* (
|
|
148
|
+
* True when an engine command node is the internal candidate-list probe
|
|
149
|
+
* (`__candidates`) the popupSelect runs to fetch the FULL candidate
|
|
127
150
|
* list from the host. Like previews, its flow node never surfaces in the
|
|
128
151
|
* transcript — it only feeds the popup — so it is hidden in every state.
|
|
129
152
|
*/
|
|
@@ -140,5 +163,8 @@ export declare function isCandidateCommand(command: CommandNode): boolean;
|
|
|
140
163
|
* still-on-surface gap of new traffic between an earlier marker and a later
|
|
141
164
|
* target. Endpoints come from the command nodes (`sourceEventSeq` is the
|
|
142
165
|
* marker's log seq; the outcome text carries the target seq).
|
|
166
|
+
*
|
|
167
|
+
* A Turn whose rows were ALL withdrawn also contributes its Turn-process summary
|
|
168
|
+
* row, whose own anchor can precede the target (see `addWithdrawnTurnChrome`).
|
|
143
169
|
*/
|
|
144
170
|
export declare function hiddenSeqsOf(snap: HiddenChat): Set<number>;
|
|
@@ -2,7 +2,7 @@
|
|
|
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
4
|
* offers the two modes. Choosing "both" first fetches the impact list through
|
|
5
|
-
* the
|
|
5
|
+
* the engine channel's `preview @seq both` command 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
|
package/lib/types/index.d.ts
CHANGED
|
@@ -29,6 +29,12 @@ export { SnapshotStore } from './snapshot.ts';
|
|
|
29
29
|
export type { CheckpointEntry, FileImpact, PruneStaleReport, RestoreOutcome, RestoreJournal, RestoreJournalState, RestoreReconcileReport } from './snapshot.ts';
|
|
30
30
|
export declare const name = "dsh-rewind";
|
|
31
31
|
export declare const inject: string[];
|
|
32
|
+
/**
|
|
33
|
+
* The internal engine channel the client drives, so the rewind surface never
|
|
34
|
+
* depends on a name a third-party plugin can take. Mirrored by `ENGINE_COMMAND`
|
|
35
|
+
* in `src/client/hidden.ts` — the two bundles share no module.
|
|
36
|
+
*/
|
|
37
|
+
export declare const ENGINE_COMMAND = "rewind-plugin";
|
|
32
38
|
/** Plugin config. */
|
|
33
39
|
export interface RewindConfig {
|
|
34
40
|
/** Checkpoint store root (exact path; beats `DSH_REWIND_SNAPSHOT_DIR` and the harness-home default). */
|
package/lib/types/locales.d.ts
CHANGED
|
@@ -45,12 +45,6 @@ export type HostLocaleId = 'zh' | 'en';
|
|
|
45
45
|
export declare function preferredLocale(preference: unknown): HostLocaleId;
|
|
46
46
|
/** English dictionary — the key-set source of truth (neutral default). */
|
|
47
47
|
export declare const en: {
|
|
48
|
-
'usage.title': string;
|
|
49
|
-
'usage.noArgs': string;
|
|
50
|
-
'usage.seq': string;
|
|
51
|
-
'usage.blocked': string;
|
|
52
|
-
'describeTarget.seq': string;
|
|
53
|
-
'describeTarget.index': string;
|
|
54
48
|
'plan.rewinding': string;
|
|
55
49
|
'plan.affects': string;
|
|
56
50
|
'plan.restore': string;
|
|
@@ -75,9 +69,11 @@ export declare const en: {
|
|
|
75
69
|
storeUnsupported: string;
|
|
76
70
|
success: string;
|
|
77
71
|
noUserMessages: string;
|
|
78
|
-
chooseMode: string;
|
|
79
72
|
'command.description': string;
|
|
80
73
|
'command.undoDescription': string;
|
|
74
|
+
'engine.description': string;
|
|
75
|
+
'engine.rejected': string;
|
|
76
|
+
'engine.inputHint': string;
|
|
81
77
|
'cleanup.description': string;
|
|
82
78
|
'cleanup.inputHint': string;
|
|
83
79
|
'cleanup.status': string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-rewind-plugin",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.2",
|
|
4
4
|
"description": "In-window conversation rewind with workspace file restore · 同窗口内对话回退并可还原工作区文件",
|
|
5
5
|
"icon": "./assets/icon.svg",
|
|
6
6
|
"keywords": [
|
|
@@ -43,11 +43,9 @@
|
|
|
43
43
|
"README.md",
|
|
44
44
|
"README.en.md",
|
|
45
45
|
"SECURITY.md",
|
|
46
|
-
"CONTRIBUTING.md",
|
|
47
|
-
"docs",
|
|
48
|
-
"assets",
|
|
49
46
|
"LICENSE",
|
|
50
|
-
"locale/*.json"
|
|
47
|
+
"locale/*.json",
|
|
48
|
+
"assets/icon.svg"
|
|
51
49
|
],
|
|
52
50
|
"dsh": {
|
|
53
51
|
"bundle": {
|
|
@@ -145,41 +143,41 @@
|
|
|
145
143
|
"devDependencies": {
|
|
146
144
|
"@deepseek-ai/cordis": "4.0.3",
|
|
147
145
|
"@deepseek-ai/cosmokit": "1.8.4",
|
|
148
|
-
"@deepseek-ai/dsh-agent": "0.2.
|
|
149
|
-
"@deepseek-ai/dsh-api-session-controller": "0.2.
|
|
150
|
-
"@deepseek-ai/dsh-attachment": "0.2.
|
|
151
|
-
"@deepseek-ai/dsh-client-connection": "0.2.
|
|
152
|
-
"@deepseek-ai/dsh-client-locale": "0.2.
|
|
153
|
-
"@deepseek-ai/dsh-client-store": "0.2.
|
|
154
|
-
"@deepseek-ai/dsh-client-ui-commands": "0.2.
|
|
155
|
-
"@deepseek-ai/dsh-client-ui-conversation": "0.2.
|
|
156
|
-
"@deepseek-ai/dsh-client-ui-input-trigger": "0.2.
|
|
157
|
-
"@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.
|
|
158
|
-
"@deepseek-ai/dsh-client-ui-primitives": "0.2.
|
|
159
|
-
"@deepseek-ai/dsh-client-ui-session": "0.2.
|
|
160
|
-
"@deepseek-ai/dsh-client-ui-settings": "0.2.
|
|
161
|
-
"@deepseek-ai/dsh-client-ui-slots": "0.2.
|
|
162
|
-
"@deepseek-ai/dsh-command-compact": "0.2.
|
|
163
|
-
"@deepseek-ai/dsh-commands": "0.2.
|
|
164
|
-
"@deepseek-ai/dsh-compaction": "0.2.
|
|
165
|
-
"@deepseek-ai/dsh-compaction-basic": "0.2.
|
|
166
|
-
"@deepseek-ai/dsh-compaction-image-offload": "0.2.
|
|
167
|
-
"@deepseek-ai/dsh-fs": "0.2.
|
|
168
|
-
"@deepseek-ai/dsh-goal": "0.2.
|
|
169
|
-
"@deepseek-ai/dsh-home-paths": "0.2.
|
|
170
|
-
"@deepseek-ai/dsh-llm": "0.2.
|
|
171
|
-
"@deepseek-ai/dsh-plan-mode": "0.2.
|
|
172
|
-
"@deepseek-ai/dsh-sandbox": "0.2.
|
|
173
|
-
"@deepseek-ai/dsh-scope": "0.2.
|
|
174
|
-
"@deepseek-ai/dsh-session": "0.2.
|
|
175
|
-
"@deepseek-ai/dsh-session-projection": "0.2.
|
|
176
|
-
"@deepseek-ai/dsh-session-stats": "0.2.
|
|
177
|
-
"@deepseek-ai/dsh-session-title": "0.2.
|
|
178
|
-
"@deepseek-ai/dsh-settings": "0.2.
|
|
179
|
-
"@deepseek-ai/dsh-token-meter": "0.2.
|
|
180
|
-
"@deepseek-ai/dsh-tools": "0.2.
|
|
181
|
-
"@deepseek-ai/dsh-typert-protocol": "0.2.
|
|
182
|
-
"@deepseek-ai/dsh-user-questions": "0.2.
|
|
146
|
+
"@deepseek-ai/dsh-agent": "0.2.1-alpha.1",
|
|
147
|
+
"@deepseek-ai/dsh-api-session-controller": "0.2.1-alpha.1",
|
|
148
|
+
"@deepseek-ai/dsh-attachment": "0.2.1-alpha.1",
|
|
149
|
+
"@deepseek-ai/dsh-client-connection": "0.2.1-alpha.1",
|
|
150
|
+
"@deepseek-ai/dsh-client-locale": "0.2.1-alpha.1",
|
|
151
|
+
"@deepseek-ai/dsh-client-store": "0.2.1-alpha.1",
|
|
152
|
+
"@deepseek-ai/dsh-client-ui-commands": "0.2.1-alpha.1",
|
|
153
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.2.1-alpha.1",
|
|
154
|
+
"@deepseek-ai/dsh-client-ui-input-trigger": "0.2.1-alpha.1",
|
|
155
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.1-alpha.1",
|
|
156
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.2.1-alpha.1",
|
|
157
|
+
"@deepseek-ai/dsh-client-ui-session": "0.2.1-alpha.1",
|
|
158
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.2.1-alpha.1",
|
|
159
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.2.1-alpha.1",
|
|
160
|
+
"@deepseek-ai/dsh-command-compact": "0.2.1-alpha.1",
|
|
161
|
+
"@deepseek-ai/dsh-commands": "0.2.1-alpha.1",
|
|
162
|
+
"@deepseek-ai/dsh-compaction": "0.2.1-alpha.1",
|
|
163
|
+
"@deepseek-ai/dsh-compaction-basic": "0.2.1-alpha.1",
|
|
164
|
+
"@deepseek-ai/dsh-compaction-image-offload": "0.2.1-alpha.1",
|
|
165
|
+
"@deepseek-ai/dsh-fs": "0.2.1-alpha.1",
|
|
166
|
+
"@deepseek-ai/dsh-goal": "0.2.1-alpha.1",
|
|
167
|
+
"@deepseek-ai/dsh-home-paths": "0.2.1-alpha.1",
|
|
168
|
+
"@deepseek-ai/dsh-llm": "0.2.1-alpha.1",
|
|
169
|
+
"@deepseek-ai/dsh-plan-mode": "0.2.1-alpha.1",
|
|
170
|
+
"@deepseek-ai/dsh-sandbox": "0.2.1-alpha.1",
|
|
171
|
+
"@deepseek-ai/dsh-scope": "0.2.1-alpha.1",
|
|
172
|
+
"@deepseek-ai/dsh-session": "0.2.1-alpha.1",
|
|
173
|
+
"@deepseek-ai/dsh-session-projection": "0.2.1-alpha.1",
|
|
174
|
+
"@deepseek-ai/dsh-session-stats": "0.2.1-alpha.1",
|
|
175
|
+
"@deepseek-ai/dsh-session-title": "0.2.1-alpha.1",
|
|
176
|
+
"@deepseek-ai/dsh-settings": "0.2.1-alpha.1",
|
|
177
|
+
"@deepseek-ai/dsh-token-meter": "0.2.1-alpha.1",
|
|
178
|
+
"@deepseek-ai/dsh-tools": "0.2.1-alpha.1",
|
|
179
|
+
"@deepseek-ai/dsh-typert-protocol": "0.2.1-alpha.1",
|
|
180
|
+
"@deepseek-ai/dsh-user-questions": "0.2.1-alpha.1",
|
|
183
181
|
"@deepseek-ai/schemastery": "3.18.4",
|
|
184
182
|
"@types/node": "^24.0.0",
|
|
185
183
|
"@types/react": "^18.3.31",
|
package/CONTRIBUTING.md
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
# Contributing
|
|
2
|
-
|
|
3
|
-
Thanks for considering a contribution to dsh-rewind. This file is short on
|
|
4
|
-
purpose: the authoritative spec for how the repo works is `AGENTS.md` (read it
|
|
5
|
-
first), and the docs live under `docs/` with an index in `docs/README.md`.
|
|
6
|
-
|
|
7
|
-
## Project principles
|
|
8
|
-
|
|
9
|
-
- **Focused on purpose** — one thing: in-window rewind to any earlier user
|
|
10
|
-
message, never forking a session.
|
|
11
|
-
- **Security first** — session logs are append-only; file restores draw only from
|
|
12
|
-
the plugin's own backup directory. See `SECURITY.md`.
|
|
13
|
-
- **Minimal** — avoid over-abstraction; keep the plugin light and maintainable.
|
|
14
|
-
|
|
15
|
-
## Prerequisites
|
|
16
|
-
|
|
17
|
-
- Node `^22.19.0 || >=24.0.0` (see `engines` in `package.json`), npm.
|
|
18
|
-
|
|
19
|
-
## Setup and commands
|
|
20
|
-
|
|
21
|
-
```sh
|
|
22
|
-
npm install # devDeps from the npm registry
|
|
23
|
-
npm run build # esbuild → lib/ (host ESM + client closure + types)
|
|
24
|
-
npm run check # one-shot full gate: typecheck + test + build + verify:host + pack --dry-run
|
|
25
|
-
npm run typecheck # tsc --noEmit (host / client / client-test)
|
|
26
|
-
npm test # vitest: unit + compatibility suites
|
|
27
|
-
npm run verify:host # end-to-end host verification (full check suite)
|
|
28
|
-
```
|
|
29
|
-
|
|
30
|
-
`prepare` runs the build, so `npm pack` / `npm publish` always carry a fresh
|
|
31
|
-
`lib/` and `LICENSE`.
|
|
32
|
-
|
|
33
|
-
## Before you open a PR
|
|
34
|
-
|
|
35
|
-
- **Every change must pass** `npm run check`.
|
|
36
|
-
- Commit messages use conventional commits with **English** subjects
|
|
37
|
-
(`feat` / `fix` / `docs` / `test` / `refactor` / `chore` / …). Code comments
|
|
38
|
-
are written in English.
|
|
39
|
-
- Keep `rewind.ts` pure and `snapshot.ts` host-independent — if a change needs
|
|
40
|
-
I/O or harness types in the planning layer, that is a design smell.
|
|
41
|
-
|
|
42
|
-
## Documentation rules
|
|
43
|
-
|
|
44
|
-
- New/changed behavior that is durable (formats, contracts, compatibility
|
|
45
|
-
findings) must update the relevant doc in the same PR:
|
|
46
|
-
- **`docs/format.md`** — any change to the on-disk format (bump version,
|
|
47
|
-
migrate, or move the state root; no silent re-interpretation).
|
|
48
|
-
- **`docs/contract/client-contract.md`** — any change to the meaning of
|
|
49
|
-
`@<seq>` / `sourceEventSeq` / `data-dsh-rewind-hidden`; breaking a listed
|
|
50
|
-
stability tier is a minor/major version bump.
|
|
51
|
-
- **`docs/compat/audit.md`** — new compatibility findings (it is the single
|
|
52
|
-
source of truth; other docs link to it instead of restating).
|
|
53
|
-
- **`SECURITY.md`** — any change to the security model (trust boundary,
|
|
54
|
-
mutation gates, containment, crash handling).
|
|
55
|
-
- Bilingual docs use the `.md` / `.zh.md` file split; keep the two mirrors in
|
|
56
|
-
sync.
|
|
57
|
-
|
|
58
|
-
## Testing expectations
|
|
59
|
-
|
|
60
|
-
- Pure planning (`rewind.ts`, `hidden.ts`) → unit tests in `tests/`.
|
|
61
|
-
- Store behavior (`snapshot.ts`) → `tests/snapshot.test.ts` plus the focused
|
|
62
|
-
suites (`snapshot-bytes`, `snapshot-mode`, `downgrade-safety`,
|
|
63
|
-
`parent-guard`), and crash-safety scenarios in `tests/crash-safety.test.ts`
|
|
64
|
-
via the test-only `crash` seam (`RestoreRunOptions.crash`).
|
|
65
|
-
- Harness interaction → the compatibility suites
|
|
66
|
-
(`compat-invariants` / `compat-interop` / `compat-gaps`) and
|
|
67
|
-
`scripts/verify-host.mjs`.
|
|
68
|
-
|
|
69
|
-
## Releasing
|
|
70
|
-
|
|
71
|
-
Releases are CI-driven via GitHub Actions Trusted Publishing (OIDC, no stored
|
|
72
|
-
token): push a `v<version>` tag and CI publishes with Sigstore provenance. Full
|
|
73
|
-
details: `docs/release/release.md`.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|