dsh-rewind-plugin 0.7.1 → 0.7.2

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.md CHANGED
@@ -145,6 +145,10 @@ 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
+ > 若使用中遇到问题,可开启详细诊断输出,复现后附上控制台 `[dsh-rewind]` 输出便于定位。详见
150
+ > [浏览器诊断与详细输出开关](docs/compat/diagnostics.zh.md)。
151
+
148
152
  ## 安全
149
153
 
150
154
  本插件只向会话日志追加回退标记事件,从不删除或改写已记录的历史。工作区文件仅在「回退对话和代码」时被改写,备份存储于 `~/.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 & the verbose output switch (`.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,82 @@
1
+ # Browser diagnostics and the verbose switch
2
+
3
+ [简体中文](diagnostics.zh.md)
4
+
5
+ Every browser-side diagnostic this plugin emits goes through one small
6
+ channel, so a report can be captured with a single console filter and a
7
+ verbose mode can be switched on without a plugin rebuild.
8
+
9
+ ## Scope-prefixed output
10
+
11
+ Diagnostics are printed as `[dsh-rewind:<scope>] ...`. The scope names the
12
+ subsystem, and is also what the verbose switch filters on:
13
+
14
+ | Scope | What it reports |
15
+ | --- | --- |
16
+ | `hiding` | The row-hiding path (which rows a rewind cuts; the `rewind not hidden` anomaly) |
17
+ | `refill` | The composer refill after a rewind (target seq, mode, used channel, write result) |
18
+ | `portals` | Per-message button mount issues (e.g. no session binding) |
19
+ | `settings` | The snapshot-cleanup settings card |
20
+
21
+ ## Levels
22
+
23
+ | Level | Default | Meaning |
24
+ | --- | --- | --- |
25
+ | `error` | on | Unexpected/breaking; always printed |
26
+ | `warn` | on | Recoverable anomaly guard (`rewind not hidden`, `refill skipped/refused/threw`); always printed |
27
+ | `info` | off | Event-level lifecycle (one line per rewind / refill) |
28
+ | `debug` | off | Additional event-level detail behind the switch (the hide-set line, one per rewind) |
29
+
30
+ `error`/`warn` are always printed so an anomaly surfaces even for a user who
31
+ never touched the switch; `info`/`debug` are gated so a normal user's console
32
+ stays clean. Both gated levels emit once per relevant event (a rewind, a
33
+ refill), never per streaming frame — so a rewound session does not flood the
34
+ console even with verbose output switched on.
35
+
36
+ ## Enabling verbose output
37
+
38
+ The switch lives in the **browser's** `localStorage` under the plugin's own
39
+ key, so it can never enable another plugin/feature and nothing else can wake
40
+ this one. Set it, reload the page, reproduce, then filter the console for
41
+ `[dsh-rewind]`:
42
+
43
+ ```js
44
+ // Every dsh-rewind namespace.
45
+ localStorage['dsh-rewind.debug'] = 'dsh-rewind*'
46
+
47
+ // Or just the subsystem you care about (exact scope match).
48
+ localStorage['dsh-rewind.debug'] = 'dsh-rewind:refill'
49
+
50
+ // Several at once (comma-separated).
51
+ localStorage['dsh-rewind.debug'] = 'dsh-rewind:refill,dsh-rewind:hiding'
52
+ ```
53
+
54
+ Reload (`F5`) after setting it. To switch it off:
55
+
56
+ ```js
57
+ delete localStorage['dsh-rewind.debug']
58
+ ```
59
+
60
+ ## Capturing a report
61
+
62
+ 1. On the machine/browser page that reproduces the problem, enable the
63
+ relevant namespace (see above) and reload.
64
+ 2. Reproduce once.
65
+ 3. In DevTools, filter the Console for `[dsh-rewind]` and copy the output
66
+ (with the plugin version and the DSH/kernel version).
67
+
68
+ For a rewind that did not refill the composer, the `refill` scope is what
69
+ matters: its `composer write` line reports whether the draft was restored
70
+ through the harness facade (`facade`) or the DOM fallback (`dom`) and whether
71
+ the write succeeded, which tells a plugin fault apart from a harness-side
72
+ render desync.
73
+
74
+ ## Notes
75
+
76
+ - The switch is an aid for maintainers and cooperating reporters, **not** a
77
+ stable public interface; its exact keys and output may change without notice.
78
+ - It is scoped to one browser origin and one browser; enable it where the
79
+ problem actually occurs.
80
+
81
+ Related: [audit.md](audit.md) for the verified-compatibility matrix and
82
+ [troubleshooting.md](troubleshooting.md) for the legacy repair steps.
@@ -0,0 +1,63 @@
1
+ # 浏览器诊断与详细输出开关
2
+
3
+ [English](diagnostics.md)
4
+
5
+ 本插件在浏览器端输出的所有诊断信息都经由同一条通道,这样可用一个控制台过滤器抓取报告,且无需重建插件即可开启详细输出模式。
6
+
7
+ ## 按作用域前缀输出
8
+
9
+ 诊断以 `[dsh-rewind:<scope>] ...` 形式打印。`scope` 标明子系统,同时也正是详细输出开关的过滤依据:
10
+
11
+ | scope | 报告内容 |
12
+ | --- | --- |
13
+ | `hiding` | 行隐藏路径(回退切断哪些行;`rewind not hidden` 异常) |
14
+ | `refill` | 回退后的输入框回填(目标 seq、模式、所用通道、写入结果) |
15
+ | `portals` | 每条消息按钮的挂载问题(如无会话绑定) |
16
+ | `settings` | 快照清理设置卡片 |
17
+
18
+ ## 分级
19
+
20
+ | 级别 | 默认 | 含义 |
21
+ | --- | --- | --- |
22
+ | `error` | 开 | 意外/破坏性;总是打印 |
23
+ | `warn` | 开 | 可恢复的异常护栏(`rewind not hidden`、`refill skipped/refused/threw`);总是打印 |
24
+ | `info` | 关 | 事件级生命周期(每次回退/回填一行) |
25
+ | `debug` | 关 | 开关控制下的追加事件级细节(隐藏集一行,每次回退一行) |
26
+
27
+ `error`/`warn` 总是打印,这样即使没碰过开关的用户,异常也会浮现;`info`/`debug` 则受开关控制,保持普通用户控制台干净。两个受控级别都按**相关事件触发一次**(一次回退、一次回填),不会随流式逐帧打印——所以即使开了详细输出,回退后的会话也不会刷屏。
28
+
29
+ ## 开启详细输出
30
+
31
+ 开关位于**浏览器**的 `localStorage`、且使用插件专属键,因此它不会开启其它插件/功能,别的功能也无法唤醒本插件。设置后刷新页面、复现一次,再把控制台按 `[dsh-rewind]` 过滤即可:
32
+
33
+ ```js
34
+ // 全部 dsh-rewind 命名空间。
35
+ localStorage['dsh-rewind.debug'] = 'dsh-rewind*'
36
+
37
+ // 或只开所关心的子系统(精确 scope 匹配)。
38
+ localStorage['dsh-rewind.debug'] = 'dsh-rewind:refill'
39
+
40
+ // 同时开多个(逗号分隔)。
41
+ localStorage['dsh-rewind.debug'] = 'dsh-rewind:refill,dsh-rewind:hiding'
42
+ ```
43
+
44
+ 设置后刷新(`F5`)。关闭:
45
+
46
+ ```js
47
+ delete localStorage['dsh-rewind.debug']
48
+ ```
49
+
50
+ ## 采集一段报告
51
+
52
+ 1. 在**复现问题的那台机器/那个浏览器页面**,开启相应命名空间(见上)并刷新。
53
+ 2. 复现一次。
54
+ 3. 在 DevTools 里把 Console 按 `[dsh-rewind]` 过滤,复制输出(连同插件版本、DSH/内核版本)。
55
+
56
+ 对「回退后没回填输入框」这类问题,重点是 `refill` 作用域:其 `composer write` 一行会报告草稿是通过 harness facade(`facade`)还是 DOM 回退(`dom`)恢复、写入是否成功,从而区分「插件没写入」与「harness 侧渲染不同步」。
57
+
58
+ ## 备注
59
+
60
+ - 该开关是维护者与配合排查者使用的工具,**不是**稳定公开接口;其具体键与输出可能不经通知而改变。
61
+ - 它受单个浏览器 origin、单个浏览器限定;请在实际出问题的地方开启。
62
+
63
+ 相关:[audit.md](audit.md) 为已验证兼容矩阵,[troubleshooting.md](troubleshooting.md) 为历史修复步骤。
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,73 @@ 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
+ rewindLog.info("refill", `rewind @${seq} not matched, no refill`);
1039
+ return;
1040
+ }
1041
+ let outcome;
1042
+ try {
1043
+ outcome = await waitForCommand(session, chatOf, (node) => isExecutedRewindCommand(node, seq) && !known.has(node.seq), 2e4);
1044
+ } catch (error) {
1045
+ rewindLog.warn("refill", `waiting for rewind @${seq} outcome threw`, error);
1046
+ return;
1047
+ }
1048
+ if (outcome === null) {
1049
+ rewindLog.warn("refill", `rewind @${seq} never settled within timeout, no refill`);
1050
+ return;
1051
+ }
992
1052
  if (outcome.kind !== "success") {
1053
+ rewindLog.warn("refill", `rewind @${seq} refused`, outcome.text);
993
1054
  showHint(outcome.text ?? "rewind failed");
994
1055
  return;
995
1056
  }
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);
1057
+ const hidSeqs = (() => {
1058
+ const chat = chatOf(session);
1059
+ return chat === void 0 ? /* @__PURE__ */ new Set() : hiddenSeqsOf(chat);
1060
+ })();
1061
+ if (hidSeqs.size === 0) {
1062
+ rewindLog.warn("hiding", `rewind not hidden (target @${seq})`, { target: seq });
1063
+ } else {
1064
+ rewindLog.debug(
1065
+ "hiding",
1066
+ `rewind hides seqs [${[...hidSeqs].slice(0, 20).join(", ")}${hidSeqs.size > 20 ? "\u2026" : ""}]`,
1067
+ { target: seq }
1068
+ );
1069
+ }
1070
+ if (currentSessionId() !== session.sessionId) {
1071
+ rewindLog.info("refill", `skipped refill @${seq}: session switched during rewind`);
1072
+ return;
1073
+ }
1074
+ let text;
1075
+ try {
1076
+ text = messageTextAt(chatOf(session), seq);
1077
+ } catch (error) {
1078
+ rewindLog.warn("refill", `reading target text for @${seq} threw`, error);
1079
+ return;
1080
+ }
1081
+ if (text === void 0 || text === "") {
1082
+ rewindLog.info("refill", `skipped refill @${seq}: no editable text`);
1083
+ return;
1084
+ }
1085
+ if (composerText().trim() !== "") {
1086
+ rewindLog.info("refill", `skipped refill @${seq}: composer already has a draft`);
1087
+ return;
1088
+ }
1089
+ let ok = false;
1090
+ try {
1091
+ ok = setComposerText(session.sessionId, text);
1092
+ } catch (error) {
1093
+ rewindLog.warn("refill", `composer refill @${seq} threw`, error);
1094
+ return;
1095
+ }
1096
+ rewindLog.info("refill", `rewound to @${seq} (${mode})`, { ok, text: text.slice(0, 80) });
1000
1097
  }
