dsh-rewind-plugin 0.2.6 → 0.2.8

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
@@ -32,12 +32,28 @@ __export(index_exports, {
32
32
  });
33
33
  module.exports = __toCommonJS(index_exports);
34
34
 
35
+ // src/client/portals.tsx
36
+ var import_react = require("react");
37
+ var import_react_dom = require("react-dom");
38
+
35
39
  // src/client/hidden.ts
36
40
  function targetOfOutcome(text) {
37
41
  if (text === void 0) return void 0;
38
42
  const match = text.match(/seq (\d+)/);
39
43
  return match !== null ? Number(match[1]) : void 0;
40
44
  }
45
+ function isExecutedRewindCommand(node, seq) {
46
+ if (node.name !== "rewind" || node.outcome?.kind !== "success") return false;
47
+ if (node.outcome.sourceEventSeq === void 0) return false;
48
+ const args = node.args ?? "";
49
+ return new RegExp(`(?:^|\\s)@${seq}(?:\\s|$)`).test(args);
50
+ }
51
+ function hasFileImpact(text) {
52
+ if (text === void 0) return true;
53
+ const match = text.match(/impact=(\d+)/);
54
+ if (match !== null) return Number(match[1]) > 0;
55
+ return text.includes("\u5C06\u5F71\u54CD");
56
+ }
41
57
  function isPreviewCommand(command) {
42
58
  return (command.args ?? "").includes("preview");
43
59
  }
@@ -73,48 +89,7 @@ function hiddenSeqsOf(snap) {
73
89
  return hidden;
74
90
  }
75
91
 
76
- // src/client/locales.ts
77
- var zh = {
78
- "button.aria": "\u56DE\u9000\u5230\u6B64\u6D88\u606F",
79
- "button.title": "\u56DE\u9000",
80
- "popover.title": "\u56DE\u9000\u5230\u8FD9\u6761\u6D88\u606F",
81
- "popover.noText": "\uFF08\u65E0\u6587\u672C\uFF09",
82
- "popover.chat": "\u4EC5\u56DE\u9000\u5BF9\u8BDD",
83
- "popover.chat.hint": "\u53EA\u56DE\u9000\u6A21\u578B\u4E0A\u4E0B\u6587\uFF0C\u4E0D\u52A8\u5DE5\u4F5C\u533A\u6587\u4EF6",
84
- "popover.both": "\u56DE\u9000\u5BF9\u8BDD\u548C\u4EE3\u7801",
85
- "popover.both.hint": "\u5BF9\u8BDD\u56DE\u9000\u5E76\u8FD8\u539F\u5DE5\u4F5C\u533A\u6587\u4EF6",
86
- "popover.checking": "\u6B63\u5728\u68C0\u67E5\u6587\u4EF6\u53D8\u66F4\u2026",
87
- "popover.noChanges": "\u6B64\u6D88\u606F\u4E4B\u540E\u6CA1\u6709\u53EF\u8FD8\u539F\u7684\u6587\u4EF6\u53D8\u66F4\uFF0C\u4EC5\u53EF\u56DE\u9000\u5BF9\u8BDD",
88
- "popover.cancel": "\u53D6\u6D88",
89
- "popover.impact.loading": "\u6B63\u5728\u83B7\u53D6\u5F71\u54CD\u6E05\u5355\u2026",
90
- "popover.impact.failed": "\u65E0\u6CD5\u83B7\u53D6\u5F71\u54CD\u6E05\u5355\uFF1A{message}",
91
- "popover.impact.none": "\u76EE\u6807\u4E4B\u540E\u6CA1\u6709\u8DDF\u8E2A\u5230\u7684\u5199\u7C7B\u53D8\u66F4\uFF0C\u65E0\u9700\u8FD8\u539F\u6587\u4EF6\u3002",
92
- "popover.confirm": "\u786E\u8BA4\u56DE\u9000",
93
- "popover.back": "\u8FD4\u56DE",
94
- "guard.hint": "/rewind \u547D\u4EE4\u4E0D\u652F\u6301\u624B\u52A8\u8F93\u5165\uFF0C\u8BF7\u4F7F\u7528\u6D88\u606F\u65C1\u7684 \u21B6 \u56DE\u9000\u6309\u94AE"
95
- };
96
- var en = {
97
- "button.aria": "Rewind to this message",
98
- "button.title": "Rewind",
99
- "popover.title": "Rewind to this message",
100
- "popover.noText": "(no text)",
101
- "popover.chat": "Rewind conversation only",
102
- "popover.chat.hint": "Cut the model context only; workspace files stay untouched",
103
- "popover.both": "Rewind conversation and code",
104
- "popover.both.hint": "Cut the context and restore workspace files",
105
- "popover.checking": "Checking for file changes\u2026",
106
- "popover.noChanges": "No tracked file changes after this message; conversation-only rewind",
107
- "popover.cancel": "Cancel",
108
- "popover.impact.loading": "Fetching impact list\u2026",
109
- "popover.impact.failed": "Could not fetch the impact list: {message}",
110
- "popover.impact.none": "No tracked file changes after the target; nothing to restore.",
111
- "popover.confirm": "Confirm rewind",
112
- "popover.back": "Back",
113
- "guard.hint": "/rewind cannot be typed manually \u2014 use the \u21B6 button on a message to rewind."
114
- };
115
-
116
92
  // src/client/styles.ts
