dsh-rewind-plugin 0.18.0 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -11,12 +11,12 @@ Conversation rewind for DeepSeek Harness: **rewind the conversation to any earli
11
11
  A deliberately focused plugin with one job: **rewind to any user message, no matter how far back, in place** — and conveniently **restore the files it changed** along the way.
12
12
 
13
13
  - **In-place rewind, no new session** — the target message and everything after it (replies, tool calls) are withdrawn from the model context *and* the rendered transcript, and the target's text is filled back into the composer for editing and re-sending. It all happens inside the original session: no new branch, no leftover copy.
14
- - **Conversation and code rewind together** — Claude Code-aligned behavior: files edited by the write tools are tracked and restored by the same rewind, and external changes to already-tracked files are restorable too. **Partial tracking, before-write backup, store only on change**, with no git dependency.
14
+ - **Conversation and files rewind together** — Claude Code-aligned behavior: files edited by the write tools are tracked and restored by the same rewind, and external changes to already-tracked files are restorable too. **Partial tracking, before-write backup, store only on change**, with no git dependency.
15
15
  - **Safety guarantees** — the session log only ever appends rewind markers and is never deleted or rewritten; file backup and restore are hardened; tests are comprehensive, and maintenance continues across DSH upgrades. Security model: [SECURITY.md](SECURITY.md).
16
16
 
17
17
  ## Preview
18
18
 
19
- Every user message carries a **↶ rewind** button in its action row. Clicking it opens a mode-selection popover — "**rewind conversation only**" or "**rewind conversation and code**", the latter showing the file-change list for confirmation first. You can also pick and rewind conveniently via the **`/rewind` and `/undo` commands**.
19
+ Every user message carries a **↶ rewind** button in its action row. Clicking it opens a mode-selection popover — "**rewind conversation only**" or "**rewind conversation and files**", the latter showing the file-change list for confirmation first. You can also pick and rewind conveniently via the **`/rewind` and `/undo` commands**.
20
20
 
21
21
  <table>
22
22
  <tr>
@@ -24,7 +24,7 @@ Every user message carries a **↶ rewind** button in its action row. Clicking i
24
24
  <td align="center"><img src="assets/screenshots/mode-popover.png" width="440" alt="Mode-selection popover"><br><sub>Mode-selection popover</sub></td>
25
25
  </tr>
26
26
  <tr>
27
- <td align="center"><img src="assets/screenshots/impact-list.png" width="440" alt="Impact list"><br><sub>"Conversation and code" impact list</sub></td>
27
+ <td align="center"><img src="assets/screenshots/impact-list.png" width="440" alt="Impact list"><br><sub>"Conversation and files" impact list</sub></td>
28
28
  <td align="center"><img src="assets/screenshots/rewind-candidates.png" width="440" alt="/rewind candidate picker"><br><sub>/rewind candidate picker</sub></td>
29
29
  </tr>
30
30
  </table>
@@ -55,7 +55,7 @@ then install the new one.
55
55
  ## Usage
56
56
 
57
57
  1. Find the user message you want to rewind to in the conversation, or type `/rewind` (or its alias `/undo`) to open the candidate picker.
58
- 2. **Select it.** A small popover offers the two modes — "conversation only" or "conversation and code".
58
+ 2. **Select it.** A small popover offers the two modes — "conversation only" or "conversation and files".
59
59
  3. The rewind takes effect immediately: the conversation returns to how it looked at the target message, and the target message's text is filled back into the composer — edit and re-send.
60
60
 
61
61
  **Keyboard**: both the candidate picker and the mode popover support ↑↓ to move, Enter to confirm, Esc to cancel/back.
@@ -121,7 +121,7 @@ The whole design rests on two principles, simple but deliberate: **the conversat
121
121
 
122
122
  ### 1. Conversation rewind: a single "mask", not a delete
123
123
 
124
- `append-only` is a hard rule: the session log only grows and is never rewritten — the foundation of auditability and privacy. A rewind never touches history; it makes a single move: append **one "empty message" marker** to the end of the log and use it to "mask + replace" everything after the target message, so the model and the UI see only the part before it.
124
+ `append-only` is a hard rule: the session log only grows and is never rewritten — the foundation of auditability and privacy. A rewind never touches history; it makes a single move: append **one "empty message" marker** to the end of the log and use it to "mask + replace" the target message and everything after it, so the model and the UI see only what came before it.
125
125
 
126
126
  - **One and the same log** — the append happens only in the current session's log: no new session, no new branch, so no residue or copy is left behind;
127
127
  - **The marker is canonical** — the same "hide + replace" as the official `/compact`: `/compact` compresses a span of history into a summary, while `/rewind` swaps in an "empty message" marker. Because it is canonical, DSH's log replay, compaction, and resume preflight all recognize it and never mistake it for a real message;
@@ -178,7 +178,7 @@ withdrew should consume the stable, locale-independent helpers exported from
178
178
 
179
179
  ## Security
180
180
 
181
- This plugin only appends rewind-marker events to the session log; it never deletes or rewrites logged history. Workspace files are written only when you choose "conversation and code"; backups are stored under `<dsh home>/rewind-snapshots/`; restores draw only from those backups. It never touches your git repository, makes no network requests, and accesses no credentials. For sessions you've left inactive for a long time, a global auto-cleanup (off by default) can remove their snapshot directory in whole, leaving the active session and the chat log untouched. Full security model: [SECURITY.md](SECURITY.md).
181
+ This plugin only appends rewind-marker events to the session log; it never deletes or rewrites logged history. Workspace files are written only when you choose "conversation and files"; backups are stored under `<dsh home>/rewind-snapshots/`; restores draw only from those backups. It never touches your git repository, makes no network requests, and accesses no credentials. For sessions you've left inactive for a long time, a global auto-cleanup (off by default) can remove their snapshot directory in whole, leaving the active session and the chat log untouched. Full security model: [SECURITY.md](SECURITY.md).
182
182
 
183
183
  ## Development
184
184
 
