dsh-rewind-plugin 0.14.0 → 0.15.0-beta.2

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.
@@ -42,7 +42,7 @@
42
42
  */
43
43
  import { type ReactNode } from 'react';
44
44
  import type { SessionFace } from '@deepseek-ai/dsh-api-session-controller/client';
45
- import { type ChatOf, type ChatWatch, type HiddenChat } from './hidden.ts';
45
+ import { type ChatOf, type ChatWatch, type HiddenChat, type MessageImageRef } from './hidden.ts';
46
46
  import type { RewindKey } from './locales.ts';
47
47
  type Translate = (key: RewindKey, params?: Record<string, unknown>) => string;
48
48
  /** One portal target: the actions row of a user/steering seat + its durable node. */
@@ -63,7 +63,7 @@ export type PortalTarget = {
63
63
  readonly container: HTMLElement;
64
64
  /** The host inbox occurrence the retract button addresses. */
65
65
  readonly itemId: string;
66
- /** Complete editable text; null when the message contains non-text blocks. */
66
+ /** Complete editable text; null when the message contains no text block. */
67
67
  readonly text: string | null;
68
68
  readonly preview: string;
69
69
  };
@@ -96,6 +96,13 @@ export interface RewindBridgeDeps {
96
96
  * Session-scoped so the refill only lands in the session that just rewound.
97
97
  */
98
98
  readonly setComposerText: (sessionId: string, text: string) => boolean;
99
+ /**
100
+ * Session-aware image restorer (see `restoreImages` in index.ts): re-adds
101
+ * a withdrawn message's durable image references to that session's
102
+ * composer draft so a rewind of an image-bearing message does not lose
103
+ * the pictures. Optional — absent degrades to the text-only refill.
104
+ */
105
+ readonly restoreImages?: (sessionId: string, images: readonly MessageImageRef[]) => Promise<void>;
99
106
  }
100
107
  /** Structural face of the runtime slot service (see the module doc). */
