dsh-rewind-plugin 0.17.1 → 0.18.0

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/lib/client.js CHANGED
@@ -94,15 +94,6 @@ function isExecutedRewindCommand(node, seq) {
94
94
  const args = node.args ?? "";
95
95
  return new RegExp(`(?:^|\\s)@${seq}(?:\\s|$)`).test(args);
96
96
  }
97
- function hasFileImpact(text) {
98
- if (text === void 0) return true;
99
- const match = text.match(/impact=(\d+)/);
100
- if (match !== null) return Number(match[1]) > 0;
101
- return false;
102
- }
103
- function hasBackupDisabled(text) {
104
- return text !== void 0 && /(?:^|\n)backup=off(?:\n|$)/.test(text);
105
- }
106
97
  function isPreviewCommand(command) {
107
98
  return (command.args ?? "").includes("preview");
108
99
  }
@@ -238,23 +229,6 @@ function rewindCandidatesOf(snap, hidden, limit = DEFAULT_CANDIDATE_LIMIT) {
238
229
  function rewindCandidatesOfChat(snap) {
239
230
  return rewindCandidatesOf(snap, hiddenSeqsOf(snap));
240
231
  }
241
- var CANDIDATE_LIST_HEADER = "candidates=";
242
- function rewindCandidatesFromHostText(text) {
243
- if (!text.startsWith(CANDIDATE_LIST_HEADER)) return [];
244
- const lines = text.split("\n").slice(1);
245
- const candidates = [];
246
- for (const line of lines) {
247
- if (line === "") continue;
248
- const parts = line.split(" ");
249
- if (parts.length !== 3) continue;
250
- const seq = Number(parts[0]);
251
- const time = Number(parts[1]);
252
- const preview = parts[2] ?? "";
253
- if (!Number.isSafeInteger(seq) || !Number.isFinite(time)) continue;
254
- candidates.push({ seq, time, preview });
255
- }
256
- return candidates;
257
- }
258
232
  function rewindOptionsFromCandidates(candidates, t) {
259
233
  return candidates.map((candidate) => ({
260
234
  id: String(candidate.seq),
@@ -263,48 +237,6 @@ function rewindOptionsFromCandidates(candidates, t) {
263
237
  }));
264
238
  }
265
239
 
266
- // src/client/log.ts
267
- var NS = "dsh-rewind";
268
- var DEBUG_KEY = "dsh-rewind.debug";
269
- var ALWAYS_ON = /* @__PURE__ */ new Set(["error", "warn"]);
270
- function switchValue() {
271
- try {
272
- return window.localStorage.getItem(DEBUG_KEY) ?? "";
273
- } catch {
274
- return "";
275
- }
276
- }
277
- function matches(value, ns) {
278
- for (const entry of value.split(",")) {
279
- const part = entry.trim();
280
- if (part === "") continue;
281
- if (part === "*" || part === `${NS}*`) return true;
282
- if (part.endsWith("*")) {
283
- if (ns.startsWith(part.slice(0, -1))) return true;
284
- } else if (ns === part) {
285
- return true;
286
- }
287
- }
288
- return false;
289
- }
290
- function tag(scope) {
291
- return `[${NS}:${scope}]`;
292
- }
293
- function log(level, scope, message, data) {
294
- if (ALWAYS_ON.has(level)) {
295
- console[level](tag(scope), message, data);
296
- return;
297
- }
298
- if (!matches(switchValue(), `${NS}:${scope}`)) return;
299
- console.info(tag(scope), message, data);
300
- }
301
- var rewindLog = {
302
- error: (scope, message, data) => log("error", scope, message, data),
303
- warn: (scope, message, data) => log("warn", scope, message, data),
304
- info: (scope, message, data) => log("info", scope, message, data),
305
- debug: (scope, message, data) => log("debug", scope, message, data)
306
- };
307
-
308
240
  // src/client/styles.ts
309
241
  var CLASS = {
310
242
  button: "dsh-rewind-btn",
@@ -356,10 +288,13 @@ var STYLE = `
356
288
  z-index: 1000;
357
289
  width: 288px;
358
290
  padding: 12px;
359
- border: 1px solid var(--dsw-alias-border-l2);
360
- border-radius: 12px;
291
+ border: 0;
292
+ border-radius: var(--dsw-radius-md);
361
293
  background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));
362
- box-shadow: var(--dsw-shadow-lv3);
294
+ backdrop-filter: var(--dsw-menu-backdrop-filter);
295
+ isolation: isolate;
296
+ --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
297
+ box-shadow: var(--dsw-elevation-prominent);
363
298
  font-size: 14px;
364
299
  line-height: 20px;
365
300
  color: var(--dsw-alias-label-primary);
@@ -471,10 +406,13 @@ var STYLE = `
471
406
  z-index: 1000;
472
407
  max-width: min(440px, calc(100vw - 24px));
473
408
  padding: 8px 12px;
474
- border: 1px solid var(--dsw-alias-border-l2);
475
- border-radius: 10px;
409
+ border: 0;
410
+ border-radius: var(--dsw-radius-md);
476
411
  background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));
477
- box-shadow: var(--dsw-shadow-lv3);
412
+ backdrop-filter: var(--dsw-menu-backdrop-filter);
413
+ isolation: isolate;
414
+ --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
415
+ box-shadow: var(--dsw-elevation-panel);
478
416
  font-size: 13px;
479
417
  line-height: 18px;
480
418
  color: var(--dsw-alias-label-primary);
@@ -545,15 +483,6 @@ function formatTarget(t, seq, time, preview) {
545
483
  const previewText = preview.length > 0 ? preview : t("popover.noText");
546
484
  return `seq ${seq} \xB7 ${hh}:${mm} \xB7 ${previewText}`;
547
485
  }
548
- function parseImpactList(text) {
549
- const restores = [];
550
- const deletes = [];
551
- for (const line of text.split("\n")) {
552
- if (line.startsWith("restore:")) restores.push(line.slice("restore:".length));
553
- else if (line.startsWith("delete:")) deletes.push(line.slice("delete:".length));
554
- }
555
- return { restores, deletes };
556
- }
557
486
  function findCommand(chat, match) {
558
487
  if (chat === void 0) return void 0;
559
488
  let found;
@@ -600,28 +529,12 @@ function waitForCommand(session, chatOf, match, timeoutMs = 8e3, watch) {
600
529
  check();
601
530
  });
602
531
  }