package/README.md CHANGED
@@ -16,7 +16,7 @@ DeepSeek Harness 插件:**一键就地回退对话到任意更早的用户消
16
16
 
17
17
  ## 效果预览
18
18
 
19
- 每条用户消息的操作行都有一个 **↶ 回退** 按钮。点击后弹出模式选择浮层——「**仅回退对话**」或「**回退对话和代码**」,后者会先展示文件变更清单再确认。还可以通过 **`/rewind` 和 `/undo` 命令**便捷地选择和回退。
19
+ 每条用户消息的操作行都有一个 **↶ 回退** 按钮。点击后弹出模式选择浮层——「**仅回退对话**」或「**回退对话和文件**」,后者会先展示文件变更清单再确认。还可以通过 **`/rewind` 和 `/undo` 命令**便捷地选择和回退。
20
20
 
21
21
  <table>
22
22
  <tr>
@@ -24,7 +24,7 @@ DeepSeek Harness 插件:**一键就地回退对话到任意更早的用户消
24
24
  <td align="center"><img src="assets/screenshots/mode-popover.png" width="440" alt="模式选择浮层"><br><sub>模式选择浮层</sub></td>
25
25
  </tr>
26
26
  <tr>
27
- <td align="center"><img src="assets/screenshots/impact-list.png" width="440" alt="影响清单"><br><sub>「回退对话和代码」影响清单</sub></td>
27
+ <td align="center"><img src="assets/screenshots/impact-list.png" width="440" alt="影响清单"><br><sub>「回退对话和文件」影响清单</sub></td>
28
28
  <td align="center"><img src="assets/screenshots/rewind-candidates.png" width="440" alt="/rewind 候选面板"><br><sub>/rewind 候选面板</sub></td>
29
29
  </tr>
30
30
  </table>
@@ -52,7 +52,7 @@ dsh plugin --profile <name> add dsh-rewind-plugin@<版本>
52
52
  ## 使用
53
53
 
54
54
  1. 在对话中找到要回退的那条用户消息,或输入 `/rewind`(或其别名 `/undo`)打开候选列表选择。
55
- 2. **选中它。** 小浮层提供两种模式——「仅回退对话」或「回退对话和代码」。
55
+ 2. **选中它。** 小浮层提供两种模式——「仅回退对话」或「回退对话和文件」。
56
56
  3. 回退立即生效:对话回到目标消息当时的样子,目标消息的文本自动回填输入框——改完直接重发。
57
57
 
58
58
  **键盘操作**:候选列表与模式浮层均支持 ↑↓ 移动、Enter 确认、Esc 取消/返回。
@@ -105,7 +105,7 @@ rm <dsh home>/snapshot-cleanup-last-sweep.json
105
105
 
106
106
  ### 1. 对话回退:一次「遮蔽」,而不是「删除」
107
107
 
108
- `append-only` 是铁律:会话日志只追加、从不改写——这是可审计与信息安全的地基。回退从不动历史,它只做一步:往日志末尾追加一条 **“空消息”标记**,把目标消息之后的全部内容「遮蔽 + 替换」掉,让模型和界面都只看得到目标之前的部分。
108
+ `append-only` 是铁律:会话日志只追加、从不改写——这是可审计与信息安全的地基。回退从不动历史,它只做一步:往日志末尾追加一条 **“空消息”标记**,把目标消息及其之后的全部内容「遮蔽 + 替换」掉,让模型和界面都只看得到目标之前的部分。
109
109
 
110
110
  - **同一份日志**——只在当前会话的日志做简单追加,不新建会话、不新建分支,因此不会留下残留和副本;
111
111
  - **标记是规范的**——采用与官方 `/compact` 相同的「隐藏 + 替换」:`/compact` 把一段历史压缩成摘要,`/rewind` 则换成一条“空消息”标记。由于其规范性,DSH 的日志重放、压缩、续接检查都能正确识别它,绝不会把它误认为真实对话;
@@ -159,7 +159,7 @@ rm <dsh home>/snapshot-cleanup-last-sweep.json
159
159
 
160
160
  ## 安全
161
161
 
162
- 本插件只向会话日志追加回退标记事件,从不删除或改写已记录的历史。工作区文件仅在「回退对话和代码」时被改写,备份存储于 `<dsh home>/rewind-snapshots/`;还原以备份为唯一来源。不触碰你的 git 仓库,无网络请求,不访问任何凭据。对**长期不活跃**的会话,另有默认关闭的全局自动清理可整目录移除其快照,不影响活动会话与对话日志。完整安全模型:[SECURITY.md](SECURITY.md)。
162
+ 本插件只向会话日志追加回退标记事件,从不删除或改写已记录的历史。工作区文件仅在「回退对话和文件」时被改写,备份存储于 `<dsh home>/rewind-snapshots/`;还原以备份为唯一来源。不触碰你的 git 仓库,无网络请求,不访问任何凭据。对**长期不活跃**的会话,另有默认关闭的全局自动清理可整目录移除其快照,不影响活动会话与对话日志。完整安全模型:[SECURITY.md](SECURITY.md)。
163
163
 
164
164
  ## 开发
165
165
 
