dsh-rewind-plugin 0.20.0-alpha.1 → 0.20.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.en.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # dsh-rewind
2
2
 
3
- Conversation rewind for DeepSeek Harness: **rewind the conversation to any earlier user message in one click, in the same window** —<br />
4
- no new branch, no window switch, with optional workspace-file restore (full Claude Code `/rewind` semantics).
3
+ Conversation rewind for DeepSeek Harness: **rewind the conversation to any earlier user message in one click, in the same window**.<br />
4
+ No new branch, no window switch, with optional workspace-file restore — the full Claude Code `/rewind` semantics.
5
5
 
6
6
  English | [中文](README.md)
7
7
 
@@ -9,7 +9,7 @@ English | [中文](README.md)
9
9
  [![npm downloads](https://img.shields.io/npm/dt/dsh-rewind-plugin.svg)](https://www.npmjs.com/package/dsh-rewind-plugin)
10
10
  [![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)
11
11
 
12
- ![Back to any earlier message: the ↶ rewind button and the conversation-only / conversation-and-files mode popover](docs/preview.svg)
12
+ ![Rewind to an earlier message, with optional file restore](docs/preview.svg)
13
13
 
14
14
  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.
15
15
 
@@ -43,15 +43,11 @@ Check your local DSH version, then find the matching plugin version in
43
43
  dsh plugin --profile <name> add dsh-rewind-plugin@<version>
44
44
  ```
45
45
 
46
- **Graphical interface**: the **Plugins** page in the sidebar → **Add plugin** → enter `dsh-rewind-plugin@<version>` → install → restart DSH and refresh the browser.
46
+ **Graphical interface**: the **Plugins** page in the sidebar → **Add plugin** → enter `dsh-rewind-plugin@<version>` → install → restart DSH.
47
47
 
48
48
  <img src="assets/screenshots/add-plugin.png" width="600" alt="Add plugin dialog: enter dsh-rewind-plugin@version in the field"><br>
49
49
  <sub>The screenshot is an example only — use the version that matches your DSH version.</sub>
50
50
 
51
- **Updating**: the command line overwrites the installed version directly; the
52
- graphical interface rejects a re-install, so uninstall the old version first,
53
- then install the new one.
54
-
55
51
  > [!WARNING]
56
52
  > The npm name `dsh-rewind` belongs to another author's package — install with `dsh-rewind-plugin`.
57
53
 
@@ -66,8 +62,8 @@ then install the new one.
66
62
  <details>
67
63
  <summary><b>Edge notes</b></summary>
68
64
 
69
- - Rewinds can be repeated — with no limit on stage or count.
70
- - A rewind itself **cannot be undone**, but the withdrawn content stays in the session log.
65
+ - **The withdrawn content is preserved** — it stays in the session log and can be inspected in the **Trajectory** panel; you can also disable the plugin to show it again (display only).
66
+ - **A rewind cannot be undone in place** — to carry the content forward, first disable the plugin so the withdrawn content shows again, then click **Branch into a new conversation** on the message just before the rewind; the new conversation's context will include it.
71
67
  - **Interruptions rewind too** — a `steering` message the model hasn't read yet is also a valid rewind target, and withdrawing it does not interrupt the current run.
72
68
  - **Rewinding a read message interrupts the running turn** — to ensure the rewind runs safely.
73
69
 
@@ -79,17 +75,16 @@ then install the new one.
79
75
  switched off in this plugin's configuration card. When off, no new snapshots
80
76
  are created; file changes made while off cannot be restored. Snapshots (the
81
77
  before-write backups) are stored under `<dsh home>/rewind-snapshots/`
82
- (the default is `~/.dsh/rewind-snapshots/`). For the **same session**, the plugin
83
- deduplicates snapshots by content and keeps the newest 100 anchor groups.
78
+ (the default is `~/.dsh/rewind-snapshots/`).
84
79
  **Deleting that directory manually** only clears the file backups (chat rewinds are
85
- unaffected); the plugin can rebuild them automatically.
80
+ unaffected); the plugin can rebuild that directory automatically.
86
81
 
87
- A **global auto-cleanup** (off by default) removes the snapshot directories of
82
+ A **snapshot auto-cleanup** (off by default) removes the snapshot directories of
88
83
  long-inactive sessions, leaving the active session and chat log untouched. Configure
89
84
  and review it on the **Plugins** page, in this plugin's configuration card (the
90
85
  auto-cleanup switch and the idle-day cutoff), or use the
91
86
  `/snapshot-auto-cleanup` command to **view, configure, and run** it. See:
92
- [Snapshot cleanup](docs/snapshot-auto-cleanup.md).
87
+ [Snapshot auto-cleanup](docs/snapshot-auto-cleanup.md).
93
88
 
94
89
  <img src="assets/screenshots/plugin-setting.png" alt="Plugin configuration card: snapshot backups and auto cleanup" width="600">
95
90
 
@@ -104,19 +99,19 @@ rm -rf <dsh home>/rewind-snapshots
104
99
  rm <dsh home>/snapshot-cleanup-last-sweep.json
105
100
  ```
106
101
 
107
- The plugin's auto-cleanup settings live in the current Profile's plugin
108
- configuration. To remove them completely, delete this plugin entry's configuration.
102
+ The plugin's configuration entries are stored in the current Profile's config
103
+ file `cordis.patch.yml`. To clean up completely, delete the corresponding entries
104
+ manually.
109
105
 
110
- ## Why it stands out
106
+ ## What this plugin changes
111
107
 
112
- Compared with the common approaches, here is the trade-off this plugin makes on "rewind":
108
+ Compared with the default behavior:
113
109
 
114
- | Dimension | Common approach | This plugin |
110
+ | Dimension | Default behavior | This plugin |
115
111
  | --- | --- | --- |
116
- | Conversation rewind | Fork / branch a new conversation | **In-place rewind** — no new session, no window switch |
117
- | File restore | No restore feature / git-managed or whole-tree snapshot | **Lightweight before-backups** — auto-captured before writes, one-click restore |
118
- | Dependencies | Often needs a Git repo or a full snapshot engine | **None** — no git required, works on any directory |
119
- | Storage footprint | Whole-tree snapshots take space | **Lightweight** — nothing is stored unless it changed, and only files touched by write tools are tracked |
112
+ | Conversation rewind | Branch into a new conversation | **In-place rewind** — no new session branch, no window switch, no leftover session copies, and rewinding is more convenient |
113
+ | File restore | Manual restore / Git management | **One-click restore** — lightweight before-write backups; optionally restore files together with the conversation, fitting agent workflows |
114
+ | Restore dependencies | Often needs a Git repository or a full snapshot engine | **None** — no Git repository and no code project required; works in any directory |
120
115
 
121
116
  ## How it works
122
117
 
@@ -131,7 +126,7 @@ The whole design rests on two principles, simple but deliberate: **the conversat
131
126
  - **The replacement is imperceptible** — the model ignores the marker, with no effect (verified empirically). Together with the plugin's UI handling, what you and the model see is exactly how the conversation looked at the target;
132
127
  - **Content is preserved** — because this is "masking, not deleting", the withdrawn content stays in the log — auditable, traceable, and in principle manually recoverable.
133
128
 
134
- > **Design highlight**: the entire conversation rewind is **a single append**. It's deterministic, auditable, and — because the log was never broken — a "clean" time-travel. Minimal action, complete semantics. The compatibility subtleties with DSH (replicating `/compact`, the empty-message mask) are where this plugin is genuinely professional.
129
+ > **Design highlight**: the entire conversation rewind is **a single append**. It's deterministic, auditable, and — because the log was never broken — a "clean" time-travel. Minimal action, complete semantics.
135
130
 
136
131
  ### 2. File restore: lightweight checkpointing, "before-write backup"
137
132
 
@@ -155,8 +150,9 @@ This plugin deliberately stays lightweight and focused on one thing — "convers
155
150
 
156
151
  ## Compatibility
157
152
 
158
- - Node.js `^22.19.0 || >=24.0.0`.
159
- - Compatibility definition, verification method, and version alignment: see [docs/compat/audit.md](docs/compat/audit.md); supported DSH versions are declared by `package.json` `engines.dsh`.
153
+ - Node.js `>=24.9.0`.
154
+ - Supported DSH versions are declared by `package.json` `engines.dsh`.
155
+ - Compatibility definition, verification method, and version alignment: see [docs/compat/audit.md](docs/compat/audit.md).
160
156
 
161
157
  > [!WARNING]
162
158
  > This project and DeepSeek Harness are both in developer preview. Pin exact
@@ -164,16 +160,17 @@ This plugin deliberately stays lightweight and focused on one thing — "convers
164
160
 
165
161
  ## Known issues
166
162
 
167
- 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.
163
+ 1. **Exported logs are complete** — `/export` exports the whole session log (raw JSONL), **withdrawn content included**; this plugin cannot alter exports.
168
164
  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).
169
- 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.
165
+ 3. **Turn navigation keeps rewound turns** — the right-side turn navigation keeps an "unloaded" tick for each rewound turn, and hovering previews the withdrawn content. Only a display difference; no functional impact.
166
+ 4. **A far rewind may show a blank transcript** — when the target predates the loaded history, the conversation area goes blank; click **Load earlier** to page back until the history appears.
170
167
 
171
168
  > [!NOTE]
172
169
  > Browser diagnostics are available; see [Browser diagnostics](docs/compat/diagnostics.md).
173
170
 
174
171
  ## Security
175
172
 
176
- 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 files"; 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. 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).
173
+ 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 files"; 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. For sessions you've left inactive for a long time, a snapshot 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).
177
174
 
178
175
  ## Development
179
176
 
@@ -186,7 +183,7 @@ npm run build # esbuild: lib/index.js (host ESM) + lib/client.js (loade
186
183
  node scripts/verify-host.mjs # end-to-end verification of the built artifact
187
184
  ```
188
185
 
189
- `prepare` runs the full build, so git installs and `npm pack` / `npm publish` always produce a complete `lib/` and the `LICENSE`.
186
+ `prepack` runs the full build, so `npm pack` / `npm publish` always ship a complete `lib/`. Install from npm or a packed tarball — a git checkout has no built `lib/`.
190
187
 
191
188
  Maintainers: the module map and harness interface reference live in [docs/harness-reference.md](docs/harness-reference.md).
192
189
 
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # dsh-rewind
2
2
 
3
- DeepSeek Harness 插件:**一键就地回退对话到任意更早的用户消息**——<br />
4
- 同窗口内完成,不新建分支、不换窗口,可一并还原工作区文件(完整 Claude Code `/rewind` 语义)。
3
+ DeepSeek Harness 插件:**一键就地回退对话到任意更早的用户消息**。<br />
4
+ 同窗口内完成,不新建分支、不换窗口,可一并还原工作区文件——即完整的 Claude Code `/rewind` 语义。
5
5
 
6
6
  [English](README.en.md) | 中文
7
7
 
@@ -9,7 +9,7 @@ DeepSeek Harness 插件:**一键就地回退对话到任意更早的用户消
9
9
  [![npm downloads](https://img.shields.io/npm/dt/dsh-rewind-plugin.svg)](https://www.npmjs.com/package/dsh-rewind-plugin)
10
10
  [![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)
11
11
 
12
- ![回到任意更早的消息:消息旁的 ↶ 回退按钮与「仅回退对话 / 回退对话和文件」模式选择浮层](docs/preview.zh.svg)
12
+ ![回退到更早的消息,可选一并还原文件](docs/preview.zh.svg)
13
13
 
14
14
  刻意聚焦、保持极简,只做一件事:**就地回退到任意远的用户消息**,还能**顺手还原改过的文件**。
15
15
 
@@ -42,13 +42,11 @@ DeepSeek Harness 插件:**一键就地回退对话到任意更早的用户消
42
42
  dsh plugin --profile <name> add dsh-rewind-plugin@<版本>
43
43
  ```
44
44
 
45
- **图形界面**:侧栏「插件」页 →「添加插件」→ 填 `dsh-rewind-plugin@<版本>` → 安装 → 重启 DSH 并刷新浏览器。
45
+ **图形界面**:侧栏「插件」页 →「添加插件」→ 填 `dsh-rewind-plugin@<版本>` → 安装 → 重启 DSH。
46
46
 
47
47
  <img src="assets/screenshots/add-plugin.png" width="600" alt="「添加插件」弹窗:在输入框填入 dsh-rewind-plugin@版本号"><br>
48
48
  <sub>图片仅为示例,请以实际适配版本号为准。</sub>
49
49
 
50
- **更新**:命令行可直接覆盖安装;图形界面会拒绝重复安装,需先卸载旧版本,再装新版本。
51
-
52
50
  > [!WARNING]
53
51
  > npm 上的 `dsh-rewind` 属于其他作者,请用 `dsh-rewind-plugin` 安装。
54
52
 
@@ -63,8 +61,8 @@ dsh plugin --profile <name> add dsh-rewind-plugin@<版本>
63
61
  <details>
64
62
  <summary><b>边界说明</b></summary>
65
63
 
66
- - 回退可以反复进行——没有阶段或次数限制。
67
- - 回退本身**无法撤销**,但被撤回的内容仍保留在会话日志中。
64
+ - **回退内容完整保存**——被撤回的内容仍在日志中,可在「轨迹」面板查看;也可禁用插件让它们重新显示(仅界面)。
65
+ - **回退无法原地撤销**——但可以先禁用插件让被撤回的内容重新显示,再在回退前的位置上点「在新对话中分支」,新会话的上下文即可包含这些内容。
68
66
  - **插话也能回退**——模型尚未读取的 `steering` 插话消息,同样可作为回退目标,不会打断当前生成。
69
67
  - **回退已读消息会打断当前正在运行的回合**——确保回退的安全执行。
70
68
 
@@ -72,9 +70,9 @@ dsh plugin --profile <name> add dsh-rewind-plugin@<版本>
72
70
 
73
71
  ## 存储管理
74
72
 
75
- **快照备份**(默认开启):用于文件还原;可在本插件的配置卡片中关闭。关闭后不再创建新的快照,关闭期间的文件改动无法还原。快照存储于 `<dsh home>/rewind-snapshots/`(默认 `~/.dsh/rewind-snapshots/`)。手动删除该目录仅清除文件备份(对话回退不受影响),插件可自动重建。
73
+ **快照备份**(默认开启):用于文件还原;可在本插件的配置卡片中关闭。关闭后不再创建新的快照,关闭期间的文件改动无法还原。快照存储于 `<dsh home>/rewind-snapshots/`(默认 `~/.dsh/rewind-snapshots/`)。手动删除该目录仅清除文件备份,对话回退不受影响,插件可自动重建。
76
74
 
77
- **全局自动清理**(默认关闭):把长期不活跃的会话快照整目录移除,不影响活动会话与对话日志。可在配置卡片查看与配置(自动清理开关、失活天数),也可用 `/snapshot-auto-cleanup` 命令查看、设置和运行。详见:[快照自动清理](docs/snapshot-auto-cleanup.zh.md)。
75
+ **快照自动清理**(默认关闭):把长期不活跃的会话快照整目录移除,不影响活动会话与对话日志。可在配置卡片查看与配置(自动清理开关、失活天数),也可用 `/snapshot-auto-cleanup` 命令查看、设置和运行。详见:[快照自动清理](docs/snapshot-auto-cleanup.zh.md)。
78
76
 
79
77
  <img src="assets/screenshots/plugin-setting.png" alt="插件配置卡片:快照备份与自动清理" width="600">
80
78
 
@@ -89,18 +87,17 @@ rm -rf <dsh home>/rewind-snapshots
89
87
  rm <dsh home>/snapshot-cleanup-last-sweep.json
90
88
  ```
91
89
 
92
- 插件的自动清理设置保存在当前 Profile 的插件配置中。如需彻底清理,可手动删除对应条目。
90
+ 本插件的配置项保存在当前 Profile 的配置文件 `cordis.patch.yml` 中。如需彻底清理,可手动删除对应条目。
93
91
 
94
- ## 本插件的优势
92
+ ## 本插件的作用
95
93
 
96
- 和常见的几种做法相比,本插件在"回退"这件事上的取舍:
94
+ 相较于默认行为,本插件带来的变化:
97
95
 
98
- | 维度 | 常见做法 | 本插件 |
96
+ | 维度 | 默认行为 | 本插件 |
99
97
  | --- | --- | --- |
100
- | 对话回退 | Fork 分支新建对话 | **就地回退**——不新建会话、不切窗口,便捷回退 |
101
- | 文件还原 | 无还原功能 / git 管理或完整快照 | **写前轻量备份**——写文件前自动存原内容,一键还原 |
102
- | 依赖 | 常依赖 Git 仓库或完整快照引擎 | **无依赖**——不依赖 git,普通目录即可用 |
103
- | 存储开销 | 整树快照占空间大 | **轻量**——不变不存,且只追踪写类工具改动过的文件 |
98
+ | 对话回退 | 新建分支对话 | **就地回退**——不用新建会话分支、不切窗口,也不产生残留会话副本,回退更便捷 |
99
+ | 文件还原 | 手动还原 / Git 管理 | **一键还原**——写前轻量备份,回退对话时可选一并还原文件,贴合 Agent 工作流 |
100
+ | 还原依赖 | 常依赖 Git 仓库或完整快照引擎 | **无依赖**——不依赖 Git 仓库,也不要求是代码项目,普通目录即可用 |
104
101
 
105
102
  ## 原理
106
103
 
@@ -115,7 +112,7 @@ rm <dsh home>/snapshot-cleanup-last-sweep.json
115
112
  - **替换内容无感**——模型对标记忽略、无感(实测验证)。配合插件对界面显示的处理,模型和你看到的对话就是目标消息当时的样子;
116
113
  - **记录完整保留**——因为是「遮蔽」而非「删除」,被撤回的内容完整留在日志里,可审计、可追溯,原则上也能手动恢复。
117
114
 
118
- > **设计点睛**:整个对话回退就是**一条**追加。它确定、可审计,且因为日志从未被破坏,回溯是「干净的」——用最小的动作,实现最完整的语义。那些与 DSH 内部的兼容细节(对 `/compact` 的复刻、空消息的遮蔽)正是插件的专业所在。
115
+ > **设计点睛**:整个对话回退就是**一条**追加。它确定、可审计,且因为日志从未被破坏,回溯是「干净的」——用最小的动作,实现最完整的语义。
119
116
 
120
117
  ### 2. 文件还原:轻量检查点,「写前备份」
121
118
 
@@ -131,7 +128,7 @@ rm <dsh home>/snapshot-cleanup-last-sweep.json
131
128
 
132
129
  ## 明确不做的事
133
130
 
134
- 本插件刻意保持轻量、聚焦"对话回退"这一件事,以下场景**不属于它的职责**:
131
+ 本插件刻意保持轻量、聚焦「对话回退」这一件事,以下场景**不属于它的职责**:
135
132
 
136
133
  - **整树 / Git 级快照**——只跟踪写类工具编辑 + 已跟踪文件的外部改动,从未被工具碰过的文件不还原。需要工作树级的完整快照回退时,请交给更专业的快照工具(git)。
137
134
  - **子代理的编辑**——不追踪,子代理会话内也不提供回退(同 Claude Code):子代理运行在自己的会话里,其备份无法由父会话的回退还原,因此也不会为子会话保留备份。
@@ -139,8 +136,9 @@ rm <dsh home>/snapshot-cleanup-last-sweep.json
139
136
 
140
137
  ## 兼容性
141
138
 
142
- - Node.js `^22.19.0 || >=24.0.0`。
143
- - 兼容性定义、验证方法与版本对齐详见 [docs/compat/audit.md](docs/compat/audit.md);支持的 DSH 版本由 `package.json` 的 `engines.dsh` 声明。
139
+ - Node.js `>=24.9.0`。
140
+ - 支持的 DSH 版本由 `package.json` 的 `engines.dsh` 声明。
141
+ - 兼容性定义、验证方法与版本对齐详见 [docs/compat/audit.md](docs/compat/audit.md)。
144
142
 
145
143
  > [!WARNING]
146
144
  > 本项目与 DeepSeek Harness 均处于开发者预览阶段。可复现环境请 pin 精确版本,
@@ -148,16 +146,17 @@ rm <dsh home>/snapshot-cleanup-last-sweep.json
148
146
 
149
147
  ## 已知问题
150
148
 
151
- 1. **导出的日志是完整内容**——回退只是把消息从模型上下文和视图中移除,`/export` 导出的会话日志包含**已撤回的消息**。本插件无法改动导出。
149
+ 1. **导出的日志是完整内容**——`/export` 导出整个会话日志(原始 JSONL),包含**已撤回的内容**;本插件无法改动导出。
152
150
  2. **轻量文件回退存在代价**——特定情况可能无法回退所有修改。行为与 Claude Code 一致。详见:[文件回退的追踪边界](docs/compat/tracking-boundary.zh.md)。
153
- 3. **导轨显示已回退轮次**——DSH `v0.1.2` 新增右侧导轨,为已撤回消息保留刻度,悬浮显示已撤回正文。仅显示差异,无功能影响。
151
+ 3. **轮次导航保留已回退轮次**——右侧轮次导航为已回退轮次保留「未加载」刻度,悬浮可预览已撤回内容。仅显示差异,无功能影响。
152
+ 4. **较远回退可能显示空白**——回退目标早于已加载的历史时,对话区会显示空白;需点击「加载更早」往前翻,才能看到历史对话。
154
153
 
155
154
  > [!NOTE]
156
155
  > 本插件提供浏览器端诊断输出;详见 [浏览器诊断](docs/compat/diagnostics.zh.md)。
157
156
 
158
157
  ## 安全
159
158
 
160
- 本插件只向会话日志追加回退标记事件,从不删除或改写已记录的历史。工作区文件仅在「回退对话和文件」时被改写,备份存储于 `<dsh home>/rewind-snapshots/`;还原以备份为唯一来源。不触碰你的 git 仓库,无网络请求,不访问任何凭据。对**长期不活跃**的会话,另有默认关闭的全局自动清理可整目录移除其快照,不影响活动会话与对话日志。完整安全模型:[SECURITY.md](SECURITY.md)。
159
+ 本插件只向会话日志追加回退标记事件,从不删除或改写已记录的历史。工作区文件仅在「回退对话和文件」时被改写,备份存储于 `<dsh home>/rewind-snapshots/`;还原以备份为唯一来源。不触碰你的 git 仓库,无网络请求,不访问任何凭据。对**长期不活跃**的会话,另有默认关闭的快照自动清理可整目录移除其快照,不影响活动会话与对话日志。完整安全模型:[SECURITY.md](SECURITY.md)。
161
160
 
162
161
  ## 开发
163
162
 
@@ -170,7 +169,7 @@ npm run build # esbuild:lib/index.js(host ESM)+ lib/client.js(l
170
169
  node scripts/verify-host.mjs # 端到端验证构建产物
171
170
  ```
172
171
 
173
- `prepare` 执行完整构建,所以 git 安装与 `npm pack` / `npm publish` 总会产出完整的 `lib/` 与 `LICENSE`。
172
+ `prepack` 在打包前执行完整构建,所以 `npm pack` / `npm publish` 总会产出完整的 `lib/`。请从 npm 或打好的 tarball 安装——git 检出的源码里没有构建好的 `lib/`。
174
173
 
175
174
  维护者:模块地图与 harness 接口参考见 [docs/harness-reference.md](docs/harness-reference.md)
176
175
 
package/lib/client.js CHANGED
@@ -108,27 +108,30 @@ function messageTextAt(chat, seq) {
108
108
  }
109
109
  return void 0;
110
110
  }
111
+ function imageRefsOf(content) {
112
+ const images = [];
113
+ for (const block of content ?? []) {
114
+ if (block.type !== "image") continue;
115
+ const attachment = block.attachment;
116
+ if (typeof attachment?.attachmentId !== "string" || typeof attachment.mediaType !== "string") continue;
117
+ images.push({
118
+ attachmentId: attachment.attachmentId,
119
+ mediaType: attachment.mediaType,
120
+ bytes: typeof attachment.bytes === "number" ? attachment.bytes : void 0,
121
+ width: typeof attachment.width === "number" ? attachment.width : void 0,
122
+ height: typeof attachment.height === "number" ? attachment.height : void 0,
123
+ name: typeof attachment.name === "string" ? attachment.name : void 0
124
+ });
125
+ }
126
+ return images;
127
+ }
111
128
  function messageAttachmentsAt(chat, seq) {
112
129
  if (chat === void 0) return void 0;
113
130
  for (const node of chatNodesOf(chat)) {
114
131
  if (node.kind !== "user" && node.kind !== "steering") continue;
115
132
  const data = node.data;
116
133
  if (data.seq !== seq) continue;
117
- const images = [];
118
- for (const block of data.content ?? []) {
119
- if (block.type !== "image") continue;
120
- const attachment = block.attachment;
121
- if (typeof attachment?.attachmentId !== "string" || typeof attachment.mediaType !== "string") continue;
122
- images.push({
123
- attachmentId: attachment.attachmentId,
124
- mediaType: attachment.mediaType,
125
- bytes: typeof attachment.bytes === "number" ? attachment.bytes : void 0,
126
- width: typeof attachment.width === "number" ? attachment.width : void 0,
127
- height: typeof attachment.height === "number" ? attachment.height : void 0,
128
- name: typeof attachment.name === "string" ? attachment.name : void 0
129
- });
130
- }
131
- return images;
134
+ return imageRefsOf(data.content);
132
135
  }
133
136
  return void 0;
134
137
  }
@@ -1089,6 +1092,7 @@ function steeringItemsOf(nextStep) {
1089
1092
  id: item.id,
1090
1093
  text,
1091
1094
  attachments: attachmentsCountOf(item.content),
1095
+ images: imageRefsOf(item.content),
1092
1096
  preview: previewOf(text)
1093
1097
  };
1094
1098
  });
