dsh-rewind-plugin 0.16.0 → 0.17.1
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.en.md +7 -5
- package/README.md +4 -5
- package/SECURITY.md +5 -4
- package/lib/client.js +127 -43
- package/lib/index.js +35 -15
- package/lib/types/client/hidden.d.ts +5 -5
- package/lib/types/client/locales.d.ts +8 -0
- package/lib/types/client/portals.d.ts +5 -4
- package/lib/types/client/settings-card.d.ts +23 -21
- package/lib/types/client/styles.d.ts +2 -1
- package/lib/types/index.d.ts +23 -15
- package/lib/types/locales.d.ts +2 -0
- package/package.json +37 -37
package/README.en.md
CHANGED
|
@@ -72,11 +72,14 @@ then install the new one.
|
|
|
72
72
|
|
|
73
73
|
## Snapshot management
|
|
74
74
|
|
|
75
|
-
|
|
75
|
+
**Snapshot backups** are on by default: they power file restore and can be
|
|
76
|
+
switched off in this plugin's configuration card. When off, no new snapshots
|
|
77
|
+
are created; file changes made while off cannot be restored. Snapshots (the
|
|
78
|
+
before-write backups) are stored under `<dsh home>/rewind-snapshots/`
|
|
76
79
|
(the default is `~/.dsh/rewind-snapshots/`). For the **same session**, the plugin
|
|
77
80
|
deduplicates snapshots by content and keeps the newest 100 anchor groups.
|
|
78
81
|
**Deleting that directory manually** only clears the file backups (chat rewinds are
|
|
79
|
-
unaffected)
|
|
82
|
+
unaffected); the plugin can rebuild them automatically.
|
|
80
83
|
|
|
81
84
|
A **global auto-cleanup** (off by default) removes the snapshot directories of
|
|
82
85
|
long-inactive sessions, leaving the active session and chat log untouched. Configure
|
|
@@ -85,7 +88,7 @@ auto-cleanup switch and the idle-day cutoff), or use the
|
|
|
85
88
|
`/snapshot-auto-cleanup` command to **view, configure, and run** it. See:
|
|
86
89
|
[Snapshot cleanup](docs/snapshot-auto-cleanup.md).
|
|
87
90
|
|
|
88
|
-
<img src="assets/screenshots/
|
|
91
|
+
<img src="assets/screenshots/plugin-setting.png" alt="Plugin configuration card: snapshot backups and auto cleanup" width="600">
|
|
89
92
|
|
|
90
93
|
## Uninstall
|
|
91
94
|
|
|
@@ -150,7 +153,7 @@ This plugin deliberately stays lightweight and focused on one thing — "convers
|
|
|
150
153
|
## Compatibility
|
|
151
154
|
|
|
152
155
|
- Node.js `^22.19.0 || >=24.0.0`.
|
|
153
|
-
- Compatibility definition, verification method, and version alignment: see [docs/compat/audit.md](docs/compat/audit.md); supported DSH versions are declared by `package.json` `
|
|
156
|
+
- Compatibility definition, verification method, and version alignment: see [docs/compat/audit.md](docs/compat/audit.md); supported DSH versions are declared by `package.json` `engines.dsh`.
|
|
154
157
|
|
|
155
158
|
> [!WARNING]
|
|
156
159
|
> This project and DeepSeek Harness are both in developer preview. Pin exact
|
|
@@ -169,7 +172,6 @@ withdrew should consume the stable, locale-independent helpers exported from
|
|
|
169
172
|
1. **Exported logs are complete** — a rewind only removes messages from the model context and the view; the exported session log (`/export`) still contains **withdrawn messages**. This plugin cannot alter exports.
|
|
170
173
|
2. **Lightweight file rewind has a cost** — in specific cases not all changes can be rewound. Consistent with Claude Code. See: [File-rewind tracking boundary](docs/compat/tracking-boundary.md).
|
|
171
174
|
3. **The turn-rail shows rewound turns** — the right-side rail added in DSH `v0.1.2` keeps ticks for withdrawn messages, and hovering shows the withdrawn text. Only a display difference; no functional impact.
|
|
172
|
-
4. **Old rewind markers are no longer compatible** — DSH `v0.1.3` rejects the rewind markers from the old plugin (≤ 0.8.0). Later versions resolve this and provide an in-session update. See the [update guide](docs/rewind-fix.md).
|
|
173
175
|
|
|
174
176
|
> [!NOTE]
|
|
175
177
|
> Browser diagnostics are available; see [Browser diagnostics](docs/compat/diagnostics.md).
|
package/README.md
CHANGED
|
@@ -69,11 +69,11 @@ dsh plugin --profile <name> add dsh-rewind-plugin@<版本>
|
|
|
69
69
|
|
|
70
70
|
## 存储管理
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
**快照备份**(默认开启):用于文件还原;可在本插件的配置卡片中关闭。关闭后不再创建新的快照,关闭期间的文件改动无法还原。快照存储于 `<dsh home>/rewind-snapshots/`(默认 `~/.dsh/rewind-snapshots/`)。手动删除该目录仅清除文件备份(对话回退不受影响),插件可自动重建。
|
|
73
73
|
|
|
74
|
-
|
|
74
|
+
**全局自动清理**(默认关闭):把长期不活跃的会话快照整目录移除,不影响活动会话与对话日志。可在配置卡片查看与配置(自动清理开关、失活天数),也可用 `/snapshot-auto-cleanup` 命令查看、设置和运行。详见:[快照自动清理](docs/snapshot-auto-cleanup.zh.md)。
|
|
75
75
|
|
|
76
|
-
<img src="assets/screenshots/
|
|
76
|
+
<img src="assets/screenshots/plugin-setting.png" alt="插件配置卡片:快照备份与自动清理" width="600">
|
|
77
77
|
|
|
78
78
|
## 卸载
|
|
79
79
|
|
|
@@ -137,7 +137,7 @@ rm <dsh home>/snapshot-cleanup-last-sweep.json
|
|
|
137
137
|
## 兼容性
|
|
138
138
|
|
|
139
139
|
- Node.js `^22.19.0 || >=24.0.0`。
|
|
140
|
-
- 兼容性定义、验证方法与版本对齐详见 [docs/compat/audit.md](docs/compat/audit.md);支持的 DSH 版本由 `package.json` 的 `
|
|
140
|
+
- 兼容性定义、验证方法与版本对齐详见 [docs/compat/audit.md](docs/compat/audit.md);支持的 DSH 版本由 `package.json` 的 `engines.dsh` 声明。
|
|
141
141
|
|
|
142
142
|
> [!WARNING]
|
|
143
143
|
> 本项目与 DeepSeek Harness 均处于开发者预览阶段。可复现环境请 pin 精确版本,
|
|
@@ -153,7 +153,6 @@ rm <dsh home>/snapshot-cleanup-last-sweep.json
|
|
|
153
153
|
1. **导出的日志是完整内容**——回退只是把消息从模型上下文和视图中移除,`/export` 导出的会话日志包含**已撤回的消息**。本插件无法改动导出。
|
|
154
154
|
2. **轻量文件回退存在代价**——特定情况可能无法回退所有修改。行为与 Claude Code 一致。详见:[文件回退的追踪边界](docs/compat/tracking-boundary.zh.md)。
|
|
155
155
|
3. **导轨显示已回退轮次**——DSH `v0.1.2` 新增右侧导轨,为已撤回消息保留刻度,悬浮显示已撤回正文。仅显示差异,无功能影响。
|
|
156
|
-
4. **旧回退标记不再兼容**——DSH `v0.1.3` 拒绝旧版插件(≤ 0.8.0)的回退标记。后续版本已解决,并提供会话更新功能。详见:[更新指南](docs/rewind-fix.zh.md)。
|
|
157
156
|
|
|
158
157
|
> [!NOTE]
|
|
159
158
|
> 本插件提供浏览器端诊断输出;详见 [浏览器诊断](docs/compat/diagnostics.zh.md)。
|
package/SECURITY.md
CHANGED
|
@@ -26,10 +26,11 @@ re-verify the host's own authority boundaries.
|
|
|
26
26
|
|
|
27
27
|
## Mutation gates
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
**never automatically applies** one.
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
When `fileCheckpointingEnabled` is on, the plugin **automatically captures**
|
|
30
|
+
before-backups of tracked mutations, but **never automatically applies** one.
|
|
31
|
+
A workspace restore happens only through an explicit user-invoked
|
|
32
|
+
`/rewind-plugin @<seq> both` (the per-message ↶ button or the command channel),
|
|
33
|
+
and only when all of the following hold:
|
|
33
34
|
|
|
34
35
|
1. **A validated target**: `planRewind` accepts only a `user/message` seq that
|
|
35
36
|
is currently on the session surface (`parseRewindTarget` → `RewindPlan`).
|
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) ?? (() => {
|
|
@@ -99,6 +100,9 @@ function hasFileImpact(text) {
|
|
|
99
100
|
if (match !== null) return Number(match[1]) > 0;
|
|
100
101
|
return false;
|
|
101
102
|
}
|
|
103
|
+
function hasBackupDisabled(text) {
|
|
104
|
+
return text !== void 0 && /(?:^|\n)backup=off(?:\n|$)/.test(text);
|
|
105
|
+
}
|
|
102
106
|
function isPreviewCommand(command) {
|
|
103
107
|
return (command.args ?? "").includes("preview");
|
|
104
108
|
}
|
|
@@ -164,7 +168,10 @@ function addWithdrawnTurnChrome(snap, cuts, hidden) {
|
|
|
164
168
|
hidden.add(chrome.anchorSeq);
|
|
165
169
|
}
|
|
166
170
|
}
|
|
171
|
+
var hiddenSeqsMemo = /* @__PURE__ */ new WeakMap();
|
|
167
172
|
function hiddenSeqsOf(snap) {
|
|
173
|
+
const memo = hiddenSeqsMemo.get(snap);
|
|
174
|
+
if (memo !== void 0) return new Set(memo);
|
|
168
175
|
const hidden = /* @__PURE__ */ new Set();
|
|
169
176
|
const spans = [];
|
|
170
177
|
for (const key of snap.order) {
|
|
@@ -195,6 +202,7 @@ function hiddenSeqsOf(snap) {
|
|
|
195
202
|
addWithdrawnTurnChrome(snap, cuts, hidden);
|
|
196
203
|
} catch {
|
|
197
204
|
}
|
|
205
|
+
hiddenSeqsMemo.set(snap, new Set(hidden));
|
|
198
206
|
return hidden;
|
|
199
207
|
}
|
|
200
208
|
|
|
@@ -307,6 +315,7 @@ var CLASS = {
|
|
|
307
315
|
popoverOptionLabel: "dsh-rewind-popover-option-label",
|
|
308
316
|
popoverOptionHint: "dsh-rewind-popover-option-hint",
|
|
309
317
|
popoverImpact: "dsh-rewind-popover-impact",
|
|
318
|
+
popoverNote: "dsh-rewind-popover-note",
|
|
310
319
|
popoverActions: "dsh-rewind-popover-actions",
|
|
311
320
|
popoverPrimary: "dsh-rewind-popover-primary",
|
|
312
321
|
popoverGhost: "dsh-rewind-popover-ghost",
|
|
@@ -417,6 +426,12 @@ var STYLE = `
|
|
|
417
426
|
max-height: 160px;
|
|
418
427
|
overflow: auto;
|
|
419
428
|
}
|
|
429
|
+
.dsh-rewind-popover-note {
|
|
430
|
+
margin-top: 6px;
|
|
431
|
+
font-size: 11px;
|
|
432
|
+
line-height: 15px;
|
|
433
|
+
color: var(--dsw-alias-label-tertiary);
|
|
434
|
+
}
|
|
420
435
|
.dsh-rewind-popover-actions {
|
|
421
436
|
display: flex;
|
|
422
437
|
justify-content: flex-end;
|
|
@@ -466,16 +481,17 @@ var STYLE = `
|
|
|
466
481
|
pointer-events: none;
|
|
467
482
|
}
|
|
468
483
|
|
|
469
|
-
/* ----
|
|
470
|
-
/*
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
484
|
+
/* ---- Plugin configuration form ---- */
|
|
485
|
+
/* Only the card's own controls: the two switch rows, their hints, and the divider. */
|
|
486
|
+
.dsh-rewind-settings-divider {
|
|
487
|
+
border-top: 1px solid var(--dsw-alias-border-l2);
|
|
488
|
+
}
|
|
489
|
+
.dsh-rewind-settings-section {
|
|
474
490
|
display: grid;
|
|
475
491
|
gap: 6px;
|
|
476
492
|
padding: 12px 0;
|
|
477
493
|
}
|
|
478
|
-
.dsh-rewind-
|
|
494
|
+
.dsh-rewind-settings-toggle-row {
|
|
479
495
|
display: flex;
|
|
480
496
|
align-items: flex-start;
|
|
481
497
|
justify-content: space-between;
|
|
@@ -484,11 +500,11 @@ var STYLE = `
|
|
|
484
500
|
line-height: 1.5;
|
|
485
501
|
color: var(--dsw-alias-label-primary);
|
|
486
502
|
}
|
|
487
|
-
.dsh-rewind-
|
|
503
|
+
.dsh-rewind-settings-toggle-label {
|
|
488
504
|
flex: 1;
|
|
489
505
|
min-width: 0;
|
|
490
506
|
}
|
|
491
|
-
.dsh-rewind-
|
|
507
|
+
.dsh-rewind-settings-hint {
|
|
492
508
|
margin: 0;
|
|
493
509
|
font-size: 12px;
|
|
494
510
|
line-height: 1.5;
|
|
@@ -676,7 +692,13 @@ function renderImpactStep(root, opts, back, cached) {
|
|
|
676
692
|
...restores.map((path) => t("popover.impact.restore", { path })),
|
|
677
693
|
...deletes.map((path) => t("popover.impact.delete", { path }))
|
|
678
694
|
];
|
|
679
|
-
|
|
695
|
+
if (hasBackupDisabled(outcome.text)) {
|
|
696
|
+
const list = document.createElement("div");
|
|
697
|
+
list.textContent = lines.join("\n");
|
|
698
|
+
impact.replaceChildren(list, el("div", CLASS.popoverNote, t("popover.backupDisabled")));
|
|
699
|
+
} else {
|
|
700
|
+
impact.textContent = lines.join("\n");
|
|
701
|
+
}
|
|
680
702
|
}
|
|
681
703
|
}
|
|
682
704
|
confirm.disabled = false;
|
|
@@ -1045,6 +1067,13 @@ function showHint(text) {
|
|
|
1045
1067
|
var COMPOSER_EDITABLE_SELECTOR = "[data-composer-input]";
|
|
1046
1068
|
var USER_SEAT_SELECTOR = '[data-chat-flow-kind="user"][data-chat-anchor-key], [data-chat-flow-kind="steering"][data-chat-anchor-key]';
|
|
1047
1069
|
var CHAT_SEAT_SELECTOR = "[data-chat-anchor-key]";
|
|
1070
|
+
var IGNORED_PORTAL_SCOPE = [
|
|
1071
|
+
"[data-composer-seat]",
|
|
1072
|
+
"[data-composer-input]",
|
|
1073
|
+
"[data-sidebar-right-panel]",
|
|
1074
|
+
"[data-sidebar-chat]",
|
|
1075
|
+
"[data-plugin-panel]"
|
|
1076
|
+
].join(", ");
|
|
1048
1077
|
var GROUP_SHELL_SELECTOR = '[data-chat-anchor-key^="group:"]';
|
|
1049
1078
|
function resolveSeatAnchorSeq(element, chat) {
|
|
1050
1079
|
if (chat === void 0) return void 0;
|
|
@@ -1197,12 +1226,14 @@ function RewindPortals({ sessionId, sessionOf, chatOf, isMainViewSession, watchC
|
|
|
1197
1226
|
const chat = chatOf(session);
|
|
1198
1227
|
const hiddenSeqs = chat === void 0 ? /* @__PURE__ */ new Set() : hiddenSeqsOf(chat);
|
|
1199
1228
|
for (const element of hidden.current) if (!element.isConnected) hidden.current.delete(element);
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1229
|
+
if (hiddenSeqs.size > 0 || hidden.current.size > 0) {
|
|
1230
|
+
hideWithdrawnSeats(
|
|
1231
|
+
chat,
|
|
1232
|
+
chat === void 0 ? [] : document.querySelectorAll(CHAT_SEAT_SELECTOR),
|
|
1233
|
+
hiddenSeqs,
|
|
1234
|
+
hidden.current
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1206
1237
|
const durable = collectDurableTargets(snapshot, chat, hiddenSeqs);
|
|
1207
1238
|
const next = [...durable, ...collectPendingTargets(snapshot, steeringItemsOf(inboxOf(session)?.["next-step"]))];
|
|
1208
1239
|
setTargets((current) => sameTargets(current, next) ? current : next);
|
|
@@ -1228,17 +1259,34 @@ function RewindPortals({ sessionId, sessionOf, chatOf, isMainViewSession, watchC
|
|
|
1228
1259
|
}
|
|
1229
1260
|
};
|
|
1230
1261
|
refresh();
|
|
1231
|
-
const
|
|
1262
|
+
const unsubscribeChat = watchChat(sessionId, queueRefresh);
|
|
1263
|
+
const observer = new MutationObserver((records) => {
|
|
1264
|
+
for (const record of records) {
|
|
1265
|
+
if (record.type === "attributes") {
|
|
1266
|
+
const target = record.target;
|
|
1267
|
+
if (target instanceof HTMLElement && hidden.current.has(target) && target.style.display !== "none") {
|
|
1268
|
+
queueRefresh();
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1273
|
+
const node = record.target instanceof Element ? record.target : record.target.parentElement;
|
|
1274
|
+
if (node !== null && node.closest(IGNORED_PORTAL_SCOPE) !== null) continue;
|
|
1275
|
+
queueRefresh();
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
});
|
|
1232
1279
|
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["style"] });
|
|
1233
1280
|
return () => {
|
|
1234
1281
|
active = false;
|
|
1235
1282
|
observer.disconnect();
|
|
1283
|
+
unsubscribeChat();
|
|
1236
1284
|
showHiddenSeats(hidden.current);
|
|
1237
1285
|
unsubscribeInbox?.();
|
|
1238
1286
|
unsubscribeInbox = void 0;
|
|
1239
1287
|
inboxOwner = void 0;
|
|
1240
1288
|
};
|
|
1241
|
-
}, [sessionId, sessionOf]);
|
|
1289
|
+
}, [sessionId, sessionOf, watchChat]);
|
|
1242
1290
|
return targets.map((target) => (0, import_react_dom.createPortal)(
|
|
1243
1291
|
target.kind === "pending" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
1244
1292
|
RetractButton,
|
|
@@ -1368,9 +1416,9 @@ function createRewindBridge(deps) {
|
|
|
1368
1416
|
}
|
|
1369
1417
|
|
|
1370
1418
|
// src/client/build-info.ts
|
|
1371
|
-
var PLUGIN_VERSION = true ? "0.
|
|
1419
|
+
var PLUGIN_VERSION = true ? "0.17.1" : "dev";
|
|
1372
1420
|
var PLUGIN_PACKAGE = "dsh-rewind-plugin";
|
|
1373
|
-
var BUILD_HASH = true ? "
|
|
1421
|
+
var BUILD_HASH = true ? "0d33ec37" : "dev";
|
|
1374
1422
|
|
|
1375
1423
|
// src/client/locales.ts
|
|
1376
1424
|
var zh = {
|
|
@@ -1390,6 +1438,7 @@ var zh = {
|
|
|
1390
1438
|
"popover.both.hint": "\u5BF9\u8BDD\u56DE\u9000\u5E76\u8FD8\u539F\u5DE5\u4F5C\u533A\u6587\u4EF6",
|
|
1391
1439
|
"popover.checking": "\u6B63\u5728\u68C0\u67E5\u6587\u4EF6\u53D8\u66F4\u2026",
|
|
1392
1440
|
"popover.noChanges": "\u6B64\u6D88\u606F\u4E4B\u540E\u6CA1\u6709\u53EF\u8FD8\u539F\u7684\u6587\u4EF6\u53D8\u66F4\uFF0C\u4EC5\u53EF\u56DE\u9000\u5BF9\u8BDD",
|
|
1441
|
+
"popover.backupDisabled": "\u5FEB\u7167\u5907\u4EFD\u5DF2\u5173\u95ED\uFF0C\u8FD8\u539F\u53EF\u80FD\u4E0D\u5145\u5206",
|
|
1393
1442
|
"popover.cancel": "\u53D6\u6D88",
|
|
1394
1443
|
"popover.impact.loading": "\u6B63\u5728\u83B7\u53D6\u5F71\u54CD\u6E05\u5355\u2026",
|
|
1395
1444
|
"popover.impact.failed": "\u65E0\u6CD5\u83B7\u53D6\u5F71\u54CD\u6E05\u5355\uFF1A{message}",
|
|
@@ -1398,11 +1447,14 @@ var zh = {
|
|
|
1398
1447
|
"popover.impact.delete": "\u5220\u9664 {path}",
|
|
1399
1448
|
"popover.confirm": "\u786E\u8BA4\u56DE\u9000",
|
|
1400
1449
|
"popover.back": "\u8FD4\u56DE",
|
|
1450
|
+
"backup.title": "\u5FEB\u7167\u5907\u4EFD",
|
|
1451
|
+
"backup.on": "\u5F00\u542F\u540E\u81EA\u52A8\u521B\u5EFA\u6587\u4EF6\u5FEB\u7167\uFF0C\u7528\u4E8E\u6587\u4EF6\u8FD8\u539F\u3002",
|
|
1452
|
+
"backup.off": "\u5173\u95ED\u540E\u4E0D\u518D\u521B\u5EFA\u6587\u4EF6\u5FEB\u7167\uFF1B\u5173\u95ED\u671F\u95F4\u7684\u6587\u4EF6\u6539\u52A8\u65E0\u6CD5\u8FD8\u539F\u3002",
|
|
1401
1453
|
"cleanup.auto": "\u81EA\u52A8\u6E05\u7406",
|
|
1402
|
-
"cleanup.auto.on": "\u5F00\u542F\u540E\u6309\u4E0D\u6D3B\u8DC3\u5929\u6570\u81EA\u52A8\u6E05\u7406\u8FC7\u671F\u4F1A\u8BDD\u5FEB\u7167",
|
|
1403
|
-
"cleanup.auto.off": "\u5173\u95ED\u540E\
|
|
1454
|
+
"cleanup.auto.on": "\u5F00\u542F\u540E\u6309\u4E0D\u6D3B\u8DC3\u5929\u6570\u81EA\u52A8\u6E05\u7406\u8FC7\u671F\u4F1A\u8BDD\u5FEB\u7167\u3002",
|
|
1455
|
+
"cleanup.auto.off": "\u5173\u95ED\u540E\u4E0D\u4F1A\u81EA\u52A8\u6E05\u7406\u5FEB\u7167\u3002",
|
|
1404
1456
|
"cleanup.maxAge": "\u4E0D\u6D3B\u8DC3\u65F6\u95F4\uFF08\u5929\uFF09",
|
|
1405
|
-
"cleanup.maxAge.hint": "\u8D85\u8FC7\u8BE5\u5929\u6570\u672A\u6D3B\u52A8\u7684\u4F1A\u8BDD\u5FEB\u7167\u4F1A\u88AB\u6E05\u7406",
|
|
1457
|
+
"cleanup.maxAge.hint": "\u8D85\u8FC7\u8BE5\u5929\u6570\u672A\u6D3B\u52A8\u7684\u4F1A\u8BDD\u5FEB\u7167\u4F1A\u88AB\u6E05\u7406\u3002",
|
|
1406
1458
|
"cleanup.invalid": "\u8BF7\u586B\u6B63\u6574\u6570\uFF1B\u7559\u7A7A\u8868\u793A\u4F7F\u7528\u9ED8\u8BA4\u503C",
|
|
1407
1459
|
"cleanup.overridden": "\u5DF2\u8986\u76D6",
|
|
1408
1460
|
"cleanup.reset": "\u6062\u590D\u9ED8\u8BA4",
|
|
@@ -1429,6 +1481,7 @@ var en = {
|
|
|
1429
1481
|
"popover.both.hint": "Cut the context and restore workspace files",
|
|
1430
1482
|
"popover.checking": "Checking for file changes\u2026",
|
|
1431
1483
|
"popover.noChanges": "No tracked file changes after this message; conversation-only rewind",
|
|
1484
|
+
"popover.backupDisabled": "Snapshots off; the restore may be incomplete",
|
|
1432
1485
|
"popover.cancel": "Cancel",
|
|
1433
1486
|
"popover.impact.loading": "Fetching impact list\u2026",
|
|
1434
1487
|
"popover.impact.failed": "Could not fetch the impact list: {message}",
|
|
@@ -1437,11 +1490,14 @@ var en = {
|
|
|
1437
1490
|
"popover.impact.delete": "Delete {path}",
|
|
1438
1491
|
"popover.confirm": "Confirm rewind",
|
|
1439
1492
|
"popover.back": "Back",
|
|
1493
|
+
"backup.title": "Snapshot backups",
|
|
1494
|
+
"backup.on": "When on, file snapshots are created automatically for file restore.",
|
|
1495
|
+
"backup.off": "When off, no file snapshots are created; file changes made while off cannot be restored.",
|
|
1440
1496
|
"cleanup.auto": "Auto cleanup",
|
|
1441
|
-
"cleanup.auto.on": "When enabled, session snapshots idle past the cutoff are cleaned automatically",
|
|
1442
|
-
"cleanup.auto.off": "When
|
|
1497
|
+
"cleanup.auto.on": "When enabled, session snapshots idle past the cutoff are cleaned automatically.",
|
|
1498
|
+
"cleanup.auto.off": "When off, snapshots are not cleaned automatically.",
|
|
1443
1499
|
"cleanup.maxAge": "Idle time (days)",
|
|
1444
|
-
"cleanup.maxAge.hint": "Snapshot backups of sessions idle longer than this many days are cleaned",
|
|
1500
|
+
"cleanup.maxAge.hint": "Snapshot backups of sessions idle longer than this many days are cleaned.",
|
|
1445
1501
|
"cleanup.invalid": "Enter a positive integer, or leave blank to use the default",
|
|
1446
1502
|
"cleanup.overridden": "Overridden",
|
|
1447
1503
|
"cleanup.reset": "Reset to default",
|
|
@@ -1455,7 +1511,7 @@ var en = {
|
|
|
1455
1511
|
// src/client/settings-card.tsx
|
|
1456
1512
|
var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
1457
1513
|
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
1458
|
-
var
|
|
1514
|
+
var SETTINGS_ENTRY_ID = "dsh-rewind-plugin";
|
|
1459
1515
|
var DEFAULT_MAX_AGE_DAYS = 30;
|
|
1460
1516
|
function autoCleanupEnabledField() {
|
|
1461
1517
|
return {
|
|
@@ -1464,6 +1520,13 @@ function autoCleanupEnabledField() {
|
|
|
1464
1520
|
parse: (text) => ({ kind: "set", value: text === "true" })
|
|
1465
1521
|
};
|
|
1466
1522
|
}
|
|
1523
|
+
function fileCheckpointingEnabledField() {
|
|
1524
|
+
return {
|
|
1525
|
+
field: "fileCheckpointingEnabled",
|
|
1526
|
+
format: (value) => String(value !== false),
|
|
1527
|
+
parse: (text) => text === "true" ? { kind: "clear" } : { kind: "set", value: false }
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1467
1530
|
function formLabels(t) {
|
|
1468
1531
|
return {
|
|
1469
1532
|
unavailable: t("cleanup.unavailable"),
|
|
@@ -1473,8 +1536,9 @@ function formLabels(t) {
|
|
|
1473
1536
|
saving: t("cleanup.saving")
|
|
1474
1537
|
};
|
|
1475
1538
|
}
|
|
1476
|
-
function
|
|
1539
|
+
function settingsForm(scope) {
|
|
1477
1540
|
const form = new import_dsh_client_ui_primitives.SettingsFormModel(scope, [
|
|
1541
|
+
fileCheckpointingEnabledField(),
|
|
1478
1542
|
autoCleanupEnabledField(),
|
|
1479
1543
|
(0, import_dsh_client_ui_primitives.settingsNumberField)("autoCleanupMaxAgeDays")
|
|
1480
1544
|
]);
|
|
@@ -1482,17 +1546,19 @@ function cleanupForm(scope) {
|
|
|
1482
1546
|
form,
|
|
1483
1547
|
store: form.bind(() => ({
|
|
1484
1548
|
...form.shell(),
|
|
1549
|
+
checkpointingEnabled: form.field("fileCheckpointingEnabled"),
|
|
1485
1550
|
enabled: form.field("autoCleanupEnabled"),
|
|
1486
1551
|
maxAgeDays: form.field("autoCleanupMaxAgeDays")
|
|
1487
1552
|
}))
|
|
1488
1553
|
};
|
|
1489
1554
|
}
|
|
1490
|
-
function
|
|
1491
|
-
const { t,
|
|
1492
|
-
const state =
|
|
1555
|
+
function SettingsCard(props) {
|
|
1556
|
+
const { t, useSettingsCard } = props;
|
|
1557
|
+
const state = useSettingsCard((snapshot) => snapshot);
|
|
1493
1558
|
if (props.view === "summary") return null;
|
|
1494
1559
|
const disabled = !state.writable || state.saving;
|
|
1495
|
-
const
|
|
1560
|
+
const checkpointing = state.checkpointingEnabled.text === "true";
|
|
1561
|
+
const cleanupEnabled = state.enabled.text === "true";
|
|
1496
1562
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
1497
1563
|
import_dsh_client_ui_primitives.SettingsForm,
|
|
1498
1564
|
{
|
|
@@ -1501,13 +1567,31 @@ function SettingsCleanupCard(props) {
|
|
|
1501
1567
|
onSave: props.save,
|
|
1502
1568
|
onDiscard: props.discard,
|
|
1503
1569
|
children: [
|
|
1504
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-rewind-
|
|
1505
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-rewind-
|
|
1506
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-rewind-
|
|
1570
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-rewind-settings-section", children: [
|
|
1571
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-rewind-settings-toggle-row", children: [
|
|
1572
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-rewind-settings-toggle-label", children: t("backup.title") }),
|
|
1573
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1574
|
+
import_dsh_client_ui_primitives.Switch,
|
|
1575
|
+
{
|
|
1576
|
+
checked: checkpointing,
|
|
1577
|
+
label: t("backup.title"),
|
|
1578
|
+
disabled,
|
|
1579
|
+
onChange: (next) => {
|
|
1580
|
+
props.edit("fileCheckpointingEnabled", String(next));
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
)
|
|
1584
|
+
] }),
|
|
1585
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "dsh-rewind-settings-hint", children: t(checkpointing ? "backup.on" : "backup.off") })
|
|
1586
|
+
] }),
|
|
1587
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "dsh-rewind-settings-divider" }),
|
|
1588
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-rewind-settings-section", children: [
|
|
1589
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-rewind-settings-toggle-row", children: [
|
|
1590
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-rewind-settings-toggle-label", children: t("cleanup.auto") }),
|
|
1507
1591
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1508
1592
|
import_dsh_client_ui_primitives.Switch,
|
|
1509
1593
|
{
|
|
1510
|
-
checked:
|
|
1594
|
+
checked: cleanupEnabled,
|
|
1511
1595
|
label: t("cleanup.auto"),
|
|
1512
1596
|
disabled,
|
|
1513
1597
|
onChange: (next) => {
|
|
@@ -1516,9 +1600,9 @@ function SettingsCleanupCard(props) {
|
|
|
1516
1600
|
}
|
|
1517
1601
|
)
|
|
1518
1602
|
] }),
|
|
1519
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "dsh-rewind-
|
|
1603
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "dsh-rewind-settings-hint", children: t(cleanupEnabled ? "cleanup.auto.on" : "cleanup.auto.off") })
|
|
1520
1604
|
] }),
|
|
1521
|
-
|
|
1605
|
+
cleanupEnabled ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1522
1606
|
import_dsh_client_ui_primitives.SettingsValueField,
|
|
1523
1607
|
{
|
|
1524
1608
|
id: "dsh-rewind-cleanup-maxage",
|
|
@@ -1668,20 +1752,20 @@ function apply(ctx) {
|
|
|
1668
1752
|
createRewindBridge({ sessionOf, chatOf, isMainViewSession, watchChat, setComposerText, restoreImages, t, subscribeLocale })
|
|
1669
1753
|
));
|
|
1670
1754
|
try {
|
|
1671
|
-
const { store, form } =
|
|
1672
|
-
ctx.configForms.get(
|
|
1755
|
+
const { store, form } = settingsForm(
|
|
1756
|
+
ctx.configForms.get(SETTINGS_ENTRY_ID)
|
|
1673
1757
|
);
|
|
1674
1758
|
ctx.effect(() => () => {
|
|
1675
1759
|
form.dispose();
|
|
1676
|
-
}, "dsh-rewind
|
|
1760
|
+
}, "dsh-rewind settings form");
|
|
1677
1761
|
yield slots.inject("plugins.bundle.config", () => slots.register(
|
|
1678
1762
|
{
|
|
1679
1763
|
name: "plugins.bundle.config",
|
|
1680
1764
|
key: PLUGIN_PACKAGE,
|
|
1681
1765
|
locale: NS2,
|
|
1682
|
-
inject: () => ({ hooks: {
|
|
1766
|
+
inject: () => ({ hooks: { settingsCard: store }, ...form.actions() })
|
|
1683
1767
|
},
|
|
1684
|
-
|
|
1768
|
+
SettingsCard
|
|
1685
1769
|
));
|
|
1686
1770
|
} catch (error) {
|
|
1687
1771
|
rewindLog.error("settings", "settings card register failed", error);
|
package/lib/index.js
CHANGED
|
@@ -14,6 +14,7 @@ var en = {
|
|
|
14
14
|
"plan.restore": "restore {path}",
|
|
15
15
|
"plan.delete": "delete {path}",
|
|
16
16
|
"plan.noChanges": "No restorable changes after the target.",
|
|
17
|
+
"plan.backupOff": "Snapshot backups are off; this restore may be incomplete.",
|
|
17
18
|
"plan.skip": " not restored: {path} ({reason})",
|
|
18
19
|
"skipReason.oversized": "over the {limit} backup limit, so the recorded state predates untracked edits",
|
|
19
20
|
"uncovered.header": "{count} tracked file(s) exceed the {limit} backup limit and are not covered:",
|
|
@@ -30,6 +31,7 @@ var en = {
|
|
|
30
31
|
"delete.count": "deleted {count} file(s)",
|
|
31
32
|
"skip.count": "skipped {count} path(s)",
|
|
32
33
|
"noRestorable": "; no restorable write-class changes after the target",
|
|
34
|
+
"backupOff": "snapshot backups are off, so the restore may be incomplete",
|
|
33
35
|
"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.",
|
|
34
36
|
"success": "Withdrawn seq {targetSeq} and everything after it (conversation returned to earlier){restore}.",
|
|
35
37
|
"noUserMessages": "This session has no rewindable user messages yet.",
|
|
@@ -65,6 +67,7 @@ var zh = {
|
|
|
65
67
|
"plan.restore": "\u8FD8\u539F {path}",
|
|
66
68
|
"plan.delete": "\u5220\u9664 {path}",
|
|
67
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",
|
|
68
71
|
"plan.skip": " \u672A\u8FD8\u539F\uFF1A{path}\uFF08{reason}\uFF09",
|
|
69
72
|
"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",
|
|
70
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",
|
|
@@ -81,6 +84,7 @@ var zh = {
|
|
|
81
84
|
"delete.count": "\u5220\u9664 {count} \u4E2A\u6587\u4EF6",
|
|
82
85
|
"skip.count": "\u8DF3\u8FC7 {count} \u4E2A\u8DEF\u5F84",
|
|
83
86
|
"noRestorable": "\uFF1B\u76EE\u6807\u4E4B\u540E\u6CA1\u6709\u53EF\u8FD8\u539F\u7684\u5199\u7C7B\u53D8\u66F4",
|
|
87
|
+
"backupOff": "\u5FEB\u7167\u5907\u4EFD\u5DF2\u5173\u95ED\uFF0C\u8FD8\u539F\u53EF\u80FD\u4E0D\u5145\u5206",
|
|
84
88
|
"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",
|
|
85
89
|
"success": "\u5DF2\u64A4\u56DE seq {targetSeq} \u53CA\u4E4B\u540E\u5185\u5BB9\uFF08\u5BF9\u8BDD\u5DF2\u56DE\u5230\u6B64\u524D\uFF09{restore}\u3002",
|
|
86
90
|
"noUserMessages": "\u5F53\u524D\u4F1A\u8BDD\u8FD8\u6CA1\u6709\u53EF\u56DE\u9000\u7684\u7528\u6237\u6D88\u606F\u3002",
|
|
@@ -2315,6 +2319,7 @@ function shouldRunAutoSweep(lastAtMs, nowMs, intervalMs = AUTO_SWEEP_INTERVAL_MS
|
|
|
2315
2319
|
var name = "dsh-rewind";
|
|
2316
2320
|
var inject = ["commands", "tools", "settings"];
|
|
2317
2321
|
var ENGINE_COMMAND = "rewind-plugin";
|
|
2322
|
+
var DEFAULT_FILE_CHECKPOINTING_ENABLED = true;
|
|
2318
2323
|
var Config = z.object({
|
|
2319
2324
|
snapshotDir: z.string().required(false),
|
|
2320
2325
|
dshHome: z.string().required(false),
|
|
@@ -2322,9 +2327,13 @@ var Config = z.object({
|
|
|
2322
2327
|
maxFileBytes: z.number().step(1).min(0).default(DEFAULT_MAX_FILE_BYTES),
|
|
2323
2328
|
maxAnchorGroups: z.number().step(1).min(1).default(MAX_ANCHOR_GROUPS),
|
|
2324
2329
|
autoSweepIntervalMs: z.number().step(1).min(1).default(AUTO_SWEEP_INTERVAL_MS),
|
|
2330
|
+
fileCheckpointingEnabled: z.boolean().default(DEFAULT_FILE_CHECKPOINTING_ENABLED).volatile(),
|
|
2325
2331
|
autoCleanupEnabled: z.boolean().default(DEFAULT_CLEANUP_CONFIG.enabled).volatile(),
|
|
2326
2332
|
autoCleanupMaxAgeDays: z.number().step(1).min(1).default(DEFAULT_CLEANUP_CONFIG.maxAgeDays).volatile()
|
|
2327
2333
|
});
|
|
2334
|
+
function isFileCheckpointingEnabled(config) {
|
|
2335
|
+
return config?.fileCheckpointingEnabled?.get() !== false;
|
|
2336
|
+
}
|
|
2328
2337
|
var TRACKED_TOOLS = /* @__PURE__ */ new Set(["write", "edit"]);
|
|
2329
2338
|
function isSubagentSession(session) {
|
|
2330
2339
|
const header = session.header;
|
|
@@ -2428,11 +2437,15 @@ async function captureBefore(fs, workingDirectory, store, exec, pending) {
|
|
|
2428
2437
|
}
|
|
2429
2438
|
pending.set(key, { path: target.displayPath, backup });
|
|
2430
2439
|
}
|
|
2431
|
-
async function commitEntry(store, pending, anchorCache, trackedBySession, exec, result) {
|
|
2440
|
+
async function commitEntry(store, pending, anchorCache, trackedBySession, exec, result, checkpointingEnabled) {
|
|
2432
2441
|
const key = `${exec.agent?.id ?? "anon"}:${exec.callId}`;
|
|
2433
2442
|
const capture = pending.get(key);
|
|
2434
2443
|
if (capture === void 0) return;
|
|
2435
2444
|
pending.delete(key);
|
|
2445
|
+
if (!checkpointingEnabled) {
|
|
2446
|
+
await discardCapture(capture);
|
|
2447
|
+
return;
|
|
2448
|
+
}
|
|
2436
2449
|
if (result.isError) {
|
|
2437
2450
|
await discardCapture(capture);
|
|
2438
2451
|
return;
|
|
@@ -2476,7 +2489,7 @@ function formatBytes(bytes) {
|
|
|
2476
2489
|
if (bytes >= 1024) return `${Math.round(bytes / 1024)} KiB`;
|
|
2477
2490
|
return `${bytes} B`;
|
|
2478
2491
|
}
|
|
2479
|
-
function formatPlan(plan, files, uncovered = [], maxFileBytes = 0) {
|
|
2492
|
+
function formatPlan(plan, files, uncovered = [], maxFileBytes = 0, checkpointingEnabled = true) {
|
|
2480
2493
|
const lines = [
|
|
2481
2494
|
t("plan.rewinding", { targetSeq: plan.targetSeq, count: plan.shadowedSeqs.length })
|
|
2482
2495
|
];
|
|
@@ -2493,10 +2506,12 @@ function formatPlan(plan, files, uncovered = [], maxFileBytes = 0) {
|
|
|
2493
2506
|
for (const file of uncovered.slice(0, 5)) lines.push(` ${file.path}`);
|
|
2494
2507
|
if (uncovered.length > 5) lines.push(" \u2026");
|
|
2495
2508
|
}
|
|
2509
|
+
if (!checkpointingEnabled) lines.push(t("plan.backupOff"));
|
|
2496
2510
|
lines.push(`impact=${files.length}`);
|
|
2497
2511
|
for (const file of files) {
|
|
2498
2512
|
lines.push(`${file.action}:${file.path}`);
|
|
2499
2513
|
}
|
|
2514
|
+
if (!checkpointingEnabled) lines.push("backup=off");
|
|
2500
2515
|
return lines.join("\n");
|
|
2501
2516
|
}
|
|
2502
2517
|
function resolveOrError(events, surface, raw) {
|
|
@@ -2567,7 +2582,7 @@ function dropPendingSteering(agent) {
|
|
|
2567
2582
|
agent.inbox.remove(message.id);
|
|
2568
2583
|
}
|
|
2569
2584
|
}
|
|
2570
|
-
async function executeRewind(ctx, store, fs, invocation, rawTarget, mode, inflight) {
|
|
2585
|
+
async function executeRewind(ctx, store, fs, invocation, rawTarget, mode, inflight, checkpointingEnabled) {
|
|
2571
2586
|
const { agent } = invocation;
|
|
2572
2587
|
const sessionId = agent.session.id;
|
|
2573
2588
|
if (inflight.has(sessionId)) {
|
|
@@ -2626,6 +2641,7 @@ async function executeRewind(ctx, store, fs, invocation, rawTarget, mode, inflig
|
|
|
2626
2641
|
const skippedLines = renderSkipped(outcome, capped, store.fileCapBytes);
|
|
2627
2642
|
if (skippedLines !== "") parts.push(skippedLines);
|
|
2628
2643
|
}
|
|
2644
|
+
if (!checkpointingEnabled) parts.push(t("backupOff"));
|
|
2629
2645
|
restore = parts.length > 0 ? `${t("sep.clause")}${parts.join(t("sep.list"))}` : t("noRestorable");
|
|
2630
2646
|
restore += renderFailures(outcome.failed);
|
|
2631
2647
|
}
|
|
@@ -2651,7 +2667,7 @@ function rewindErrorResult(error) {
|
|
|
2651
2667
|
}
|
|
2652
2668
|
throw error;
|
|
2653
2669
|
}
|
|
2654
|
-
async function handleRewind(ctx, store, fs, invocation, inflight) {
|
|
2670
|
+
async function handleRewind(ctx, store, fs, invocation, inflight, checkpointingEnabled) {
|
|
2655
2671
|
const session = invocation.agent.session;
|
|
2656
2672
|
const input = invocation.rawInput.trim();
|
|
2657
2673
|
if (input === "") {
|
|
@@ -2659,7 +2675,7 @@ async function handleRewind(ctx, store, fs, invocation, inflight) {
|
|
|
2659
2675
|
if (candidates.length === 0) {
|
|
2660
2676
|
return { kind: "error", text: t("noUserMessages") };
|
|
2661
2677
|
}
|
|
2662
|
-
return executeRewind(ctx, store, fs, invocation, `@${candidates[0].seq}`, "chat", inflight);
|
|
2678
|
+
return executeRewind(ctx, store, fs, invocation, `@${candidates[0].seq}`, "chat", inflight, checkpointingEnabled);
|
|
2663
2679
|
}
|
|
2664
2680
|
const parts = input.split(/\s+/);
|
|
2665
2681
|
if (parts[0] === "preview") {
|
|
@@ -2679,7 +2695,7 @@ async function handleRewind(ctx, store, fs, invocation, inflight) {
|
|
|
2679
2695
|
return { kind: "error", text: t("storeUnsupported", { version: await store.readStoreVersion(session.id) ?? 0 }) };
|
|
2680
2696
|
}
|
|
2681
2697
|
const uncovered = await store.uncoveredPaths(session.id).catch(() => []);
|
|
2682
|
-
return { kind: "success", text: formatPlan(plan, impacts, uncovered, store.fileCapBytes) };
|
|
2698
|
+
return { kind: "success", text: formatPlan(plan, impacts, uncovered, store.fileCapBytes, checkpointingEnabled) };
|
|
2683
2699
|
}
|
|
2684
2700
|
if (parts[0] === "__candidates") {
|
|
2685
2701
|
const candidates = listRewindCandidates(session.snapshotEvents(), session.surface.nodes);
|
|
@@ -2691,7 +2707,7 @@ async function handleRewind(ctx, store, fs, invocation, inflight) {
|
|
|
2691
2707
|
return { kind: "error", text: t("engine.rejected") };
|
|
2692
2708
|
}
|
|
2693
2709
|
if (mode === void 0) return { kind: "error", text: t("engine.rejected") };
|
|
2694
|
-
return executeRewind(ctx, store, fs, invocation, target, mode, inflight);
|
|
2710
|
+
return executeRewind(ctx, store, fs, invocation, target, mode, inflight, checkpointingEnabled);
|
|
2695
2711
|
}
|
|
2696
2712
|
var autoSweepChecked = false;
|
|
2697
2713
|
async function maybeRunAutoCleanup(ctx, store, sessionId, dshHome, sweepIntervalMs, signal) {
|
|
@@ -2825,7 +2841,7 @@ var INCLUDE_ENTRY_PREFIX = "include:";
|
|
|
2825
2841
|
function rewindConfigKey(ctx) {
|
|
2826
2842
|
return ctx.fiber?.entry?.id;
|
|
2827
2843
|
}
|
|
2828
|
-
function
|
|
2844
|
+
function profileEntryKey(entryId) {
|
|
2829
2845
|
if (entryId === void 0) return void 0;
|
|
2830
2846
|
return entryId.startsWith(INCLUDE_ENTRY_PREFIX) ? entryId.slice(INCLUDE_ENTRY_PREFIX.length) : entryId;
|
|
2831
2847
|
}
|
|
@@ -2847,7 +2863,7 @@ function apply(ctx, config) {
|
|
|
2847
2863
|
maxFileBytes: config?.maxFileBytes,
|
|
2848
2864
|
maxAnchorGroups: config?.maxAnchorGroups
|
|
2849
2865
|
});
|
|
2850
|
-
const configKey =
|
|
2866
|
+
const configKey = profileEntryKey(rewindConfigKey(ctx));
|
|
2851
2867
|
if (config !== void 0 && configKey !== void 0) {
|
|
2852
2868
|
const writer = {
|
|
2853
2869
|
entryId: configKey,
|
|
@@ -2872,10 +2888,10 @@ function apply(ctx, config) {
|
|
|
2872
2888
|
return handler(invocation);
|
|
2873
2889
|
};
|
|
2874
2890
|
const rewindHandler = withHostLocale(
|
|
2875
|
-
(invocation) => handleRewind(ctx, store, fsService, invocation, inflight)
|
|
2891
|
+
(invocation) => handleRewind(ctx, store, fsService, invocation, inflight, isFileCheckpointingEnabled(config))
|
|
2876
2892
|
);
|
|
2877
2893
|
const engineHandler = withHostLocale(
|
|
2878
|
-
(invocation) => invocation.rawInput.trim() === "" ? Promise.resolve({ kind: "error", text: t("engine.rejected") }) : handleRewind(ctx, store, fsService, invocation, inflight)
|
|
2894
|
+
(invocation) => invocation.rawInput.trim() === "" ? Promise.resolve({ kind: "error", text: t("engine.rejected") }) : handleRewind(ctx, store, fsService, invocation, inflight, isFileCheckpointingEnabled(config))
|
|
2879
2895
|
);
|
|
2880
2896
|
try {
|
|
2881
2897
|
yield ctx.commands.register({
|
|
@@ -2948,6 +2964,7 @@ function apply(ctx, config) {
|
|
|
2948
2964
|
if (lifecycle.signal.aborted) return;
|
|
2949
2965
|
const sessionId = session.id;
|
|
2950
2966
|
void maybeRunAutoCleanup(ctx, store, sessionId, dshHome, config?.autoSweepIntervalMs, lifecycle.signal);
|
|
2967
|
+
if (!isFileCheckpointingEnabled(config)) return;
|
|
2951
2968
|
let tracked = trackedBySession.get(sessionId);
|
|
2952
2969
|
if (tracked === void 0) {
|
|
2953
2970
|
tracked = await store.trackedPaths(sessionId);
|
|
@@ -2970,7 +2987,9 @@ function apply(ctx, config) {
|
|
|
2970
2987
|
}, "dsh-rewind fs capture");
|
|
2971
2988
|
scope.on("tools/execute", async (exec, next) => {
|
|
2972
2989
|
try {
|
|
2973
|
-
|
|
2990
|
+
if (isFileCheckpointingEnabled(config)) {
|
|
2991
|
+
await captureBefore(fs, workingDirectory, store, exec, pending);
|
|
2992
|
+
}
|
|
2974
2993
|
} catch (error) {
|
|
2975
2994
|
ctx.logger.warn(`[dsh-rewind] before-capture failed for ${exec.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2976
2995
|
}
|
|
@@ -2982,7 +3001,7 @@ function apply(ctx, config) {
|
|
|
2982
3001
|
if (session !== void 0 && !isSubagentSession(session)) {
|
|
2983
3002
|
void maybeRunAutoCleanup(ctx, store, session.id, dshHome, config?.autoSweepIntervalMs, lifecycle.signal);
|
|
2984
3003
|
}
|
|
2985
|
-
await commitEntry(store, pending, anchorCache, trackedBySession, exec, result);
|
|
3004
|
+
await commitEntry(store, pending, anchorCache, trackedBySession, exec, result, isFileCheckpointingEnabled(config));
|
|
2986
3005
|
} catch (error) {
|
|
2987
3006
|
ctx.logger.warn(`[dsh-rewind] checkpoint commit failed for ${exec.name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2988
3007
|
}
|
|
@@ -2999,10 +3018,11 @@ function apply(ctx, config) {
|
|
|
2999
3018
|
}
|
|
3000
3019
|
export {
|
|
3001
3020
|
Config,
|
|
3021
|
+
DEFAULT_FILE_CHECKPOINTING_ENABLED,
|
|
3002
3022
|
ENGINE_COMMAND,
|
|
3003
3023
|
SnapshotStore,
|
|
3004
3024
|
apply,
|
|
3005
|
-
cleanupConfigKey,
|
|
3006
3025
|
inject,
|
|
3007
|
-
name
|
|
3026
|
+
name,
|
|
3027
|
+
profileEntryKey
|
|
3008
3028
|
};
|
|
@@ -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
|
|
@@ -144,6 +142,8 @@ export declare function isExecutedRewindCommand(node: CommandNode, seq: number):
|
|
|
144
142
|
* always-show so a working option is never hidden on a failed probe.
|
|
145
143
|
*/
|
|
146
144
|
export declare function hasFileImpact(text: string | undefined): boolean;
|
|
145
|
+
/** Whether the preview reports that new file checkpoints are disabled. */
|
|
146
|
+
export declare function hasBackupDisabled(text: string | undefined): boolean;
|
|
147
147
|
/**
|
|
148
148
|
* True when an engine command node is the internal candidate-list probe
|
|
149
149
|
* (`__candidates`) the popupSelect runs to fetch the FULL candidate
|
|
@@ -17,6 +17,7 @@ export declare const zh: {
|
|
|
17
17
|
'popover.both.hint': string;
|
|
18
18
|
'popover.checking': string;
|
|
19
19
|
'popover.noChanges': string;
|
|
20
|
+
'popover.backupDisabled': string;
|
|
20
21
|
'popover.cancel': string;
|
|
21
22
|
'popover.impact.loading': string;
|
|
22
23
|
'popover.impact.failed': string;
|
|
@@ -25,6 +26,9 @@ export declare const zh: {
|
|
|
25
26
|
'popover.impact.delete': string;
|
|
26
27
|
'popover.confirm': string;
|
|
27
28
|
'popover.back': string;
|
|
29
|
+
'backup.title': string;
|
|
30
|
+
'backup.on': string;
|
|
31
|
+
'backup.off': string;
|
|
28
32
|
'cleanup.auto': string;
|
|
29
33
|
'cleanup.auto.on': string;
|
|
30
34
|
'cleanup.auto.off': string;
|
|
@@ -65,6 +69,7 @@ export declare const en: {
|
|
|
65
69
|
'popover.both.hint': string;
|
|
66
70
|
'popover.checking': string;
|
|
67
71
|
'popover.noChanges': string;
|
|
72
|
+
'popover.backupDisabled': string;
|
|
68
73
|
'popover.cancel': string;
|
|
69
74
|
'popover.impact.loading': string;
|
|
70
75
|
'popover.impact.failed': string;
|
|
@@ -73,6 +78,9 @@ export declare const en: {
|
|
|
73
78
|
'popover.impact.delete': string;
|
|
74
79
|
'popover.confirm': string;
|
|
75
80
|
'popover.back': string;
|
|
81
|
+
'backup.title': string;
|
|
82
|
+
'backup.on': string;
|
|
83
|
+
'backup.off': string;
|
|
76
84
|
'cleanup.auto': string;
|
|
77
85
|
'cleanup.auto.on': string;
|
|
78
86
|
'cleanup.auto.off': string;
|
|
@@ -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`.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* This bundle's configuration form for the
|
|
3
|
-
* on its Plugins-page card (`plugins.bundle.config`,
|
|
4
|
-
* name).
|
|
2
|
+
* This bundle's configuration form for the file-checkpointing and snapshot
|
|
3
|
+
* cleanup switches, rendered on its Plugins-page card (`plugins.bundle.config`,
|
|
4
|
+
* keyed by the package name).
|
|
5
5
|
*
|
|
6
6
|
* The staging model, the field controls, and the form frame are the harness's
|
|
7
7
|
* own (`SettingsFormModel` + `SettingsValueField` + `SettingsForm`); the one
|
|
@@ -31,28 +31,30 @@ export declare const CARD_LOCALE_NS = "rewind";
|
|
|
31
31
|
* The plugin entry whose form this is: the bundle's package name, which is the
|
|
32
32
|
* profile entry id the host addresses the configuration by.
|
|
33
33
|
*/
|
|
34
|
-
export declare const
|
|
34
|
+
export declare const SETTINGS_ENTRY_ID = "dsh-rewind-plugin";
|
|
35
35
|
/** The default max age; an empty numeric draft means "use this". */
|
|
36
36
|
export declare const DEFAULT_MAX_AGE_DAYS = 30;
|
|
37
|
-
/** The
|
|
38
|
-
export interface
|
|
37
|
+
/** The editable knobs, exactly as the entry's configuration document names them. */
|
|
38
|
+
export interface SettingsPolicy {
|
|
39
|
+
readonly fileCheckpointingEnabled: boolean;
|
|
39
40
|
readonly autoCleanupEnabled: boolean;
|
|
40
41
|
readonly autoCleanupMaxAgeDays: number;
|
|
41
42
|
}
|
|
42
43
|
/**
|
|
43
44
|
* One form snapshot as this card reads it: the shared form frame's own state
|
|
44
|
-
* (`SettingsFormShell`) plus the
|
|
45
|
+
* (`SettingsFormShell`) plus the three field drafts.
|
|
45
46
|
*/
|
|
46
|
-
export interface
|
|
47
|
+
export interface SettingsCardSnapshot extends SettingsFormShell {
|
|
48
|
+
readonly checkpointingEnabled: SettingsFieldState;
|
|
47
49
|
readonly enabled: SettingsFieldState;
|
|
48
50
|
readonly maxAgeDays: SettingsFieldState;
|
|
49
51
|
}
|
|
50
52
|
/**
|
|
51
53
|
* The bound snapshot store the slot renderer rebinds as the card's
|
|
52
|
-
* `
|
|
54
|
+
* `useSettingsCard` selector hook.
|
|
53
55
|
*/
|
|
54
|
-
export interface
|
|
55
|
-
getSnapshot():
|
|
56
|
+
export interface SettingsCardStore {
|
|
57
|
+
getSnapshot(): SettingsCardSnapshot;
|
|
56
58
|
subscribe(listener: () => void): () => void;
|
|
57
59
|
}
|
|
58
60
|
/**
|
|
@@ -60,7 +62,7 @@ export interface SettingsCleanupStore {
|
|
|
60
62
|
* `SettingsFormScope` read/write face, declared structurally so the card never
|
|
61
63
|
* couples to an internal declaration file's path.
|
|
62
64
|
*/
|
|
63
|
-
export interface
|
|
65
|
+
export interface SettingsFormScope<T> {
|
|
64
66
|
getSnapshot(): {
|
|
65
67
|
readonly status: 'loading' | 'ready' | 'unavailable';
|
|
66
68
|
readonly value: T | undefined;
|
|
@@ -84,22 +86,22 @@ export interface CleanupFormScope<T> {
|
|
|
84
86
|
* runtime owner share (`view`), the locale `t` seat, and the injected face with
|
|
85
87
|
* its `hooks` compartment rebound as a `use<Name>` selector hook.
|
|
86
88
|
*/
|
|
87
|
-
export type
|
|
89
|
+
export type SettingsCardProps = PropsRuntime<'plugins.bundle.config'> & PropsLocale<typeof CARD_LOCALE_NS> & InjectFace<SettingsCardFace>;
|
|
88
90
|
/**
|
|
89
91
|
* Every dictionary key this card reads: the switch row, the day field, and the
|
|
90
92
|
* five labels the shared form frame renders.
|
|
91
93
|
*/
|
|
92
|
-
export type
|
|
94
|
+
export type SettingsLabelKey = 'backup.title' | 'backup.on' | 'backup.off' | 'cleanup.auto' | 'cleanup.auto.on' | 'cleanup.auto.off' | 'cleanup.maxAge' | 'cleanup.maxAge.hint' | 'cleanup.overridden' | 'cleanup.reset' | 'cleanup.invalid' | 'cleanup.unavailable' | 'cleanup.readonly' | 'cleanup.saveFailed' | 'cleanup.save' | 'cleanup.saving';
|
|
93
95
|
/** The form frame's copy, read from this plugin's dictionary. */
|
|
94
|
-
export declare function formLabels(t: (key:
|
|
96
|
+
export declare function formLabels(t: (key: SettingsLabelKey) => string): SettingsFormLabels;
|
|
95
97
|
/**
|
|
96
98
|
* Build the staged form over one entry's configuration.
|
|
97
99
|
* @param scope - the shared per-entry configuration form.
|
|
98
100
|
* @returns the model plus the card's bound read hook.
|
|
99
101
|
*/
|
|
100
|
-
export declare function
|
|
101
|
-
form: SettingsFormModel<
|
|
102
|
-
store: import("@deepseek-ai/dsh-client-store").SnapshotStore<
|
|
102
|
+
export declare function settingsForm(scope: SettingsFormScope<SettingsPolicy>): {
|
|
103
|
+
form: SettingsFormModel<SettingsPolicy>;
|
|
104
|
+
store: import("@deepseek-ai/dsh-client-store").SnapshotStore<SettingsCardSnapshot>;
|
|
103
105
|
};
|
|
104
106
|
/**
|
|
105
107
|
* The bundle's configuration form; `summary` renders nothing (the page only
|
|
@@ -107,10 +109,10 @@ export declare function cleanupForm(scope: CleanupFormScope<CleanupPolicy>): {
|
|
|
107
109
|
* @param props - the view, the card's read hook, locale seat, and form actions.
|
|
108
110
|
* @returns the form element, or null for the summary view.
|
|
109
111
|
*/
|
|
110
|
-
export declare function
|
|
112
|
+
export declare function SettingsCard(props: SettingsCardProps): import("react").JSX.Element | null;
|
|
111
113
|
/** The face the slot registration injects into the card. */
|
|
112
|
-
export interface
|
|
114
|
+
export interface SettingsCardFace extends SettingsFormActions {
|
|
113
115
|
readonly hooks: {
|
|
114
|
-
readonly
|
|
116
|
+
readonly settingsCard: SettingsCardStore;
|
|
115
117
|
};
|
|
116
118
|
}
|
|
@@ -15,6 +15,7 @@ export declare const CLASS: {
|
|
|
15
15
|
readonly popoverOptionLabel: 'dsh-rewind-popover-option-label';
|
|
16
16
|
readonly popoverOptionHint: 'dsh-rewind-popover-option-hint';
|
|
17
17
|
readonly popoverImpact: 'dsh-rewind-popover-impact';
|
|
18
|
+
readonly popoverNote: 'dsh-rewind-popover-note';
|
|
18
19
|
readonly popoverActions: 'dsh-rewind-popover-actions';
|
|
19
20
|
readonly popoverPrimary: 'dsh-rewind-popover-primary';
|
|
20
21
|
readonly popoverGhost: 'dsh-rewind-popover-ghost';
|
|
@@ -31,4 +32,4 @@ export declare const CLASS: {
|
|
|
31
32
|
*/
|
|
32
33
|
export declare const REWIND_ICON_SVG: string;
|
|
33
34
|
/** One injected stylesheet (scoped under `.dsh-rewind-*`). */
|
|
34
|
-
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: 1px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n box-shadow: var(--dsw-shadow-lv3);\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-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: 1px solid var(--dsw-alias-border-l2);\n border-radius: 10px;\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n box-shadow: var(--dsw-shadow-lv3);\n font-size: 13px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n pointer-events: none;\n}\n\n/* ----
|
|
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: 1px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n box-shadow: var(--dsw-shadow-lv3);\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: 1px solid var(--dsw-alias-border-l2);\n border-radius: 10px;\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n box-shadow: var(--dsw-shadow-lv3);\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/index.d.ts
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
* target with itself, cutting only the model-visible surface
|
|
9
9
|
* (`docs/architecture.md`).
|
|
10
10
|
*
|
|
11
|
-
* File restore (mode `both`) follows Claude Code's checkpointing:
|
|
12
|
-
*
|
|
11
|
+
* File restore (mode `both`) follows Claude Code's checkpointing: unless
|
|
12
|
+
* `fileCheckpointingEnabled` is off, the plugin backs up each tracked
|
|
13
|
+
* write-class edit BEFORE it happens (at the
|
|
13
14
|
* `tools/execute` around-dispatch stage, so an approval short-circuit cannot
|
|
14
15
|
* skip the capture and a denied call never records), commits the backup under
|
|
15
16
|
* the turn's anchor message seq at `tools/post-execute`, and a rewind to
|
|
@@ -22,7 +23,7 @@
|
|
|
22
23
|
*
|
|
23
24
|
* @module dsh-rewind
|
|
24
25
|
*/
|
|
25
|
-
import type { Context } from '@deepseek-ai/cordis';
|
|
26
|
+
import type { Context, Volatile } from '@deepseek-ai/cordis';
|
|
26
27
|
import z from '@deepseek-ai/schemastery';
|
|
27
28
|
import { type CleanupSettings } from './snapshot-cleanup.ts';
|
|
28
29
|
export { SnapshotStore } from './snapshot.ts';
|
|
@@ -50,18 +51,22 @@ export interface RewindConfig {
|
|
|
50
51
|
/** Automatic snapshot-sweep cadence in ms. Default {@link AUTO_SWEEP_INTERVAL_MS}. */
|
|
51
52
|
readonly autoSweepIntervalMs?: number;
|
|
52
53
|
}
|
|
54
|
+
/** The default for {@link FileCheckpointingSettings.fileCheckpointingEnabled}. */
|
|
55
|
+
export declare const DEFAULT_FILE_CHECKPOINTING_ENABLED = true;
|
|
53
56
|
/**
|
|
54
|
-
* The
|
|
55
|
-
*
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
*
|
|
62
|
-
*
|
|
57
|
+
* The live file-checkpointing switch (Claude Code's
|
|
58
|
+
* `fileCheckpointingEnabled`): whether NEW before-backups may be recorded.
|
|
59
|
+
*/
|
|
60
|
+
export interface FileCheckpointingSettings {
|
|
61
|
+
readonly fileCheckpointingEnabled: Volatile<boolean>;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The plugin's full configuration: store options plus the three user-editable
|
|
65
|
+
* policy fields. `.volatile()` resolves them into live references for the
|
|
66
|
+
* Plugins page; the names say what they gate, because a bare `enabled` would
|
|
67
|
+
* read like the entry's own `disabled:` flag.
|
|
63
68
|
*/
|
|
64
|
-
export interface Config extends RewindConfig, CleanupSettings {
|
|
69
|
+
export interface Config extends RewindConfig, CleanupSettings, FileCheckpointingSettings {
|
|
65
70
|
}
|
|
66
71
|
/** The `Config` schema the host resolves and projects as this entry's form. */
|
|
67
72
|
export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
@@ -71,6 +76,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
71
76
|
maxFileBytes: z<number, number, "defined">;
|
|
72
77
|
maxAnchorGroups: z<number, number, "defined">;
|
|
73
78
|
autoSweepIntervalMs: z<number, number, "defined">;
|
|
79
|
+
fileCheckpointingEnabled: z<boolean, boolean, "volatile-defined">;
|
|
74
80
|
autoCleanupEnabled: z<boolean, boolean, "volatile-defined">;
|
|
75
81
|
autoCleanupMaxAgeDays: z<number, number, "volatile-defined">;
|
|
76
82
|
}>>, Schemastery.ObjectT<NoInfer<{
|
|
@@ -80,6 +86,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
80
86
|
maxFileBytes: z<number, number, "defined">;
|
|
81
87
|
maxAnchorGroups: z<number, number, "defined">;
|
|
82
88
|
autoSweepIntervalMs: z<number, number, "defined">;
|
|
89
|
+
fileCheckpointingEnabled: z<boolean, boolean, "volatile-defined">;
|
|
83
90
|
autoCleanupEnabled: z<boolean, boolean, "volatile-defined">;
|
|
84
91
|
autoCleanupMaxAgeDays: z<number, number, "volatile-defined">;
|
|
85
92
|
}>>, "plain">;
|
|
@@ -92,7 +99,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
92
99
|
* @param entryId - this fiber's Loader entry id, when it has one.
|
|
93
100
|
* @returns the configuration key, or undefined without an entry.
|
|
94
101
|
*/
|
|
95
|
-
export declare function
|
|
102
|
+
export declare function profileEntryKey(entryId: string | undefined): string | undefined;
|
|
96
103
|
/**
|
|
97
104
|
* Register the `/rewind` command and the checkpoint pipeline (before-capture
|
|
98
105
|
* at `tools/execute`, disk commit at `tools/post-execute`).
|
|
@@ -113,6 +120,7 @@ export declare function cleanupConfigKey(entryId: string | undefined): string |
|
|
|
113
120
|
*
|
|
114
121
|
* @param ctx - context carrying `commands`, `tools`, and an optional `fs`.
|
|
115
122
|
* @param config - resolved plugin config: the store options (`snapshotDir`,
|
|
116
|
-
* `dshHome`, `dedup`) plus the live
|
|
123
|
+
* `dshHome`, `dedup`) plus the live policy (`fileCheckpointingEnabled`, cleanup
|
|
124
|
+
* `enabled`/`maxAgeDays`).
|
|
117
125
|
*/
|
|
118
126
|
export declare function apply(ctx: Context, config?: Config): void;
|
package/lib/types/locales.d.ts
CHANGED
|
@@ -50,6 +50,7 @@ export declare const en: {
|
|
|
50
50
|
'plan.restore': string;
|
|
51
51
|
'plan.delete': string;
|
|
52
52
|
'plan.noChanges': string;
|
|
53
|
+
'plan.backupOff': string;
|
|
53
54
|
'plan.skip': string;
|
|
54
55
|
'skipReason.oversized': string;
|
|
55
56
|
'uncovered.header': string;
|
|
@@ -66,6 +67,7 @@ export declare const en: {
|
|
|
66
67
|
'delete.count': string;
|
|
67
68
|
'skip.count': string;
|
|
68
69
|
noRestorable: string;
|
|
70
|
+
backupOff: string;
|
|
69
71
|
storeUnsupported: string;
|
|
70
72
|
success: string;
|
|
71
73
|
noUserMessages: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-rewind-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"description": "In-window conversation rewind with workspace file restore · 同窗口内对话回退并可还原工作区文件",
|
|
5
5
|
"icon": "./assets/icon.svg",
|
|
6
6
|
"keywords": [
|
|
@@ -147,42 +147,42 @@
|
|
|
147
147
|
"devDependencies": {
|
|
148
148
|
"@deepseek-ai/cordis": "4.0.5",
|
|
149
149
|
"@deepseek-ai/cosmokit": "1.8.6",
|
|
150
|
-
"@deepseek-ai/dsh-agent": "0.2.1-rc.
|
|
151
|
-
"@deepseek-ai/dsh-api-session-controller": "0.2.1-rc.
|
|
152
|
-
"@deepseek-ai/dsh-attachment": "0.2.1-rc.
|
|
153
|
-
"@deepseek-ai/dsh-client-connection": "0.2.1-rc.
|
|
154
|
-
"@deepseek-ai/dsh-client-locale": "0.2.1-rc.
|
|
155
|
-
"@deepseek-ai/dsh-client-store": "0.2.1-rc.
|
|
156
|
-
"@deepseek-ai/dsh-client-ui-commands": "0.2.1-rc.
|
|
157
|
-
"@deepseek-ai/dsh-client-ui-conversation": "0.2.1-rc.
|
|
158
|
-
"@deepseek-ai/dsh-client-ui-input-trigger": "0.2.1-rc.
|
|
159
|
-
"@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.1-rc.
|
|
160
|
-
"@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.
|
|
161
|
-
"@deepseek-ai/dsh-client-ui-session": "0.2.1-rc.
|
|
162
|
-
"@deepseek-ai/dsh-client-ui-settings": "0.2.1-rc.
|
|
163
|
-
"@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.
|
|
164
|
-
"@deepseek-ai/dsh-command-compact": "0.2.1-rc.
|
|
165
|
-
"@deepseek-ai/dsh-commands": "0.2.1-rc.
|
|
166
|
-
"@deepseek-ai/dsh-compaction": "0.2.1-rc.
|
|
167
|
-
"@deepseek-ai/dsh-compaction-basic": "0.2.1-rc.
|
|
168
|
-
"@deepseek-ai/dsh-compaction-image-offload": "0.2.1-rc.
|
|
169
|
-
"@deepseek-ai/dsh-fs": "0.2.1-rc.
|
|
170
|
-
"@deepseek-ai/dsh-goal": "0.2.1-rc.
|
|
171
|
-
"@deepseek-ai/dsh-home-paths": "0.2.1-rc.
|
|
172
|
-
"@deepseek-ai/dsh-llm": "0.2.1-rc.
|
|
173
|
-
"@deepseek-ai/dsh-plan-mode": "0.2.1-rc.
|
|
174
|
-
"@deepseek-ai/dsh-sandbox": "0.2.1-rc.
|
|
175
|
-
"@deepseek-ai/dsh-scope": "0.2.1-rc.
|
|
176
|
-
"@deepseek-ai/dsh-session": "0.2.1-rc.
|
|
177
|
-
"@deepseek-ai/dsh-session-projection": "0.2.1-rc.
|
|
178
|
-
"@deepseek-ai/dsh-session-stats": "0.2.1-rc.
|
|
179
|
-
"@deepseek-ai/dsh-session-title": "0.2.1-rc.
|
|
180
|
-
"@deepseek-ai/dsh-settings": "0.2.1-rc.
|
|
181
|
-
"@deepseek-ai/dsh-token-meter": "0.2.1-rc.
|
|
182
|
-
"@deepseek-ai/dsh-tools": "0.2.1-rc.
|
|
183
|
-
"@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.
|
|
184
|
-
"@deepseek-ai/dsh-user-questions": "0.2.1-rc.
|
|
185
|
-
"@deepseek-ai/dsh-working-directory": "0.2.1-rc.
|
|
150
|
+
"@deepseek-ai/dsh-agent": "0.2.1-rc.2",
|
|
151
|
+
"@deepseek-ai/dsh-api-session-controller": "0.2.1-rc.2",
|
|
152
|
+
"@deepseek-ai/dsh-attachment": "0.2.1-rc.2",
|
|
153
|
+
"@deepseek-ai/dsh-client-connection": "0.2.1-rc.2",
|
|
154
|
+
"@deepseek-ai/dsh-client-locale": "0.2.1-rc.2",
|
|
155
|
+
"@deepseek-ai/dsh-client-store": "0.2.1-rc.2",
|
|
156
|
+
"@deepseek-ai/dsh-client-ui-commands": "0.2.1-rc.2",
|
|
157
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.2.1-rc.2",
|
|
158
|
+
"@deepseek-ai/dsh-client-ui-input-trigger": "0.2.1-rc.2",
|
|
159
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.1-rc.2",
|
|
160
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.2",
|
|
161
|
+
"@deepseek-ai/dsh-client-ui-session": "0.2.1-rc.2",
|
|
162
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.2.1-rc.2",
|
|
163
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.2",
|
|
164
|
+
"@deepseek-ai/dsh-command-compact": "0.2.1-rc.2",
|
|
165
|
+
"@deepseek-ai/dsh-commands": "0.2.1-rc.2",
|
|
166
|
+
"@deepseek-ai/dsh-compaction": "0.2.1-rc.2",
|
|
167
|
+
"@deepseek-ai/dsh-compaction-basic": "0.2.1-rc.2",
|
|
168
|
+
"@deepseek-ai/dsh-compaction-image-offload": "0.2.1-rc.2",
|
|
169
|
+
"@deepseek-ai/dsh-fs": "0.2.1-rc.2",
|
|
170
|
+
"@deepseek-ai/dsh-goal": "0.2.1-rc.2",
|
|
171
|
+
"@deepseek-ai/dsh-home-paths": "0.2.1-rc.2",
|
|
172
|
+
"@deepseek-ai/dsh-llm": "0.2.1-rc.2",
|
|
173
|
+
"@deepseek-ai/dsh-plan-mode": "0.2.1-rc.2",
|
|
174
|
+
"@deepseek-ai/dsh-sandbox": "0.2.1-rc.2",
|
|
175
|
+
"@deepseek-ai/dsh-scope": "0.2.1-rc.2",
|
|
176
|
+
"@deepseek-ai/dsh-session": "0.2.1-rc.2",
|
|
177
|
+
"@deepseek-ai/dsh-session-projection": "0.2.1-rc.2",
|
|
178
|
+
"@deepseek-ai/dsh-session-stats": "0.2.1-rc.2",
|
|
179
|
+
"@deepseek-ai/dsh-session-title": "0.2.1-rc.2",
|
|
180
|
+
"@deepseek-ai/dsh-settings": "0.2.1-rc.2",
|
|
181
|
+
"@deepseek-ai/dsh-token-meter": "0.2.1-rc.2",
|
|
182
|
+
"@deepseek-ai/dsh-tools": "0.2.1-rc.2",
|
|
183
|
+
"@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.2",
|
|
184
|
+
"@deepseek-ai/dsh-user-questions": "0.2.1-rc.2",
|
|
185
|
+
"@deepseek-ai/dsh-working-directory": "0.2.1-rc.2",
|
|
186
186
|
"@deepseek-ai/schemastery": "3.18.5",
|
|
187
187
|
"@types/node": "^24.0.0",
|
|
188
188
|
"@types/react": "^18.3.31",
|