dsh-rewind-plugin 0.15.1 → 0.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/SECURITY.md CHANGED
@@ -28,7 +28,7 @@ re-verify the host's own authority boundaries.
28
28
 
29
29
  The plugin **automatically captures** before-backups of tracked mutations, but
30
30
  **never automatically applies** one. A workspace restore happens only through
31
- an explicit user-invoked `/rewind @<seq> both` (the per-message ↶ button or the
31
+ an explicit user-invoked `/rewind-plugin @<seq> both` (the per-message ↶ button or the
32
32
  command channel), and only when all of the following hold:
33
33
 
34
34
  1. **A validated target**: `planRewind` accepts only a `user/message` seq that
package/lib/client.js CHANGED
@@ -35,6 +35,8 @@ __export(index_exports, {
35
35
  module.exports = __toCommonJS(index_exports);
36
36
 
37
37
  // src/client/hidden.ts
38
+ var ENGINE_COMMAND = "rewind-plugin";
39
+ var ENGINE_COMMANDS = /* @__PURE__ */ new Set([ENGINE_COMMAND, "rewind"]);
38
40
  function resolveChatWatch(resolveView, sessionId, cb) {
39
41
  const view = resolveView(sessionId);
40
42
  return view?.subscribe?.(cb) ?? (() => {
@@ -86,7 +88,7 @@ function targetSeqOfArgs(args) {
86
88
  return match !== null ? Number(match[1]) : void 0;
87
89
  }
88
90
  function isExecutedRewindCommand(node, seq) {
89
- if (node.name !== "rewind" || node.outcome?.kind !== "success") return false;
91
+ if (node.name === null || !ENGINE_COMMANDS.has(node.name) || node.outcome?.kind !== "success") return false;
90
92
  if (node.outcome.sourceEventSeq === void 0) return false;
91
93
  const args = node.args ?? "";
92
94
  return new RegExp(`(?:^|\\s)@${seq}(?:\\s|$)`).test(args);
@@ -129,6 +131,39 @@ function cutsContain(cuts, seq) {
129
131
  }
130
132
  return false;
131
133
  }
134
+ function liveAnchorKeys(snap, cuts) {
135
+ const live = /* @__PURE__ */ new Map();
136
+ for (const key of snap.order) {
137
+ const node = snap.nodes.get(key);
138
+ if (node === void 0 || cutsContain(cuts, node.anchorSeq)) continue;
139
+ const keys = live.get(node.anchorSeq);
140
+ if (keys === void 0) live.set(node.anchorSeq, [key]);
141
+ else keys.push(key);
142
+ }
143
+ return live;
144
+ }
145
+ var EMPTY_KEYS = [];
146
+ function addWithdrawnTurnChrome(snap, cuts, hidden) {
147
+ const locations = snap.locations;
148
+ if (locations === void 0 || cuts.length === 0) return;
149
+ let live;
150
+ for (const key of snap.order) {
151
+ const chrome = snap.nodes.get(key);
152
+ if (chrome === void 0 || chrome.kind !== "turn-process") continue;
153
+ const located = chrome.location?.turn;
154
+ if (located?.start === void 0) continue;
155
+ const turn = located.turn;
156
+ if (typeof turn !== "number") continue;
157
+ const keys = locations.getTurn(turn);
158
+ if (keys.length === 0) continue;
159
+ const rows = keys.map((member) => snap.nodes.get(member)).filter((member) => member !== void 0);
160
+ if (rows.length !== keys.length) continue;
161
+ if (!rows.every((member) => member.key === chrome.key || cutsContain(cuts, member.anchorSeq))) continue;
162
+ live ?? (live = liveAnchorKeys(snap, cuts));
163
+ if ((live.get(chrome.anchorSeq) ?? EMPTY_KEYS).some((holder) => holder !== chrome.key)) continue;
164
+ hidden.add(chrome.anchorSeq);
165
+ }
166
+ }
132
167
  function hiddenSeqsOf(snap) {
133
168
  const hidden = /* @__PURE__ */ new Set();
134
169
  const spans = [];
@@ -136,7 +171,7 @@ function hiddenSeqsOf(snap) {
136
171
  const node = snap.nodes.get(key);
137
172
  if (node === void 0 || node.kind !== "command") continue;
138
173
  const command = node.data;
139
- if (command.name !== "rewind") continue;
174
+ if (command.name === null || !ENGINE_COMMANDS.has(command.name)) continue;
140
175
  if (isPreviewCommand(command) || isCandidateCommand(command)) {
141
176
  hidden.add(command.seq);
142
177
  continue;
@@ -156,6 +191,10 @@ function hiddenSeqsOf(snap) {
156
191
  if (node === void 0) continue;
157
192
  if (cutsContain(cuts, node.anchorSeq)) hidden.add(node.anchorSeq);
158
193
  }
194
+ try {
195
+ addWithdrawnTurnChrome(snap, cuts, hidden);
196
+ } catch {
197
+ }
159
198
  return hidden;
160
199
  }
161
200
 
@@ -547,13 +586,13 @@ function waitForCommand(session, chatOf, match, timeoutMs = 8e3, watch) {
547
586
  }
548
587
  function isPreviewFor(node, seq) {
549
588
  const args = node.args ?? "";
550
- return node.name === "rewind" && args.includes("preview") && new RegExp(`(?:^|\\s)@${seq}(?=\\s|$)`).test(args);
589
+ return node.name !== null && ENGINE_COMMANDS.has(node.name) && args.includes("preview") && new RegExp(`(?:^|\\s)@${seq}(?=\\s|$)`).test(args);
551
590
  }
552
591
  async function previewImpact(session, chatOf, seq, watch) {
553
592
  const known = knownCommandSeqs(session, chatOf, (node) => isPreviewFor(node, seq));
554
593
  let result;
555
594
  try {
556
- result = await session.command(`/rewind preview @${seq} both`);
595
+ result = await session.command(`/${ENGINE_COMMAND} preview @${seq} both`);
557
596
  } catch (error) {
558
597
  rewindLog.warn("preview", `preview command threw for @${seq}`, error);
559
598
  return { kind: "error", text: error instanceof Error ? error.message : String(error) };
@@ -924,7 +963,7 @@ async function runRewindAndFill(session, seq, mode, isMainViewSession, chatOf, w
924
963
  const known = knownCommandSeqs(session, chatOf, (node) => isExecutedRewindCommand(node, seq));
925
964
  let result;
926
965
  try {
927
- result = await session.command(`/rewind @${seq} ${mode}`);
966
+ result = await session.command(`/${ENGINE_COMMAND} @${seq} ${mode}`);
928
967
  } catch (error) {
929
968
  rewindLog.warn("refill", `rewind command threw, skipping refill @${seq}`, error);
930
969
  return;
@@ -1320,9 +1359,9 @@ function createRewindBridge(deps) {
1320
1359
  }
1321
1360
 
1322
1361
  // src/client/build-info.ts
1323
- var PLUGIN_VERSION = true ? "0.15.1" : "dev";
1362
+ var PLUGIN_VERSION = true ? "0.15.2" : "dev";
1324
1363
  var PLUGIN_PACKAGE = "dsh-rewind-plugin";
1325
- var BUILD_HASH = true ? "0582c3a6" : "dev";
1364
+ var BUILD_HASH = true ? "75d20acc" : "dev";
1326
1365
 
1327
1366
  // src/client/locales.ts
1328
1367
  var zh = {
@@ -1646,7 +1685,7 @@ function apply(ctx) {
1646
1685
  };
1647
1686
  const fetchHostCandidates = async (face, chatOf2) => {
1648
1687
  const known = knownCommandSeqs(face, chatOf2, (node) => isCandidateCommand(node));
1649
- const result = await face.command("/rewind __candidates");
1688
+ const result = await face.command(`/${ENGINE_COMMAND} __candidates`);
1650
1689
  if (!result.ok || result.value?.matched !== true) return void 0;
1651
1690
  const outcome = await waitForCommand(face, chatOf2, (node) => isCandidateCommand(node) && !known.has(node.seq), 8e3, (cb) => watchChat(face.sessionId, cb));
1652
1691
  if (outcome === null || outcome.kind !== "success" || outcome.text === void 0) return void 0;
package/lib/index.js CHANGED
@@ -9,12 +9,6 @@ function preferredLocale(preference) {
9
9
  return preference.toLowerCase().split("-")[0] === "zh" ? "zh" : "en";
10
10
  }
11
11
  var en = {
12
- "usage.title": "Usage:",
13
- "usage.noArgs": " /rewind (no args) withdraw the most recent user message",
14
- "usage.seq": " /rewind @<seq> chat|both rewind to the given message (chat = conversation only / both = conversation + files)",
15
- "usage.blocked": " /rewind or /undo open the rewind picker",
16
- "describeTarget.seq": "seq {seq}",
17
- "describeTarget.index": "message {index}",
18
12
  "plan.rewinding": "Rewind to seq {targetSeq}, removing {count} node(s) from the model context (conversation log kept).",
19
13
  "plan.affects": "Affects {count} file(s):",
20
14
  "plan.restore": "restore {path}",
@@ -39,9 +33,11 @@ var en = {
39
33
  "storeUnsupported": "file restore unavailable: these snapshots use a newer store format (v{version}) than this plugin understands; nothing was changed. Update the plugin or clear this session's snapshots.",
40
34
  "success": "Withdrawn seq {targetSeq} and everything after it (conversation returned to earlier){restore}.",
41
35
  "noUserMessages": "This session has no rewindable user messages yet.",
42
- "chooseMode": "Rewind to {target}. Choose a mode:\n /rewind {target} chat conversation only\n /rewind {target} both conversation + file restore",
43
36
  "command.description": "Rewind the conversation and/or code to a previous point",
44
37
  "command.undoDescription": "Alias of /rewind",
38
+ "engine.description": "dsh-rewind internal command",
39
+ "engine.rejected": "Internal command: use /rewind or /undo.",
40
+ "engine.inputHint": "@<seq> chat|both",
45
41
  "cleanup.description": "Manage automatic cleanup of session snapshot backups",
46
42
  "cleanup.inputHint": "on | off | max-age <days> | run [--apply] [--current]",
47
43
  "cleanup.status": "Auto-cleanup: {state}. Max age: {days} day(s).",
@@ -64,12 +60,6 @@ var en = {
64
60
  "cleanup.usage": "Usage:\n /snapshot-auto-cleanup show status\n /snapshot-auto-cleanup on|off enable/disable auto-cleanup\n /snapshot-auto-cleanup max-age <days> set the idle cutoff\n /snapshot-auto-cleanup run [--apply] dry-run, or execute with --apply\n /snapshot-auto-cleanup run --current [--apply] dry-run/clear this session's snapshots"
65
61
  };
66
62
  var zh = {
67
- "usage.title": "\u7528\u6CD5\uFF1A",
68
- "usage.noArgs": " /rewind \uFF08\u65E0\u53C2\u6570\uFF09\u64A4\u56DE\u6700\u8FD1\u4E00\u6761\u7528\u6237\u6D88\u606F",
69
- "usage.seq": " /rewind @<seq> chat|both \u56DE\u9000\u5230\u6307\u5B9A\u6D88\u606F\uFF08chat \u4EC5\u5BF9\u8BDD / both \u5BF9\u8BDD+\u6587\u4EF6\uFF09",
70
- "usage.blocked": " /rewind \u6216 /undo \u6253\u5F00\u56DE\u9000\u9009\u62E9\u9762\u677F",
71
- "describeTarget.seq": "seq {seq}",
72
- "describeTarget.index": "\u7B2C {index} \u6761\u6D88\u606F",
73
63
  "plan.rewinding": "\u5C06\u56DE\u9000\u5230 seq {targetSeq}\uFF0C\u4ECE\u6A21\u578B\u4E0A\u4E0B\u6587\u79FB\u9664 {count} \u4E2A\u8282\u70B9\uFF08\u5BF9\u8BDD\u65E5\u5FD7\u4FDD\u7559\uFF09\u3002",
74
64
  "plan.affects": "\u5C06\u5F71\u54CD {count} \u4E2A\u6587\u4EF6\uFF1A",
75
65
  "plan.restore": "\u8FD8\u539F {path}",
@@ -94,9 +84,11 @@ var zh = {
94
84
  "storeUnsupported": "\u6587\u4EF6\u8FD8\u539F\u4E0D\u53EF\u7528\uFF1A\u8FD9\u4E9B\u5FEB\u7167\u4F7F\u7528\u4E86\u6BD4\u672C\u63D2\u4EF6\u66F4\u65B0\u7684\u5B58\u50A8\u683C\u5F0F\uFF08v{version}\uFF09\uFF1B\u5DE5\u4F5C\u533A\u672A\u505A\u4EFB\u4F55\u6539\u52A8\u3002\u8BF7\u66F4\u65B0\u63D2\u4EF6\uFF0C\u6216\u6E05\u9664\u8BE5\u4F1A\u8BDD\u7684\u5FEB\u7167\u3002",
95
85
  "success": "\u5DF2\u64A4\u56DE seq {targetSeq} \u53CA\u4E4B\u540E\u5185\u5BB9\uFF08\u5BF9\u8BDD\u5DF2\u56DE\u5230\u6B64\u524D\uFF09{restore}\u3002",
96
86
  "noUserMessages": "\u5F53\u524D\u4F1A\u8BDD\u8FD8\u6CA1\u6709\u53EF\u56DE\u9000\u7684\u7528\u6237\u6D88\u606F\u3002",
97
- "chooseMode": "\u5C06\u56DE\u9000\u5230 {target}\u3002\u9009\u62E9\u6A21\u5F0F\uFF1A\n /rewind {target} chat \u4EC5\u56DE\u9000\u5BF9\u8BDD\n /rewind {target} both \u56DE\u9000\u5BF9\u8BDD\u5E76\u8FD8\u539F\u6587\u4EF6",
98
87
  "command.description": "\u56DE\u9000\u5BF9\u8BDD\u548C/\u6216\u4EE3\u7801\u5230\u4E4B\u524D\u7684\u67D0\u6761\u6D88\u606F",
99
88
  "command.undoDescription": "/rewind \u7684\u522B\u540D",
89
+ "engine.description": "dsh-rewind \u5185\u90E8\u547D\u4EE4",
90
+ "engine.rejected": "\u5185\u90E8\u547D\u4EE4\uFF1A\u8BF7\u4F7F\u7528 /rewind \u6216 /undo\u3002",
91
+ "engine.inputHint": "@<seq> chat|both",
100
92
  "cleanup.description": "\u7BA1\u7406\u4F1A\u8BDD\u5FEB\u7167\u5907\u4EFD\u7684\u81EA\u52A8\u6E05\u7406",
101
93
  "cleanup.inputHint": "on | off | max-age <\u5929\u6570> | run [--apply] [--current]",
102
94
  "cleanup.status": "\u81EA\u52A8\u6E05\u7406\uFF1A{state}\u3002\u6700\u5927\u4FDD\u7559\u5929\u6570\uFF1A{days} \u5929\u3002",
@@ -2332,6 +2324,7 @@ function shouldRunAutoSweep(lastAtMs, nowMs) {
2332
2324
  // src/index.ts
2333
2325
  var name = "dsh-rewind";
2334
2326
  var inject = ["commands", "tools", "settings"];
2327
+ var ENGINE_COMMAND = "rewind-plugin";
2335
2328
  var Config = z.object({
2336
2329
  snapshotDir: z.string().required(false),
2337
2330
  dshHome: z.string().required(false),
@@ -2359,13 +2352,11 @@ var cleanupStore;
2359
2352
  function t(key, params) {
2360
2353
  return translate(activeLocale, key, params);
2361
2354
  }
2362
- function usage() {
2363
- return [
2364
- t("usage.title"),
2365
- t("usage.noArgs"),
2366
- t("usage.seq"),
2367
- t("usage.blocked")
2368
- ].join("\n");
2355
+ function warnCommandUnavailable(ctx, name2, error) {
2356
+ ctx.logger.warn(`[dsh-rewind] command "${name2}" was not registered: ${error instanceof Error ? error.message : String(error)}`);
2357
+ if (name2 === ENGINE_COMMAND) {
2358
+ ctx.logger.warn(`[dsh-rewind] the internal "${ENGINE_COMMAND}" engine channel is unavailable; the rewind button and picker will not work.`);
2359
+ }
2369
2360
  }
2370
2361
  async function discardCapture(capture) {
2371
2362
  if (capture === void 0 || capture.backup === null) return;
@@ -2485,9 +2476,6 @@ function buildMarker() {
2485
2476
  source: REWIND_MARKER_SOURCE
2486
2477
  });
2487
2478
  }
2488
- function describeTarget(target) {
2489
- return target.kind === "seq" ? t("describeTarget.seq", { seq: target.seq }) : t("describeTarget.index", { index: target.index });
2490
- }
2491
2479
  function formatBytes(bytes) {
2492
2480
  if (bytes >= 1024 * 1024) return `${Math.round(bytes / (1024 * 1024))} MiB`;
2493
2481
  if (bytes >= 1024) return `${Math.round(bytes / 1024)} KiB`;
@@ -2681,7 +2669,7 @@ async function handleRewind(ctx, store, fs, invocation, inflight) {
2681
2669
  const parts = input.split(/\s+/);
2682
2670
  if (parts[0] === "preview") {
2683
2671
  const target2 = parts[1];
2684
- if (target2 === void 0) return { kind: "error", text: usage() };
2672
+ if (target2 === void 0) return { kind: "error", text: t("engine.rejected") };
2685
2673
  let plan;
2686
2674
  try {
2687
2675
  plan = resolveOrError(session.snapshotEvents(), session.surface.nodes, target2);
@@ -2705,16 +2693,9 @@ async function handleRewind(ctx, store, fs, invocation, inflight) {
2705
2693
  const target = parts[0];
2706
2694
  const mode = parts[1];
2707
2695
  if (mode !== void 0 && mode !== "chat" && mode !== "both") {
2708
- return { kind: "error", text: usage() };
2709
- }
2710
- if (mode === void 0) {
2711
- const parsed = parseRewindTarget(target);
2712
- if (parsed === void 0) return { kind: "error", text: usage() };
2713
- return {
2714
- kind: "success",
2715
- text: t("chooseMode", { target: describeTarget(parsed) })
2716
- };
2696
+ return { kind: "error", text: t("engine.rejected") };
2717
2697
  }
2698
+ if (mode === void 0) return { kind: "error", text: t("engine.rejected") };
2718
2699
  return executeRewind(ctx, store, fs, invocation, target, mode, inflight);
2719
2700
  }
2720
2701
  var autoSweepChecked = false;
@@ -2892,22 +2873,47 @@ function apply(ctx, config) {
2892
2873
  const rewindHandler = withHostLocale(
2893
2874
  (invocation) => handleRewind(ctx, store, fsService, invocation, inflight)
2894
2875
  );
2895
- yield ctx.commands.register({
2896
- name: "rewind",
2897
- description: t("command.description"),
2898
- handler: rewindHandler
2899
- });
2900
- yield ctx.commands.register({
2901
- name: "undo",
2902
- description: t("command.undoDescription"),
2903
- handler: rewindHandler
2904
- });
2905
- yield ctx.commands.register({
2906
- name: "snapshot-auto-cleanup",
2907
- description: t("cleanup.description"),
2908
- input: { hint: t("cleanup.inputHint") },
2909
- handler: withHostLocale((invocation) => handleSnapshotCleanup(store, invocation, dshHome, trackedBySession))
2910
- });
2876
+ const engineHandler = withHostLocale(
2877
+ (invocation) => invocation.rawInput.trim() === "" ? Promise.resolve({ kind: "error", text: t("engine.rejected") }) : handleRewind(ctx, store, fsService, invocation, inflight)
2878
+ );
2879
+ try {
2880
+ yield ctx.commands.register({
2881
+ name: "rewind",
2882
+ description: t("command.description"),
2883
+ handler: rewindHandler
2884
+ });
2885
+ } catch (error) {
2886
+ warnCommandUnavailable(ctx, "rewind", error);
2887
+ }
2888
+ try {
2889
+ yield ctx.commands.register({
2890
+ name: "undo",
2891
+ description: t("command.undoDescription"),
2892
+ handler: rewindHandler
2893
+ });
2894
+ } catch (error) {
2895
+ warnCommandUnavailable(ctx, "undo", error);
2896
+ }
2897
+ try {
2898
+ yield ctx.commands.register({
2899
+ name: ENGINE_COMMAND,
2900
+ description: t("engine.description"),
2901
+ input: { hint: t("engine.inputHint") },
2902
+ handler: engineHandler
2903
+ });
2904
+ } catch (error) {
2905
+ warnCommandUnavailable(ctx, ENGINE_COMMAND, error);
2906
+ }
2907
+ try {
2908
+ yield ctx.commands.register({
2909
+ name: "snapshot-auto-cleanup",
2910
+ description: t("cleanup.description"),
2911
+ input: { hint: t("cleanup.inputHint") },
2912
+ handler: withHostLocale((invocation) => handleSnapshotCleanup(store, invocation, dshHome, trackedBySession))
2913
+ });
2914
+ } catch (error) {
2915
+ warnCommandUnavailable(ctx, "snapshot-auto-cleanup", error);
2916
+ }
2911
2917
  }, "dsh-rewind command");
2912
2918
  ctx.on("agent/created", ({ agent }) => {
2913
2919
  const session = agent.session;
@@ -2987,6 +2993,7 @@ function apply(ctx, config) {
2987
2993
  }
2988
2994
  export {
2989
2995
  Config,
2996
+ ENGINE_COMMAND,
2990
2997
  SnapshotStore,
2991
2998
  apply,
2992
2999
  cleanupConfigKey,
@@ -6,6 +6,17 @@
6
6
  * @module dsh-rewind/client/hidden
7
7
  */
8
8
  import type { CommandNode } from '@deepseek-ai/dsh-client-ui-conversation/client';
9
+ /**
10
+ * The internal engine channel every client call is sent to. Mirrored by
11
+ * `ENGINE_COMMAND` in `src/index.ts` — the two bundles share no module.
12
+ */
13
+ export declare const ENGINE_COMMAND = "rewind-plugin";
14
+ /**
15
+ * Recognized engine command names. `rewind` is what sessions written before the
16
+ * engine channel existed contain, so keeping it preserves their withdrawn
17
+ * spans (forward compatibility only: an older client cannot know the new name).
18
+ */
19
+ export declare const ENGINE_COMMANDS: ReadonlySet<string>;
9
20
  /** A chat-snapshot view node as the hiding / composer-refill logic reads it.
10
21
  * The harness's view node shape plus the `anchorSeq` the chat snapshot carries
11
22
  * (the plugin reads `anchorSeq` off each node; it is not declared on the
@@ -15,6 +26,14 @@ export interface ChatConversationViewNode {
15
26
  readonly kind?: string;
16
27
  readonly data?: unknown;
17
28
  readonly anchorSeq: number;
29
+ /** Owning Turn as the chat snapshot carries it; its `turn` and `start` are read
30
+ * (see `addWithdrawnTurnChrome`). */
31
+ readonly location?: {
32
+ readonly turn?: {
33
+ readonly turn?: unknown;
34
+ readonly start?: unknown;
35
+ };
36
+ };
18
37
  }
19
38
  /** Minimal chat snapshot reader the hiding logic needs. */
20
39
  export interface HiddenChat {
@@ -22,6 +41,10 @@ export interface HiddenChat {
22
41
  readonly nodes: {
23
42
  get(key: string): ChatConversationViewNode | undefined;
24
43
  };
44
+ /** Turn-keyed node index; absent on older snapshots, which the chrome pass tolerates. */
45
+ readonly locations?: {
46
+ getTurn(turn: number): readonly string[];
47
+ };
25
48
  }
26
49
  /**
27
50
  * Reader for one session's live chat snapshot: the chat is served by the
@@ -103,9 +126,9 @@ export declare function messageAttachmentsAt(chat: HiddenChat | undefined, seq:
103
126
  */
104
127
  export declare function targetSeqOfArgs(args: string | null | undefined): number | undefined;
105
128
  /**
106
- * True when a `/rewind` command node is an EXECUTED rewind for `seq` — the
107
- * admission form the popover drives (`@<seq> chat` / `both`) that settled
108
- * with a marker-carrying success outcome. The composer refill waits for
129
+ * True when one of this plugin's engine command nodes is an EXECUTED rewind for
130
+ * `seq` — the admission form the popover drives (`@<seq> chat` / `both`) that
131
+ * settled with a marker-carrying success outcome. The composer refill waits for
109
132
  * exactly this node after the user confirms, so a history-loaded command can
110
133
  * never trigger a fill.
111
134
  */
@@ -122,8 +145,8 @@ export declare function isExecutedRewindCommand(node: CommandNode, seq: number):
122
145
  */
123
146
  export declare function hasFileImpact(text: string | undefined): boolean;
124
147
  /**
125
- * True when a `/rewind` command node is the internal candidate-list probe
126
- * (`/rewind __candidates`) the popupSelect runs to fetch the FULL candidate
148
+ * True when an engine command node is the internal candidate-list probe
149
+ * (`__candidates`) the popupSelect runs to fetch the FULL candidate
127
150
  * list from the host. Like previews, its flow node never surfaces in the
128
151
  * transcript — it only feeds the popup — so it is hidden in every state.
129
152
  */
@@ -140,5 +163,8 @@ export declare function isCandidateCommand(command: CommandNode): boolean;
140
163
  * still-on-surface gap of new traffic between an earlier marker and a later
141
164
  * target. Endpoints come from the command nodes (`sourceEventSeq` is the
142
165
  * marker's log seq; the outcome text carries the target seq).
166
+ *
167
+ * A Turn whose rows were ALL withdrawn also contributes its Turn-process summary
168
+ * row, whose own anchor can precede the target (see `addWithdrawnTurnChrome`).
143
169
  */
144
170
  export declare function hiddenSeqsOf(snap: HiddenChat): Set<number>;
@@ -2,7 +2,7 @@
2
2
  * The rewind mode-selection popover (plain DOM, no React). Step two of the
3
3
  * interaction: the target is already fixed (the clicked message); the popover
4
4
  * offers the two modes. Choosing "both" first fetches the impact list through
5
- * the `/rewind preview @seq both` command and shows it before confirming.
5
+ * the engine channel's `preview @seq both` command and shows it before confirming.
6
6
  *
7
7
  * Keyboard: ↑/↓ move focus across the step's ACTION buttons only (the two
8
8
  * modes, or the confirm button on the impact step), Enter activates the
@@ -29,6 +29,12 @@ export { SnapshotStore } from './snapshot.ts';
29
29
  export type { CheckpointEntry, FileImpact, PruneStaleReport, RestoreOutcome, RestoreJournal, RestoreJournalState, RestoreReconcileReport } from './snapshot.ts';
30
30
  export declare const name = "dsh-rewind";
31
31
  export declare const inject: string[];
32
+ /**
33
+ * The internal engine channel the client drives, so the rewind surface never
34
+ * depends on a name a third-party plugin can take. Mirrored by `ENGINE_COMMAND`
35
+ * in `src/client/hidden.ts` — the two bundles share no module.
36
+ */
37
+ export declare const ENGINE_COMMAND = "rewind-plugin";
32
38
  /** Plugin config. */
33
39
  export interface RewindConfig {
34
40
  /** Checkpoint store root (exact path; beats `DSH_REWIND_SNAPSHOT_DIR` and the harness-home default). */
@@ -45,12 +45,6 @@ export type HostLocaleId = 'zh' | 'en';
45
45
  export declare function preferredLocale(preference: unknown): HostLocaleId;
46
46
  /** English dictionary — the key-set source of truth (neutral default). */
47
47
  export declare const en: {
48
- 'usage.title': string;
49
- 'usage.noArgs': string;
50
- 'usage.seq': string;
51
- 'usage.blocked': string;
52
- 'describeTarget.seq': string;
53
- 'describeTarget.index': string;
54
48
  'plan.rewinding': string;
55
49
  'plan.affects': string;
56
50
  'plan.restore': string;
@@ -75,9 +69,11 @@ export declare const en: {
75
69
  storeUnsupported: string;
76
70
  success: string;
77
71
  noUserMessages: string;
78
- chooseMode: string;
79
72
  'command.description': string;
80
73
  'command.undoDescription': string;
74
+ 'engine.description': string;
75
+ 'engine.rejected': string;
76
+ 'engine.inputHint': string;
81
77
  'cleanup.description': string;
82
78
  'cleanup.inputHint': string;
83
79
  'cleanup.status': string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-rewind-plugin",
3
- "version": "0.15.1",
3
+ "version": "0.15.2",
4
4
  "description": "In-window conversation rewind with workspace file restore · 同窗口内对话回退并可还原工作区文件",
5
5
  "icon": "./assets/icon.svg",
6
6
  "keywords": [