dsh-rewind-plugin 0.10.0-alpha.2 → 0.11.0-beta.1

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
3
  > [!WARNING]
4
- > **Planning to use DSH `0.1.3`? Install a `v0.9.x` release as soon as possible and run `/dsh-rewind-fix` to update old rewind markers** ([update guide](docs/rewind-fix.md)).
4
+ > **Install a `v0.9.x` release on DSH `0.1.2-rc.1` as early as possible, and run `/dsh-rewind-fix` to update old sessions' rewind markers** ([update guide](docs/rewind-fix.md)).
5
5
 
6
6
  Conversation rewind for DeepSeek Harness: **rewind the conversation to any earlier user message in one click, in the same window** — no new branch, no window switch, with optional workspace-file restore (full Claude Code `/rewind` semantics).
7
7
 
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # dsh-rewind
2
2
 
3
3
  > [!WARNING]
4
- > **计划使用 DSH `0.1.3` 的用户:请尽早安装 `v0.9.x` 版本,并运行 `/dsh-rewind-fix` 更新旧回退标记**([更新指南](docs/rewind-fix.zh.md))。
4
+ > **请尽早在 DSH `0.1.2-rc.1` 安装 `v0.9.x` 版本,运行 `/dsh-rewind-fix` 更新旧会话的回退标记**([更新指南](docs/rewind-fix.zh.md))。
5
5
 
6
6
  DeepSeek Harness 插件:**一键就地回退对话到任意更早的用户消息**——同窗口内完成,不新建分支、不换窗口,可一并还原工作区文件(完整 Claude Code `/rewind` 语义)。
7
7
 
package/SECURITY.md CHANGED
@@ -59,13 +59,16 @@ than hidden.
59
59
  A failed gate fails closed: an invalid target, a missing store, an absent
60
60
  backup, or a cancelled invocation aborts the rewind with an error.
61
61
 
62
- **Automatic store deletion (opt-in)**: unrelated to restores, an enabled
63
- `snapshot-auto-cleanup` background sweep deletes whole session directories of
64
- **long-inactive** sessions (default off the store is otherwise only mutated by
65
- an explicit rewind apply). It is confined to the store root, uses `lstat` (so it
66
- never follows a symlink out of the root), skips the active session, and never
67
- touches the conversation log. When disabled (the default), no automatic deletion
68
- runs.
62
+ **Automatic store deletion**: two things delete session snapshots. The opt-in
63
+ `snapshot-auto-cleanup` sweep (default off) removes the whole directories of
64
+ **long-inactive** sessions, unrelated to restores. Separately, a session's
65
+ snapshots are cleared automatically when the conversation log's session format
66
+ has changed (a DSH upgrade), because those backups are anchored to message
67
+ positions that no longer line up. Both stay confined to the store root, use
68
+ `lstat` (so they never follow a symlink out of the root), the sweep never targets
69
+ the active session, and neither touches the conversation log. When auto-cleanup
70
+ is disabled (the default), only the format-change clear runs, and only for an
71
+ affected session.
69
72
 
70
73
  ## Conversation integrity
71
74
 
