dsh-rewind-plugin 0.7.1 → 0.7.3

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
@@ -158,6 +158,9 @@ withdrew should consume the stable, locale-independent helpers exported from
158
158
  4. **Rewinds from `≤ v0.3.3`** — compaction (`/compact`) is unavailable for those sessions. Newer versions are compatible; for affected old sessions, start a new session.
159
159
  5. **The turn-rail shows rewound turns** — the right-side rail added in DSH `v0.1.2-alpha.1` keeps ticks for withdrawn messages: clicking does not jump and hovering shows the withdrawn text. Only a display difference; no functional impact.
160
160
 
161
+ > [!NOTE]
162
+ > Browser diagnostics are available; see [Browser diagnostics](docs/compat/diagnostics.md).
163
+
161
164
  ## Security
162
165
 
163
166
  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. Delete `~/.dsh/rewind-snapshots/` to wipe file backups only (chat rewinds are unaffected); the plugin rebuilds automatically. 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).
package/README.md CHANGED
@@ -145,6 +145,9 @@ dsh plugin --profile web add dsh-rewind-plugin
145
145
  4. **v0.3.3 及更早版本**回退过的会话,压缩对话(compact)不可用。新版本已兼容;受影响的旧会话建议新建会话。
146
146
  5. **导轨显示已回退轮次**——DSH `v0.1.2-alpha.1` 新增的右侧导轨,会为已撤回消息保留刻度:点击不跳转、悬浮显示已撤回正文。仅显示差异,无功能影响。
147
147
 
148
+ > [!NOTE]
149
+ > 本插件提供浏览器端诊断输出;详见 [浏览器诊断](docs/compat/diagnostics.zh.md)。
150
+
148
151
  ## 安全
149
152
 
150
153
  本插件只向会话日志追加回退标记事件,从不删除或改写已记录的历史。工作区文件仅在「回退对话和代码」时被改写,备份存储于 `~/.dsh/rewind-snapshots/`;还原以备份为唯一来源。不触碰你的 git 仓库,无网络请求,不访问任何凭据。对**长期不活跃**的会话,另有默认关闭的全局自动清理可整目录移除其快照,不影响活动会话与对话日志。完整安全模型:[SECURITY.md](SECURITY.md)。
package/docs/README.md CHANGED
@@ -15,6 +15,7 @@ index/navigation entry point.
15
15
  | `contract/client-contract.md` | Rewind visibility contract for third-party DOM plugins (`.zh` mirror) | integrators |
16
16
  | `compat/audit.md` | Compatibility audit: verified surfaces, recorded findings, probe matrix | maintainers |
17
17
  | `compat/troubleshooting.md` | Known issues & repair steps (`.zh` mirror) | users / maintainers |
18
+ | `compat/diagnostics.md` | Browser diagnostics (anomaly alerts; verbose switch gates the startup identity line) (`.zh` mirror) | users / maintainers |
18
19
  | `release/release.md` | Release workflow & DSH peer-version alignment (`.zh` mirror) | maintainers |
19
20
 
20
21
  Repo-root docs outside `docs/`: `SECURITY.md` (security model) and
