dsh-rewind-plugin 0.7.2 → 0.7.4

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)
@@ -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
@@ -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) | 中文
@@ -146,8 +146,7 @@ dsh plugin --profile web add dsh-rewind-plugin
146
146
  5. **导轨显示已回退轮次**——DSH `v0.1.2-alpha.1` 新增的右侧导轨,会为已撤回消息保留刻度:点击不跳转、悬浮显示已撤回正文。仅显示差异,无功能影响。
147
147
 
148
148
  > [!NOTE]
149
- > 若使用中遇到问题,可开启详细诊断输出,复现后附上控制台 `[dsh-rewind]` 输出便于定位。详见
150
- > [浏览器诊断与详细输出开关](docs/compat/diagnostics.zh.md)。
149
+ > 本插件提供浏览器端诊断输出;详见 [浏览器诊断](docs/compat/diagnostics.zh.md)。
151
150
 
152
151
  ## 安全
153
152
 
package/docs/README.md CHANGED
@@ -15,7 +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
+ | `compat/diagnostics.md` | Browser diagnostics (anomaly alerts; verbose switch gates the startup identity line) (`.zh` mirror) | users / maintainers |
19
19
  | `release/release.md` | Release workflow & DSH peer-version alignment (`.zh` mirror) | maintainers |
20
20
 
21
21
  Repo-root docs outside `docs/`: `SECURITY.md` (security model) and
@@ -22,15 +22,15 @@
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
25
+ > `0.1.2-alpha.2` … `0.1.2-alpha.5` verified-compatible. Built/tested on the rc.2
26
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.
27
+ > line, and `alpha.2`/`alpha.3`/`alpha.4`/`alpha.5` share the `0.1.2` tuple, so the
28
+ > single `^0.1.2-alpha.2` peer member covers all four. `rc.2` (`latest`) remains the
29
+ > primary baseline; five versions verified: rc.2, alpha.2, alpha.3, alpha.4, alpha.5.
30
30
 
31
31
  ### Channeled seams (version × channel)
32
32
 
33
- | Seam | rc.2 (`0.1.1-rc.2`) | alpha.2 / alpha.3 | alpha.4 |
33
+ | Seam | rc.2 (`0.1.1-rc.2`) | alpha.2 / alpha.3 | alpha.4 / alpha.5 |
34
34
  | --- | --- | --- | --- |
35
35
  | Host session log (`eventsOf`) | `Session.events` | `Session.events` | `session.snapshotEvents()` |
36
36
  | Client chat snapshot (`chatSnapshotOf`) | session-face `chat` field | `uiConversation` `chat` view | `uiConversation` `chat` view |
@@ -1,54 +1,57 @@
1
- # Browser diagnostics and the verbose switch
1
+ # Browser diagnostics
2
2
 
3
3
  [简体中文](diagnostics.zh.md)
4
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.
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.
8
10
 
9
- ## Scope-prefixed output
11
+ ## Always-on anomaly alerts
10
12
 
11
- Diagnostics are printed as `[dsh-rewind:<scope>] ...`. The scope names the
12
- subsystem, and is also what the verbose switch filters on:
13
+ `error` and `warn` are always printed (no switch, no configuration) so a fault
14
+ surfaces even for a user who never configured anything:
13
15
 
14
- | Scope | What it reports |
16
+ | Level | Meaning |
15
17
  | --- | --- |
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 |
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
20
 
21
- ## Levels
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.
22
25
 
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) |
26
+ ## Scopes
29
27
 
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.
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:
35
31
 
36
- ## Enabling verbose output
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. |
37
39
 
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]`:
40
+ ## Verbose switch
42
41
 
43
- ```js
44
- // Every dsh-rewind namespace.
45
- localStorage['dsh-rewind.debug'] = 'dsh-rewind*'
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:
46
48
 
