dsh-rewind-plugin 0.1.10 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,34 @@
1
+ # DeepSeek Harness interface reference
2
+
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:
6
+ > [github.com/deepseek-ai/deepseek-harness](https://github.com/deepseek-ai/deepseek-harness)
7
+
8
+ ## Subsystem docs (`docs/subsystems/`)
9
+
10
+ - [session.md](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/subsystems/session.md) — `Session` / `SessionStore` / event model (`Session.append`, `surfaceOp`, `sourceEventSeqs`)
11
+ - [core.md](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/subsystems/core.md) — `Agent` (`status`, `session`) and other core types
12
+ - [commands.md](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/subsystems/commands.md) — command registration (`ctx.commands.register`, `CommandInvocation`, `CommandResult`)
13
+ - [tools.md](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/subsystems/tools.md) — tool execution seam (`tools/pre-execute` / `tools/post-execute`, `ToolExecution`)
14
+ - [session-query.md](https://github.com/deepseek-ai/deepseek-harness/blob/main/docs/subsystems/session-query.md) — session query / `foldSurface` read-only interfaces
15
+
16
+ Also under `docs/` at the repo root: `persistence-catalog.md` (full
17
+ `SessionEventMap`), `tool-catalog.md` (tool inventory), `config-catalog.md`
18
+ (configuration inventory).
19
+
20
+ ## Key source (`packages/`)
21
+
22
+ | Interface | File |
23
+ |---|---|
24
+ | `Session.append`, surface validation | [packages/core/session/src/index.ts](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/core/session/src/index.ts) |
25
+ | `foldSurface`, replacement rules | [packages/core/session/src/surface.ts](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/core/session/src/surface.ts) |
26
+ | `SessionEventMap`, `SurfaceOp` | [packages/core/session/src/types.ts](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/core/session/src/types.ts) |
27
+ | `createUserMessage`, `MessageSource` | [packages/llm/llm/src/message.ts](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/llm/llm/src/message.ts) |
28
+ | `CommandDefinition`, `CommandInvocation` | [packages/interaction/commands/src/index.ts](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/interaction/commands/src/index.ts) |
29
+ | `Agent` (`status` / `session`) | [packages/core/agent/src/runtime-types.ts](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/core/agent/src/runtime-types.ts) |
30
+ | `tools/pre-execute` / `execute` / `post-execute` | [packages/core/tools/src/index.ts](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/core/tools/src/index.ts) |
31
+ | Client DOM anchors (`data-chat-flow-kind` / `data-chat-anchor-key`) | [packages/client/ui-conversation/src/client/chat/ChatNodeSeat.tsx](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/client/ui-conversation/src/client/chat/ChatNodeSeat.tsx) |
32
+ | User bubble rendering | [packages/client/ui-conversation/src/client/chat/MessageItem.tsx](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/client/ui-conversation/src/client/chat/MessageItem.tsx) |
33
+ | Client `SessionFace` (`command` / `cancel`) | [packages/client/runtime/src/client/contract/session.ts](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/client/runtime/src/client/contract/session.ts) |
34
+ | Client `PendingWait` (`respond`) | [packages/client/runtime/src/client/sessions/pending.ts](https://github.com/deepseek-ai/deepseek-harness/blob/main/packages/client/runtime/src/client/sessions/pending.ts) |
package/lib/client.js CHANGED
@@ -32,40 +32,80 @@ __export(index_exports, {
32
32
  });
33
33
  module.exports = __toCommonJS(index_exports);
34
34
 
35
+ // src/client/hidden.ts
36
+ function targetOfOutcome(text) {
37
+ if (text === void 0) return void 0;
38
+ const match = text.match(/seq (\d+)/);
39
+ return match !== null ? Number(match[1]) : void 0;
40
+ }
41
+ function hiddenSeqsOf(snap) {
42
+ const hidden = /* @__PURE__ */ new Set();
43
+ let minTarget = Number.POSITIVE_INFINITY;
44
+ let maxMarker = Number.NEGATIVE_INFINITY;
45
+ for (const key of snap.order) {
46
+ const node = snap.nodes.get(key);
47
+ if (node === void 0 || node.kind !== "command") continue;
48
+ const command = node.data;
49
+ if (command.name !== "rewind") continue;
50
+ if (command.outcome?.kind !== "success") continue;
51
+ if (command.outcome.sourceEventSeq === void 0) continue;
52
+ hidden.add(command.seq);
53
+ const marker = command.outcome.sourceEventSeq;
54
+ const target = targetOfOutcome(command.outcome.text);
55
+ if (target !== void 0) {
56
+ if (target < minTarget) minTarget = target;
57
+ if (marker > maxMarker) maxMarker = marker;
58
+ }
59
+ }
60
+ if (Number.isFinite(minTarget)) {
61
+ for (const key of snap.order) {
62
+ const node = snap.nodes.get(key);
63
+ if (node === void 0) continue;
64
+ const anchor = node.anchorSeq;
65
+ if (anchor >= minTarget && anchor <= maxMarker) hidden.add(anchor);
66
+ }
67
+ }
68
+ return hidden;
69
+ }
70
+
35
71
  // src/client/locales.ts
36
72
  var zh = {
37
73
  "button.aria": "\u56DE\u9000\u5230\u6B64\u6D88\u606F",
38
74
  "button.title": "\u56DE\u9000",
39
75
  "popover.title": "\u56DE\u9000\u5230\u8FD9\u6761\u6D88\u606F",
40
- "popover.target": "seq {seq} \xB7 {time}",
76
+ "popover.noText": "\uFF08\u65E0\u6587\u672C\uFF09",
41
77
  "popover.chat": "\u4EC5\u56DE\u9000\u5BF9\u8BDD",
42
78
  "popover.chat.hint": "\u53EA\u56DE\u9000\u6A21\u578B\u4E0A\u4E0B\u6587\uFF0C\u4E0D\u52A8\u5DE5\u4F5C\u533A\u6587\u4EF6",
43
79
  "popover.both": "\u56DE\u9000\u5BF9\u8BDD\u548C\u4EE3\u7801",
44
80
  "popover.both.hint": "\u5BF9\u8BDD\u56DE\u9000\u5E76\u8FD8\u539F\u5DE5\u4F5C\u533A\u6587\u4EF6",
81
+ "popover.checking": "\u6B63\u5728\u68C0\u67E5\u6587\u4EF6\u53D8\u66F4\u2026",
82
+ "popover.noChanges": "\u6B64\u6D88\u606F\u4E4B\u540E\u6CA1\u6709\u53EF\u8FD8\u539F\u7684\u6587\u4EF6\u53D8\u66F4\uFF0C\u4EC5\u53EF\u56DE\u9000\u5BF9\u8BDD",
45
83
  "popover.cancel": "\u53D6\u6D88",
46
84
  "popover.impact.loading": "\u6B63\u5728\u83B7\u53D6\u5F71\u54CD\u6E05\u5355\u2026",
47
85
  "popover.impact.failed": "\u65E0\u6CD5\u83B7\u53D6\u5F71\u54CD\u6E05\u5355\uFF1A{message}",
48
- "popover.impact.none": "\u76EE\u6807\u4E4B\u540E\u6CA1\u6709\u53F0\u8D26\u8BB0\u5F55\u7684\u5199\u7C7B\u53D8\u66F4\uFF0C\u65E0\u9700\u8FD8\u539F\u6587\u4EF6\u3002",
86
+ "popover.impact.none": "\u76EE\u6807\u4E4B\u540E\u6CA1\u6709\u8DDF\u8E2A\u5230\u7684\u5199\u7C7B\u53D8\u66F4\uFF0C\u65E0\u9700\u8FD8\u539F\u6587\u4EF6\u3002",
49
87
  "popover.confirm": "\u786E\u8BA4\u56DE\u9000",
50
88
  "popover.back": "\u8FD4\u56DE",
51
- "guard.hint": "/rewind \u4E0D\u63A5\u53D7\u53C2\u6570\uFF0C\u53EA\u80FD\u64A4\u56DE\u6700\u8FD1\u4E00\u6761\u6D88\u606F\uFF1B\u56DE\u9000\u5230\u66F4\u65E9\u6D88\u606F\u8BF7\u7528\u8BE5\u6D88\u606F\u65C1\u7684 \u21B6 \u6309\u94AE"
89
+ "guard.hint": "/rewind \u547D\u4EE4\u4E0D\u652F\u6301\u624B\u52A8\u8F93\u5165\uFF0C\u8BF7\u4F7F\u7528\u6D88\u606F\u65C1\u7684 \u21B6 \u56DE\u9000\u6309\u94AE"
52
90
  };
53
91
  var en = {
54
92
  "button.aria": "Rewind to this message",
55
93
  "button.title": "Rewind",
56
94
  "popover.title": "Rewind to this message",
57
- "popover.target": "seq {seq} \xB7 {time}",
95
+ "popover.noText": "(no text)",
58
96
  "popover.chat": "Rewind conversation only",
59
97
  "popover.chat.hint": "Cut the model context only; workspace files stay untouched",
60
98
  "popover.both": "Rewind conversation and code",
61
99
  "popover.both.hint": "Cut the context and restore workspace files",
100
+ "popover.checking": "Checking for file changes\u2026",
101
+ "popover.noChanges": "No tracked file changes after this message; conversation-only rewind",
62
102
  "popover.cancel": "Cancel",
63
103
  "popover.impact.loading": "Fetching impact list\u2026",
64
104
  "popover.impact.failed": "Could not fetch the impact list: {message}",
65
- "popover.impact.none": "No ledger-recorded file changes after the target; nothing to restore.",
105
+ "popover.impact.none": "No tracked file changes after the target; nothing to restore.",
66
106
  "popover.confirm": "Confirm rewind",
67
107
  "popover.back": "Back",
68
- "guard.hint": "/rewind takes no parameters \u2014 it only withdraws the most recent message. To rewind to an earlier message, use the \u21B6 button on that message."
108
+ "guard.hint": "/rewind cannot be typed manually \u2014 use the \u21B6 button on a message to rewind."
69
109
  };
70
110
 
71
111
  // src/client/styles.ts
@@ -114,9 +154,10 @@ var STYLE = `
114
154
  z-index: 1000;
115
155
  width: 288px;
116
156
  padding: 12px;
157
+ border: 1px solid var(--dsw-alias-border-l2);
117
158
  border-radius: 12px;
118
- background: var(--dsw-specific-surface-1, var(--dsw-alias-surface-1, #1f2127));
119
- box-shadow: 0 8px 28px rgba(0, 0, 0, 0.32);
159
+ background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));
160
+ box-shadow: var(--dsw-shadow-lv3);
120
161
  font-size: 14px;
121
162
  line-height: 20px;
122
163
  color: var(--dsw-alias-label-primary);
@@ -151,6 +192,10 @@ var STYLE = `
151
192
  .dsh-rewind-popover-option:hover {
152
193
  background: var(--dsw-alias-interactive-bg-hover);
153
194
  }
195
+ .dsh-rewind-popover-option:disabled {
196
+ opacity: 0.5;
197
+ cursor: default;
198
+ }
154
199
  .dsh-rewind-popover-option-label {
155
200
  font-weight: 500;
156
201
  }
@@ -187,8 +232,11 @@ var STYLE = `
187
232
  cursor: pointer;
188
233
  }
189
234
  .dsh-rewind-popover-primary {
190
- background: var(--dsw-alias-accent, var(--dsw-accent, #5b8cff));
191
- color: var(--dsw-alias-on-accent, #fff);
235
+ background: var(--dsw-alias-button-primary-fill);
236
+ color: var(--dsw-alias-label-primary-foreground);
237
+ }
238
+ .dsh-rewind-popover-primary:hover:not(:disabled) {
239
+ background: var(--dsw-alias-button-primary-hover);
192
240
  }
193
241
  .dsh-rewind-popover-primary:disabled {
194
242
  opacity: 0.5;
@@ -207,9 +255,10 @@ var STYLE = `
207
255
  z-index: 1000;
208
256
  max-width: min(440px, calc(100vw - 24px));
209
257
  padding: 8px 12px;
258
+ border: 1px solid var(--dsw-alias-border-l2);
210
259
  border-radius: 10px;
211
- background: var(--dsw-specific-surface-1, var(--dsw-alias-surface-1, #1f2127));
212
- box-shadow: 0 8px 28px rgba(0, 0, 0, 0.32);
260
+ background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));
261
+ box-shadow: var(--dsw-shadow-lv3);
213
262
  font-size: 13px;
214
263
  line-height: 18px;
215
264
  color: var(--dsw-alias-label-primary);
@@ -230,11 +279,11 @@ function closePopover() {
230
279
  disposeOutside = null;
231
280
  }
232
281
  }
233
- function formatTarget(seq, time, preview) {
282
+ function formatTarget(t, seq, time, preview) {
234
283
  const d = new Date(time);
235
284
  const hh = String(d.getHours()).padStart(2, "0");
236
285
  const mm = String(d.getMinutes()).padStart(2, "0");
237
- const previewText = preview.length > 0 ? preview : "(no text)";
286
+ const previewText = preview.length > 0 ? preview : t("popover.noText");
238
287
  return `seq ${seq} \xB7 ${hh}:${mm} \xB7 ${previewText}`;
239
288
  }
240
289
  function findCommand(snapshot, match) {
@@ -269,6 +318,15 @@ function waitForCommand(session, match, timeoutMs = 8e3) {
269
318
  check();
270
319
  });
271
320
  }
321
+ function isPreviewFor(node, seq) {
322
+ const args = node.args ?? "";
323
+ return node.name === "rewind" && args.includes("preview") && new RegExp(`(?:^|\\s)@${seq}(?=\\s|$)`).test(args);
324
+ }
325
+ async function previewImpact(session, seq) {
326
+ const result = await session.command(`/rewind preview @${seq} both`);
327
+ if (!result.ok || result.value?.matched !== true) return null;
328
+ return waitForCommand(session, (node) => isPreviewFor(node, seq));
329
+ }
272
330
  function el(tag, className, text) {
273
331
  const node = document.createElement(tag);
274
332
  node.className = className;
@@ -285,7 +343,7 @@ function modeOption(label, hint, onClick) {
285
343
  button.addEventListener("click", onClick);
286
344
  return button;
287
345
  }
288
- function renderImpactStep(root, opts, back) {
346
+ function renderImpactStep(root, opts, back, cached) {
289
347
  const { session, seq, t } = opts;
290
348
  const impact = el("div", CLASS.popoverImpact, t("popover.impact.loading"));
291
349
  const actions = el("div", CLASS.popoverActions);
@@ -302,24 +360,10 @@ function renderImpactStep(root, opts, back) {
302
360
  confirm.disabled = true;
303
361
  actions.append(confirm);
304
362
  root.replaceChildren(impact, actions);
305
- let outcome = null;
306
363
  void (async () => {
307
- const result = await session.command(`/rewind preview @${seq} both`);
308
- if (!result.ok || result.value?.matched !== true) {
309
- impact.textContent = t("popover.impact.failed", {
310
- message: result.ok ? "command not matched" : result.error?.message ?? "unknown error"
311
- });
312
- return;
313
- }
314
- outcome = await waitForCommand(
315
- session,
316
- (node) => {
317
- const args = node.args ?? "";
318
- return node.name === "rewind" && args.includes("preview") && new RegExp(`(?:^|\\s)@${seq}(?=\\s|$)`).test(args);
319
- }
320
- );
364
+ const outcome = cached ?? await previewImpact(session, seq);
321
365
  if (outcome === null) {
322
- impact.textContent = t("popover.impact.failed", { message: "timeout" });
366
+ impact.textContent = t("popover.impact.failed", { message: "preview command failed or timed out" });
323
367
  return;
324
368
  }
325
369
  if (outcome.kind === "error") {
@@ -342,41 +386,65 @@ function openPopover(opts) {
342
386
  const root = el("div", CLASS.popover);
343
387
  root.setAttribute("role", "dialog");
344
388
  root.setAttribute("aria-label", t("popover.title"));
345
- root.append(
346
- el("div", CLASS.popoverTitle, t("popover.title")),
347
- el("div", CLASS.popoverTarget, formatTarget(seq, time, preview))
348
- );
389
+ let bothState = { state: "loading" };
390
+ let impactOutcome = null;
349
391
  const renderModes = () => {
350
- root.replaceChildren(
392
+ const children = [
351
393
  el("div", CLASS.popoverTitle, t("popover.title")),
352
- el("div", CLASS.popoverTarget, formatTarget(seq, time, preview)),
394
+ el("div", CLASS.popoverTarget, formatTarget(t, seq, time, preview)),
353
395
  modeOption(t("popover.chat"), t("popover.chat.hint"), () => {
354
396
  closePopover();
355
397
  void session.command(`/rewind @${seq} chat`);
356
- }),
357
- modeOption(t("popover.both"), t("popover.both.hint"), () => {
358
- renderImpactStep(root, opts, renderModes);
359
- }),
360
- (() => {
361
- const actions = el("div", CLASS.popoverActions);
362
- const cancel = document.createElement("button");
363
- cancel.type = "button";
364
- cancel.className = CLASS.popoverGhost;
365
- cancel.textContent = t("popover.cancel");
366
- cancel.addEventListener("click", closePopover);
367
- actions.append(cancel);
368
- return actions;
369
- })()
370
- );
398
+ })
399
+ ];
400
+ if (bothState.state === "noChanges") {
401
+ children.push(el("div", CLASS.popoverImpact, t("popover.noChanges")));
402
+ } else {
403
+ const option = modeOption(
404
+ t("popover.both"),
405
+ bothState.state === "loading" ? t("popover.checking") : t("popover.both.hint"),
406
+ () => {
407
+ renderImpactStep(root, opts, renderModes, impactOutcome);
408
+ }
409
+ );
410
+ if (bothState.state === "loading") option.disabled = true;
411
+ children.push(option);
412
+ }
413
+ const actions = el("div", CLASS.popoverActions);
414
+ const cancel = document.createElement("button");
415
+ cancel.type = "button";
416
+ cancel.className = CLASS.popoverGhost;
417
+ cancel.textContent = t("popover.cancel");
418
+ cancel.addEventListener("click", closePopover);
419
+ actions.append(cancel);
420
+ children.push(actions);
421
+ root.replaceChildren(...children);
422
+ };
423
+ const position = () => {
424
+ const rect = anchor.getBoundingClientRect();
425
+ const gap = 4;
426
+ const height = root.offsetHeight;
427
+ const top = rect.bottom + gap + height <= window.innerHeight - 8 ? rect.bottom + gap : Math.max(8, rect.top - gap - height);
428
+ root.style.top = `${Math.round(top)}px`;
429
+ root.style.left = `${Math.round(Math.min(rect.right, window.innerWidth - 8 - root.offsetWidth))}px`;
371
430
  };
372
431
  renderModes();
373
432
  document.body.append(root);
374
- const rect = anchor.getBoundingClientRect();
375
- const gap = 4;
376
- const height = root.offsetHeight;
377
- const top = rect.bottom + gap + height <= window.innerHeight - 8 ? rect.bottom + gap : Math.max(8, rect.top - gap - height);
378
- root.style.top = `${Math.round(top)}px`;
379
- root.style.left = `${Math.round(Math.min(rect.right, window.innerWidth - 8 - root.offsetWidth))}px`;
433
+ position();
434
+ const hasFileImpact = (text) => text === void 0 || text.includes("\u5C06\u5F71\u54CD");
435
+ void (async () => {
436
+ const outcome = await previewImpact(session, seq);
437
+ impactOutcome = outcome;
438
+ if (outcome !== null && outcome.kind === "success") {
439
+ bothState = { state: hasFileImpact(outcome.text) ? "hasChanges" : "noChanges" };
440
+ }
441
+ renderModes();
442
+ position();
443
+ })().catch(() => {
444
+ bothState = { state: "hasChanges" };
445
+ renderModes();
446
+ position();
447
+ });
380
448
  popoverEl = root;
381
449
  const onPointerDown = (event) => {
382
450
  const target = event.target;
@@ -405,41 +473,9 @@ var USER_SEAT_SELECTOR = '[data-chat-flow-kind="user"]';
405
473
  var CHAT_SEAT_SELECTOR = "[data-chat-anchor-key]";
406
474
  var ACTIONS_ROOT_SELECTOR = "[data-time-hover-root]";
407
475
  var COMPOSER_SELECTOR = "[data-input-scroll] textarea, textarea[data-phase]";
408
- function targetOfOutcome(text) {
409
- if (text === void 0) return void 0;
410
- const match = text.match(/seq (\d+)/);
411
- return match !== null ? Number(match[1]) : void 0;
412
- }
413
476
  function messagePreviewOf(node) {
414
- return node.content.map((block) => block.type === "text" && typeof block.text === "string" ? block.text : "").join("").replace(/\s+/g, " ").trim().slice(0, 80);
415
- }
416
- function hiddenSeqsOf(session) {
417
- const hidden = /* @__PURE__ */ new Set();
418
- const snap = session.getSnapshot();
419
- let latest = null;
420
- for (const key of snap.chat.order) {
421
- const node = snap.chat.nodes.get(key);
422
- if (node === void 0 || node.kind !== "command") continue;
423
- const command = node.data;
424
- if (command.name !== "rewind") continue;
425
- if (command.outcome?.kind !== "success") continue;
426
- if (command.outcome.sourceEventSeq === void 0) continue;
427
- hidden.add(command.seq);
428
- const marker = command.outcome.sourceEventSeq;
429
- const target = targetOfOutcome(command.outcome.text);
430
- if (target !== void 0 && (latest === null || marker > latest.marker)) {
431
- latest = { marker, target };
432
- }
433
- }
434
- if (latest !== null) {
435
- for (const key of snap.chat.order) {
436
- const node = snap.chat.nodes.get(key);
437
- if (node === void 0) continue;
438
- const anchor = node.anchorSeq;
439
- if (anchor >= latest.target && anchor <= latest.marker) hidden.add(anchor);
440
- }
441
- }
442
- return hidden;
477
+ const text = node.content.map((block) => block.type === "text" && typeof block.text === "string" ? block.text : "").join("").replace(/\s+/g, " ").trim();
478
+ return text.length <= 80 ? text : `${text.slice(0, 79)}\u2026`;
443
479
  }
444
480
  function userTextAt(session, seq) {
445
481
  const snap = session.getSnapshot();
@@ -474,6 +510,13 @@ function apply(ctx) {
474
510
  const hidden = /* @__PURE__ */ new WeakSet();
475
511
  const buttons = /* @__PURE__ */ new Map();
476
512
  let observer = null;
513
+ const refreshButtonLabels = () => {
514
+ for (const button of buttons.values()) {
515
+ button.setAttribute("aria-label", t("button.aria"));
516
+ button.title = t("button.title");
517
+ }
518
+ };
519
+ const unsubscribeLocale = ctx.locale.subscribe(refreshButtonLabels);
477
520
  const sessionFor = () => {
478
521
  const sessionId = ctx.sessions.list.getSnapshot().current;
479
522
  if (sessionId === void 0) return void 0;
@@ -524,7 +567,7 @@ function apply(ctx) {
524
567
  if (!button.isConnected) buttons.delete(key);
525
568
  }
526
569
  const session = sessionFor();
527
- const hiddenSeqs = session !== void 0 ? hiddenSeqsOf(session) : /* @__PURE__ */ new Set();
570
+ const hiddenSeqs = session !== void 0 ? hiddenSeqsOf(session.getSnapshot().chat) : /* @__PURE__ */ new Set();
528
571
  let hiddenCount = 0;
529
572
  for (const seat of document.querySelectorAll(CHAT_SEAT_SELECTOR)) {
530
573
  const key = seat.dataset.chatAnchorKey;
@@ -564,11 +607,11 @@ function apply(ctx) {
564
607
  if (fillComposer(text)) filled.add(target);
565
608
  }
566
609
  };
567
- const REWIND_WITH_ARGS = /^\s*\/rewind\s+\S/i;
610
+ const MANUAL_REWIND = /^\s*\/rewind(?:\s|$)/i;
568
611
  const composerTextarea = () => document.querySelector(COMPOSER_SELECTOR);
569
612
  const hasBlockedRewindDraft = () => {
570
613
  const textarea = composerTextarea();
571
- return textarea !== null && REWIND_WITH_ARGS.test(textarea.value);
614
+ return textarea !== null && MANUAL_REWIND.test(textarea.value);
572
615
  };
573
616
  let guardHintEl = null;
574
617
  let guardHintTimer;
@@ -621,6 +664,7 @@ function apply(ctx) {
621
664
  observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ["style"] });
622
665
  scan();
623
666
  yield () => {
667
+ unsubscribeLocale();
624
668
  document.removeEventListener("keydown", onKeyDownGuard, true);
625
669
  document.removeEventListener("click", onClickGuard, true);
626
670
  if (guardHintEl !== null) guardHintEl.remove();