dsh-rewind-plugin 0.14.0-alpha.1 → 0.14.0-beta.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.en.md CHANGED
@@ -10,14 +10,13 @@ Conversation rewind for DeepSeek Harness: **rewind the conversation to any earli
10
10
 
11
11
  A deliberately focused plugin with one job: **rewind to any user message, no matter how far back, in place** — and conveniently **restore the files it changed** along the way.
12
12
 
13
- - **Rewinding is time-travel** — the target message and everything after it (agent replies, tool calls) are withdrawn from the model context *and* the rendered transcript at once, with no new session and no window switch; the target's text is offered back in the composer so you can edit and re-send it — **truly seamless and convenient by design**.
14
- - **Lightweight workspace backup** — Claude Code-aligned behavior: tracks files edited by the write-class tools, and external changes to **already-tracked** files are restorable too. Selective tracking, before-write backup, store only on change. One lightweight plugin gives you a **complete** agentic rewind capability.
15
- - **Privacy-first** — the plugin never deletes or rewrites the session log (append-only) and never actually deletes any of your conversation; backups live in the plugin's own snapshot directory; restores draw only from those backups. Full security model: [SECURITY.md](SECURITY.md).
16
- - **A complete test system** — unit, probe, and end-to-end host verification, covering compatibility probing, log replay, resume, cross-restart and other scenarios; maintained continuously as the harness evolves to ensure feature stability.
13
+ - **In-place rewind, no new session** — the target message and everything after it (replies, tool calls) are withdrawn from the model context *and* the rendered transcript, and the target's text is filled back into the composer for editing and re-sending. It all happens inside the original session: no new branch, no leftover copy.
14
+ - **Conversation and code rewind together** — Claude Code-aligned behavior: files edited by the write tools are tracked and restored by the same rewind, and external changes to already-tracked files are restorable too. **Partial tracking, before-write backup, store only on change**, with no git dependency.
15
+ - **Safety guarantees** — the session log only ever appends rewind markers and is never deleted or rewritten; file backup and restore are hardened; tests are comprehensive, and maintenance continues across DSH upgrades. Security model: [SECURITY.md](SECURITY.md).
17
16
 
18
17
  ## Preview
19
18
 
20
- Every user message carries a **↶ rewind** button in its action row. Clicking it opens a mode-selection popover — "**rewind conversation only**" or "**rewind conversation and code**", the latter showing the file-change list for confirmation first. You can also rewind conveniently via the **`/rewind` command** or a **keyboard shortcut**.
19
+ Every user message carries a **↶ rewind** button in its action row. Clicking it opens a mode-selection popover — "**rewind conversation only**" or "**rewind conversation and code**", the latter showing the file-change list for confirmation first. You can also pick and rewind conveniently via the **`/rewind` and `/undo` commands**.
21
20
 
22
21
  <table>
23
22
  <tr>
@@ -35,11 +34,23 @@ Every user message carries a **↶ rewind** button in its action row. Clicking i
35
34
  Check your local DSH version, then find the matching plugin version in
36
35
  [Releases](https://github.com/SiriLee/dsh-rewind/releases).
37
36
 
37
+ **Command line**:
38
+
38
39
  ```sh
39
40
  dsh plugin --profile web add dsh-rewind-plugin@<version>
40
41
  ```
41
42
 
42
- > ⚠️ The npm name `dsh-rewind` belongs to another author's package — install with `dsh-rewind-plugin`.
43
+ **Graphical interface**: the **Plugins** page in the sidebar → **Add plugin** → enter `dsh-rewind-plugin@<version>` → install → restart DSH and refresh the browser.
44
+
45
+ <img src="assets/screenshots/add-plugin.png" width="600" alt="Add plugin dialog: enter dsh-rewind-plugin@version in the field"><br>
46
+ <sub>The screenshot is an example only — use the version that matches your DSH version.</sub>
47
+
48
+ **Updating**: the command line overwrites the installed version directly; the
49
+ graphical interface rejects a re-install, so uninstall the old version first,
50
+ then install the new one.
51
+
52
+ > [!WARNING]
53
+ > The npm name `dsh-rewind` belongs to another author's package — install with `dsh-rewind-plugin`.
43
54
 
44
55
  ## Usage
45
56
 
@@ -69,12 +80,12 @@ unaffected) and the plugin rebuilds them automatically.
69
80
 
70
81
  A **global auto-cleanup** (off by default) removes the snapshot directories of
71
82
  long-inactive sessions, leaving the active session and chat log untouched. Configure
72
- and review it in the **Settings &gt; Plugins &gt; Plugin configuration &gt; Snapshot
73
- cleanup** panel (the auto-cleanup switch and the idle-day cutoff), or use the
83
+ and review it on the **Plugins** page, in this plugin's configuration card (the
84
+ auto-cleanup switch and the idle-day cutoff), or use the
74
85
  `/snapshot-auto-cleanup` command to **view, configure, and run** it. See:
75
86
  [Snapshot cleanup](docs/snapshot-auto-cleanup.md).
76
87
 
77
- <img src="assets/screenshots/cleanup-setting.png" alt="Snapshot cleanup settings: auto-cleanup and idle days" width="600">
88
+ <img src="assets/screenshots/cleanup-setting.png" alt="Plugin configuration card: auto cleanup and idle time (days)" width="600">
78
89
 
79
90
  ## Uninstall
80
91
 
@@ -87,8 +98,8 @@ rm -rf <dsh home>/rewind-snapshots
87
98
  rm <dsh home>/snapshot-cleanup-last-sweep.json
88
99
  ```
89
100
 
90
- The plugin's auto-cleanup settings live in the settings document
91
- (`<dsh home>/settings.yaml`). To remove them completely, delete the key manually.
101
+ The plugin's auto-cleanup settings live in the current Profile's plugin
102
+ configuration. To remove them completely, delete this plugin entry's configuration.
92
103
 
93
104
  ## Why it stands out
94
105
 
@@ -158,8 +169,7 @@ withdrew should consume the stable, locale-independent helpers exported from
158
169
  1. **Exported logs are complete** — a rewind only removes messages from the model context and the view; the exported session log (`/export`) still contains **withdrawn messages**. This plugin cannot alter exports.
159
170
  2. **Lightweight file rewind has a cost** — in specific cases not all changes can be rewound. Consistent with Claude Code. See: [File-rewind tracking boundary](docs/compat/tracking-boundary.md).
160
171
  3. **The turn-rail shows rewound turns** — the right-side rail added in DSH `v0.1.2` keeps ticks for withdrawn messages, and hovering shows the withdrawn text. Only a display difference; no functional impact.
161
- 4. **The system prompt is re-displayed after a rewind** — in DSH `v0.1.2`, rewinding and resending a message shows the "System prompt" component again, just like `/compact`. Only a display difference; no functional impact.
162
- 5. **Old rewind markers are no longer compatible** — DSH `v0.1.3` rejects the rewind markers from the old plugin (≤ 0.8.0). The new version resolves this and provides an in-session update. See the [update guide](docs/rewind-fix.md).
172
+ 4. **Old rewind markers are no longer compatible** — DSH `v0.1.3` rejects the rewind markers from the old plugin (≤ 0.8.0). Later versions resolve this and provide an in-session update. See the [update guide](docs/rewind-fix.md).
163
173
 
164
174
  > [!NOTE]
165
175
  > Browser diagnostics are available; see [Browser diagnostics](docs/compat/diagnostics.md).
package/README.md CHANGED
@@ -10,14 +10,13 @@ DeepSeek Harness 插件:**一键就地回退对话到任意更早的用户消
10
10
 
11
11
  刻意聚焦、保持极简,只做一件事:**就地回退到任意远的用户消息**,还能**顺手还原改过的文件**。
12
12
 
13
- - **回退 = 时间回溯**——目标消息及其之后的全部内容(agent 回复、工具调用)同时从**模型上下文**和**渲染对话**中撤回,不新建会话、不切换窗口;目标消息文本会回填输入框,改完可重发。**在原理上就真正无感、便捷**。
14
- - **轻量工作区备份**——对齐 Claude Code:追踪写类工具编辑过的文件,**已跟踪文件的外部变更也能还原**。局部追踪、写前备份、不变不存。一个轻型插件,即拥有**完备的智能体回退能力**。
15
- - **信息安全优先**——插件从不删改会话日志(append-only),从不真正删除你的任何对话;备份存于自己的快照目录;还原只用这些备份。完整安全模型:[SECURITY.md](SECURITY.md)。
16
- - **完备测试系统**——单元、探针、端到端主机验证,覆盖兼容性探测、日志重放、续接、跨重启等场景;随 harness 升级持续维护,确保功能稳定。
13
+ - **就地消息回退**——目标消息及其之后的内容同时从**模型上下文**和**渲染对话**中撤回,目标消息文本自动回填输入框。始终在原会话内完成,不新建分支、不残留副本。
14
+ - **轻量备份还原**——对齐 Claude Code:追踪写类工具编辑过的文件,回退时一并还原,已跟踪文件的外部变更也能还原。**局部追踪、写前备份、不变不存**,不依赖 git。
15
+ - **安全保障**——会话日志仅追加回退标记,从不删改;文件备份与还原经安全加固;**完备测试**,随 DSH 升级长期维护。安全模型:[SECURITY.md](SECURITY.md)。
17
16
 
18
17
  ## 效果预览
19
18
 
20
- 每条用户消息的操作行都有一个 **↶ 回退** 按钮。点击后弹出模式选择浮层——「**仅回退对话**」或「**回退对话和代码**」,后者会先展示文件变更清单再确认。还可以通过 **`/rewind` 命令**和**快捷键**便捷地选择和回退。
19
+ 每条用户消息的操作行都有一个 **↶ 回退** 按钮。点击后弹出模式选择浮层——「**仅回退对话**」或「**回退对话和代码**」,后者会先展示文件变更清单再确认。还可以通过 **`/rewind` 和 `/undo` 命令**便捷地选择和回退。
21
20
 
22
21
  <table>
23
22
  <tr>
@@ -34,11 +33,21 @@ DeepSeek Harness 插件:**一键就地回退对话到任意更早的用户消
34
33
 
35
34
  确认本机 DSH 版本后,在 [Release](https://github.com/SiriLee/dsh-rewind/releases) 找到适配的插件版本。
36
35
 
36
+ **命令行**:
37
+
37
38
  ```sh