603
- function isPreviewFor(node, seq) {
604
- const args = node.args ?? "";
605
- return node.name !== null && ENGINE_COMMANDS.has(node.name) && args.includes("preview") && new RegExp(`(?:^|\\s)@${seq}(?=\\s|$)`).test(args);
532
+ var previewCall = null;
533
+ function registerPreviewCall(call) {
534
+ previewCall = call;
606
535
  }
607
- async function previewImpact(session, chatOf, seq, watch) {
608
- const known = knownCommandSeqs(session, chatOf, (node) => isPreviewFor(node, seq));
609
- let result;
610
- try {
611
- result = await session.command(`/${ENGINE_COMMAND} preview @${seq} both`);
612
- } catch (error) {
613
- rewindLog.warn("preview", `preview command threw for @${seq}`, error);
614
- return { kind: "error", text: error instanceof Error ? error.message : String(error) };
615
- }
616
- if (!result.ok) {
617
- rewindLog.warn("preview", `preview command rejected for @${seq}`, result.error);
618
- return { kind: "error", text: `${result.error.code}: ${result.error.message}` };
619
- }
620
- if (result.value?.matched !== true) {
621
- rewindLog.warn("preview", `preview command was not matched for @${seq}`);
622
- return { kind: "error", text: "the rewind command is not registered on this host" };
623
- }
624
- return waitForCommand(session, chatOf, (node) => isPreviewFor(node, seq) && !known.has(node.seq), 8e3, watch);
536
+ function previewImpact(sessionId, seq) {
537
+ return previewCall?.(sessionId, seq) ?? Promise.resolve({ kind: "error", text: "host RPC is unavailable" });
625
538
  }
626
539
  function el(tag2, className, text) {
627
540
  const node = document.createElement(tag2);
@@ -672,33 +585,30 @@ function renderImpactStep(root, opts, back, cached) {
672
585
  root.replaceChildren(impact, actions);
673
586
  focusFirst(root);
674
587
  void (async () => {
675
- const outcome = cached ?? await previewImpact(session, opts.chatOf, seq, (cb) => opts.watchChat(session.sessionId, cb));
588
+ const outcome = cached ?? await previewImpact(session.sessionId, seq);
676
589
  if (outcome === null) {
677
- impact.textContent = t("popover.impact.failed", { message: "preview command failed or timed out" });
590
+ impact.textContent = t("popover.impact.failed", { message: "preview request timed out" });
678
591
  return;
679
592
  }
680
593
  if (outcome.kind === "error") {
681
- impact.textContent = t("popover.impact.failed", { message: outcome.text ?? "unknown error" });
594
+ impact.textContent = t("popover.impact.failed", { message: outcome.text });
682
595
  return;
683
596
  }
684
- if (outcome.text === void 0) {
597
+ const restores = outcome.files.filter((file) => file.action === "restore").map((file) => file.path);
598
+ const deletes = outcome.files.filter((file) => file.action === "delete").map((file) => file.path);
599
+ if (restores.length === 0 && deletes.length === 0) {
685
600
  impact.textContent = t("popover.impact.none");
686
601
  } else {
687
- const { restores, deletes } = parseImpactList(outcome.text);
688
- if (restores.length === 0 && deletes.length === 0) {
689
- impact.textContent = t("popover.impact.none");
602
+ const lines = [
603
+ ...restores.map((path) => t("popover.impact.restore", { path })),
604
+ ...deletes.map((path) => t("popover.impact.delete", { path }))
605
+ ];
606
+ if (outcome.backupOff) {
607
+ const list = document.createElement("div");
608
+ list.textContent = lines.join("\n");
609
+ impact.replaceChildren(list, el("div", CLASS.popoverNote, t("popover.backupDisabled")));
690
610
  } else {
691
- const lines = [
692
- ...restores.map((path) => t("popover.impact.restore", { path })),
693
- ...deletes.map((path) => t("popover.impact.delete", { path }))
694
- ];
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
- }
611
+ impact.textContent = lines.join("\n");
702
612
  }
703
613
  }
704
614
  confirm.disabled = false;
@@ -798,10 +708,10 @@ function openPopover(opts) {
798
708
  openRetractPopover(opts);
799
709
  return;
800
710
  }
801
- const { session, seq, time, preview, anchor, t, chatOf } = opts;
711
+ const { session, seq, time, preview, anchor, t } = opts;
802
712
  const onRewind = opts.onRewind;
803
713
  if (seq === void 0 || time === void 0 || onRewind === void 0) return;
804
- const durableOpts = { session, seq, time, preview, anchor, t, chatOf, watchChat: opts.watchChat, onRewind };
714
+ const durableOpts = { session, seq, time, preview, anchor, t, onRewind };
805
715
  const root = el("div", CLASS.popover);
806
716
  root.setAttribute("role", "dialog");
807
717
  root.setAttribute("aria-label", t("popover.title"));
@@ -871,12 +781,12 @@ function openPopover(opts) {
871
781
  popoverEl = root;
872
782
  disposeOutside = shell.dispose;
873
783
  void (async () => {
874
- const outcome = await previewImpact(session, chatOf, seq, (cb) => opts.watchChat(session.sessionId, cb));
784
+ const outcome = await previewImpact(session.sessionId, seq);
875
785
  impactOutcome = outcome;
876
786
  if (outcome !== null && outcome.kind === "success") {
877
- bothState = { state: hasFileImpact(outcome.text) ? "hasChanges" : "noChanges" };
787
+ bothState = { state: outcome.files.length > 0 ? "hasChanges" : "noChanges" };
878
788
  } else {
879
- bothState = { state: "error", message: outcome?.text ?? "preview command timed out" };
789
+ bothState = { state: "error", message: outcome?.kind === "error" ? outcome.text : "preview request timed out" };
880
790
  }
881
791
  renderModes();
882
792
  shell.position();
@@ -921,11 +831,11 @@ function matchPendingRows(rows, steering) {
921
831
  for (let i = 0; i < rows.length; i++) {
922
832
  const row = rows[i];
923
833
  const item = steering[i];
924
- matched.push(item !== void 0 && matches2(item, row) ? item.id : null);
834
+ matched.push(item !== void 0 && matches(item, row) ? item.id : null);
925
835
  }
926
836
  return matched;
927
837
  }
928
- function matches2(item, row) {
838
+ function matches(item, row) {
929
839
  if (item.attachments > 0) return row.attachments === item.attachments;
930
840
  return row.text === (item.text ?? "");
931
841
  }
@@ -935,6 +845,48 @@ function retractSpan(steering, targetId) {
935
845
  return steering.slice(index).map((item) => item.id);
936
846
  }
937
847
 
848
+ // src/client/log.ts
849
+ var NS = "dsh-rewind";
850
+ var DEBUG_KEY = "dsh-rewind.debug";
851
+ var ALWAYS_ON = /* @__PURE__ */ new Set(["error", "warn"]);
852
+ function switchValue() {
853
+ try {
854
+ return window.localStorage.getItem(DEBUG_KEY) ?? "";
855
+ } catch {
856
+ return "";
857
+ }
858
+ }
859
+ function matches2(value, ns) {
860
+ for (const entry of value.split(",")) {
861
+ const part = entry.trim();
862
+ if (part === "") continue;
863
+ if (part === "*" || part === `${NS}*`) return true;
864
+ if (part.endsWith("*")) {
865
+ if (ns.startsWith(part.slice(0, -1))) return true;
866
+ } else if (ns === part) {
867
+ return true;
868
+ }
869
+ }
870
+ return false;
871
+ }
872
+ function tag(scope) {
873
+ return `[${NS}:${scope}]`;
874
+ }
875
+ function log(level, scope, message, data) {
876
+ if (ALWAYS_ON.has(level)) {
877
+ console[level](tag(scope), message, data);
878
+ return;
879
+ }
880
+ if (!matches2(switchValue(), `${NS}:${scope}`)) return;
881
+ console.info(tag(scope), message, data);
882
+ }
883
+ var rewindLog = {
884
+ error: (scope, message, data) => log("error", scope, message, data),
885
+ warn: (scope, message, data) => log("warn", scope, message, data),
886
+ info: (scope, message, data) => log("info", scope, message, data),
887
+ debug: (scope, message, data) => log("debug", scope, message, data)
888
+ };
889
+
938
890
  // src/client/portals.tsx
939
891
  var import_jsx_runtime = require("react/jsx-runtime");
940
892
  function fillComposerEditable(text) {
@@ -1294,8 +1246,6 @@ function RewindPortals({ sessionId, sessionOf, chatOf, isMainViewSession, watchC
1294
1246
  target,
1295
1247
  sessionId,
1296
1248
  sessionOf,
1297
- chatOf,
1298
- watchChat,
1299
1249
  setComposerText,
1300
1250
  t
1301
1251
  },
@@ -1331,8 +1281,6 @@ function RewindButton({ target, sessionId, sessionOf, chatOf, watchChat, isMainV
1331
1281
  if (node === void 0) return;
1332
1282
  openPopover({
1333
1283
  session,
1334
- chatOf,
1335
- watchChat,
1336
1284
  seq: node.seq,
1337
1285
  time: node.time,
1338
1286
  preview: messagePreviewOf(node),
@@ -1379,15 +1327,13 @@ async function retractPending(session, itemId, text, setComposerText) {
1379
1327
  setComposerText(session.sessionId, text);
1380
1328
  }
1381
1329
  }
1382
- function RetractButton({ target, sessionId, sessionOf, chatOf, watchChat, setComposerText, t }) {
1330
+ function RetractButton({ target, sessionId, sessionOf, setComposerText, t }) {
1383
1331
  const onClick = (event) => {
1384
1332
  event.stopPropagation();
1385
1333
  const session = sessionOf(sessionId);
1386
1334
  if (session === void 0) return;
1387
1335
  openPopover({
1388
1336
  session,
1389
- chatOf,
1390
- watchChat,
1391
1337
  preview: target.preview,
1392
1338
  anchor: event.currentTarget,
1393
1339
  t,
@@ -1415,10 +1361,69 @@ function createRewindBridge(deps) {
1415
1361
  };
1416
1362
  }
1417
1363
 
1364
+ // src/client/host-rpc.ts
1365
+ var CHANNEL = "/dsh-rewind";
1366
+ var TIMEOUT_MS = 8e3;
1367
+ function isRecord(value) {
1368
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1369
+ }
1370
+ function isImpact(value) {
1371
+ return isRecord(value) && typeof value["path"] === "string" && (value["action"] === "restore" || value["action"] === "delete");
1372
+ }
1373
+ function isCandidate(value) {
1374
+ return isRecord(value) && Number.isSafeInteger(value["seq"]) && typeof value["time"] === "number" && Number.isFinite(value["time"]) && typeof value["preview"] === "string";
1375
+ }
1376
+ function isAbort(error) {
1377
+ const name2 = error?.name;
1378
+ return name2 === "AbortError" || name2 === "TimeoutError";
1379
+ }
1380
+ async function previewOverRpc(connection, sessionId, seq) {
1381
+ if (connection === void 0) return { kind: "error", text: "host RPC is unavailable" };
1382
+ try {
1383
+ const result = await connection.rpc.call(CHANNEL, "preview", { sessionId, seq }, AbortSignal.timeout(TIMEOUT_MS));
1384
+ if (!result.ok) {
1385
+ rewindLog.warn("preview", `RPC preview failed for @${seq}`, result.error);
1386
+ return { kind: "error", text: result.error.message };
1387
+ }
1388
+ const value = result.value;
1389
+ if (!isRecord(value) || typeof value["backupOff"] !== "boolean" || !Array.isArray(value["files"]) || !value["files"].every(isImpact)) {
1390
+ rewindLog.warn("preview", `RPC preview returned an invalid result for @${seq}`);
1391
+ return { kind: "error", text: "invalid preview response" };
1392
+ }
1393
+ return { kind: "success", files: value["files"], backupOff: value["backupOff"] };
1394
+ } catch (error) {
1395
+ if (isAbort(error)) {
1396
+ rewindLog.warn("preview", `RPC preview timed out for @${seq}`);
1397
+ return null;
1398
+ }
1399
+ rewindLog.warn("preview", `RPC preview threw for @${seq}`, error);
1400
+ return { kind: "error", text: error instanceof Error ? error.message : String(error) };
1401
+ }
1402
+ }
1403
+ async function candidatesOverRpc(connection, sessionId) {
1404
+ if (connection === void 0) return void 0;
1405
+ try {
1406
+ const result = await connection.rpc.call(CHANNEL, "candidates", { sessionId }, AbortSignal.timeout(TIMEOUT_MS));
1407
+ if (!result.ok) {
1408
+ rewindLog.warn("preview", "RPC candidates failed", result.error);
1409
+ return void 0;
1410
+ }
1411
+ const value = result.value;
1412
+ if (!isRecord(value) || !Array.isArray(value["candidates"]) || !value["candidates"].every(isCandidate)) {
1413
+ rewindLog.warn("preview", "RPC candidates returned an invalid result");
1414
+ return void 0;
1415
+ }
1416
+ return value["candidates"];
1417
+ } catch (error) {
1418
+ rewindLog.warn("preview", "RPC candidates threw", error);
1419
+ return void 0;
1420
+ }
1421
+ }
1422
+
1418
1423
  // src/client/build-info.ts
1419
- var PLUGIN_VERSION = true ? "0.17.1" : "dev";
1424
+ var PLUGIN_VERSION = true ? "0.18.0" : "dev";
1420
1425
  var PLUGIN_PACKAGE = "dsh-rewind-plugin";
1421
- var BUILD_HASH = true ? "0d33ec37" : "dev";
1426
+ var BUILD_HASH = true ? "18b65dfa" : "dev";
1422
1427
 
1423
1428
  // src/client/locales.ts
1424
1429
  var zh = {
@@ -1630,7 +1635,7 @@ function SettingsCard(props) {
1630
1635
 
1631
1636
  // src/client/index.ts
1632
1637
  var name = "dsh-rewind";
1633
- var inject = ["slots", "sessions", "locale", "commandUi", "configForms"];
1638
+ var inject = ["slots", "sessions", "locale", "commandUi", "configForms", "connection"];
1634
1639
  var NS2 = "rewind";
1635
1640
  var HEADER_ACTIONS_SLOT = "conversation.session.header.actions";
1636
1641
  var COMPOSER_EDITABLE_SELECTOR2 = "[data-composer-input]";
@@ -1639,6 +1644,11 @@ function apply(ctx) {
1639
1644
  ctx.effect(function* () {
1640
1645
  yield ctx.locale.register(NS2, { zh, en });
1641
1646
  const t = ctx.locale.bind(NS2);
1647
+ const connection = ctx.get("connection");
1648
+ registerPreviewCall((sessionId, seq) => previewOverRpc(connection, sessionId, seq));
1649
+ yield () => {
1650
+ registerPreviewCall(null);
1651
+ };
1642
1652
  const style = document.createElement("style");
1643
1653
  style.dataset.plugin = PLUGIN_PACKAGE;
1644
1654
  style.textContent = STYLE;
@@ -1777,14 +1787,7 @@ function apply(ctx) {
1777
1787
  const chat = chatOf(face);
1778
1788
  return chat !== void 0 && rewindCandidatesOfChat(chat).length > 0;
1779
1789
  };
1780
- const fetchHostCandidates = async (face, chatOf2) => {
1781
- const known = knownCommandSeqs(face, chatOf2, (node) => isCandidateCommand(node));
1782
- const result = await face.command(`/${ENGINE_COMMAND} __candidates`);
1783
- if (!result.ok || result.value?.matched !== true) return void 0;
1784
- const outcome = await waitForCommand(face, chatOf2, (node) => isCandidateCommand(node) && !known.has(node.seq), 8e3, (cb) => watchChat(face.sessionId, cb));
1785
- if (outcome === null || outcome.kind !== "success" || outcome.text === void 0) return void 0;
1786
- return rewindCandidatesFromHostText(outcome.text);
1787
- };
1790
+ const fetchHostCandidates = async (face) => candidatesOverRpc(connection, face.sessionId);
1788
1791
  const hostCandidatesCache = /* @__PURE__ */ new Map();
1789
1792
  const composerAnchor = () => {
1790
1793
  const surface = composerSurface2();
@@ -1802,7 +1805,7 @@ function apply(ctx) {
1802
1805
  options: async (session) => {
1803
1806
  const face = sessionOf(session.sessionId);
1804
1807
  if (face === void 0) return [];
1805
- const candidates = await fetchHostCandidates(face, chatOf);
1808
+ const candidates = await fetchHostCandidates(face);
1806
1809
  if (candidates !== void 0) hostCandidatesCache.set(session.sessionId, candidates);
1807
1810
  return candidates === void 0 ? [] : rewindOptionsFromCandidates(candidates, t);
1808
1811
  },
@@ -1815,8 +1818,6 @@ function apply(ctx) {
1815
1818
  if (candidate === void 0) return;
1816
1819
  openPopover({
1817
1820
  session: face,
1818
- chatOf,
1819
- watchChat,
1820
1821
  seq: candidate.seq,
1821
1822
  time: candidate.time,
1822
1823
  preview: candidate.preview,
package/lib/index.js CHANGED
@@ -9,15 +9,8 @@ function preferredLocale(preference) {
9
9
  return preference.toLowerCase().split("-")[0] === "zh" ? "zh" : "en";
10
10
  }
11
11
  var en = {
12
- "plan.rewinding": "Rewind to seq {targetSeq}, removing {count} node(s) from the model context (conversation log kept).",
13
- "plan.affects": "Affects {count} file(s):",
14
- "plan.restore": "restore {path}",
15
- "plan.delete": "delete {path}",
16
- "plan.noChanges": "No restorable changes after the target.",
17
- "plan.backupOff": "Snapshot backups are off; this restore may be incomplete.",
18
12
  "plan.skip": " not restored: {path} ({reason})",
19
13
  "skipReason.oversized": "over the {limit} backup limit, so the recorded state predates untracked edits",
20
- "uncovered.header": "{count} tracked file(s) exceed the {limit} backup limit and are not covered:",
21
14
  "error.invalidTarget": 'Cannot parse target "{raw}" (expected <index> or @<seq>)',
22
15
  "sep.list": ", ",
23
16
  "sep.clause": "; ",
@@ -62,15 +55,8 @@ var en = {
62
55
  "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"
63
56
  };
64
57
  var zh = {
65
- "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",
66
- "plan.affects": "\u5C06\u5F71\u54CD {count} \u4E2A\u6587\u4EF6\uFF1A",
67
- "plan.restore": "\u8FD8\u539F {path}",
68
- "plan.delete": "\u5220\u9664 {path}",
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",
71
58
  "plan.skip": " \u672A\u8FD8\u539F\uFF1A{path}\uFF08{reason}\uFF09",
72
59
  "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",
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",
74
60
  "error.invalidTarget": '\u65E0\u6CD5\u89E3\u6790\u76EE\u6807 "{raw}"\uFF08\u5E94\u4E3A <\u5E8F\u53F7> \u6216 @<seq>\uFF09',
75
61
  "sep.list": "\u3001",
76
62
  "sep.clause": "\uFF1B",
@@ -182,14 +168,6 @@ function listRewindCandidates(events, surface, limit = DEFAULT_CANDIDATE_LIMIT)
182
168
  }
183
169
  return candidates;
184
170
  }
185
- var CANDIDATE_LIST_HEADER = "candidates=";
186
- function formatCandidateList(candidates) {
187
- const lines = [`${CANDIDATE_LIST_HEADER}${candidates.length}`];
188
- for (const candidate of candidates) {
189
- lines.push(`${candidate.seq} ${candidate.time} ${candidate.preview}`);
190
- }
191
- return lines.join("\n");
192
- }
193
171
  function planRewind(events, surface, target) {
194
172
  let targetSeq;
195
173
  if (target.kind === "seq") {
@@ -2319,6 +2297,7 @@ function shouldRunAutoSweep(lastAtMs, nowMs, intervalMs = AUTO_SWEEP_INTERVAL_MS
2319
2297
  var name = "dsh-rewind";
2320
2298
  var inject = ["commands", "tools", "settings"];
2321
2299
  var ENGINE_COMMAND = "rewind-plugin";
2300
+ var REWIND_RPC_CHANNEL = "/dsh-rewind";
2322
2301
  var DEFAULT_FILE_CHECKPOINTING_ENABLED = true;
2323
2302
  var Config = z.object({
2324
2303
  snapshotDir: z.string().required(false),
@@ -2357,7 +2336,7 @@ function t(key, params) {
2357
2336
  function warnCommandUnavailable(ctx, name2, error) {
2358
2337
  ctx.logger.warn(`[dsh-rewind] command "${name2}" was not registered: ${error instanceof Error ? error.message : String(error)}`);
2359
2338
  if (name2 === ENGINE_COMMAND) {
2360
- ctx.logger.warn(`[dsh-rewind] the internal "${ENGINE_COMMAND}" engine channel is unavailable; the rewind button and picker will not work.`);
2339
+ ctx.logger.warn(`[dsh-rewind] the internal "${ENGINE_COMMAND}" engine channel is unavailable; rewind execution will not work.`);
2361
2340
  }
2362
2341
  }
2363
2342
  async function discardCapture(capture) {
@@ -2489,31 +2468,6 @@ function formatBytes(bytes) {
2489
2468
  if (bytes >= 1024) return `${Math.round(bytes / 1024)} KiB`;
2490
2469
  return `${bytes} B`;
2491
2470
  }
2492
- function formatPlan(plan, files, uncovered = [], maxFileBytes = 0, checkpointingEnabled = true) {
2493
- const lines = [
2494
- t("plan.rewinding", { targetSeq: plan.targetSeq, count: plan.shadowedSeqs.length })
2495
- ];
2496
- if (files.length > 0) {
2497
- lines.push(t("plan.affects", { count: files.length }));
2498
- for (const file of files) {
2499
- lines.push(` ${file.action === "restore" ? t("plan.restore", { path: file.path }) : t("plan.delete", { path: file.path })}`);
2500
- }
2501
- } else {
2502
- lines.push(t("plan.noChanges"));
2503
- }
2504
- if (uncovered.length > 0) {
2505
- lines.push(t("uncovered.header", { count: uncovered.length, limit: formatBytes(maxFileBytes) }));
2506
- for (const file of uncovered.slice(0, 5)) lines.push(` ${file.path}`);
2507
- if (uncovered.length > 5) lines.push(" \u2026");
2508
- }
2509
- if (!checkpointingEnabled) lines.push(t("plan.backupOff"));
2510
- lines.push(`impact=${files.length}`);
2511
- for (const file of files) {
2512
- lines.push(`${file.action}:${file.path}`);
2513
- }
2514
- if (!checkpointingEnabled) lines.push("backup=off");
2515
- return lines.join("\n");
2516
- }
2517
2471
  function resolveOrError(events, surface, raw) {
2518
2472
  const target = parseRewindTarget(raw);
2519
2473
  if (target === void 0) {
@@ -2521,6 +2475,54 @@ function resolveOrError(events, surface, raw) {
2521
2475
  }
2522
2476
  return planRewind(events, surface, target);
2523
2477
  }
2478
+ async function previewImpact(store, session, seq, checkpointingEnabled) {
2479
+ const plan = resolveOrError(session.snapshotEvents(), session.surface.nodes, `@${seq}`);
2480
+ let impacts;
2481
+ try {
2482
+ impacts = await store.impactsAfter(session.id, plan.targetSeq);
2483
+ } catch (error) {
2484
+ if (!(error instanceof UnknownStoreVersionError)) throw error;
2485
+ throw new Error(t("storeUnsupported", { version: await store.readStoreVersion(session.id) ?? 0 }));
2486
+ }
2487
+ const uncovered = await store.uncoveredPaths(session.id).catch(() => []);
2488
+ return { files: impacts, uncovered, backupOff: !checkpointingEnabled };
2489
+ }
2490
+ function isRecord(value) {
2491
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2492
+ }
2493
+ function rpcFailure(code, message) {
2494
+ return { ok: false, error: { code, message, details: {} } };
2495
+ }
2496
+ function createRewindRpcHandler(store, agentsOf, checkpointingEnabled) {
2497
+ return async (endpoint, payload) => {
2498
+ try {
2499
+ if (!isRecord(payload)) return rpcFailure("rewind/bad-request", "invalid payload");
2500
+ const sessionId = payload["sessionId"];
2501
+ if (typeof sessionId !== "string" || sessionId.length === 0) {
2502
+ return rpcFailure("rewind/bad-request", "sessionId must be a non-empty string");
2503
+ }
2504
+ const agents = agentsOf();
2505
+ if (agents === void 0) return rpcFailure("rewind/agents-unavailable", "agent registry is unavailable");
2506
+ const agent = agents.get(sessionId);
2507
+ if (agent === void 0) return rpcFailure("rewind/session-not-found", `session ${sessionId} is not attached`);
2508
+ const session = agent.session;
2509
+ if (endpoint === "preview") {
2510
+ const seq = payload["seq"];
2511
+ if (!Number.isSafeInteger(seq)) return rpcFailure("rewind/bad-request", "seq must be an integer");
2512
+ return { ok: true, value: await previewImpact(store, session, seq, checkpointingEnabled()) };
2513
+ }
2514
+ if (endpoint === "candidates") {
2515
+ return {
2516
+ ok: true,
2517
+ value: { candidates: listRewindCandidates(session.snapshotEvents(), session.surface.nodes) }
2518
+ };
2519
+ }
2520
+ return rpcFailure("rewind/unknown-endpoint", `unknown endpoint: ${endpoint}`);
2521
+ } catch (error) {
2522
+ return rpcFailure("rewind/failed", error instanceof Error ? error.message : String(error));
2523
+ }
2524
+ };
2525
+ }
2524
2526
  function renderFailures(failed) {
2525
2527
  if (failed.length === 0) return "";
2526
2528
  return t("failures.suffix", {
@@ -2678,29 +2680,6 @@ async function handleRewind(ctx, store, fs, invocation, inflight, checkpointingE
2678
2680
  return executeRewind(ctx, store, fs, invocation, `@${candidates[0].seq}`, "chat", inflight, checkpointingEnabled);
2679
2681
  }
2680
2682
  const parts = input.split(/\s+/);
2681
- if (parts[0] === "preview") {
2682
- const target2 = parts[1];
2683
- if (target2 === void 0) return { kind: "error", text: t("engine.rejected") };
2684
- let plan;
2685
- try {
2686
- plan = resolveOrError(session.snapshotEvents(), session.surface.nodes, target2);
2687
- } catch (error) {
2688
- return rewindErrorResult(error);
2689
- }
2690
- const impacts = await store.impactsAfter(session.id, plan.targetSeq).catch((error) => {
2691
- if (error instanceof UnknownStoreVersionError) return void 0;
2692
- throw error;
2693
- });
2694
- if (impacts === void 0) {
2695
- return { kind: "error", text: t("storeUnsupported", { version: await store.readStoreVersion(session.id) ?? 0 }) };
2696
- }
2697
- const uncovered = await store.uncoveredPaths(session.id).catch(() => []);
2698
- return { kind: "success", text: formatPlan(plan, impacts, uncovered, store.fileCapBytes, checkpointingEnabled) };
2699
- }
2700
- if (parts[0] === "__candidates") {
2701
- const candidates = listRewindCandidates(session.snapshotEvents(), session.surface.nodes);
2702
- return { kind: "success", text: formatCandidateList(candidates) };
2703
- }
2704
2683
  const target = parts[0];
2705
2684
  const mode = parts[1];
2706
2685
  if (mode !== void 0 && mode !== "chat" && mode !== "both") {
@@ -2978,6 +2957,23 @@ function apply(ctx, config) {
2978
2957
  }
2979
2958
  })();
2980
2959
  }, { global: true });
2960
+ ctx.inject(["connection"], (scope) => {
2961
+ try {
2962
+ const connection = scope.root.connection;
2963
+ if (connection === void 0) return;
2964
+ const dispose = connection.rpc.handle(
2965
+ REWIND_RPC_CHANNEL,
2966
+ createRewindRpcHandler(
2967
+ store,
2968
+ () => scope.get("agents"),
2969
+ () => isFileCheckpointingEnabled(config)
2970
+ )
2971
+ );
2972
+ scope.effect(() => dispose, "dsh-rewind RPC channel");
2973
+ } catch (error) {
2974
+ ctx.logger.warn(`[dsh-rewind] RPC channel registration failed: ${error instanceof Error ? error.message : String(error)}`);
2975
+ }
2976
+ });
2981
2977
  ctx.inject(["fs", "workingDirectory"], (scope) => {
2982
2978
  const fs = scope.fs;
2983
2979
  const workingDirectory = scope.workingDirectory;
@@ -75,15 +75,8 @@ export declare function rewindCandidatesOf(snap: CandidateChat, hidden: Readonly
75
75
  /** The candidates of a live chat snapshot, withdrawn rows already excluded. */
76
76
  export declare function rewindCandidatesOfChat(snap: CandidateChat): RewindCandidate[];
77
77
  /**
78
- * Parse the host's candidate-list encoding (see `formatCandidateList` in
79
- * src/rewind.ts) into typed candidates. Malformed lines are skipped; a
80
- * missing/zero header yields an empty list.
81
- */
82
- export declare function rewindCandidatesFromHostText(text: string): RewindCandidate[];
83
- /**
84
- * Map typed candidates to popupSelect rows (the host-derived path). The
85
- * popupSelect sources its options from the FULL host surface via the
86
- * `__candidates` channel instead of the windowed chat snapshot.
78
+ * Map typed candidates to popupSelect rows. The list comes from the host over
79
+ * RPC, so it covers the FULL host surface rather than the windowed chat snapshot.
87
80
  */
88
81
  export declare function rewindOptionsFromCandidates(candidates: readonly RewindCandidate[], t: Translate): SelectOption[];
89
82
  export {};
@@ -119,7 +119,7 @@ export interface MessageImageRef {
119
119
  export declare function messageAttachmentsAt(chat: HiddenChat | undefined, seq: number): readonly MessageImageRef[] | undefined;
120
120
  /**
121
121
  * Extract the rewind target seq from a `/rewind` command's structured `args`
122
- * (e.g. `@5 chat`, `preview @5 both`). Locale-independent — never parses the
122
+ * (e.g. `@5 chat`). Locale-independent — never parses the
123
123
  * host's human outcome copy.
124
124
  */
125
125
  export declare function targetSeqOfArgs(args: string | null | undefined): number | undefined;
@@ -132,28 +132,14 @@ export declare function targetSeqOfArgs(args: string | null | undefined): number
132
132
  */
133
133
  export declare function isExecutedRewindCommand(node: CommandNode, seq: number): boolean;
134
134
  /**
135
- * Whether a preview outcome reports tracked file changes — the availability
136
- * of the "rewind conversation and code" option (Claude Code hides the
137
- * code-restore options when the checkpoint has no tracked changes).
138
- *
139
- * Reads ONLY the machine-readable `impact=<n>` trailer the host appends to
140
- * preview text. Older host output without the trailer is treated as having no
141
- * changes (never guesses from human copy). Absent text (undefined) degrades to
142
- * always-show so a working option is never hidden on a failed probe.
143
- */
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
- /**
148
- * True when an engine command node is the internal candidate-list probe
149
- * (`__candidates`) the popupSelect runs to fetch the FULL candidate
150
- * list from the host. Like previews, its flow node never surfaces in the
151
- * transcript — it only feeds the popup — so it is hidden in every state.
135
+ * True when an engine command node is a legacy candidate-list probe
136
+ * (`__candidates`). New candidate lists travel over RPC; this only keeps
137
+ * probe rows in historical sessions hidden.
152
138
  */
153
139
  export declare function isCandidateCommand(command: CommandNode): boolean;
154
140
  /**
155
141
  * Anchor seqs that must be hidden from the rendered transcript so the user sees
156
- * the conversation as the agent sees it: every impact-preview flow node (it only
142
+ * the conversation as the agent sees it: every legacy probe flow node (it only
157
143
  * exists to feed the popover) and every SUCCESSFUL executed `/rewind` command
158
144
  * row, plus every message a rewind withdrew — the target, everything after it,
159
145
  * and the (unrendered) marker.
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The browser half of the rewind RPC channel: the mode popover's impact
3
+ * preview and the `/rewind` candidate list. Both are read-only host queries,
4
+ * so they travel over Connection RPC instead of the human command channel.
5
+ *
6
+ * @module dsh-rewind/client/host-rpc
7
+ */
8
+ import type { RewindCandidate } from './candidates.ts';
9
+ /** One file affected by a rewind. */
10
+ export interface PreviewFileImpact {
11
+ readonly path: string;
12
+ readonly action: 'restore' | 'delete';
13
+ }
14
+ /** One impact probe resolved, failed, or timed out (`null`). */
15
+ export type PreviewOutcome = {
16
+ readonly kind: 'success';
17
+ readonly files: readonly PreviewFileImpact[];
18
+ readonly backupOff: boolean;
19
+ } | {
20
+ readonly kind: 'error';
21
+ readonly text: string;
22
+ } | null;
23
+ /** Fetch one target's impact through the registered RPC call. */
24
+ export type PreviewCall = (sessionId: string, seq: number) => Promise<PreviewOutcome>;
25
+ type RpcResult = {
26
+ readonly ok: true;
27
+ readonly value: unknown;
28
+ } | {
29
+ readonly ok: false;
30
+ readonly error: {
31
+ readonly code: string;
32
+ readonly message: string;
33
+ };
34
+ };
35
+ /** The slice of the client `connection` service this module reads. */
36
+ export interface ConnectionLike {
37
+ readonly rpc: {
38
+ call(channel: string, endpoint: string, payload: unknown, signal?: AbortSignal): Promise<RpcResult>;
39
+ };
40
+ }
41
+ /** Resolve one target's file impact over RPC. */
42
+ export declare function previewOverRpc(connection: ConnectionLike | undefined, sessionId: string, seq: number): Promise<PreviewOutcome>;
43
+ /** Fetch the full rewind candidate list over RPC. */
44
+ export declare function candidatesOverRpc(connection: ConnectionLike | undefined, sessionId: string): Promise<readonly RewindCandidate[] | undefined>;
45
+ export {};
@@ -8,9 +8,9 @@
8
8
  * so the plugin imports no conversation UI types and survives harness drift.
9
9
  * The command decoration (`ctx.commandUi.decorate`) makes a bare `/rewind` (or
10
10
  * its alias `/undo`) open the harness's own popupSelect instead of executing;
11
- * the parameterized `@<seq>` / `preview` forms stay internal channels the
12
- * button and the popover drive. The exported contract is specified in
13
- * `docs/contract/client-contract.md`.
11
+ * the parameterized `@<seq>` form stays the execution channel, while the
12
+ * popover's impact and candidate reads go over the host RPC channel. The
13
+ * exported contract is specified in `docs/contract/client-contract.md`.
14
14
  *
15
15
  * @module dsh-rewind/client
16
16
  */
@@ -1,8 +1,8 @@
1
1
  /**
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
- * offers the two modes. Choosing "both" first fetches the impact list through
5
- * the engine channel's `preview @seq both` command and shows it before confirming.
4
+ * offers the two modes. Choosing "both" fetches the impact list over the host
5
+ * RPC channel 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
@@ -15,7 +15,8 @@
15
15
  */
16
16
  import type { SessionFace } from '@deepseek-ai/dsh-api-session-controller/client';
17
17
  import type { CommandNode } from '@deepseek-ai/dsh-client-ui-conversation/client';
18
- import { type ChatOf, type ChatWatch } from './hidden.ts';
18
+ import { type ChatOf } from './hidden.ts';
19
+ import type { PreviewCall } from './host-rpc.ts';
19
20
  import type { RewindKey } from './locales.ts';
20
21
  type Translate = (key: RewindKey, params?: Record<string, unknown>) => string;
21
22
  export interface PopoverOptions {
@@ -32,18 +33,6 @@ export interface PopoverOptions {
32
33
  /** Pending variant: executed after the retract confirm closes the popover. */
33
34
  readonly onRetract?: () => void;
34
35
  readonly preview: string;
35
- /**
36
- * Chat reader on the `uiConversation` "chat" view: the durable
37
- * variant's command probes scan the chat through it. Unused by the
38
- * pending-retract variant.
39
- */
40
- readonly chatOf: ChatOf;
41
- /**
42
- * Subscribe to one session's live chat-update signal, so a probe waiting on
43
- * a command's chat node can be woken when the chat snapshot changes. Passed
44
- * straight through to `waitForCommand`.
45
- */
46
- readonly watchChat: ChatWatch;
47
36
  /** The button that opened the popover (outside-click ignore target). */
48
37
  readonly anchor: HTMLElement;
49
38
  readonly t: Translate;
@@ -64,9 +53,8 @@ export declare function closePopover(): void;
64
53
  /**
65
54
  * Seqs of the command nodes currently matching `match`. Sample BEFORE issuing
66
55
  * a new command of the same shape so the subsequent wait can exclude them: a
67
- * repeated preview/rewind of the same target must not settle on the previous
68
- * command's stale outcome (e.g. an older preview that found file changes,
69
- * after those changes were already restored).
56
+ * repeated rewind of the same target must not settle on the previous command's
57
+ * stale outcome.
70
58
  */
71
59
  export declare function knownCommandSeqs(session: SessionFace, chatOf: ChatOf, match: (node: CommandNode) => boolean): Set<number>;
72
60
  /**
@@ -78,6 +66,8 @@ export declare function waitForCommand(session: SessionFace, chatOf: ChatOf, mat
78
66
  kind: 'success' | 'error';
79
67
  text?: string;
80
68
  } | null>;
69
+ /** Register the host RPC preview call (null clears it on unload). */
70
+ export declare function registerPreviewCall(call: PreviewCall | null): void;
81
71
  /** Open the mode-selection popover anchored near the given button. */
82
72
  export declare function openPopover(opts: PopoverOptions): void;
83
73
  export {};
@@ -32,4 +32,4 @@ export declare const CLASS: {
32
32
  */
33
33
  export declare const REWIND_ICON_SVG: string;
34
34
  /** One injected stylesheet (scoped under `.dsh-rewind-*`). */
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";
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: 0;\n border-radius: var(--dsw-radius-md);\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n backdrop-filter: var(--dsw-menu-backdrop-filter);\n isolation: isolate;\n --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);\n box-shadow: var(--dsw-elevation-prominent);\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: 0;\n border-radius: var(--dsw-radius-md);\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n backdrop-filter: var(--dsw-menu-backdrop-filter);\n isolation: isolate;\n --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);\n box-shadow: var(--dsw-elevation-panel);\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";
@@ -15,9 +15,8 @@
15
15
  * public read the settings UI and the client's own locale runtime perform.
16
16
  *
17
17
  * The client half (`src/client/locales.ts`) owns all interactive UI copy via
18
- * `ctx.locale` + `t()`; the host's human text is a machine channel the client
19
- * renders through machine tokens (`impact=<n>`, `args` @seq), never by parsing
20
- * host prose.
18
+ * `ctx.locale` + `t()`; the client never parses host prose (the popover reads
19
+ * travel over the RPC channel).
21
20
  *
22
21
  * DSH localizes only its OWN command descriptions via the client locale — a
23
22
  * closed `HOST_FACES` allowlist; third-party plugin descriptions are
@@ -45,15 +44,8 @@ export type HostLocaleId = 'zh' | 'en';
45
44
  export declare function preferredLocale(preference: unknown): HostLocaleId;
46
45
  /** English dictionary — the key-set source of truth (neutral default). */
47
46
  export declare const en: {
48
- 'plan.rewinding': string;
49
- 'plan.affects': string;
50
- 'plan.restore': string;
51
- 'plan.delete': string;
52
- 'plan.noChanges': string;
53
- 'plan.backupOff': string;
54
47
  'plan.skip': string;
55
48
  'skipReason.oversized': string;
56
- 'uncovered.header': string;
57
49
  'error.invalidTarget': string;
58
50
  'sep.list': string;
59
51
  'sep.clause': string;
@@ -106,23 +106,6 @@ export declare function parseRewindTarget(raw: string): RewindTarget | undefined
106
106
  * @returns candidates numbered 1..N by recency.
107
107
  */
108
108
  export declare function listRewindCandidates(events: readonly SessionEvent[], surface: readonly number[], limit?: number): RewindCandidate[];
109
- /** Header line of the machine-readable candidate list (locale-independent). */
110
- export declare const CANDIDATE_LIST_HEADER = "candidates=";
111
- /**
112
- * Encode a candidate list as the host→client machine channel (the same
113
- * trailer pattern `formatPlan` uses for `impact=`). The client popupSelect
114
- * parses this instead of reading the windowed chat snapshot, so the candidate
115
- * list reflects the FULL host surface — not just the already-loaded history.
116
- *
117
- * Lines (each preview is already whitespace-collapsed and tab-free by
118
- * `messagePreview`):
119
- * candidates=<n>
120
- * <seq>\t<time>\t<preview>
121
- * … (one line per candidate, newest first, matching `listRewindCandidates`)
122
- *
123
- * A list with no candidates is just `candidates=0`.
124
- */
125
- export declare function formatCandidateList(candidates: readonly RewindCandidate[]): string;
126
109
  /**
127
110
  * Resolve a target against the session log and surface into a validated plan.
128
111
  * @param events - the full session event log.
@@ -179,7 +179,7 @@ export interface LinkEntry {
179
179
  export type StoredEntry = CheckpointEntry | LinkEntry;
180
180
  /** True when an entry is a dedup link (carries `ref`, not `before`). */
181
181
  export declare function isLinkEntry(entry: StoredEntry): entry is LinkEntry;
182
- /** Per-file restore impact preview (`/rewind preview @seq both`). */
182
+ /** Per-file restore impact preview (served by the host RPC channel). */
183
183
  export interface FileImpact {
184
184
  readonly path: string;
185
185
  /** `restore` = write the before content back; `delete` = remove the file. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-rewind-plugin",
3
- "version": "0.17.1",
3
+ "version": "0.18.0",
4
4
  "description": "In-window conversation rewind with workspace file restore · 同窗口内对话回退并可还原工作区文件",
5
5
  "icon": "./assets/icon.svg",
6
6
  "keywords": [
@@ -54,6 +54,7 @@
54
54
  "client": {
55
55
  "inject": [
56
56
  "@deepseek-ai/dsh-client-locale",
57
+ "@deepseek-ai/dsh-client-connection",
57
58
  "@deepseek-ai/dsh-client-ui-commands",
58
59
  "@deepseek-ai/dsh-client-ui-conversation",
59
60
  "@deepseek-ai/dsh-client-ui-settings"
@@ -74,6 +75,7 @@
74
75
  },
75
76
  "peerDependencies": {
76
77
  "@deepseek-ai/cordis": "^4.0.5",
78
+ "@deepseek-ai/dsh-client-connection": "^0.2.1-rc.1",
77
79
  "@deepseek-ai/dsh-client-locale": "^0.2.1-rc.1",
78
80
  "@deepseek-ai/dsh-client-ui-commands": "^0.2.1-rc.1",
79
81
  "@deepseek-ai/dsh-client-ui-plugin-manager": "^0.2.1-rc.1",
@@ -95,6 +97,9 @@
95
97
  "@deepseek-ai/cordis": {
96
98
  "optional": true
97
99
  },
100
+ "@deepseek-ai/dsh-client-connection": {
101
+ "optional": true
102
+ },
98
103
  "@deepseek-ai/dsh-client-locale": {
99
104
  "optional": true
100
105
  },