dsh-rewind-plugin 0.6.2 → 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 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; file restores stay inside the plugin's own backup directory. Full security model: [SECURITY.md](SECURITY.md).
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
@@ -156,7 +156,7 @@ withdrew should consume the stable, locale-independent helpers exported from
156
156
 
157
157
  ## Security
158
158
 
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 and restores stay under `<dsh home>/rewind-snapshots/`. 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
+ 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).
160
160
 
161
161
  ## Development
162
162
 
package/README.md CHANGED
@@ -12,7 +12,7 @@ DeepSeek Harness 插件:**一键就地回退对话到任意更早的用户消
12
12
 
13
13
  - **回退 = 时间回溯**——目标消息及其之后的全部内容(agent 回复、工具调用)同时从**模型上下文**和**渲染对话**中撤回,不新建会话、不切换窗口;目标消息文本会回填输入框,改完可重发。**在原理上就真正无感、便捷**。
14
14
  - **轻量工作区备份**——对齐 Claude Code:追踪写类工具编辑过的文件,**已跟踪文件的外部变更也能还原**。局部追踪、写前备份、不变不存。一个轻型插件,即拥有**完备的智能体回退能力**。
15
- - **信息安全优先**——插件从不删改会话日志(append-only),从不真正删除你的任何对话;文件还原限定在插件自己的备份目录。完整安全模型:[SECURITY.md](SECURITY.md)。
15
+ - **信息安全优先**——插件从不删改会话日志(append-only),从不真正删除你的任何对话;备份存于自己的快照目录;还原只用这些备份。完整安全模型:[SECURITY.md](SECURITY.md)。
16
16
  - **完备测试系统**——单元、探针、端到端主机验证,覆盖兼容性探测、日志重放、续接、跨重启等场景;随 harness 升级持续维护,确保功能稳定。
17
17
 
18
18
  ## 效果预览
@@ -144,7 +144,7 @@ dsh plugin --profile web add dsh-rewind-plugin
144
144
 
145
145
  ## 安全
146
146
 
147
- 本插件只向会话日志追加回退标记事件,从不删除或改写已记录的历史。工作区文件仅在「回退对话和代码」时被改写,备份与还原都限定在 `~/.dsh/rewind-snapshots/` 内。不触碰你的 git 仓库,无网络请求,不访问任何凭据。对**长期不活跃**的会话,另有默认关闭的全局自动清理可整目录移除其快照,不影响活动会话与对话日志。完整安全模型:[SECURITY.md](SECURITY.md)。
147
+ 本插件只向会话日志追加回退标记事件,从不删除或改写已记录的历史。工作区文件仅在「回退对话和代码」时被改写,备份存储于 `~/.dsh/rewind-snapshots/`;还原以备份为唯一来源。不触碰你的 git 仓库,无网络请求,不访问任何凭据。对**长期不活跃**的会话,另有默认关闭的全局自动清理可整目录移除其快照,不影响活动会话与对话日志。完整安全模型:[SECURITY.md](SECURITY.md)。
148
148
 
149
149
  ## 开发