package/lib/client.js CHANGED
@@ -278,47 +278,58 @@ var STYLE = `
278
278
  width: calc(15px + var(--dsh-content-font-delta, 0px));
279
279
  height: calc(15px + var(--dsh-content-font-delta, 0px));
280
280
  }
281
- .dsh-rewind-btn:hover {
282
- background: var(--dsw-alias-interactive-bg-hover);
283
- color: var(--dsw-alias-label-secondary);
281
+ @media (hover: hover) {
282
+ .dsh-rewind-btn:hover {
283
+ background: var(--dsw-alias-interactive-bg-hover);
284
+ color: var(--dsw-alias-label-secondary);
285
+ }
284
286
  }
285
287
 
286
288
  .dsh-rewind-popover {
287
289
  position: fixed;
288
- z-index: 1000;
289
- width: 288px;
290
- padding: 12px;
290
+ z-index: 1100;
291
+ box-sizing: border-box;
292
+ width: min(300px, calc(100vw - 24px));
293
+ padding: 16px;
291
294
  border: 0;
292
- border-radius: var(--dsw-radius-md);
295
+ border-radius: var(--dsw-radius-lg);
293
296
  background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));
294
297
  backdrop-filter: var(--dsw-menu-backdrop-filter);
295
298
  isolation: isolate;
296
299
  --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);
300
+ /* Elevated surface: match the menu cards' l2 scrollbar tokens. */
301
+ --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
302
+ --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
297
303
  box-shadow: var(--dsw-elevation-prominent);
298
- font-size: 14px;
299
- line-height: 20px;
304
+ font-size: 12px;
305
+ line-height: 18px;
300
306
  color: var(--dsw-alias-label-primary);
301
307
  }
302
308
  .dsh-rewind-popover-title {
303
- font-size: 14px;
304
- font-weight: 600;
305
- line-height: 20px;
309
+ font-size: 12px;
310
+ font-weight: 500;
311
+ line-height: 18px;
306
312
  }
307
313
  .dsh-rewind-popover-target {
308
- margin: 4px 0 10px;
314
+ margin: 2px 0 10px;
309
315
  font-size: 12px;
310
- line-height: 16px;
316
+ line-height: 18px;
311
317
  color: var(--dsw-alias-label-tertiary);
312
318
  word-break: break-all;
319
+ /* The full message sits above the popover; two lines are enough context. */
320
+ display: -webkit-box;
321
+ -webkit-box-orient: vertical;
322
+ -webkit-line-clamp: 2;
323
+ overflow: hidden;
313
324
  }
314
325
  .dsh-rewind-popover-option {
315
326
  display: flex;
316
327
  flex-direction: column;
317
328
  gap: 2px;
318
329
  width: 100%;
319
- margin: 0 0 6px;
320
- padding: 8px 10px;
321
- border: 1px solid transparent;
330
+ margin: 0 0 2px;
331
+ padding: 6px 8px;
332
+ border: 0;
322
333
  border-radius: 8px;
323
334
  background: transparent;
324
335
  color: inherit;
@@ -338,7 +349,7 @@ var STYLE = `
338
349
  outline: none;
339
350
  }
340
351
  .dsh-rewind-popover-option:disabled {
341
- opacity: 0.5;
352
+ opacity: 0.4;
342
353
  cursor: default;
343
354
  }
344
355
  .dsh-rewind-popover-option-label {
@@ -371,15 +382,15 @@ var STYLE = `
371
382
  display: flex;
372
383
  justify-content: flex-end;
373
384
  gap: 8px;
385
+ margin-top: 8px;
374
386
  }
375
387
  .dsh-rewind-popover-primary,
376
388
  .dsh-rewind-popover-ghost {
377
- padding: 5px 12px;
389
+ height: 28px;
390
+ padding: 0 10px;
378
391
  border: none;
379
392
  border-radius: 8px;
380
393
  font: inherit;
381
- font-size: 13px;
382
- line-height: 18px;
383
394
  cursor: pointer;
384
395
  }
385
396
  .dsh-rewind-popover-primary {
@@ -390,7 +401,7 @@ var STYLE = `
390
401
  background: var(--dsw-alias-button-primary-hover);
391
402
  }
392
403
  .dsh-rewind-popover-primary:disabled {
393
- opacity: 0.5;
404
+ opacity: 0.4;
394
405
  cursor: default;
395
406
  }