@@ -56,7 +56,7 @@ machine channels (see [Compatibility strategy](#compatibility-strategy)).
56
56
  → agent.cancel({ keepInbox: true }) if running; waitForAgentIdle
57
57
  → dropPendingSteering (next-step inbox only; queued messages untouched)
58
58
  → append the rewind marker = a user/message with surfaceOp
59
- { op: 'replace', start, end } over every surface node after the
59
+ { op: 'replace', startSeq, endSeq } over every surface node after the
60
60
  target (+ sourceEventSeqs = shadowed seqs) [a single event]
61
61
  → if mode 'both': store.restoreAfter(targetSeq) + syncRestoreObservations
62
62
  → result text carries machine tokens (impact=<n>, restore:/delete: lines)
@@ -7,27 +7,28 @@
7
7
  > compatibility invariants. A probe failure is a finding; it enters the
8
8
  > fix/pin/record loop.
9
9
  >
10
- > Targeted version: npm `@deepseek-ai/*@0.1.2-rc.1` (matches `package-lock.json`).
10
+ > Targeted version: npm `@deepseek-ai/*@0.1.5-rc.1` (the range the peers and `dsh.engines.dsh` declare).
11
11
  > Source reference: the upstream [github.com/deepseek-ai/deepseek-harness](https://github.com/deepseek-ai/deepseek-harness).
12
12
  >
13
13
  > Version alignment: `peerDependencies` use one tuple per DSH line
14
14
  > (e.g. `^0.1.2-rc.1`). npm's prerelease matching rules require a candidate to
15
15
  > share the range comparator's `[major, minor, patch]` tuple, so a new DSH tuple
16
- > replaces the peer tuple (single-line model); any prerelease within the SAME
17
- > tuple is a no-op. Signal: `npm view @deepseek-ai/dsh version`;
16
+ > replaces the peer tuple (single-line model). The range is conservative — it
17
+ > declares only what was verified, so a release already inside it changes
18
+ > nothing. Signal: `npm view @deepseek-ai/dsh dist-tags`;
18
19
  > flow: `scripts/check-dsh-version.mjs` (it reads the `latest` dist-tag only; a
19
20
  > prerelease published under another tag is a manual pre-release check).
20
21
  >
21
- > The plugin targets a single DSH version line (`0.1.2-rc.1`); compatibility
22
- > with earlier lines is not kept.
22
+ > The plugin targets a single DSH version line; compatibility with earlier
23
+ > lines is not kept.
23
24
 
24
- ### Single channel (`0.1.2-rc.1`)
25
+ ### Single channel
25
26
 
26
- The plugin targets one DSH channel. Each seam below reads the 0.1.2-rc.1 shape
27
- only (no `Session.events` / `[data-time-hover-root]` / `<textarea>` / face-`chat`
28
- legacy branch).
27
+ The plugin targets one DSH channel. Each seam below reads the targeted
28
+ version's shape only (no `Session.events` / `[data-time-hover-root]` /
29
+ `<textarea>` / face-`chat` legacy branch).
29
30
 
30
- | Seam | 0.1.2-rc.1 implementation |
31
+ | Seam | Implementation at the targeted version |
31
32
  | --- | --- |
32
33
  | Host session log | `session.snapshotEvents()` |
33
34
  | Client chat snapshot | `uiConversation` `chat` view (`chatSnapshotOf`) |
@@ -66,6 +67,7 @@ legacy branch).
66
67
  - **session-stats / session-telemetry fold the full log**: post-rewind stats do **not** rewind — `turns`/`steps`/`llmMs` still include withdrawn content; the `user/message` marker is folded as a present user turn (it adds no step). This is the intended "fold the full log" semantics, pinned by probe.
67
68
  - **token-meter usage anchor stays stable** (G3): the `user/message` marker carries no usage, but because it is not an `assistant/message`, the baseline anchor does not drop to a heuristic estimate — it stays `usage` across a rewind. Pinned by `compat-gaps` G3.
68
69
  - **marker content is the constant `(empty message)` placeholder**: never empty, because the session log is immutable but the model serving a session may change later — a strict OpenAI-compatible gateway rejects an empty user message (HTTP 400, Issue #21). The marker is a small visible user turn in derived history. Pin: `verify-host` (`marker is a user/message with the dsh-rewind plugin source and the (empty message) placeholder`).
70
+ - **marker shape (v3)**: the replace `surfaceOp` is `{ op: 'replace', startSeq, endSeq }` (renamed from `start`/`end` in session-format v3); the dsh-rewind source is the plain CLOSED third-party plugin shape `{ kind: 'plugin', plugin: 'dsh-rewind' }` (no private field — the released-format source validator rejects any member outside `kind`/`plugin` + the context-injection `form`/`sections`/`summary`, and a plugin extends the source map by adding a `kind`, not fields); the `user/message` + `sourceEventSeqs` shape and the `(empty message)` content are unchanged.
69
71
  - **Withdrawn content stays searchable/exportable**: session-query full-text and `/export` read the raw log; a rewind cuts only the surface, so withdrawn messages remain (declared in the README).
70
72
  - **Session title auto-regeneration**: the title derives from the surface, so an automatically-derived title may change after a rewind.
71
73
  - **Files written but uncommitted in a cancelled turn**: a `both` rewind cannot restore them (tool side-effect timing; same as Claude Code).
@@ -101,6 +103,14 @@ The plugin treats these as harness-side defects it does not compensate for. Each
101
103
  are English for **every** host command — system plugins also pass raw English `description`
102
104
  strings, e.g. `/goal` — so this behavior is consistent with the ecosystem, not a plugin
103
105
  deviation.)
106
+ - **Client-side command-description i18n is first-party-only**: DSH also localizes host command
107
+ descriptions through the client `locale` binding (`ui-commands`), but the description keys come
108
+ from a **closed allowlist** (`HOST_DESCRIPTION_KEYS`: compact, export, feedback, goal,
109
+ permission, plan). A command outside that set — every third-party plugin — is passed through
110
+ verbatim, never translated (`hostDescription` only rewrites a description that equals the
111
+ first-party English copy). So the plugin's own `/rewind` command description can never ride this
112
+ channel either; it is authored in the host's tongue (English by default), same as the host
113
+ runtime copy above.
104
114
 
105
115
  ### R-OPENSTEP (rewind part resolved): an unclosed `step` in the log breaks token-meter replay; the rewind no longer compounds it
106
116
 
@@ -73,28 +73,28 @@ Each release is `git push <branch>` followed by `git push <branch> --tags`.
73
73
 
74
74
  DSH is still in rc; npm's prerelease matching rules require a peer range to
75
75
  share the host version's `[major, minor, patch]` tuple. So `peerDependencies`
76
- uses one peer tuple per DSH line (e.g. `^0.1.2-rc.1`), replaced (never
77
- appended) when DSH releases a new tuple.
78
-
79
- - **When to update**: only when DSH releases a new tuple (`0.1.2 → 0.1.3 →
80
- 0.2.x`); rc rolling within a tuple (`0.1.2-rc.1 rc.2`) needs nothing. All
81
- `@deepseek-ai/*` packages release together; `npm view @deepseek-ai/dsh version`
82
- is the authoritative signal.
76
+ uses one peer tuple per DSH line (e.g. `^0.1.2-rc.1`). The range is
77
+ conservative: it declares only what was verified, and guarantees nothing
78
+ outside it.
79
+
80
+ - **When to update**: when the verified range changes a new DSH tuple, or a
81
+ deliberate narrowing (e.g. dropping the internal `alpha` series); a release
82
+ already inside the range changes nothing. All `@deepseek-ai/*` packages
83
+ release together; `npm view @deepseek-ai/dsh dist-tags` is the signal.
83
84
  - **Published-tuple check (optional)**: `node scripts/check-dsh-version.mjs`
84
85
  compares the `latest` dist-tag version against the tuple the peers cover
85
86
  (exit 0 = nothing to do, exit 1 = update). It reads the `latest` tag only; a
86
87
  pre-release published under another tag or bundled without
87
88
  going to npm is a manual pre-release check — see the "Before bumping" step above.
88
- - **Update steps**: replace every `@deepseek-ai/dsh-*` peer's tuple with
89
- `^<latest>` bump devDependencies to the latest → `npm install` →
90
- `npm run check` → release.
89
+ - **Update steps**: point every `@deepseek-ai/dsh-*` peer and devDependency at
90
+ the verified range `npm install` → `npm run check` → release.
91
91
  - **After DSH goes final**: final releases are not bound by the prerelease
92
92
  tuple rule, so the peers can converge to a single stable range (e.g.
93
93
  `^0.1.x`); this section can then be deleted.
94
94
  - **Declared minimum (`dsh.engines.dsh`)**: alongside the peer tuple, each
95
95
  release declares the DSH runtime floor under `dsh.engines.dsh` (e.g.
96
96
  `>=0.1.2-rc.1`), consumed by the plugin-manager update guard. Bump it in
97
- the same release that raises the peer tuple; never leave code raised while
97
+ the same release that changes the peer range; never leave code raised while
98
98
  the declared floor stays behind. Only the `>=X.Y.Z[-pre]` form is
99
99
  supported (`^`/`~`/multi-range are treated as "cannot verify" and
100
100
  fail closed).
@@ -66,22 +66,22 @@ npm publish --access public
66
66
 
67
67
  DSH 仍在 rc 阶段,npm 的 prerelease 匹配规则要求 peer 范围与宿主版本
68
68
  **同 `[major, minor, patch]` 元组**才能匹配。因此 peerDependencies 采用
69
- **每一条 DSH 线一个 peer 元组**(如 `^0.1.2-rc.1`),DSH 发新元组时**替换**而非追加。
69
+ **每一条 DSH 线一个 peer 元组**(如 `^0.1.2-rc.1`);范围是保守的,只声明实际验证过的内容。
70
70
 
71
- - **何时需要更新**:仅当 DSH 发布新元组(`0.1.2 → 0.1.3 → 0.2.x`)时;
72
- 同元组内 rc 滚动(`0.1.2-rc.1 → rc.2`)无需动作。DSH 所有包同版本发布,
73
- `npm view @deepseek-ai/dsh version` 即权威信号。
71
+ - **何时需要更新**:**已验证范围**变化时——DSH 发新元组,或主动收窄(例如不再
72
+ 声明内测的 DSH `alpha` 系列);落在已声明范围内的发布不改变任何东西。DSH 所有包
73
+ 同版本发布;信号是 `npm view @deepseek-ai/dsh dist-tags`。
74
74
  - **已发布元组检查(可选)**:`node scripts/check-dsh-version.mjs` 用 npm `latest`
75
75
  dist-tag 版本对比 peer 覆盖的元组(exit 0 无需动作,exit 1 需要)。它**只读
76
76
  `latest` tag**;发布在其它 tag 的 pre-release 走**手动发布前检查**
77
77
  ——见上文"升版前手动确认"。
78
- - **更新步骤**:把每个 `@deepseek-ai/dsh-*` peer 的元组**替换**为 `^<最新版>`
79
- devDependencies 同步升到最新 → `npm install` → `npm run check` → 发版。
78
+ - **更新步骤**:把每个 `@deepseek-ai/dsh-*` peer devDependency 指向已验证
79
+ 范围 → `npm install` → `npm run check` → 发版。
80
80
  - **正式版后收敛**:DSH 发布 final 版本后,正式版不受 prerelease 元组规则
81
81
  限制,peer 可收敛为稳定的 `^0.1.x` 单范围,此节即可删除。
82
82
  - **声明的最低运行时(`dsh.engines.dsh`)**:与 peer 元组一起,每个发布在
83
83
  `dsh.engines.dsh` 声明 DSH 运行时下限(如 `>=0.1.2-rc.1`),供插件管理器
84
- 更新守卫读取。**与 peer 元组持平的同一发布里一并 bump**;不可只升代码、
84
+ 更新守卫读取。**与 peer 范围持平的同一发布里一并 bump**;不可只升代码、
85
85
  声明下限停留在旧值。仅支持 `>=X.Y.Z[-pre]` 形式(`^`/`~`/多范围会被视为
86
86
  「无法校验」而 fail-closed)。
87
87
 
@@ -4,18 +4,19 @@
4
4
 
5
5
  ## Applies to
6
6
 
7
- - **DSH**: `0.1.2-rc.1`
8
- - **dsh-rewind**: `0.9.0-alpha.1`, `0.9.0-alpha.2`, `0.9.0`, `0.9.1`
7
+ - **DSH version**: `0.1.2-rc.1`.
8
+ - **Plugin version**: `v0.9.0-alpha.1`, `v0.9.0-alpha.2`, `v0.9.0`, `v0.9.1` (recommended).
9
+ - **Audience**: users planning to use a future DSH version who used an early plugin (≤ 0.8.0).
9
10
 
10
11
  ## Background
11
12
 
12
- `dsh-rewind` rewinds **within the same session** — it never forks a branch. It also follows the conservative policy of **deleting no history** (the session log is an append-only audit trail): it only **appends a marker**, telling DSH "the model-visible conversation continues from this message; everything after it is rolled back". Older plugin versions wrote that **rewind marker** as a "ghost step frame", which worked reliably on DSH v0.1.2-rc.1 and earlier.
13
+ `dsh-rewind` rewinds **within the same session** — it never forks a branch, and the session log stays the single source. The plugin follows a conservative policy: it deletes no history, and only **appends a rewind marker** telling DSH "the model-visible conversation continues from this message; everything after it is rolled back". Older plugin versions wrote that **rewind marker** as a "ghost step frame", which worked reliably on DSH `v0.1.2-rc.1` and earlier.
13
14
 
14
- But DSH v0.1.3 (alpha) introduces a stricter **session-format validation**. We discovered ahead of time that the **rewind markers** written by older plugin versions **cannot be validated** under those stricter checks, so **a session rewound with `/rewind` may fail to open** when that line ships.
15
+ But starting with DSH `v0.1.3-alpha.1`, a stricter **session-format validation** is introduced. We discovered ahead of time that the **rewind markers** written by older plugin versions **cannot be validated** under those stricter checks, so **a session that was rewound may fail to open**.
15
16
 
16
17
  The plugin is prepared in two parts, both shipped in the new version:
17
18
 
18
- 1. **New rewind markers use the new shape** (forward) — this is a **correct, low-risk**, long-term change (see the [README](../README.en.md), the "How it works" section), and the new shape is fully compatible with **both old and new** DSH.
19
+ 1. **New rewind markers use the new shape** (forward) — this is a **correct, low-risk**, long-term change aligned with the official `/compact` design (see the [README](../README.en.md), the "How it works" section), and the new shape is fully compatible with **both old and new** DSH.
19
20
 
20
21
  2. **The `/dsh-rewind-fix` command** (backward) — for **already-existing** old sessions, the plugin ships a convenient update command that translates those old markers into the new shape so the sessions are usable again.
21
22
 
@@ -54,7 +55,7 @@ Then run the command **in this new session**, and it will update the old session
54
55
 
55
56
  ### Step 4 · Preview the update scope
56
57
 
57
- Type `/dsh-rewind-fix` in the composer and send it. It only scans, doesn't write. The editor locks while it runs, which is normal. **Don't switch session windows midway.** When it finishes, it reports how many sessions were scanned, how many will be updated, how many skipped, and how many failed. Confirm those are what you expect before moving on.
58
+ Type `/dsh-rewind-fix` in the composer and send it. It only scans, doesn't write. The editor locks while it runs, which is normal. **Don't switch session windows midway**. When it finishes, it reports how many sessions were scanned, how many will be updated, how many skipped, and how many failed. Confirm those are what you expect before moving on.
58
59
  ```
59
60
  /dsh-rewind-fix
60
61
  ```
@@ -73,7 +74,7 @@ After the run, restart DSH, then **preview once more**:
73
74
  ```
74
75
  /dsh-rewind-fix
75
76
  ```
76
- If it says there are no more sessions to update, the markers are all current and the old sessions will open normally.
77
+ If it says there are no more sessions to update, the markers are all current and the old sessions will open normally. If there are still sessions to update, they may have been loaded during the run — restart in a temporary session window and retry.
77
78
 
78
79
  ### Step 7 · Delete the backup after confirming (optional)
79
80
 
@@ -84,5 +85,5 @@ rm -rf ~/.dsh/sessions.backup ~/.dsh/rewind-snapshots.backup
84
85
 
85
86
  ## Notes
86
87
 
87
- - The session logs are still v0 format; when DSH's later `v0 → v1 → v2` migration (in `v0.1.3`) arrives, if you've completed the `rewind` marker update, the markers themselves won't block it.
88
+ - The session logs are still v0 format; when DSH's later `v0 → v1 → v2 → v3` migration arrives, if you've completed the `rewind` marker update, the markers themselves won't block it.
88
89
  - Some sessions may still be blocked by an **unclosed turn** (a turn that was interrupted/cancelled and never wrote `turn/end`). Per the empirical analysis these are widespread and **unrelated to rewind** — they're a DSH-side issue and this tool doesn't handle them. **So updating the markers doesn't guarantee you can upgrade to the next DSH line.**
@@ -4,18 +4,19 @@
4
4
 
5
5
  ## 适配
6
6
 
7
- - **DSH**: `0.1.2-rc.1`
8
- - **dsh-rewind**: `0.9.0-alpha.1`, `0.9.0-alpha.2`, `0.9.0`, `0.9.1`
7
+ - **DSH 版本**:`v0.1.2-rc.1`。
8
+ - **插件版本**:`v0.9.0-alpha.1`,`v0.9.0-alpha.2`,`v0.9.0`,`v0.9.1`(推荐)。
9
+ - **适用人群**:计划使用未来版本 DSH,且使用过早期版本插件(≤ 0.8.0)的用户。
9
10
 
10
11
  ## 背景
11
12
 
12
- `dsh-rewind` 的回退是**同一会话**的消息回退。它从不新建分支,同时遵循保守策略:**不删除任何历史**(会话日志是 append-only 审计轨迹),只在日志里**追加一条标记**,表示“模型可见的对话从这条消息继续,其后内容回卷”。旧版插件的 **rewind 标记**用的是「幽灵步骤框架」写法,在 DSH v0.1.2-rc.1 及之前版本长期验证有效。
13
+ `dsh-rewind` 的回退是**同一会话**的消息回退。它从不新建分支,会话日志保持同一份。插件遵循保守策略:不删除任何历史,只在会话日志里**追加一条 rewind 标记**,表示“模型可见的对话从这条消息继续,其后内容回卷”。旧版插件的 **rewind 标记**用的是「幽灵步骤框架」写法,在 DSH `v0.1.2-rc.1` 及之前的版本长期验证有效。
13
14
 
14
- DSH v0.1.3alpha)引入了更严格的**会话格式校验**。我们提前发现:旧版本往会话日志里写入的 **rewind 标记**,在这种严格校验下**无法自证合法**,届时会导致**碰过 `/rewind` 的旧会话打不开**。
15
+ 但从 `v0.1.3-alpha.1` 开始,DSH 引入了更严格的**会话格式校验**。我们提前发现:旧版本往会话日志里写入的 **rewind 标记**,在这种严格校验下**无法自证合法**,届时会导致**回退过的旧会话打不开**。
15
16
 
