dsh-rewind-plugin 0.7.3 → 0.7.5

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
@@ -3,7 +3,7 @@
3
3
  Conversation rewind for DeepSeek Harness: **rewind the conversation to any earlier user message in one click, in the same window** — no new branch, no window switch, with optional workspace-file restore (full Claude Code `/rewind` semantics).
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/dsh-rewind-plugin.svg)](https://www.npmjs.com/package/dsh-rewind-plugin)
6
- [![npm downloads](https://img.shields.io/npm/dm/dsh-rewind-plugin.svg)](https://www.npmjs.com/package/dsh-rewind-plugin)
6
+ [![npm downloads](https://img.shields.io/npm/dt/dsh-rewind-plugin.svg)](https://www.npmjs.com/package/dsh-rewind-plugin)
7
7
  [![tests](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FSiriLee%2Fdb3b9260351c2b26eb3d201c2ed29df1%2Fraw%2Fbadge.json)](https://github.com/SiriLee/dsh-rewind/actions/workflows/ci.yml)
8
8
 
9
9
  > English | [中文](README.md)
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  DeepSeek Harness 插件:**一键就地回退对话到任意更早的用户消息**——同窗口内完成,不新建分支、不换窗口,可一并还原工作区文件(完整 Claude Code `/rewind` 语义)。
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/dsh-rewind-plugin.svg)](https://www.npmjs.com/package/dsh-rewind-plugin)
6
- [![npm downloads](https://img.shields.io/npm/dm/dsh-rewind-plugin.svg)](https://www.npmjs.com/package/dsh-rewind-plugin)
6
+ [![npm downloads](https://img.shields.io/npm/dt/dsh-rewind-plugin.svg)](https://www.npmjs.com/package/dsh-rewind-plugin)
7
7
  [![tests](https://img.shields.io/endpoint?url=https%3A%2F%2Fgist.githubusercontent.com%2FSiriLee%2Fdb3b9260351c2b26eb3d201c2ed29df1%2Fraw%2Fbadge.json)](https://github.com/SiriLee/dsh-rewind/actions/workflows/ci.yml)
8
8
 
9
9
  > [English](README.en.md) | 中文
@@ -7,30 +7,29 @@
7
7
  > compatibility invariants. A probe failure is a finding; it enters the
8
8
  > fix/pin/record loop.
9
9
  >
10
- > Targeted version: npm `@deepseek-ai/*@0.1.1-rc.2` (matches `package-lock.json`);
11
- > source reference: the `oss/deepseek-harness` local fork.
10
+ > Targeted version: npm `@deepseek-ai/*@0.1.1-rc.2` (matches `package-lock.json`).
11
+ > Source reference: the upstream [github.com/deepseek-ai/deepseek-harness](https://github.com/deepseek-ai/deepseek-harness).
12
12
  >
13
13
  > Version alignment: `peerDependencies` use an OR-union (e.g.
14
14
  > `^0.1.0-rc.6 || ^0.1.1-rc.2 || ^0.1.2-alpha.2`) covering each published tuple series. npm's
15
15
  > prerelease matching rules require a candidate to share the range comparator's
16
- > `[major, minor, patch]` tuple, so each new tuple series (e.g. a future
17
- > `0.1.2-rc.x`, `0.2.x`) requires appending a union member; rc rolling within a
18
- > tuple (`0.1.1-rc.2 → rc.3`) is a no-op. Signal: `npm view @deepseek-ai/dsh version`;
16
+ > `[major, minor, patch]` tuple, so appending is needed only for a genuinely new
17
+ > tuple (e.g. a future `0.2.x`); any prerelease within the SAME tuple (alpha rc)
18
+ > is a no-op. Signal: `npm view @deepseek-ai/dsh version`;
19
19
  > flow: `scripts/check-dsh-version.mjs` (it reads the `latest` dist-tag only; a
20
20
  > `-alpha` prerelease published under another tag, e.g. `0.1.2-alpha.2` under
21
21
  > `alpha`, is a manual pre-release check).
22
22
  >
23
23
  > `0.1.2-alpha.1` was never published to npm, so the peer OR-union is declared only from `0.1.2-alpha.2`; the plugin code still supports the `alpha.1`+ client.
24
24
  >
25
- > `0.1.2-alpha.2` … `0.1.2-alpha.4` verified-compatible. Built/tested on the rc.2
26
- > baseline (the `latest` dist-tag); the same published build also drives the alpha
27
- > line, and `alpha.2`/`alpha.3`/`alpha.4` share the `0.1.2` tuple, so the single
28
- > `^0.1.2-alpha.2` peer member covers all three. `rc.2` (`latest`) remains the
29
- > primary baseline; four versions verified: rc.2, alpha.2, alpha.3, alpha.4.
25
+ > `0.1.2-alpha.2` … `0.1.2-alpha.5` and `0.1.2-rc.1` verified-compatible; `rc.1` is
26
+ > source-identical to `alpha.5`. They share the `0.1.2` tuple, so the single
27
+ > `^0.1.2-alpha.2` peer member covers all six. `rc.2` (`latest`) stays the primary
28
+ > baseline; verified: rc.2, alpha.2, alpha.3, alpha.4, alpha.5, rc.1.
30
29
 
31
30
  ### Channeled seams (version × channel)
32
31
 
33
- | Seam | rc.2 (`0.1.1-rc.2`) | alpha.2 / alpha.3 | alpha.4 |
32
+ | Seam | rc.2 (`0.1.1-rc.2`) | alpha.2 / alpha.3 | alpha.4 / alpha.5 / rc.1 |
34
33
  | --- | --- | --- | --- |
35
34
  | Host session log (`eventsOf`) | `Session.events` | `Session.events` | `session.snapshotEvents()` |
36
35
  | Client chat snapshot (`chatSnapshotOf`) | session-face `chat` field | `uiConversation` `chat` view | `uiConversation` `chat` view |
@@ -21,7 +21,7 @@ the plugin itself uses (it never reads the DOM or host copy):
21
21
  ```ts
22
22
  import { hiddenSeqsOf, type HiddenChat } from 'dsh-rewind-plugin/client'
23
23
 
24
- const chat = session.getSnapshot().chat // rc.2 source; 0.1.2-alpha.1+ uses the uiConversation "chat" view
24
+ const chat = session.getSnapshot().chat // rc.2 source; 0.1.2-rc.1 uses the uiConversation "chat" view
25
25
  const hidden = hiddenSeqsOf(chat as HiddenChat) // Set<number> of anchor seqs
26
26
  ```
27
27
 
@@ -20,7 +20,7 @@
20
20
  ```ts
21
21
  import { hiddenSeqsOf, type HiddenChat } from 'dsh-rewind-plugin/client'
22
22
 
23
- const chat = session.getSnapshot().chat // rc.2 来源;0.1.2-alpha.1+ 用 uiConversation "chat" 视图
23
+ const chat = session.getSnapshot().chat // rc.2 来源;0.1.2-rc.1 用 uiConversation "chat" 视图
24
24
  const hidden = hiddenSeqsOf(chat as HiddenChat) // 被隐藏的 anchor seq 集合
25
25
  ```
26
26
 
@@ -1,8 +1,7 @@
1
1
  # DeepSeek Harness interface reference
2
2
 
3
3
  > Maintainer doc: the harness subsystems this plugin depends on, and the key
4
- > source files behind each interface. Local fork (if present):
5
- > `<workspace>/oss/deepseek-harness/` — official repo:
4
+ > source files behind each interface. Official repo:
6
5
  > [github.com/deepseek-ai/deepseek-harness](https://github.com/deepseek-ai/deepseek-harness)
7
6
 
8
7
  ## Subsystem docs (`docs/subsystems/`)
@@ -61,6 +61,9 @@ uses an OR-union covering every published tuple series
61
61
  (`0.1.1 → 0.1.2 → 0.2.x`); rc rolling within a tuple (`0.1.1-rc.2 → rc.3`)
62
62
  needs nothing. All `@deepseek-ai/*` packages release together;
63
63
  `npm view @deepseek-ai/dsh version` is the authoritative signal.
64
+ - **Exception — `@deepseek-ai/dsh-client-runtime`**: it never published a
65
+ `0.1.2-alpha.*` (npm `next` is `0.1.1-rc.2`) and is imported `import type`
66
+ only, so keep it at `^0.1.0-rc.6 || ^0.1.1-rc.2` (no `0.1.2` member).
64
67
  - **Published-tuple check (optional)**: `node scripts/check-dsh-version.mjs`
65
68
  compares the `latest` dist-tag version against the tuples the peers cover
66
69
  (exit 0 = nothing to do, exit 1 = update). It reads the `latest` tag only; a
@@ -55,6 +55,9 @@ OR 并集覆盖 DSH 已发布的每个元组系列(如 `^0.1.0-rc.6 || ^0.1.1-
55
55
  - **何时需要更新**:仅当 DSH 发布新元组(`0.1.1 → 0.1.2 → 0.2.x`)时;
56
56
  同元组内 rc 滚动(`0.1.1-rc.2 → rc.3`)无需动作。DSH 所有包同版本发布,
57
57
  `npm view @deepseek-ai/dsh version` 即权威信号。
58
+ - **例外 —— `@deepseek-ai/dsh-client-runtime`**:它从未发布 `0.1.2-alpha.*`
59
+ (npm `next` 为 `0.1.1-rc.2`)且仅被 `import type` 引用,保持
60
+ `^0.1.0-rc.6 || ^0.1.1-rc.2` 即可(无 `0.1.2` 元组项)。
58
61
  - **已发布元组检查(可选)**:`node scripts/check-dsh-version.mjs` 用 npm `latest`
59
62
  dist-tag 版本对比 peer 覆盖的元组(exit 0 无需动作,exit 1 需要)。它**只读
60
63
  `latest` tag**;发布在其它 tag(如 `alpha`)的 pre-release 走**手动发布前检查**
package/lib/client.js CHANGED
@@ -35,6 +35,13 @@ __export(index_exports, {
35
35
  module.exports = __toCommonJS(index_exports);
36
36
 
37
37
  // src/client/hidden.ts
38
+ function resolveChatWatch(resolveView, resolveFace, sessionId, cb) {
39
+ const view = resolveView(sessionId);
40
+ if (view?.subscribe !== void 0) return view.subscribe(cb);
41
+ const face = resolveFace(sessionId);
42
+ return face?.subscribe(cb) ?? (() => {
43
+ });
44
+ }
38
45
  function chatSnapshotOf(face, chatView) {
39
46
  const legacy = face?.getSnapshot().chat;
40
47
  if (legacy !== void 0) return legacy;
@@ -321,6 +328,12 @@ var STYLE = `
321
328
  }
322
329
 
323
330
  /* ---- Snapshot-cleanup settings card (mirrors the harness PluginCard look) ---- */
331
+ /* Dual-channel note: the harness card corner/border drifted across versions \u2014
332
+ 0.1.1-rc.2 uses border-radius 12px + 1px solid border-l2, while 0.1.2-rc.1
333
+ uses 16px + 0.5px border-l4 (and adds corner-shape: round). We intentionally
334
+ keep the 0.1.1-rc.2 values here for now; if the 0.1.2-rc.1 look is wanted,
335
+ switch these on the host version like the other dual-channel surfaces (see the
336
+ host-version probe in src/client/index.ts). */
324
337
  .dsh-rewind-cleanup-card {
325
338
  list-style: none;
326
339
  border: 1px solid var(--dsw-alias-border-l2);
@@ -619,7 +632,7 @@ function knownCommandSeqs(session, chatOf, match) {
619
632
  }
620
633
  return known;
621
634
  }
622
- function waitForCommand(session, chatOf, match, timeoutMs = 8e3) {
635
+ function waitForCommand(session, chatOf, match, timeoutMs = 8e3, watch) {
623
636
  return new Promise((resolve) => {
624
637
  let settled = false;
625
638
  const settle = (value) => {
@@ -635,7 +648,7 @@ function waitForCommand(session, chatOf, match, timeoutMs = 8e3) {
635
648
  settle({ kind: node.outcome.kind, text: node.outcome.text });
636
649
  }
637
650
  };
638
- const unsubscribe = session.subscribe(check);
651
+ const unsubscribe = (watch ?? ((cb) => session.subscribe(cb)))(check);
639
652
  const timer = setTimeout(() => settle(null), timeoutMs);
640
653
  check();
641
654
  });
@@ -644,11 +657,11 @@ function isPreviewFor(node, seq) {
644
657
  const args = node.args ?? "";
645
658
  return node.name === "rewind" && args.includes("preview") && new RegExp(`(?:^|\\s)@${seq}(?=\\s|$)`).test(args);
646
659
  }
647
- async function previewImpact(session, chatOf, seq) {
660
+ async function previewImpact(session, chatOf, seq, watch) {
648
661
  const known = knownCommandSeqs(session, chatOf, (node) => isPreviewFor(node, seq));
649
662
  const result = await session.command(`/rewind preview @${seq} both`);
650
663
  if (!result.ok || result.value?.matched !== true) return null;
651
- return waitForCommand(session, chatOf, (node) => isPreviewFor(node, seq) && !known.has(node.seq));
664
+ return waitForCommand(session, chatOf, (node) => isPreviewFor(node, seq) && !known.has(node.seq), 8e3, watch);
652
665
  }
653
666
  function el(tag2, className, text) {
654
667
  const node = document.createElement(tag2);
@@ -699,7 +712,7 @@ function renderImpactStep(root, opts, back, cached) {
699
712
  root.replaceChildren(impact, actions);
700
713
  focusFirst(root);
701
714
  void (async () => {
702
- const outcome = cached ?? await previewImpact(session, opts.chatOf, seq);
715
+ const outcome = cached ?? await previewImpact(session, opts.chatOf, seq, (cb) => opts.watchChat(session.sessionId, cb));
703
716
  if (outcome === null) {
704
717
  impact.textContent = t("popover.impact.failed", { message: "preview command failed or timed out" });
705
718
  return;
@@ -821,7 +834,7 @@ function openPopover(opts) {
821
834
  const { session, seq, time, preview, anchor, t, chatOf } = opts;
822
835
  const onRewind = opts.onRewind;
823
836
  if (seq === void 0 || time === void 0 || onRewind === void 0) return;
824
- const durableOpts = { session, seq, time, preview, anchor, t, chatOf, onRewind };
837
+ const durableOpts = { session, seq, time, preview, anchor, t, chatOf, watchChat: opts.watchChat, onRewind };
825
838
  const root = el("div", CLASS.popover);
826
839
  root.setAttribute("role", "dialog");
827
840
  root.setAttribute("aria-label", t("popover.title"));
@@ -891,7 +904,7 @@ function openPopover(opts) {
891
904
  popoverEl = root;
892
905
  disposeOutside = shell.dispose;
893
906
  void (async () => {
894
- const outcome = await previewImpact(session, chatOf, seq);
907
+ const outcome = await previewImpact(session, chatOf, seq, (cb) => opts.watchChat(session.sessionId, cb));
895
908
  impactOutcome = outcome;
896
909
  if (outcome !== null && outcome.kind === "success") {
897
910
  bothState = { state: hasFileImpact(outcome.text) ? "hasChanges" : "noChanges" };
@@ -1025,7 +1038,7 @@ function userNodeOf(chat, key) {
1025
1038
  if (node === void 0 || node.kind !== "user" && node.kind !== "steering") return void 0;
1026
1039
  return node.data;
1027
1040
  }
1028
- async function runRewindAndFill(session, seq, mode, currentSessionId, chatOf, setComposerText) {
1041
+ async function runRewindAndFill(session, seq, mode, currentSessionId, chatOf, watchChat, setComposerText) {
1029
1042
  const known = knownCommandSeqs(session, chatOf, (node) => isExecutedRewindCommand(node, seq));
1030
1043
  let result;
1031
1044
  try {
@@ -1039,7 +1052,7 @@ async function runRewindAndFill(session, seq, mode, currentSessionId, chatOf, se
1039
1052
  }
1040
1053
  let outcome;
1041
1054
  try {
1042
- outcome = await waitForCommand(session, chatOf, (node) => isExecutedRewindCommand(node, seq) && !known.has(node.seq), 2e4);
1055
+ outcome = await waitForCommand(session, chatOf, (node) => isExecutedRewindCommand(node, seq) && !known.has(node.seq), 2e4, (cb) => watchChat(session.sessionId, cb));
1043
1056
  } catch (error) {
1044
1057
  rewindLog.warn("refill", `waiting for rewind @${seq} outcome threw`, error);
1045
1058
  return;
@@ -1103,8 +1116,9 @@ function actionsContainerOf(row) {
1103
1116
  const root = row?.matches(ACTIONS_ROOT_SELECTOR) ? row : row?.querySelector(ACTIONS_ROOT_SELECTOR);
1104
1117
  const actions = root?.lastElementChild;
1105
1118
  if (actions instanceof HTMLElement && actions.querySelector("button") !== null) return actions;
1106
- const copy = row?.querySelector("button");
1107
- const structural = copy?.parentElement;
1119
+ const buttons = Array.from(row?.querySelectorAll("button") ?? []);
1120
+ const lastButton = buttons.filter((button) => !button.classList.contains(CLASS.button)).at(-1);
1121
+ const structural = lastButton?.parentElement;
1108
1122
  if (structural instanceof HTMLElement && structural.querySelector("button") !== null) return structural;
1109
1123
  return void 0;
1110
1124
  }
@@ -1169,7 +1183,7 @@ function sameTargets(left, right) {
1169
1183
  return other.kind === "pending" && target.itemId === other.itemId;
1170
1184
  });
1171
1185
  }
1172
- function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, t, subscribeLocale, setComposerText }) {
1186
+ function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, watchChat, t, subscribeLocale, setComposerText }) {
1173
1187
  const [targets, setTargets] = (0, import_react.useState)([]);
1174
1188
  const hidden = (0, import_react.useRef)(/* @__PURE__ */ new WeakSet());
1175
1189
  const [, forceRender] = (0, import_react.useReducer)((count) => count + 1, 0);
@@ -1230,6 +1244,7 @@ function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, t, subs
1230
1244
  sessionId,
1231
1245
  sessionOf,
1232
1246
  chatOf,
1247
+ watchChat,
1233
1248
  setComposerText,
1234
1249
  t
1235
1250
  },
@@ -1241,6 +1256,7 @@ function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, t, subs
1241
1256
  sessionId,
1242
1257
  sessionOf,
1243
1258
  chatOf,
1259
+ watchChat,
1244
1260
  currentSessionId,
1245
1261
  setComposerText,
1246
1262
  t
@@ -1251,7 +1267,7 @@ function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, t, subs
1251
1267
  target.key
1252
1268
  ));
1253
1269
  }
1254
- function RewindButton({ target, sessionId, sessionOf, chatOf, currentSessionId, setComposerText, t }) {
1270
+ function RewindButton({ target, sessionId, sessionOf, chatOf, watchChat, currentSessionId, setComposerText, t }) {
1255
1271
  const onClick = (event) => {
1256
1272
  event.stopPropagation();
1257
1273
  const session = sessionOf(sessionId);
@@ -1264,13 +1280,14 @@ function RewindButton({ target, sessionId, sessionOf, chatOf, currentSessionId,
1264
1280
  openPopover({
1265
1281
  session,
1266
1282
  chatOf,
1283
+ watchChat,
1267
1284
  seq: node.seq,
1268
1285
  time: node.time,
1269
1286
  preview: messagePreviewOf(node),
1270
1287
  anchor: event.currentTarget,
1271
1288
  t,
1272
1289
  onRewind: (mode) => {
1273
- void runRewindAndFill(session, node.seq, mode, currentSessionId, chatOf, setComposerText);
1290
+ void runRewindAndFill(session, node.seq, mode, currentSessionId, chatOf, watchChat, setComposerText);
1274
1291
  }
1275
1292
  });
1276
1293
  };
@@ -1303,7 +1320,7 @@ async function retractPending(session, itemId, text, setComposerText) {
1303
1320
  setComposerText(session.sessionId, text);
1304
1321
  }
1305
1322
  }
1306
- function RetractButton({ target, sessionId, sessionOf, chatOf, setComposerText, t }) {
1323
+ function RetractButton({ target, sessionId, sessionOf, chatOf, watchChat, setComposerText, t }) {
1307
1324
  const onClick = (event) => {
1308
1325
  event.stopPropagation();
1309
1326
  const session = sessionOf(sessionId);
@@ -1311,6 +1328,7 @@ function RetractButton({ target, sessionId, sessionOf, chatOf, setComposerText,
1311
1328
  openPopover({
1312
1329
  session,
1313
1330
  chatOf,
1331
+ watchChat,
1314
1332
  preview: target.preview,
1315
1333
  anchor: event.currentTarget,
1316
1334
  t,
@@ -1339,8 +1357,8 @@ function createRewindBridge(deps) {
1339
1357
  }
1340
1358
 
1341
1359
  // src/client/build-info.ts
1342
- var PLUGIN_VERSION = true ? "0.7.3" : "dev";
1343
- var BUILD_HASH = true ? "0752e2ff" : "dev";
1360
+ var PLUGIN_VERSION = true ? "0.7.5" : "dev";
1361
+ var BUILD_HASH = true ? "25e8b3ca" : "dev";
1344
1362
 
1345
1363
  // src/client/locales.ts
1346
1364
  var zh = {
@@ -1641,6 +1659,18 @@ function apply(ctx) {
1641
1659
  return false;
1642
1660
  }
1643
1661
  };
1662
+ const watchChat = (sessionId, cb) => resolveChatWatch(
1663
+ (id) => {
1664
+ try {
1665
+ return uiConversation()?.binding(id).target(CHAT_VIEW);
1666
+ } catch {
1667
+ return void 0;
1668
+ }
1669
+ },
1670
+ (id) => sessionOf(id),
1671
+ sessionId,
1672
+ cb
1673
+ );
1644
1674
  const slots = ctx.slots;
1645
1675
  yield slots.inject(HEADER_ACTIONS_SLOT, () => slots.register(
1646
1676
  {
@@ -1650,7 +1680,7 @@ function apply(ctx) {
1650
1680
  id: "dsh-rewind-portals",
1651
1681
  order: 1e3
1652
1682
  },
1653
- createRewindBridge({ sessionOf, chatOf, currentSessionId, setComposerText, t, subscribeLocale })
1683
+ createRewindBridge({ sessionOf, chatOf, currentSessionId, watchChat, setComposerText, t, subscribeLocale })
1654
1684
  ));
1655
1685
  const clientCtx = ctx;
1656
1686
  clientCtx.inject(["settingsScope"], (scoped) => {
@@ -1696,7 +1726,7 @@ function apply(ctx) {
1696
1726
  const known = knownCommandSeqs(face, chatOf2, (node) => isCandidateCommand(node));
1697
1727
  const result = await face.command("/rewind __candidates");
1698
1728
  if (!result.ok || result.value?.matched !== true) return void 0;
1699
- const outcome = await waitForCommand(face, chatOf2, (node) => isCandidateCommand(node) && !known.has(node.seq));
1729
+ const outcome = await waitForCommand(face, chatOf2, (node) => isCandidateCommand(node) && !known.has(node.seq), 8e3, (cb) => watchChat(face.sessionId, cb));
1700
1730
  if (outcome === null || outcome.kind !== "success" || outcome.text === void 0) return void 0;
1701
1731
  return rewindCandidatesFromHostText(outcome.text);
1702
1732
  };
@@ -1731,13 +1761,14 @@ function apply(ctx) {
1731
1761
  openPopover({
1732
1762
  session: face,
1733
1763
  chatOf,
1764
+ watchChat,
1734
1765
  seq: candidate.seq,
1735
1766
  time: candidate.time,
1736
1767
  preview: candidate.preview,
1737
1768
  anchor: composerAnchor(),
1738
1769
  t,
1739
1770
  onRewind: (mode) => {
1740
- void runRewindAndFill(face, candidate.seq, mode, currentSessionId, chatOf, setComposerText);
1771
+ void runRewindAndFill(face, candidate.seq, mode, currentSessionId, chatOf, watchChat, setComposerText);
1741
1772
  }
1742
1773
  });
1743
1774
  }
@@ -14,11 +14,12 @@ export interface HiddenChat {
14
14
  };
15
15
  }
16
16
  /**
17
- * Reader for one session's live chat snapshot. The dual channel hides the
18
- * harness split behind SiriLee/dsh-rewind#7: rc.2 serves the chat from the
19
- * session face snapshot, while 0.1.2-alpha.1+ serves it from the
20
- * `uiConversation` service's named "chat" view (contributed by
21
- * dsh-client-ui-chat through the uiSession slot hook).
17
+ * Reader for one session's live chat snapshot. This is one of the plugin's
18
+ * "0.1.1-rc.2 0.1.2-rc.1" dual channels: 0.1.1-rc.2 serves the chat from the
19
+ * session face snapshot, while 0.1.2-rc.1 (and the whole 0.1.2 line) serves it
20
+ * from the `uiConversation` service's named "chat" view (contributed by
21
+ * dsh-client-ui-chat through the uiSession slot hook). See
22
+ * SiriLee/dsh-rewind#7.
22
23
  */
23
24
  export type ChatOf = (session: {
24
25
  readonly sessionId: string;
@@ -26,13 +27,36 @@ export type ChatOf = (session: {
26
27
  chat?: unknown;
27
28
  };
28
29
  } | undefined) => HiddenChat | undefined;
30
+ /**
31
+ * Subscribe to one session's live chat-update signal, for waiting on a chat
32
+ * snapshot change without polling. Dual-channel, mirroring `chatOf`: the
33
+ * 0.1.2-rc.1 `uiConversation` "chat" view when registered, else the session
34
+ * face (0.1.1-rc.2, whose snapshot still carries `chat`, so its own
35
+ * `subscribe` is the chat-update signal). `cb` fires whenever the chat
36
+ * snapshot invalidates.
37
+ */
38
+ export type ChatWatch = (sessionId: string, cb: () => void) => () => void;
39
+ /**
40
+ * Choose the chat-update subscription for `waitForCommand`: prefer the
41
+ * `uiConversation` "chat" view's own `subscribe` when available (0.1.2-rc.1,
42
+ * where the chat-update signal lives), else the session face's `subscribe`
43
+ * (0.1.1-rc.2, whose snapshot still carries the chat, so its own subscribe is
44
+ * the chat-update signal). Extracted as a pure channel-selection step so the
45
+ * "view vs face" branch is unit-testable; the resolvers are injected by the
46
+ * caller (see `watchChat` in index.ts). Never throws.
47
+ */
48
+ export declare function resolveChatWatch(resolveView: (sessionId: string) => {
49
+ subscribe?(cb: () => void): () => void;
50
+ } | undefined, resolveFace: (sessionId: string) => {
51
+ subscribe(cb: () => void): () => void;
52
+ } | undefined, sessionId: string, cb: () => void): () => void;
29
53
  /**
30
54
  * Resolve the chat snapshot across the two harness channels: the session-face
31
- * snapshot first (rc.2 — on alpha.1+ the face no longer carries `chat`, so the
32
- * field reads `undefined`), then the `uiConversation` "chat" view. The view's
33
- * `getSnapshot()` returns undefined until the named view is registered, so
34
- * both channels missing degrades to `undefined` (no targets, no hiding —
35
- * never a crash).
55
+ * snapshot first (0.1.1-rc.2 — on 0.1.2-rc.1 the face no longer carries
56
+ * `chat`, so the field reads `undefined`), then the `uiConversation` "chat"
57
+ * view. The view's `getSnapshot()` returns undefined until the named view is
58
+ * registered, so both channels missing degrades to `undefined` (no targets, no
59
+ * hiding — never a crash).
36
60
  */
37
61
  export declare function chatSnapshotOf(face: {
38
62
  getSnapshot(): {
@@ -15,7 +15,7 @@
15
15
  */
16
16
  import type { SessionFace } from '@deepseek-ai/dsh-client-runtime/client';
17
17
  import type { CommandNode } from '@deepseek-ai/dsh-client-runtime/client';
18
- import { type ChatOf } from './hidden.ts';
18
+ import { type ChatOf, type ChatWatch } from './hidden.ts';
19
19
  import type { RewindKey } from './locales.ts';
20
20
  type Translate = (key: RewindKey, params?: Record<string, unknown>) => string;
21
21
  export interface PopoverOptions {
@@ -33,11 +33,18 @@ export interface PopoverOptions {
33
33
  readonly onRetract?: () => void;
34
34
  readonly preview: string;
35
35
  /**
36
- * Dual-channel chat reader (rc.2 session face / alpha.1+ uiConversation
37
- * view): the durable variant's command probes scan the chat through it.
38
- * Unused by the pending-retract variant.
36
+ * Dual-channel chat reader (0.1.1-rc.2 session face / 0.1.2-rc.1
37
+ * uiConversation view): the durable variant's command probes scan the chat
38
+ * through it. Unused by the pending-retract variant.
39
39
  */
40
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 (0.1.2-rc.1
44
+ * the session face no longer fires on a chat update). Passed straight through
45
+ * to `waitForCommand`.
46
+ */
47
+ readonly watchChat: ChatWatch;
41
48
  /** The button that opened the popover (outside-click ignore target). */
42
49
  readonly anchor: HTMLElement;
43
50
  readonly t: Translate;
@@ -63,7 +70,7 @@ export declare function knownCommandSeqs(session: SessionFace, chatOf: ChatOf, m
63
70
  * session snapshot (command/run + command/done land as one CommandNode).
64
71
  * @returns the outcome text-bearing node, or null on timeout.
65
72
  */
66
- export declare function waitForCommand(session: SessionFace, chatOf: ChatOf, match: (node: CommandNode) => boolean, timeoutMs?: number): Promise<{
73
+ export declare function waitForCommand(session: SessionFace, chatOf: ChatOf, match: (node: CommandNode) => boolean, timeoutMs?: number, watch?: (cb: () => void) => () => void): Promise<{
67
74
  kind: 'success' | 'error';
68
75
  text?: string;
69
76
  } | null>;
@@ -24,7 +24,7 @@
24
24
  */
25
25
  import { type ReactNode } from 'react';
26
26
  import type { SessionFace } from '@deepseek-ai/dsh-client-runtime/client';
27
- import { type ChatOf, type HiddenChat } from './hidden.ts';
27
+ import { type ChatOf, type ChatWatch, type HiddenChat } from './hidden.ts';
28
28
  import type { RewindKey } from './locales.ts';
29
29
  type Translate = (key: RewindKey, params?: Record<string, unknown>) => string;
30
30
  /** One portal target: the actions row of a user/steering seat + its durable node. */
@@ -53,19 +53,26 @@ export type PortalTarget = {
53
53
  export interface RewindBridgeDeps {
54
54
  readonly sessionOf: (sessionId: string) => SessionFace | undefined;
55
55
  /**
56
- * Dual-channel chat reader (rc.2 session face / alpha.1+ uiConversation
57
- * "chat" view): every chat snapshot read goes through it. See
56
+ * Dual-channel chat reader (0.1.1-rc.2 session face / 0.1.2-rc.1
57
+ * uiConversation "chat" view): every chat snapshot read goes through it. See
58
58
  * `chatSnapshotOf` in hidden.ts for the channel precedence.
59
59
  */
60
60
  readonly chatOf: ChatOf;
61
+ /**
62
+ * Subscribe to one session's live chat-update signal, so the composer refill
63
+ * waiting on an executed rewind's chat node can be woken when the chat
64
+ * snapshot changes (0.1.2-rc.1 the session face no longer fires on a chat
65
+ * update). Passed through to `waitForCommand`.
66
+ */
67
+ readonly watchChat: ChatWatch;
61
68
  readonly currentSessionId: () => string | undefined;
62
69
  readonly t: Translate;
63
70
  readonly subscribeLocale: (cb: () => void) => () => void;
64
71
  /**
65
72
  * Session-aware dual-channel composer writer (see `writeComposer`): the
66
- * alpha.1+ `conversation.input` facade `setDraft` when reachable, else the
67
- * rc.2/alpha.1 DOM fill. Session-scoped so the refill only lands in the
68
- * session that just rewound.
73
+ * 0.1.2-rc.1 `conversation.input` facade `setDraft` when reachable, else the
74
+ * 0.1.1-rc.2/0.1.2-rc.1 DOM fill. Session-scoped so the refill only lands in
75
+ * the session that just rewound.
69
76
  */
70
77
  readonly setComposerText: (sessionId: string, text: string) => boolean;
71
78
  }
@@ -83,7 +90,7 @@ export interface SlotsLike {
83
90
  }
84
91
  /** Join the text blocks of a user message into one plain preview. */
85
92
  /**
86
- * The alpha.1+ session input facade's write face (structural, so the plugin
93
+ * The 0.1.2-rc.1 session input facade's write face (structural, so the plugin
87
94
  * never imports the conversation UI package). `setDraft` replaces the whole
88
95
  * composer draft through the harness's own Lexical editor — the correct way
89
96
  * to restore the withdrawn text.
@@ -92,8 +99,8 @@ interface ComposerDraftWriter {
92
99
  setDraft(text: string): void;
93
100
  }
94
101
  /**
95
- * Fill the dsh composer with `text`. DOM dual-channel fallback: the rc.2
96
- * `<textarea>` path, then the alpha.1+ `contenteditable` path. Used by
102
+ * Fill the dsh composer with `text`. DOM dual-channel fallback: the 0.1.1-rc.2
103
+ * `<textarea>` path, then the 0.1.2-rc.1 `contenteditable` path. Used by
97
104
  * `setComposerText` (the harness-facade-aware writer) as the last-resort and
98
105
  * by `runRewindAndFill` to put the withdrawn target message back into the
99
106
  * composer after a rewind. Best-effort — no composer match means false,
@@ -102,12 +109,12 @@ interface ComposerDraftWriter {
102
109
  export declare function fillComposer(text: string): boolean;
103
110
  /**
104
111
  * Dual-channel composer write, mirroring `chatSnapshotOf`: prefer the harness
105
- * facade's `setDraft` (alpha.1+, correct whole-draft replace), then degrade
106
- * to the DOM `fillComposer` (rc.2 textarea / alpha.1 contenteditable). A
107
- * facade that throws (session teardown) is treated as absent so the DOM path
112
+ * facade's `setDraft` (0.1.2-rc.1, correct whole-draft replace), then degrade
113
+ * to the DOM `fillComposer` (0.1.1-rc.2 textarea / 0.1.2-rc.1 contenteditable).
114
+ * A facade that throws (session teardown) is treated as absent so the DOM path
108
115
  * still restores the text. Never throws.
109
116
  * @param text - the withdrawn target message text.
110
- * @param facade - the alpha.1+ session input draft writer, when reachable.
117
+ * @param facade - the 0.1.2-rc.1 session input draft writer, when reachable.
111
118
  * @returns whether a channel applied the text.
112
119
  */
113
120
  export declare function writeComposer(text: string, facade: ComposerDraftWriter | undefined): boolean;
@@ -124,22 +131,25 @@ export declare function writeComposer(text: string, facade: ComposerDraftWriter
124
131
  * the old baseline heuristic refilled withdrawn text into the composer
125
132
  * after switching sessions or restarting dsh.
126
133
  */
127
- export declare function runRewindAndFill(session: SessionFace, seq: number, mode: 'chat' | 'both', currentSessionId: () => string | undefined, chatOf: ChatOf, setComposerText: (sessionId: string, text: string) => boolean): Promise<void>;
134
+ export declare function runRewindAndFill(session: SessionFace, seq: number, mode: 'chat' | 'both', currentSessionId: () => string | undefined, chatOf: ChatOf, watchChat: ChatWatch, setComposerText: (sessionId: string, text: string) => boolean): Promise<void>;
128
135
  /**
129
136
  * Locate the actions container of a user/steering seat row — the element the
130
137
  * ↶ button portals into (the copy/branch IconActions row).
131
138
  *
132
- * Dual channel:
133
- * - rc.2 (0.1.1-rc.2) / alpha.2+ (0.1.2-alpha.2): the hover-reveal root is
134
- * attribute-marked (`[data-time-hover-root]` rc.x, `[data-actions-reveal]`
135
- * alpha.2+) and mounts the actions row as its LAST child. A pending row IS
136
- * that root; a durable row CONTAINS it as a descendant.
137
- * - alpha.4 (0.1.2-alpha.4): the marker was removed from user rows (it now
138
- * lives only on the per-turn tail footer, `TurnTailNodeView`), and the user
139
- * action row is revealed via CSS `:has()`. The container is instead located
140
- * structurally: the direct holder of the copy `<button>` (the
141
- * `MessageIconActions` container, which mounts that button as a direct
142
- * child — `MessageIconActions.tsx:83,86`).
139
+ * @dualmode 0.1.1-rc.2 ↔ 0.1.2-rc.1
140
+ * - 0.1.1-rc.2: the hover-reveal root is attribute-marked
141
+ * `[data-time-hover-root]` and mounts the actions row as its LAST child. A
142
+ * pending row IS that root; a durable row CONTAINS it as a descendant.
143
+ * - 0.1.2-rc.1: the marker was removed from user rows (it now lives only on the
144
+ * per-turn tail footer, `TurnTailNodeView`), and the user action row is
145
+ * revealed via CSS `:has()`. The container is instead located structurally:
146
+ * the direct holder of the copy `<button>` (the `MessageIconActions`
147
+ * container, which mounts that button as a direct child —
148
+ * `MessageIconActions.tsx:83,86`).
149
+ *
150
+ * The 0.1.2-alpha.2/3 `[data-actions-reveal]` attribute-marked rows (see
151
+ * `ACTIONS_ROOT_SELECTOR`) are an intermediate state absorbed by the
152
+ * attribute branch below; they are not needed by either endpoint.
143
153
  *
144
154
  * Returns undefined when no qualifying container is found; the caller refuses
145
155
  * to portal (never a crash, never a wrong attachment). Exported as a test seam
@@ -163,10 +173,11 @@ interface RewindPortalsProps extends RewindBridgeDeps {
163
173
  * skipped when the target set is unchanged), so the plugin never runs a
164
174
  * synchronous full-transcript scan inside a commit microtask.
165
175
  */
166
- export declare function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, t, subscribeLocale, setComposerText }: RewindPortalsProps): ReactNode;
167
- /** The current composer draft, on whichever channel: rc.2 textarea `.value`,
168
- * alpha.1+ contenteditable `textContent`. Empty when the composer is absent.
169
- * Exported as a test seam (the empty-composer guard in `retractPending`). */
176
+ export declare function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, watchChat, t, subscribeLocale, setComposerText }: RewindPortalsProps): ReactNode;
177
+ /** The current composer draft, on whichever channel: 0.1.1-rc.2 textarea
178
+ * `.value`, 0.1.2-rc.1 contenteditable `textContent`. Empty when the composer
179
+ * is absent. Exported as a test seam (the empty-composer guard in
180
+ * `retractPending`). */
170
181
  export declare function composerText(): string;
171
182
  /**
172
183
  * Build the slot-entry component for the plugin apply(): a tiny bridge that
@@ -11,10 +11,11 @@
11
11
  * host schema enforces). "Discard changes" restores the last-read baseline.
12
12
  *
13
13
  * It neither imports the client settings typed contract nor depends on the
14
- * alpha-only `mutate` write API: it reads `getSnapshot().value` and writes via
15
- * the `set(field, value)` method present on both rc.2 and alpha, and the card
16
- * receives a tiny structural `CleanupCardApi` supplied by `src/client/index.ts`
17
- * so the component stays harness-agnostic and unit-testable in isolation.
14
+ * 0.1.2-rc.1-only `mutate` write API: it reads `getSnapshot().value` and writes
15
+ * via the `set(field, value)` method present on both 0.1.1-rc.2 and 0.1.2-rc.1,
16
+ * and the card receives a tiny structural `CleanupCardApi` supplied by
17
+ * `src/client/index.ts` so the component stays harness-agnostic and
18
+ * unit-testable in isolation.
18
19
  *
19
20
  * @module dsh-rewind/client/settings-card
20
21
  */
@@ -23,4 +23,4 @@ export declare const CLASS: {
23
23
  /** The ↶ glyph, drawn inline so the bundle stays dependency-free. */
24
24
  export declare const REWIND_ICON_SVG: string;
25
25
  /** One injected stylesheet (scoped under `.dsh-rewind-*`). */
26
- export declare const STYLE = "\n.dsh-rewind-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n padding: 6px;\n border: none;\n border-radius: 28px;\n background: transparent;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\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.dsh-rewind-popover-option:disabled {\n opacity: 0.5;\n cursor: default;\n}\n.dsh-rewind-popover-option-label {\n font-weight: 500;\n}\n.dsh-rewind-popover-option-hint {\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-popover-impact {\n margin: 4px 0 10px;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-interactive-bg-hover);\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-secondary);\n white-space: pre-wrap;\n max-height: 160px;\n overflow: auto;\n}\n.dsh-rewind-popover-actions {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n}\n.dsh-rewind-popover-primary,\n.dsh-rewind-popover-ghost {\n padding: 5px 12px;\n border: none;\n border-radius: 8px;\n font: inherit;\n font-size: 13px;\n line-height: 18px;\n cursor: pointer;\n}\n.dsh-rewind-popover-primary {\n background: var(--dsw-alias-button-primary-fill);\n color: var(--dsw-alias-label-primary-foreground);\n}\n.dsh-rewind-popover-primary:hover:not(:disabled) {\n background: var(--dsw-alias-button-primary-hover);\n}\n.dsh-rewind-popover-primary:disabled {\n opacity: 0.5;\n cursor: default;\n}\n.dsh-rewind-popover-ghost {\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-popover-ghost:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dsh-rewind-guard-hint {\n position: fixed;\n z-index: 1000;\n max-width: min(440px, calc(100vw - 24px));\n padding: 8px 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 10px;\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n box-shadow: var(--dsw-shadow-lv3);\n font-size: 13px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n pointer-events: none;\n}\n\n/* ---- Snapshot-cleanup settings card (mirrors the harness PluginCard look) ---- */\n.dsh-rewind-cleanup-card {\n list-style: none;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\n background: var(--dsw-alias-bg-layer-3);\n transition: border-color .16s, background .16s;\n}\n.dsh-rewind-cleanup-card:hover {\n border-color: var(--dsw-alias-label-dimmed);\n}\n.dsh-rewind-cleanup-card-open {\n background: var(--dsw-alias-bg-layer-2);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.dsh-rewind-cleanup-header {\n width: 100%;\n appearance: none;\n border: 0;\n background: none;\n font: inherit;\n color: inherit;\n text-align: left;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 14px 16px;\n border-radius: 12px;\n}\n.dsh-rewind-cleanup-header:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n.dsh-rewind-cleanup-head-text {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.dsh-rewind-cleanup-name {\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-desc {\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-cleanup-chevron {\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n transition: transform .16s;\n}\n.dsh-rewind-cleanup-chevron-open {\n transform: rotate(180deg);\n}\n.dsh-rewind-cleanup-pending {\n flex: none;\n border-radius: 999px;\n padding: 1px 8px;\n font-size: 11px;\n line-height: 17px;\n font-weight: 500;\n white-space: nowrap;\n background: var(--dsw-alias-bg-module-platform);\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-cleanup-body {\n border-top: 1px solid var(--dsw-alias-border-l2);\n margin: 0 16px;\n padding: 4px 0 8px;\n}\n.dsh-rewind-cleanup-readonly {\n margin: 12px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-cleanup-permission {\n display: grid;\n gap: 6px;\n padding: 12px 0;\n}\n.dsh-rewind-cleanup-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 12px 0;\n}\n.dsh-rewind-cleanup-field + .dsh-rewind-cleanup-field {\n border-top: 1px solid var(--dsw-alias-border-l2);\n}\n.dsh-rewind-cleanup-head {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.dsh-rewind-cleanup-label {\n flex: 1;\n min-width: 0;\n font-size: 13px;\n font-weight: 500;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-hint {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-cleanup-error {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-error);\n}\n/* Switch row: label left, role=switch button right, hint below (Subagent module). */\n.dsh-rewind-cleanup-toggle-row {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 16px;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-toggle-label {\n flex: 1;\n min-width: 0;\n}\n.dsh-rewind-cleanup-switch {\n box-sizing: border-box;\n position: relative;\n flex: 0 0 auto;\n width: 36px;\n height: 20px;\n padding: 2px;\n border: 0;\n border-radius: 10px;\n background: var(--dsw-alias-border-l3);\n cursor: pointer;\n}\n.dsh-rewind-cleanup-switch-on {\n background: var(--dsw-alias-brand-primary);\n}\n.dsh-rewind-cleanup-switch:disabled {\n cursor: default;\n opacity: 0.5;\n}\n.dsh-rewind-cleanup-switch:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n}\n.dsh-rewind-cleanup-thumb {\n display: block;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n corner-shape: round;\n background: var(--dsw-alias-label-primary-foreground);\n transition: transform 120ms ease;\n}\n.dsh-rewind-cleanup-switch-on .dsh-rewind-cleanup-thumb {\n transform: translateX(16px);\n}\n.dsh-rewind-cleanup-input {\n box-sizing: border-box;\n height: 34px;\n padding: 0 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-input:focus-visible {\n outline: none;\n border-color: var(--dsw-alias-brand-primary);\n}\n.dsh-rewind-cleanup-input:disabled {\n color: var(--dsw-alias-label-tertiary);\n cursor: default;\n}\n.dsh-rewind-cleanup-input-invalid {\n border-color: var(--dsw-alias-label-error);\n}\n.dsh-rewind-cleanup-footer {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 12px 0 4px;\n border-top: 1px solid var(--dsw-alias-border-l2);\n}\n.dsh-rewind-cleanup-failed {\n flex: 1;\n min-width: 0;\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-error);\n}\n.dsh-rewind-cleanup-discard,\n.dsh-rewind-cleanup-save {\n appearance: none;\n border: 1px solid transparent;\n border-radius: 8px;\n padding: 5px 14px;\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n cursor: pointer;\n}\n.dsh-rewind-cleanup-discard {\n border-color: var(--dsw-alias-border-l2);\n background: none;\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-cleanup-discard:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.dsh-rewind-cleanup-save {\n background: var(--dsw-alias-label-primary);\n color: var(--dsw-alias-bg-layer-3);\n}\n.dsh-rewind-cleanup-discard:disabled,\n.dsh-rewind-cleanup-save:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.dsh-rewind-cleanup-discard:focus-visible,\n.dsh-rewind-cleanup-save:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n";
26
+ export declare const STYLE = "\n.dsh-rewind-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n padding: 6px;\n border: none;\n border-radius: 28px;\n background: transparent;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\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.dsh-rewind-popover-option:disabled {\n opacity: 0.5;\n cursor: default;\n}\n.dsh-rewind-popover-option-label {\n font-weight: 500;\n}\n.dsh-rewind-popover-option-hint {\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-popover-impact {\n margin: 4px 0 10px;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-interactive-bg-hover);\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-secondary);\n white-space: pre-wrap;\n max-height: 160px;\n overflow: auto;\n}\n.dsh-rewind-popover-actions {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n}\n.dsh-rewind-popover-primary,\n.dsh-rewind-popover-ghost {\n padding: 5px 12px;\n border: none;\n border-radius: 8px;\n font: inherit;\n font-size: 13px;\n line-height: 18px;\n cursor: pointer;\n}\n.dsh-rewind-popover-primary {\n background: var(--dsw-alias-button-primary-fill);\n color: var(--dsw-alias-label-primary-foreground);\n}\n.dsh-rewind-popover-primary:hover:not(:disabled) {\n background: var(--dsw-alias-button-primary-hover);\n}\n.dsh-rewind-popover-primary:disabled {\n opacity: 0.5;\n cursor: default;\n}\n.dsh-rewind-popover-ghost {\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-popover-ghost:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dsh-rewind-guard-hint {\n position: fixed;\n z-index: 1000;\n max-width: min(440px, calc(100vw - 24px));\n padding: 8px 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 10px;\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n box-shadow: var(--dsw-shadow-lv3);\n font-size: 13px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n pointer-events: none;\n}\n\n/* ---- Snapshot-cleanup settings card (mirrors the harness PluginCard look) ---- */\n/* Dual-channel note: the harness card corner/border drifted across versions \u2014\n 0.1.1-rc.2 uses border-radius 12px + 1px solid border-l2, while 0.1.2-rc.1\n uses 16px + 0.5px border-l4 (and adds corner-shape: round). We intentionally\n keep the 0.1.1-rc.2 values here for now; if the 0.1.2-rc.1 look is wanted,\n switch these on the host version like the other dual-channel surfaces (see the\n host-version probe in src/client/index.ts). */\n.dsh-rewind-cleanup-card {\n list-style: none;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\n background: var(--dsw-alias-bg-layer-3);\n transition: border-color .16s, background .16s;\n}\n.dsh-rewind-cleanup-card:hover {\n border-color: var(--dsw-alias-label-dimmed);\n}\n.dsh-rewind-cleanup-card-open {\n background: var(--dsw-alias-bg-layer-2);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.dsh-rewind-cleanup-header {\n width: 100%;\n appearance: none;\n border: 0;\n background: none;\n font: inherit;\n color: inherit;\n text-align: left;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 14px 16px;\n border-radius: 12px;\n}\n.dsh-rewind-cleanup-header:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n.dsh-rewind-cleanup-head-text {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.dsh-rewind-cleanup-name {\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-desc {\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-cleanup-chevron {\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n transition: transform .16s;\n}\n.dsh-rewind-cleanup-chevron-open {\n transform: rotate(180deg);\n}\n.dsh-rewind-cleanup-pending {\n flex: none;\n border-radius: 999px;\n padding: 1px 8px;\n font-size: 11px;\n line-height: 17px;\n font-weight: 500;\n white-space: nowrap;\n background: var(--dsw-alias-bg-module-platform);\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-cleanup-body {\n border-top: 1px solid var(--dsw-alias-border-l2);\n margin: 0 16px;\n padding: 4px 0 8px;\n}\n.dsh-rewind-cleanup-readonly {\n margin: 12px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-cleanup-permission {\n display: grid;\n gap: 6px;\n padding: 12px 0;\n}\n.dsh-rewind-cleanup-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 12px 0;\n}\n.dsh-rewind-cleanup-field + .dsh-rewind-cleanup-field {\n border-top: 1px solid var(--dsw-alias-border-l2);\n}\n.dsh-rewind-cleanup-head {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.dsh-rewind-cleanup-label {\n flex: 1;\n min-width: 0;\n font-size: 13px;\n font-weight: 500;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-hint {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-cleanup-error {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-error);\n}\n/* Switch row: label left, role=switch button right, hint below (Subagent module). */\n.dsh-rewind-cleanup-toggle-row {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 16px;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-toggle-label {\n flex: 1;\n min-width: 0;\n}\n.dsh-rewind-cleanup-switch {\n box-sizing: border-box;\n position: relative;\n flex: 0 0 auto;\n width: 36px;\n height: 20px;\n padding: 2px;\n border: 0;\n border-radius: 10px;\n background: var(--dsw-alias-border-l3);\n cursor: pointer;\n}\n.dsh-rewind-cleanup-switch-on {\n background: var(--dsw-alias-brand-primary);\n}\n.dsh-rewind-cleanup-switch:disabled {\n cursor: default;\n opacity: 0.5;\n}\n.dsh-rewind-cleanup-switch:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n}\n.dsh-rewind-cleanup-thumb {\n display: block;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n corner-shape: round;\n background: var(--dsw-alias-label-primary-foreground);\n transition: transform 120ms ease;\n}\n.dsh-rewind-cleanup-switch-on .dsh-rewind-cleanup-thumb {\n transform: translateX(16px);\n}\n.dsh-rewind-cleanup-input {\n box-sizing: border-box;\n height: 34px;\n padding: 0 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-input:focus-visible {\n outline: none;\n border-color: var(--dsw-alias-brand-primary);\n}\n.dsh-rewind-cleanup-input:disabled {\n color: var(--dsw-alias-label-tertiary);\n cursor: default;\n}\n.dsh-rewind-cleanup-input-invalid {\n border-color: var(--dsw-alias-label-error);\n}\n.dsh-rewind-cleanup-footer {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 12px 0 4px;\n border-top: 1px solid var(--dsw-alias-border-l2);\n}\n.dsh-rewind-cleanup-failed {\n flex: 1;\n min-width: 0;\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-error);\n}\n.dsh-rewind-cleanup-discard,\n.dsh-rewind-cleanup-save {\n appearance: none;\n border: 1px solid transparent;\n border-radius: 8px;\n padding: 5px 14px;\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n cursor: pointer;\n}\n.dsh-rewind-cleanup-discard {\n border-color: var(--dsw-alias-border-l2);\n background: none;\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-cleanup-discard:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.dsh-rewind-cleanup-save {\n background: var(--dsw-alias-label-primary);\n color: var(--dsw-alias-bg-layer-3);\n}\n.dsh-rewind-cleanup-discard:disabled,\n.dsh-rewind-cleanup-save:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.dsh-rewind-cleanup-discard:focus-visible,\n.dsh-rewind-cleanup-save:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n";
@@ -1,16 +1,17 @@
1
1
  /**
2
2
  * Cross-channel session event-log reader.
3
3
  *
4
- * Harness 0.1.1-rc.2 exposes the full log as `Session.events`
5
- * (`get events(): readonly SessionEvent[]`), while 0.1.2-alpha.4 removed that
4
+ * This is a "0.1.1-rc.2 0.1.2-rc.1" dual-channel reader. Harness 0.1.1-rc.2
5
+ * exposes the full log as `Session.events` (`get events(): readonly
6
+ * SessionEvent[]`); the 0.1.2 line (alpha.4+, now 0.1.2-rc.1) removed that
6
7
  * member and replaced it with the on-demand APIs `snapshotEvents(from?, to?)`
7
8
  * (half-open range, internally cached), `eventAt(seq)`, `ownEvents()` and
8
9
  * `seq`. This module reads the full log through whichever channel the host
9
- * exposes, so the plugin keeps one code path across both.
10
+ * exposes, so the plugin keeps one code path across both endpoints.
10
11
  *
11
12
  * `snapshotEvents()` is deliberately preferred over `ownEvents()`:
12
13
  * `snapshotEvents()` defaults to the whole log (including any fork-inherited
13
- * prefix), which is exactly what `Session.events` returned on rc.2 — the
14
+ * prefix), which is exactly what `Session.events` returned on 0.1.1-rc.2 — the
14
15
  * rewind semantics are preserved for fork-seeded sessions. `ownEvents()`
15
16
  * would drop the inherited prefix and change behaviour.
16
17
  *
@@ -20,15 +21,17 @@ import type { Session, SessionEvent } from '@deepseek-ai/dsh-session';
20
21
  /**
21
22
  * Read the full event log of a session, transparently across harness channels:
22
23
  * - 0.1.1-rc.2 (`Session.events`): the live immutable log array.
23
- * - 0.1.2-alpha.4 (`Session.snapshotEvents()`): a deep-frozen snapshot, cached
24
- * by the session until the next append.
24
+ * - 0.1.2-rc.1 (`Session.snapshotEvents()`): a deep-frozen snapshot, cached
25
+ * by the session until the next append. This is the single 0.1.2-line
26
+ * channel (alpha.2/3 still exposed `events` and thus fall into the
27
+ * 0.1.1-rc.2 branch below).
25
28
  *
26
29
  * Falls back to an empty log when the session exposes neither member (a future
27
30
  * harness shape) rather than throwing — callers already treat the empty log as
28
31
  * "no candidates".
29
32
  *
30
33
  * @param session - the session to read.
31
- * @returns the full event log in log order (the same shape rc.2's `events`
34
+ * @returns the full event log in log order (the same shape 0.1.1-rc.2's `events`
32
35
  * returned, so existing consumers are unchanged).
33
36
  */
34
37
  export declare function eventsOf(session: Session): readonly SessionEvent[];
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * Version-neutral settings-namespace reading for the host half.
3
3
  *
4
- * DSH rc.2 exposes a `settingsNamespace(value)` brand helper, and `settings.get`
5
- * is typed to require a branded `SettingsNamespace`. DSH 0.1.2-alpha.2 removed
4
+ * This is a "0.1.1-rc.2 0.1.2-rc.1" dual channel. DSH 0.1.1-rc.2 exposes a
5
+ * `settingsNamespace(value)` brand helper, and `settings.get` is typed to
6
+ * require a branded `SettingsNamespace`. The 0.1.2 line (0.1.2-rc.1) removed
6
7
  * that helper (the `SettingsNamespace` *type* remains) and `settings.get`
7
8
  * accepts the raw namespace string. This module collapses both into one runtime
8
- * call: on rc.2 the brand is a compile-time marker erased at runtime (so
9
- * `settingsNamespace(ns)` returns `ns`), on 0.1.2-alpha.2 the brand helper is
9
+ * call: on 0.1.1-rc.2 the brand is a compile-time marker erased at runtime (so
10
+ * `settingsNamespace(ns)` returns `ns`), on 0.1.2-rc.1 the brand helper is
10
11
  * absent and the raw `ns` string is used directly. That is what lets a single
11
12
  * compiled host bundle link and run on both harness generations.
12
13
  *
@@ -16,17 +17,17 @@
16
17
  export interface SettingsProviderLike {
17
18
  /**
18
19
  * Read one registered settings section by namespace. Accepts whatever the
19
- * running DSH generation passes: a branded `SettingsNamespace` (rc.2, brand
20
- * erased) or the raw namespace string (0.1.2-alpha.2).
20
+ * running DSH generation passes: a branded `SettingsNamespace` (0.1.1-rc.2,
21
+ * brand erased) or the raw namespace string (0.1.2-rc.1).
21
22
  */
22
23
  get(ns: string): unknown;
23
24
  }
24
- /** The `settingsNamespace(ns)` brand helper, or `undefined` when removed (0.1.2-alpha.2). */
25
+ /** The `settingsNamespace(ns)` brand helper, or `undefined` when removed (0.1.2-rc.1). */
25
26
  export type SettingsNamespaceBrand = ((value: string) => string) | undefined;
26
27
  /**
27
28
  * Read one settings section keyed by `ns`, tolerant of the settings-namespace
28
- * brand across DSH generations. Pass the brand helper when available (the rc.2
29
- * path); it is `undefined` on 0.1.2-alpha.2, where the raw `ns` string is used
30
- * directly. Never throws: an absent section simply returns `undefined`.
29
+ * brand across DSH generations. Pass the brand helper when available (the
30
+ * 0.1.1-rc.2 path); it is `undefined` on 0.1.2-rc.1, where the raw `ns` string
31
+ * is used directly. Never throws: an absent section simply returns `undefined`.
31
32
  */
32
33
  export declare function readSettingsSection(provider: SettingsProviderLike, ns: string, brand: SettingsNamespaceBrand): unknown;
@@ -46,9 +46,9 @@ export declare const CleanupConfigSchema: z<CleanupConfig>;
46
46
  /**
47
47
  * Structural face of the settings scope the host needs for the policy: a
48
48
  * resolved read and a validated write. Kept local (never imports the settings
49
- * contract) so the host bundle links on both rc.2 and alpha — the settings
50
- * API drift (alpha adds `mutate`, rd2 does not) is confined to the seam the
51
- * host passes in, never to this module.
49
+ * contract) so the host bundle links on both 0.1.1-rc.2 and 0.1.2-rc.1 — the
50
+ * settings API drift (0.1.2 adds `mutate`, 0.1.1 does not) is confined to the
51
+ * seam the host passes in, never to this module.
52
52
  */
53
53
  export interface CleanupSettingsScope {
54
54
  /** The resolved policy: schema defaults, then base, then the user layer. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-rewind-plugin",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
4
4
  "description": "DSH 插件:真正便捷无感的同窗口内对话回退,从不新建分支;自带轻量工作区备份,可一并还原文件(完整 Claude Code /rewind 语义)。 · DSH plugin: genuinely effortless in-window conversation rewind — never forking a new session; ships a lightweight workspace backup that restores files together with the rewind (full Claude Code /rewind semantics).",
5
5
  "keywords": [
6
6
  "deepseek-harness",