117
- var REWIND_ATTACHED = "data-dsh-rewind-attached";
118
93
  var CLASS = {
119
94
  button: "dsh-rewind-btn",
120
95
  popover: "dsh-rewind-popover",
@@ -291,6 +266,9 @@ function formatTarget(t, seq, time, preview) {
291
266
  const previewText = preview.length > 0 ? preview : t("popover.noText");
292
267
  return `seq ${seq} \xB7 ${hh}:${mm} \xB7 ${previewText}`;
293
268
  }
269
+ function stripImpactToken(text) {
270
+ return text.replace(/\n?impact=\d+\s*$/, "");
271
+ }
294
272
  function findCommand(snapshot, match) {
295
273
  let found;
296
274
  for (const key of snapshot.chat.order) {
@@ -302,6 +280,18 @@ function findCommand(snapshot, match) {
302
280
  }
303
281
  return found;
304
282
  }
283
+ function knownCommandSeqs(session, match) {
284
+ const known = /* @__PURE__ */ new Set();
285
+ const snapshot = session.getSnapshot();
286
+ for (const key of snapshot.chat.order) {
287
+ const node = snapshot.chat.nodes.get(key);
288
+ if (node !== void 0 && node.kind === "command") {
289
+ const command = node.data;
290
+ if (match(command)) known.add(command.seq);
291
+ }
292
+ }
293
+ return known;
294
+ }
305
295
  function waitForCommand(session, match, timeoutMs = 8e3) {
306
296
  return new Promise((resolve) => {
307
297
  let settled = false;
@@ -328,9 +318,10 @@ function isPreviewFor(node, seq) {
328
318
  return node.name === "rewind" && args.includes("preview") && new RegExp(`(?:^|\\s)@${seq}(?=\\s|$)`).test(args);
329
319
  }
330
320
  async function previewImpact(session, seq) {
321
+ const known = knownCommandSeqs(session, (node) => isPreviewFor(node, seq));
331
322
  const result = await session.command(`/rewind preview @${seq} both`);
332
323
  if (!result.ok || result.value?.matched !== true) return null;
333
- return waitForCommand(session, (node) => isPreviewFor(node, seq));
324
+ return waitForCommand(session, (node) => isPreviewFor(node, seq) && !known.has(node.seq));
334
325
  }
335
326
  function el(tag, className, text) {
336
327
  const node = document.createElement(tag);
@@ -375,11 +366,11 @@ function renderImpactStep(root, opts, back, cached) {
375
366
  impact.textContent = t("popover.impact.failed", { message: outcome.text ?? "unknown error" });
376
367
  return;
377
368
  }
378
- impact.textContent = outcome.text ?? t("popover.impact.none");
369
+ impact.textContent = outcome.text === void 0 ? t("popover.impact.none") : stripImpactToken(outcome.text);
379
370
  confirm.disabled = false;
380
371
  confirm.addEventListener("click", () => {
381
372
  closePopover();
382
- void session.command(`/rewind @${seq} both`);
373
+ opts.onRewind("both");
383
374
  });
384
375
  })().catch(() => {
385
376
  impact.textContent = t("popover.impact.failed", { message: "unexpected error" });
@@ -399,7 +390,7 @@ function openPopover(opts) {
399
390
  el("div", CLASS.popoverTarget, formatTarget(t, seq, time, preview)),
400
391
  modeOption(t("popover.chat"), t("popover.chat.hint"), () => {
401
392
  closePopover();
402
- void session.command(`/rewind @${seq} chat`);
393
+ opts.onRewind("chat");
403
394
  })
404
395
  ];
405
396
  if (bothState.state === "noChanges") {
@@ -436,7 +427,6 @@ function openPopover(opts) {
436
427
  renderModes();
437
428
  document.body.append(root);
438
429
  position();
439
- const hasFileImpact = (text) => text === void 0 || text.includes("\u5C06\u5F71\u54CD");
440
430
  void (async () => {
441
431
  const outcome = await previewImpact(session, seq);
442
432
  impactOutcome = outcome;
@@ -470,14 +460,8 @@ function openPopover(opts) {
470
460
  };
471
461
  }
472
462
 
473
- // src/client/index.ts
474
- var name = "dsh-rewind";
475
- var inject = ["sessions", "locale"];
476
- var NS = "rewind";
477
- var USER_SEAT_SELECTOR = '[data-chat-flow-kind="user"]';
478
- var CHAT_SEAT_SELECTOR = "[data-chat-anchor-key]";
479
- var ACTIONS_ROOT_SELECTOR = "[data-time-hover-root]";
480
- var COMPOSER_SELECTOR = "[data-input-scroll] textarea, textarea[data-phase]";
463
+ // src/client/portals.tsx
464
+ var import_jsx_runtime = require("react/jsx-runtime");
481
465
  function messagePreviewOf(node) {
482
466
  const text = node.content.map((block) => block.type === "text" && typeof block.text === "string" ? block.text : "").join("").replace(/\s+/g, " ").trim();
483
467
  return text.length <= 80 ? text : `${text.slice(0, 79)}\u2026`;
@@ -503,133 +487,234 @@ function fillComposer(text) {
503
487
  textarea.focus();
504
488
  return true;
505
489
  }
506
- function apply(ctx) {
507
- ctx.effect(function* () {
508
- yield ctx.locale.register(NS, { zh, en });
509
- const t = ctx.locale.bind(NS);
510
- const style = document.createElement("style");
511
- style.dataset.plugin = "dsh-rewind";
512
- style.textContent = STYLE;
513
- document.head.appendChild(style);
514
- const attached = /* @__PURE__ */ new WeakSet();
515
- const hidden = /* @__PURE__ */ new WeakSet();
516
- const buttons = /* @__PURE__ */ new Map();
517
- let observer = null;
518
- const refreshButtonLabels = () => {
519
- for (const button of buttons.values()) {
520
- button.setAttribute("aria-label", t("button.aria"));
521
- button.title = t("button.title");
522
- }
523
- };
524
- const unsubscribeLocale = ctx.locale.subscribe(refreshButtonLabels);
525
- const sessionFor = () => {
526
- const sessionId = ctx.sessions.list.getSnapshot().current;
527
- if (sessionId === void 0) return void 0;
528
- return ctx.sessions.binding(sessionId)?.session;
529
- };
530
- const userNodeFor = (key, session) => {
531
- const node = session.getSnapshot().chat.nodes.get(key);
532
- if (node === void 0 || node.kind !== "user") return void 0;
533
- return node.data;
534
- };
535
- const attach = (seat) => {
536
- const key = seat.dataset.chatAnchorKey;
537
- if (key === void 0 || attached.has(seat)) return;
538
- const hoverRoot = seat.querySelector(ACTIONS_ROOT_SELECTOR);
539
- const actions = hoverRoot?.lastElementChild;
540
- if (!(actions instanceof HTMLElement) || actions.querySelector("button") === null) return;
541
- attached.add(seat);
542
- const button = document.createElement("button");
543
- button.type = "button";
544
- button.className = CLASS.button;
545
- button.setAttribute("aria-label", t("button.aria"));
546
- button.title = t("button.title");
547
- button.innerHTML = REWIND_ICON_SVG;
548
- button.addEventListener("click", (event) => {
549
- event.stopPropagation();
550
- const session = sessionFor();
551
- if (session === void 0) {
552
- console.warn("[dsh-rewind] rewind button clicked with no current session");
553
- return;
554
- }
555
- const node = userNodeFor(key, session);
556
- if (node === void 0) return;
557
- openPopover({
558
- session,
559
- seq: node.seq,
560
- time: node.time,
561
- preview: messagePreviewOf(node),
562
- anchor: button,
563
- t
564
- });
565
- });
566
- actions.appendChild(button);
567
- buttons.set(key, button);
568
- seat.setAttribute(REWIND_ATTACHED, "");
569
- };
570
- const scan = () => {
571
- for (const [key, button] of buttons) {
572
- if (!button.isConnected) buttons.delete(key);
490
+ function userNodeOf(session, key) {
491
+ const node = session.getSnapshot().chat.nodes.get(key);
492
+ if (node === void 0 || node.kind !== "user" && node.kind !== "steering") return void 0;
493
+ return node.data;
494
+ }
495
+ async function runRewindAndFill(session, seq, mode, currentSessionId) {
496
+ const known = knownCommandSeqs(session, (node) => isExecutedRewindCommand(node, seq));
497
+ const result = await session.command(`/rewind @${seq} ${mode}`);
498
+ if (!result.ok || result.value?.matched !== true) return;
499
+ const outcome = await waitForCommand(session, (node) => isExecutedRewindCommand(node, seq) && !known.has(node.seq), 2e4);
500
+ if (outcome === null || outcome.kind !== "success") return;
501
+ if (currentSessionId() !== session.sessionId) return;
502
+ const text = userTextAt(session, seq);
503
+ if (text === void 0 || text === "") return;
504
+ fillComposer(text);
505
+ }
506
+ var COMPOSER_SELECTOR = "[data-input-scroll] textarea, textarea[data-phase]";
507
+ var USER_SEAT_SELECTOR = '[data-chat-flow-kind="user"][data-chat-anchor-key], [data-chat-flow-kind="steering"][data-chat-anchor-key]';
508
+ var CHAT_SEAT_SELECTOR = "[data-chat-anchor-key]";
509
+ var ACTIONS_ROOT_SELECTOR = "[data-time-hover-root]";
510
+ function collectTargets(chat, hiddenSeqs) {
511
+ const rows = /* @__PURE__ */ new Map();
512
+ for (const element of document.querySelectorAll(USER_SEAT_SELECTOR)) {
513
+ const key = element.dataset.chatAnchorKey;
514
+ if (key !== void 0) rows.set(key, element);
515
+ }
516
+ const targets = [];
517
+ for (const key of chat.order) {
518
+ const node = chat.nodes.get(key);
519
+ if (node === void 0 || node.kind !== "user" && node.kind !== "steering") continue;
520
+ const user = node.data;
521
+ if (hiddenSeqs.has(node.anchorSeq ?? user.seq)) continue;
522
+ const row = rows.get(key);
523
+ const messageRoot = row?.querySelector(ACTIONS_ROOT_SELECTOR);
524
+ const actions = messageRoot?.lastElementChild;
525
+ if (!(actions instanceof HTMLElement) || actions.querySelector("button") === null) continue;
526
+ targets.push({ key, container: actions, seq: user.seq, time: user.time, preview: messagePreviewOf(user) });
527
+ }
528
+ return targets;
529
+ }
530
+ function sameTargets(left, right) {
531
+ return left.length === right.length && left.every((target, index) => {
532
+ const other = right[index];
533
+ return other !== void 0 && target.key === other.key && target.container === other.container && target.seq === other.seq;
534
+ });
535
+ }
536
+ function RewindPortals({ sessionId, sessionOf, currentSessionId, t, subscribeLocale }) {
537
+ const [targets, setTargets] = (0, import_react.useState)([]);
538
+ const hidden = (0, import_react.useRef)(/* @__PURE__ */ new WeakSet());
539
+ const [, forceRender] = (0, import_react.useReducer)((count) => count + 1, 0);
540
+ (0, import_react.useEffect)(() => subscribeLocale(() => {
541
+ forceRender();
542
+ }), [subscribeLocale]);
543
+ (0, import_react.useLayoutEffect)(() => {
544
+ let active = true;
545
+ let queued = false;
546
+ const refresh = () => {
547
+ if (!active) return;
548
+ const session = sessionOf(sessionId);
549
+ if (session === void 0) {
550
+ setTargets([]);
551
+ return;
573
552
  }
574
- const session = sessionFor();
575
- const hiddenSeqs = session !== void 0 ? hiddenSeqsOf(session.getSnapshot().chat) : /* @__PURE__ */ new Set();
553
+ const chat = session.getSnapshot().chat;
554
+ const hiddenSeqs = hiddenSeqsOf(chat);
576
555
  let hiddenCount = 0;
577
556
  for (const seat of document.querySelectorAll(CHAT_SEAT_SELECTOR)) {
578
557
  const key = seat.dataset.chatAnchorKey;
579
- const anchor = key !== void 0 && session !== void 0 ? session.getSnapshot().chat.nodes.get(key)?.anchorSeq : void 0;
558
+ const anchor = key !== void 0 ? chat.nodes.get(key)?.anchorSeq : void 0;
580
559
  if (anchor !== void 0 && hiddenSeqs.has(anchor)) {
581
560
  seat.style.display = "none";
582
- hidden.add(seat);
561
+ hidden.current.add(seat);
583
562
  hiddenCount += 1;
584
- } else if (hidden.has(seat)) {
563
+ } else if (hidden.current.has(seat)) {
585
564
  seat.style.display = "";
586
- hidden.delete(seat);
587
- }
588
- }
589
- if (session !== void 0) {
590
- const snap = session.getSnapshot();
591
- if (!fillBaselineTaken && snap.chat.order.length > 0) {
592
- fillBaselineTaken = true;
593
- for (const key of snap.chat.order) {
594
- const node = snap.chat.nodes.get(key);
595
- if (node?.kind === "command") {
596
- const command = node.data;
597
- if (command.seq > fillBaselineSeq) fillBaselineSeq = command.seq;
598
- }
599
- }
565
+ hidden.current.delete(seat);
600
566
  }
601
- fillComposerForRewind(session, filledTargets);
602
567
  }
603
568
  if (hiddenSeqs.size > 0 || hiddenCount > 0) {
604
569
  console.info(
605
570
  `[dsh-rewind] hiding: ${hiddenCount} rows, seqs [${[...hiddenSeqs].slice(0, 20).join(", ")}${hiddenSeqs.size > 20 ? "\u2026" : ""}]`
606
571
  );
607
572
  }
608
- for (const seat of document.querySelectorAll(USER_SEAT_SELECTOR)) {
609
- if (!hidden.has(seat)) attach(seat);
610
- }
573
+ const next = collectTargets(chat, hiddenSeqs);
574
+ setTargets((current) => sameTargets(current, next) ? current : next);
611
575
  };
612
- const filledTargets = /* @__PURE__ */ new Set();
613
- let fillBaselineSeq = -1;
614
- let fillBaselineTaken = false;
615
- const fillComposerForRewind = (session, filled) => {
616
- const snap = session.getSnapshot();
617
- for (const key of snap.chat.order) {
618
- const node = snap.chat.nodes.get(key);
619
- if (node === void 0 || node.kind !== "command") continue;
620
- const command = node.data;
621
- if (command.name !== "rewind" || command.outcome?.kind !== "success") continue;
622
- if (command.seq <= fillBaselineSeq) continue;
623
- if (command.outcome.sourceEventSeq === void 0) continue;
624
- const target = targetOfOutcome(command.outcome.text);
625
- if (target === void 0 || filled.has(target)) continue;
626
- const text = userTextAt(session, target);
627
- if (text === void 0 || text === "") continue;
628
- if (fillComposer(text)) filled.add(target);
629
- }
576
+ const queueRefresh = () => {
577
+ if (queued || !active) return;
578
+ queued = true;
579
+ queueMicrotask(() => {
580
+ queued = false;
581
+ refresh();
582
+ });
583
+ };
584
+ refresh();
585
+ const observer = new MutationObserver(queueRefresh);
586
+ observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["style"] });
587
+ return () => {
588
+ active = false;
589
+ observer.disconnect();
630
590
  };
591
+ }, [sessionId, sessionOf]);
592
+ return targets.map((target) => (0, import_react_dom.createPortal)(
593
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
594
+ RewindButton,
595
+ {
596
+ target,
597
+ sessionId,
598
+ sessionOf,
599
+ currentSessionId,
600
+ t
601
+ },
602
+ target.key
603
+ ),
604
+ target.container,
605
+ target.key
606
+ ));
607
+ }
608
+ function RewindButton({ target, sessionId, sessionOf, currentSessionId, t }) {
609
+ const onClick = (event) => {
610
+ event.stopPropagation();
611
+ const session = sessionOf(sessionId);
612
+ if (session === void 0) {
613
+ console.warn("[dsh-rewind] rewind button clicked with no session binding");
614
+ return;
615
+ }
616
+ const node = userNodeOf(session, target.key);
617
+ if (node === void 0) return;
618
+ openPopover({
619
+ session,
620
+ seq: node.seq,
621
+ time: node.time,
622
+ preview: messagePreviewOf(node),
623
+ anchor: event.currentTarget,
624
+ t,
625
+ onRewind: (mode) => {
626
+ void runRewindAndFill(session, node.seq, mode, currentSessionId);
627
+ }
628
+ });
629
+ };
630
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
631
+ "button",
632
+ {
633
+ type: "button",
634
+ className: CLASS.button,
635
+ "aria-label": t("button.aria"),
636
+ title: t("button.title"),
637
+ onClick,
638
+ dangerouslySetInnerHTML: { __html: REWIND_ICON_SVG }
639
+ }
640
+ );
641
+ }
642
+ function createRewindBridge(deps) {
643
+ return function RewindBridge({ sessionId }) {
644
+ return (0, import_react.createElement)(RewindPortals, { sessionId, ...deps });
645
+ };
646
+ }
647
+
648
+ // src/client/locales.ts
649
+ var zh = {
650
+ "button.aria": "\u56DE\u9000\u5230\u6B64\u6D88\u606F",
651
+ "button.title": "\u56DE\u9000",
652
+ "popover.title": "\u56DE\u9000\u5230\u8FD9\u6761\u6D88\u606F",
653
+ "popover.noText": "\uFF08\u65E0\u6587\u672C\uFF09",
654
+ "popover.chat": "\u4EC5\u56DE\u9000\u5BF9\u8BDD",
655
+ "popover.chat.hint": "\u53EA\u56DE\u9000\u6A21\u578B\u4E0A\u4E0B\u6587\uFF0C\u4E0D\u52A8\u5DE5\u4F5C\u533A\u6587\u4EF6",
656
+ "popover.both": "\u56DE\u9000\u5BF9\u8BDD\u548C\u4EE3\u7801",
657
+ "popover.both.hint": "\u5BF9\u8BDD\u56DE\u9000\u5E76\u8FD8\u539F\u5DE5\u4F5C\u533A\u6587\u4EF6",
658
+ "popover.checking": "\u6B63\u5728\u68C0\u67E5\u6587\u4EF6\u53D8\u66F4\u2026",
659
+ "popover.noChanges": "\u6B64\u6D88\u606F\u4E4B\u540E\u6CA1\u6709\u53EF\u8FD8\u539F\u7684\u6587\u4EF6\u53D8\u66F4\uFF0C\u4EC5\u53EF\u56DE\u9000\u5BF9\u8BDD",
660
+ "popover.cancel": "\u53D6\u6D88",
661
+ "popover.impact.loading": "\u6B63\u5728\u83B7\u53D6\u5F71\u54CD\u6E05\u5355\u2026",
662
+ "popover.impact.failed": "\u65E0\u6CD5\u83B7\u53D6\u5F71\u54CD\u6E05\u5355\uFF1A{message}",
663
+ "popover.impact.none": "\u76EE\u6807\u4E4B\u540E\u6CA1\u6709\u8DDF\u8E2A\u5230\u7684\u5199\u7C7B\u53D8\u66F4\uFF0C\u65E0\u9700\u8FD8\u539F\u6587\u4EF6\u3002",
664
+ "popover.confirm": "\u786E\u8BA4\u56DE\u9000",
665
+ "popover.back": "\u8FD4\u56DE",
666
+ "guard.hint": "/rewind \u547D\u4EE4\u4E0D\u652F\u6301\u624B\u52A8\u8F93\u5165\uFF0C\u8BF7\u4F7F\u7528\u6D88\u606F\u65C1\u7684 \u21B6 \u56DE\u9000\u6309\u94AE"
667
+ };
668
+ var en = {
669
+ "button.aria": "Rewind to this message",
670
+ "button.title": "Rewind",
671
+ "popover.title": "Rewind to this message",
672
+ "popover.noText": "(no text)",
673
+ "popover.chat": "Rewind conversation only",
674
+ "popover.chat.hint": "Cut the model context only; workspace files stay untouched",
675
+ "popover.both": "Rewind conversation and code",
676
+ "popover.both.hint": "Cut the context and restore workspace files",
677
+ "popover.checking": "Checking for file changes\u2026",
678
+ "popover.noChanges": "No tracked file changes after this message; conversation-only rewind",
679
+ "popover.cancel": "Cancel",
680
+ "popover.impact.loading": "Fetching impact list\u2026",
681
+ "popover.impact.failed": "Could not fetch the impact list: {message}",
682
+ "popover.impact.none": "No tracked file changes after the target; nothing to restore.",
683
+ "popover.confirm": "Confirm rewind",
684
+ "popover.back": "Back",
685
+ "guard.hint": "/rewind cannot be typed manually \u2014 use the \u21B6 button on a message to rewind."
686
+ };
687
+
688
+ // src/client/index.ts
689
+ var name = "dsh-rewind";
690
+ var inject = ["slots", "sessions", "locale"];
691
+ var NS = "rewind";
692
+ var HEADER_ACTIONS_SLOT = "conversation.session.header.actions";
693
+ var COMPOSER_SELECTOR2 = "[data-input-scroll] textarea, textarea[data-phase]";
694
+ function apply(ctx) {
695
+ ctx.effect(function* () {
696
+ yield ctx.locale.register(NS, { zh, en });
697
+ const t = ctx.locale.bind(NS);
698
+ const style = document.createElement("style");
699
+ style.dataset.plugin = "dsh-rewind";
700
+ style.textContent = STYLE;
701
+ document.head.appendChild(style);
702
+ const sessionOf = (sessionId) => ctx.sessions.binding(sessionId)?.session;
703
+ const currentSessionId = () => ctx.sessions.list.getSnapshot().current;
704
+ const subscribeLocale = (cb) => ctx.locale.subscribe(cb);
705
+ const slots = ctx.slots;
706
+ yield slots.inject(HEADER_ACTIONS_SLOT, () => slots.register(
707
+ {
708
+ name: HEADER_ACTIONS_SLOT,
709
+ // A distinct list-entry id keeps the bridge from shadowing any other
710
+ // header action; the entry renders portals only, never header UI.
711
+ id: "dsh-rewind-portals",
712
+ order: 1e3
713
+ },
714
+ createRewindBridge({ sessionOf, currentSessionId, t, subscribeLocale })
715
+ ));
631
716
  const MANUAL_REWIND = /^\s*\/rewind(?:\s|$)/i;
632
- const composerTextarea = () => document.querySelector(COMPOSER_SELECTOR);
717
+ const composerTextarea = () => document.querySelector(COMPOSER_SELECTOR2);
633
718
  const hasBlockedRewindDraft = () => {
634
719
  const textarea = composerTextarea();
635
720
  return textarea !== null && MANUAL_REWIND.test(textarea.value);
@@ -643,7 +728,7 @@ function apply(ctx) {
643
728
  if (textarea === null) return;
644
729
  const card = textarea.closest("[data-composer-card]");
645
730
  const hint = document.createElement("div");
646
- hint.className = CLASS.guardHint;
731
+ hint.className = "dsh-rewind-guard-hint";
647
732
  hint.setAttribute("role", "status");
648
733
  hint.textContent = t("guard.hint");
649
734
  document.body.appendChild(hint);
@@ -681,19 +766,11 @@ function apply(ctx) {
681
766
  };
682
767
  document.addEventListener("keydown", onKeyDownGuard, true);
683
768
  document.addEventListener("click", onClickGuard, true);
684
- observer = new MutationObserver(scan);
685
- observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["style"] });
686
- scan();
687
769
  yield () => {
688
- unsubscribeLocale();
689
770
  document.removeEventListener("keydown", onKeyDownGuard, true);
690
771
  document.removeEventListener("click", onClickGuard, true);
691
772
  if (guardHintEl !== null) guardHintEl.remove();
692
773
  if (guardHintTimer !== void 0) window.clearTimeout(guardHintTimer);
693
- observer?.disconnect();
694
- for (const button of buttons.values()) button.remove();
695
- buttons.clear();
696
- closePopover();
697
774
  style.remove();
698
775
  };
699
776
  }, "dsh-rewind client lifecycle");