47
- // Or just the subsystem you care about (exact scope match).
48
- localStorage['dsh-rewind.debug'] = 'dsh-rewind:refill'
49
+ ```js
50
+ // Just the startup identity line.
51
+ localStorage['dsh-rewind.debug'] = 'dsh-rewind:boot'
49
52
 
50
- // Several at once (comma-separated).
51
- localStorage['dsh-rewind.debug'] = 'dsh-rewind:refill,dsh-rewind:hiding'
53
+ // Or everything (avoids needing an exact scope).
54
+ localStorage['dsh-rewind.debug'] = 'dsh-rewind*'
52
55
  ```
53
56
 
54
57
  Reload (`F5`) after setting it. To switch it off:
@@ -57,26 +60,26 @@ Reload (`F5`) after setting it. To switch it off:
57
60
  delete localStorage['dsh-rewind.debug']
58
61
  ```
59
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
+
60
68
  ## Capturing a report
61
69
 
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
70
+ 1. Reproduce once on the affected page.
71
+ 2. In DevTools, filter the Console for `[dsh-rewind]` and copy the output
66
72
  (with the plugin version and the DSH/kernel version).
67
73
 
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.
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.
73
78
 
74
79
  ## Notes
75
80
 
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.
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.
@@ -1,44 +1,42 @@
1
- # 浏览器诊断与详细输出开关
1
+ # 浏览器诊断
2
2
 
3
3
  [English](diagnostics.md)
4
4
 
5
- 本插件在浏览器端输出的所有诊断信息都经由同一条通道,这样可用一个控制台过滤器抓取报告,且无需重建插件即可开启详细输出模式。
5
+ 本插件在浏览器端输出的所有诊断信息都经由同一条分级通道,因此异常可在单个控制台过滤器下浮现。该通道**刻意收窄**:只记录插件自身检测到的、偏离预期契约的真实内部异常——**不记录**用户早已在屏幕上看到、且无归因价值的普通路径事实。
6
6
 
7
- ## 按作用域前缀输出
7
+ ## 常开的异常告警
8
8
 
9
- 诊断以 `[dsh-rewind:<scope>] ...` 形式打印。`scope` 标明子系统,同时也正是详细输出开关的过滤依据:
9
+ `error` `warn` **总是打印**(无需开关、无需配置),即使从未配置的用户也能看到故障:
10
10
 
11
- | scope | 报告内容 |
11
+ | 级别 | 含义 |
12
12
  | --- | --- |
13
- | `hiding` | 行隐藏路径(回退切断哪些行;`rewind not hidden` 异常) |
14
- | `refill` | 回退后的输入框回填(目标 seq、模式、所用通道、写入结果) |
15
- | `portals` | 每条消息按钮的挂载问题(如无会话绑定) |
16
- | `settings` | 快照清理设置卡片 |
13
+ | `error` | 意外/破坏性故障(如设置卡片注册失败) |
14
+ | `warn` | 可恢复的异常护栏(如回退命令/回填抛出异常、等待的结果始终未落定、会话绑定缺失) |
17
15
 
18
- ## 分级
16
+ 这些情形少见、廉价、且偏离契约。普通路径的细节(回填走了哪个分支、写入是否匹配、回退切断哪些行)则**刻意不记录**:它们重复用户已经看到的现象,对归因毫无增量。
19
17
 
20
- | 级别 | 默认 | 含义 |
21
- | --- | --- | --- |
22
- | `error` | 开 | 意外/破坏性;总是打印 |
23
- | `warn` | 开 | 可恢复的异常护栏(`rewind not hidden`、`refill skipped/refused/threw`);总是打印 |
24
- | `info` | 关 | 事件级生命周期(每次回退/回填一行) |
25
- | `debug` | 关 | 开关控制下的追加事件级细节(隐藏集一行,每次回退一行) |
18
+ ## 作用域
26
19
 
27
- `error`/`warn` 总是打印,这样即使没碰过开关的用户,异常也会浮现;`info`/`debug` 则受开关控制,保持普通用户控制台干净。两个受控级别都按**相关事件触发一次**(一次回退、一次回填),不会随流式逐帧打印——所以即使开了详细输出,回退后的会话也不会刷屏。
20
+ 诊断以 `[dsh-rewind:<scope>] ...` 形式打印。`scope` 标明异常所属子系统,因此可用单个控制台过滤器抓取报告:
28
21
 
29
- ## 开启详细输出
22
+ | scope | 这里的异常含义 |
23
+ | --- | --- |
24
+ | `boot` | 启动身份行——`loaded v<version> (build <hash>)`,由详细输出开关控制(见下)。用于确认运行中的 bundle 是否匹配某个修复。 |
25
+ | `refill` | 回退后的输入框回填(命令/等待/回填抛出异常、等待的结果始终未落定) |
26
+ | `portals` | 每条消息按钮的挂载问题(如无会话绑定) |
27
+ | `settings` | 快照清理设置卡片 |
28
+ | `hiding` | **保留区**——当前无活跃告警。若未来新增行隐藏诊断,应归属此区域。 |
30
29
 
31
- 开关位于**浏览器**的 `localStorage`、且使用插件专属键,因此它不会开启其它插件/功能,别的功能也无法唤醒本插件。设置后刷新页面、复现一次,再把控制台按 `[dsh-rewind]` 过滤即可:
30
+ ## 详细输出开关
32
31
 
33
- ```js
34
- // 全部 dsh-rewind 命名空间。
35
- localStorage['dsh-rewind.debug'] = 'dsh-rewind*'
32
+ `info`/`debug` 级别受 `localStorage['dsh-rewind.debug']` 控制,每次调用读取并按命名空间过滤。在撤下那些复述现象的详细输出后,**恰好剩一条** verbose 行:`boot` 作用域的启动身份行。它**默认关闭**(普通用户控制台保持干净,且它不是异常),排查者需先开启该作用域才能看到:
36
33
 