@@ -1402,6 +1406,7 @@ function RewindPortals({ sessionId, sessionOf, chatOf, isMainViewSession, watchC
1402
1406
  sessionId,
1403
1407
  sessionOf,
1404
1408
  setComposerText,
1409
+ restoreImages,
1405
1410
  t
1406
1411
  },
1407
1412
  target.key
@@ -1463,8 +1468,9 @@ function composerText() {
1463
1468
  if (surface === null) return "";
1464
1469
  return surface.textContent ?? "";
1465
1470
  }
1466
- async function retractPending(session, itemId, text, setComposerText) {
1471
+ async function retractPending(session, itemId, text, setComposerText, restoreImages) {
1467
1472
  const steering = steeringItemsOf(inboxOf(session)?.["next-step"]);
1473
+ const images = steering.find((item) => item.id === itemId)?.images ?? [];
1468
1474
  let removedTarget = false;
1469
1475
  for (const id of retractSpan(steering, itemId)) {
1470
1476
  let accepted = false;
@@ -1478,11 +1484,19 @@ async function retractPending(session, itemId, text, setComposerText) {
1478
1484
  if (!accepted) break;
1479
1485
  if (id === itemId) removedTarget = true;
1480
1486
  }
1481
- if (removedTarget && text !== null && text !== "" && composerText().trim() === "") {
1487
+ if (!removedTarget) return;
1488
+ if (images.length > 0 && restoreImages !== void 0) {
1489
+ try {
1490
+ await restoreImages(session.sessionId, images);
1491
+ } catch (error) {
1492
+ rewindLog.warn("retract", `image restore for ${itemId} threw`, error);
1493
+ }
1494
+ }
1495
+ if (text !== null && text !== "" && composerText().trim() === "") {
1482
1496
  setComposerText(session.sessionId, text);
1483
1497
  }
1484
1498
  }
1485
- function RetractButton({ target, sessionId, sessionOf, setComposerText, t }) {
1499
+ function RetractButton({ target, sessionId, sessionOf, setComposerText, restoreImages, t }) {
1486
1500
  const onClick = (event) => {
1487
1501
  event.stopPropagation();
1488
1502
  const session = sessionOf(sessionId);
@@ -1494,7 +1508,7 @@ function RetractButton({ target, sessionId, sessionOf, setComposerText, t }) {
1494
1508
  t,
1495
1509
  retract: { itemId: target.itemId, text: target.text },
1496
1510
  onRetract: () => {
1497
- void retractPending(session, target.itemId, target.text, setComposerText);
1511
+ void retractPending(session, target.itemId, target.text, setComposerText, restoreImages);
1498
1512
  }
1499
1513
  });
1500
1514
  };
@@ -1576,9 +1590,9 @@ async function candidatesOverRpc(connection, sessionId) {
1576
1590
  }
1577
1591
 
1578
1592
  // src/client/build-info.ts
1579
- var PLUGIN_VERSION = true ? "0.20.0-alpha.1" : "dev";
1593
+ var PLUGIN_VERSION = true ? "0.20.0-alpha.3" : "dev";
1580
1594
  var PLUGIN_PACKAGE = "dsh-rewind-plugin";
1581
- var BUILD_HASH = true ? "fa33ad17" : "dev";
1595
+ var BUILD_HASH = true ? "46008907" : "dev";
1582
1596
 
1583
1597
  // src/client/locales.ts
1584
1598
  var en = {
package/lib/index.js CHANGED
@@ -650,10 +650,13 @@ function refToSource(raw, sessionDir) {
650
650
  if (typeof ref.text === "string") return textSourceOf(ref.text);
651
651
  return void 0;
652
652
  }
653
+ function sessionRelativeRef(sessionDir, file) {
654
+ return relative(sessionDir, file).split(sep).join("/");
655
+ }
653
656
  function sourceToRef(source, sessionDir, entryPath) {
654
657
  if (source === null) return null;
655
658
  if (source.kind === "blob") {
656
- const blob = relative(sessionDir, source.path);
659
+ const blob = sessionRelativeRef(sessionDir, source.path);
657
660
  if (!isSafeBackupRef(blob)) throw new Error(`unsafe backup ref ${blob} for ${entryPath}`);
658
661
  return { blob };
659
662
  }
@@ -996,7 +999,7 @@ var SnapshotStore = class _SnapshotStore {
996
999
  * the call id would produce a dangling reference.
997
1000
  */
998
1001
  refOfRead(sessionId, entry) {
999
- if (entry.file !== void 0) return relative(this.sessionDir(sessionId), entry.file);
1002
+ if (entry.file !== void 0) return sessionRelativeRef(this.sessionDir(sessionId), entry.file);
1000
1003
  return this.entryRefOf(sessionId, entry.callId, entry.anchorSeq);
1001
1004
  }
1002
1005
  /** Drop every in-memory trace of one session (its directory is gone). */
@@ -2064,7 +2067,7 @@ var SnapshotStore = class _SnapshotStore {
2064
2067
  for (const action of journal.actions) {
2065
2068
  const source = action.before;
2066
2069
  if (source === null || source.kind !== "blob") continue;
2067
- const ref = relative(sessionDir, source.path);
2070
+ const ref = sessionRelativeRef(sessionDir, source.path);
2068
2071
  if (!isSafeBackupRef(ref)) continue;
2069
2072
  const anchor = refAnchorOf(ref);
2070
2073
  if (Number.isSafeInteger(anchor)) pinned.add(anchor);
@@ -103,6 +103,25 @@ export interface MessageImageRef {
103
103
  readonly height?: number;
104
104
  readonly name?: string;
105
105
  }
106
+ /** One wire content block that may carry a durable image reference. */
107
+ export interface ImageBlockLike {
108
+ readonly type?: string;
109
+ readonly attachment?: {
110
+ readonly attachmentId?: unknown;
111
+ readonly mediaType?: unknown;
112
+ readonly bytes?: unknown;
113
+ readonly width?: unknown;
114
+ readonly height?: unknown;
115
+ readonly name?: unknown;
116
+ };
117
+ }
118
+ /**
119
+ * Image references carried by a message's content blocks, in order.
120
+ * Malformed and non-image blocks are skipped; `file` blocks are not restorable.
121
+ * @param content - one message's content blocks.
122
+ * @returns the image references, in message order.
123
+ */
124
+ export declare function imageRefsOf(content: readonly ImageBlockLike[] | undefined): readonly MessageImageRef[];
106
125
  /**
107
126
  * The durable image references of the human message at `seq` in the chat
108
127
  * snapshot, in message order. The composer-refill counterpart of
@@ -17,6 +17,7 @@
17
17
  *
18
18
  * @module dsh-rewind/client/pending
19
19
  */
20
+ import { type ImageBlockLike, type MessageImageRef } from './hidden.ts';
20
21
  /** One rendered pending-steering bubble row (only the fields matching reads). */
21
22
  export interface PendingRow {
22
23
  /** The bubble's message text, excluding the actions container (see module doc). */
@@ -24,8 +25,8 @@ export interface PendingRow {
24
25
  /** Message-attachment blocks the row renders; compared against the item's count. */
25
26
  readonly attachments: number;
26
27
  }
27
- /** One wire content block of an inbox row (only the fields the derivation reads). */
28
- export interface InboxBlockLike {
28
+ /** One wire content block of an inbox row. */
29
+ export interface InboxBlockLike extends ImageBlockLike {
29
30
  /** Wire block type (`text` | `image` | `file` | …). */
30
31
  readonly type: string;
31
32
  /** The block's text, on a `text` block. */
@@ -59,6 +60,8 @@ export interface PendingSteeringItem {
59
60
  readonly text: string | null;
60
61
  /** Non-text blocks (image/file/…) the message carries, in prompt order. */
61
62
  readonly attachments: number;
63
+ /** Image references the message carries, in prompt order. */
64
+ readonly images: readonly MessageImageRef[];
62
65
  /** Space-collapsed preview with image/file blocks excluded (the harness QueueDock rule). */
63
66
  readonly preview: string;
64
67
  }
@@ -280,9 +280,8 @@ export declare function composerText(): string;
280
280
  *
281
281
  * 1. Remove the target and its future (steering only; queued stays) through
282
282
  * the session's own `updateQueue` channel, oldest first.
283
- * 2. Put the target's text back in the composer ONLY when the target was
284
- * actually removed and the composer is empty (Claude Code's auto-restore
285
- * guard, so a draft the user is typing is never clobbered).
283
+ * 2. Put the target's own images back, then its text when the composer is
284
+ * empty (the empty-composer guard, so a draft is never clobbered).
286
285
  *
287
286
  * The realistic failure is `queue-item-not-found`: a step boundary claimed
288
287
  * the whole next-step batch a moment ago, so the message is now durable and
@@ -290,7 +289,7 @@ export declare function composerText(): string;
290
289
  * removed then, and the composer is NOT refilled — refilling would invite a
291
290
  * duplicate send of a message the model has already received.
292
291
  */
293
- export declare function retractPending(session: SessionFace, itemId: string, text: string | null, setComposerText: (sessionId: string, text: string) => boolean): Promise<void>;
292
+ export declare function retractPending(session: SessionFace, itemId: string, text: string | null, setComposerText: (sessionId: string, text: string) => boolean, restoreImages?: (sessionId: string, images: readonly MessageImageRef[]) => Promise<void>): Promise<void>;
294
293
  /**
295
294
  * Build the slot-entry component for the plugin apply(): a tiny bridge that
296
295
  * injects the apply-time capabilities (session resolution, locale, rewind
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-rewind-plugin",
3
- "version": "0.20.0-alpha.1",
3
+ "version": "0.20.0-alpha.3",
4
4
  "description": "In-window conversation rewind with workspace file restore",
5
5
  "icon": "./assets/icon.svg",
6
6
  "keywords": [
@@ -17,6 +17,9 @@
17
17
  "url": "git+https://github.com/SiriLee/dsh-rewind.git"
18
18
  },
19
19
  "homepage": "https://github.com/SiriLee/dsh-rewind#readme",
20
+ "bugs": {
21
+ "url": "https://github.com/SiriLee/dsh-rewind/issues"
22
+ },
20
23
  "license": "MIT",
21
24
  "engines": {
22
25
  "node": ">=24.9.0",
@@ -70,7 +73,7 @@
70
73
  "verify:host": "node scripts/verify-host.mjs",
71
74
  "verify:previews": "node scripts/verify-preview-assets.mjs",
72
75
  "check": "npm run typecheck && npm run build && npm test && node scripts/verify-host.mjs && npm pack --dry-run",
73
- "prepare": "npm run build"
76
+ "prepack": "npm run build"
74
77
  },
75
78
  "publishConfig": {
76
79
  "access": "public"