396
407
  .dsh-rewind-popover-ghost {
@@ -400,6 +411,9 @@ var STYLE = `
400
411
  .dsh-rewind-popover-ghost:hover {
401
412
  background: var(--dsw-alias-interactive-bg-hover);
402
413
  }
414
+ .dsh-rewind-popover-ghost:active {
415
+ background: var(--dsw-alias-interactive-bg-active);
416
+ }
403
417
 
404
418
  .dsh-rewind-guard-hint {
405
419
  position: fixed;
@@ -452,12 +466,21 @@ var STYLE = `
452
466
 
453
467
  // src/client/popover.ts
454
468
  var popoverEl = null;
469
+ var popoverAnchor = null;
455
470
  var disposeOutside = null;
456
471
  var liveSessionId = null;
457
472
  var composerFocuser = null;
458
473
  function registerComposerFocuser(focus) {
459
474
  composerFocuser = focus;
460
475
  }
476
+ function markOpen(anchor) {
477
+ popoverAnchor = anchor;
478
+ anchor.setAttribute("aria-expanded", "true");
479
+ }
480
+ function markClosed() {
481
+ popoverAnchor?.setAttribute("aria-expanded", "false");
482
+ popoverAnchor = null;
483
+ }
461
484
  function closePopover() {
462
485
  if (popoverEl !== null) {
463
486
  popoverEl.remove();
@@ -467,6 +490,7 @@ function closePopover() {
467
490
  disposeOutside();
468
491
  disposeOutside = null;
469
492
  }
493
+ markClosed();
470
494
  const sessionId = liveSessionId;
471
495
  liveSessionId = null;
472
496
  if (sessionId !== null) {
@@ -476,13 +500,6 @@ function closePopover() {
476
500
  }
477
501
  }
478
502
  }
479
- function formatTarget(t, seq, time, preview) {
480
- const d = new Date(time);
481
- const hh = String(d.getHours()).padStart(2, "0");
482
- const mm = String(d.getMinutes()).padStart(2, "0");
483
- const previewText = preview.length > 0 ? preview : t("popover.noText");
484
- return `seq ${seq} \xB7 ${hh}:${mm} \xB7 ${previewText}`;
485
- }
486
503
  function findCommand(chat, match) {
487
504
  if (chat === void 0) return void 0;
488
505
  let found;
@@ -566,7 +583,7 @@ function moveFocus(root, dir) {
566
583
  const next = index === -1 ? dir === 1 ? 0 : buttons.length - 1 : (index + dir + buttons.length) % buttons.length;
567
584
  buttons[next]?.focus();
568
585
  }
569
- function renderImpactStep(root, opts, back, cached) {
586
+ function renderImpactStep(root, opts, back, reposition, cached) {
570
587
  const { session, seq, t } = opts;
571
588
  const impact = el("div", CLASS.popoverImpact, t("popover.impact.loading"));
572
589
  const actions = el("div", CLASS.popoverActions);
@@ -584,6 +601,7 @@ function renderImpactStep(root, opts, back, cached) {
584
601
  actions.append(confirm);
585
602
  root.replaceChildren(impact, actions);
586
603
  focusFirst(root);
604
+ reposition();
587
605
  void (async () => {
588
606
  const outcome = cached ?? await previewImpact(session.sessionId, seq);
589
607
  if (outcome === null) {
@@ -619,16 +637,44 @@ function renderImpactStep(root, opts, back, cached) {
619
637
  });
620
638
  })().catch(() => {
621
639
  impact.textContent = t("popover.impact.failed", { message: "unexpected error" });
640
+ }).finally(() => {
641
+ reposition();
622
642
  });
623
643
  }
624
644
  function mountShell(root, anchor, onKeyDown) {
645
+ const margin = 12;
646
+ const gap = 4;
647
+ let flippedAbove = null;
625
648
  const position = () => {
649
+ if (!root.isConnected) return;
650
+ if (!anchor.isConnected) {
651
+ if (popoverEl === root) closePopover();
652
+ return;
653
+ }
626
654
  const rect = anchor.getBoundingClientRect();
627
- const gap = 4;
628
655
  const height = root.offsetHeight;
629
- const top = rect.bottom + gap + height <= window.innerHeight - 8 ? rect.bottom + gap : Math.max(8, rect.top - gap - height);
656
+ if (flippedAbove === null) {
657
+ flippedAbove = rect.bottom + gap + height > window.innerHeight - margin;
658
+ }
659
+ const top = flippedAbove ? Math.max(margin, rect.top - gap - height) : Math.min(Math.max(margin, rect.bottom + gap), Math.max(margin, window.innerHeight - margin - height));
630
660
  root.style.top = `${Math.round(top)}px`;
631
- root.style.left = `${Math.round(Math.min(rect.right, window.innerWidth - 8 - root.offsetWidth))}px`;
661
+ root.style.left = `${Math.round(Math.max(margin, Math.min(rect.right, window.innerWidth - margin - root.offsetWidth)))}px`;
662
+ };
663
+ let frame = 0;
664
+ const schedule = () => {
665
+ if (typeof requestAnimationFrame !== "function") {
666
+ position();
667
+ return;
668
+ }
669
+ if (frame !== 0) return;
670
+ frame = requestAnimationFrame(() => {
671
+ frame = 0;
672
+ position();
673
+ });
674
+ };
675
+ const onScroll = (event) => {
676
+ if (event.target instanceof Node && root.contains(event.target)) return;
677
+ schedule();
632
678
  };
633
679
  const onPointerDown = (event) => {
634
680
  const target = event.target;
@@ -639,19 +685,30 @@ function mountShell(root, anchor, onKeyDown) {
639
685
  document.addEventListener("pointerdown", onPointerDown);
640
686
  document.addEventListener("keydown", onKeyDown, true);
641
687
  }, 0);
688
+ document.body.append(root);
689
+ position();
690
+ const observer = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => {
691
+ schedule();
692
+ });
693
+ observer?.observe(root);
694
+ window.addEventListener("scroll", onScroll, true);
695
+ window.addEventListener("resize", schedule);
642
696
  const dispose = () => {
643
697
  clearTimeout(deferred);
644
698
  document.removeEventListener("pointerdown", onPointerDown);
645
699
  document.removeEventListener("keydown", onKeyDown, true);
700
+ window.removeEventListener("scroll", onScroll, true);
701
+ window.removeEventListener("resize", schedule);
702
+ observer?.disconnect();
703
+ if (frame !== 0) cancelAnimationFrame(frame);
646
704
  };
647
- document.body.append(root);
648
- position();
649
705
  return { position, dispose };
650
706
  }
651
707
  function openRetractPopover(opts) {
652
708
  closePopover();
653
709
  const { preview, anchor, t, retract, onRetract } = opts;
654
710
  if (retract === void 0 || onRetract === void 0) return;
711
+ markOpen(anchor);
655
712
  const root = el("div", CLASS.popover);
656
713
  root.setAttribute("role", "dialog");
657
714
  root.setAttribute("aria-label", t("popover.retract.title"));
@@ -711,6 +768,7 @@ function openPopover(opts) {
711
768
  const { session, seq, time, preview, anchor, t } = opts;
712
769
  const onRewind = opts.onRewind;
713
770
  if (seq === void 0 || time === void 0 || onRewind === void 0) return;
771
+ markOpen(anchor);
714
772
  const durableOpts = { session, seq, time, preview, anchor, t, onRewind };
715
773
  const root = el("div", CLASS.popover);
716
774
  root.setAttribute("role", "dialog");
@@ -718,11 +776,14 @@ function openPopover(opts) {
718
776
  let bothState = { state: "loading" };
719
777
  let impactOutcome = null;
720
778
  let step = "modes";
779
+ let reposition = () => {
780
+ };
721
781
  const renderModes = () => {
722
782
  step = "modes";
783
+ const previewText = preview.length > 0 ? preview : t("popover.noText");
723
784
  const children = [
724
785
  el("div", CLASS.popoverTitle, t("popover.title")),
725
- el("div", CLASS.popoverTarget, formatTarget(t, seq, time, preview)),
786
+ el("div", CLASS.popoverTarget, previewText),
726
787
  modeOption(t("popover.chat"), t("popover.chat.hint"), () => {
727
788
  closePopover();
728
789
  durableOpts.onRewind("chat");
@@ -741,20 +802,13 @@ function openPopover(opts) {
741
802
  if (bothState.state === "loading") option.disabled = true;
742
803
  children.push(option);
743
804
  }
744
- const actions = el("div", CLASS.popoverActions);
745
- const cancel = document.createElement("button");
746
- cancel.type = "button";
747
- cancel.className = CLASS.popoverGhost;
748
- cancel.textContent = t("popover.cancel");
749
- cancel.addEventListener("click", closePopover);
750
- actions.append(cancel);
751
- children.push(actions);
752
805
  root.replaceChildren(...children);
753
806
  focusFirst(root);
807
+ reposition();
754
808
  };
755
809
  const renderImpact = () => {
756
810
  step = "impact";
757
- renderImpactStep(root, durableOpts, renderModes, impactOutcome);
811
+ renderImpactStep(root, durableOpts, renderModes, reposition, impactOutcome);
758
812
  };
759
813
  const onKeyDown = (event) => {
760
814
  if (event.key === "ArrowDown") {
@@ -778,6 +832,7 @@ function openPopover(opts) {
778
832
  };
779
833
  renderModes();
780
834
  const shell = mountShell(root, anchor, onKeyDown);
835
+ reposition = shell.position;
781
836
  popoverEl = root;
782
837
  disposeOutside = shell.dispose;
783
838
  void (async () => {
@@ -789,17 +844,16 @@ function openPopover(opts) {
789
844
  bothState = { state: "error", message: outcome?.kind === "error" ? outcome.text : "preview request timed out" };
790
845
  }
791
846
  renderModes();
792
- shell.position();
793
847
  })().catch(() => {
794
848
  bothState = { state: "error", message: "unexpected error" };
795
849
  renderModes();
796
- shell.position();
797
850
  });
798
851
  }
799
852
 
800
853
  // src/client/portals.tsx
801
854
  var import_react = require("react");
802
855
  var import_react_dom = require("react-dom");
856
+ var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
803
857
 
804
858
  // src/client/pending.ts
805
859
  var QUEUE_PREVIEW_CHARS = 200;
@@ -1291,17 +1345,17 @@ function RewindButton({ target, sessionId, sessionOf, chatOf, watchChat, isMainV
1291
1345
  }
1292
1346
  });
1293
1347
  };
1294
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1348
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Tooltip, { label: t("button.title"), side: "bottom", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1295
1349
  "button",
1296
1350
  {
1297
1351
  type: "button",
1298
1352
  className: CLASS.button,
1299
1353
  "aria-label": t("button.aria"),
1300
- title: t("button.title"),
1354
+ "aria-haspopup": "dialog",
1301
1355
  onClick,
1302
1356
  dangerouslySetInnerHTML: { __html: REWIND_ICON_SVG }
1303
1357
  }
1304
- );
1358
+ ) });
1305
1359
  }
1306
1360
  function composerText() {
1307
1361
  const surface = composerSurface();
@@ -1343,17 +1397,17 @@ function RetractButton({ target, sessionId, sessionOf, setComposerText, t }) {
1343
1397
  }
1344
1398
  });
1345
1399
  };
1346
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1400
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Tooltip, { label: t("button.retract.title"), side: "bottom", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1347
1401
  "button",
1348
1402
  {
1349
1403
  type: "button",
1350
1404
  className: CLASS.button,
1351
1405
  "aria-label": t("button.retract.aria"),
1352
- title: t("button.retract.title"),
1406
+ "aria-haspopup": "dialog",
1353
1407
  onClick,
1354
1408
  dangerouslySetInnerHTML: { __html: REWIND_ICON_SVG }
1355
1409
  }
1356
- );
1410
+ ) });
1357
1411
  }