150
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. The store never
87
- overlaps the workspace; deleting it only removes file backups and the store
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: recording and restore mirror the
142
- paths the model's file tools touched, so a sensitive file (e.g. `.env`) the
143
- model may read or edit will be backed up and restorable. Keeping one out is
144
- a **DSH model-permission** concern (a per-path deny), not a rewind feature.
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`
@@ -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 rc tuples** (`^0.1.0-rc.6 || ^0.1.1-rc.2`):
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`.
@@ -11,32 +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 rc tuple series. npm's
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`, a **bundled pre-release — NOT published
22
- > to npm**): the plugin also reads the session chat when a DSH Desktop build ships
23
- > that version (`latest` on the registry is still `0.1.1-rc.2`; the alpha is only
24
- > in the bundled `app.asar.unpacked/node_modules/@deepseek-ai/*`). There
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.1 path); both missing degrades to `undefined` (no targets, never a
30
- > crash). Peers are intentionally **NOT** extended for `0.1.2-alpha.1` because it is
31
- > not a registry version (see above); the OR-union stays
32
- > `^0.1.0-rc.6 || ^0.1.1-rc.2` and will gain the `0.1.2` tuple when the harness
33
- > publishes it. Pinned by `tests/chat-channel.test.ts` (channel precedence + the
34
- > alpha.1 snapshot shape) and `tests/client-dom.test.ts` (the button-target
35
- > pairing that consumes the chat). The composer refill is dual-channel the same
36
- > way: on alpha.1 the withdrawn text is written through the `conversation`
37
- > service's `input` resolver's `setDraft` (the harness's own Lexical editor),
38
- > else the rc.2 `<textarea>` / alpha.1 `contenteditable` DOM write (`writeComposer`
39
- > in `src/client/portals.tsx`); pinned by `tests/client-composer.test.ts`.
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`.
40
45
 
41
46
  ## Definition of "fully compatible" (invariants)
42
47
 
@@ -60,7 +65,7 @@
60
65
  - **session-title / goal fold**: a marker does not disturb `foldSessionTitle` / `foldGoal`.
61
66
  - **client ordering**: turn-tail ordering + `step/start` uniqueness hold for tool turns + marker logs.
62
67
  - **rewind across a compact checkpoint**: `RewindError('not-on-surface')` refuses cleanly, no crash.
63
- - **plan-mode**: `hasOpenTurn` pairs only `turn/start`/`turn/end`; a marker produces no `turn/start`, so a rewind during active plan is unaffected (static review confirmed).
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`).
64
69
  - **agent-loop cancellation**: `finally` guarantees step/turn closure; the rewind force-stop path leaves no dangling frame.
65
70
 
66
71
  ## Known behavior boundaries (deterministic differences, non-crash, documented)
@@ -71,6 +76,7 @@
71
76
  - **Session title auto-regeneration**: the title derives from the surface, so an automatically-derived title may change after a rewind.
72
77
  - **Files written but uncommitted in a cancelled turn**: a `both` rewind cannot restore them (tool side-effect timing; same as Claude Code).
73
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`.
74
80
 
75
81
  ## Upstream (harness) issues and the plugin's no-compensation stance
76
82
 
@@ -161,7 +167,7 @@ new turn at `phase.turn + 1` (`agent.ts:251-255`) **without closing the leftover
161
167
  | resume / session-query replay | ✓ | — | ✓ | — | — | — | ✓ | — |
162
168
  | tool pipeline (snapshot/restore) | — | — | — | — | — | ✓ | — | ✓ |
163
169
  | client ordering | — | — | ✓ | — | — | — | ✓ | — |
164
- | plan-mode | | — | | — | — | — | — | ✓ (static) |
170
+ | plan-mode | | — | | — | — | — | — | ✓ (static) |
165
171
 
166
172
  ✓ = probe passes; — = not applicable. RU-I18N and R-OPENSTEP are upstream issues the plugin
167
173
  does not compensate for (see above); G3 is a confirmed-non-defect behavior pinned in
@@ -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 rc tuple series
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 npm version against the tuples the peers cover (exit 0 =
66
- nothing to do, exit 1 = update). It reads npm published versions only; a
67
- newer-but-unpublished pre-release is a manual pre-release check see the
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 已发布的每个 rc 元组系列(如 `^0.1.0-rc.6 || ^0.1.1-rc.2`),
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
- 对比 peer 覆盖的元组(exit 0 无需动作,exit 1 需要)。它**只读 npm 已发布版本**;
60
- 更新的未发布 pre-release 走**手动发布前检查**——见上文"升版前手动确认"。
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,18 +689,6 @@ function retractSpan(steering, targetId) {
677
689
 
678
690
  // src/client/portals.tsx
679
691
  var import_jsx_runtime = require("react/jsx-runtime");
680
- function userTextAt(chat, seq) {
681
- if (chat === void 0) return void 0;
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
692
  function fillComposerTextarea(text) {
693
693
  const textarea = document.querySelector(COMPOSER_TEXTAREA_SELECTOR);
694
694
  if (textarea === null) return false;
@@ -750,7 +750,7 @@ async function runRewindAndFill(session, seq, mode, currentSessionId, chatOf, se
750
750
  return;
751
751
  }
752
752
  if (currentSessionId() !== session.sessionId) return;
753
- const text = userTextAt(chatOf(session), seq);
753
+ const text = messageTextAt(chatOf(session), seq);
754
754
  if (text === void 0 || text === "") return;
755
755
  setComposerText(session.sessionId, text);
756
756
  }
@@ -776,8 +776,8 @@ var COMPOSER_TEXTAREA_SELECTOR = "[data-input-scroll] textarea, textarea[data-ph
776
776
  var COMPOSER_EDITABLE_SELECTOR = "[data-composer-input]";
777
777
  var USER_SEAT_SELECTOR = '[data-chat-flow-kind="user"][data-chat-anchor-key], [data-chat-flow-kind="steering"][data-chat-anchor-key]';
778
778
  var CHAT_SEAT_SELECTOR = "[data-chat-anchor-key]";
779
- var ACTIONS_ROOT_SELECTOR = "[data-time-hover-root]";
780
- 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]";
781
781
  function collectTargets(chat, hiddenSeqs) {
782
782
  const rows = /* @__PURE__ */ new Map();
783
783
  for (const element of document.querySelectorAll(USER_SEAT_SELECTOR)) {
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 { settingsNamespace } from "@deepseek-ai/dsh-settings";
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 = settingsCtx.settings.get(settingsNamespace("locale"));
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
@@ -80,6 +80,7 @@ export interface SlotsLike {
80
80
  readonly sessionId: string;
81
81
  }) => ReactNode): () => void;
82
82
  }
83
+ /** Join the text blocks of a user message into one plain preview. */
83
84
  /**
84
85
  * The alpha.1+ session input facade's write face (structural, so the plugin
85
86
  * never imports the conversation UI package). `setDraft` replaces the whole
@@ -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.2",
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",