16
17
  插件做了两阶段准备,已在新版本中包含:
17
18
 
18
- 1. **新产生的 rewind 标记换成新格式**(向前)——这一步是**正确、低风险**的长期改动(见 [README](../README.md) 的「原理」一节),新格式对**新旧版本** DSH 均完全兼容。
19
+ 1. **新产生的 rewind 标记换成新格式**(向前)——这一步是**正确、低风险**的长期改动(对标官方 `/compact` 设计,见 [README](../README.md)「原理」一节),新格式对**新旧版本** DSH 均完全兼容。
19
20
 
20
21
  2. **提供 `/dsh-rewind-fix` 命令**(向后)——对于**已经存在**的旧会话,本插件提供便捷更新命令,把旧标记「翻译」成新格式,让会话重新可用。
21
22
 
@@ -54,7 +55,7 @@ cp -r ~/.dsh/rewind-snapshots ~/.dsh/rewind-snapshots.backup
54
55
 
55
56
  ### 第四步 · 预览更新范围
56
57
 
57
- 在编辑区域输入 `/dsh-rewind-fix` 并发送。它只扫描、不写盘。执行时编辑区会正常锁定。**不要中途切换会话窗口。**完成后会告诉你:扫描了几个会话、其中几个将被更新、几个跳过、几个失败。确认是你预期的那几项,再进下一步。
58
+ 在编辑区域输入 `/dsh-rewind-fix` 并发送。它只扫描、不写盘。执行时编辑区会正常锁定。**不要中途切换会话窗口**。完成后会告诉你:扫描了几个会话、其中几个将被更新、几个跳过、几个失败。确认是你预期的那几项,再进下一步。
58
59
  ```
59
60
  /dsh-rewind-fix
60
61
  ```
@@ -73,7 +74,7 @@ cp -r ~/.dsh/rewind-snapshots ~/.dsh/rewind-snapshots.backup
73
74
  ```
74
75
  /dsh-rewind-fix
75
76
  ```
76
- 如果提示已经没有待更新的会话,说明标记已全部更新到位,旧会话就能正常打开了。
77
+ 如果提示已经没有待更新的会话,说明标记已全部更新到位,旧会话就能正常打开了。如果还有未更新的会话,可能是执行时被加载过,在临时会话窗口重启并重试即可。
77
78
 
78
79
  ### 第七步 · 确认正常后删除备份(可选)
79
80
 
@@ -84,5 +85,5 @@ rm -rf ~/.dsh/sessions.backup ~/.dsh/rewind-snapshots.backup
84
85
 
85
86
  ## 补充说明
86
87
 
87
- - 当前会话日志仍为 v0 格式;DSH 后续(`v0.1.3`)的 `v0 → v1 → v2` 迁移到来时,如果你完成了 `rewind` 标记更新,标记本身就不会阻断它。
88
+ - 当前会话日志仍为 v0 格式;DSH 后续的 `v0 → v1 → v2 → v3` 迁移到来时,如果你完成了 `rewind` 标记更新,标记本身就不会阻断它。
88
89
  - 个别会话还可能因**未闭合的 turn**(某次回合被打断/取消、没写 `turn/end`)等问题而卡住迁移——根据实测分析,这些问题普遍存在,**与 rewind 无关**,属 DSH 侧自身问题,本工具不处理。**所以更新完标记并不保证一定能升到下一 DSH 线。**
package/lib/client.js CHANGED
@@ -399,7 +399,7 @@ var STYLE = `
399
399
  .dsh-rewind-cleanup-body {
400
400
  border-top: 0.5px solid var(--dsw-alias-border-l2);
401
401
  margin: 0 16px;
402
- padding: 4px 0 8px;
402
+ padding: 0 0 8px;
403
403
  }
404
404
  .dsh-rewind-cleanup-readonly {
405
405
  margin: 12px 0 0;
@@ -421,6 +421,12 @@ var STYLE = `
421
421
  .dsh-rewind-cleanup-field + .dsh-rewind-cleanup-field {
422
422
  border-top: 0.5px solid var(--dsw-alias-border-l2);
423
423
  }
