dsh-rewind-plugin 0.6.1 → 0.6.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 +3 -2
- package/README.md +3 -2
- package/SECURITY.md +14 -7
- package/docs/architecture.md +1 -1
- package/docs/compat/audit.md +26 -16
- package/docs/contract/client-contract.md +1 -1
- package/docs/contract/client-contract.zh.md +1 -1
- package/docs/release/release.md +6 -6
- package/docs/release/release.zh.md +5 -4
- package/lib/client.js +96 -40
- package/lib/index.js +12 -2
- package/lib/types/client/hidden.d.ts +10 -0
- package/lib/types/client/portals.d.ts +40 -7
- package/lib/types/settings-locale.d.ts +32 -0
- package/package.json +13 -12
package/README.en.md
CHANGED
|
@@ -12,7 +12,7 @@ A deliberately focused plugin with one job: **rewind to any user message, no mat
|
|
|
12
12
|
|
|
13
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
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;
|
|
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
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.
|
|
17
17
|
|
|
18
18
|
## Preview
|
|
@@ -152,10 +152,11 @@ withdrew should consume the stable, locale-independent helpers exported from
|
|
|
152
152
|
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).
|
|
153
153
|
3. **Rewinds from `≤ v0.2.4`** — sessions rewound with these versions may **fail to load history** after more conversation. Install a v0.3.3-or-earlier release and use its bundled repair tool ([docs/compat/troubleshooting.md](docs/compat/troubleshooting.md)).
|
|
154
154
|
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.
|
|
155
|
+
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.
|
|
155
156
|
|
|
156
157
|
## Security
|
|
157
158
|
|
|
158
|
-
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
|
|
159
|
+
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).
|
|
159
160
|
|
|
160
161
|
## Development
|
|
161
162
|
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ DeepSeek Harness 插件:**一键就地回退对话到任意更早的用户消
|
|
|
12
12
|
|
|
13
13
|
- **回退 = 时间回溯**——目标消息及其之后的全部内容(agent 回复、工具调用)同时从**模型上下文**和**渲染对话**中撤回,不新建会话、不切换窗口;目标消息文本会回填输入框,改完可重发。**在原理上就真正无感、便捷**。
|
|
14
14
|
- **轻量工作区备份**——对齐 Claude Code:追踪写类工具编辑过的文件,**已跟踪文件的外部变更也能还原**。局部追踪、写前备份、不变不存。一个轻型插件,即拥有**完备的智能体回退能力**。
|
|
15
|
-
- **信息安全优先**——插件从不删改会话日志(append-only
|
|
15
|
+
- **信息安全优先**——插件从不删改会话日志(append-only),从不真正删除你的任何对话;备份存于自己的快照目录;还原只用这些备份。完整安全模型:[SECURITY.md](SECURITY.md)。
|
|
16
16
|
- **完备测试系统**——单元、探针、端到端主机验证,覆盖兼容性探测、日志重放、续接、跨重启等场景;随 harness 升级持续维护,确保功能稳定。
|
|
17
17
|
|
|
18
18
|
## 效果预览
|
|
@@ -140,10 +140,11 @@ dsh plugin --profile web add dsh-rewind-plugin
|
|
|
140
140
|
2. **轻量文件回退存在代价**——特定情况可能无法回退所有修改。行为与 Claude Code 一致。详见:[文件回退的追踪边界](docs/compat/tracking-boundary.zh.md)。
|
|
141
141
|
3. **v0.2.4 及更早版本**回退过的会话,继续对话后可能加载历史失败。可安装 v0.3.3 及之前版本的随附修复工具处理([完整步骤](docs/compat/troubleshooting.zh.md))。
|
|
142
142
|
4. **v0.3.3 及更早版本**回退过的会话,压缩对话(compact)不可用。新版本已兼容;受影响的旧会话建议新建会话。
|
|
143
|
+
5. **导轨显示已回退轮次**——DSH `v0.1.2-alpha.1` 新增的右侧导轨,会为已撤回消息保留刻度:点击不跳转、悬浮显示已撤回正文。仅显示差异,无功能影响。
|
|
143
144
|
|
|
144
145
|
## 安全
|
|
145
146
|
|
|
146
|
-
|
|
147
|
+
本插件只向会话日志追加回退标记事件,从不删除或改写已记录的历史。工作区文件仅在「回退对话和代码」时被改写,备份存储于 `~/.dsh/rewind-snapshots/`;还原以备份为唯一来源。不触碰你的 git 仓库,无网络请求,不访问任何凭据。对**长期不活跃**的会话,另有默认关闭的全局自动清理可整目录移除其快照,不影响活动会话与对话日志。完整安全模型:[SECURITY.md](SECURITY.md)。
|
|
147
148
|
|
|
148
149
|
## 开发
|
|
149
150
|
|
package/SECURITY.md
CHANGED
|
@@ -83,9 +83,8 @@ and is repaired offline (see `docs/compat/troubleshooting.md`).
|
|
|
83
83
|
|
|
84
84
|
- **Store root**: `<harness home>/rewind-snapshots/` by default (resolved via
|
|
85
85
|
`resolveDshHome`, so `~/.dsh/...` when `DSH_HOME` is unset); the `snapshotDir`
|
|
86
|
-
config, then `DSH_REWIND_SNAPSHOT_DIR` env, override it.
|
|
87
|
-
|
|
88
|
-
rebuilds from scratch.
|
|
86
|
+
config, then `DSH_REWIND_SNAPSHOT_DIR` env, override it. Deleting it only
|
|
87
|
+
removes file backups and the store rebuilds from scratch.
|
|
89
88
|
- **Path sanitization**: session ids and call ids are scrubbed to
|
|
90
89
|
`[a-zA-Z0-9._-]` (`safeSessionId` / `safeFileId`) before joining the store
|
|
91
90
|
root; `.` and `..` bare values are replaced — hostile ids cannot traverse
|
|
@@ -138,10 +137,18 @@ and is repaired offline (see `docs/compat/troubleshooting.md`).
|
|
|
138
137
|
same operating-system user: state files are created with the process
|
|
139
138
|
default permissions (no special modes are set — a standard umask applies),
|
|
140
139
|
and the host user remains trusted.
|
|
141
|
-
- It does **not** exclude paths from the store
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
140
|
+
- It does **not** exclude paths from the store, and it never oversteps: the
|
|
141
|
+
plugin only adds backup/restore on top of permissions DSH already holds. Two
|
|
142
|
+
distinct cases follow:
|
|
143
|
+
1. **Sensitive / personal-information files** (e.g. a `.env` file, which may
|
|
144
|
+
sit inside the workspace) the model may read or edit are backed up and
|
|
145
|
+
restorable.
|
|
146
|
+
2. **Files outside the session workspace** the model is allowed to touch are
|
|
147
|
+
likewise backed up and restored.
|
|
148
|
+
In both cases the plugin introduces no authority of its own — DSH already
|
|
149
|
+
granted the read/edit; the plugin merely records a before-backup and can
|
|
150
|
+
restore it. Keeping a path out is a **DSH model-permission** concern (a
|
|
151
|
+
per-path deny), not a rewind feature.
|
|
145
152
|
- It does **not** touch git (no refs, index, or worktree operations), makes
|
|
146
153
|
**no network requests**, and does **not** access credentials.
|
|
147
154
|
- It does **not** roll back whole-log state: telemetry, search, and `/export`
|
package/docs/architecture.md
CHANGED
|
@@ -98,7 +98,7 @@ pruneStale cross-session auto-cleanup (default off): whole
|
|
|
98
98
|
|
|
99
99
|
## Compatibility strategy
|
|
100
100
|
|
|
101
|
-
- **Peer ranges as OR-union of DSH
|
|
101
|
+
- **Peer ranges as OR-union of DSH version tuples** (`^0.1.0-rc.6 || ^0.1.1-rc.2 || ^0.1.2-alpha.2`):
|
|
102
102
|
npm's prerelease rules require the peer range to share the host's
|
|
103
103
|
`[major, minor, patch]` tuple; `scripts/check-dsh-version.mjs` detects when
|
|
104
104
|
a new tuple requires appending. See `docs/release/release.md`.
|
package/docs/compat/audit.md
CHANGED
|
@@ -11,28 +11,37 @@
|
|
|
11
11
|
> source reference: the `oss/deepseek-harness` local fork.
|
|
12
12
|
>
|
|
13
13
|
> Version alignment: `peerDependencies` use an OR-union (e.g.
|
|
14
|
-
> `^0.1.0-rc.6 || ^0.1.1-rc.2`) covering each published
|
|
14
|
+
> `^0.1.0-rc.6 || ^0.1.1-rc.2 || ^0.1.2-alpha.2`) covering each published tuple series. npm's
|
|
15
15
|
> prerelease matching rules require a candidate to share the range comparator's
|
|
16
16
|
> `[major, minor, patch]` tuple, so each new tuple series (e.g. a future
|
|
17
17
|
> `0.1.2-rc.x`, `0.2.x`) requires appending a union member; rc rolling within a
|
|
18
18
|
> tuple (`0.1.1-rc.2 → rc.3`) is a no-op. Signal: `npm view @deepseek-ai/dsh version`;
|
|
19
|
-
> flow: `scripts/check-dsh-version.mjs
|
|
19
|
+
> flow: `scripts/check-dsh-version.mjs` (it reads the `latest` dist-tag only; a
|
|
20
|
+
> `-alpha` prerelease published under another tag, e.g. `0.1.2-alpha.2` under
|
|
21
|
+
> `alpha`, is a manual pre-release check).
|
|
20
22
|
>
|
|
21
|
-
> Client channel (harness `0.1.2-alpha.1
|
|
22
|
-
>
|
|
23
|
-
>
|
|
24
|
-
>
|
|
25
|
-
> `uiConversation` exposes the chat as a per-session named `"chat"` view
|
|
23
|
+
> Client channel (harness `0.1.2-alpha.x`; `0.1.2-alpha.2` is published to npm
|
|
24
|
+
> under the `alpha` dist-tag while `latest` stays `0.1.1-rc.2`): the plugin reads
|
|
25
|
+
> the session chat through `uiConversation` whenever the DSH client exposes it.
|
|
26
|
+
> `uiConversation` surfaces the chat as a per-session named `"chat"` view
|
|
26
27
|
> (contributed by `dsh-client-ui-chat` through the `uiSession` slot hook) instead
|
|
27
28
|
> of the session-face `chat` field. `src/client/hidden.ts` (`chatSnapshotOf`) reads
|
|
28
29
|
> the session-face snapshot first (rc.2 path), then the `uiConversation` `"chat"`
|
|
29
|
-
> view (alpha
|
|
30
|
-
> crash).
|
|
31
|
-
>
|
|
32
|
-
> `^0.1.0-rc.6 || ^0.1.1-rc.2
|
|
33
|
-
>
|
|
34
|
-
>
|
|
35
|
-
>
|
|
30
|
+
> view (alpha path); both missing degrades to `undefined` (no targets, never a
|
|
31
|
+
> crash). The channel is held via a lazy `ctx.get` (never a declared `inject`), so
|
|
32
|
+
> the plugin keeps the rc.2 type baseline while the OR-union covers the published
|
|
33
|
+
> `0.1.2` tuple: `^0.1.0-rc.6 || ^0.1.1-rc.2 || ^0.1.2-alpha.2`. Pinned by
|
|
34
|
+
> `tests/chat-channel.test.ts` (channel precedence + the alpha snapshot shape) and
|
|
35
|
+
> `tests/client-dom.test.ts` (the button-target pairing that consumes the chat).
|
|
36
|
+
> The composer refill is dual-channel the same way: on alpha the withdrawn text is
|
|
37
|
+
> written through the `conversation` service's `input` resolver's `setDraft` (the
|
|
38
|
+
> harness's own Lexical editor), else the rc.2 `<textarea>` / alpha `contenteditable`
|
|
39
|
+
> DOM write (`writeComposer` in `src/client/portals.tsx`); pinned by
|
|
40
|
+
> `tests/client-composer.test.ts`. The session-seat button DOM is dual-channel
|
|
41
|
+
> the same way: the actions (copy/branch) row was located by `[data-time-hover-root]`
|
|
42
|
+
> on ≤ 0.1.1-rc.x and by `[data-actions-reveal]` on 0.1.2-alpha.2
|
|
43
|
+
> (`ACTIONS_ROOT_SELECTOR` / `PENDING_SEAT_SELECTOR` in `src/client/portals.tsx`);
|
|
44
|
+
> pinned by `tests/client-dom.test.ts`.
|
|
36
45
|
|
|
37
46
|
## Definition of "fully compatible" (invariants)
|
|
38
47
|
|
|
@@ -56,7 +65,7 @@
|
|
|
56
65
|
- **session-title / goal fold**: a marker does not disturb `foldSessionTitle` / `foldGoal`.
|
|
57
66
|
- **client ordering**: turn-tail ordering + `step/start` uniqueness hold for tool turns + marker logs.
|
|
58
67
|
- **rewind across a compact checkpoint**: `RewindError('not-on-surface')` refuses cleanly, no crash.
|
|
59
|
-
- **plan-mode**:
|
|
68
|
+
- **plan-mode**: a marker reuses the last started turn (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`).
|
|
60
69
|
- **agent-loop cancellation**: `finally` guarantees step/turn closure; the rewind force-stop path leaves no dangling frame.
|
|
61
70
|
|
|
62
71
|
## Known behavior boundaries (deterministic differences, non-crash, documented)
|
|
@@ -67,6 +76,7 @@
|
|
|
67
76
|
- **Session title auto-regeneration**: the title derives from the surface, so an automatically-derived title may change after a rewind.
|
|
68
77
|
- **Files written but uncommitted in a cancelled turn**: a `both` rewind cannot restore them (tool side-effect timing; same as Claude Code).
|
|
69
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).
|
|
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`.
|
|
70
80
|
|
|
71
81
|
## Upstream (harness) issues and the plugin's no-compensation stance
|
|
72
82
|
|
|
@@ -157,7 +167,7 @@ new turn at `phase.turn + 1` (`agent.ts:251-255`) **without closing the leftover
|
|
|
157
167
|
| resume / session-query replay | ✓ | — | ✓ | — | — | — | ✓ | — |
|
|
158
168
|
| tool pipeline (snapshot/restore) | — | — | — | — | — | ✓ | — | ✓ |
|
|
159
169
|
| client ordering | — | — | ✓ | — | — | — | ✓ | — |
|
|
160
|
-
| plan-mode |
|
|
170
|
+
| plan-mode | ✓ | — | ✓ | — | — | — | — | ✓ (static) |
|
|
161
171
|
|
|
162
172
|
✓ = probe passes; — = not applicable. RU-I18N and R-OPENSTEP are upstream issues the plugin
|
|
163
173
|
does not compensate for (see above); G3 is a confirmed-non-defect behavior pinned in
|
|
@@ -21,7 +21,7 @@ the plugin itself uses (it never reads the DOM or host copy):
|
|
|
21
21
|
```ts
|
|
22
22
|
import { hiddenSeqsOf, type HiddenChat } from 'dsh-rewind-plugin/client'
|
|
23
23
|
|
|
24
|
-
const chat = session.getSnapshot().chat
|
|
24
|
+
const chat = session.getSnapshot().chat // rc.2 source; 0.1.2-alpha.1+ uses the uiConversation "chat" view
|
|
25
25
|
const hidden = hiddenSeqsOf(chat as HiddenChat) // Set<number> of anchor seqs
|
|
26
26
|
```
|
|
27
27
|
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
```ts
|
|
21
21
|
import { hiddenSeqsOf, type HiddenChat } from 'dsh-rewind-plugin/client'
|
|
22
22
|
|
|
23
|
-
const chat = session.getSnapshot().chat
|
|
23
|
+
const chat = session.getSnapshot().chat // rc.2 来源;0.1.2-alpha.1+ 用 uiConversation "chat" 视图
|
|
24
24
|
const hidden = hiddenSeqsOf(chat as HiddenChat) // 被隐藏的 anchor seq 集合
|
|
25
25
|
```
|
|
26
26
|
|
package/docs/release/release.md
CHANGED
|
@@ -54,18 +54,18 @@ git push origin main --tags # triggers .github/workflows/publish.yml
|
|
|
54
54
|
|
|
55
55
|
DSH is still in rc; npm's prerelease matching rules require a peer range to
|
|
56
56
|
share the host version's `[major, minor, patch]` tuple. So `peerDependencies`
|
|
57
|
-
uses an OR-union covering every published
|
|
58
|
-
(e.g. `^0.1.0-rc.6 || ^0.1.1-rc.2`), extended as DSH releases new tuples.
|
|
57
|
+
uses an OR-union covering every published tuple series
|
|
58
|
+
(e.g. `^0.1.0-rc.6 || ^0.1.1-rc.2 || ^0.1.2-alpha.2`), extended as DSH releases new tuples.
|
|
59
59
|
|
|
60
60
|
- **When to update**: only when DSH releases a new tuple
|
|
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
64
|
- **Published-tuple check (optional)**: `node scripts/check-dsh-version.mjs`
|
|
65
|
-
compares the latest
|
|
66
|
-
nothing to do, exit 1 = update). It reads
|
|
67
|
-
|
|
68
|
-
"Before bumping" step above.
|
|
65
|
+
compares the `latest` dist-tag version against the tuples the peers cover
|
|
66
|
+
(exit 0 = nothing to do, exit 1 = update). It reads the `latest` tag only; a
|
|
67
|
+
pre-release published under another tag (e.g. `alpha`) or bundled without
|
|
68
|
+
going to npm is a manual pre-release check — see the "Before bumping" step above.
|
|
69
69
|
- **Update steps**: append `|| ^<new-tuple>-rc.<n>` to every
|
|
70
70
|
`@deepseek-ai/dsh-*` peer → bump devDependencies to the latest → `npm
|
|
71
71
|
install` → `npm run check` → release.
|
|
@@ -49,15 +49,16 @@ git push origin main --tags # 触发 .github/workflows/publish.yml
|
|
|
49
49
|
|
|
50
50
|
DSH 仍在 rc 阶段,npm 的 prerelease 匹配规则要求 peer 范围与宿主版本
|
|
51
51
|
**同 `[major, minor, patch]` 元组**才能匹配。因此 peerDependencies 采用
|
|
52
|
-
OR 并集覆盖 DSH
|
|
52
|
+
OR 并集覆盖 DSH 已发布的每个元组系列(如 `^0.1.0-rc.6 || ^0.1.1-rc.2 || ^0.1.2-alpha.2`),
|
|
53
53
|
并随 DSH 发版追加。
|
|
54
54
|
|
|
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
|
-
- **已发布元组检查(可选)**:`node scripts/check-dsh-version.mjs` 用 npm
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
- **已发布元组检查(可选)**:`node scripts/check-dsh-version.mjs` 用 npm `latest`
|
|
59
|
+
dist-tag 版本对比 peer 覆盖的元组(exit 0 无需动作,exit 1 需要)。它**只读
|
|
60
|
+
`latest` tag**;发布在其它 tag(如 `alpha`)的 pre-release 走**手动发布前检查**
|
|
61
|
+
——见上文"升版前手动确认"。
|
|
61
62
|
- **更新步骤**:给每个 `@deepseek-ai/dsh-*` peer 追加 `|| ^<新元组>-rc.<n>`
|
|
62
63
|
→ devDependencies 同步升到最新 → `npm install` → `npm run check` → 发版。
|
|
63
64
|
- **正式版后收敛**:DSH 发布 final 版本后,正式版不受 prerelease 元组规则
|
package/lib/client.js
CHANGED
|
@@ -40,6 +40,18 @@ function chatSnapshotOf(face, chatView) {
|
|
|
40
40
|
if (legacy !== void 0) return legacy;
|
|
41
41
|
return chatView?.getSnapshot() ?? void 0;
|
|
42
42
|
}
|
|
43
|
+
function messageTextAt(chat, seq) {
|
|
44
|
+
if (chat === void 0) return void 0;
|
|
45
|
+
for (const key of chat.order) {
|
|
46
|
+
const node = chat.nodes.get(key);
|
|
47
|
+
if (node === void 0 || node.kind !== "user" && node.kind !== "steering") continue;
|
|
48
|
+
const data = node.data;
|
|
49
|
+
if (data.seq === seq) {
|
|
50
|
+
return data.content?.map((block) => block.type === "text" && typeof block.text === "string" ? block.text : "").join("");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return void 0;
|
|
54
|
+
}
|
|
43
55
|
function targetSeqOfArgs(args) {
|
|
44
56
|
if (args === void 0 || args === null) return void 0;
|
|
45
57
|
const match = args.match(/@(\d+)/);
|
|
@@ -677,20 +689,8 @@ function retractSpan(steering, targetId) {
|
|
|
677
689
|
|
|
678
690
|
// src/client/portals.tsx
|
|
679
691
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
680
|
-
function
|
|
681
|
-
|
|
682
|
-
for (const key of chat.order) {
|
|
683
|
-
const node = chat.nodes.get(key);
|
|
684
|
-
if (node === void 0 || node.kind !== "user") continue;
|
|
685
|
-
const user = node.data;
|
|
686
|
-
if (user.seq === seq) {
|
|
687
|
-
return user.content.map((block) => block.type === "text" && typeof block.text === "string" ? block.text : "").join("");
|
|
688
|
-
}
|
|
689
|
-
}
|
|
690
|
-
return void 0;
|
|
691
|
-
}
|
|
692
|
-
function fillComposer(text) {
|
|
693
|
-
const textarea = document.querySelector(COMPOSER_SELECTOR);
|
|
692
|
+
function fillComposerTextarea(text) {
|
|
693
|
+
const textarea = document.querySelector(COMPOSER_TEXTAREA_SELECTOR);
|
|
694
694
|
if (textarea === null) return false;
|
|
695
695
|
const setter = Object.getOwnPropertyDescriptor(HTMLTextAreaElement.prototype, "value")?.set;
|
|
696
696
|
setter?.call(textarea, text);
|
|
@@ -698,12 +698,48 @@ function fillComposer(text) {
|
|
|
698
698
|
textarea.focus();
|
|
699
699
|
return true;
|
|
700
700
|
}
|
|
701
|
+
function fillComposerEditable(text) {
|
|
702
|
+
const editable = document.querySelector(COMPOSER_EDITABLE_SELECTOR);
|
|
703
|
+
if (editable === null) return false;
|
|
704
|
+
editable.focus();
|
|
705
|
+
const selection = document.getSelection();
|
|
706
|
+
if (selection !== null) {
|
|
707
|
+
const range = document.createRange();
|
|
708
|
+
range.selectNodeContents(editable);
|
|
709
|
+
selection.removeAllRanges();
|
|
710
|
+
selection.addRange(range);
|
|
711
|
+
}
|
|
712
|
+
let ok = false;
|
|
713
|
+
try {
|
|
714
|
+
ok = document.execCommand("insertText", false, text);
|
|
715
|
+
} catch {
|
|
716
|
+
ok = false;
|
|
717
|
+
}
|
|
718
|
+
if (ok) return true;
|
|
719
|
+
editable.textContent = text;
|
|
720
|
+
editable.dispatchEvent(new Event("input", { bubbles: true }));
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
function fillComposer(text) {
|
|
724
|
+
if (fillComposerTextarea(text)) return true;
|
|
725
|
+
return fillComposerEditable(text);
|
|
726
|
+
}
|
|
727
|
+
function writeComposer(text, facade) {
|
|
728
|
+
if (facade !== void 0) {
|
|
729
|
+
try {
|
|
730
|
+
facade.setDraft(text);
|
|
731
|
+
return true;
|
|
732
|
+
} catch {
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return fillComposer(text);
|
|
736
|
+
}
|
|
701
737
|
function userNodeOf(chat, key) {
|
|
702
738
|
const node = chat?.nodes.get(key);
|
|
703
739
|
if (node === void 0 || node.kind !== "user" && node.kind !== "steering") return void 0;
|
|
704
740
|
return node.data;
|
|
705
741
|
}
|
|
706
|
-
async function runRewindAndFill(session, seq, mode, currentSessionId, chatOf) {
|
|
742
|
+
async function runRewindAndFill(session, seq, mode, currentSessionId, chatOf, setComposerText) {
|
|
707
743
|
const known = knownCommandSeqs(session, chatOf, (node) => isExecutedRewindCommand(node, seq));
|
|
708
744
|
const result = await session.command(`/rewind @${seq} ${mode}`);
|
|
709
745
|
if (!result.ok || result.value?.matched !== true) return;
|
|
@@ -714,30 +750,34 @@ async function runRewindAndFill(session, seq, mode, currentSessionId, chatOf) {
|
|
|
714
750
|
return;
|
|
715
751
|
}
|
|
716
752
|
if (currentSessionId() !== session.sessionId) return;
|
|
717
|
-
const text =
|
|
753
|
+
const text = messageTextAt(chatOf(session), seq);
|
|
718
754
|
if (text === void 0 || text === "") return;
|
|
719
|
-
|
|
755
|
+
setComposerText(session.sessionId, text);
|
|
756
|
+
}
|
|
757
|
+
function composerSurface() {
|
|
758
|
+
return document.querySelector(COMPOSER_TEXTAREA_SELECTOR) ?? document.querySelector(COMPOSER_EDITABLE_SELECTOR);
|
|
720
759
|
}
|
|
721
760
|
function showHint(text) {
|
|
722
|
-
const
|
|
761
|
+
const surface = composerSurface();
|
|
723
762
|
const hint = document.createElement("div");
|
|
724
763
|
hint.className = CLASS.guardHint;
|
|
725
764
|
hint.setAttribute("role", "status");
|
|
726
765
|
hint.textContent = text;
|
|
727
766
|
document.body.appendChild(hint);
|
|
728
|
-
if (
|
|
729
|
-
const card =
|
|
730
|
-
const rect = card instanceof HTMLElement ? card.getBoundingClientRect() :
|
|
767
|
+
if (surface !== null) {
|
|
768
|
+
const card = surface.closest("[data-composer-card]");
|
|
769
|
+
const rect = card instanceof HTMLElement ? card.getBoundingClientRect() : surface.getBoundingClientRect();
|
|
731
770
|
hint.style.left = `${Math.round(rect.left)}px`;
|
|
732
771
|
hint.style.bottom = `${Math.round(window.innerHeight - rect.top + 8)}px`;
|
|
733
772
|
}
|
|
734
773
|
window.setTimeout(() => hint.remove(), 3200);
|
|
735
774
|
}
|
|
736
|
-
var
|
|
775
|
+
var COMPOSER_TEXTAREA_SELECTOR = "[data-input-scroll] textarea, textarea[data-phase]";
|
|
776
|
+
var COMPOSER_EDITABLE_SELECTOR = "[data-composer-input]";
|
|
737
777
|
var USER_SEAT_SELECTOR = '[data-chat-flow-kind="user"][data-chat-anchor-key], [data-chat-flow-kind="steering"][data-chat-anchor-key]';
|
|
738
778
|
var CHAT_SEAT_SELECTOR = "[data-chat-anchor-key]";
|
|
739
|
-
var ACTIONS_ROOT_SELECTOR = "[data-time-hover-root]";
|
|
740
|
-
var PENDING_SEAT_SELECTOR = "[data-pending-steering][data-time-hover-root]";
|
|
779
|
+
var ACTIONS_ROOT_SELECTOR = "[data-time-hover-root], [data-actions-reveal]";
|
|
780
|
+
var PENDING_SEAT_SELECTOR = "[data-pending-steering][data-time-hover-root], [data-pending-steering][data-actions-reveal]";
|
|
741
781
|
function collectTargets(chat, hiddenSeqs) {
|
|
742
782
|
const rows = /* @__PURE__ */ new Map();
|
|
743
783
|
for (const element of document.querySelectorAll(USER_SEAT_SELECTOR)) {
|
|
@@ -801,7 +841,7 @@ function sameTargets(left, right) {
|
|
|
801
841
|
return other.kind === "pending" && target.itemId === other.itemId;
|
|
802
842
|
});
|
|
803
843
|
}
|
|
804
|
-
function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, t, subscribeLocale }) {
|
|
844
|
+
function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, t, subscribeLocale, setComposerText }) {
|
|
805
845
|
const [targets, setTargets] = (0, import_react.useState)([]);
|
|
806
846
|
const hidden = (0, import_react.useRef)(/* @__PURE__ */ new WeakSet());
|
|
807
847
|
const [, forceRender] = (0, import_react.useReducer)((count) => count + 1, 0);
|
|
@@ -869,6 +909,7 @@ function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, t, subs
|
|
|
869
909
|
sessionId,
|
|
870
910
|
sessionOf,
|
|
871
911
|
chatOf,
|
|
912
|
+
setComposerText,
|
|
872
913
|
t
|
|
873
914
|
},
|
|
874
915
|
target.key
|
|
@@ -880,6 +921,7 @@ function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, t, subs
|
|
|
880
921
|
sessionOf,
|
|
881
922
|
chatOf,
|
|
882
923
|
currentSessionId,
|
|
924
|
+
setComposerText,
|
|
883
925
|
t
|
|
884
926
|
},
|
|
885
927
|
target.key
|
|
@@ -888,7 +930,7 @@ function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, t, subs
|
|
|
888
930
|
target.key
|
|
889
931
|
));
|
|
890
932
|
}
|
|
891
|
-
function RewindButton({ target, sessionId, sessionOf, chatOf, currentSessionId, t }) {
|
|
933
|
+
function RewindButton({ target, sessionId, sessionOf, chatOf, currentSessionId, setComposerText, t }) {
|
|
892
934
|
const onClick = (event) => {
|
|
893
935
|
event.stopPropagation();
|
|
894
936
|
const session = sessionOf(sessionId);
|
|
@@ -907,7 +949,7 @@ function RewindButton({ target, sessionId, sessionOf, chatOf, currentSessionId,
|
|
|
907
949
|
anchor: event.currentTarget,
|
|
908
950
|
t,
|
|
909
951
|
onRewind: (mode) => {
|
|
910
|
-
void runRewindAndFill(session, node.seq, mode, currentSessionId, chatOf);
|
|
952
|
+
void runRewindAndFill(session, node.seq, mode, currentSessionId, chatOf, setComposerText);
|
|
911
953
|
}
|
|
912
954
|
});
|
|
913
955
|
};
|
|
@@ -924,10 +966,12 @@ function RewindButton({ target, sessionId, sessionOf, chatOf, currentSessionId,
|
|
|
924
966
|
);
|
|
925
967
|
}
|
|
926
968
|
function composerText() {
|
|
927
|
-
const
|
|
928
|
-
|
|
969
|
+
const surface = composerSurface();
|
|
970
|
+
if (surface === null) return "";
|
|
971
|
+
if (surface instanceof HTMLTextAreaElement) return surface.value;
|
|
972
|
+
return surface.textContent ?? "";
|
|
929
973
|
}
|
|
930
|
-
async function retractPending(session, itemId, text) {
|
|
974
|
+
async function retractPending(session, itemId, text, setComposerText) {
|
|
931
975
|
await session.cancel();
|
|
932
976
|
const queue = session.getSnapshot().queue;
|
|
933
977
|
const steering = queue.filter((item) => item.placement === "steering");
|
|
@@ -935,10 +979,10 @@ async function retractPending(session, itemId, text) {
|
|
|
935
979
|
await session.updateQueue(id, { kind: "remove" });
|
|
936
980
|
}
|
|
937
981
|
if (text !== null && text !== "" && composerText().trim() === "") {
|
|
938
|
-
|
|
982
|
+
setComposerText(session.sessionId, text);
|
|
939
983
|
}
|
|
940
984
|
}
|
|
941
|
-
function RetractButton({ target, sessionId, sessionOf, chatOf, t }) {
|
|
985
|
+
function RetractButton({ target, sessionId, sessionOf, chatOf, setComposerText, t }) {
|
|
942
986
|
const onClick = (event) => {
|
|
943
987
|
event.stopPropagation();
|
|
944
988
|
const session = sessionOf(sessionId);
|
|
@@ -951,7 +995,7 @@ function RetractButton({ target, sessionId, sessionOf, chatOf, t }) {
|
|
|
951
995
|
t,
|
|
952
996
|
retract: { itemId: target.itemId, text: target.text },
|
|
953
997
|
onRetract: () => {
|
|
954
|
-
void retractPending(session, target.itemId, target.text);
|
|
998
|
+
void retractPending(session, target.itemId, target.text, setComposerText);
|
|
955
999
|
}
|
|
956
1000
|
});
|
|
957
1001
|
};
|
|
@@ -1032,7 +1076,8 @@ var name = "dsh-rewind";
|
|
|
1032
1076
|
var inject = ["slots", "sessions", "locale", "commandUi"];
|
|
1033
1077
|
var NS = "rewind";
|
|
1034
1078
|
var HEADER_ACTIONS_SLOT = "conversation.session.header.actions";
|
|
1035
|
-
var
|
|
1079
|
+
var COMPOSER_TEXTAREA_SELECTOR2 = "[data-input-scroll] textarea, textarea[data-phase]";
|
|
1080
|
+
var COMPOSER_EDITABLE_SELECTOR2 = "[data-composer-input]";
|
|
1036
1081
|
function apply(ctx) {
|
|
1037
1082
|
ctx.effect(function* () {
|
|
1038
1083
|
yield ctx.locale.register(NS, { zh, en });
|
|
@@ -1055,6 +1100,17 @@ function apply(ctx) {
|
|
|
1055
1100
|
return void 0;
|
|
1056
1101
|
}
|
|
1057
1102
|
};
|
|
1103
|
+
const setComposerText = (sessionId, text) => {
|
|
1104
|
+
const conversation = ctx.get("conversation");
|
|
1105
|
+
const input = conversation?.input;
|
|
1106
|
+
const scope = ctx.sessions.scope?.(sessionId);
|
|
1107
|
+
return writeComposer(
|
|
1108
|
+
text,
|
|
1109
|
+
input !== void 0 && scope !== void 0 ? { setDraft: (draft) => {
|
|
1110
|
+
input.for(scope).setDraft(draft);
|
|
1111
|
+
} } : void 0
|
|
1112
|
+
);
|
|
1113
|
+
};
|
|
1058
1114
|
const slots = ctx.slots;
|
|
1059
1115
|
yield slots.inject(HEADER_ACTIONS_SLOT, () => slots.register(
|
|
1060
1116
|
{
|
|
@@ -1064,7 +1120,7 @@ function apply(ctx) {
|
|
|
1064
1120
|
id: "dsh-rewind-portals",
|
|
1065
1121
|
order: 1e3
|
|
1066
1122
|
},
|
|
1067
|
-
createRewindBridge({ sessionOf, chatOf, currentSessionId, t, subscribeLocale })
|
|
1123
|
+
createRewindBridge({ sessionOf, chatOf, currentSessionId, setComposerText, t, subscribeLocale })
|
|
1068
1124
|
));
|
|
1069
1125
|
const commandUi = ctx.get("commandUi");
|
|
1070
1126
|
const hasCandidates = (sessionId) => {
|
|
@@ -1082,9 +1138,9 @@ function apply(ctx) {
|
|
|
1082
1138
|
};
|
|
1083
1139
|
const hostCandidatesCache = /* @__PURE__ */ new Map();
|
|
1084
1140
|
const composerAnchor = () => {
|
|
1085
|
-
const
|
|
1086
|
-
const card =
|
|
1087
|
-
return card ??
|
|
1141
|
+
const surface = composerSurface2();
|
|
1142
|
+
const card = surface?.closest("[data-composer-card]");
|
|
1143
|
+
return card ?? surface ?? document.body;
|
|
1088
1144
|
};
|
|
1089
1145
|
const rewindPopupSpec = {
|
|
1090
1146
|
// The picker exists exactly while the surface has a reachable user
|
|
@@ -1117,7 +1173,7 @@ function apply(ctx) {
|
|
|
1117
1173
|
anchor: composerAnchor(),
|
|
1118
1174
|
t,
|
|
1119
1175
|
onRewind: (mode) => {
|
|
1120
|
-
void runRewindAndFill(face, candidate.seq, mode, currentSessionId, chatOf);
|
|
1176
|
+
void runRewindAndFill(face, candidate.seq, mode, currentSessionId, chatOf, setComposerText);
|
|
1121
1177
|
}
|
|
1122
1178
|
});
|
|
1123
1179
|
}
|
|
@@ -1126,7 +1182,7 @@ function apply(ctx) {
|
|
|
1126
1182
|
for (const name2 of ["rewind", "undo"]) {
|
|
1127
1183
|
yield commandUi.decorate({ name: name2, ...rewindPopupSpec });
|
|
1128
1184
|
}
|
|
1129
|
-
const
|
|
1185
|
+
const composerSurface2 = () => document.querySelector(COMPOSER_TEXTAREA_SELECTOR2) ?? document.querySelector(COMPOSER_EDITABLE_SELECTOR2);
|
|
1130
1186
|
yield () => {
|
|
1131
1187
|
style.remove();
|
|
1132
1188
|
};
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
2
|
import { createAssistantMessage } from "@deepseek-ai/dsh-llm";
|
|
3
3
|
import { unlink } from "node:fs/promises";
|
|
4
|
-
import
|
|
4
|
+
import * as dshSettings from "@deepseek-ai/dsh-settings";
|
|
5
5
|
|
|
6
6
|
// src/locales.ts
|
|
7
7
|
var en = {
|
|
@@ -114,6 +114,12 @@ function translate(lang, key, params = {}) {
|
|
|
114
114
|
return text;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
// src/settings-locale.ts
|
|
118
|
+
function readSettingsSection(provider, ns, brand) {
|
|
119
|
+
const key = brand?.(ns) ?? ns;
|
|
120
|
+
return provider.get(key);
|
|
121
|
+
}
|
|
122
|
+
|
|
117
123
|
// src/rewind.ts
|
|
118
124
|
var RewindError = class extends Error {
|
|
119
125
|
constructor(code, message) {
|
|
@@ -1887,7 +1893,11 @@ function apply(ctx, config) {
|
|
|
1887
1893
|
const trackedBySession = /* @__PURE__ */ new Map();
|
|
1888
1894
|
let fsService;
|
|
1889
1895
|
ctx.inject(["settings"], (settingsCtx) => {
|
|
1890
|
-
const section =
|
|
1896
|
+
const section = readSettingsSection(
|
|
1897
|
+
settingsCtx.settings,
|
|
1898
|
+
"locale",
|
|
1899
|
+
dshSettings.settingsNamespace
|
|
1900
|
+
);
|
|
1891
1901
|
if (section?.preference === "zh" || section?.preference === "en") {
|
|
1892
1902
|
activeLocale = section.preference;
|
|
1893
1903
|
}
|
|
@@ -41,6 +41,16 @@ export declare function chatSnapshotOf(face: {
|
|
|
41
41
|
} | undefined, chatView: {
|
|
42
42
|
getSnapshot(): unknown;
|
|
43
43
|
} | undefined): HiddenChat | undefined;
|
|
44
|
+
/**
|
|
45
|
+
* The plain text of the human message at `seq` in the chat snapshot, for
|
|
46
|
+
* filling the composer after a withdraw. Accepts BOTH `user` and `steering`
|
|
47
|
+
* nodes: a plan-mode (`/plan <text>`) input is delivered through the agent
|
|
48
|
+
* inbox next-step and claimed, so it renders as `steering`, and its text must
|
|
49
|
+
* still return to the composer (`portals.tsx` `runRewindAndFill`) — the old
|
|
50
|
+
* `user`-only read silently left it empty. State absent → undefined; a message
|
|
51
|
+
* with no text blocks → ''. Same text-blocks join the candidate side uses.
|
|
52
|
+
*/
|
|
53
|
+
export declare function messageTextAt(chat: HiddenChat | undefined, seq: number): string | undefined;
|
|
44
54
|
/**
|
|
45
55
|
* Extract the rewind target seq from a `/rewind` command's structured `args`
|
|
46
56
|
* (e.g. `@5 chat`, `preview @5 both`). Locale-independent — never parses the
|
|
@@ -61,6 +61,13 @@ export interface RewindBridgeDeps {
|
|
|
61
61
|
readonly currentSessionId: () => string | undefined;
|
|
62
62
|
readonly t: Translate;
|
|
63
63
|
readonly subscribeLocale: (cb: () => void) => () => void;
|
|
64
|
+
/**
|
|
65
|
+
* Session-aware dual-channel composer writer (see `writeComposer`): the
|
|
66
|
+
* alpha.1+ `conversation.input` facade `setDraft` when reachable, else the
|
|
67
|
+
* rc.2/alpha.1 DOM fill. Session-scoped so the refill only lands in the
|
|
68
|
+
* session that just rewound.
|
|
69
|
+
*/
|
|
70
|
+
readonly setComposerText: (sessionId: string, text: string) => boolean;
|
|
64
71
|
}
|
|
65
72
|
/** Structural face of the runtime slot service (see the module doc). */
|
|
66
73
|
export interface SlotsLike {
|
|
@@ -73,14 +80,36 @@ export interface SlotsLike {
|
|
|
73
80
|
readonly sessionId: string;
|
|
74
81
|
}) => ReactNode): () => void;
|
|
75
82
|
}
|
|
83
|
+
/** Join the text blocks of a user message into one plain preview. */
|
|
76
84
|
/**
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
|
|
85
|
+
* The alpha.1+ session input facade's write face (structural, so the plugin
|
|
86
|
+
* never imports the conversation UI package). `setDraft` replaces the whole
|
|
87
|
+
* composer draft through the harness's own Lexical editor — the correct way
|
|
88
|
+
* to restore the withdrawn text.
|
|
89
|
+
*/
|
|
90
|
+
interface ComposerDraftWriter {
|
|
91
|
+
setDraft(text: string): void;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Fill the dsh composer with `text`. DOM dual-channel fallback: the rc.2
|
|
95
|
+
* `<textarea>` path, then the alpha.1+ `contenteditable` path. Used by
|
|
96
|
+
* `setComposerText` (the harness-facade-aware writer) as the last-resort and
|
|
97
|
+
* by `runRewindAndFill` to put the withdrawn target message back into the
|
|
98
|
+
* composer after a rewind. Best-effort — no composer match means false,
|
|
99
|
+
* never a throw.
|
|
82
100
|
*/
|
|
83
101
|
export declare function fillComposer(text: string): boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Dual-channel composer write, mirroring `chatSnapshotOf`: prefer the harness
|
|
104
|
+
* facade's `setDraft` (alpha.1+, correct whole-draft replace), then degrade
|
|
105
|
+
* to the DOM `fillComposer` (rc.2 textarea / alpha.1 contenteditable). A
|
|
106
|
+
* facade that throws (session teardown) is treated as absent so the DOM path
|
|
107
|
+
* still restores the text. Never throws.
|
|
108
|
+
* @param text - the withdrawn target message text.
|
|
109
|
+
* @param facade - the alpha.1+ session input draft writer, when reachable.
|
|
110
|
+
* @returns whether a channel applied the text.
|
|
111
|
+
*/
|
|
112
|
+
export declare function writeComposer(text: string, facade: ComposerDraftWriter | undefined): boolean;
|
|
84
113
|
/**
|
|
85
114
|
* Execute one rewind from the popover and, when it settles successfully,
|
|
86
115
|
* put the withdrawn target message's text back into the composer so the
|
|
@@ -94,7 +123,7 @@ export declare function fillComposer(text: string): boolean;
|
|
|
94
123
|
* the old baseline heuristic refilled withdrawn text into the composer
|
|
95
124
|
* after switching sessions or restarting dsh.
|
|
96
125
|
*/
|
|
97
|
-
export declare function runRewindAndFill(session: SessionFace, seq: number, mode: 'chat' | 'both', currentSessionId: () => string | undefined, chatOf: ChatOf): Promise<void>;
|
|
126
|
+
export declare function runRewindAndFill(session: SessionFace, seq: number, mode: 'chat' | 'both', currentSessionId: () => string | undefined, chatOf: ChatOf, setComposerText: (sessionId: string, text: string) => boolean): Promise<void>;
|
|
98
127
|
/**
|
|
99
128
|
* Collect the portal targets of one session: user rows × snapshot nodes.
|
|
100
129
|
* Exported as a test seam — the DOM→targets pairing that drives the ↶ button
|
|
@@ -111,7 +140,11 @@ interface RewindPortalsProps extends RewindBridgeDeps {
|
|
|
111
140
|
* skipped when the target set is unchanged), so the plugin never runs a
|
|
112
141
|
* synchronous full-transcript scan inside a commit microtask.
|
|
113
142
|
*/
|
|
114
|
-
export declare function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, t, subscribeLocale }: RewindPortalsProps): ReactNode;
|
|
143
|
+
export declare function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, t, subscribeLocale, setComposerText }: RewindPortalsProps): ReactNode;
|
|
144
|
+
/** The current composer draft, on whichever channel: rc.2 textarea `.value`,
|
|
145
|
+
* alpha.1+ contenteditable `textContent`. Empty when the composer is absent.
|
|
146
|
+
* Exported as a test seam (the empty-composer guard in `retractPending`). */
|
|
147
|
+
export declare function composerText(): string;
|
|
115
148
|
/**
|
|
116
149
|
* Build the slot-entry component for the plugin apply(): a tiny bridge that
|
|
117
150
|
* injects the apply-time capabilities (session resolution, locale, rewind
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Version-neutral settings-namespace reading for the host half.
|
|
3
|
+
*
|
|
4
|
+
* DSH rc.2 exposes a `settingsNamespace(value)` brand helper, and `settings.get`
|
|
5
|
+
* is typed to require a branded `SettingsNamespace`. DSH 0.1.2-alpha.2 removed
|
|
6
|
+
* that helper (the `SettingsNamespace` *type* remains) and `settings.get`
|
|
7
|
+
* accepts the raw namespace string. This module collapses both into one runtime
|
|
8
|
+
* call: on rc.2 the brand is a compile-time marker erased at runtime (so
|
|
9
|
+
* `settingsNamespace(ns)` returns `ns`), on 0.1.2-alpha.2 the brand helper is
|
|
10
|
+
* absent and the raw `ns` string is used directly. That is what lets a single
|
|
11
|
+
* compiled host bundle link and run on both harness generations.
|
|
12
|
+
*
|
|
13
|
+
* @module dsh-rewind/settings-locale
|
|
14
|
+
*/
|
|
15
|
+
/** Minimal structural face of the settings provider the host reads from. */
|
|
16
|
+
export interface SettingsProviderLike {
|
|
17
|
+
/**
|
|
18
|
+
* Read one registered settings section by namespace. Accepts whatever the
|
|
19
|
+
* running DSH generation passes: a branded `SettingsNamespace` (rc.2, brand
|
|
20
|
+
* erased) or the raw namespace string (0.1.2-alpha.2).
|
|
21
|
+
*/
|
|
22
|
+
get(ns: string): unknown;
|
|
23
|
+
}
|
|
24
|
+
/** The `settingsNamespace(ns)` brand helper, or `undefined` when removed (0.1.2-alpha.2). */
|
|
25
|
+
export type SettingsNamespaceBrand = ((value: string) => string) | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* Read one settings section keyed by `ns`, tolerant of the settings-namespace
|
|
28
|
+
* brand across DSH generations. Pass the brand helper when available (the rc.2
|
|
29
|
+
* path); it is `undefined` on 0.1.2-alpha.2, where the raw `ns` string is used
|
|
30
|
+
* directly. Never throws: an absent section simply returns `undefined`.
|
|
31
|
+
*/
|
|
32
|
+
export declare function readSettingsSection(provider: SettingsProviderLike, ns: string, brand: SettingsNamespaceBrand): unknown;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-rewind-plugin",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
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",
|
|
@@ -72,18 +72,18 @@
|
|
|
72
72
|
},
|
|
73
73
|
"peerDependencies": {
|
|
74
74
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
75
|
-
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6 || ^0.1.1-rc.2",
|
|
75
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6 || ^0.1.1-rc.2 || ^0.1.2-alpha.2",
|
|
76
76
|
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6 || ^0.1.1-rc.2",
|
|
77
|
-
"@deepseek-ai/dsh-client-ui-commands": "^0.1.0-rc.6 || ^0.1.1-rc.2",
|
|
78
|
-
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6 || ^0.1.1-rc.2",
|
|
79
|
-
"@deepseek-ai/dsh-commands": "^0.1.0-rc.6 || ^0.1.1-rc.2",
|
|
80
|
-
"@deepseek-ai/dsh-fs": "^0.1.0-rc.6 || ^0.1.1-rc.2",
|
|
81
|
-
"@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6 || ^0.1.1-rc.2",
|
|
82
|
-
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.1.1-rc.2",
|
|
83
|
-
"@deepseek-ai/dsh-sandbox": "^0.1.0-rc.6 || ^0.1.1-rc.2",
|
|
84
|
-
"@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-rc.2",
|
|
85
|
-
"@deepseek-ai/dsh-settings": "^0.1.0-rc.8 || ^0.1.1-rc.2",
|
|
86
|
-
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.1.1-rc.2"
|
|
77
|
+
"@deepseek-ai/dsh-client-ui-commands": "^0.1.0-rc.6 || ^0.1.1-rc.2 || ^0.1.2-alpha.2",
|
|
78
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6 || ^0.1.1-rc.2 || ^0.1.2-alpha.2",
|
|
79
|
+
"@deepseek-ai/dsh-commands": "^0.1.0-rc.6 || ^0.1.1-rc.2 || ^0.1.2-alpha.2",
|
|
80
|
+
"@deepseek-ai/dsh-fs": "^0.1.0-rc.6 || ^0.1.1-rc.2 || ^0.1.2-alpha.2",
|
|
81
|
+
"@deepseek-ai/dsh-home-paths": "^0.1.0-rc.6 || ^0.1.1-rc.2 || ^0.1.2-alpha.2",
|
|
82
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6 || ^0.1.1-rc.2 || ^0.1.2-alpha.2",
|
|
83
|
+
"@deepseek-ai/dsh-sandbox": "^0.1.0-rc.6 || ^0.1.1-rc.2 || ^0.1.2-alpha.2",
|
|
84
|
+
"@deepseek-ai/dsh-session": "^0.1.0-rc.6 || ^0.1.1-rc.2 || ^0.1.2-alpha.2",
|
|
85
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.8 || ^0.1.1-rc.2 || ^0.1.2-alpha.2",
|
|
86
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6 || ^0.1.1-rc.2 || ^0.1.2-alpha.2"
|
|
87
87
|
},
|
|
88
88
|
"peerDependenciesMeta": {
|
|
89
89
|
"@deepseek-ai/cordis": {
|
|
@@ -142,6 +142,7 @@
|
|
|
142
142
|
"@deepseek-ai/dsh-goal": "^0.1.1-rc.2",
|
|
143
143
|
"@deepseek-ai/dsh-home-paths": "^0.1.1-rc.2",
|
|
144
144
|
"@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
|
|
145
|
+
"@deepseek-ai/dsh-plan-mode": "^0.1.1-rc.2",
|
|
145
146
|
"@deepseek-ai/dsh-sandbox": "^0.1.1-rc.2",
|
|
146
147
|
"@deepseek-ai/dsh-session": "^0.1.1-rc.2",
|
|
147
148
|
"@deepseek-ai/dsh-session-projection": "^0.1.1-rc.2",
|