1001
1098
  function composerSurface() {
1002
1099
  return document.querySelector(COMPOSER_TEXTAREA_SELECTOR) ?? document.querySelector(COMPOSER_EDITABLE_SELECTOR);
@@ -1092,40 +1189,6 @@ function sameTargets(left, right) {
1092
1189
  return other.kind === "pending" && target.itemId === other.itemId;
1093
1190
  });
1094
1191
  }
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
1192
  function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, t, subscribeLocale, setComposerText }) {
1130
1193
  const [targets, setTargets] = (0, import_react.useState)([]);
1131
1194
  const hidden = (0, import_react.useRef)(/* @__PURE__ */ new WeakSet());
@@ -1146,7 +1209,6 @@ function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, t, subs
1146
1209
  const snapshot = session.getSnapshot();
1147
1210
  const chat = chatOf(session);
1148
1211
  const hiddenSeqs = chat === void 0 ? /* @__PURE__ */ new Set() : hiddenSeqsOf(chat);
1149
- let hiddenCount = 0;
1150
1212
  for (const seat of chat === void 0 ? [] : document.querySelectorAll(CHAT_SEAT_SELECTOR)) {
1151
1213
  const key = seat.dataset.chatAnchorKey;
1152
1214
  const anchor = key !== void 0 ? chat?.nodes.get(key)?.anchorSeq : void 0;
@@ -1154,21 +1216,12 @@ function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, t, subs
1154
1216
  seat.style.display = "none";
1155
1217
  seat.dataset.dshRewindHidden = "true";
1156
1218
  hidden.current.add(seat);
1157
- hiddenCount += 1;
1158
1219
  } else if (hidden.current.has(seat)) {
1159
1220
  seat.style.display = "";
1160
1221
  delete seat.dataset.dshRewindHidden;
1161
1222
  hidden.current.delete(seat);
1162
1223
  }
1163
1224
  }
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
1225
  const durable = chat === void 0 ? [] : collectTargets(chat, hiddenSeqs);
