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 CHANGED
@@ -72,11 +72,14 @@ then install the new one.
72
72
 
73
73
  ## Snapshot management
74
74
 
75
- Snapshots (the before-write backups) are stored under `<dsh home>/rewind-snapshots/`
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) and the plugin rebuilds them automatically.
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/cleanup-setting.png" alt="Plugin configuration card: auto cleanup and idle time (days)" width="600">
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` `peerDependencies`.
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
- 快照(写前备份)存储于 `<dsh home>/rewind-snapshots/`(默认 `~/.dsh/rewind-snapshots/`)。插件对**同一会话**的快照做内容去重并保留最近 100 组锚点;**手动删除该目录**仅清除文件备份(对话回退不受影响),插件会自动重建。
72
+ **快照备份**(默认开启):用于文件还原;可在本插件的配置卡片中关闭。关闭后不再创建新的快照,关闭期间的文件改动无法还原。快照存储于 `<dsh home>/rewind-snapshots/`(默认 `~/.dsh/rewind-snapshots/`)。手动删除该目录仅清除文件备份(对话回退不受影响),插件可自动重建。
73
73
 
74
- 另提供**全局自动清理**(默认关闭):把长期不活跃的会话快照整目录移除,不影响活动会话与对话日志。可在**插件**页本插件的配置卡片查看与配置(自动清理开关、失活天数),也可用 `/snapshot-auto-cleanup` 命令查看、设置和运行。详见:[快照自动清理](docs/snapshot-auto-cleanup.zh.md)。
74
+ **全局自动清理**(默认关闭):把长期不活跃的会话快照整目录移除,不影响活动会话与对话日志。可在配置卡片查看与配置(自动清理开关、失活天数),也可用 `/snapshot-auto-cleanup` 命令查看、设置和运行。详见:[快照自动清理](docs/snapshot-auto-cleanup.zh.md)。
75
75
 
76
- <img src="assets/screenshots/cleanup-setting.png" alt="插件配置卡片:自动清理与不活跃时间(天)" width="600">
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` 的 `peerDependencies` 声明。
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
- The plugin **automatically captures** before-backups of tracked mutations, but
30
- **never automatically applies** one. A workspace restore happens only through
31
- an explicit user-invoked `/rewind-plugin @<seq> both` (the per-message ↶ button or the
32
- command channel), and only when all of the following hold:
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 ENGINE_COMMANDS = /* @__PURE__ */ new Set([ENGINE_COMMAND, "rewind"]);
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
- /* ---- Snapshot-cleanup configuration form ---- */
470
- /* The Plugins page draws the bundle's title, icon, crumb, description, and the
471
- shared settings form frame (labels, save, read-only and failure lines); this
472
- styles only the one control the card owns: the auto-cleanup toggle row. */
473
- .dsh-rewind-cleanup-permission {
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-cleanup-toggle-row {
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-cleanup-toggle-label {
503
+ .dsh-rewind-settings-toggle-label {
488
504
  flex: 1;
489
505
  min-width: 0;
490
506
  }
491
- .dsh-rewind-cleanup-hint {
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
- impact.textContent = lines.join("\n");
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
- hideWithdrawnSeats(
1201
- chat,
1202
- chat === void 0 ? [] : document.querySelectorAll(CHAT_SEAT_SELECTOR),
1203
- hiddenSeqs,
1204
- hidden.current
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 observer = new MutationObserver(queueRefresh);
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.16.0" : "dev";
1419
+ var PLUGIN_VERSION = true ? "0.17.1" : "dev";
1372
1420
  var PLUGIN_PACKAGE = "dsh-rewind-plugin";
1373
- var BUILD_HASH = true ? "c6cfbc55" : "dev";
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\u4FDD\u7559\u5168\u90E8\u5FEB\u7167\uFF0C\u4E0D\u81EA\u52A8\u6E05\u7406",
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 disabled, all snapshot backups are kept; nothing is cleaned automatically.",
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 CLEANUP_ENTRY_ID = "dsh-rewind-plugin";
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 cleanupForm(scope) {
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 SettingsCleanupCard(props) {
1491
- const { t, useCleanupCard } = props;
1492
- const state = useCleanupCard((snapshot) => snapshot);
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 enabled = state.enabled.text === "true";
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-cleanup-permission", children: [
1505
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-rewind-cleanup-toggle-row", children: [
1506
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-rewind-cleanup-toggle-label", children: t("cleanup.auto") }),
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: enabled,
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-cleanup-hint", children: t(enabled ? "cleanup.auto.on" : "cleanup.auto.off") })
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
- enabled ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
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 } = cleanupForm(
1672
- ctx.configForms.get(CLEANUP_ENTRY_ID)
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 cleanup form");
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: { cleanupCard: store }, ...form.actions() })
1766
+ inject: () => ({ hooks: { settingsCard: store }, ...form.actions() })
1683
1767
  },
1684
- SettingsCleanupCard
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 cleanupConfigKey(entryId) {
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 = cleanupConfigKey(rewindConfigKey(ctx));
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
- await captureBefore(fs, workingDirectory, store, exec, pending);
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
- * 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
- */
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 mutation batch via queueMicrotask) and diffed (setState is
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 scan is driven by both channels its targets are made of: the DOM (rows and
323
- * their actions containers) and the session's `inbox` projection (the pending
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 snapshot-cleanup policy, rendered
3
- * on its Plugins-page card (`plugins.bundle.config`, keyed by the package
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 CLEANUP_ENTRY_ID = "dsh-rewind-plugin";
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 two editable knobs, exactly as the entry's configuration document names them. */
38
- export interface CleanupPolicy {
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 two field drafts.
45
+ * (`SettingsFormShell`) plus the three field drafts.
45
46
  */
46
- export interface CleanupCardSnapshot extends SettingsFormShell {
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
- * `useCleanupCard` selector hook.
54
+ * `useSettingsCard` selector hook.
53
55
  */
54
- export interface SettingsCleanupStore {
55
- getSnapshot(): CleanupCardSnapshot;
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 CleanupFormScope<T> {
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 SettingsCleanupCardProps = PropsRuntime<'plugins.bundle.config'> & PropsLocale<typeof CARD_LOCALE_NS> & InjectFace<SettingsCleanupFace>;
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 CleanupLabelKey = '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';
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: CleanupLabelKey) => string): SettingsFormLabels;
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 cleanupForm(scope: CleanupFormScope<CleanupPolicy>): {
101
- form: SettingsFormModel<CleanupPolicy>;
102
- store: import("@deepseek-ai/dsh-client-store").SnapshotStore<CleanupCardSnapshot>;
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 SettingsCleanupCard(props: SettingsCleanupCardProps): import("react").JSX.Element | null;
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 SettingsCleanupFace extends SettingsFormActions {
114
+ export interface SettingsCardFace extends SettingsFormActions {
113
115
  readonly hooks: {
114
- readonly cleanupCard: SettingsCleanupStore;
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/* ---- Snapshot-cleanup configuration form ---- */\n/* The Plugins page draws the bundle's title, icon, crumb, description, and the\n shared settings form frame (labels, save, read-only and failure lines); this\n styles only the one control the card owns: the auto-cleanup toggle row. */\n.dsh-rewind-cleanup-permission {\n display: grid;\n gap: 6px;\n padding: 12px 0;\n}\n.dsh-rewind-cleanup-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-cleanup-toggle-label {\n flex: 1;\n min-width: 0;\n}\n.dsh-rewind-cleanup-hint {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\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";
@@ -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: the plugin
12
- * backs up each tracked write-class edit BEFORE it happens (at the
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 plugin's full configuration: the ordinary store options plus the
55
- * user-editable cleanup policy. The two policy fields are `.volatile()`, so the
56
- * host resolves them into live references before `apply` and the Plugins page
57
- * exposes exactly them.
58
- *
59
- * The policy fields are named for WHAT they gate (the automatic sweep; the
60
- * `/snapshot-auto-cleanup` command's manual `run` ignores the switch), because
61
- * this entry's own `disabled:` flag sits beside them in the same document and a
62
- * bare `enabled` reads like the plugin switch.
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 cleanupConfigKey(entryId: string | undefined): string | undefined;
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 cleanup policy (`enabled`, `maxAgeDays`).
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;
@@ -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.16.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.1",
151
- "@deepseek-ai/dsh-api-session-controller": "0.2.1-rc.1",
152
- "@deepseek-ai/dsh-attachment": "0.2.1-rc.1",
153
- "@deepseek-ai/dsh-client-connection": "0.2.1-rc.1",
154
- "@deepseek-ai/dsh-client-locale": "0.2.1-rc.1",
155
- "@deepseek-ai/dsh-client-store": "0.2.1-rc.1",
156
- "@deepseek-ai/dsh-client-ui-commands": "0.2.1-rc.1",
157
- "@deepseek-ai/dsh-client-ui-conversation": "0.2.1-rc.1",
158
- "@deepseek-ai/dsh-client-ui-input-trigger": "0.2.1-rc.1",
159
- "@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.1-rc.1",
160
- "@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.1",
161
- "@deepseek-ai/dsh-client-ui-session": "0.2.1-rc.1",
162
- "@deepseek-ai/dsh-client-ui-settings": "0.2.1-rc.1",
163
- "@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.1",
164
- "@deepseek-ai/dsh-command-compact": "0.2.1-rc.1",
165
- "@deepseek-ai/dsh-commands": "0.2.1-rc.1",
166
- "@deepseek-ai/dsh-compaction": "0.2.1-rc.1",
167
- "@deepseek-ai/dsh-compaction-basic": "0.2.1-rc.1",
168
- "@deepseek-ai/dsh-compaction-image-offload": "0.2.1-rc.1",
169
- "@deepseek-ai/dsh-fs": "0.2.1-rc.1",
170
- "@deepseek-ai/dsh-goal": "0.2.1-rc.1",
171
- "@deepseek-ai/dsh-home-paths": "0.2.1-rc.1",
172
- "@deepseek-ai/dsh-llm": "0.2.1-rc.1",
173
- "@deepseek-ai/dsh-plan-mode": "0.2.1-rc.1",
174
- "@deepseek-ai/dsh-sandbox": "0.2.1-rc.1",
175
- "@deepseek-ai/dsh-scope": "0.2.1-rc.1",
176
- "@deepseek-ai/dsh-session": "0.2.1-rc.1",
177
- "@deepseek-ai/dsh-session-projection": "0.2.1-rc.1",
178
- "@deepseek-ai/dsh-session-stats": "0.2.1-rc.1",
179
- "@deepseek-ai/dsh-session-title": "0.2.1-rc.1",
180
- "@deepseek-ai/dsh-settings": "0.2.1-rc.1",
181
- "@deepseek-ai/dsh-token-meter": "0.2.1-rc.1",
182
- "@deepseek-ai/dsh-tools": "0.2.1-rc.1",
183
- "@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.1",
184
- "@deepseek-ai/dsh-user-questions": "0.2.1-rc.1",
185
- "@deepseek-ai/dsh-working-directory": "0.2.1-rc.1",
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",