1358
1412
  function createRewindBridge(deps) {
1359
1413
  return function RewindBridge({ sessionId }) {
@@ -1421,9 +1475,9 @@ async function candidatesOverRpc(connection, sessionId) {
1421
1475
  }
1422
1476
 
1423
1477
  // src/client/build-info.ts
1424
- var PLUGIN_VERSION = true ? "0.18.0" : "dev";
1478
+ var PLUGIN_VERSION = true ? "0.18.1" : "dev";
1425
1479
  var PLUGIN_PACKAGE = "dsh-rewind-plugin";
1426
- var BUILD_HASH = true ? "18b65dfa" : "dev";
1480
+ var BUILD_HASH = true ? "8a3779b4" : "dev";
1427
1481
 
1428
1482
  // src/client/locales.ts
1429
1483
  var zh = {
@@ -1438,11 +1492,11 @@ var zh = {
1438
1492
  "popover.retract.hint": "\u64A4\u56DE\u8FD9\u6761\u53CA\u4E4B\u540E\u7684\u63D2\u8BDD\u6D88\u606F\uFF0C\u4E0D\u6253\u65AD\u5F53\u524D\u751F\u6210",
1439
1493
  "popover.retract.confirm": "\u786E\u8BA4\u56DE\u9000",
1440
1494
  "popover.chat": "\u4EC5\u56DE\u9000\u5BF9\u8BDD",
1441
- "popover.chat.hint": "\u53EA\u56DE\u9000\u6A21\u578B\u4E0A\u4E0B\u6587\uFF0C\u4E0D\u52A8\u5DE5\u4F5C\u533A\u6587\u4EF6",
1442
- "popover.both": "\u56DE\u9000\u5BF9\u8BDD\u548C\u4EE3\u7801",
1443
- "popover.both.hint": "\u5BF9\u8BDD\u56DE\u9000\u5E76\u8FD8\u539F\u5DE5\u4F5C\u533A\u6587\u4EF6",
1495
+ "popover.chat.hint": "\u4EC5\u56DE\u9000\u4E0A\u4E0B\u6587\uFF0C\u4E0D\u52A8\u5DE5\u4F5C\u533A\u6587\u4EF6",
1496
+ "popover.both": "\u56DE\u9000\u5BF9\u8BDD\u548C\u6587\u4EF6",
1497
+ "popover.both.hint": "\u56DE\u9000\u4E0A\u4E0B\u6587\u5E76\u8FD8\u539F\u5DE5\u4F5C\u533A\u6587\u4EF6",
1444
1498
  "popover.checking": "\u6B63\u5728\u68C0\u67E5\u6587\u4EF6\u53D8\u66F4\u2026",
1445
- "popover.noChanges": "\u6B64\u6D88\u606F\u4E4B\u540E\u6CA1\u6709\u53EF\u8FD8\u539F\u7684\u6587\u4EF6\u53D8\u66F4\uFF0C\u4EC5\u53EF\u56DE\u9000\u5BF9\u8BDD",
1499
+ "popover.noChanges": "\u65E0\u53EF\u8FD8\u539F\u7684\u6587\u4EF6\u53D8\u66F4",
1446
1500
  "popover.backupDisabled": "\u5FEB\u7167\u5907\u4EFD\u5DF2\u5173\u95ED\uFF0C\u8FD8\u539F\u53EF\u80FD\u4E0D\u5145\u5206",
1447
1501
  "popover.cancel": "\u53D6\u6D88",
1448
1502
  "popover.impact.loading": "\u6B63\u5728\u83B7\u53D6\u5F71\u54CD\u6E05\u5355\u2026",
@@ -1481,11 +1535,11 @@ var en = {
1481
1535
  "popover.retract.hint": "Retracts this pending message and the ones after it, without interrupting the run",
1482
1536
  "popover.retract.confirm": "Confirm rewind",
1483
1537
  "popover.chat": "Rewind conversation only",
1484
- "popover.chat.hint": "Cut the model context only; workspace files stay untouched",
1485
- "popover.both": "Rewind conversation and code",
1486
- "popover.both.hint": "Cut the context and restore workspace files",
1538
+ "popover.chat.hint": "Context only; workspace untouched",
1539
+ "popover.both": "Rewind conversation and files",
1540
+ "popover.both.hint": "Context and restore workspace files",
1487
1541
  "popover.checking": "Checking for file changes\u2026",
1488
- "popover.noChanges": "No tracked file changes after this message; conversation-only rewind",
1542
+ "popover.noChanges": "No restorable file changes",
1489
1543
  "popover.backupDisabled": "Snapshots off; the restore may be incomplete",
1490
1544
  "popover.cancel": "Cancel",
1491
1545
  "popover.impact.loading": "Fetching impact list\u2026",
@@ -1514,7 +1568,7 @@ var en = {
1514
1568
  };
1515
1569
 
1516
1570
  // src/client/settings-card.tsx
1517
- var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
1571
+ var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives");
1518
1572
  var import_jsx_runtime2 = require("react/jsx-runtime");
1519
1573
  var SETTINGS_ENTRY_ID = "dsh-rewind-plugin";
1520
1574
  var DEFAULT_MAX_AGE_DAYS = 30;
@@ -1542,10 +1596,10 @@ function formLabels(t) {
1542
1596
  };
1543
1597
  }
1544
1598
  function settingsForm(scope) {
1545
- const form = new import_dsh_client_ui_primitives.SettingsFormModel(scope, [
1599
+ const form = new import_dsh_client_ui_primitives2.SettingsFormModel(scope, [
1546
1600
  fileCheckpointingEnabledField(),
1547
1601
  autoCleanupEnabledField(),
1548
- (0, import_dsh_client_ui_primitives.settingsNumberField)("autoCleanupMaxAgeDays")
1602
+ (0, import_dsh_client_ui_primitives2.settingsNumberField)("autoCleanupMaxAgeDays")
1549
1603
  ]);
1550
1604
  return {
1551
1605
  form,
@@ -1565,7 +1619,7 @@ function SettingsCard(props) {
1565
1619
  const checkpointing = state.checkpointingEnabled.text === "true";
1566
1620
  const cleanupEnabled = state.enabled.text === "true";
1567
1621
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1568
- import_dsh_client_ui_primitives.SettingsForm,
1622
+ import_dsh_client_ui_primitives2.SettingsForm,
1569
1623
  {
1570
1624
  labels: formLabels(t),
1571
1625
  state,
@@ -1576,7 +1630,7 @@ function SettingsCard(props) {
1576
1630
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-rewind-settings-toggle-row", children: [
1577
1631
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-rewind-settings-toggle-label", children: t("backup.title") }),
1578
1632
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1579
- import_dsh_client_ui_primitives.Switch,
1633
+ import_dsh_client_ui_primitives2.Switch,
1580
1634
  {
1581
1635
  checked: checkpointing,
1582
1636
  label: t("backup.title"),
@@ -1594,7 +1648,7 @@ function SettingsCard(props) {
1594
1648
  /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "dsh-rewind-settings-toggle-row", children: [
1595
1649
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "dsh-rewind-settings-toggle-label", children: t("cleanup.auto") }),
1596
1650
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1597
- import_dsh_client_ui_primitives.Switch,
1651
+ import_dsh_client_ui_primitives2.Switch,
1598
1652
  {
1599
1653
  checked: cleanupEnabled,
1600
1654
  label: t("cleanup.auto"),
@@ -1608,7 +1662,7 @@ function SettingsCard(props) {
1608
1662
  /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("p", { className: "dsh-rewind-settings-hint", children: t(cleanupEnabled ? "cleanup.auto.on" : "cleanup.auto.off") })
1609
1663
  ] }),
1610
1664
  cleanupEnabled ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1611
- import_dsh_client_ui_primitives.SettingsValueField,
1665
+ import_dsh_client_ui_primitives2.SettingsValueField,
1612
1666
  {
1613
1667
  id: "dsh-rewind-cleanup-maxage",
1614
1668
  label: t("cleanup.maxAge"),
package/lib/index.js CHANGED
@@ -28,7 +28,7 @@ var en = {
28
28
  "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.",
29
29
  "success": "Withdrawn seq {targetSeq} and everything after it (conversation returned to earlier){restore}.",
30
30
  "noUserMessages": "This session has no rewindable user messages yet.",
31
- "command.description": "Rewind the conversation and/or code to a previous point",
31
+ "command.description": "Rewind the conversation and/or files to a previous point",
32
32
  "command.undoDescription": "Alias of /rewind",
33
33
  "engine.description": "dsh-rewind internal command",
34
34
  "engine.rejected": "Internal command: use /rewind or /undo.",
@@ -74,7 +74,7 @@ var zh = {
74
74
  "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",
75
75
  "success": "\u5DF2\u64A4\u56DE seq {targetSeq} \u53CA\u4E4B\u540E\u5185\u5BB9\uFF08\u5BF9\u8BDD\u5DF2\u56DE\u5230\u6B64\u524D\uFF09{restore}\u3002",
76
76
  "noUserMessages": "\u5F53\u524D\u4F1A\u8BDD\u8FD8\u6CA1\u6709\u53EF\u56DE\u9000\u7684\u7528\u6237\u6D88\u606F\u3002",
77
- "command.description": "\u56DE\u9000\u5BF9\u8BDD\u548C/\u6216\u4EE3\u7801\u5230\u4E4B\u524D\u7684\u67D0\u6761\u6D88\u606F",
77
+ "command.description": "\u628A\u5BF9\u8BDD\u548C/\u6216\u6587\u4EF6\u56DE\u9000\u5230\u4E4B\u524D\u7684\u67D0\u6761\u6D88\u606F",
78
78
  "command.undoDescription": "/rewind \u7684\u522B\u540D",
79
79
  "engine.description": "dsh-rewind \u5185\u90E8\u547D\u4EE4",
80
80
  "engine.rejected": "\u5185\u90E8\u547D\u4EE4\uFF1A\u8BF7\u4F7F\u7528 /rewind \u6216 /undo\u3002",
@@ -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: 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";
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@media (hover: hover) {\n .dsh-rewind-btn:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n }\n}\n\n.dsh-rewind-popover {\n position: fixed;\n z-index: 1100;\n box-sizing: border-box;\n width: min(300px, calc(100vw - 24px));\n padding: 16px;\n border: 0;\n border-radius: var(--dsw-radius-lg);\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 /* Elevated surface: match the menu cards' l2 scrollbar tokens. */\n --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);\n --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);\n box-shadow: var(--dsw-elevation-prominent);\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-popover-title {\n font-size: 12px;\n font-weight: 500;\n line-height: 18px;\n}\n.dsh-rewind-popover-target {\n margin: 2px 0 10px;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-all;\n /* The full message sits above the popover; two lines are enough context. */\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n overflow: hidden;\n}\n.dsh-rewind-popover-option {\n display: flex;\n flex-direction: column;\n gap: 2px;\n width: 100%;\n margin: 0 0 2px;\n padding: 6px 8px;\n border: 0;\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.4;\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 margin-top: 8px;\n}\n.dsh-rewind-popover-primary,\n.dsh-rewind-popover-ghost {\n height: 28px;\n padding: 0 10px;\n border: none;\n border-radius: 8px;\n font: inherit;\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.4;\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.dsh-rewind-popover-ghost:active {\n background: var(--dsw-alias-interactive-bg-active);\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";
@@ -4,8 +4,8 @@
4
4
  * lives in `src/client/`).
5
5
  *
6
6
  * Rewind mechanism: planning is pure (`src/rewind.ts`); execution appends a
7
- * marker `user/message` whose `surfaceOp` replaces every surface node after the
8
- * target with itself, cutting only the model-visible surface
7
+ * marker `user/message` whose `surfaceOp` replaces the target and every surface
8
+ * node after it with itself, cutting only the model-visible surface
9
9
  * (`docs/architecture.md`).
10
10
  *
11
11
  * File restore (mode `both`) follows Claude Code's checkpointing: unless
@@ -4,10 +4,10 @@
4
4
  * and the ordered surface, so this module stays unit-testable.
5
5
  *
6
6
  * Rewind semantics: rewinding to a user message appends a marker node into the
7
- * session log whose `surfaceOp` replaces every surface node AFTER the target
8
- * with itself. The log (audit trail, rendered transcript) is untouched; only
9
- * the model-visible surface is cut, so the next request derives its context
10
- * from the target message onward. The marker is a `user/message` carrying a
7
+ * session log whose `surfaceOp` replaces the target and every surface node
8
+ * after it with itself. The log (audit trail, rendered transcript) is
9
+ * untouched; only the model-visible surface is cut, so the next request
10
+ * derives its context from before the target message. The marker is a `user/message` carrying a
11
11
  * replace `surfaceOp` and the producer-owned source `{ kind: 'dsh-rewind' }`
12
12
  * (see `marker.ts`); the shape's invariants and history are documented in
13
13
  * `docs/architecture.md`.
@@ -51,13 +51,13 @@ export interface RewindCandidate {
51
51
  }
52
52
  /** A validated rewind: the target plus the exact surface range to shadow. */
53
53
  export interface RewindPlan {
54
- /** Target user message seq (stays on the surface). */
54
+ /** Target user message seq (withdrawn with the surface range). */
55
55
  readonly targetSeq: number;
56
56
  /** The target's ordered position in the surface. */
57
57
  readonly targetIndex: number;
58
- /** Ordered surface node seqs the rewind shadows (everything after the target). */
58
+ /** Ordered surface node seqs the rewind shadows (the target and everything after it). */
59
59
  readonly shadowedSeqs: readonly number[];
60
- /** First surface node after the target — the replace range start (inclusive). */
60
+ /** The target — the replace range start (inclusive). */
61
61
  readonly surfaceStart: number;
62
62
  /** Last surface node — the replace range end (inclusive). */
63
63
  readonly surfaceEnd: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-rewind-plugin",
3
- "version": "0.18.0",
3
+ "version": "0.18.1",
4
4
  "description": "In-window conversation rewind with workspace file restore · 同窗口内对话回退并可还原工作区文件",
5
5
  "icon": "./assets/icon.svg",
6
6
  "keywords": [
@@ -152,42 +152,42 @@
152
152
  "devDependencies": {
153
153
  "@deepseek-ai/cordis": "4.0.5",
154
154
  "@deepseek-ai/cosmokit": "1.8.6",
155
- "@deepseek-ai/dsh-agent": "0.2.1-rc.2",
156
- "@deepseek-ai/dsh-api-session-controller": "0.2.1-rc.2",
157
- "@deepseek-ai/dsh-attachment": "0.2.1-rc.2",
158
- "@deepseek-ai/dsh-client-connection": "0.2.1-rc.2",
159
- "@deepseek-ai/dsh-client-locale": "0.2.1-rc.2",
160
- "@deepseek-ai/dsh-client-store": "0.2.1-rc.2",
161
- "@deepseek-ai/dsh-client-ui-commands": "0.2.1-rc.2",
162
- "@deepseek-ai/dsh-client-ui-conversation": "0.2.1-rc.2",
163
- "@deepseek-ai/dsh-client-ui-input-trigger": "0.2.1-rc.2",
164
- "@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.1-rc.2",
165
- "@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.2",
166
- "@deepseek-ai/dsh-client-ui-session": "0.2.1-rc.2",
167
- "@deepseek-ai/dsh-client-ui-settings": "0.2.1-rc.2",
168
- "@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.2",
169
- "@deepseek-ai/dsh-command-compact": "0.2.1-rc.2",
170
- "@deepseek-ai/dsh-commands": "0.2.1-rc.2",
171
- "@deepseek-ai/dsh-compaction": "0.2.1-rc.2",
172
- "@deepseek-ai/dsh-compaction-basic": "0.2.1-rc.2",
173
- "@deepseek-ai/dsh-compaction-image-offload": "0.2.1-rc.2",
174
- "@deepseek-ai/dsh-fs": "0.2.1-rc.2",
175
- "@deepseek-ai/dsh-goal": "0.2.1-rc.2",
176
- "@deepseek-ai/dsh-home-paths": "0.2.1-rc.2",
177
- "@deepseek-ai/dsh-llm": "0.2.1-rc.2",
178
- "@deepseek-ai/dsh-plan-mode": "0.2.1-rc.2",
179
- "@deepseek-ai/dsh-sandbox": "0.2.1-rc.2",
180
- "@deepseek-ai/dsh-scope": "0.2.1-rc.2",
181
- "@deepseek-ai/dsh-session": "0.2.1-rc.2",
182
- "@deepseek-ai/dsh-session-projection": "0.2.1-rc.2",
183
- "@deepseek-ai/dsh-session-stats": "0.2.1-rc.2",
184
- "@deepseek-ai/dsh-session-title": "0.2.1-rc.2",
185
- "@deepseek-ai/dsh-settings": "0.2.1-rc.2",
186
- "@deepseek-ai/dsh-token-meter": "0.2.1-rc.2",
187
- "@deepseek-ai/dsh-tools": "0.2.1-rc.2",
188
- "@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.2",
189
- "@deepseek-ai/dsh-user-questions": "0.2.1-rc.2",
190
- "@deepseek-ai/dsh-working-directory": "0.2.1-rc.2",
155
+ "@deepseek-ai/dsh-agent": "0.2.1-rc.3",
156
+ "@deepseek-ai/dsh-api-session-controller": "0.2.1-rc.3",
157
+ "@deepseek-ai/dsh-attachment": "0.2.1-rc.3",
158
+ "@deepseek-ai/dsh-client-connection": "0.2.1-rc.3",
159
+ "@deepseek-ai/dsh-client-locale": "0.2.1-rc.3",
160
+ "@deepseek-ai/dsh-client-store": "0.2.1-rc.3",
161
+ "@deepseek-ai/dsh-client-ui-commands": "0.2.1-rc.3",
162
+ "@deepseek-ai/dsh-client-ui-conversation": "0.2.1-rc.3",
163
+ "@deepseek-ai/dsh-client-ui-input-trigger": "0.2.1-rc.3",
164
+ "@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.1-rc.3",
165
+ "@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.3",
166
+ "@deepseek-ai/dsh-client-ui-session": "0.2.1-rc.3",
167
+ "@deepseek-ai/dsh-client-ui-settings": "0.2.1-rc.3",
168
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.3",
169
+ "@deepseek-ai/dsh-command-compact": "0.2.1-rc.3",
170
+ "@deepseek-ai/dsh-commands": "0.2.1-rc.3",
171
+ "@deepseek-ai/dsh-compaction": "0.2.1-rc.3",
172
+ "@deepseek-ai/dsh-compaction-basic": "0.2.1-rc.3",
173
+ "@deepseek-ai/dsh-compaction-image-offload": "0.2.1-rc.3",
174
+ "@deepseek-ai/dsh-fs": "0.2.1-rc.3",
175
+ "@deepseek-ai/dsh-goal": "0.2.1-rc.3",
176
+ "@deepseek-ai/dsh-home-paths": "0.2.1-rc.3",
177
+ "@deepseek-ai/dsh-llm": "0.2.1-rc.3",
178
+ "@deepseek-ai/dsh-plan-mode": "0.2.1-rc.3",
179
+ "@deepseek-ai/dsh-sandbox": "0.2.1-rc.3",
180
+ "@deepseek-ai/dsh-scope": "0.2.1-rc.3",
181
+ "@deepseek-ai/dsh-session": "0.2.1-rc.3",
182
+ "@deepseek-ai/dsh-session-projection": "0.2.1-rc.3",
183
+ "@deepseek-ai/dsh-session-stats": "0.2.1-rc.3",
184
+ "@deepseek-ai/dsh-session-title": "0.2.1-rc.3",
185
+ "@deepseek-ai/dsh-settings": "0.2.1-rc.3",
186
+ "@deepseek-ai/dsh-token-meter": "0.2.1-rc.3",
187
+ "@deepseek-ai/dsh-tools": "0.2.1-rc.3",
188
+ "@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.3",
189
+ "@deepseek-ai/dsh-user-questions": "0.2.1-rc.3",
190
+ "@deepseek-ai/dsh-working-directory": "0.2.1-rc.3",
191
191
  "@deepseek-ai/schemastery": "3.18.5",
192
192
  "@types/node": "^24.0.0",
193
193
  "@types/react": "^18.3.31",