38
39
  dsh plugin --profile web add dsh-rewind-plugin@<版本>
39
40
  ```
40
41
 
41
- > ⚠️ npm 上的 `dsh-rewind` 属于其他作者,请用 `dsh-rewind-plugin` 安装。
42
+ **图形界面**:侧栏「插件」页 →「添加插件」→ 填 `dsh-rewind-plugin@<版本>` → 安装 → 重启 DSH 并刷新浏览器。
43
+
44
+ <img src="assets/screenshots/add-plugin.png" width="600" alt="「添加插件」弹窗:在输入框填入 dsh-rewind-plugin@版本号"><br>
45
+ <sub>图片仅为示例,请以实际适配版本号为准。</sub>
46
+
47
+ **更新**:命令行可直接覆盖安装;图形界面会拒绝重复安装,需先卸载旧版本,再装新版本。
48
+
49
+ > [!WARNING]
50
+ > npm 上的 `dsh-rewind` 属于其他作者,请用 `dsh-rewind-plugin` 安装。
42
51
 
43
52
  ## 使用
44
53
 
@@ -62,9 +71,9 @@ dsh plugin --profile web add dsh-rewind-plugin@<版本>
62
71
 
63
72
  快照(写前备份)存储于 `<dsh home>/rewind-snapshots/`(默认 `~/.dsh/rewind-snapshots/`)。插件对**同一会话**的快照做内容去重并保留最近 100 组锚点;**手动删除该目录**仅清除文件备份(对话回退不受影响),插件会自动重建。
64
73
 
65
- 另提供**全局自动清理**(默认关闭):把长期不活跃的会话快照整目录移除,不影响活动会话与对话日志。可在 `设置→插件→插件配置→快照清理` 面板查看与配置(自动清理开关、失活天数),也可用 `/snapshot-auto-cleanup` 命令查看、设置和运行。详见:[快照自动清理](docs/snapshot-auto-cleanup.zh.md)。
74
+ 另提供**全局自动清理**(默认关闭):把长期不活跃的会话快照整目录移除,不影响活动会话与对话日志。可在**插件**页本插件的配置卡片查看与配置(自动清理开关、失活天数),也可用 `/snapshot-auto-cleanup` 命令查看、设置和运行。详见:[快照自动清理](docs/snapshot-auto-cleanup.zh.md)。
66
75
 
67
- <img src="assets/screenshots/cleanup-setting.png" alt="快照清理设置:自动清理与失活天数" width="600">
76
+ <img src="assets/screenshots/cleanup-setting.png" alt="插件配置卡片:自动清理与不活跃时间(天)" width="600">
68
77
 
69
78
  ## 卸载
70
79
 
@@ -77,7 +86,7 @@ rm -rf <dsh home>/rewind-snapshots
77
86
  rm <dsh home>/snapshot-cleanup-last-sweep.json
78
87
  ```
79
88
 
80
- 插件的自动清理设置保存在设置文档(`<dsh home>/settings.yaml`)中。如需彻底清理,可手动删除对应键。
89
+ 插件的自动清理设置保存在当前 Profile 的插件配置中。如需彻底清理,可手动删除对应条目。
81
90
 
82
91
  ## 本插件的优势
83
92
 
@@ -144,8 +153,7 @@ rm <dsh home>/snapshot-cleanup-last-sweep.json
144
153
  1. **导出的日志是完整内容**——回退只是把消息从模型上下文和视图中移除,`/export` 导出的会话日志包含**已撤回的消息**。本插件无法改动导出。
145
154
  2. **轻量文件回退存在代价**——特定情况可能无法回退所有修改。行为与 Claude Code 一致。详见:[文件回退的追踪边界](docs/compat/tracking-boundary.zh.md)。
146
155
  3. **导轨显示已回退轮次**——DSH `v0.1.2` 新增右侧导轨,为已撤回消息保留刻度,悬浮显示已撤回正文。仅显示差异,无功能影响。
147
- 4. **回退重显系统提示词**——DSH `v0.1.2` 回退重发消息时,与 `/compact` 一样重显“系统提示词”组件。仅显示差异,无功能影响。
148
- 5. **旧回退标记不再兼容**——DSH `v0.1.3` 拒绝旧版插件(≤ 0.8.0)的回退标记。新版本已解决,并提供会话更新功能。详见:[更新指南](docs/rewind-fix.zh.md)。
156
+ 4. **旧回退标记不再兼容**——DSH `v0.1.3` 拒绝旧版插件(≤ 0.8.0)的回退标记。后续版本已解决,并提供会话更新功能。详见:[更新指南](docs/rewind-fix.zh.md)。
149
157
 
150
158
  > [!NOTE]
151
159
  > 本插件提供浏览器端诊断输出;详见 [浏览器诊断](docs/compat/diagnostics.zh.md)。
package/assets/icon.svg CHANGED
@@ -1,7 +1,7 @@
1
1
  <svg width="36" height="36" viewBox="0 0 36 36" fill="none" xmlns="http://www.w3.org/2000/svg">
2
2
  <!-- Layout follows the terminal card glyph (packages/client/ui-sidebar-terminal):
3
- a rounded tile inset from the canvas (28/36 here, 22/28 there) with a
4
- white round-capped prompt. The fill follows the Subagent plugin artwork
3
+ a rounded tile inset from the canvas with a white round-capped prompt. The
4
+ fill follows the Subagent plugin artwork
5
5
  (packages/client/ui-primitives/src/plugin-artwork.tsx), whose blue square
6
6
  carries the same vertical light-to-dark gradient: #69B9FF at the top,
7
7
  #324DE2 at the bottom.
@@ -9,11 +9,18 @@
9
9
  grid) scaled 1.4x. Its ink box including the 0.75 stroke reaches
10
10
  (1.75,1.75)-(14.25,12.75) on that grid, so the translate lands its centre
11
11
  on the tile centre (18,18), and the ink spans 62.5% of the tile width —
12
- the terminal glyph's own ratio. -->
13
- <rect x="4" y="4" width="28" height="28" rx="8" fill="url(#rewind_blue)" />
14
- <g transform="translate(6.8 7.85) scale(1.4)" stroke="#FFFFFF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
15
- <path d="M6.5 2.5 2.5 6.5l4 4" />
16
- <path d="M2.5 6.5h7a4 4 0 0 1 4 4v1.5" />
12
+ the terminal glyph's own ratio.
13
+ The canvas stays 36x36, the size the harness and the official artwork use,
14
+ so the consumer's `size` attribute keeps the authority over display size;
15
+ the inner group shrinks the finished drawing to 26/28 of the canvas around
16
+ its centre — one uniform scale over coordinates that are already correct,
17
+ so the tile, its radius, the mark and the stroke ratio stay in proportion. -->
18
+ <g transform="translate(18 18) scale(0.9286) translate(-18 -18)">
19
+ <rect x="4" y="4" width="28" height="28" rx="8" fill="url(#rewind_blue)" />
20
+ <g transform="translate(6.8 7.85) scale(1.4)" stroke="#FFFFFF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
21
+ <path d="M6.5 2.5 2.5 6.5l4 4" />
22
+ <path d="M2.5 6.5h7a4 4 0 0 1 4 4v1.5" />
23
+ </g>
17
24
  </g>