@@ -0,0 +1,85 @@
1
+ # Browser diagnostics
2
+
3
+ [简体中文](diagnostics.zh.md)
4
+
5
+ Every browser-side diagnostic this plugin emits is routed through one small,
6
+ level-filtered channel, so an anomaly surfaces under a single console filter.
7
+ The channel is deliberately narrow: it records only genuine internal anomalies
8
+ the plugin itself detects as off-contract — **not** the normal-path facts the
9
+ user already sees on screen, which carry no attribution.
10
+
11
+ ## Always-on anomaly alerts
12
+
13
+ `error` and `warn` are always printed (no switch, no configuration) so a fault
14
+ surfaces even for a user who never configured anything:
15
+
16
+ | Level | Meaning |
17
+ | --- | --- |
18
+ | `error` | Unexpected/breaking failure (e.g. the settings card fails to register) |
19
+ | `warn` | Recoverable anomaly guard (e.g. a rewind command/refill throws, a waited-on outcome never settles, an unmet session binding) |
20
+
21
+ These cases are rare, cheap, and off-contract. Normal-path detail (which
22
+ branch a refill took, whether a write matched, which rows a rewind cut) is
23
+ deliberately **not** logged: it re-states what the user already sees and adds
24
+ nothing to attribution.
25
+
26
+ ## Scopes
27
+
28
+ Diagnostics are printed as `[dsh-rewind:<scope>] ...`. The scope names the
29
+ subsystem an anomaly belongs to, so a report can be captured with a single
30
+ console filter:
31
+
32
+ | Scope | What an anomaly here means |
33
+ | --- | --- |
34
+ | `boot` | Startup identity — `loaded v<version> (build <hash>)`, gated by the verbose switch (see below). Confirms the running bundle matches a fix. |
35
+ | `refill` | The composer refill after a rewind (command/wait/refill throws, an outcome that never settles) |
36
+ | `portals` | Per-message button mount issues (e.g. no session binding) |
37
+ | `settings` | The snapshot-cleanup settings card |
38
+ | `hiding` | **Reserved** — no active alert at present. If a future row-hiding diagnostic is added, it belongs in this region. |
39
+
40
+ ## Verbose switch
41
+
42
+ The `info`/`debug` levels are gated by `localStorage['dsh-rewind.debug']`, read
43
+ once per call and filtered by namespace. After the withdrawal of the restating
44
+ verbose detail, exactly **one** verbose line remains: the `boot` scope's startup
45
+ identity line. It is deliberately **off** by default (a normal user's console
46
+ stays clean, and it is not an anomaly), so a reporter enables the scope to see
47
+ it:
48
+
49
+ ```js
50
+ // Just the startup identity line.
51
+ localStorage['dsh-rewind.debug'] = 'dsh-rewind:boot'
52
+
53
+ // Or everything (avoids needing an exact scope).
54
+ localStorage['dsh-rewind.debug'] = 'dsh-rewind*'
55
+ ```
56
+
57
+ Reload (`F5`) after setting it. To switch it off:
58
+
59
+ ```js
60
+ delete localStorage['dsh-rewind.debug']
61
+ ```
62
+
63
+ The `error`/`warn` anomaly alerts are **not** gated by this switch — they are
64
+ always printed. The other verbose detail (used write channel, an empty
65
+ hide-set, per-rewind lifecycle lines) has been withdrawn: it re-stated behavior
66
+ the user already sees and carried no attribution.
67
+
68
+ ## Capturing a report
69
+
70
+ 1. Reproduce once on the affected page.
71
+ 2. In DevTools, filter the Console for `[dsh-rewind]` and copy the output
72
+ (with the plugin version and the DSH/kernel version).
73
+
74
+ The `error`/`warn` anomaly alerts require no setup — they always print. To also
75
+ capture the startup identity line (`boot`) — useful for ruling out a stale
76
+ bundle / un-restarted host — enable the verbose switch first (see above), then
77
+ reload.
78
+
79
+ ## Notes
80
+
81
+ - This diagnostic surface is an aid for maintainers and cooperating reporters,
82
+ **not** a stable public interface; its exact keys and output may change
83
+ without notice.
84
+ - It is scoped to one browser origin and one browser; capture where the problem
85
+ actually occurs.
@@ -0,0 +1,60 @@
1
+ # 浏览器诊断
2
+
3
+ [English](diagnostics.md)
4
+
5
+ 本插件在浏览器端输出的所有诊断信息都经由同一条分级通道,因此异常可在单个控制台过滤器下浮现。该通道**刻意收窄**:只记录插件自身检测到的、偏离预期契约的真实内部异常——**不记录**用户早已在屏幕上看到、且无归因价值的普通路径事实。
6
+
7
+ ## 常开的异常告警
8
+
9
+ `error` 与 `warn` **总是打印**(无需开关、无需配置),即使从未配置的用户也能看到故障:
10
+
11
+ | 级别 | 含义 |
12
+ | --- | --- |
13
+ | `error` | 意外/破坏性故障(如设置卡片注册失败) |
14
+ | `warn` | 可恢复的异常护栏(如回退命令/回填抛出异常、等待的结果始终未落定、会话绑定缺失) |
15
+
16
+ 这些情形少见、廉价、且偏离契约。普通路径的细节(回填走了哪个分支、写入是否匹配、回退切断哪些行)则**刻意不记录**:它们重复用户已经看到的现象,对归因毫无增量。
17
+
18
+ ## 作用域
19
+
20
+ 诊断以 `[dsh-rewind:<scope>] ...` 形式打印。`scope` 标明异常所属子系统,因此可用单个控制台过滤器抓取报告:
21
+
22
+ | scope | 这里的异常含义 |
23
+ | --- | --- |
24
+ | `boot` | 启动身份行——`loaded v<version> (build <hash>)`,由详细输出开关控制(见下)。用于确认运行中的 bundle 是否匹配某个修复。 |
25
+ | `refill` | 回退后的输入框回填(命令/等待/回填抛出异常、等待的结果始终未落定) |
26
+ | `portals` | 每条消息按钮的挂载问题(如无会话绑定) |
27
+ | `settings` | 快照清理设置卡片 |
28
+ | `hiding` | **保留区**——当前无活跃告警。若未来新增行隐藏诊断,应归属此区域。 |
29
+
30
+ ## 详细输出开关
31
+
32
+ `info`/`debug` 级别受 `localStorage['dsh-rewind.debug']` 控制,每次调用读取并按命名空间过滤。在撤下那些复述现象的详细输出后,**恰好剩一条** verbose 行:`boot` 作用域的启动身份行。它**默认关闭**(普通用户控制台保持干净,且它不是异常),排查者需先开启该作用域才能看到:
33
+
34
+ ```js
35
+ // 仅启动身份行。
36
+ localStorage['dsh-rewind.debug'] = 'dsh-rewind:boot'
37
+
38
+ // 或全部开启(省去精确 scope)。
39
+ localStorage['dsh-rewind.debug'] = 'dsh-rewind*'
40
+ ```
41
+
42
+ 设置后刷新(`F5`)。关闭:
43
+
44
+ ```js
45
+ delete localStorage['dsh-rewind.debug']
46
+ ```
47
+
48
+ `error`/`warn` 异常告警**不受**该开关控制——它们总是打印。其余详细输出(所用写入通道、空的隐藏集、每次回退的生命周期行)已撤下:它们重复用户已经看到的现象、无归因增量。
49
+
50
+ ## 采集一段报告
51
+
52
+ 1. 在出问题的页面上复现一次。
53
+ 2. 在 DevTools 里把 Console 按 `[dsh-rewind]` 过滤,复制输出(连同插件版本、DSH/内核版本)。
54
+
55
+ `error`/`warn` 异常告警无需任何设置——它们总是打印。若要同时采集启动身份行(`boot`,用于排除「旧 bundle / 宿主未重启」),请先开启详细输出开关(见上),再刷新。
56
+
57
+ ## 备注
58
+
59
+ - 该诊断表面是维护者与配合排查者使用的工具,**不是**稳定公开接口;其具体键与输出可能不经通知而改变。
60
+ - 它受单个浏览器 origin、单个浏览器限定;请在实际出问题的地方采集。
package/lib/client.js CHANGED
@@ -650,8 +650,8 @@ async function previewImpact(session, chatOf, seq) {
650
650
  if (!result.ok || result.value?.matched !== true) return null;
651
651
  return waitForCommand(session, chatOf, (node) => isPreviewFor(node, seq) && !known.has(node.seq));
652
652
  }