424
+ /* Auto-cleanup (permission) block and the max-age field are consecutive control
425
+ * sections of the open card \u2014 give them the same 0.5px divider the harness gives
426
+ * every pair of config fields (fields.module.css field + field). */
427
+ .dsh-rewind-cleanup-permission + .dsh-rewind-cleanup-field {
428
+ border-top: 0.5px solid var(--dsw-alias-border-l2);
429
+ }
424
430
  .dsh-rewind-cleanup-head {
425
431
  display: flex;
426
432
  align-items: center;
@@ -505,7 +511,7 @@ var STYLE = `
505
511
  box-sizing: border-box;
506
512
  height: 34px;
507
513
  padding: 0 12px;
508
- border: 1px solid var(--dsw-alias-border-l2);
514
+ border: 0.5px solid var(--dsw-alias-border-l4);
509
515
  border-radius: 8px;
510
516
  background: var(--dsw-alias-bg-layer-3);
511
517
  font: inherit;
@@ -530,7 +536,7 @@ var STYLE = `
530
536
  justify-content: flex-end;
531
537
  gap: 8px;
532
538
  padding: 12px 0 4px;
533
- border-top: 1px solid var(--dsw-alias-border-l2);
539
+ border-top: 0.5px solid var(--dsw-alias-border-l2);
534
540
  }