18
25
  <defs>
19
26
  <linearGradient id="rewind_blue" x1="18" y1="4" x2="18" y2="32" gradientUnits="userSpaceOnUse">
@@ -134,11 +134,11 @@ pruneStale cross-session auto-cleanup (default off): whole
134
134
 
135
135
  ## Compatibility strategy
136
136
 
137
- - **Peer ranges as one tuple per DSH line** (`^0.1.6-alpha.2`): npm's prerelease
138
- rules require the peer range to share the host's `[major, minor, patch]`
139
- tuple, so a new DSH tuple replaces the peer tuple (the single-line model);
140
- `scripts/check-dsh-version.mjs` flags when a new tuple arrives. See
141
- `docs/release/release.md`.
137
+ - **Peer ranges as one tuple per DSH line**: npm's prerelease rules require the
138
+ peer range to share the host's `[major, minor, patch]` tuple, so a new DSH
139
+ tuple replaces the peer tuple (the single-line model) and `package.json` is
140
+ the only place the current tuple is written down; `scripts/check-dsh-version.mjs`
141
+ flags when a new tuple arrives. See `docs/release/release.md`.
142
142
  - **Test-driven investigation**: `tests/compat-invariants.test.ts` /
143
143
  `compat-interop.test.ts` / `compat-gaps.test.ts` probe harness behavior and pin findings in
144
144
  `docs/compat/audit.md`; `scripts/verify-host.mjs` runs a real end-to-end
@@ -1,31 +1,24 @@
1
1
  # Compatibility audit (compat-audit)
2
2
 
3
3
  > Method: **the tests are the audit**. The probes in `tests/compat-invariants.test.ts`,
4
- > `tests/compat-interop.test.ts`, `tests/compat-gaps.test.ts` and
4
+ > `tests/compat-interop.test.ts`, `tests/compat-gaps.test.ts`,
5
+ > `tests/compat-tool-updates.test.ts` and
5
6
  > `scripts/verify-host.mjs` drive the plugin's real execution paths through the DSH
6
7
  > subsystems' **real consumer paths** (real `@deepseek-ai/*` packages) and assert
7
8
  > compatibility invariants. A probe failure is a finding; it enters the
8
9
  > fix/pin/record loop.
9
10
  >
10
- > Targeted version: npm `@deepseek-ai/*@0.1.7-alpha.1` (the range the peers and `dsh.engines.dsh` declare).
11
+ > Targeted version: npm `@deepseek-ai/*@0.1.7-rc.2`, declared by the peers as `^0.1.7-rc.2`.
11
12
  > Source reference: the upstream [github.com/deepseek-ai/deepseek-harness](https://github.com/deepseek-ai/deepseek-harness).
12
13
  >
13
- > Version alignment: `peerDependencies` use one tuple per DSH line
14
- > (e.g. `^0.1.2-rc.1`). npm's prerelease matching rules require a candidate to
15
- > share the range comparator's `[major, minor, patch]` tuple, so a new DSH tuple
16
- > replaces the peer tuple (single-line model). The range is conservative — it
17
- > declares only what was verified, so a release already inside it changes
18
- > nothing — unless it changes interfaces inside the tuple: `0.1.6-alpha.2`
19
- > removed the client session-ownership and queue-mirror APIs, and
20
- > `0.1.7-alpha.1` moved the session writer to format v4 (producer-owned message
21
- > sources, tool-role results, `developer/message`) and replaced the settings
22
- > namespace registry with each entry's volatile `Config`, so the peer floor and
23
- > both plugin seams moved with them. Signal: `npm view @deepseek-ai/dsh dist-tags`;
24
- > flow: `scripts/check-dsh-version.mjs` (it reads the `latest` dist-tag only; a
25
- > prerelease published under another tag is a manual pre-release check).
26
- >
27
- > The plugin targets a single DSH version line; compatibility with earlier
28
- > lines is not kept.
14
+ > Version alignment: one peer tuple per DSH line, declaring only what was verified — the
15
+ > model, the update steps, and the `check-dsh-version.mjs` signal live in
16
+ > `docs/release/release.md`. The floor moved when a release changed interfaces inside the
17
+ > tuple: `0.1.6-alpha.2` removed the client session-ownership and queue-mirror APIs, and
18
+ > `0.1.7-alpha.1` moved the session writer to format v4 (producer-owned message sources,
19
+ > tool-role results, `developer/message`) and replaced the settings namespace registry with
20
+ > each entry's volatile `Config`, so the peer floor and both plugin seams moved with them.
21
+ > The plugin targets a single DSH version line; compatibility with earlier lines is not kept.
29
22
 
30
23
  ### Single channel
31
24
 
@@ -53,28 +46,25 @@ legacy branch).
53
46
  | I6 tool pipeline | before-snapshot capture/commit/restore is correct (existing `snapshot.test.ts` + `verify-host` 4–8) for the tracked tools `write` / `edit` — `str_replace_editor` is an optional DSH package that stopped being a default tool in DSH 0.1.3, so it is not tracked; cancellation timing never hangs | `verify-host` 4–9 |
54
47
  | I7 client ordering | A log carrying tool turns and rewind markers (a single `user/message` replace) satisfies the client builder ordering | `compat-interop` I7 |
55
48
  | I8 runtime safety | `rewind`/`compact` combinations never leave a dangling step/turn frame | `verify-host` 13 |
49
+ | I9 tool-update projection | rc.2 folds mid-conversation tool additions from the whole log while a rewind cuts only the surface: after a withdrawal no `tool-addition`/`tool-removal` block reaches the provider and the current declarations stay complete | `compat-tool-updates` I9 |
56
50
 
57
51
  ## Verified-compatible surfaces (probes pass)
58
52
 
59
- - **token-meter replay** (the `user/message` marker + multiple rewinds + interleaved real turns + compact stacking).
60
53
  - **compaction transactions**: `toolPairingBalancedBefore/After` stays balanced after a marker cut; the real `/compact` command (`command-compact` + `compaction-basic`, stub summarizer) can land `compaction/start…end` on top of a rewind marker and stay replayable; `/compact` is a legal no-op on a small surface.
61
- - **resume replay**: `Session.create(id, events)` replays a rewind/compact-bearing log.
54
+ - **tool-update history (`0.1.7-rc.2`)**: `Session.toolHistory()` folds the full append-only log, so a withdrawn `tool-addition` stays recorded after a rewind; `projectToolUpdates` drops every update whose message left the request and falls back to the complete current declarations. Pin: `tests/compat-tool-updates.test.ts` (on-surface projection, then the post-rewind fallback).
62
55
  - **session-stats**: the `user/message` marker adds no step (the step count stays at the real turns' steps), no phantom turn.
63
56
  - **session-title / goal fold**: a marker does not disturb `foldSessionTitle` / `foldGoal`.
64
- - **client ordering**: turn-tail ordering + `step/start` uniqueness hold for tool turns + marker logs.
65
- - **rewind across a compact checkpoint**: `RewindError('not-on-surface')` refuses cleanly, no crash.
66
57
  - **plan-mode**: the marker is a turn-less `user/message` (no phantom turn); a rewind never touches the log-only `plan/mode` state (plan mode stays active; the user leaves it with `/plan off`) and the log stays replayable (`compat-invariants` I1/I3 marker + `plan/mode` probe, `verify-host`).
67
58
  - **agent-loop cancellation**: `finally` guarantees step/turn closure; the rewind force-stop path leaves no dangling frame.
68
- - **settings-card registration (`0.1.7-alpha.1`)**: `settingsScope` is gone with the namespace registry. The card declares `configForms` as a module-level inject (the web profile always composes `ui-settings`), stages through the harness's own `SettingsFormModel` + `SettingsForm`, and registers into `plugins.bundle.config` keyed by the bundle package name; it reads the entry's form snapshot and writes through its revision-fenced `mutate`, and disposes the model's subscription with the fiber.
59
+ - **settings card (`0.1.7-alpha.1`)**: the card declares `configForms` as a module-level inject (the web profile always composes `ui-settings`), stages through the harness's own `SettingsFormModel` + `SettingsForm`, and registers into `plugins.bundle.config` keyed by the bundle package name; it reads that shared per-entry form's snapshot and writes through its revision-fenced `mutate`, disposing the model's subscription with the fiber. The host half declares the bundle's page policy (`configure({ auto: false }, ctx.fiber)` inside a `ctx.inject(['settings'])` child, per the dsh-settings README), opting the entry out of a client-built schema page. Pins: `verify:host` (`the bundle declares its own config page`), `tests/client-settings-card.test.ts` (staged plan, refusal, subscription release), `tests/client-lifecycle.test.ts`.
69
60
  - **client session ownership (`0.1.6-alpha.2`)**: `SessionListState.current` / `currentAddress` are gone; main-view ownership is read from the row's `retainedBy.mainView` label — what `ui-workspace` retains the open session with. Pin: `tests/client-refill.test.ts`.
70
61
  - **client pending input (`0.1.6-alpha.2`)**: `SessionSnapshot.queue` and the host `queue-mirror` are gone; pending steering rows come from the session's own `inbox` projection, and only USER-sourced `next-step` rows are retractable (the removed mapping was `source.kind === 'user' ? 'steering' : 'context'`). Pins: `tests/pending.test.ts`, `tests/client-retract.test.ts`.
71
62
  - **client settings slot (`0.1.6-alpha.2`)**: `settings.plugin.item` is gone; the configuration form registers into `plugins.bundle.config`, keyed by the bundle package name. Pin: `tests/client-contract.test.ts`.
72
- - **client settings form (`0.1.7-alpha.1`)**: the same release retired the `settingsScope` bind (namespace registry) in favour of the shared per-entry form; the card now reads that form's snapshot and stages edits through `mutate`. Pin: `tests/client-settings-card.test.ts` (staged plan, refusal, and the fiber releasing the model's subscription), `tests/client-lifecycle.test.ts`.
73
-
74
63
  - **`agent/created` lifecycle guard**: the session-format reconcile runs on the alpha line's `agent/created` (fire-and-forget); pin: `verify-host` 4e dispatches it.