101
108
  export interface SlotsLike {
@@ -162,7 +169,7 @@ export declare function writeComposer(text: string, facade: ComposerDraftWriter
162
169
  * the old baseline heuristic refilled withdrawn text into the composer
163
170
  * after switching sessions or restarting dsh.
164
171
  */
165
- export declare function runRewindAndFill(session: SessionFace, seq: number, mode: 'chat' | 'both', isMainViewSession: (sessionId: string) => boolean, chatOf: ChatOf, watchChat: ChatWatch, setComposerText: (sessionId: string, text: string) => boolean): Promise<void>;
172
+ export declare function runRewindAndFill(session: SessionFace, seq: number, mode: 'chat' | 'both', isMainViewSession: (sessionId: string) => boolean, chatOf: ChatOf, watchChat: ChatWatch, setComposerText: (sessionId: string, text: string) => boolean, restoreImages?: (sessionId: string, images: readonly MessageImageRef[]) => Promise<void>): Promise<void>;
166
173
  /**
167
174
  * The snapshot seq a rendered seat stands for.
168
175
  *
@@ -183,12 +190,12 @@ export declare function resolveSeatAnchorSeq(element: HTMLElement, chat: HiddenC
183
190
  * Locate the actions container of a user/steering seat row — the element the
184
191
  * ↶ button portals into (the copy/branch IconActions row).
185
192
  *
186
- * The finder is STRUCTURAL on purpose: no harness attribute marks the actions
187
- * row on the user seat (the old `data-time-hover-root` marker lived on the
188
- * per-turn tail footer and is gone in 0.1.6-alpha.2), so the container is
189
- * located as the direct holder of the copy `<button>` (the `MessageIconActions`
190
- * container, which mounts that button as a direct child —
191
- * `MessageIconActions.tsx:83,86`).
193
+ * The finder is STRUCTURAL on purpose: `MessageIconActions` marks this container
194
+ * with `data-clock`, which names clock placement rather than the action row (the
195
+ * old `data-time-hover-root` marker lived on the per-turn tail footer and is
196
+ * gone in 0.1.6-alpha.2), so the container is located as the direct holder of
197
+ * the copy `<button>` (the `MessageIconActions` container, which mounts that
198
+ * button as a direct child).
192
199
  *
193
200
  * Returns undefined when no qualifying container is found; the caller refuses
194
201
  * to portal (never a crash, never a wrong attachment). Exported as a test seam
@@ -220,14 +227,8 @@ export declare function actionsContainerOf(row: HTMLElement | undefined): HTMLEl
220
227
  * withdrawn "command executed" tool group on screen as an empty expandable row
221
228
  * after every member below it had been hidden.
222
229
  *
223
- * Cost stays O(seats) per refresh: the caller queries the batch once, and each
224
- * shell decision reuses the member outcomes already computed here — no second
225
- * document scan, and no per-shell subtree walk.
226
- *
227
- * Every hidden element carries `data-dsh-rewind-hidden`, so DevTools, other DOM
228
- * plugins and tests can tell a rewind-hide apart from a collapse/filter hide; a
229
- * re-created row has no marker and is re-judged, and a row that leaves the
230
- * withdrawn span is shown again.
230
+ * Every hidden element carries `data-dsh-rewind-hidden` (see
231
+ * `docs/contract/client-contract.md`).
231
232
  *
232
233
  * @param chat - the live chat snapshot the seats belong to.
233
234
  * @param seats - this refresh's seat batch (every `[data-chat-anchor-key]`).
@@ -275,6 +276,27 @@ export declare function isRewindInertSession(snapshot: SessionKindLike): boolean
275
276
  * @param hiddenSeqs - anchor seqs withdrawn by previous rewinds.
276
277
  */
277
278
  export declare function collectDurableTargets(snapshot: SessionKindLike, chat: HiddenChat | undefined, hiddenSeqs: ReadonlySet<number>): readonly PortalTarget[];
279
+ /**
280
+ * The pending bubble row's compared text and its message-attachment block
281
+ * count, read from a CLONE (the live row is never touched).
282
+ *
283
+ * Both the actions container and the message-attachment block are excluded from
284
+ * the text: the copy button's Tooltip mounts its label inside that button, and
285
+ * the attachment block renders text of its own (a file name/size, a lone
286
+ * image's loading label) that no inbox row reproduces. The block is counted
287
+ * instead, as the attachment half of the match key (`matchPendingRows`).
288
+ *
289
+ * The clone's counterpart is taken by query position, not by position among the
290
+ * row's children: the harness renders the attachment block BEFORE the message
291
+ * bubble, so "drop the first child" would drop the message text.
292
+ *
293
+ * @param row - one rendered `[data-pending-steering]` row.
294
+ * @returns the compared text and the attachment block count.
295
+ */
296
+ export declare function bubbleTextOf(row: HTMLElement): {
297
+ readonly text: string;
298
+ readonly attachments: number;
299
+ };
278
300
  interface RewindPortalsProps extends RewindBridgeDeps {
279
301
  readonly sessionId: string;
280
302
  }
@@ -289,7 +311,7 @@ interface RewindPortalsProps extends RewindBridgeDeps {
289
311
  * their actions containers) and the session's `inbox` projection (the pending
290
312
  * occurrences — see `syncInboxSubscription`).
291
313
  */
292
- export declare function RewindPortals({ sessionId, sessionOf, chatOf, isMainViewSession, watchChat, t, subscribeLocale, setComposerText }: RewindPortalsProps): ReactNode;
314
+ export declare function RewindPortals({ sessionId, sessionOf, chatOf, isMainViewSession, watchChat, t, subscribeLocale, setComposerText, restoreImages }: RewindPortalsProps): ReactNode;
293
315
  /** The current composer draft: the contenteditable `textContent`.
294
316
  * Empty when the composer is absent. Exported as a test seam (the
295
317
  * empty-composer guard in `retractPending`). */
@@ -39,7 +39,6 @@ export interface CleanupPolicy {
39
39
  readonly autoCleanupEnabled: boolean;
40
40
  readonly autoCleanupMaxAgeDays: number;
41
41
  }
42
- /** One form snapshot as this card reads it (the shared store's `getSnapshot`). */
43
42
  /**
44
43
  * One form snapshot as this card reads it: the shared form frame's own state
45
44
  * (`SettingsFormShell`) plus the two field drafts.
@@ -31,4 +31,4 @@ export declare const CLASS: {
31
31
  */
32
32
  export declare const REWIND_ICON_SVG: string;
33
33
  /** One injected stylesheet (scoped under `.dsh-rewind-*`). */
34
- 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: var(--dsw-radius-sm, 8px);\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/* Arrow navigation moves real focus, so the focused row carries the pointer's\n fill and the browser ring is suppressed: in dark mode the UA two-tone ring\n draws its dark half against the filled row and reads as a residue edge (the\n harness Menu suppresses it the same way). */\n.dsh-rewind-popover-option:focus-visible:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n outline: none;\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 configuration form ---- */\n/* The Plugins page draws the bundle's title, icon, crumb, description, and the\n shared settings form frame (labels, save, read-only and failure lines); this\n styles only the one control the card owns: the auto-cleanup toggle row. */\n.dsh-rewind-cleanup-permission {\n display: grid;\n gap: 6px;\n padding: 12px 0;\n}\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-hint {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n";
34
+ export declare const STYLE = "\n.dsh-rewind-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n /* The neighbouring action buttons scale by this delta too. */\n width: calc(28px + var(--dsh-content-font-delta, 0px));\n height: calc(28px + var(--dsh-content-font-delta, 0px));\n padding: 6px;\n border: none;\n border-radius: var(--dsw-radius-sm, 8px);\n background: transparent;\n color: var(--dsw-alias-label-tertiary);\n cursor: pointer;\n}\n.dsh-rewind-btn svg {\n width: calc(15px + var(--dsh-content-font-delta, 0px));\n height: calc(15px + var(--dsh-content-font-delta, 0px));\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/* Arrow navigation moves real focus, so the focused row carries the pointer's\n fill and the browser ring is suppressed: in dark mode the UA two-tone ring\n draws its dark half against the filled row and reads as a residue edge (the\n harness Menu suppresses it the same way). */\n.dsh-rewind-popover-option:focus-visible:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover);\n outline: none;\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 configuration form ---- */\n/* The Plugins page draws the bundle's title, icon, crumb, description, and the\n shared settings form frame (labels, save, read-only and failure lines); this\n styles only the one control the card owns: the auto-cleanup toggle row. */\n.dsh-rewind-cleanup-permission {\n display: grid;\n gap: 6px;\n padding: 12px 0;\n}\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-hint {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n";
@@ -4,17 +4,9 @@
4
4
  * lives in `src/client/`).
5
5
  *
6
6
  * Rewind mechanism: planning is pure (`src/rewind.ts`); execution appends a
7
- * marker `user/message` into the session log whose `surfaceOp` replaces every
8
- * surface node after the target message with the marker. The append-only log
9
- * (and the rendered transcript) is untouched — only the model-visible surface
10
- * is cut, so the next request derives its context from the target onward.
11
- * The marker is a `user/message` carrying the shadowed-seq citations
12
- * (`sourceEventSeqs`): v3 reserves surface `replace` to a node that cites
13
- * every shadowed seq, and `assistant/message` can no longer carry those — so
14
- * the replacement node is a `user/message`, exactly as /compact's checkpoint
15
- * is. It derives to itself (a present user turn), so the marker stays as the
16
- * surface-tail cut point rather than vanishing.
17
- *
7
+ * marker `user/message` whose `surfaceOp` replaces every surface node after the
8
+ * target with itself, cutting only the model-visible surface
9
+ * (`docs/architecture.md`).
18
10
  *
19
11
  * File restore (mode `both`) follows Claude Code's checkpointing: the plugin
20
12
  * backs up each tracked write-class edit BEFORE it happens (at the
@@ -60,6 +60,8 @@ export declare const en: {
60
60
  'skipReason.oversized': string;
61
61
  'uncovered.header': string;
62
62
  'error.invalidTarget': string;
63
+ 'sep.list': string;
64
+ 'sep.clause': string;
63
65
  'failures.suffix': string;
64
66
  'failures.item': string;
65
67
  inflight: string;
@@ -1,12 +1,10 @@
1
1
  /**
2
- * The rewind marker's message-source identity, shared by the host and the
3
- * client.
2
+ * The rewind marker's message-source identity.
4
3
  *
5
4
  * A producer owns its source `kind` outright (`MessageSourceMap` is
6
5
  * merge-extensible and has no catch-all `plugin` kind since session format v4),
7
- * so this module declares `dsh-rewind` and owns the one predicate that
8
- * recognizes the marker. Keeping it in a leaf with no cordis or node imports
9
- * lets the browser half name the marker without pulling in the host plugin.
6
+ * so this module declares the `dsh-rewind` source every marker carries. Kept in
7
+ * a leaf with no cordis or node imports.
10
8
  *
11
9
  * @module dsh-rewind/marker
12
10
  */
@@ -17,23 +15,7 @@ declare module '@deepseek-ai/dsh-llm' {
17
15
  };
18
16
  }
19
17
  }
20
- /** Producer kind carried by every marker this plugin appends. */
21
- export declare const REWIND_MARKER_KIND = "dsh-rewind";
22
18
  /** The marker's source: one producer-owned kind, no private fields. */
23
19
  export declare const REWIND_MARKER_SOURCE: Readonly<{
24
20
  readonly kind: "dsh-rewind";
25
21
  }>;
26
- /**
27
- * Test whether a persisted message source identifies a rewind marker.
28
- *
29
- * Three shapes are recognized because a marker's source is immutable once
30
- * written: the current producer-owned kind, the released v3 plugin wrapper
31
- * (`{ kind: 'plugin', plugin: 'dsh-rewind' }`) on logs this build has not yet
32
- * rewritten, and the migrated form of that wrapper (`plugin:dsh-rewind`) that
33
- * the v3→v4 conversion produces for a producer it does not know by name.
34
- * @param source - source restored from a surface user message, or any stored
35
- * value: a log written by an earlier build carries shapes the current union
36
- * no longer names.
37
- * @returns whether the source carries the rewind brand.
38
- */
39
- export declare function isRewindMarker(source: unknown): boolean;
@@ -3,32 +3,19 @@
3
3
  * No I/O and no `Session` dependency — everything derives from the event log
4
4
  * and the ordered surface, so this module stays unit-testable.
5
5
  *
6
- * Rewind semantics (see README): rewinding to a user message appends a marker
7
- * node into the session log whose `surfaceOp` replaces every surface node
8
- * AFTER the target with itself. The log (the audit trail and the rendered
9
- * transcript) is untouched; only the model-visible surface is cut, so the
10
- * next request derives its context from the target message onward.
11
- *
12
- * Marker shape (v4): the marker is a `user/message` carrying a replace
13
- * `surfaceOp` and the producer-owned source `{ kind: 'dsh-rewind' }` (see
14
- * `marker.ts`) — a single event:
15
- *
16
- * user/message (marker content) → { surfaceOp {replace, startSeq, endSeq} }
17
- *
18
- * v3 reserves surface `replace` to a node that cites every shadowed seq via
19
- * `sourceEventSeqs`, and `assistant/message` can no longer carry
20
- * `sourceEventSeqs` (it now embeds its provider stream instead) — so the
21
- * replacement node must be a `user/message`, exactly as /compact's checkpoint
22
- * is. No ghost `step/start`…`step/end` frame is needed: the token-meter's
23
- * step state machine ignores `user/message`, and the session invariant
24
- * (`invariant.ts`) imposes no open-turn requirement on it, so the marker is
25
- * appended while idle, outside any turn. It sits at the surface tail as the
26
- * model-visible "cut point" — a present user turn in derived history.
6
+ * Rewind semantics: rewinding to a user message appends a marker node into the
7
+ * session log whose `surfaceOp` replaces every surface node AFTER the target
8
+ * with itself. The log (audit trail, rendered transcript) is untouched; only
9
+ * the model-visible surface is cut, so the next request derives its context
10
+ * from the target message onward. The marker is a `user/message` carrying a
11
+ * replace `surfaceOp` and the producer-owned source `{ kind: 'dsh-rewind' }`
12
+ * (see `marker.ts`); the shape's invariants and history are documented in
13
+ * `docs/architecture.md`.
27
14
  *
28
15
  * @module dsh-rewind/rewind
29
16
  */
30
17
  import type { SessionEvent, UserMessage } from '@deepseek-ai/dsh-session';
31
- export { REWIND_MARKER_KIND, REWIND_MARKER_SOURCE, isRewindMarker } from './marker.ts';
18
+ export { REWIND_MARKER_SOURCE } from './marker.ts';
32
19
  /** Which of the two rewind modes a rewind executes. */
33
20
  export type RewindMode = 'chat' | 'both';
34
21
  /**
@@ -101,7 +88,9 @@ export declare function isHumanUserMessageEvent(event: SessionEvent): event is S
101
88
  /** Join the text blocks of a message into one plain string. */
102
89
  export declare function messagePreview(message: UserMessage): string;
103
90
  /**
104
- * Parse a raw command token into a rewind target.
91
+ * Parse a raw command token into a rewind target. Both forms must be plain
92
+ * decimal integers: a bare `Number()` would also accept `@1e3`, `@0x10` and
93
+ * `@+5`, and would read a bare `@` as seq 0.
105
94
  * @param raw - one token: `@123` (absolute seq) or `12` (recency index).
106
95
  * @returns the parsed target, or undefined when the token is malformed.
107
96
  */
@@ -20,33 +20,9 @@
20
20
  * - Because entries live on disk under the dsh data directory, they survive a
21
21
  * host restart, are bounded (the newest 100 anchor groups per session are
22
22
  * kept), and restores read/write the real file system with plain `node:fs`
23
- * — independent of the fs service.
24
- *
25
- * Security note: this `node:fs` authority is the DSH host authority every host
26
- * plugin holds — the model-facing fences constrain the model's tools, not this
27
- * code. The store stays bounded to the model-touched paths, so excluding a
28
- * file (e.g. `.env`) is a model-permission concern (see `SECURITY.md`).
29
- *
30
- * Crash safety (this module's own engineering asset):
31
- * - Checkpoint commits are ATOMIC: the sidecar is fully written first, then
32
- * the entry JSON is written to a sibling temp file and renamed over the
33
- * target, so a host crash mid-write can never leave a readable entry
34
- * without its bytes — at worst an unreferenced orphan sidecar, or an inert
35
- * `.tmp` leftover that the next commit of the same file overwrites and that
36
- * no reader ever picks up.
37
- * - Every restore pass is JOURNALED. Before mutating anything the store
38
- * captures the pre-restore ("rescue") state of each planned path as a raw
39
- * byte copy and persists an intent journal (`journal-<op>.json` in the
40
- * session dir) holding only references, then marks each action done as it
41
- * is applied. A crash at any point leaves the journal on disk; after a host
42
- * restart `reconcileRestores(sessionId)` re-derives from the REAL disk which
43
- * paths already match the target and which are still pending (reporting
44
- * "restored up to where, what changed"), auto-heals journals whose goal is
45
- * already reached, and `continueRestore` / `rollbackRestore` finish the
46
- * interrupted op or undo it back to the exact pre-restore state.
47
- * - Journal IO is best-effort and never fails a restore: if the journal
48
- * cannot be written the restore proceeds exactly like the pre-journal code
49
- * (crash safety degrades, behavior does not).
23
+ * — independent of the fs service. The atomic-commit and journaled-restore
24
+ * guarantees, the security model and the in-store layout are specified in
25
+ * `SECURITY.md` and `docs/format.md`.
50
26
  *
51
27
  * Restore semantics (identical to Claude Code): for every path with entries
52
28
  * anchored at or after the target message, apply the EARLIEST entry — write
@@ -54,13 +30,8 @@
54
30
  * creation. Symlinked and hard-linked paths are skipped and reported, never
55
31
  * written through.
56
32
  *
57
- * Format compatibility: entries written before this module stored bytes
58
- * (released v1: `{callId, anchorSeq, path, before: string | null}` plus the
59
- * `restore-journal-` prefix) are still READ — their string content is the
60
- * exact UTF-8 bytes it always was, except for records that were decoded
61
- * lossily (they contain U+FFFD: comparable, but never written back). New
62
- * writes are always the byte format; the marker contract that keeps a
63
- * downgraded v1 build from touching the workspace lives in
33
+ * Released-v1 entries stay readable and the byte format is specified in
34
+ * `docs/format.md`; the downgrade marker contract lives in
64
35
  * `tests/downgrade-safety.test.ts`.
65
36
  *
66
37
  * @module dsh-rewind/snapshot
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-rewind-plugin",
3
- "version": "0.14.0",
3
+ "version": "0.15.0-beta.2",
4
4
  "description": "In-window conversation rewind with workspace file restore · 同窗口内对话回退并可还原工作区文件",
5
5
  "icon": "./assets/icon.svg",
6
6
  "keywords": [
@@ -19,7 +19,8 @@
19
19
  "homepage": "https://github.com/SiriLee/dsh-rewind#readme",
20
20
  "license": "MIT",
21
21
  "engines": {
22
- "node": "^22.19.0 || >=24.0.0"
22
+ "node": "^22.19.0 || >=24.0.0",
23
+ "dsh": ">=0.2.0-rc.1"
23
24
  },
24
25
  "type": "module",
25
26
  "main": "lib/index.js",
@@ -52,9 +53,6 @@
52
53
  "bundle": {
53
54
  "patch": "./cordis.patch.yml"
54
55
  },
55
- "engines": {
56
- "dsh": ">=0.1.7-rc.2"
57
- },
58
56
  "client": {
59
57
  "inject": [
60
58
  "@deepseek-ai/dsh-client-locale",
@@ -70,7 +68,7 @@
70
68
  "typecheck": "tsc --noEmit -p tsconfig.json && tsc --noEmit -p tsconfig.client.json && tsc --noEmit -p tsconfig.client-test.json",
71
69
  "test": "vitest run",
72
70
  "verify:host": "node scripts/verify-host.mjs",
73
- "check": "npm run typecheck && npm test && npm run build && node scripts/verify-host.mjs && npm pack --dry-run",
71
+ "check": "npm run typecheck && npm run build && npm test && node scripts/verify-host.mjs && npm pack --dry-run",
74
72
  "prepare": "npm run build"
75
73
  },
76
74
  "publishConfig": {
@@ -78,20 +76,20 @@
78
76
  },
79
77
  "peerDependencies": {
80
78
  "@deepseek-ai/cordis": "^4.0.1",
81
- "@deepseek-ai/dsh-client-locale": "^0.1.7-rc.2",
82
- "@deepseek-ai/dsh-client-ui-commands": "^0.1.7-rc.2",
83
- "@deepseek-ai/dsh-client-ui-plugin-manager": "^0.1.7-rc.2",
84
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.7-rc.2",
85
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.7-rc.2",
86
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.7-rc.2",
87
- "@deepseek-ai/dsh-commands": "^0.1.7-rc.2",
88
- "@deepseek-ai/dsh-fs": "^0.1.7-rc.2",
89
- "@deepseek-ai/dsh-home-paths": "^0.1.7-rc.2",
90
- "@deepseek-ai/dsh-llm": "^0.1.7-rc.2",
91
- "@deepseek-ai/dsh-sandbox": "^0.1.7-rc.2",
92
- "@deepseek-ai/dsh-session": "^0.1.7-rc.2",
93
- "@deepseek-ai/dsh-settings": "^0.1.7-rc.2",
94
- "@deepseek-ai/dsh-tools": "^0.1.7-rc.2",
79
+ "@deepseek-ai/dsh-client-locale": "^0.2.0-rc.1",
80
+ "@deepseek-ai/dsh-client-ui-commands": "^0.2.0-rc.1",
81
+ "@deepseek-ai/dsh-client-ui-plugin-manager": "^0.2.0-rc.1",
82
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.2.0-rc.1",
83
+ "@deepseek-ai/dsh-client-ui-settings": "^0.2.0-rc.1",
84
+ "@deepseek-ai/dsh-client-ui-slots": "^0.2.0-rc.1",
85
+ "@deepseek-ai/dsh-commands": "^0.2.0-rc.1",
86
+ "@deepseek-ai/dsh-fs": "^0.2.0-rc.1",
87
+ "@deepseek-ai/dsh-home-paths": "^0.2.0-rc.1",
88
+ "@deepseek-ai/dsh-llm": "^0.2.0-rc.1",
89
+ "@deepseek-ai/dsh-sandbox": "^0.2.0-rc.1",
90
+ "@deepseek-ai/dsh-session": "^0.2.0-rc.1",
91
+ "@deepseek-ai/dsh-settings": "^0.2.0-rc.1",
92
+ "@deepseek-ai/dsh-tools": "^0.2.0-rc.1",
95
93
  "@deepseek-ai/schemastery": "^3.18.4"
96
94
  },
97
95
  "peerDependenciesMeta": {
@@ -147,41 +145,41 @@
147
145
  "devDependencies": {
148
146
  "@deepseek-ai/cordis": "4.0.3",
149
147
  "@deepseek-ai/cosmokit": "1.8.4",
150
- "@deepseek-ai/dsh-agent": "0.1.7-rc.2",
151
- "@deepseek-ai/dsh-api-session-controller": "0.1.7-rc.2",
152
- "@deepseek-ai/dsh-attachment": "0.1.7-rc.2",
153
- "@deepseek-ai/dsh-client-connection": "0.1.7-rc.2",
154
- "@deepseek-ai/dsh-client-locale": "0.1.7-rc.2",
155
- "@deepseek-ai/dsh-client-store": "0.1.7-rc.2",
156
- "@deepseek-ai/dsh-client-ui-commands": "0.1.7-rc.2",
157
- "@deepseek-ai/dsh-client-ui-conversation": "0.1.7-rc.2",
158
- "@deepseek-ai/dsh-client-ui-input-trigger": "0.1.7-rc.2",
159
- "@deepseek-ai/dsh-client-ui-plugin-manager": "0.1.7-rc.2",
160
- "@deepseek-ai/dsh-client-ui-primitives": "0.1.7-rc.2",
161
- "@deepseek-ai/dsh-client-ui-session": "0.1.7-rc.2",
162
- "@deepseek-ai/dsh-client-ui-settings": "0.1.7-rc.2",
163
- "@deepseek-ai/dsh-client-ui-slots": "0.1.7-rc.2",
164
- "@deepseek-ai/dsh-command-compact": "0.1.7-rc.2",
165
- "@deepseek-ai/dsh-commands": "0.1.7-rc.2",
166
- "@deepseek-ai/dsh-compaction": "0.1.7-rc.2",
167
- "@deepseek-ai/dsh-compaction-basic": "0.1.7-rc.2",
168
- "@deepseek-ai/dsh-compaction-image-offload": "0.1.7-rc.2",
169
- "@deepseek-ai/dsh-fs": "0.1.7-rc.2",
170
- "@deepseek-ai/dsh-goal": "0.1.7-rc.2",
171
- "@deepseek-ai/dsh-home-paths": "0.1.7-rc.2",
172
- "@deepseek-ai/dsh-llm": "0.1.7-rc.2",
173
- "@deepseek-ai/dsh-plan-mode": "0.1.7-rc.2",
174
- "@deepseek-ai/dsh-sandbox": "0.1.7-rc.2",
175
- "@deepseek-ai/dsh-scope": "0.1.7-rc.2",
176
- "@deepseek-ai/dsh-session": "0.1.7-rc.2",
177
- "@deepseek-ai/dsh-session-projection": "0.1.7-rc.2",
178
- "@deepseek-ai/dsh-session-stats": "0.1.7-rc.2",
179
- "@deepseek-ai/dsh-session-title": "0.1.7-rc.2",
180
- "@deepseek-ai/dsh-settings": "0.1.7-rc.2",
181
- "@deepseek-ai/dsh-token-meter": "0.1.7-rc.2",
182
- "@deepseek-ai/dsh-tools": "0.1.7-rc.2",
183
- "@deepseek-ai/dsh-typert-protocol": "0.1.7-rc.2",
184
- "@deepseek-ai/dsh-user-questions": "0.1.7-rc.2",
148
+ "@deepseek-ai/dsh-agent": "0.2.0-rc.2",
149
+ "@deepseek-ai/dsh-api-session-controller": "0.2.0-rc.2",
150
+ "@deepseek-ai/dsh-attachment": "0.2.0-rc.2",
151
+ "@deepseek-ai/dsh-client-connection": "0.2.0-rc.2",
152
+ "@deepseek-ai/dsh-client-locale": "0.2.0-rc.2",
153
+ "@deepseek-ai/dsh-client-store": "0.2.0-rc.2",
154
+ "@deepseek-ai/dsh-client-ui-commands": "0.2.0-rc.2",
155
+ "@deepseek-ai/dsh-client-ui-conversation": "0.2.0-rc.2",
156
+ "@deepseek-ai/dsh-client-ui-input-trigger": "0.2.0-rc.2",
157
+ "@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.0-rc.2",
158
+ "@deepseek-ai/dsh-client-ui-primitives": "0.2.0-rc.2",
159
+ "@deepseek-ai/dsh-client-ui-session": "0.2.0-rc.2",
160
+ "@deepseek-ai/dsh-client-ui-settings": "0.2.0-rc.2",
161
+ "@deepseek-ai/dsh-client-ui-slots": "0.2.0-rc.2",
162
+ "@deepseek-ai/dsh-command-compact": "0.2.0-rc.2",
163
+ "@deepseek-ai/dsh-commands": "0.2.0-rc.2",
164
+ "@deepseek-ai/dsh-compaction": "0.2.0-rc.2",
165
+ "@deepseek-ai/dsh-compaction-basic": "0.2.0-rc.2",
166
+ "@deepseek-ai/dsh-compaction-image-offload": "0.2.0-rc.2",
167
+ "@deepseek-ai/dsh-fs": "0.2.0-rc.2",
168
+ "@deepseek-ai/dsh-goal": "0.2.0-rc.2",
169
+ "@deepseek-ai/dsh-home-paths": "0.2.0-rc.2",
170
+ "@deepseek-ai/dsh-llm": "0.2.0-rc.2",
171
+ "@deepseek-ai/dsh-plan-mode": "0.2.0-rc.2",
172
+ "@deepseek-ai/dsh-sandbox": "0.2.0-rc.2",
173
+ "@deepseek-ai/dsh-scope": "0.2.0-rc.2",
174
+ "@deepseek-ai/dsh-session": "0.2.0-rc.2",
175
+ "@deepseek-ai/dsh-session-projection": "0.2.0-rc.2",
176
+ "@deepseek-ai/dsh-session-stats": "0.2.0-rc.2",
177
+ "@deepseek-ai/dsh-session-title": "0.2.0-rc.2",
178
+ "@deepseek-ai/dsh-settings": "0.2.0-rc.2",
179
+ "@deepseek-ai/dsh-token-meter": "0.2.0-rc.2",
180
+ "@deepseek-ai/dsh-tools": "0.2.0-rc.2",
181
+ "@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.2",
182
+ "@deepseek-ai/dsh-user-questions": "0.2.0-rc.2",
185
183
  "@deepseek-ai/schemastery": "3.18.4",
186
184
  "@types/node": "^24.0.0",
187
185
  "@types/react": "^18.3.31",