dsh-rewind-plugin 0.4.2 → 0.6.0

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/docs/format.md ADDED
@@ -0,0 +1,151 @@
1
+ # Durable format
2
+
3
+ The on-disk format of the dsh-rewind checkpoint store, pinned as a spec. The
4
+ implementation is `src/snapshot.ts`; this document is the reference for what
5
+ readers may rely on and what a future incompatible change must do. If the code
6
+ and this spec disagree, the code wins and this spec is a bug.
7
+
8
+ ## State root
9
+
10
+ The store root defaults to `<harness home>/rewind-snapshots/` — the dsh data
11
+ directory (`~/.dsh/rewind-snapshots/` when `DSH_HOME` is unset) — overridable in
12
+ order by the `snapshotDir` plugin config, then the `DSH_REWIND_SNAPSHOT_DIR`
13
+ environment variable. It is a sibling of the workspace, never a subtree of it.
14
+ Deleting the root only removes file backups; the store rebuilds from scratch.
15
+
16
+ ```
17
+ <root>/
18
+ └── <sessionId>/ # safeSessionId(sessionId)
19
+ ├── <anchorSeq>/ # decimal seq of the anchoring user/message
20
+ │ └── <callId>.json # one committed before-backup
21
+ └── restore-journal-<opId>.json # one restore-op journal
22
+ ```
23
+
24
+ - `sessionId` is sanitized to `[a-zA-Z0-9._-]`; the bare values `.` and `..`
25
+ are replaced (`safeSessionId`) so a hostile id cannot traverse out of the
26
+ root.
27
+ - `callId` is sanitized to `[a-zA-Z0-9._-]` (`safeFileId`).
28
+ - `<anchorSeq>` is a decimal integer; directories with non-integer names are
29
+ ignored by readers.
30
+ - Journal files are recognized by the `restore-journal-` prefix; everything
31
+ else ending in `.json` under the session dir is treated as a checkpoint
32
+ entry.
33
+
34
+ ## Checkpoint entry
35
+
36
+ One JSON file per before-backup, named `<callId>.json`:
37
+
38
+ ```ts
39
+ interface CheckpointEntry {
40
+ callId: string // the tool call that mutated the file
41
+ anchorSeq: number // seq of the user message anchoring the turn of the change
42
+ path: string // resolved display path (absolute)
43
+ before: string | null // full content before the change; null = file was created
44
+ time: number // epoch ms, strictly increasing within a store instance
45
+ }
46
+ ```
47
+
48
+ Semantics:
49
+
50
+ - **`before` is the pre-edit state**: `null` means the call created the file.
51
+ - **`anchorSeq` ties the backup to a user message**: rewinding to message N
52
+ applies every entry anchored at or after N (the boundary is inclusive).
53
+ - **`time` is the ordering key within an anchor group**: it is monotonic per
54
+ store instance (bumped past the previous commit), so same-millisecond
55
+ commits stay capture-ordered and a re-read always picks the same "earliest"
56
+ entry per path.
57
+ - Synthetic re-check entries (external edits/deletions seen at a user-message
58
+ boundary) use `callId = recheck-<anchorSeq>-<sha256(path) first 8 hex>`.
59
+
60
+ ### Dedup link entry
61
+
62
+ A tracked file that records the same `before` content as its immediately-prior
63
+ entry for that path is stored as a **link** instead of a full copy: the entry
64
+ carries a `ref` (the `<anchorSeq>/<callId>.json` of that prior entry) and omits
65
+ `before`, so identical content is never duplicated across entries. A reader
66
+ resolves the `ref` back to the terminal real snapshot; `before: null` still
67
+ means "the file was created". A `ref` is validated as a single-level,
68
+ `<digits>/<callId>.json` relative reference (no traversal) so a corrupt or
69
+ hostile ref cannot escape the store root when followed. Because links reference
70
+ prior entries, `prune` materializes a surviving link whose `ref` lands on a
71
+ group it is about to drop before deleting that group, so no kept link is left
72
+ dangling.
73
+
74
+ Real entries (with `before`) are unchanged and read identically before and
75
+ after this addition; a link entry is a distinct kind that lacks `before`.
76
+
77
+ ## Restore journal
78
+
79
+ One JSON file per restore operation, written **before any mutation** and
80
+ updated as the pass applies:
81
+
82
+ ```ts
83
+ interface RestoreJournal {
84
+ version: 1
85
+ id: string // `op-<base36 ms>-<random>`; file name suffix
86
+ sessionId: string
87
+ targetSeq: number // rewind target the restore belongs to
88
+ startedAt: number // epoch ms
89
+ finishedAt?: number // set on a terminal state
90
+ state: 'running' | 'rollback-running' | 'completed' | 'rolled-back' | 'recovery-required'
91
+ actions: RestoreJournalAction[]
92
+ rollbackError?: string // set when a rollback pass failed partway
93
+ }
94
+
95
+ interface RestoreJournalAction {
96
+ path: string
97
+ action: 'restore' | 'delete' // restore = write `before` back; delete = unlink
98
+ before: string | null // target content for restore; null for delete
99
+ rescue: string | null // pre-restore disk state; null = file was absent
100
+ rescueError?: string // set when the rescue capture failed (rollback skips it)
101
+ done: boolean // true once the action's fs op completed and was marked
102
+ failed?: string // per-action failure message (the pass never aborts)
103
+ }
104
+ ```
105
+
106
+ States: `running` and `rollback-running` are non-terminal; a host restart turns
107
+ them into `interrupted` (or `recovery-required` when the journal is corrupt or
108
+ a rollback could not complete). `completed` / `rolled-back` are terminal.
109
+
110
+ ## Write guarantees
111
+
112
+ - **Atomicity**: every JSON write serializes to a sibling `<target>.tmp` and
113
+ renames over the target. A crash between the two steps leaves only the temp
114
+ file — never a readable half-written target — and readers ignore temp files
115
+ (they do not end in `.json`). The next write of the same target overwrites
116
+ a leftover temp.
117
+ - **Journal before mutation**: the rescue state of every planned path is
118
+ captured and the intent journal persisted atomically BEFORE the first fs
119
+ mutation; each action is marked `done` as it is applied.
120
+ - **Disk is truth**: after a restart, reconciliation compares the real disk
121
+ against each action's goal (the restore target for `running` journals, the
122
+ rescue state for `rollback-running` / `recovery-required` ones). A path
123
+ whose disk already matches is marked done without being touched.
124
+ - **Bounded storage**: `prune` keeps the newest 100 anchor groups per session
125
+ (`MAX_ANCHOR_GROUPS`), materializing any surviving dedup link that references
126
+ a group being dropped before deleting whole anchor directories; it also
127
+ recycles terminal journals (`completed` / `rolled-back`). Non-terminal and
128
+ corrupt journals are always kept. Across sessions, `pruneStale` removes whole
129
+ long-inactive session directories whose newest member stamp is older than a
130
+ configurable idle cutoff (default off), so the store root does not grow
131
+ without bound either.
132
+
133
+ ## Validation and failure policy
134
+
135
+ - **Entries**: a missing or malformed entry is read as `undefined` (silently
136
+ skipped) — losing one backup, never the recovery path.
137
+ - **Journals**: a corrupt or schema-invalid journal **fails loud** —
138
+ `reconcileRestores` reports it as `recovery-required` and never drops it,
139
+ because dropping it would silently erase the interrupted restore's recovery
140
+ record.
141
+ - **Journal IO**: best-effort by design — if a journal cannot be written, the
142
+ restore proceeds with pre-journal semantics (crash safety degrades,
143
+ behavior does not).
144
+
145
+ ## Versioning policy
146
+
147
+ The journal schema is `version: 1`. Checkpoint entries currently carry no
148
+ version field. A future incompatible format must either bump the journal
149
+ `version` (readers reject unknown values — there is no best-effort fallback or
150
+ legacy coercion) or move the state root (e.g. `rewind-snapshots/v2`) and ship
151
+ an explicit migration tool. Old-format data is never silently re-interpreted.
@@ -21,7 +21,7 @@
21
21
 