75
64
  - **marker vs `/compact`**: the checkpoint shape is unchanged on this line — a `user/message` replace (`surfaceOp {replace, startSeq, endSeq}` + `sourceEventSeqs`); `assistant/message` still cannot carry `sourceEventSeqs`.
76
65
  - **`image/offload` projection**: the alpha line's first `SessionMessageProjection` changes derived content without touching surface node membership; candidate listing and target resolution tolerate it. Pin: `tests/image-offload-projection.test.ts`.
77
66
  - **session-cwd**: the fs tools no longer canonicalize a parent-traversing cwd, so `src/session-cwd.ts` returns `header.cwd` verbatim. Pin: `tests/session-cwd.test.ts`.
67
+ - **startup admission (`0.1.7-rc.1`)**: the gate reads the manifest's own `@deepseek-ai/dsh-*` peers and requires each to satisfy the runtime with `includePrerelease`; it runs for a profile bundle and again for every row its patch inserts, and a denial skips the bundle or disables the row. This plugin's declarations all satisfy `0.1.7-rc.2`, so it is admitted with no exemption — a skipped bundle on a later line is a peer mismatch, not a load failure.
78
68
 
79
69
  ## Known behavior boundaries (deterministic differences, non-crash, documented)
80
70
 
@@ -88,107 +78,88 @@ legacy branch).
88
78
  - **Attachment files left after a message is shadowed**: attachment storage is not cleaned with the surface (`dsh-attachment-local` not installed, not automatically verified).
89
79
  - **Rewind leaves plan mode untouched**: `/plan text` is two independent actions (enter plan mode + steer the message). Rewinding the message undoes only the message — the log-only `plan/mode` state stays active, and the user leaves plan mode with `/plan off`, which still commits after a rewind (the marker creates no open turn). Pin: `verify-host` plan checks (`plan rewind leaves plan mode active`, `/plan off after rewind turns plan mode off`), `tests/hidden.test.ts` `messageTextAt`.
90
80
  - **Bundle card metadata is declared by the plugin, not by the manager (`0.1.7-alpha.1`)**: the manager still titles a bundle with `shortName(pkg.name)` and its raw `package.json` description when nothing else is declared, but it now reads plugin-owned resources first — `locale/<lang>.json` (`meta.title`/`meta.description`) through the package exports map, and an `icon` file relative to the manifest. This plugin declares both, so its card shows an icon and copy that follows the active language; only the fallback description remains mixed-language. Pin: `tests/package-layout.test.ts` (icon path/media/size and both dictionaries).
91
-
92
- - **Synchronous Session history reads are deprecated** on this line (`snapshotEvents` / `eventAt` / `ownEvents`): existing calls may remain, new calls are prohibited. The plugin keeps its existing reads (turn anchor + candidate listing); the migration path is a `ctx.sessionProjections` projection unit or the async paged history read this line has not shipped yet — not a plugin-side index. Not migrated in this release.
81
+ - **Synchronous Session history reads are deprecated** on this line (`snapshotEvents` / `eventAt` / `ownEvents`): existing calls may remain, new calls are prohibited. The plugin keeps its existing reads (turn anchor + candidate listing); the migration path is a `ctx.sessionProjections` projection unit or the async paged history read this line has not shipped yet — not a plugin-side index.
93
82
 
94
83
  ## Upstream (harness) issues and the plugin's no-compensation stance
95
84
 
96
- The plugin treats these as harness-side defects it does not compensate for. Each entry records the harness issue, its current status, and the plugin's stance, so a future maintainer does not "fix the wrong direction."
85
+ The plugin treats these as harness-side defects it does not compensate for. Each entry records
86
+ the harness issue, its current status, and the plugin's stance, so a future maintainer does not
87
+ "fix the wrong direction."
97
88
 
98
89
  ### RU-I18N (resolved on the 0.1.7 line): host command copy follows the user's language
99
90
 