653
- function el(tag, className, text) {
654
- const node = document.createElement(tag);
653
+ function el(tag2, className, text) {
654
+ const node = document.createElement(tag2);
655
655
  node.className = className;
656
656
  if (text !== void 0) node.textContent = text;
657
657
  return node;
@@ -931,6 +931,48 @@ function retractSpan(steering, targetId) {
931
931
  return steering.slice(index).map((item) => item.id);
932
932
  }
933
933
 
934
+ // src/client/log.ts
935
+ var NS = "dsh-rewind";
936
+ var DEBUG_KEY = "dsh-rewind.debug";
937
+ var ALWAYS_ON = /* @__PURE__ */ new Set(["error", "warn"]);
938
+ function switchValue() {
939
+ try {
940
+ return window.localStorage.getItem(DEBUG_KEY) ?? "";
941
+ } catch {
942
+ return "";
943
+ }
944
+ }
945
+ function matches(value, ns) {
946
+ for (const entry of value.split(",")) {
947
+ const part = entry.trim();
948
+ if (part === "") continue;
949
+ if (part === "*" || part === `${NS}*`) return true;
950
+ if (part.endsWith("*")) {
951
+ if (ns.startsWith(part.slice(0, -1))) return true;
952
+ } else if (ns === part) {
953
+ return true;
954
+ }
955
+ }
956
+ return false;
957
+ }
958
+ function tag(scope) {
959
+ return `[${NS}:${scope}]`;
960
+ }
961
+ function log(level, scope, message, data) {
962
+ if (ALWAYS_ON.has(level)) {
963
+ console[level](tag(scope), message, data);
964
+ return;
965
+ }
966
+ if (!matches(switchValue(), `${NS}:${scope}`)) return;
967
+ console.info(tag(scope), message, data);
968
+ }
969
+ var rewindLog = {
970
+ error: (scope, message, data) => log("error", scope, message, data),
971
+ warn: (scope, message, data) => log("warn", scope, message, data),
972
+ info: (scope, message, data) => log("info", scope, message, data),
973
+ debug: (scope, message, data) => log("debug", scope, message, data)
974
+ };
975
+
934
976
  // src/client/portals.tsx
935
977
  var import_jsx_runtime = require("react/jsx-runtime");
936
978
  function fillComposerTextarea(text) {
@@ -985,18 +1027,53 @@ function userNodeOf(chat, key) {
985
1027
  }
986
1028
  async function runRewindAndFill(session, seq, mode, currentSessionId, chatOf, setComposerText) {
987
1029
  const known = knownCommandSeqs(session, chatOf, (node) => isExecutedRewindCommand(node, seq));
988
- const result = await session.command(`/rewind @${seq} ${mode}`);
989
- if (!result.ok || result.value?.matched !== true) return;
990
- const outcome = await waitForCommand(session, chatOf, (node) => isExecutedRewindCommand(node, seq) && !known.has(node.seq), 2e4);
991
- if (outcome === null) return;
1030
+ let result;
1031
+ try {
1032
+ result = await session.command(`/rewind @${seq} ${mode}`);
1033
+ } catch (error) {
1034
+ rewindLog.warn("refill", `rewind command threw, skipping refill @${seq}`, error);
1035
+ return;
1036
+ }
1037
+ if (!result.ok || result.value?.matched !== true) {
1038
+ return;
1039
+ }
1040
+ let outcome;
1041
+ try {
1042
+ outcome = await waitForCommand(session, chatOf, (node) => isExecutedRewindCommand(node, seq) && !known.has(node.seq), 2e4);
1043
+ } catch (error) {
1044
+ rewindLog.warn("refill", `waiting for rewind @${seq} outcome threw`, error);
1045
+ return;
1046
+ }
1047
+ if (outcome === null) {
1048
+ rewindLog.warn("refill", `rewind @${seq} never settled within timeout, no refill`);
1049
+ return;
1050
+ }
992
1051
  if (outcome.kind !== "success") {
993
1052
  showHint(outcome.text ?? "rewind failed");
994
1053
  return;
995
1054
  }
996
- if (currentSessionId() !== session.sessionId) return;
997
- const text = messageTextAt(chatOf(session), seq);
998
- if (text === void 0 || text === "") return;
999
- setComposerText(session.sessionId, text);
1055
+ if (currentSessionId() !== session.sessionId) {
1056
+ return;
1057
+ }
1058
+ let text;
1059
+ try {
1060
+ text = messageTextAt(chatOf(session), seq);
1061
+ } catch (error) {
1062
+ rewindLog.warn("refill", `reading target text for @${seq} threw`, error);
1063
+ return;
1064
+ }
1065
+ if (text === void 0 || text === "") {
1066
+ return;
1067
+ }
1068
+ if (composerText().trim() !== "") {
1069
+ return;
1070
+ }
1071
+ try {
1072
+ setComposerText(session.sessionId, text);
1073
+ } catch (error) {
1074
+ rewindLog.warn("refill", `composer refill @${seq} threw`, error);
1075
+ return;
1076
+ }
1000
1077
  }
1001
1078
  function composerSurface() {
1002
1079
  return document.querySelector(COMPOSER_TEXTAREA_SELECTOR) ?? document.querySelector(COMPOSER_EDITABLE_SELECTOR);
@@ -1092,40 +1169,6 @@ function sameTargets(left, right) {
1092
1169
  return other.kind === "pending" && target.itemId === other.itemId;
1093
1170
  });
1094
1171
  }
1095
- function hasExecutedRewindCommand(chat) {
1096
- for (const key of chat.order) {
1097
- const node = chat.nodes.get(key);
1098
- if (node === void 0 || node.kind !== "command") continue;
1099
- const command = node.data;
1100
- if (command.name !== "rewind") continue;
1101
- const args = command.args ?? "";
1102
- if (args.includes("preview") || args.includes("__candidates")) continue;
1103
- return true;
1104
- }
1105
- return false;
1106
- }
1107
- function describeHiding(chat, hiddenSeqs) {
1108
- const commands = [];
1109
- for (const key of chat.order) {
1110
- const node = chat.nodes.get(key);
1111
- if (node === void 0 || node.kind !== "command") continue;
1112
- const c = node.data;
1113
- commands.push(`#${c.seq} name=${c.name} outcome=${c.outcome?.kind ?? "-"} marker=${c.outcome?.sourceEventSeq ?? "-"} args=${JSON.stringify(c.args)}`);
1114
- }
1115
- const scoped = hiddenSeqs.size > 0 ? `hidden=[${[...hiddenSeqs].slice(0, 20).join(",")}${hiddenSeqs.size > 20 ? "\u2026" : ""}]` : "";
1116
- let seats = 0;
1117
- let resolved = 0;
1118
- let inHidden = 0;
1119
- for (const seat of document.querySelectorAll(CHAT_SEAT_SELECTOR)) {
1120
- seats += 1;
1121
- const key = seat.dataset.chatAnchorKey;
1122
- const node = key === void 0 ? void 0 : chat.nodes.get(key);
1123
- if (node === void 0) continue;
1124
- resolved += 1;
1125
- if (hiddenSeqs.has(node.anchorSeq)) inHidden += 1;
1126
- }
1127
- return `commands=[${commands.join(" | ")}] ${scoped} seats=${seats} resolved=${resolved} inHidden=${inHidden}`;
1128
- }
1129
1172
  function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, t, subscribeLocale, setComposerText }) {
1130
1173
  const [targets, setTargets] = (0, import_react.useState)([]);
1131
1174
  const hidden = (0, import_react.useRef)(/* @__PURE__ */ new WeakSet());
@@ -1146,7 +1189,6 @@ function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, t, subs
1146
1189
  const snapshot = session.getSnapshot();
1147
1190
  const chat = chatOf(session);
1148
1191
  const hiddenSeqs = chat === void 0 ? /* @__PURE__ */ new Set() : hiddenSeqsOf(chat);
1149
- let hiddenCount = 0;
1150
1192
  for (const seat of chat === void 0 ? [] : document.querySelectorAll(CHAT_SEAT_SELECTOR)) {
1151
1193
  const key = seat.dataset.chatAnchorKey;
1152
1194
  const anchor = key !== void 0 ? chat?.nodes.get(key)?.anchorSeq : void 0;
@@ -1154,21 +1196,12 @@ function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, t, subs
1154
1196
  seat.style.display = "none";
1155
1197
  seat.dataset.dshRewindHidden = "true";
1156
1198
  hidden.current.add(seat);
1157
- hiddenCount += 1;
1158
1199
  } else if (hidden.current.has(seat)) {
1159
1200
  seat.style.display = "";
1160
1201
  delete seat.dataset.dshRewindHidden;
1161
1202
  hidden.current.delete(seat);
1162
1203
  }
1163
1204
  }