22
22
  Compatibility probes against these subsystems (test-driven investigation, the
23
23
  `compat-invariants` / `compat-interop` vitest suites + the `verify-host` real
24
- `/compact` chain): [compat-audit.md](compat-audit.md).
24
+ `/compact` chain): [compat/audit.md](compat/audit.md).
25
25
 
26
26
  Also under `docs/` at the repo root: `persistence-catalog.md` (full
27
27
  `SessionEventMap`), `tool-catalog.md` (tool inventory), `config-catalog.md`
@@ -45,20 +45,24 @@ Also under `docs/` at the repo root: `persistence-catalog.md` (full
45
45
 
46
46
  ## Plugin source layout
47
47
 
48
+ The module map below duplicates `AGENTS.md` (`Layout at a glance`), which is
49
+ the canonical source; this block only adds the finer-grained client-side files
50
+ and packaging entries.
51
+
48
52
  ```
49
53
  src/index.ts host plugin: /rewind command + checkpoint pipeline (tools/execute|post-execute)
50
54
  src/rewind.ts pure planning: target resolution, surface range, candidate listing
51
55
  src/snapshot.ts checkpoint store (disk before-backups, restore/preview, bounded prune)
52
56
  src/session-cwd.ts session-cwd resolution (fs-tools rule)
53
- src/client/index.ts client plugin: per-message ↶ button + manual /rewind guard
57
+ src/client/index.ts client plugin: /rewind command decoration + per-message ↶ button portals
54
58
  src/client/popover.ts mode-selection popover (both-mode impact confirm)
55
59
  src/client/hidden.ts withdrawn-span computation (hiddenSeqsOf), pure
56
60
  src/client/locales.ts zh / en copy (LocaleNamespaceMap)
57
61
  src/client/styles.ts injected styles (dsh design tokens)
58
62
  scripts/build.mjs esbuild: lib/index.js (host ESM) + lib/client.js (loader closure) + .d.ts
59
- scripts/verify-host.mjs end-to-end host verification (18 checks)
63
+ scripts/verify-host.mjs end-to-end host verification (full check suite)
60
64
  tests/ vitest suites (rewind / snapshot / hidden / session-cwd / integration)
61
- docs/ maintainer docs: harness reference, troubleshooting, release steps
65
+ docs/ maintainer docs: contract/, compat/, release/ subdirectories
62
66
  assets/screenshots/ UI screenshots
63
67
  cordis.patch.yml bundle patch (mounts the dual-face plugin row)
64
68
  package.json dsh.bundle + dsh.client manifests, optional peerDependencies
@@ -0,0 +1,69 @@
1
+ # Release
2
+
3
+ [简体中文](release.zh.md)
4
+
5
+ ## First release (manual, one-time)
6
+
7
+ Trusted Publisher can only be configured once the package exists, so the first
8
+ version is published locally:
9
+
10
+ ```sh
11
+ npm login
12
+ npm publish --access public
13
+ ```
14
+
15
+ - If prompted for `EOTP`: complete the browser auth link the CLI prints, or
16
+ retry with a 6-digit code — `npm publish --otp=<code>`.
17
+ - The first version carries no provenance (local path) — acceptable; every CI
18
+ release after that publishes with Sigstore/SLSA provenance automatically.
19
+
20
+ ## Configure Trusted Publisher (npmjs.com, one-time)
21
+
22
+ Open `https://www.npmjs.com/package/dsh-rewind-plugin` → package **settings** →
23
+ **Trusted Publisher**:
24
+
25
+ | Field | Value |
26
+ | --- | --- |
27
+ | Provider | GitHub Actions |
28
+ | Organization or user | `SiriLee` |
29
+ | Repository | `dsh-rewind` (the GitHub repo, not the npm name) |
30
+ | Workflow filename | `publish.yml` |
31
+ | Environment | empty |
32
+ | Allowed actions | `npm publish` |
33
+
34
+ ## Subsequent releases (CI, automatic)
35
+
36
+ ```sh
37
+ npm version patch
38
+ git push origin main --tags # triggers .github/workflows/publish.yml
39
+ ```
40
+
41
+ - The workflow verifies the tag matches `package.json`, runs typecheck + tests +
42
+ a full build + artifact verification, publishes with `--provenance`
43
+ (Sigstore), and creates a GitHub Release. It is **idempotent** — an already
44
+ published version is skipped.
45
+ - CI (`.github/workflows/ci.yml`) runs `npm run check` — typecheck + tests +
46
+ build + artifact verification + a `npm pack --dry-run` — on every push / PR
47
+ across both Node engines boundary versions; the tarball layout is guarded by
48
+ `tests/package-layout.test.ts`.
49
+
50
+ ## DSH version alignment (peer range maintenance)
51
+
52
+ DSH is still in rc; npm's prerelease matching rules require a peer range to
53
+ share the host version's `[major, minor, patch]` tuple. So `peerDependencies`
54
+ uses an OR-union covering every published rc tuple series
55
+ (e.g. `^0.1.0-rc.6 || ^0.1.1-rc.2`), extended as DSH releases new tuples.
56
+
57
+ - **When to update**: only when DSH releases a new tuple
58
+ (`0.1.1 → 0.1.2 → 0.2.x`); rc rolling within a tuple (`0.1.1-rc.2 → rc.3`)
59
+ needs nothing. All `@deepseek-ai/*` packages release together;
60
+ `npm view @deepseek-ai/dsh version` is the authoritative signal.
61
+ - **Automatic detection**: `node scripts/check-dsh-version.mjs` compares the
62
+ latest npm version against the tuples the peers cover and reports whether an
63
+ extension is needed (exit 0 = nothing to do, exit 1 = update).
64
+ - **Update steps**: append `|| ^<new-tuple>-rc.<n>` to every
65
+ `@deepseek-ai/dsh-*` peer → bump devDependencies to the latest → `npm
66
+ install` → `npm run check` → release.
67
+ - **After DSH goes final**: final releases are not bound by the prerelease
68
+ tuple rule, so the peers can converge to a single stable range (e.g.
69
+ `^0.1.x`); this section can then be deleted.
@@ -1,53 +1,7 @@
1
- # Release
2
-
3
- ## First release (manual, one-time)
4
-
5
- Trusted Publisher can only be configured once the package exists, so the first
6
- version is published locally:
7
-
8
- ```sh
9
- npm login
10
- npm publish --access public
11
- ```
12
-
13
- - If prompted for `EOTP`: complete the browser auth link the CLI prints, or
14
- retry with a 6-digit code — `npm publish --otp=<code>`.
15
- - The first version carries no provenance (local path) — acceptable; every CI
16
- release after that publishes with Sigstore/SLSA provenance automatically.
17
-
18
- ## Configure Trusted Publisher (npmjs.com, one-time)
19
-
20
- Open `https://www.npmjs.com/package/dsh-rewind-plugin` → package **settings** →
21
- **Trusted Publisher**:
22
-
23
- | Field | Value |
24
- | --- | --- |
25
- | Provider | GitHub Actions |
26
- | Organization or user | `SiriLee` |
27
- | Repository | `dsh-rewind` (the GitHub repo, not the npm name) |
28
- | Workflow filename | `publish.yml` |
29
- | Environment | empty |
30
- | Allowed actions | `npm publish` |
31
-
32
- ## Subsequent releases (CI, automatic)
33
-
34
- ```sh
35
- npm version patch
36
- git push origin main --tags # triggers .github/workflows/publish.yml
37
- ```
38
-
39
- - The workflow verifies the tag matches `package.json`, runs typecheck + tests +
40
- a full build + artifact verification, publishes with `--provenance`
41
- (Sigstore), and creates a GitHub Release. It is **idempotent** — an already
42
- published version is skipped.
43
- - CI (`.github/workflows/ci.yml`) runs the same checks on every push / PR, plus
44
- a `npm pack --dry-run` sanity check that the tarball carries `lib/` and
45
- `LICENSE`.
46
-
47
- ---
48
-
49
1
  # 发布流程
50
2
 
3
+ [English](release.md)
4
+
51
5
  ## 首次发布(手动,一次性)
52
6
 
53
7
  Trusted Publisher 要求**包已存在**才能配置,因此首个版本走本地发布:
@@ -86,8 +40,9 @@ git push origin main --tags # 触发 .github/workflows/publish.yml
86
40
  - workflow 校验 tag 与 `package.json` 版本一致,跑 typecheck + 测试 + 完整
87
41
  构建 + 产物验证,以 `--provenance`(Sigstore)发布并创建 GitHub Release。
88
42
  **幂等**——已发布的版本会跳过。
89
- - CI(`.github/workflows/ci.yml`)在每次 push / PR 跑相同检查,外加
90
- `npm pack --dry-run` 校验 tarball 含 `lib/` 与 `LICENSE`。
43
+ - CI(`.github/workflows/ci.yml`)在每次 push / PR 跑 `npm run check`——
44
+ typecheck + 测试 + 构建 + 产物验证 + `npm pack --dry-run`,且覆盖
45
+ engines 两个边界版本;tarball 布局由 `tests/package-layout.test.ts` 守护。
91
46
 
92
47
  ## DSH 版本适配(peer 范围维护)
93
48
 
@@ -102,7 +57,6 @@ OR 并集覆盖 DSH 已发布的每个 rc 元组系列(如 `^0.1.0-rc.6 || ^0.
102
57
  - **自动检测**:`node scripts/check-dsh-version.mjs` 对比 npm 最新版本与
103
58
  peer 覆盖的元组,输出是否需追加(exit 0 无需动作,exit 1 需要)。
104
59
  - **更新步骤**:给每个 `@deepseek-ai/dsh-*` peer 追加 `|| ^<新元组>-rc.<n>`
105
- → devDependencies 同步升到最新 → `npm install` → `npm run typecheck` /
106
- `npm test` / `npm run verify:host` → 发版。
60
+ → devDependencies 同步升到最新 → `npm install` → `npm run check` → 发版。
107
61
  - **正式版后收敛**:DSH 发布 final 版本后,正式版不受 prerelease 元组规则
108
62
  限制,peer 可收敛为稳定的 `^0.1.x` 单范围,此节即可删除。
@@ -0,0 +1,90 @@
1
+ # Snapshot cleanup
2
+
3
+ The plugin saves a backup of each file before it's edited, so you can rewind your
4
+ code to an earlier point. These backups are called **snapshots**, and they're
5
+ grouped by message and stored per session.
6
+
7
+ `/snapshot-auto-cleanup` helps you manage those snapshots — whether you want to
8
+ reclaim disk space from old sessions or clear the **current** session so its
9
+ rewind history starts fresh.
10
+
11
+ ## What it does
12
+
13
+ **Automatic cleanup** (off by default): the plugin can remember which sessions
14
+ you've stopped using, and now and then remove their snapshots to keep disk usage
15
+ down. It never touches your active session, and it never touches your
16
+ conversation.
17
+
18
+ **Manual:** even with automatic cleanup off, you can run the cleanup yourself:
19
+
20
+ - `/snapshot-auto-cleanup run [--apply]` — preview, then actually remove the
21
+ snapshots of sessions you haven't used for a while.
22
+ - `/snapshot-auto-cleanup run --current [--apply]` — preview, then actually
23
+ clear the **current** session's snapshots. This resets its rewind history to
24
+ "from now on" (your conversation is unaffected). If a turn is currently
25
+ running, the plugin pauses it first, then clears.
26
+
27
+ ## Commands
28
+
29
+ ```
30
+ /snapshot-auto-cleanup show the current settings
31
+ /snapshot-auto-cleanup on|off turn automatic cleanup on or off
32
+ /snapshot-auto-cleanup max-age <days> how many idle days before a session's snapshots are removed
33
+ /snapshot-auto-cleanup run preview what the automatic cleanup would remove
34
+ /snapshot-auto-cleanup run --apply actually remove those snapshots
35
+ /snapshot-auto-cleanup run --current preview clearing this session's snapshots
36
+ /snapshot-auto-cleanup run --current --apply actually clear this session's snapshots
37
+ ```
38
+
39
+ `run` always starts as a preview; add `--apply` to make the change. `run` works
40
+ whether or not automatic cleanup is on.
41
+
42
+ ## Settings
43
+
44
+ The settings live in `<dsh home>/snapshot-cleanup.json`:
45
+
46
+ ```json
47
+ { "enabled": false, "maxAgeDays": 30 }
48
+ ```
49
+
50
+ - `enabled` — whether automatic cleanup runs (default `false`).
51
+ - `maxAgeDays` — how many idle days before a session's snapshots are removed
52
+ (default `30`). Only positive numbers are accepted, so a broken setting can
53
+ never delete everything.
54
+
55
+ You can point the plugin at a different file with the `DSH_SNAPSHOT_CLEANUP_CONFIG`
56
+ environment variable. The file is changed only when you turn cleanup on/off or
57
+ set `max-age`; a missing file reads as the safe default (off).
58
+
59
+ ## When automatic cleanup runs
60
+
61
+ Automatic cleanup checks at most **once per run** (a restart lets it check again),
62
+ on the first session activity (a message you send or a tool call that finishes),
63
+ and only when it's enabled and at least 24 hours have passed since the last
64
+ check. The 24-hour clock is saved to `<dsh home>/snapshot-cleanup-last-sweep.json`,
65
+ so restarting doesn't reset it. It runs in the background and never blocks
66
+ what you're doing.
67
+
68
+ If you want a change to take effect right away, use `run`; automatic cleanup
69
+ picks up a fresh setting on the next run.
70
+
71
+ ## Safety
72
+
73
+ - Only rewind **snapshots** (the file backups) are ever removed. Your
74
+ conversation is never touched, and the plugin never rewrites or deletes your
75
+ session history.
76
+ - Automatic cleanup never removes your **active** session's snapshots — only
77
+ sessions that have been idle past the cutoff.
78
+ - `run --current` clears the current session's snapshots. This is one-way for
79
+ that session's file-rewind history: you can't rewind code to before the clear,
80
+ but your conversation stays intact, and the session starts recording fresh
81
+ snapshots from now on.
82
+
83
+ ## Known limitation
84
+
85
+ The plugin keeps the most recent **100 messages'** snapshots per session. If you
86
+ rewind or compact a lot in one long session, those 100 slots can be taken up by
87
+ messages that are no longer reachable, so you may find you can't rewind as far
88
+ back as you'd like. (Claude Code behaves the same way.) To get back to a clean
89
+ state, run `/snapshot-auto-cleanup run --current --apply` to clear the current
90
+ session and start fresh.
@@ -0,0 +1,57 @@
1
+ # 快照清理
2
+
3
+ 插件会在每次修改文件前保存一份备份,方便你把代码回退到之前的状态。这些备份叫**快照**,按消息分组、按会话存放。
4
+
5
+ `/snapshot-auto-cleanup` 帮你管理这些快照——既可以回收旧会话占用的磁盘,也可以清空**当前**会话、让它从"现在"重新开始记录回退历史。
6
+
7
+ ## 它能做什么
8
+
9
+ **自动清理**(默认关闭):插件会记住哪些会话你已不再使用,并定期移除它们的快照以节省磁盘。它**绝不**触碰你当前活跃的会话,也**绝不**触碰你的对话。
10
+
11
+ **手动:**即使自动清理是关闭的,你也可以自己执行清理:
12
+
13
+ - `/snapshot-auto-cleanup run [--apply]` — 先预演,再真正移除你已有一阵子没用的会话的快照。
14
+ - `/snapshot-auto-cleanup run --current [--apply]` — 先预演,再真正清空**当前**会话的快照。这会让它从"现在"重新开始记录回退历史(你的对话不受影响)。若此刻有正在进行中的回合,插件会先暂停它,再清空。
15
+
16
+ ## 命令
17
+
18
+ ```
19
+ /snapshot-auto-cleanup 查看当前设置
20
+ /snapshot-auto-cleanup on|off 开启/关闭自动清理
21
+ /snapshot-auto-cleanup max-age <天数> 会话失活多少天后移除其快照
22
+ /snapshot-auto-cleanup run 预演:看自动清理会移除什么
23
+ /snapshot-auto-cleanup run --apply 真正移除这些快照
24
+ /snapshot-auto-cleanup run --current 预演:看清空本会话快照会怎样
25
+ /snapshot-auto-cleanup run --current --apply 真正清空本会话快照
26
+ ```
27
+
28
+ `run` 一开始总是预演;加 `--apply` 才真正执行。`run` 无论自动清理是否开启都能用。
29
+
30
+ ## 设置
31
+
32
+ 设置保存在 `<dsh home>/snapshot-cleanup.json`:
33
+
34
+ ```json
35
+ { "enabled": false, "maxAgeDays": 30 }
36
+ ```
37
+
38
+ - `enabled` — 是否运行自动清理(默认 `false`)。
39
+ - `maxAgeDays` — 会话失活多少天后移除其快照(默认 `30`)。只接受正数,因此一个损坏的设置永远不可能"删掉一切"。
40
+
41
+ 你可以用环境变量 `DSH_SNAPSHOT_CLEANUP_CONFIG` 指向别的文件。该文件只在运行 `/snapshot-auto-cleanup` 时写入;文件缺失按安全默认值(关闭)处理。
42
+
43
+ ## 自动清理何时运行
44
+
45
+ 自动清理每次运行(宿主启动)**最多检查一次**,发生在首次活动时(你发出一条消息或某次工具调用完成),且仅在**已开启**且距上次检查**已满 24 小时**时才执行。这个 24 小时时钟会写入 `<dsh home>/snapshot-cleanup-last-sweep.json`,重启不会重置。它后台运行,从不阻塞你在做的事。
46
+
47
+ 想让改动立即生效,请用 `run`;自动清理要到下次运行才会采用新设置。
48
+
49
+ ## 安全
50
+
51
+ - 只会移除回退用的**快照**(那份文件备份)。你的对话绝不会被动到,插件也绝不改写或删除你的会话历史。
52
+ - 自动清理**永不**移除你**当前活跃**会话的快照——只清理已失活超过阈值的会话。
53
+ - `run --current` 会清空当前会话的快照。这对该会话的文件回退历史是**不可逆**的:你无法回退到清空之前的代码状态,但你的对话保持完整,会话会从"现在"开始重新记录快照。
54
+
55
+ ## 已知限制
56
+
57
+ 插件每个会话只保留**最近 100 条消息**的快照。若你在一个很长的会话里多次回退或压缩,这 100 个名额常会被"已经无法再回退到"的消息占满,你可能无法回退到想要的位置。(Claude Code 也是同样行为。)想恢复到一个干净状态,运行 `/snapshot-auto-cleanup run --current --apply` 清空当前会话、重新开始。
package/lib/client.js CHANGED
@@ -986,8 +986,7 @@ var zh = {
986
986
  "popover.impact.restore": "\u8FD8\u539F {path}",
987
987
  "popover.impact.delete": "\u5220\u9664 {path}",
988
988
  "popover.confirm": "\u786E\u8BA4\u56DE\u9000",
989
- "popover.back": "\u8FD4\u56DE",
990
- "guard.hint": "/rewind \u624B\u52A8\u8F93\u5165\u4E0D\u63A5\u53D7\u53C2\u6570\uFF0C\u8BF7\u76F4\u63A5\u8F93\u5165 /rewind \u6253\u5F00\u56DE\u9000\u83DC\u5355"
989
+ "popover.back": "\u8FD4\u56DE"
991
990
  };
992
991
  var en = {
993
992
  "button.aria": "Rewind to this message",
@@ -1013,8 +1012,7 @@ var en = {
1013
1012
  "popover.impact.restore": "Restore {path}",
1014
1013
  "popover.impact.delete": "Delete {path}",
1015
1014
  "popover.confirm": "Confirm rewind",
1016
- "popover.back": "Back",
1017
- "guard.hint": "/rewind takes no typed arguments \u2014 enter /rewind to open the rewind picker."
1015
+ "popover.back": "Back"
1018
1016
  };
1019
1017
 
1020
1018
  // src/client/index.ts
@@ -1069,8 +1067,7 @@ function apply(ctx) {
1069
1067
  const card = textarea?.closest("[data-composer-card]");
1070
1068
  return card ?? textarea ?? document.body;
1071
1069
  };
1072
- yield commandUi.decorate({
1073
- name: "rewind",
1070
+ const rewindPopupSpec = {
1074
1071
  // The picker exists exactly while the surface has a reachable user
1075
1072
  // message: a fresh session (no candidates) falls through to the host
1076
1073
  // command, which fails with "no user messages" — matching the harness's
@@ -1105,66 +1102,12 @@ function apply(ctx) {
1105
1102
  });
1106
1103
  }
1107
1104
  }
1108
- });
1109
- const PARAM_REWIND = /^\s*\/rewind\s+\S+/i;
1110
- const composerTextarea = () => document.querySelector(COMPOSER_SELECTOR2);
1111
- const hasParamRewindDraft = () => {
1112
- const textarea = composerTextarea();
1113
- return textarea !== null && PARAM_REWIND.test(textarea.value);
1114
- };
1115
- let guardHintEl = null;
1116
- let guardHintTimer;
1117
- const showGuardHint = () => {
1118
- if (guardHintEl !== null) guardHintEl.remove();
1119
- if (guardHintTimer !== void 0) window.clearTimeout(guardHintTimer);
1120
- const textarea = composerTextarea();
1121
- if (textarea === null) return;
1122
- const card = textarea.closest("[data-composer-card]");
1123
- const hint = document.createElement("div");
1124
- hint.className = "dsh-rewind-guard-hint";
1125
- hint.setAttribute("role", "status");
1126
- hint.textContent = t("guard.hint");
1127
- document.body.appendChild(hint);
1128
- const rect = card instanceof HTMLElement ? card.getBoundingClientRect() : textarea.getBoundingClientRect();
1129
- hint.style.left = `${Math.round(rect.left)}px`;
1130
- hint.style.bottom = `${Math.round(window.innerHeight - rect.top + 8)}px`;
1131
- guardHintEl = hint;
1132
- guardHintTimer = window.setTimeout(() => {
1133
- hint.remove();
1134
- if (guardHintEl === hint) guardHintEl = null;
1135
- guardHintTimer = void 0;
1136
- }, 3200);
1137
- };
1138
- const onParamRewindSubmit = (event) => {
1139
- if (!hasParamRewindDraft()) return;
1140
- event.preventDefault();
1141
- event.stopPropagation();
1142
- showGuardHint();
1143
- };
1144
- const onKeyDownGuard = (event) => {
1145
- if (event.key !== "Enter" || event.shiftKey || event.isComposing) return;
1146
- onParamRewindSubmit(event);
1147
1105
  };
1148
- const onClickGuard = (event) => {
1149
- if (event.button !== 0 || !hasParamRewindDraft()) return;
1150
- const target = event.target;
1151
- if (!(target instanceof Element)) return;
1152
- const button = target.closest("button");
1153
- if (button === null) return;
1154
- const card = button.closest("[data-composer-card]");
1155
- if (card === null) return;
1156
- const all = card.querySelectorAll("button");
1157
- if (all[all.length - 1] !== button) return;
1158
- if (button.querySelector("rect") !== null) return;
1159
- onParamRewindSubmit(event);
1160
- };
1161
- document.addEventListener("keydown", onKeyDownGuard, true);
1162
- document.addEventListener("click", onClickGuard, true);
1106
+ for (const name2 of ["rewind", "undo"]) {
1107
+ yield commandUi.decorate({ name: name2, ...rewindPopupSpec });
1108
+ }
1109
+ const composerTextarea = () => document.querySelector(COMPOSER_SELECTOR2);
1163
1110
  yield () => {
1164
- document.removeEventListener("keydown", onKeyDownGuard, true);
1165
- document.removeEventListener("click", onClickGuard, true);
1166
- if (guardHintEl !== null) guardHintEl.remove();
1167
- if (guardHintTimer !== void 0) window.clearTimeout(guardHintTimer);
1168
1111
  style.remove();
1169
1112
  };
1170
1113
  }, "dsh-rewind client lifecycle");