1173
1226
  const next = [...durable, ...collectPendingTargets(snapshot)];
1174
1227
  setTargets((current) => sameTargets(current, next) ? current : next);
@@ -1223,7 +1276,7 @@ function RewindButton({ target, sessionId, sessionOf, chatOf, currentSessionId,
1223
1276
  event.stopPropagation();
1224
1277
  const session = sessionOf(sessionId);
1225
1278
  if (session === void 0) {
1226
- console.warn("[dsh-rewind] rewind button clicked with no session binding");
1279
+ rewindLog.warn("portals", "rewind button clicked with no session binding");
1227
1280
  return;
1228
1281
  }
1229
1282
  const node = userNodeOf(chatOf(session), target.key);
@@ -1559,14 +1612,14 @@ function SettingsCleanupCard({ api, t }) {
1559
1612
  // src/client/index.ts
1560
1613
  var name = "dsh-rewind";
1561
1614
  var inject = ["slots", "sessions", "locale", "commandUi"];
1562
- var NS = "rewind";
1615
+ var NS2 = "rewind";
1563
1616
  var HEADER_ACTIONS_SLOT = "conversation.session.header.actions";
1564
1617
  var COMPOSER_TEXTAREA_SELECTOR2 = "[data-input-scroll] textarea, textarea[data-phase]";
1565
1618
  var COMPOSER_EDITABLE_SELECTOR2 = "[data-composer-input]";
1566
1619
  function apply(ctx) {
1567
1620
  ctx.effect(function* () {
1568
- yield ctx.locale.register(NS, { zh, en });
1569
- const t = ctx.locale.bind(NS);
1621
+ yield ctx.locale.register(NS2, { zh, en });
1622
+ const t = ctx.locale.bind(NS2);
1570
1623
  const style = document.createElement("style");
1571
1624
  style.dataset.plugin = "dsh-rewind";
1572
1625
  style.textContent = STYLE;
@@ -1586,15 +1639,23 @@ function apply(ctx) {
1586
1639
  }
1587
1640
  };
1588
1641
  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
- );
1642
+ try {
1643
+ const conversation = ctx.get("conversation");
1644
+ const input = conversation?.input;
1645
+ const scope = ctx.sessions.scope?.(sessionId);
1646
+ const facade = input !== void 0 && scope !== void 0;
1647
+ const ok = writeComposer(
1648
+ text,
1649
+ facade ? { setDraft: (draft) => {
1650
+ input.for(scope).setDraft(draft);
1651
+ } } : void 0
1652
+ );
1653
+ rewindLog.info("refill", "composer write", { channel: facade ? "facade" : "dom", ok });
1654
+ return ok;
1655
+ } catch (error) {
1656
+ rewindLog.warn("refill", "composer write threw", error);
1657
+ return false;
1658
+ }
1598
1659
  };
1599
1660
  const slots = ctx.slots;
1600
1661
  yield slots.inject(HEADER_ACTIONS_SLOT, () => slots.register(
@@ -1632,13 +1693,13 @@ function apply(ctx) {
1632
1693
  // Match the official cards / dsh-market: locale + inject provide
1633
1694
  // the card its props through the slot renderer (the keyed card owns
1634
1695
  // its internals, but the page feeds it locale + the bound api).
1635
- locale: NS,
1696
+ locale: NS2,
1636
1697
  inject: () => ({ t, api: cardApi })
1637
1698
  },
1638
1699
  SettingsCleanupCard
1639
1700
  ));
1640
1701
  } catch (error) {
1641
- console.error("[dsh-rewind] settings card register failed:", error);
1702
+ rewindLog.error("settings", "settings card register failed", error);
1642
1703
  }
1643
1704
  });
1644
1705
  const commandUi = ctx.get("commandUi");
@@ -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.2",
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",