37
- // 或只开所关心的子系统(精确 scope 匹配)。
38
- localStorage['dsh-rewind.debug'] = 'dsh-rewind:refill'
34
+ ```js
35
+ // 仅启动身份行。
36
+ localStorage['dsh-rewind.debug'] = 'dsh-rewind:boot'
39
37
 
40
- // 同时开多个(逗号分隔)。
41
- localStorage['dsh-rewind.debug'] = 'dsh-rewind:refill,dsh-rewind:hiding'
38
+ // 或全部开启(省去精确 scope)。
39
+ localStorage['dsh-rewind.debug'] = 'dsh-rewind*'
42
40
  ```
43
41
 
44
42
  设置后刷新(`F5`)。关闭:
@@ -47,17 +45,16 @@ localStorage['dsh-rewind.debug'] = 'dsh-rewind:refill,dsh-rewind:hiding'
47
45
  delete localStorage['dsh-rewind.debug']
48
46
  ```
49
47
 
48
+ `error`/`warn` 异常告警**不受**该开关控制——它们总是打印。其余详细输出(所用写入通道、空的隐藏集、每次回退的生命周期行)已撤下:它们重复用户已经看到的现象、无归因增量。
49
+
50
50
  ## 采集一段报告
51
51
 
52
- 1. 在**复现问题的那台机器/那个浏览器页面**,开启相应命名空间(见上)并刷新。
53
- 2. 复现一次。
54
- 3. 在 DevTools 里把 Console 按 `[dsh-rewind]` 过滤,复制输出(连同插件版本、DSH/内核版本)。
52
+ 1. 在出问题的页面上复现一次。
53
+ 2. 在 DevTools 里把 Console 按 `[dsh-rewind]` 过滤,复制输出(连同插件版本、DSH/内核版本)。
55
54
 
56
- 对「回退后没回填输入框」这类问题,重点是 `refill` 作用域:其 `composer write` 一行会报告草稿是通过 harness facade(`facade`)还是 DOM 回退(`dom`)恢复、写入是否成功,从而区分「插件没写入」与「harness 侧渲染不同步」。
55
+ `error`/`warn` 异常告警无需任何设置——它们总是打印。若要同时采集启动身份行(`boot`,用于排除「旧 bundle / 宿主未重启」),请先开启详细输出开关(见上),再刷新。
57
56
 
58
57
  ## 备注
59
58
 
60
- - 该开关是维护者与配合排查者使用的工具,**不是**稳定公开接口;其具体键与输出可能不经通知而改变。
61
- - 它受单个浏览器 origin、单个浏览器限定;请在实际出问题的地方开启。
62
-
63
- 相关:[audit.md](audit.md) 为已验证兼容矩阵,[troubleshooting.md](troubleshooting.md) 为历史修复步骤。
59
+ - 该诊断表面是维护者与配合排查者使用的工具,**不是**稳定公开接口;其具体键与输出可能不经通知而改变。
60
+ - 它受单个浏览器 origin、单个浏览器限定;请在实际出问题的地方采集。
@@ -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
+ rc.2 uses border-radius 12px + 1px solid border-l2, while alpha uses 16px +
333
+ 0.5px border-l4 (and adds corner-shape: round). We intentionally keep the
334
+ rc.2 values here for now; if the alpha look is wanted, switch these on the
335
+ host version like the other dual-channel surfaces (see the host-version probe
336
+ 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 {
@@ -1035,12 +1048,11 @@ async function runRewindAndFill(session, seq, mode, currentSessionId, chatOf, se
1035
1048
  return;
1036
1049
  }
1037
1050
  if (!result.ok || result.value?.matched !== true) {
1038
- rewindLog.info("refill", `rewind @${seq} not matched, no refill`);
1039
1051
  return;
1040
1052
  }
1041
1053
  let outcome;
1042
1054
  try {
1043
- 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));
1044
1056
  } catch (error) {
1045
1057
  rewindLog.warn("refill", `waiting for rewind @${seq} outcome threw`, error);
1046
1058
  return;
@@ -1050,25 +1062,10 @@ async function runRewindAndFill(session, seq, mode, currentSessionId, chatOf, se
1050
1062
  return;
1051
1063
  }
1052
1064
  if (outcome.kind !== "success") {
1053
- rewindLog.warn("refill", `rewind @${seq} refused`, outcome.text);
1054
1065
  showHint(outcome.text ?? "rewind failed");
1055
1066
  return;
1056
1067
  }
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
1068
  if (currentSessionId() !== session.sessionId) {
1071
- rewindLog.info("refill", `skipped refill @${seq}: session switched during rewind`);
1072
1069
  return;
1073
1070
  }
1074
1071
  let text;
@@ -1079,21 +1076,17 @@ async function runRewindAndFill(session, seq, mode, currentSessionId, chatOf, se
1079
1076
  return;
1080
1077
  }
1081
1078
  if (text === void 0 || text === "") {
1082
- rewindLog.info("refill", `skipped refill @${seq}: no editable text`);
1083
1079
  return;
1084
1080
  }
1085
1081
  if (composerText().trim() !== "") {
1086
- rewindLog.info("refill", `skipped refill @${seq}: composer already has a draft`);
1087
1082
  return;
1088
1083
  }
1089
- let ok = false;
1090
1084
  try {
1091
- ok = setComposerText(session.sessionId, text);
1085
+ setComposerText(session.sessionId, text);
1092
1086
  } catch (error) {
1093
1087
  rewindLog.warn("refill", `composer refill @${seq} threw`, error);
1094
1088
  return;
1095
1089
  }
1096
- rewindLog.info("refill", `rewound to @${seq} (${mode})`, { ok, text: text.slice(0, 80) });
1097
1090
  }
1098
1091
  function composerSurface() {
1099
1092
  return document.querySelector(COMPOSER_TEXTAREA_SELECTOR) ?? document.querySelector(COMPOSER_EDITABLE_SELECTOR);
@@ -1189,7 +1182,7 @@ function sameTargets(left, right) {
1189
1182
  return other.kind === "pending" && target.itemId === other.itemId;
1190
1183
  });
1191
1184
  }
1192
- function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, t, subscribeLocale, setComposerText }) {
1185
+ function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, watchChat, t, subscribeLocale, setComposerText }) {
1193
1186
  const [targets, setTargets] = (0, import_react.useState)([]);
1194
1187
  const hidden = (0, import_react.useRef)(/* @__PURE__ */ new WeakSet());
1195
1188
  const [, forceRender] = (0, import_react.useReducer)((count) => count + 1, 0);
@@ -1250,6 +1243,7 @@ function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, t, subs
1250
1243
  sessionId,
1251
1244
  sessionOf,
1252
1245
  chatOf,
1246
+ watchChat,
1253
1247
  setComposerText,
1254
1248
  t
1255
1249
  },
@@ -1261,6 +1255,7 @@ function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, t, subs
1261
1255
  sessionId,
1262
1256
  sessionOf,
1263
1257
  chatOf,
1258
+ watchChat,
1264
1259
  currentSessionId,
1265
1260
  setComposerText,
1266
1261
  t
@@ -1271,7 +1266,7 @@ function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, t, subs
1271
1266
  target.key
1272
1267
  ));
1273
1268
  }
1274
- function RewindButton({ target, sessionId, sessionOf, chatOf, currentSessionId, setComposerText, t }) {
1269
+ function RewindButton({ target, sessionId, sessionOf, chatOf, watchChat, currentSessionId, setComposerText, t }) {
1275
1270
  const onClick = (event) => {
1276
1271
  event.stopPropagation();
1277
1272
  const session = sessionOf(sessionId);
@@ -1284,13 +1279,14 @@ function RewindButton({ target, sessionId, sessionOf, chatOf, currentSessionId,
1284
1279
  openPopover({
1285
1280
  session,
1286
1281
  chatOf,
1282
+ watchChat,
1287
1283
  seq: node.seq,
1288
1284
  time: node.time,
1289
1285
  preview: messagePreviewOf(node),
1290
1286
  anchor: event.currentTarget,
1291
1287
  t,
1292
1288
  onRewind: (mode) => {
1293
- void runRewindAndFill(session, node.seq, mode, currentSessionId, chatOf, setComposerText);
1289
+ void runRewindAndFill(session, node.seq, mode, currentSessionId, chatOf, watchChat, setComposerText);
1294
1290
  }
1295
1291
  });
1296
1292
  };
@@ -1323,7 +1319,7 @@ async function retractPending(session, itemId, text, setComposerText) {
1323
1319
  setComposerText(session.sessionId, text);
1324
1320
  }
1325
1321
  }
1326
- function RetractButton({ target, sessionId, sessionOf, chatOf, setComposerText, t }) {
1322
+ function RetractButton({ target, sessionId, sessionOf, chatOf, watchChat, setComposerText, t }) {
1327
1323
  const onClick = (event) => {
1328
1324
  event.stopPropagation();
1329
1325
  const session = sessionOf(sessionId);
@@ -1331,6 +1327,7 @@ function RetractButton({ target, sessionId, sessionOf, chatOf, setComposerText,
1331
1327
  openPopover({
1332
1328
  session,
1333
1329
  chatOf,
1330
+ watchChat,
1334
1331
  preview: target.preview,
1335
1332
  anchor: event.currentTarget,
1336
1333
  t,
@@ -1358,6 +1355,10 @@ function createRewindBridge(deps) {
1358
1355
  };
1359
1356
  }
1360
1357
 
1358
+ // src/client/build-info.ts
1359
+ var PLUGIN_VERSION = true ? "0.7.4" : "dev";
1360
+ var BUILD_HASH = true ? "73e01232" : "dev";
1361
+
1361
1362
  // src/client/locales.ts
1362
1363
  var zh = {
1363
1364
  "button.aria": "\u56DE\u9000\u5230\u6B64\u6D88\u606F",
@@ -1617,6 +1618,7 @@ var HEADER_ACTIONS_SLOT = "conversation.session.header.actions";
1617
1618
  var COMPOSER_TEXTAREA_SELECTOR2 = "[data-input-scroll] textarea, textarea[data-phase]";
1618
1619
  var COMPOSER_EDITABLE_SELECTOR2 = "[data-composer-input]";
1619
1620
  function apply(ctx) {
1621
+ rewindLog.info("boot", `loaded v${PLUGIN_VERSION} (build ${BUILD_HASH})`);
1620
1622
  ctx.effect(function* () {
1621
1623
  yield ctx.locale.register(NS2, { zh, en });
1622
1624
  const t = ctx.locale.bind(NS2);
@@ -1650,13 +1652,24 @@ function apply(ctx) {
1650
1652
  input.for(scope).setDraft(draft);
1651
1653
  } } : void 0
1652
1654
  );
1653
- rewindLog.info("refill", "composer write", { channel: facade ? "facade" : "dom", ok });
1654
1655
  return ok;
1655
1656
  } catch (error) {
1656
1657
  rewindLog.warn("refill", "composer write threw", error);
1657
1658
  return false;
1658
1659
  }
1659
1660
  };
1661
+ const watchChat = (sessionId, cb) => resolveChatWatch(
1662
+ (id) => {
1663
+ try {
1664
+ return uiConversation()?.binding(id).target(CHAT_VIEW);
1665
+ } catch {
1666
+ return void 0;
1667
+ }
1668
+ },
1669
+ (id) => sessionOf(id),
1670
+ sessionId,
1671
+ cb
1672
+ );
1660
1673
  const slots = ctx.slots;
1661
1674
  yield slots.inject(HEADER_ACTIONS_SLOT, () => slots.register(
1662
1675
  {
@@ -1666,7 +1679,7 @@ function apply(ctx) {
1666
1679
  id: "dsh-rewind-portals",
1667
1680
  order: 1e3
1668
1681
  },
1669
- createRewindBridge({ sessionOf, chatOf, currentSessionId, setComposerText, t, subscribeLocale })
1682
+ createRewindBridge({ sessionOf, chatOf, currentSessionId, watchChat, setComposerText, t, subscribeLocale })
1670
1683
  ));
1671
1684
  const clientCtx = ctx;
1672
1685
  clientCtx.inject(["settingsScope"], (scoped) => {
@@ -1712,7 +1725,7 @@ function apply(ctx) {
1712
1725
  const known = knownCommandSeqs(face, chatOf2, (node) => isCandidateCommand(node));
1713
1726
  const result = await face.command("/rewind __candidates");
1714
1727
  if (!result.ok || result.value?.matched !== true) return void 0;
1715
- const outcome = await waitForCommand(face, chatOf2, (node) => isCandidateCommand(node) && !known.has(node.seq));
1728
+ const outcome = await waitForCommand(face, chatOf2, (node) => isCandidateCommand(node) && !known.has(node.seq), 8e3, (cb) => watchChat(face.sessionId, cb));
1716
1729
  if (outcome === null || outcome.kind !== "success" || outcome.text === void 0) return void 0;
1717
1730
  return rewindCandidatesFromHostText(outcome.text);
1718
1731
  };
@@ -1747,13 +1760,14 @@ function apply(ctx) {
1747
1760
  openPopover({
1748
1761
  session: face,
1749
1762
  chatOf,
1763
+ watchChat,
1750
1764
  seq: candidate.seq,
1751
1765
  time: candidate.time,
1752
1766
  preview: candidate.preview,
1753
1767
  anchor: composerAnchor(),
1754
1768
  t,
1755
1769
  onRewind: (mode) => {
1756
- void runRewindAndFill(face, candidate.seq, mode, currentSessionId, chatOf, setComposerText);
1770
+ void runRewindAndFill(face, candidate.seq, mode, currentSessionId, chatOf, watchChat, setComposerText);
1757
1771
  }
1758
1772
  });
1759
1773
  }
@@ -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;
@@ -26,6 +26,28 @@ export type ChatOf = (session: {
26
26
  chat?: unknown;
27
27
  };
28
28
  } | undefined) => HiddenChat | undefined;
29
+ /**
30
+ * Subscribe to one session's live chat-update signal, for waiting on a chat
31
+ * snapshot change without polling. Dual-channel, mirroring `chatOf`: the
32
+ * alpha.1+ `uiConversation` "chat" view when registered, else the session face
33
+ * (rc.2, whose snapshot still carries `chat`, so its own `subscribe` is the
34
+ * chat-update signal). `cb` fires whenever the chat snapshot invalidates.
35
+ */
36
+ export type ChatWatch = (sessionId: string, cb: () => void) => () => void;
37
+ /**
38
+ * Choose the chat-update subscription for `waitForCommand`: prefer the
39
+ * `uiConversation` "chat" view's own `subscribe` when available (alpha.1+,
40
+ * where the chat-update signal lives), else the session face's `subscribe`
41
+ * (rc.2, whose snapshot still carries the chat, so its own subscribe is the
42
+ * chat-update signal). Extracted as a pure channel-selection step so the
43
+ * "view vs face" branch is unit-testable; the resolvers are injected by the
44
+ * caller (see `watchChat` in index.ts). Never throws.
45
+ */
46
+ export declare function resolveChatWatch(resolveView: (sessionId: string) => {
47
+ subscribe?(cb: () => void): () => void;
48
+ } | undefined, resolveFace: (sessionId: string) => {
49
+ subscribe(cb: () => void): () => void;
50
+ } | undefined, sessionId: string, cb: () => void): () => void;
29
51
  /**
30
52
  * Resolve the chat snapshot across the two harness channels: the session-face
31
53
  * snapshot first (rc.2 — on alpha.1+ the face no longer carries `chat`, so the
@@ -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 {
@@ -38,6 +38,13 @@ export interface PopoverOptions {
38
38
  * 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 (alpha.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. */
@@ -58,6 +58,13 @@ export interface RewindBridgeDeps {
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 (alpha.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;
@@ -124,7 +131,7 @@ 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).
@@ -163,7 +170,7 @@ interface RewindPortalsProps extends RewindBridgeDeps {
163
170
  * skipped when the target set is unchanged), so the plugin never runs a
164
171
  * synchronous full-transcript scan inside a commit microtask.
165
172
  */
166
- export declare function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, t, subscribeLocale, setComposerText }: RewindPortalsProps): ReactNode;
173
+ export declare function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, watchChat, t, subscribeLocale, setComposerText }: RewindPortalsProps): ReactNode;
167
174
  /** The current composer draft, on whichever channel: rc.2 textarea `.value`,
168
175
  * alpha.1+ contenteditable `textContent`. Empty when the composer is absent.
169
176
  * Exported as a test seam (the empty-composer guard in `retractPending`). */
@@ -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 rc.2 uses border-radius 12px + 1px solid border-l2, while alpha uses 16px +\n 0.5px border-l4 (and adds corner-shape: round). We intentionally keep the\n rc.2 values here for now; if the alpha look is wanted, switch these on the\n host version like the other dual-channel surfaces (see the host-version probe\n 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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-rewind-plugin",
3
- "version": "0.7.2",
3
+ "version": "0.7.4",
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",