1164
- if (hiddenSeqs.size > 0 || hiddenCount > 0) {
1165
- console.info(
1166
- `[dsh-rewind] hiding: ${hiddenCount} rows, seqs [${[...hiddenSeqs].slice(0, 20).join(", ")}${hiddenSeqs.size > 20 ? "\u2026" : ""}]`
1167
- );
1168
- }
1169
- if (chat !== void 0 && hiddenCount === 0 && hasExecutedRewindCommand(chat)) {
1170
- console.warn(`[dsh-rewind] rewind not hidden: ${describeHiding(chat, hiddenSeqs)}`);
1171
- }
1172
1205
  const durable = chat === void 0 ? [] : collectTargets(chat, hiddenSeqs);
1173
1206
  const next = [...durable, ...collectPendingTargets(snapshot)];
1174
1207
  setTargets((current) => sameTargets(current, next) ? current : next);
@@ -1223,7 +1256,7 @@ function RewindButton({ target, sessionId, sessionOf, chatOf, currentSessionId,
1223
1256
  event.stopPropagation();
1224
1257
  const session = sessionOf(sessionId);
1225
1258
  if (session === void 0) {
1226
- console.warn("[dsh-rewind] rewind button clicked with no session binding");
1259
+ rewindLog.warn("portals", "rewind button clicked with no session binding");
1227
1260
  return;
1228
1261
  }
1229
1262
  const node = userNodeOf(chatOf(session), target.key);
@@ -1305,6 +1338,10 @@ function createRewindBridge(deps) {
1305
1338
  };
1306
1339
  }
1307
1340
 
1341
+ // src/client/build-info.ts
1342
+ var PLUGIN_VERSION = true ? "0.7.3" : "dev";
1343
+ var BUILD_HASH = true ? "0752e2ff" : "dev";
1344
+
1308
1345
  // src/client/locales.ts
1309
1346
  var zh = {
1310
1347
  "button.aria": "\u56DE\u9000\u5230\u6B64\u6D88\u606F",
@@ -1559,14 +1596,15 @@ function SettingsCleanupCard({ api, t }) {
1559
1596
  // src/client/index.ts
1560
1597
  var name = "dsh-rewind";
1561
1598
  var inject = ["slots", "sessions", "locale", "commandUi"];
1562
- var NS = "rewind";
1599
+ var NS2 = "rewind";
1563
1600
  var HEADER_ACTIONS_SLOT = "conversation.session.header.actions";
1564
1601
  var COMPOSER_TEXTAREA_SELECTOR2 = "[data-input-scroll] textarea, textarea[data-phase]";
1565
1602
  var COMPOSER_EDITABLE_SELECTOR2 = "[data-composer-input]";
1566
1603
  function apply(ctx) {
1604
+ rewindLog.info("boot", `loaded v${PLUGIN_VERSION} (build ${BUILD_HASH})`);
1567
1605
  ctx.effect(function* () {
1568
- yield ctx.locale.register(NS, { zh, en });
1569
- const t = ctx.locale.bind(NS);
1606
+ yield ctx.locale.register(NS2, { zh, en });
1607
+ const t = ctx.locale.bind(NS2);
1570
1608
  const style = document.createElement("style");
1571
1609
  style.dataset.plugin = "dsh-rewind";
1572
1610
  style.textContent = STYLE;
@@ -1586,15 +1624,22 @@ function apply(ctx) {
1586
1624
  }
1587
1625
  };
1588
1626
  const setComposerText = (sessionId, text) => {
1589
- const conversation = ctx.get("conversation");
1590
- const input = conversation?.input;
1591
- const scope = ctx.sessions.scope?.(sessionId);
1592
- return writeComposer(
1593
- text,
1594
- input !== void 0 && scope !== void 0 ? { setDraft: (draft) => {
1595
- input.for(scope).setDraft(draft);
1596
- } } : void 0
1597
- );
1627
+ try {
1628
+ const conversation = ctx.get("conversation");
1629
+ const input = conversation?.input;
1630
+ const scope = ctx.sessions.scope?.(sessionId);
1631
+ const facade = input !== void 0 && scope !== void 0;
1632
+ const ok = writeComposer(
1633
+ text,
1634
+ facade ? { setDraft: (draft) => {
1635
+ input.for(scope).setDraft(draft);
1636
+ } } : void 0
1637
+ );
1638
+ return ok;
1639
+ } catch (error) {
1640
+ rewindLog.warn("refill", "composer write threw", error);
1641
+ return false;
1642
+ }
1598
1643
  };
1599
1644
  const slots = ctx.slots;
1600
1645
  yield slots.inject(HEADER_ACTIONS_SLOT, () => slots.register(
@@ -1632,13 +1677,13 @@ function apply(ctx) {
1632
1677
  // Match the official cards / dsh-market: locale + inject provide
1633
1678
  // the card its props through the slot renderer (the keyed card owns
1634
1679
  // its internals, but the page feeds it locale + the bound api).
1635
- locale: NS,
1680
+ locale: NS2,
1636
1681
  inject: () => ({ t, api: cardApi })
1637
1682
  },
1638
1683
  SettingsCleanupCard
1639
1684
  ));
1640
1685
  } catch (error) {
1641
- console.error("[dsh-rewind] settings card register failed:", error);
1686
+ rewindLog.error("settings", "settings card register failed", error);
1642
1687
  }
1643
1688
  });
1644
1689
  const commandUi = ctx.get("commandUi");
@@ -0,0 +1,19 @@
1
+ /**
2
+ * dsh-rewind client build identity.
3
+ *
4
+ * Two constants are injected by `scripts/build.mjs` at esbuild time (see the
5
+ * `define` block there) and are NOT present in the source. They answer the
6
+ * "am I even running the fixed bundle?" question — the most common, cheapest
7
+ * root cause to rule in/out when a report lands:
8
+ * - `__DSH_REWIND_VERSION__` — the plugin version from `package.json`.
9
+ * - `__DSH_REWIND_BUILD__` — a short content hash of the client source.
10
+ *
11
+ * They are read once at module load and surfaced (behind the existing
12
+ * `dsh-rewind.debug` switch, never a new key) by `apply` in `index.ts`.
13
+ *
14
+ * @module dsh-rewind/client/build-info
15
+ */
16
+ /** Plugin version baked in at build time (from `package.json`). */
17
+ export declare const PLUGIN_VERSION: string;
18
+ /** Short content hash of the client source, for stale-bundle detection. */
19
+ export declare const BUILD_HASH: string;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * dsh-rewind client logger: a single, namespaced, level-filtered logging
3
+ * channel for every browser-side diagnostic in this plugin.
4
+ *
5
+ * Design (industry-normal layering, kept dependency-free):
6
+ * - `error` / `warn` are ALWAYS emitted (they are the anomaly guard: rare,
7
+ * cheap, and must surface even for a user who never touched the switch).
8
+ * - `info` / `debug` are gated by a DEBUG switch and further filtered by
9
+ * namespace, so verbose detail never floods a normal user's console.
10
+ *
11
+ * The DEBUG switch is a runtime, per-origin knob read from
12
+ * `localStorage['dsh-rewind.debug']` — the convention-debug-scan pattern
13
+ * (namespace match, comma-separated, `*` wildcard), scoped to an
14
+ * exclusively-own key so it can never enable any other plugin/feature and no
15
+ * other feature can wake this one. Because it is read on every call (never
16
+ * cached), flipping it and reloading takes effect on any published build
17
+ * without a plugin rebuild.
18
+ *
19
+ * Values accepted by the switch (empty/unset = off):
20
+ * - `dsh-rewind*` — every dsh-rewind namespace.
21
+ * - `dsh-rewind:refill` — just one subsystem (exact match).
22
+ * - `dsh-rewind:refill,dsh-rewind:hiding` — several (comma-separated).
23
+ *
24
+ * @module dsh-rewind/client/log
25
+ */
26
+ export type LogLevel = 'error' | 'warn' | 'info' | 'debug';
27
+ /**
28
+ * Emit one diagnostic line. `error`/`warn` always print; `info`/`debug` print
29
+ * only when the DEBUG switch selects the namespace. Both gated levels are
30
+ * routed to the always-visible `console.info` rather than `console.debug`,
31
+ * whose "Verbose" level Chrome filters out by default — otherwise a reporter
32
+ * who turns the switch on still would not see the line without also changing
33
+ * the DevTools level filter (mapped to `console.debug`). `data` is spread last
34
+ * so DevTools' structured view keeps it inspectable (never stringified).
35
+ */
36
+ export declare function log(level: LogLevel, scope: string, message: string, data?: unknown): void;
37
+ /** Convenience shorthands (typed so call sites read cleanly). */
38
+ export declare const rewindLog: {
39
+ readonly error: (scope: string, message: string, data?: unknown) => void;
40
+ readonly warn: (scope: string, message: string, data?: unknown) => void;
41
+ readonly info: (scope: string, message: string, data?: unknown) => void;
42
+ readonly debug: (scope: string, message: string, data?: unknown) => void;
43
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-rewind-plugin",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
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",