100
- - **History (0.1.2–0.1.6 lines)**: the plugin resolved `activeLocale` in an
101
- `ctx.inject(['settings'])` callback that read `settings.get('locale').preference`
102
- **once, with no retry** (`src/index.ts`), while `dsh-client-locale`'s host half registered that
103
- `locale` settings section through its own `ctx.inject(['settings'])`
104
- (`packages/client/locale/src/index.ts`). Both callbacks wait only on `settings` and are
105
- independent of each other, so cordis made **no ordering guarantee** between the section
106
- registration and the plugin's read. The plugin's own command descriptors were registered
107
- **synchronously** in the same `apply`, i.e. before that microtask ran, so their `description`
108
- strings were baked in the neutral English default — the defect reported as #28 — while the
109
- runtime output, evaluated later at command execution from the then-updated `activeLocale`, did
110
- follow the stored preference. A preference changed *after* the mount reached the output only on
111
- the next mount, because the read was one-shot.
112
- - **Resolution (0.1.7)**: the section is gone — the locale host half now only calls
113
- `settings.configure({ auto: false })`, and the preference lives in the `locale` entry's own
114
- Config. The plugin reads that same value through the settings service's descriptor read
115
- (`readHostLocale` in `src/index.ts` calls `settings.describe({ redactSecrets: true })`, the route
116
- the settings UI and the client's own locale runtime take): at mount, which is what the
117
- once-registered command descriptions carry, and again before each command renders, so the output
118
- follows a language switch without a remount (the descriptors keep the mount-time language until
119
- the plugin remounts). The read is best-effort: a missing method, a missing entry, or a
120
- throwing read stays on the neutral English default.
121
- - **Client-side command-description i18n is still first-party-only**: DSH localizes host command
122
- descriptions through the client `locale` binding (`ui-commands`), but the description keys come
123
- from a **closed allowlist** (`HOST_FACES`: compact, export, feedback, goal,
124
- permission, plan). A command outside that set — every third-party plugin — is passed through
125
- verbatim, never translated (`builtinRowFace` only rewrites a description that equals the
126
- first-party English copy), and no host command *output* is localized by the harness for anyone.
127
- This plugin's host dictionary is what carries both, in the language read above.
128
-
129
- ### R-OPENSTEP (rewind part resolved): an unclosed `step` in the log breaks token-meter replay; the rewind no longer compounds it
130
-
131
- > **Root cause (harness-side)**: an unclosed step left by a crash makes token-meter replay
132
- > reject any later step activity. DSH `0.1.1-rc.2` now auto-closes crash-left step/turn/tool
133
- > boundaries at load via `interruptedTurnClosers` (`dsh-session`, consumed by
134
- > `packages/core/agent-loop/src/index.ts:907`) — **the crash path is fixed**.
91
+ > **Status**: the `locale` settings section is gone — the locale host half only calls
92
+ > `settings.configure({ auto: false })`, and the preference lives in the `locale` entry's own
93
+ > Config. The plugin reads that value through the settings service's descriptor read
94
+ > (`readHostLocale` in `src/index.ts` calls `settings.describe({ redactSecrets: true })`, the
95
+ > route the settings UI and the client's own locale runtime take) at mount — which is what the
96
+ > once-registered command descriptions carry — and again before each command renders, so the
97
+ > output follows a language switch without a remount. The read is best-effort: a missing
98
+ > method, a missing entry, or a throwing read stays on the neutral English default.
99
+ >
100
+ > **Client-side command-description i18n is still first-party-only**: DSH localizes host
101
+ > command descriptions through the client `locale` binding (`ui-commands`), but the
102
+ > description keys come from a **closed allowlist** (`HOST_FACES`: compact, export, feedback,
103
+ > goal, permission, plan). A command outside that set — every third-party plugin — is passed
104
+ > through verbatim, and no host command *output* is localized by the harness for anyone. This
105
+ > plugin's host dictionary is what carries both, in the language read above.
135
106
  >
136
- > **Plugin guard (attempted and reverted)**: a `hasOpenStep` + `planRewind` pre-refusal was
137
- > implemented (`open-step`) but misjudged on **real session logs** (normal rewinds refused, GUI
138
- > verification broken), and was reverted (`177ec14`). Conclusion: **the plugin sets no guard**,
139
- > accepting residual risk (an unclosed step produced by a third-party plugin can break
140
- > `/compact` — that log is already abnormal, and continuing the conversation triggers the same).
141
- > The fix direction is in the harness (token-meter recovery for unclosed steps), not the plugin.
142
-
143
- #### Concrete `step/start` trigger paths (source-confirmed)
144
-
145
- The tree has exactly **one** `append('step/start')` producer: `packages/core/agent-loop/src/agent.ts:303`
146
- (no other producer inside the official packages; `session/end-seed` etc. only truncate torn writes,
147
- not logically-unclosed steps).
148
-
149
- | # | Trigger path | Plausibility | Basis |
150
- |---|---|---|---|
151
- | P1 | **Abnormal process termination**: `step/start` is batched to disk (write-behind, `maxDelayMs` per batch) → the step is mid-execution (LLM stream/tool, seconds to minutes) → SIGKILL / OOM-kill / power loss / WSL hard-close → `step/end` (in `finally`, only runs while the process is alive) is never persisted | **Most realistic** | `agent.ts:313` finally; write-behind batching; torn-write fix truncates only a half-written line |
152
- | P2 | **Third-party plugin bug**: only the official agent-loop produces one, but external plugins may `session.append('step/start', …)` and never close it | possible | public `Session.append` |
153
- | P3 | **Manual session-file editing**: edit `~/.dsh/…/session.jsonl[.zstd]` (zstd needs decompress/recompress; plaintext config edits directly) | possible but laborious | `persistence-jsonl/format.ts` (`JsonlCompression = 'zstd' \| 'none'`) |
154
- | P4 | **append itself failing**: `append('step/end')` in `finally` throws (payload is plain numbers, nearly impossible) | theoretical | `agent.ts:313` |
155
-
156
- **Amplifier (rewind is not the only trigger)**: after a crash resume, agent-loop `turn()` opens a
157
- new turn at `phase.turn + 1` (`agent.ts:277-283`) **without closing the leftover step** — so
158
- "continue the conversation" (a new `step/start`) trips the same token-meter check. Scope:
159
-
160
- - **The conversation itself is unaffected** (the request path does not call `tokenMeter.measure`; only compaction-basic does tree-wide).
161
- - **Manual `/compact` fails permanently** (`compactNow`'s first `measure()` throws the raw error).
162
- - **Automatic compaction silently stays disabled** (the `agent/pre-step` hook catches and warns; the conversation continues).
163
- - **rewind's role (resolved)**: the v2 `user/message` rewind marker appends no `step/start`, so a rewind no longer introduces the first trip-wire and the R-OPENSTEP rewind amplifier is closed. The harness-side root cause (an unclosed step after a crash, tripped by continuing the conversation) remains.
107
+ > **Plugin stance**: no compensation; the seam is the descriptor read above. The wrong direction
108
+ > is the earlier one — reading `settings.get('locale').preference` once inside an
109
+ > `ctx.inject(['settings'])` callback and assuming cordis ordered the locale section before it
110
+ > (it does not; the descriptors were baked in the English default, defect #28).
111
+
112
+ ### R-OPENSTEP (rewind part resolved): an unclosed `step` in the log breaks token-meter replay
113
+
114
+ > **Situation**: once a step is left unclosed, replay rejects any later step activity, so
115
+ > manual `/compact` fails permanently and automatic compaction silently stays disabled (the
116
+ > `agent/pre-step` hook catches and warns). The conversation itself is unaffected — only
117
+ > compaction-basic measures tree-wide. DSH now auto-closes crash-left step/turn/tool
118
+ > boundaries at load (`interruptedTurnClosers`, consumed by
119
+ > `packages/core/agent-loop/src/index.ts:907`), so **the crash path is fixed**. The tree has
120
+ > exactly one `append('step/start')` producer, `agent-loop/src/agent.ts:303`; an unclosed step
121
+ > otherwise comes from a third-party plugin appending `step/start` through the public
122
+ > `Session.append`, or from a hand-edited log. After a crash resume, `turn()` opens a new turn
123
+ > (`agent.ts:277-283`) without closing the leftover step, so continuing the conversation trips
124
+ > the same check — a rewind is **not** the only trigger.
125
+ >
126
+ > **Rewind's part**: the marker is a `user/message` and appends no `step/start`, so a rewind
127
+ > no longer introduces the first trip-wire and the R-OPENSTEP rewind amplifier is closed.
128
+ >
129
+ > **Plugin stance**: no guard. A `hasOpenStep` + `planRewind` pre-refusal was implemented and
130
+ > misjudged on **real session logs** (normal rewinds refused, GUI verification broken), so it
131
+ > was reverted — do not re-attempt a plugin-side pre-refusal. The fix direction is upstream
132
+ > (token-meter recovery for unclosed steps), and the residual risk is accepted: that log is
133
+ > already abnormal.
164
134
 
165
135
  ### R-PROMPTCARD (resolved on the 0.1.7 line): the repeated `System prompt` card is filtered out of chat
166
136
 
167
- > **History (0.1.2–0.1.6 lines)**: Chat rendered a collapsed `System prompt`
168
- > card at every request-series start. Any surface mutation bumps `contentGeneration` (a positional
169
- > `replace` — rewind, `/compact` — or a `SessionMessageProjection`, e.g. image offload), so the next
170
- > request logs `request/header { reason: 'series' }`; a process restart logs `'resume'`. The client
171
- > shows the card for every reason except `'change'`. The effective prompt is unchanged — only the
172
- > card repeats. Triggers beyond rewind: `/compact`, restart/resume, projections, explicit
173
- > `startsSeries`.
174
- >
175
- > **Upstream decision (deliberate, twice)**: `6ad01cbd8d` suppressed the repeat on `resume` (note
176
- > `2026-09-03-resume-headers-do-not-repeat-system-prompts.md`, rejecting the card as a "lifecycle
177
- > marker" that "incorrectly implies another prompt injection"); three days later `77b6b10c21`
178
- > restored it — a resume/series header starts a visible request series. Locked by the `ui-chat` test
179
- > `repeats an unchanged system prompt after a surface rewrite…`.
137
+ > **Status**: `isVisibleChatNode`
138
+ > (`packages/client/ui-chat/src/client/contract/chat-visibility.ts`) drops `system-prompt`
139
+ > nodes — and the `permission` command — from the visible chat rows before the row is built,
140
+ > so a rewind on this line adds no prompt row at all; the durable events and the trajectory
141
+ > inspection keep them. #22, #31 and README known issue #4 no longer reproduce.
180
142
  >
181
- > **Resolution (0.1.7)**: upstream stopped rendering the card in chat at all. `isVisibleChatNode`
182
- > (`packages/client/ui-chat/src/client/contract/chat-visibility.ts`, added by `b6f99fad55`
183
- > "feat(ui-chat): filter system prompts and permission commands") drops `system-prompt` nodes — and
184
- > the `permission` command — from the visible chat rows, while the durable events and the trajectory
185
- > inspection keep them. The `showsPrompt` computation is unchanged and no longer matters: the node is
186
- > filtered before the row is built, so a rewind on this line adds no prompt row at all. #22, #31 and
187
- > README known issue #4 no longer reproduce.
143
+ > **Plugin stance**: still no compensation. The DOM row-hiding seam (`hiddenSeqsOf`) covers
144
+ > withdrawn messages, where hiding is essential, not cosmetic.
145
+
146
+ ### R-PANELKEYS: the panel keyboard stays the plugin's own
147
+
148
+ > **Situation**: on Web, Escape, Tab, the four arrows, and Enter without Alt are
149
+ > platform-reserved — inline in the shortcuts `bindingIssue` list, with no exported form — so
150
+ > neither a plugin nor a user may bind them and there is nothing to read and follow. `Menu`
151
+ > is the dropdown primitive, while the harness's command-option panel (`ui-commands`'
152
+ > popupSelect) renders `MenuSurface` plus its own controller with `role="listbox"`/
153
+ > `role="option"`. `ui-commands`' `settle()` also calls `focusComposer()` unconditionally
154
+ > after `await onSelect(...)`; its only early return requires replacing the popup binding,
155
+ > which needs the internal `segment` that business code cannot reach.
188
156
  >
189
- > **Plugin stance**: still no compensation. The DOM row-hiding seam (`hiddenSeqsOf`) covers withdrawn
190
- > messages, where hiding is essential, not cosmetic; nothing was added for the card, and the
191
- > display-layer workup above stays valid history.
157
+ > **Decision**: the mode-selection and retract panels keep their own capture-phase
158
+ > Escape/↑/↓ handling — focus-independent, the shape the official command panel uses — and
159
+ > keep `role="dialog"`. The plugin neither renders `Menu` nor compensates the composer focus
160
+ > restore. Capture ownership is pinned by `tests/client-popover.test.ts`; the ghost-row walk
161
+ > exclusion is **not** pinned, and on the typed `/rewind` path Enter still needs a row to hold
162
+ > focus after the shell's focus restore.
192
163
 
193
164
  ## Uncovered boundaries (need an additional e2e layer; non-blocking)
194
165
 
@@ -215,8 +186,6 @@ new turn at `phase.turn + 1` (`agent.ts:277-283`) **without closing the leftover
215
186
  | client ordering | — | — | ✓ | — | — | — | ✓ | — |
216
187
  | plan-mode | ✓ | — | ✓ | — | — | — | — | ✓ (static) |
217
188
 
218
- ✓ = probe passes; — = not applicable. RU-I18N and R-PROMPTCARD are resolved on the 0.1.7 line (see
219
- above); R-OPENSTEP is an upstream issue the
220
- plugin does not compensate for (see above); G3 is a confirmed-non-defect behavior pinned in
221
- `compat-gaps.test.ts` (G1 surface classification via `foldSurface` and G2 projection checkpoint
222
- both pass).
189
+ ✓ = probe passes; — = not applicable. Statuses and stances for the named findings live in the
190
+ sections above; G1 (surface classification via `foldSurface`), G2 (projection checkpoint) and
191
+ G3 (the confirmed-non-defect usage anchor on token-meter) all pass in `compat-gaps.test.ts`.
@@ -53,7 +53,7 @@ src/index.ts host plugin: /rewind|/undo|/snapshot-auto-cleanup
53
53
  + checkpoint pipeline (tools/execute|post-execute)
54
54
  src/rewind.ts pure planning: target resolution, surface range, candidate listing
55
55
  src/snapshot.ts checkpoint store (disk before-backups, restore/preview, bounded prune)
56
- src/snapshot-cleanup.ts cleanup policy + dsh-settings persistence + auto-sweep throttle
56
+ src/snapshot-cleanup.ts cleanup policy + plugin-entry config persistence + auto-sweep throttle
57
57
  src/session-cwd.ts session-cwd resolution (fs-tools rule)
58
58
  src/locales.ts host i18n (t() renderer, HostKey)
59
59
  src/client/index.ts client plugin: /rewind command decoration + per-message ↶ button portals;
@@ -99,6 +99,11 @@ outside it.
99
99
  forward-looking declaration rather than an enforced guard. Bump it in
100
100
  the same release that changes the peer range; never leave code raised while
101
101
  the declared floor stays behind. Only the `>=X.Y.Z[-pre]` form is used.
102
+ - **The peers are enforced at startup (`0.1.7-rc.1`)**: DSH checks a profile
103
+ bundle's own `@deepseek-ai/dsh-*` peers against the running version and skips
104
+ the whole bundle when one does not satisfy it, so a peer range that is merely
105
+ stale now costs the plugin its load instead of a warning. See
106
+ `docs/compat/audit.md`.
102
107
 
103
108
  ## Versioned-line release model
104
109
 
@@ -85,6 +85,10 @@ DSH 仍在 rc 阶段,npm 的 prerelease 匹配规则要求 peer 范围与宿
85
85
  `dsh.engines.dsh` 声明 DSH 运行时下限(如 `>=0.1.2-rc.1`)。当前线上还没有
86
86
  任何读取方,因此它只是前置声明、尚未被强制执行。**与 peer 范围持平的同一发布里
87
87
  一并 bump**;不可只升代码、声明下限停留在旧值。仅使用 `>=X.Y.Z[-pre]` 形式。
88
+ - **peer 在启动时会被强制执行(`0.1.7-rc.1`)**:DSH 会拿 bundle 自身的
89
+ `@deepseek-ai/dsh-*` peer 与运行版本比对,只要有一个不满足就**整包跳过**,
90
+ 因此过期的 peer 范围不再是"仅告警",而是直接导致插件不加载。见
91
+ `docs/compat/audit.md`。
88
92
 
89
93
  ## 发布版本线模型
90
94
 
@@ -45,7 +45,7 @@ whether or not automatic cleanup is on.
45
45
 
46
46
  The auto-cleanup switch and the idle-day cutoff live in the **dsh-settings configuration document**. View and edit them in the **Settings &gt; Plugins &gt; Plugin configuration &gt; Snapshot cleanup** panel (the auto-cleanup switch and the idle days), or view and set them with the `/snapshot-auto-cleanup` command:
47
47
 
48
- <img src="../assets/screenshots/cleanup-setting.png" alt="Snapshot cleanup settings: auto-cleanup and idle days" width="600">
48
+ <img src="../assets/screenshots/cleanup-setting.png" alt="Plugin configuration card: auto cleanup and idle time (days)" width="600">
49
49
 
50
50
  - `enabled` — whether automatic cleanup runs (default `false`).
51
51
  - `maxAgeDays` — how many idle days before a session's snapshots are removed (default `30`). Only positive integers are accepted, so a broken setting can never delete everything.
@@ -31,9 +31,9 @@
31
31
 
32
32
  ## 设置
33
33
 
34
- 自动清理开关与失活天数保存在 **dsh-settings 配置文档**。可在 **设置→插件→插件配置→快照清理** 面板查看与修改(自动清理开关、失活天数),也可用 `/snapshot-auto-cleanup` 命令查看和设置:
34
+ 自动清理开关与失活天数保存在**本插件条目自身的配置**中。可在**插件**页本插件的配置卡片查看与修改(自动清理开关、失活天数),也可用 `/snapshot-auto-cleanup` 命令查看和设置:
35
35
 
36
- <img src="../assets/screenshots/cleanup-setting.png" alt="快照清理设置:自动清理与失活天数" width="600">
36
+ <img src="../assets/screenshots/cleanup-setting.png" alt="插件配置卡片:自动清理与不活跃时间(天)" width="600">
37
37
 
38
38
  - `enabled` — 是否运行自动清理(默认 `false`)。
39
39
  - `maxAgeDays` — 会话失活多少天后移除其快照(默认 `30`)。只接受正整数,因此一个损坏的设置永远不可能"删掉一切"。
package/lib/client.js CHANGED
@@ -430,6 +430,11 @@ var STYLE = `
430
430
  // src/client/popover.ts
431
431
  var popoverEl = null;
432
432
  var disposeOutside = null;
433
+ var liveSessionId = null;
434
+ var composerFocuser = null;
435
+ function registerComposerFocuser(focus) {
436
+ composerFocuser = focus;
437
+ }
433
438
  function closePopover() {
434
439
  if (popoverEl !== null) {
435
440
  popoverEl.remove();
@@ -439,6 +444,14 @@ function closePopover() {
439
444
  disposeOutside();
440
445
  disposeOutside = null;
441
446
  }
447
+ const sessionId = liveSessionId;
448
+ liveSessionId = null;
449
+ if (sessionId !== null) {
450
+ try {
451
+ composerFocuser?.(sessionId);
452
+ } catch {
453
+ }
454
+ }
442
455
  }
443
456
  function formatTarget(t, seq, time, preview) {
444
457
  const d = new Date(time);
@@ -689,6 +702,7 @@ function openRetractPopover(opts) {
689
702
  }
690
703
  function openPopover(opts) {
691
704
  closePopover();
705
+ liveSessionId = opts.session.sessionId;
692
706
  if (opts.retract !== void 0) {
693
707
  openRetractPopover(opts);
694
708
  return;
@@ -854,6 +868,10 @@ function writeComposer(text, facade) {
854
868
  if (facade !== void 0) {
855
869
  try {
856
870
  facade.setDraft(text);
871
+ try {
872
+ facade.focus?.();
873
+ } catch {
874
+ }
857
875
  return true;
858
876
  } catch {
859
877
  }
@@ -973,6 +991,7 @@ function hideWithdrawnSeats(chat, seats, hiddenSeqs, hidden) {
973
991
  delete seat.dataset.dshRewindHidden;
974
992
  hidden.delete(seat);
975
993
  }
994
+ if (seat.dataset.chatNodeKey === void 0) continue;
976
995
  const shell = seat.parentElement?.closest(GROUP_SHELL_SELECTOR);
977
996
  if (shell === null || shell === void 0) continue;
978
997
  shells.set(shell, (shells.get(shell) ?? true) && withdrawn);
@@ -1233,9 +1252,9 @@ function createRewindBridge(deps) {
1233
1252
  }
1234
1253
 
1235
1254
  // src/client/build-info.ts
1236
- var PLUGIN_VERSION = true ? "0.14.0-alpha.1" : "dev";
1255
+ var PLUGIN_VERSION = true ? "0.14.0-beta.2" : "dev";
1237
1256
  var PLUGIN_PACKAGE = "dsh-rewind-plugin";
1238
- var BUILD_HASH = true ? "190a881a" : "dev";
1257
+ var BUILD_HASH = true ? "1afddb1d" : "dev";
1239
1258
 
1240
1259
  // src/client/locales.ts
1241
1260
  var zh = {
@@ -1438,24 +1457,35 @@ function apply(ctx) {
1438
1457
  return void 0;
1439
1458
  }
1440
1459
  };
1460
+ const composerFacade = (sessionId) => {
1461
+ const conversation = ctx.get("conversation");
1462
+ const input = conversation?.input;
1463
+ const scope = ctx.sessions.scope?.(sessionId);
1464
+ if (input === void 0 || scope === void 0) return void 0;
1465
+ return {
1466
+ setDraft: (draft) => {
1467
+ input.for(scope).setDraft(draft);
1468
+ },
1469
+ focus: () => {
1470
+ input.for(scope).focus();
1471
+ }
1472
+ };
1473
+ };
1441
1474
  const setComposerText = (sessionId, text) => {
1442
1475
  try {
1443
- const conversation = ctx.get("conversation");
1444
- const input = conversation?.input;
1445
- const scope = ctx.sessions.scope?.(sessionId);
1446
- const facade = input !== void 0 && scope !== void 0;
1447
- const ok = writeComposer(
1448
- text,
1449
- facade ? { setDraft: (draft) => {
1450
- input.for(scope).setDraft(draft);
1451
- } } : void 0
1452
- );
1453
- return ok;
1476
+ return writeComposer(text, composerFacade(sessionId));
1454
1477
  } catch (error) {
1455
1478
  rewindLog.warn("refill", "composer write threw", error);
1456
1479
  return false;
1457
1480
  }
1458
1481
  };
1482
+ registerComposerFocuser((sessionId) => {
1483
+ try {
1484
+ composerFacade(sessionId)?.focus();
1485
+ } catch (error) {
1486
+ rewindLog.warn("refill", "composer focus threw", error);
1487
+ }
1488
+ });
1459
1489
  const watchChat = (sessionId, cb) => resolveChatWatch(
1460
1490
  (id) => {
1461
1491
  try {
package/lib/index.js CHANGED
@@ -2766,6 +2766,9 @@ function apply(ctx, config) {
2766
2766
  ctx.effect(() => () => {
2767
2767
  lifecycle.abort();
2768
2768
  }, "dsh-rewind lifecycle abort");
2769
+ ctx.inject(["settings"], (child) => {
2770
+ child.effect(() => child.settings.configure({ auto: false }, ctx.fiber), "dsh-rewind page policy");
2771
+ });
2769
2772
  const dshHome = config?.dshHome;
2770
2773
  const store = new SnapshotStore(config?.snapshotDir, { dedup: config?.dedup, dshHome });
2771
2774
  const configKey = cleanupConfigKey(rewindConfigKey(ctx));
@@ -54,7 +54,12 @@ export interface PopoverOptions {
54
54
  */
55
55
  readonly onRewind?: (mode: 'chat' | 'both') => void;
56
56
  }
57
- /** Close the current popover, if any. */
57
+ /**
58
+ * Register the composer focus channel (called once by the client plugin).
59
+ * @param focus - returns the keyboard to one session's composer, caret included.
60
+ */
61
+ export declare function registerComposerFocuser(focus: (sessionId: string) => void): void;
62
+ /** Close the current popover, if any, handing the keyboard back to its composer. */
58
63
  export declare function closePopover(): void;
59
64
  /**
60
65
  * Seqs of the command nodes currently matching `match`. Sample BEFORE issuing
@@ -124,6 +124,12 @@ export interface SlotsLike {
124
124
  */
125
125
  interface ComposerDraftWriter {
126
126
  setDraft(text: string): void;
127
+ /**
128
+ * Return the keyboard to the composer with the caret it last held (the
129
+ * facade's own `focus`). Optional so the DOM fallback and older faces still
130
+ * restore the text.
131
+ */
132
+ focus?(): void;
127
133
  }
128
134
  /**
129
135
  * Fill the dsh composer with `text` through the `contenteditable`
@@ -206,6 +212,14 @@ export declare function actionsContainerOf(row: HTMLElement | undefined): HTMLEl
206
212
  * that member is genuinely on the surface, in which case the shell stays; a
207
213
  * shell the batch carries no member of is left alone.
208
214
  *
215
+ * A MEMBER is a real chat node seat — `ChatNodeSeat` is the one shape that
216
+ * writes `data-chat-node-key` beside the anchor key. An anchor-only descendant
217
+ * (the tool call tree's own `call:<callId>` row, or a third-party anchor) is
218
+ * nested decoration: it resolves to no chat node, so it does NOT vote on the
219
+ * shell's emptiness. Counting such a row as "still live" is what left a fully
220
+ * withdrawn "command executed" tool group on screen as an empty expandable row
221
+ * after every member below it had been hidden.
222
+ *
209
223
  * Cost stays O(seats) per refresh: the caller queries the batch once, and each
210
224
  * shell decision reuses the member outcomes already computed here — no second
211
225
  * document scan, and no per-shell subtree walk.
package/locale/en.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
3
  "title": "Conversation rewind",
4
- "description": "Rewind the conversation in place to an earlier message, optionally restoring the workspace files it changed."
4
+ "description": "Rewind to an earlier message, optionally restoring workspace files."
5
5
  }
6
6
  }
package/locale/zh.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "meta": {
3
3
  "title": "对话回退",
4
- "description": "在同一窗口内把对话回退到更早的消息,可选择一并还原它改动过的工作区文件。"
4
+ "description": "把对话回退到更早的消息,可选一并还原工作区文件。"
5
5
  }
6
6
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-rewind-plugin",
3
- "version": "0.14.0-alpha.1",
4
- "description": "In-window conversation rewind with workspace file restore \u00b7 \u540c\u7a97\u53e3\u5185\u5bf9\u8bdd\u56de\u9000\u5e76\u53ef\u8fd8\u539f\u5de5\u4f5c\u533a\u6587\u4ef6",
3
+ "version": "0.14.0-beta.2",
4
+ "description": "In-window conversation rewind with workspace file restore · 同窗口内对话回退并可还原工作区文件",
5
5
  "icon": "./assets/icon.svg",
6
6
  "keywords": [
7
7
  "deepseek-harness",
@@ -53,7 +53,7 @@
53
53
  "patch": "./cordis.patch.yml"
54
54
  },
55
55
  "engines": {
56
- "dsh": ">=0.1.7-alpha.1"
56
+ "dsh": ">=0.1.7-rc.2"
57
57
  },
58
58
  "client": {
59
59
  "inject": [
@@ -78,20 +78,20 @@
78
78
  },
79
79
  "peerDependencies": {
80
80
  "@deepseek-ai/cordis": "^4.0.1",
81
- "@deepseek-ai/dsh-client-locale": "^0.1.7-alpha.1",
82
- "@deepseek-ai/dsh-client-ui-commands": "^0.1.7-alpha.1",
83
- "@deepseek-ai/dsh-client-ui-plugin-manager": "^0.1.7-alpha.1",
84
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.7-alpha.1",
85
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.7-alpha.1",
86
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.7-alpha.1",
87
- "@deepseek-ai/dsh-commands": "^0.1.7-alpha.1",
88
- "@deepseek-ai/dsh-fs": "^0.1.7-alpha.1",
89
- "@deepseek-ai/dsh-home-paths": "^0.1.7-alpha.1",
90
- "@deepseek-ai/dsh-llm": "^0.1.7-alpha.1",
91
- "@deepseek-ai/dsh-sandbox": "^0.1.7-alpha.1",
92
- "@deepseek-ai/dsh-session": "^0.1.7-alpha.1",
93
- "@deepseek-ai/dsh-settings": "^0.1.7-alpha.1",
94
- "@deepseek-ai/dsh-tools": "^0.1.7-alpha.1",
81
+ "@deepseek-ai/dsh-client-locale": "^0.1.7-rc.2",
82
+ "@deepseek-ai/dsh-client-ui-commands": "^0.1.7-rc.2",
83
+ "@deepseek-ai/dsh-client-ui-plugin-manager": "^0.1.7-rc.2",
84
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.7-rc.2",
85
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.7-rc.2",
86
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.7-rc.2",
87
+ "@deepseek-ai/dsh-commands": "^0.1.7-rc.2",
88
+ "@deepseek-ai/dsh-fs": "^0.1.7-rc.2",
89
+ "@deepseek-ai/dsh-home-paths": "^0.1.7-rc.2",
90
+ "@deepseek-ai/dsh-llm": "^0.1.7-rc.2",
91
+ "@deepseek-ai/dsh-sandbox": "^0.1.7-rc.2",
92
+ "@deepseek-ai/dsh-session": "^0.1.7-rc.2",
93
+ "@deepseek-ai/dsh-settings": "^0.1.7-rc.2",
94
+ "@deepseek-ai/dsh-tools": "^0.1.7-rc.2",
95
95
  "@deepseek-ai/schemastery": "^3.18.1"
96
96
  },
97
97
  "peerDependenciesMeta": {
@@ -147,41 +147,41 @@
147
147
  "devDependencies": {
148
148
  "@deepseek-ai/cordis": "4.0.3",
149
149
  "@deepseek-ai/cosmokit": "1.8.4",
150
- "@deepseek-ai/dsh-agent": "0.1.7-alpha.1",
151
- "@deepseek-ai/dsh-api-session-controller": "0.1.7-alpha.1",
152
- "@deepseek-ai/dsh-attachment": "0.1.7-alpha.1",
153
- "@deepseek-ai/dsh-client-connection": "0.1.7-alpha.1",
154
- "@deepseek-ai/dsh-client-locale": "0.1.7-alpha.1",
155
- "@deepseek-ai/dsh-client-store": "0.1.7-alpha.1",
156
- "@deepseek-ai/dsh-client-ui-commands": "0.1.7-alpha.1",
157
- "@deepseek-ai/dsh-client-ui-conversation": "0.1.7-alpha.1",
158
- "@deepseek-ai/dsh-client-ui-input-trigger": "0.1.7-alpha.1",
159
- "@deepseek-ai/dsh-client-ui-plugin-manager": "0.1.7-alpha.1",
160
- "@deepseek-ai/dsh-client-ui-primitives": "0.1.7-alpha.1",
161
- "@deepseek-ai/dsh-client-ui-session": "0.1.7-alpha.1",
162
- "@deepseek-ai/dsh-client-ui-settings": "0.1.7-alpha.1",
163
- "@deepseek-ai/dsh-client-ui-slots": "0.1.7-alpha.1",
164
- "@deepseek-ai/dsh-command-compact": "0.1.7-alpha.1",
165
- "@deepseek-ai/dsh-commands": "0.1.7-alpha.1",
166
- "@deepseek-ai/dsh-compaction": "0.1.7-alpha.1",
167
- "@deepseek-ai/dsh-compaction-basic": "0.1.7-alpha.1",
168
- "@deepseek-ai/dsh-compaction-image-offload": "0.1.7-alpha.1",
169
- "@deepseek-ai/dsh-fs": "0.1.7-alpha.1",
170
- "@deepseek-ai/dsh-goal": "0.1.7-alpha.1",
171
- "@deepseek-ai/dsh-home-paths": "0.1.7-alpha.1",
172
- "@deepseek-ai/dsh-llm": "0.1.7-alpha.1",
173
- "@deepseek-ai/dsh-plan-mode": "0.1.7-alpha.1",
174
- "@deepseek-ai/dsh-sandbox": "0.1.7-alpha.1",
175
- "@deepseek-ai/dsh-scope": "0.1.7-alpha.1",
176
- "@deepseek-ai/dsh-session": "0.1.7-alpha.1",
177
- "@deepseek-ai/dsh-session-projection": "0.1.7-alpha.1",
178
- "@deepseek-ai/dsh-session-stats": "0.1.7-alpha.1",
179
- "@deepseek-ai/dsh-session-title": "0.1.7-alpha.1",
180
- "@deepseek-ai/dsh-settings": "0.1.7-alpha.1",
181
- "@deepseek-ai/dsh-token-meter": "0.1.7-alpha.1",
182
- "@deepseek-ai/dsh-tools": "0.1.7-alpha.1",
183
- "@deepseek-ai/dsh-typert-protocol": "0.1.7-alpha.1",
184
- "@deepseek-ai/dsh-user-questions": "0.1.7-alpha.1",
150
+ "@deepseek-ai/dsh-agent": "0.1.7-rc.2",
151
+ "@deepseek-ai/dsh-api-session-controller": "0.1.7-rc.2",
152
+ "@deepseek-ai/dsh-attachment": "0.1.7-rc.2",
153
+ "@deepseek-ai/dsh-client-connection": "0.1.7-rc.2",
154
+ "@deepseek-ai/dsh-client-locale": "0.1.7-rc.2",
155
+ "@deepseek-ai/dsh-client-store": "0.1.7-rc.2",
156
+ "@deepseek-ai/dsh-client-ui-commands": "0.1.7-rc.2",
157
+ "@deepseek-ai/dsh-client-ui-conversation": "0.1.7-rc.2",
158
+ "@deepseek-ai/dsh-client-ui-input-trigger": "0.1.7-rc.2",
159
+ "@deepseek-ai/dsh-client-ui-plugin-manager": "0.1.7-rc.2",
160
+ "@deepseek-ai/dsh-client-ui-primitives": "0.1.7-rc.2",
161
+ "@deepseek-ai/dsh-client-ui-session": "0.1.7-rc.2",
162
+ "@deepseek-ai/dsh-client-ui-settings": "0.1.7-rc.2",
163
+ "@deepseek-ai/dsh-client-ui-slots": "0.1.7-rc.2",
164
+ "@deepseek-ai/dsh-command-compact": "0.1.7-rc.2",
165
+ "@deepseek-ai/dsh-commands": "0.1.7-rc.2",
166
+ "@deepseek-ai/dsh-compaction": "0.1.7-rc.2",
167
+ "@deepseek-ai/dsh-compaction-basic": "0.1.7-rc.2",
168
+ "@deepseek-ai/dsh-compaction-image-offload": "0.1.7-rc.2",
169
+ "@deepseek-ai/dsh-fs": "0.1.7-rc.2",
170
+ "@deepseek-ai/dsh-goal": "0.1.7-rc.2",
171
+ "@deepseek-ai/dsh-home-paths": "0.1.7-rc.2",
172
+ "@deepseek-ai/dsh-llm": "0.1.7-rc.2",
173
+ "@deepseek-ai/dsh-plan-mode": "0.1.7-rc.2",
174
+ "@deepseek-ai/dsh-sandbox": "0.1.7-rc.2",
175
+ "@deepseek-ai/dsh-scope": "0.1.7-rc.2",
176
+ "@deepseek-ai/dsh-session": "0.1.7-rc.2",
177
+ "@deepseek-ai/dsh-session-projection": "0.1.7-rc.2",
178
+ "@deepseek-ai/dsh-session-stats": "0.1.7-rc.2",
179
+ "@deepseek-ai/dsh-session-title": "0.1.7-rc.2",
180
+ "@deepseek-ai/dsh-settings": "0.1.7-rc.2",
181
+ "@deepseek-ai/dsh-token-meter": "0.1.7-rc.2",
182
+ "@deepseek-ai/dsh-tools": "0.1.7-rc.2",
183
+ "@deepseek-ai/dsh-typert-protocol": "0.1.7-rc.2",
184
+ "@deepseek-ai/dsh-user-questions": "0.1.7-rc.2",
185
185
  "@deepseek-ai/schemastery": "3.18.3",
186
186
  "@types/node": "^24.0.0",
187
187
  "@types/react": "^18.3.31",