535
541
  .dsh-rewind-cleanup-failed {
536
542
  flex: 1;
@@ -1339,8 +1345,8 @@ function createRewindBridge(deps) {
1339
1345
  }
1340
1346
 
1341
1347
  // src/client/build-info.ts
1342
- var PLUGIN_VERSION = true ? "0.10.0-alpha.2" : "dev";
1343
- var BUILD_HASH = true ? "cc039b5d" : "dev";
1348
+ var PLUGIN_VERSION = true ? "0.11.0-beta.1" : "dev";
1349
+ var BUILD_HASH = true ? "d770983e" : "dev";
1344
1350
 
1345
1351
  // src/client/locales.ts
1346
1352
  var zh = {
package/lib/index.js CHANGED
@@ -119,6 +119,7 @@ var RewindError = class extends Error {
119
119
  }
120
120
  code;
121
121
  };
122
+ var REWIND_MARKER_SOURCE = Object.freeze({ kind: "plugin", plugin: "dsh-rewind" });
122
123
  var CANDIDATE_PREVIEW_CHARS = 80;
123
124
  var DEFAULT_CANDIDATE_LIMIT = 100;
124
125
  function isUserMessageEvent(event) {
@@ -356,6 +357,13 @@ var SnapshotStore = class _SnapshotStore {
356
357
  lastEntry = /* @__PURE__ */ new Map();
357
358
  /** Sessions whose dedup state has been seeded from disk this process. */
358
359
  seededSessions = /* @__PURE__ */ new Set();
360
+ /**
361
+ * Session-format version snapshots are anchored under, stamped into each
362
+ * session's `format` marker when an entry is recorded. `null` until the host
363
+ * sets it (from `agent/session-start`), so a session that never records is
364
+ * never materialized and a marker is only written where snapshots exist.
365
+ */
366
+ formatVersion = null;
359
367
  constructor(root, opts) {
360
368
  this.dedup = opts?.dedup ?? true;
361
369
  this.root = root ?? process.env[SNAPSHOT_ROOT_ENV] ?? join(resolveDshHome(opts?.dshHome), SNAPSHOT_DIR_NAME);
@@ -435,6 +443,9 @@ var SnapshotStore = class _SnapshotStore {
435
443
  await writeJsonAtomic(file, committed, () => opts?.crash?.("after-temp-write"));
436
444
  this.lastEntry.set(key, { content: entry.before, ref: selfRef });
437
445
  }
446
+ if (this.formatVersion !== null) {
447
+ await this.markFormatVersion(sessionId, this.formatVersion);
448
+ }
438
449
  const now = Date.now();
439
450
  if (now - this.lastPruneAt >= _SnapshotStore.PRUNE_INTERVAL_MS) {
440
451
  this.lastPruneAt = now;
@@ -631,6 +642,8 @@ var SnapshotStore = class _SnapshotStore {
631
642
  }
632
643
  /** Prefix of one restore-op journal file inside the session dir. */
633
644
  static JOURNAL_PREFIX = "restore-journal-";
645
+ /** Session-format-version marker file inside the session dir. Non-`.json`, so it never counts as a checkpoint entry. */
646
+ static FORMAT_FILE = "format";
634
647
  /** Absolute path of one restore-op journal file. */
635
648
  journalPath(sessionId, opId) {
636
649
  return join(this.sessionDir(sessionId), `${_SnapshotStore.JOURNAL_PREFIX}${safeFileId(opId)}.json`);
@@ -1275,6 +1288,75 @@ var SnapshotStore = class _SnapshotStore {
1275
1288
  }
1276
1289
  return { sessionId, ...stats, dryRun };
1277
1290
  }
1291
+ /**
1292
+ * Read the session-format version marker recorded for a session, or `null`
1293
+ * when there is no marker — a pre-marker, legacy snapshot dir, or a session
1294
+ * that never materialized a dir.
1295
+ */
1296
+ async readFormatVersion(sessionId) {
1297
+ try {
1298
+ const raw = await readFile(join(this.sessionDir(sessionId), _SnapshotStore.FORMAT_FILE), "utf8");
1299
+ const parsed = Number(raw.trim());
1300
+ return Number.isFinite(parsed) ? parsed : null;
1301
+ } catch {
1302
+ return null;
1303
+ }
1304
+ }
1305
+ /**
1306
+ * Record the session-format version a session's snapshots are anchored under
1307
+ * (`session.header.version`: 2 for the v2 format, 3 for the v3 format). The
1308
+ * marker is a tiny non-`.json` file, so it never counts as a checkpoint
1309
+ * entry in `sessionStats`/`clearSession`. Written atomically (temp + rename)
1310
+ * like every other persisted marker, so a crash mid-write can only leave an
1311
+ * inert `format.tmp` — never a partial marker that a later reconcile could
1312
+ * misread as a version mismatch and wrongly clear.
1313
+ */
1314
+ async markFormatVersion(sessionId, sessionVersion) {
1315
+ const dir = this.sessionDir(sessionId);
1316
+ await mkdir(dir, { recursive: true });
1317
+ const file = join(dir, _SnapshotStore.FORMAT_FILE);
1318
+ const tmp = `${file}.tmp`;
1319
+ await writeFile(tmp, `${sessionVersion}`, "utf8");
1320
+ await rename(tmp, file);
1321
+ }
1322
+ /**
1323
+ * Set the session-format version the store stamps onto every snapshot it
1324
+ * records. The host sets this once per process from `agent/session-start`
1325
+ * (`agent.session.header.version`), so a marker is only materialized for a
1326
+ * session that actually records a snapshot.
1327
+ */
1328
+ setFormatVersion(sessionVersion) {
1329
+ this.formatVersion = sessionVersion;
1330
+ }
1331
+ /**
1332
+ * Session-format-version guard: clear a session's snapshot dir when the
1333
+ * format its snapshots were anchored under differs from the current session
1334
+ * format, so seq-anchored references can never survive a format migration
1335
+ * mis-mapped. Runs at `agent/session-start` — after DSH has migrated/loaded
1336
+ * the session, so `sessionVersion` is the post-migration value.
1337
+ *
1338
+ * Conservative rule (per the "delete stale snapshots" policy): a session
1339
+ * with no recorded marker but with snapshot content is treated as legacy and
1340
+ * cleared; a session whose marker differs from `sessionVersion` is cleared.
1341
+ * A matching version — or an untouched session with nothing to protect — is
1342
+ * left alone. The marker is re-stamped to the current version afterward so a
1343
+ * FUTURE format change is detected on the next start.
1344
+ *
1345
+ * @returns whether a session snapshot dir was cleared.
1346
+ */
1347
+ async reconcileFormatVersion(sessionId, sessionVersion) {
1348
+ const stored = await this.readFormatVersion(sessionId);
1349
+ if (stored === sessionVersion) return { cleared: false };
1350
+ const stats = await this.sessionStats(sessionId);
1351
+ const hasContent = stats.anchorGroups > 0 || stats.journals > 0;
1352
+ if (hasContent) {
1353
+ await this.clearSession(sessionId);
1354
+ }
1355
+ if (hasContent || stored !== null) {
1356
+ await this.markFormatVersion(sessionId, sessionVersion);
1357
+ }
1358
+ return { cleared: hasContent };
1359
+ }
1278
1360
  };
1279
1361
  function hashPath(path) {
1280
1362
  return createHash("sha256").update(path).digest("hex").slice(0, 8);
@@ -1511,7 +1593,6 @@ async function commitEntry(store, pending, anchorCache, trackedBySession, exec,
1511
1593
  }
1512
1594
  tracked.add(capture.path);
1513
1595
  }
1514
- var REWIND_MARKER_SOURCE = { kind: "plugin", plugin: "dsh-rewind" };
1515
1596
  var REWIND_MARKER_CONTENT = [{ type: "text", text: "(empty message)" }];
1516
1597
  function buildMarker() {
1517
1598
  return createUserMessage({
@@ -1632,7 +1713,7 @@ async function executeRewind(ctx, store, fs, invocation, rawTarget, mode, inflig
1632
1713
  let event;
1633
1714
  try {
1634
1715
  event = agent.session.append("user/message", marker, {
1635
- surfaceOp: { op: "replace", start: plan.surfaceStart, end: plan.surfaceEnd },
1716
+ surfaceOp: { op: "replace", startSeq: plan.surfaceStart, endSeq: plan.surfaceEnd },
1636
1717
  sourceEventSeqs: [...plan.shadowedSeqs]
1637
1718
  });
1638
1719
  } catch (error) {
@@ -1874,6 +1955,21 @@ function apply(ctx, config) {
1874
1955
  handler: (invocation) => handleSnapshotCleanup(store, invocation, dshHome, trackedBySession)
1875
1956
  });
1876
1957
  }, "dsh-rewind command");
1958
+ ctx.on("agent/session-start", ({ agent }) => {
1959
+ const session = agent.session;
1960
+ if (session.header.origin === "subagent" || (session.header.delegationDepth ?? 0) > 0) return;
1961
+ void (async () => {
1962
+ try {
1963
+ store.setFormatVersion(session.header.version);
1964
+ const result = await store.reconcileFormatVersion(session.id, session.header.version);
1965
+ if (result.cleared) {
1966
+ ctx.logger.warn(`[dsh-rewind] cleared snapshots for ${session.id}: session format changed (v${session.header.version})`);
1967
+ }
1968
+ } catch (error) {
1969
+ ctx.logger.warn(`[dsh-rewind] session-format reconcile failed: ${error instanceof Error ? error.message : String(error)}`);
1970
+ }
1971
+ })();
1972
+ }, { global: true });
1877
1973
  ctx.on("session/event", (session, event) => {
1878
1974
  if (event.type !== "user/message") return;
1879
1975
  const header = session.header;
@@ -20,6 +20,20 @@
20
20
  * typed structurally (see `SlotsLike`) so the plugin never imports the
21
21
  * conversation UI package's types and survives its version drift.
22
22
  *
23
+ * COUPLING NOTE — HIGH. There is no harness interface for a per-user-message
24
+ * action (see docs/compat/audit.md): `MessageIconActions.extraActions` and the
25
+ * `conversation.chat.assistant-actions` slot are wired for assistant messages
26
+ * only, and no per-user-message action slot exists. So this portal targets
27
+ * undocumented internal structure — the `data-chat-flow-kind`,
28
+ * `data-chat-anchor-key`, `data-composer-input`, `data-composer-card`,
29
+ * `data-pending-steering` and `data-time-hover-root` attributes plus the
30
+ * `anchorSeq` field read in `client/hidden.ts`. Those are harness-internal and
31
+ * may change with the UI; this module (and `hidden.ts`) must be re-adapted to
32
+ * follow, and is the migration target when a first-class user-action slot or
33
+ * an official renderer hook surface appears. The coupling is accepted
34
+ * deliberately because a standards-conformant alternative does not exist
35
+ * today; it is not a defect to be removed while the DOM-portal approach stands.
36
+ *
23
37
  * @module dsh-rewind/client/portals
24
38
  */
25
39
  import { type ReactNode } from 'react';
@@ -23,4 +23,4 @@ export declare const CLASS: {
23
23
  /** The ↶ glyph, drawn inline so the bundle stays dependency-free. */
24
24
  export declare const REWIND_ICON_SVG: string;
25
25
  /** One injected stylesheet (scoped under `.dsh-rewind-*`). */
26
- export declare const STYLE = "\n.dsh-rewind-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n padding: 6px;\n border: none;\n border-radius: 28px;\n background: transparent;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n}\n.dsh-rewind-btn:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n\n.dsh-rewind-popover {\n position: fixed;\n z-index: 1000;\n width: 288px;\n padding: 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n box-shadow: var(--dsw-shadow-lv3);\n font-size: 14px;\n line-height: 20px;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-popover-title {\n font-size: 14px;\n font-weight: 600;\n line-height: 20px;\n}\n.dsh-rewind-popover-target {\n margin: 4px 0 10px;\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-all;\n}\n.dsh-rewind-popover-option {\n display: flex;\n flex-direction: column;\n gap: 2px;\n width: 100%;\n margin: 0 0 6px;\n padding: 8px 10px;\n border: 1px solid transparent;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n.dsh-rewind-popover-option:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.dsh-rewind-popover-option:disabled {\n opacity: 0.5;\n cursor: default;\n}\n.dsh-rewind-popover-option-label {\n font-weight: 500;\n}\n.dsh-rewind-popover-option-hint {\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-popover-impact {\n margin: 4px 0 10px;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-interactive-bg-hover);\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-secondary);\n white-space: pre-wrap;\n max-height: 160px;\n overflow: auto;\n}\n.dsh-rewind-popover-actions {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n}\n.dsh-rewind-popover-primary,\n.dsh-rewind-popover-ghost {\n padding: 5px 12px;\n border: none;\n border-radius: 8px;\n font: inherit;\n font-size: 13px;\n line-height: 18px;\n cursor: pointer;\n}\n.dsh-rewind-popover-primary {\n background: var(--dsw-alias-button-primary-fill);\n color: var(--dsw-alias-label-primary-foreground);\n}\n.dsh-rewind-popover-primary:hover:not(:disabled) {\n background: var(--dsw-alias-button-primary-hover);\n}\n.dsh-rewind-popover-primary:disabled {\n opacity: 0.5;\n cursor: default;\n}\n.dsh-rewind-popover-ghost {\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-popover-ghost:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dsh-rewind-guard-hint {\n position: fixed;\n z-index: 1000;\n max-width: min(440px, calc(100vw - 24px));\n padding: 8px 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 10px;\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n box-shadow: var(--dsw-shadow-lv3);\n font-size: 13px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n pointer-events: none;\n}\n\n/* ---- Snapshot-cleanup settings card (mirrors the harness PluginCard look) ---- */\n/* Standardized on the 0.1.2 harness card look (border-radius 16px + 0.5px\n border-l4, 0.5px internal separators). */\n.dsh-rewind-cleanup-card {\n list-style: none;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: 16px;\n background: var(--dsw-alias-bg-layer-3);\n transition: border-color .16s, background .16s;\n}\n.dsh-rewind-cleanup-card:hover {\n border-color: var(--dsw-alias-label-dimmed);\n}\n.dsh-rewind-cleanup-card-open {\n background: var(--dsw-alias-bg-layer-2);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.dsh-rewind-cleanup-header {\n width: 100%;\n appearance: none;\n border: 0;\n background: none;\n font: inherit;\n color: inherit;\n text-align: left;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 14px 16px;\n border-radius: 12px;\n}\n.dsh-rewind-cleanup-header:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n.dsh-rewind-cleanup-head-text {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.dsh-rewind-cleanup-name {\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-desc {\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-cleanup-chevron {\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n transition: transform .16s;\n}\n.dsh-rewind-cleanup-chevron-open {\n transform: rotate(180deg);\n}\n.dsh-rewind-cleanup-pending {\n flex: none;\n border-radius: 999px;\n padding: 1px 8px;\n font-size: 11px;\n line-height: 17px;\n font-weight: 500;\n white-space: nowrap;\n background: var(--dsw-alias-bg-module-platform);\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-cleanup-body {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n margin: 0 16px;\n padding: 4px 0 8px;\n}\n.dsh-rewind-cleanup-readonly {\n margin: 12px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-cleanup-permission {\n display: grid;\n gap: 6px;\n padding: 12px 0;\n}\n.dsh-rewind-cleanup-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 12px 0;\n}\n.dsh-rewind-cleanup-field + .dsh-rewind-cleanup-field {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n}\n.dsh-rewind-cleanup-head {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.dsh-rewind-cleanup-label {\n flex: 1;\n min-width: 0;\n font-size: 13px;\n font-weight: 500;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-hint {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-cleanup-error {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-error);\n}\n/* Switch row: label left, role=switch button right, hint below (Subagent module).\n * The track mirrors the harness ui-primitives Switch (0.1.3-alpha.2 line): the\n * on/off appearance keys off aria-checked rather than a parallel class, so the\n * visual state cannot disagree with the state assistive technology reads, and\n * corner-shape: round opts the capsule track out of the global superellipse\n * (which would square the capsule ends off against the round thumb inside). */\n.dsh-rewind-cleanup-toggle-row {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 16px;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-toggle-label {\n flex: 1;\n min-width: 0;\n}\n.dsh-rewind-cleanup-switch {\n box-sizing: border-box;\n position: relative;\n flex: 0 0 auto;\n width: 36px;\n height: 20px;\n padding: 2px;\n border: 0;\n border-radius: 10px;\n corner-shape: round;\n background: var(--dsw-alias-border-l3);\n cursor: pointer;\n}\n.dsh-rewind-cleanup-switch[aria-checked='true'] {\n background: var(--dsw-alias-brand-primary);\n}\n.dsh-rewind-cleanup-switch:disabled {\n cursor: default;\n opacity: 0.5;\n}\n.dsh-rewind-cleanup-switch:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n}\n.dsh-rewind-cleanup-thumb {\n display: block;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n corner-shape: round;\n background: var(--dsw-alias-label-primary-foreground);\n transition: transform 120ms ease;\n}\n.dsh-rewind-cleanup-switch[aria-checked='true'] .dsh-rewind-cleanup-thumb {\n transform: translateX(16px);\n}\n.dsh-rewind-cleanup-input {\n box-sizing: border-box;\n height: 34px;\n padding: 0 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-input:focus-visible {\n outline: none;\n border-color: var(--dsw-alias-brand-primary);\n}\n.dsh-rewind-cleanup-input:disabled {\n color: var(--dsw-alias-label-tertiary);\n cursor: default;\n}\n.dsh-rewind-cleanup-input-invalid {\n border-color: var(--dsw-alias-label-error);\n}\n.dsh-rewind-cleanup-footer {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 12px 0 4px;\n border-top: 1px solid var(--dsw-alias-border-l2);\n}\n.dsh-rewind-cleanup-failed {\n flex: 1;\n min-width: 0;\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-error);\n}\n.dsh-rewind-cleanup-discard,\n.dsh-rewind-cleanup-save {\n appearance: none;\n border: 1px solid transparent;\n border-radius: 8px;\n padding: 5px 14px;\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n cursor: pointer;\n}\n.dsh-rewind-cleanup-discard {\n border-color: var(--dsw-alias-border-l2);\n background: none;\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-cleanup-discard:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.dsh-rewind-cleanup-save {\n background: var(--dsw-alias-label-primary);\n color: var(--dsw-alias-bg-layer-3);\n}\n.dsh-rewind-cleanup-discard:disabled,\n.dsh-rewind-cleanup-save:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.dsh-rewind-cleanup-discard:focus-visible,\n.dsh-rewind-cleanup-save:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n";
26
+ export declare const STYLE = "\n.dsh-rewind-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 28px;\n height: 28px;\n padding: 6px;\n border: none;\n border-radius: 28px;\n background: transparent;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n}\n.dsh-rewind-btn:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n}\n\n.dsh-rewind-popover {\n position: fixed;\n z-index: 1000;\n width: 288px;\n padding: 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n box-shadow: var(--dsw-shadow-lv3);\n font-size: 14px;\n line-height: 20px;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-popover-title {\n font-size: 14px;\n font-weight: 600;\n line-height: 20px;\n}\n.dsh-rewind-popover-target {\n margin: 4px 0 10px;\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-all;\n}\n.dsh-rewind-popover-option {\n display: flex;\n flex-direction: column;\n gap: 2px;\n width: 100%;\n margin: 0 0 6px;\n padding: 8px 10px;\n border: 1px solid transparent;\n border-radius: 8px;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n.dsh-rewind-popover-option:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n.dsh-rewind-popover-option:disabled {\n opacity: 0.5;\n cursor: default;\n}\n.dsh-rewind-popover-option-label {\n font-weight: 500;\n}\n.dsh-rewind-popover-option-hint {\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-popover-impact {\n margin: 4px 0 10px;\n padding: 8px 10px;\n border-radius: 8px;\n background: var(--dsw-alias-interactive-bg-hover);\n font-size: 12px;\n line-height: 16px;\n color: var(--dsw-alias-label-secondary);\n white-space: pre-wrap;\n max-height: 160px;\n overflow: auto;\n}\n.dsh-rewind-popover-actions {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n}\n.dsh-rewind-popover-primary,\n.dsh-rewind-popover-ghost {\n padding: 5px 12px;\n border: none;\n border-radius: 8px;\n font: inherit;\n font-size: 13px;\n line-height: 18px;\n cursor: pointer;\n}\n.dsh-rewind-popover-primary {\n background: var(--dsw-alias-button-primary-fill);\n color: var(--dsw-alias-label-primary-foreground);\n}\n.dsh-rewind-popover-primary:hover:not(:disabled) {\n background: var(--dsw-alias-button-primary-hover);\n}\n.dsh-rewind-popover-primary:disabled {\n opacity: 0.5;\n cursor: default;\n}\n.dsh-rewind-popover-ghost {\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-popover-ghost:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.dsh-rewind-guard-hint {\n position: fixed;\n z-index: 1000;\n max-width: min(440px, calc(100vw - 24px));\n padding: 8px 12px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 10px;\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n box-shadow: var(--dsw-shadow-lv3);\n font-size: 13px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n pointer-events: none;\n}\n\n/* ---- Snapshot-cleanup settings card (mirrors the harness PluginCard look) ---- */\n/* Standardized on the 0.1.2 harness card look (border-radius 16px + 0.5px\n border-l4, 0.5px internal separators). */\n.dsh-rewind-cleanup-card {\n list-style: none;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: 16px;\n background: var(--dsw-alias-bg-layer-3);\n transition: border-color .16s, background .16s;\n}\n.dsh-rewind-cleanup-card:hover {\n border-color: var(--dsw-alias-label-dimmed);\n}\n.dsh-rewind-cleanup-card-open {\n background: var(--dsw-alias-bg-layer-2);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.dsh-rewind-cleanup-header {\n width: 100%;\n appearance: none;\n border: 0;\n background: none;\n font: inherit;\n color: inherit;\n text-align: left;\n cursor: pointer;\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 14px 16px;\n border-radius: 12px;\n}\n.dsh-rewind-cleanup-header:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n.dsh-rewind-cleanup-head-text {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.dsh-rewind-cleanup-name {\n font-size: 15px;\n font-weight: 600;\n line-height: 1.4;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-desc {\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-cleanup-chevron {\n flex: none;\n color: var(--dsw-alias-label-tertiary);\n transition: transform .16s;\n}\n.dsh-rewind-cleanup-chevron-open {\n transform: rotate(180deg);\n}\n.dsh-rewind-cleanup-pending {\n flex: none;\n border-radius: 999px;\n padding: 1px 8px;\n font-size: 11px;\n line-height: 17px;\n font-weight: 500;\n white-space: nowrap;\n background: var(--dsw-alias-bg-module-platform);\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-cleanup-body {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n margin: 0 16px;\n padding: 0 0 8px;\n}\n.dsh-rewind-cleanup-readonly {\n margin: 12px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-cleanup-permission {\n display: grid;\n gap: 6px;\n padding: 12px 0;\n}\n.dsh-rewind-cleanup-field {\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 12px 0;\n}\n.dsh-rewind-cleanup-field + .dsh-rewind-cleanup-field {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n}\n/* Auto-cleanup (permission) block and the max-age field are consecutive control\n * sections of the open card \u2014 give them the same 0.5px divider the harness gives\n * every pair of config fields (fields.module.css field + field). */\n.dsh-rewind-cleanup-permission + .dsh-rewind-cleanup-field {\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n}\n.dsh-rewind-cleanup-head {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n.dsh-rewind-cleanup-label {\n flex: 1;\n min-width: 0;\n font-size: 13px;\n font-weight: 500;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-hint {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-cleanup-error {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-error);\n}\n/* Switch row: label left, role=switch button right, hint below (Subagent module).\n * The track mirrors the harness ui-primitives Switch (0.1.3-alpha.2 line): the\n * on/off appearance keys off aria-checked rather than a parallel class, so the\n * visual state cannot disagree with the state assistive technology reads, and\n * corner-shape: round opts the capsule track out of the global superellipse\n * (which would square the capsule ends off against the round thumb inside). */\n.dsh-rewind-cleanup-toggle-row {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 16px;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-toggle-label {\n flex: 1;\n min-width: 0;\n}\n.dsh-rewind-cleanup-switch {\n box-sizing: border-box;\n position: relative;\n flex: 0 0 auto;\n width: 36px;\n height: 20px;\n padding: 2px;\n border: 0;\n border-radius: 10px;\n corner-shape: round;\n background: var(--dsw-alias-border-l3);\n cursor: pointer;\n}\n.dsh-rewind-cleanup-switch[aria-checked='true'] {\n background: var(--dsw-alias-brand-primary);\n}\n.dsh-rewind-cleanup-switch:disabled {\n cursor: default;\n opacity: 0.5;\n}\n.dsh-rewind-cleanup-switch:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n}\n.dsh-rewind-cleanup-thumb {\n display: block;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n corner-shape: round;\n background: var(--dsw-alias-label-primary-foreground);\n transition: transform 120ms ease;\n}\n.dsh-rewind-cleanup-switch[aria-checked='true'] .dsh-rewind-cleanup-thumb {\n transform: translateX(16px);\n}\n.dsh-rewind-cleanup-input {\n box-sizing: border-box;\n height: 34px;\n padding: 0 12px;\n border: 0.5px solid var(--dsw-alias-border-l4);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-3);\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-cleanup-input:focus-visible {\n outline: none;\n border-color: var(--dsw-alias-brand-primary);\n}\n.dsh-rewind-cleanup-input:disabled {\n color: var(--dsw-alias-label-tertiary);\n cursor: default;\n}\n.dsh-rewind-cleanup-input-invalid {\n border-color: var(--dsw-alias-label-error);\n}\n.dsh-rewind-cleanup-footer {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 12px 0 4px;\n border-top: 0.5px solid var(--dsw-alias-border-l2);\n}\n.dsh-rewind-cleanup-failed {\n flex: 1;\n min-width: 0;\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-error);\n}\n.dsh-rewind-cleanup-discard,\n.dsh-rewind-cleanup-save {\n appearance: none;\n border: 1px solid transparent;\n border-radius: 8px;\n padding: 5px 14px;\n font: inherit;\n font-size: 13px;\n line-height: 1.5;\n cursor: pointer;\n}\n.dsh-rewind-cleanup-discard {\n border-color: var(--dsw-alias-border-l2);\n background: none;\n color: var(--dsw-alias-label-secondary);\n}\n.dsh-rewind-cleanup-discard:hover:not(:disabled) {\n color: var(--dsw-alias-label-primary);\n border-color: var(--dsw-alias-label-dimmed);\n}\n.dsh-rewind-cleanup-save {\n background: var(--dsw-alias-label-primary);\n color: var(--dsw-alias-bg-layer-3);\n}\n.dsh-rewind-cleanup-discard:disabled,\n.dsh-rewind-cleanup-save:disabled {\n opacity: 0.4;\n cursor: default;\n}\n.dsh-rewind-cleanup-discard:focus-visible,\n.dsh-rewind-cleanup-save:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n";
@@ -15,6 +15,11 @@
15
15
  * renders through machine tokens (`impact=<n>`, `args` @seq), never by parsing
16
16
  * host prose.
17
17
  *
18
+ * DSH localizes only its OWN command descriptions via the client locale — a
19
+ * closed `HOST_DESCRIPTION_KEYS` allowlist; third-party plugin descriptions are
20
+ * not in that channel (see docs/compat/audit.md, RU-I18N), so this plugin's host
21
+ * command copy stays in the host's tongue (English by default).
22
+ *
18
23
  * English is the key-set source of truth; zh is checked complete against it.
19
24
  *
20
25
  * @module dsh-rewind/locales
@@ -9,10 +9,10 @@
9
9
  * transcript) is untouched; only the model-visible surface is cut, so the
10
10
  * next request derives its context from the target message onward.
11
11
  *
12
- * Marker shape (v0.1.3/v2): the marker is a `user/message` carrying a replace
12
+ * Marker shape (v0.1.5/v3): the marker is a `user/message` carrying a replace
13
13
  * `surfaceOp` — a single event:
14
14
  *
15
- * user/message (marker content) → { surfaceOp {replace, start, end} }
15
+ * user/message (marker content) → { surfaceOp {replace, startSeq, endSeq} }
16
16
  *
17
17
  * v2 reserves surface `replace` to a node that cites every shadowed seq via
18
18
  * `sourceEventSeqs`, and `assistant/message` can no longer carry
@@ -26,6 +26,7 @@
26
26
  *
27
27
  * @module dsh-rewind/rewind
28
28
  */
29
+ import type { MessageSource } from '@deepseek-ai/dsh-llm/message';
29
30
  import type { SessionEvent, UserMessage } from '@deepseek-ai/dsh-session';
30
31
  /** Which of the two rewind modes a rewind executes. */
31
32
  export type RewindMode = 'chat' | 'both';
@@ -73,6 +74,25 @@ export interface RewindPlan {
73
74
  /** Last surface node — the replace range end (inclusive). */
74
75
  readonly surfaceEnd: number;
75
76
  }
77
+ /**
78
+ * The rewind-marker source: the backend-independent identity carried by every
79
+ * marker the plugin appends. It is the plain third-party plugin source shape
80
+ * `{ kind: 'plugin', plugin: 'dsh-rewind' }` — no extra fields, because a
81
+ * plugin source is a CLOSED shape in the harness (only `kind`/`plugin`, plus
82
+ * the context-injection `form`/`sections`/`summary`; see `MessageSourceMap` and
83
+ * the released-format source validator). A plugin extends the source map by
84
+ * adding a new `kind`, never by hanging private fields off `plugin`.
85
+ */
86
+ export declare const REWIND_MARKER_SOURCE: Readonly<{
87
+ readonly kind: 'plugin';
88
+ readonly plugin: 'dsh-rewind';
89
+ }>;
90
+ /**
91
+ * Test whether a persisted message source identifies a rewind marker.
92
+ * @param source - source restored from a surface user message.
93
+ * @returns whether the source carries the backend-independent rewind brand.
94
+ */
95
+ export declare function isRewindMarker(source: MessageSource): boolean;
76
96
  /** Preview length cap for candidate listings. */
77
97
  export declare const CANDIDATE_PREVIEW_CHARS = 80;
78
98
  /**
@@ -317,6 +317,13 @@ export declare class SnapshotStore {
317
317
  private readonly lastEntry;
318
318
  /** Sessions whose dedup state has been seeded from disk this process. */
319
319
  private readonly seededSessions;
320
+ /**
321
+ * Session-format version snapshots are anchored under, stamped into each
322
+ * session's `format` marker when an entry is recorded. `null` until the host
323
+ * sets it (from `agent/session-start`), so a session that never records is
324
+ * never materialized and a marker is only written where snapshots exist.
325
+ */
326
+ private formatVersion;
320
327
  constructor(root?: string, opts?: {
321
328
  readonly dedup?: boolean;
322
329
  readonly dshHome?: string;
@@ -423,6 +430,8 @@ export declare class SnapshotStore {
423
430
  restoreAfter(sessionId: string, targetSeq: number, deleteFile: DeleteFile, probe?: DiskProbe, opts?: RestoreRunOptions): Promise<RestoreOutcome>;
424
431
  /** Prefix of one restore-op journal file inside the session dir. */
425
432
  private static readonly JOURNAL_PREFIX;
433
+ /** Session-format-version marker file inside the session dir. Non-`.json`, so it never counts as a checkpoint entry. */
434
+ private static readonly FORMAT_FILE;
426
435
  /** Absolute path of one restore-op journal file. */
427
436
  private journalPath;
428
437
  /**
@@ -622,6 +631,48 @@ export declare class SnapshotStore {
622
631
  clearSession(sessionId: string, opts?: {
623
632
  readonly dryRun?: boolean;
624
633
  }): Promise<ClearSessionReport>;
634
+ /**
635
+ * Read the session-format version marker recorded for a session, or `null`
636
+ * when there is no marker — a pre-marker, legacy snapshot dir, or a session
637
+ * that never materialized a dir.
638
+ */
639
+ private readFormatVersion;
640
+ /**
641
+ * Record the session-format version a session's snapshots are anchored under
642
+ * (`session.header.version`: 2 for the v2 format, 3 for the v3 format). The
643
+ * marker is a tiny non-`.json` file, so it never counts as a checkpoint
644
+ * entry in `sessionStats`/`clearSession`. Written atomically (temp + rename)
645
+ * like every other persisted marker, so a crash mid-write can only leave an
646
+ * inert `format.tmp` — never a partial marker that a later reconcile could
647
+ * misread as a version mismatch and wrongly clear.
648
+ */
649
+ markFormatVersion(sessionId: string, sessionVersion: number): Promise<void>;
650
+ /**
651
+ * Set the session-format version the store stamps onto every snapshot it
652
+ * records. The host sets this once per process from `agent/session-start`
653
+ * (`agent.session.header.version`), so a marker is only materialized for a
654
+ * session that actually records a snapshot.
655
+ */
656
+ setFormatVersion(sessionVersion: number): void;
657
+ /**
658
+ * Session-format-version guard: clear a session's snapshot dir when the
659
+ * format its snapshots were anchored under differs from the current session
660
+ * format, so seq-anchored references can never survive a format migration
661
+ * mis-mapped. Runs at `agent/session-start` — after DSH has migrated/loaded
662
+ * the session, so `sessionVersion` is the post-migration value.
663
+ *
664
+ * Conservative rule (per the "delete stale snapshots" policy): a session
665
+ * with no recorded marker but with snapshot content is treated as legacy and
666
+ * cleared; a session whose marker differs from `sessionVersion` is cleared.
667
+ * A matching version — or an untouched session with nothing to protect — is
668
+ * left alone. The marker is re-stamped to the current version afterward so a
669
+ * FUTURE format change is detected on the next start.
670
+ *
671
+ * @returns whether a session snapshot dir was cleared.
672
+ */
673
+ reconcileFormatVersion(sessionId: string, sessionVersion: number): Promise<{
674
+ cleared: boolean;
675
+ }>;
625
676
  }
626
677
  /**
627
678
  * Re-check every tracked file at a user-message boundary and record the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-rewind-plugin",
3
- "version": "0.10.0-alpha.2",
3
+ "version": "0.11.0-beta.1",
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",
@@ -50,7 +50,7 @@
50
50
  "patch": "./cordis.patch.yml"
51
51
  },
52
52
  "engines": {
53
- "dsh": ">=0.1.3-alpha.2"
53
+ "dsh": ">=0.1.5-rc.1"
54
54
  },
55
55
  "client": {
56
56
  "inject": [
@@ -76,18 +76,18 @@
76
76
  "peerDependencies": {
77
77
  "@deepseek-ai/cordis": "^4.0.1",
78
78
  "@deepseek-ai/schemastery": "^3.18.1",
79
- "@deepseek-ai/dsh-client-locale": "^0.1.3-alpha.2",
80
- "@deepseek-ai/dsh-client-ui-commands": "^0.1.3-alpha.2",
81
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.3-alpha.2",
82
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.3-alpha.2",
83
- "@deepseek-ai/dsh-commands": "^0.1.3-alpha.2",
84
- "@deepseek-ai/dsh-fs": "^0.1.3-alpha.2",
85
- "@deepseek-ai/dsh-home-paths": "^0.1.3-alpha.2",
86
- "@deepseek-ai/dsh-llm": "^0.1.3-alpha.2",
87
- "@deepseek-ai/dsh-sandbox": "^0.1.3-alpha.2",
88
- "@deepseek-ai/dsh-session": "^0.1.3-alpha.2",
89
- "@deepseek-ai/dsh-settings": "^0.1.3-alpha.2",
90
- "@deepseek-ai/dsh-tools": "^0.1.3-alpha.2"
79
+ "@deepseek-ai/dsh-client-locale": "^0.1.5-rc.1",
80
+ "@deepseek-ai/dsh-client-ui-commands": "^0.1.5-rc.1",
81
+ "@deepseek-ai/dsh-client-ui-settings": "^0.1.5-rc.1",
82
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.1",
83
+ "@deepseek-ai/dsh-commands": "^0.1.5-rc.1",
84
+ "@deepseek-ai/dsh-fs": "^0.1.5-rc.1",
85
+ "@deepseek-ai/dsh-home-paths": "^0.1.5-rc.1",
86
+ "@deepseek-ai/dsh-llm": "^0.1.5-rc.1",
87
+ "@deepseek-ai/dsh-sandbox": "^0.1.5-rc.1",
88
+ "@deepseek-ai/dsh-session": "^0.1.5-rc.1",
89
+ "@deepseek-ai/dsh-settings": "^0.1.5-rc.1",
90
+ "@deepseek-ai/dsh-tools": "^0.1.5-rc.1"
91
91
  },
92
92
  "peerDependenciesMeta": {
93
93
  "@deepseek-ai/cordis": {
@@ -136,37 +136,37 @@
136
136
  "devDependencies": {
137
137
  "@deepseek-ai/cordis": "^4.0.1",
138
138
  "@deepseek-ai/schemastery": "^3.18.1",
139
- "@deepseek-ai/dsh-agent": "^0.1.3-alpha.2",
140
- "@deepseek-ai/dsh-attachment": "^0.1.3-alpha.2",
141
- "@deepseek-ai/dsh-client-locale": "^0.1.3-alpha.2",
142
- "@deepseek-ai/dsh-client-connection": "^0.1.3-alpha.2",
143
- "@deepseek-ai/dsh-client-store": "^0.1.3-alpha.2",
144
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.3-alpha.2",
145
- "@deepseek-ai/dsh-api-session-controller": "^0.1.3-alpha.2",
146
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.3-alpha.2",
147
- "@deepseek-ai/dsh-scope": "^0.1.3-alpha.2",
148
- "@deepseek-ai/dsh-typert-protocol": "^0.1.3-alpha.2",
149
- "@deepseek-ai/dsh-user-questions": "^0.1.3-alpha.2",
150
- "@deepseek-ai/dsh-client-ui-commands": "^0.1.3-alpha.2",
151
- "@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.3-alpha.2",
152
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.3-alpha.2",
153
- "@deepseek-ai/dsh-command-compact": "^0.1.3-alpha.2",
154
- "@deepseek-ai/dsh-commands": "^0.1.3-alpha.2",
155
- "@deepseek-ai/dsh-compaction": "^0.1.3-alpha.2",
156
- "@deepseek-ai/dsh-compaction-basic": "^0.1.3-alpha.2",
157
- "@deepseek-ai/dsh-fs": "^0.1.3-alpha.2",
158
- "@deepseek-ai/dsh-goal": "^0.1.3-alpha.2",
159
- "@deepseek-ai/dsh-home-paths": "^0.1.3-alpha.2",
160
- "@deepseek-ai/dsh-llm": "^0.1.3-alpha.2",
161
- "@deepseek-ai/dsh-plan-mode": "^0.1.3-alpha.2",
162
- "@deepseek-ai/dsh-sandbox": "^0.1.3-alpha.2",
163
- "@deepseek-ai/dsh-session": "^0.1.3-alpha.2",
164
- "@deepseek-ai/dsh-session-projection": "^0.1.3-alpha.2",
165
- "@deepseek-ai/dsh-session-stats": "^0.1.3-alpha.2",
166
- "@deepseek-ai/dsh-session-title": "^0.1.3-alpha.2",
167
- "@deepseek-ai/dsh-settings": "^0.1.3-alpha.2",
168
- "@deepseek-ai/dsh-token-meter": "^0.1.3-alpha.2",
169
- "@deepseek-ai/dsh-tools": "^0.1.3-alpha.2",
139
+ "@deepseek-ai/dsh-agent": "^0.1.5-rc.1",
140
+ "@deepseek-ai/dsh-attachment": "^0.1.5-rc.1",
141
+ "@deepseek-ai/dsh-client-locale": "^0.1.5-rc.1",
142
+ "@deepseek-ai/dsh-client-connection": "^0.1.5-rc.1",
143
+ "@deepseek-ai/dsh-client-store": "^0.1.5-rc.1",
144
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.5-rc.1",
145
+ "@deepseek-ai/dsh-api-session-controller": "^0.1.5-rc.1",
146
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.5-rc.1",
147
+ "@deepseek-ai/dsh-scope": "^0.1.5-rc.1",
148
+ "@deepseek-ai/dsh-typert-protocol": "^0.1.5-rc.1",
149
+ "@deepseek-ai/dsh-user-questions": "^0.1.5-rc.1",
150
+ "@deepseek-ai/dsh-client-ui-commands": "^0.1.5-rc.1",
151
+ "@deepseek-ai/dsh-client-ui-input-trigger": "^0.1.5-rc.1",
152
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.5-rc.1",
153
+ "@deepseek-ai/dsh-command-compact": "^0.1.5-rc.1",
154
+ "@deepseek-ai/dsh-commands": "^0.1.5-rc.1",
155
+ "@deepseek-ai/dsh-compaction": "^0.1.5-rc.1",
156
+ "@deepseek-ai/dsh-compaction-basic": "^0.1.5-rc.1",
157
+ "@deepseek-ai/dsh-fs": "^0.1.5-rc.1",
158
+ "@deepseek-ai/dsh-goal": "^0.1.5-rc.1",
159
+ "@deepseek-ai/dsh-home-paths": "^0.1.5-rc.1",
160
+ "@deepseek-ai/dsh-llm": "^0.1.5-rc.1",
161
+ "@deepseek-ai/dsh-plan-mode": "^0.1.5-rc.1",
162
+ "@deepseek-ai/dsh-sandbox": "^0.1.5-rc.1",
163
+ "@deepseek-ai/dsh-session": "^0.1.5-rc.1",
164
+ "@deepseek-ai/dsh-session-projection": "^0.1.5-rc.1",
165
+ "@deepseek-ai/dsh-session-stats": "^0.1.5-rc.1",
166
+ "@deepseek-ai/dsh-session-title": "^0.1.5-rc.1",
167
+ "@deepseek-ai/dsh-settings": "^0.1.5-rc.1",
168
+ "@deepseek-ai/dsh-token-meter": "^0.1.5-rc.1",
169
+ "@deepseek-ai/dsh-tools": "^0.1.5-rc.1",
170
170
  "@types/node": "^24.0.0",
171
171
  "@types/react": "^18.3.31",
172
172
  "@types/react-dom": "^18.3.7",