dsh-rewind-plugin 0.7.5 → 0.9.0-alpha.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.
@@ -5,7 +5,17 @@
5
5
  *
6
6
  * @module dsh-rewind/client/hidden
7
7
  */
8
- import type { ChatConversationViewNode, CommandNode } from '@deepseek-ai/dsh-client-runtime/client';
8
+ import type { CommandNode } from '@deepseek-ai/dsh-client-ui-conversation/client';
9
+ /** A chat-snapshot view node as the hiding / composer-refill logic reads it.
10
+ * The harness's view node shape plus the `anchorSeq` the chat snapshot carries
11
+ * (the plugin reads `anchorSeq` off each node; it is not declared on the
12
+ * harness's `ConversationViewNode`). */
13
+ export interface ChatConversationViewNode {
14
+ readonly key: string;
15
+ readonly kind?: string;
16
+ readonly data?: unknown;
17
+ readonly anchorSeq: number;
18
+ }
9
19
  /** Minimal chat snapshot reader the hiding logic needs. */
10
20
  export interface HiddenChat {
11
21
  readonly order: readonly string[];
@@ -14,55 +24,36 @@ export interface HiddenChat {
14
24
  };
15
25
  }
16
26
  /**
17
- * Reader for one session's live chat snapshot. This is one of the plugin's
18
- * "0.1.1-rc.2 ↔ 0.1.2-rc.1" dual channels: 0.1.1-rc.2 serves the chat from the
19
- * session face snapshot, while 0.1.2-rc.1 (and the whole 0.1.2 line) serves it
20
- * from the `uiConversation` service's named "chat" view (contributed by
21
- * dsh-client-ui-chat through the uiSession slot hook). See
22
- * SiriLee/dsh-rewind#7.
27
+ * Reader for one session's live chat snapshot. On the 0.1.2-rc.1 line (the
28
+ * plugin's single baseline) the chat is served by the `uiConversation`
29
+ * service's named "chat" view (contributed by dsh-client-ui-chat through the
30
+ * uiSession slot hook).
23
31
  */
24
32
  export type ChatOf = (session: {
25
33
  readonly sessionId: string;
26
- getSnapshot(): {
27
- chat?: unknown;
28
- };
29
34
  } | undefined) => HiddenChat | undefined;
30
35
  /**
31
36
  * Subscribe to one session's live chat-update signal, for waiting on a chat
32
- * snapshot change without polling. Dual-channel, mirroring `chatOf`: the
33
- * 0.1.2-rc.1 `uiConversation` "chat" view when registered, else the session
34
- * face (0.1.1-rc.2, whose snapshot still carries `chat`, so its own
35
- * `subscribe` is the chat-update signal). `cb` fires whenever the chat
36
- * snapshot invalidates.
37
+ * snapshot change without polling. The 0.1.2-rc.1 `uiConversation` "chat"
38
+ * view's own `subscribe` is the chat-update signal; `cb` fires whenever the
39
+ * chat snapshot invalidates.
37
40
  */
38
41
  export type ChatWatch = (sessionId: string, cb: () => void) => () => void;
39
42
  /**
40
- * Choose the chat-update subscription for `waitForCommand`: prefer the
41
- * `uiConversation` "chat" view's own `subscribe` when available (0.1.2-rc.1,
42
- * where the chat-update signal lives), else the session face's `subscribe`
43
- * (0.1.1-rc.2, whose snapshot still carries the chat, so its own subscribe is
44
- * the chat-update signal). Extracted as a pure channel-selection step so the
45
- * "view vs face" branch is unit-testable; the resolvers are injected by the
46
- * caller (see `watchChat` in index.ts). Never throws.
43
+ * Choose the chat-update subscription for `waitForCommand`: the
44
+ * `uiConversation` "chat" view's own `subscribe`. Extracted as a pure
45
+ * channel-selection step so the resolver is unit-testable; the resolver is
46
+ * injected by the caller (see `watchChat` in index.ts). Never throws.
47
47
  */
48
48
  export declare function resolveChatWatch(resolveView: (sessionId: string) => {
49
49
  subscribe?(cb: () => void): () => void;
50
- } | undefined, resolveFace: (sessionId: string) => {
51
- subscribe(cb: () => void): () => void;
52
50
  } | undefined, sessionId: string, cb: () => void): () => void;
53
51
  /**
54
- * Resolve the chat snapshot across the two harness channels: the session-face
55
- * snapshot first (0.1.1-rc.2 — on 0.1.2-rc.1 the face no longer carries
56
- * `chat`, so the field reads `undefined`), then the `uiConversation` "chat"
57
- * view. The view's `getSnapshot()` returns undefined until the named view is
58
- * registered, so both channels missing degrades to `undefined` (no targets, no
59
- * hiding — never a crash).
52
+ * Resolve the chat snapshot from the `uiConversation` "chat" view. The view's
53
+ * `getSnapshot()` returns undefined until the named view is registered, which
54
+ * degrades to `undefined` (no targets, no hiding — never a crash).
60
55
  */
61
- export declare function chatSnapshotOf(face: {
62
- getSnapshot(): {
63
- chat?: unknown;
64
- };
65
- } | undefined, chatView: {
56
+ export declare function chatSnapshotOf(chatView: {
66
57
  getSnapshot(): unknown;
67
58
  } | undefined): HiddenChat | undefined;
68
59
  /**
@@ -26,9 +26,27 @@
26
26
  *
27
27
  * @module dsh-rewind/client
28
28
  */
29
- import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
29
+ import type { ISessions } from '@deepseek-ai/dsh-api-session-controller/client';
30
30
  export declare const name = "dsh-rewind";
31
31
  export declare const inject: string[];
32
+ /**
33
+ * The client plugin root context read by `apply(ctx)`. Local structural face:
34
+ * the harness client context is a cordis `Context` augmented by runtime
35
+ * services, so the plugin declares the subset it reads. `sessions` is the real
36
+ * `ISessions` from `@deepseek-ai/dsh-api-session-controller`.
37
+ */
38
+ export interface ClientContext {
39
+ effect(execute: () => Iterable<unknown>, label?: string): unknown;
40
+ locale: {
41
+ register(namespace: string, messages: Record<string, Record<string, string>>): unknown;
42
+ bind(namespace: string): (key: string) => string;
43
+ subscribe(cb: () => void): () => void;
44
+ };
45
+ sessions: ISessions;
46
+ get(name: string): unknown;
47
+ slots: unknown;
48
+ commandUi: unknown;
49
+ }
32
50
  /**
33
51
  * Client plugin body: command decoration + parameterized guard + locale + the
34
52
  * portal bridge.
@@ -13,8 +13,8 @@
13
13
  *
14
14
  * @module dsh-rewind/client/popover
15
15
  */
16
- import type { SessionFace } from '@deepseek-ai/dsh-client-runtime/client';
17
- import type { CommandNode } from '@deepseek-ai/dsh-client-runtime/client';
16
+ import type { SessionFace } from '@deepseek-ai/dsh-api-session-controller/client';
17
+ import type { CommandNode } from '@deepseek-ai/dsh-client-ui-conversation/client';
18
18
  import { type ChatOf, type ChatWatch } from './hidden.ts';
19
19
  import type { RewindKey } from './locales.ts';
20
20
  type Translate = (key: RewindKey, params?: Record<string, unknown>) => string;
@@ -33,16 +33,15 @@ export interface PopoverOptions {
33
33
  readonly onRetract?: () => void;
34
34
  readonly preview: string;
35
35
  /**
36
- * Dual-channel chat reader (0.1.1-rc.2 session face / 0.1.2-rc.1
37
- * uiConversation view): the durable variant's command probes scan the chat
38
- * through it. Unused by the pending-retract variant.
36
+ * Chat reader on the 0.1.2-rc.1 `uiConversation` "chat" view: the durable
37
+ * variant's command probes scan the chat through it. Unused by the
38
+ * pending-retract variant.
39
39
  */
40
40
  readonly chatOf: ChatOf;
41
41
  /**
42
42
  * Subscribe to one session's live chat-update signal, so a probe waiting on
43
- * a command's chat node can be woken when the chat snapshot changes (0.1.2-rc.1
44
- * the session face no longer fires on a chat update). Passed straight through
45
- * to `waitForCommand`.
43
+ * a command's chat node can be woken when the chat snapshot changes. Passed
44
+ * straight through to `waitForCommand`.
46
45
  */
47
46
  readonly watchChat: ChatWatch;
48
47
  /** The button that opened the popover (outside-click ignore target). */
@@ -70,7 +69,7 @@ export declare function knownCommandSeqs(session: SessionFace, chatOf: ChatOf, m
70
69
  * session snapshot (command/run + command/done land as one CommandNode).
71
70
  * @returns the outcome text-bearing node, or null on timeout.
72
71
  */
73
- export declare function waitForCommand(session: SessionFace, chatOf: ChatOf, match: (node: CommandNode) => boolean, timeoutMs?: number, watch?: (cb: () => void) => () => void): Promise<{
72
+ export declare function waitForCommand(session: SessionFace, chatOf: ChatOf, match: (node: CommandNode) => boolean, timeoutMs: number | undefined, watch: (cb: () => void) => () => void): Promise<{
74
73
  kind: 'success' | 'error';
75
74
  text?: string;
76
75
  } | null>;
@@ -23,7 +23,7 @@
23
23
  * @module dsh-rewind/client/portals
24
24
  */
25
25
  import { type ReactNode } from 'react';
26
- import type { SessionFace } from '@deepseek-ai/dsh-client-runtime/client';
26
+ import type { SessionFace } from '@deepseek-ai/dsh-api-session-controller/client';
27
27
  import { type ChatOf, type ChatWatch, type HiddenChat } from './hidden.ts';
28
28
  import type { RewindKey } from './locales.ts';
29
29
  type Translate = (key: RewindKey, params?: Record<string, unknown>) => string;
@@ -53,26 +53,23 @@ export type PortalTarget = {
53
53
  export interface RewindBridgeDeps {
54
54
  readonly sessionOf: (sessionId: string) => SessionFace | undefined;
55
55
  /**
56
- * Dual-channel chat reader (0.1.1-rc.2 session face / 0.1.2-rc.1
57
- * uiConversation "chat" view): every chat snapshot read goes through it. See
58
- * `chatSnapshotOf` in hidden.ts for the channel precedence.
56
+ * Chat reader on the 0.1.2-rc.1 `uiConversation` "chat" view: every chat
57
+ * snapshot read goes through it. See `chatSnapshotOf` in hidden.ts.
59
58
  */
60
59
  readonly chatOf: ChatOf;
61
60
  /**
62
61
  * Subscribe to one session's live chat-update signal, so the composer refill
63
62
  * waiting on an executed rewind's chat node can be woken when the chat
64
- * snapshot changes (0.1.2-rc.1 the session face no longer fires on a chat
65
- * update). Passed through to `waitForCommand`.
63
+ * snapshot changes. Passed through to `waitForCommand`.
66
64
  */
67
65
  readonly watchChat: ChatWatch;
68
66
  readonly currentSessionId: () => string | undefined;
69
67
  readonly t: Translate;
70
68
  readonly subscribeLocale: (cb: () => void) => () => void;
71
69
  /**
72
- * Session-aware dual-channel composer writer (see `writeComposer`): the
73
- * 0.1.2-rc.1 `conversation.input` facade `setDraft` when reachable, else the
74
- * 0.1.1-rc.2/0.1.2-rc.1 DOM fill. Session-scoped so the refill only lands in
75
- * the session that just rewound.
70
+ * Session-aware composer writer (see `writeComposer`): the 0.1.2-rc.1
71
+ * `conversation.input` facade `setDraft` when reachable, else the DOM fill.
72
+ * Session-scoped so the refill only lands in the session that just rewound.
76
73
  */
77
74
  readonly setComposerText: (sessionId: string, text: string) => boolean;
78
75
  }
@@ -99,20 +96,18 @@ interface ComposerDraftWriter {
99
96
  setDraft(text: string): void;
100
97
  }
101
98
  /**
102
- * Fill the dsh composer with `text`. DOM dual-channel fallback: the 0.1.1-rc.2
103
- * `<textarea>` path, then the 0.1.2-rc.1 `contenteditable` path. Used by
104
- * `setComposerText` (the harness-facade-aware writer) as the last-resort and
105
- * by `runRewindAndFill` to put the withdrawn target message back into the
106
- * composer after a rewind. Best-effort — no composer match means false,
107
- * never a throw.
99
+ * Fill the dsh composer with `text` through the 0.1.2-rc.1 `contenteditable`
100
+ * DOM path. Used by `setComposerText` (the harness-facade-aware writer) as the
101
+ * last-resort and by `runRewindAndFill` to put the withdrawn target message
102
+ * back into the composer after a rewind. Best-effort — no composer match means
103
+ * false, never a throw.
108
104
  */
109
105
  export declare function fillComposer(text: string): boolean;
110
106
  /**
111
- * Dual-channel composer write, mirroring `chatSnapshotOf`: prefer the harness
112
- * facade's `setDraft` (0.1.2-rc.1, correct whole-draft replace), then degrade
113
- * to the DOM `fillComposer` (0.1.1-rc.2 textarea / 0.1.2-rc.1 contenteditable).
114
- * A facade that throws (session teardown) is treated as absent so the DOM path
115
- * still restores the text. Never throws.
107
+ * Composer write: prefer the harness facade's `setDraft` (0.1.2-rc.1, correct
108
+ * whole-draft replace), then degrade to the DOM `fillComposer` (0.1.2-rc.1
109
+ * contenteditable). A facade that throws (session teardown) is treated as
110
+ * absent so the DOM path still restores the text. Never throws.
116
111
  * @param text - the withdrawn target message text.
117
112
  * @param facade - the 0.1.2-rc.1 session input draft writer, when reachable.
118
113
  * @returns whether a channel applied the text.
@@ -136,24 +131,15 @@ export declare function runRewindAndFill(session: SessionFace, seq: number, mode
136
131
  * Locate the actions container of a user/steering seat row — the element the
137
132
  * ↶ button portals into (the copy/branch IconActions row).
138
133
  *
139
- * @dualmode 0.1.1-rc.2 ↔ 0.1.2-rc.1
140
- * - 0.1.1-rc.2: the hover-reveal root is attribute-marked
141
- * `[data-time-hover-root]` and mounts the actions row as its LAST child. A
142
- * pending row IS that root; a durable row CONTAINS it as a descendant.
143
- * - 0.1.2-rc.1: the marker was removed from user rows (it now lives only on the
144
- * per-turn tail footer, `TurnTailNodeView`), and the user action row is
145
- * revealed via CSS `:has()`. The container is instead located structurally:
146
- * the direct holder of the copy `<button>` (the `MessageIconActions`
147
- * container, which mounts that button as a direct child —
148
- * `MessageIconActions.tsx:83,86`).
149
- *
150
- * The 0.1.2-alpha.2/3 `[data-actions-reveal]` attribute-marked rows (see
151
- * `ACTIONS_ROOT_SELECTOR`) are an intermediate state absorbed by the
152
- * attribute branch below; they are not needed by either endpoint.
134
+ * On the 0.1.2-rc.1 line the `data-time-hover-root` marker lives only on the
135
+ * per-turn tail footer (`TurnTailNodeView`), and the user action row is
136
+ * revealed via CSS `:has()`. The container is located structurally: the direct
137
+ * holder of the copy `<button>` (the `MessageIconActions` container, which
138
+ * mounts that button as a direct child — `MessageIconActions.tsx:83,86`).
153
139
  *
154
140
  * Returns undefined when no qualifying container is found; the caller refuses
155
141
  * to portal (never a crash, never a wrong attachment). Exported as a test seam
156
- * (see `collectTargets`) so the dual-channel finder is exercised directly for
142
+ * (see `collectTargets`) so the row-shape finder is exercised directly for
157
143
  * both durable and pending row shapes without a full React portal render.
158
144
  */
159
145
  export declare function actionsContainerOf(row: HTMLElement | undefined): HTMLElement | undefined;
@@ -174,10 +160,9 @@ interface RewindPortalsProps extends RewindBridgeDeps {
174
160
  * synchronous full-transcript scan inside a commit microtask.
175
161
  */
176
162
  export declare function RewindPortals({ sessionId, sessionOf, chatOf, currentSessionId, watchChat, t, subscribeLocale, setComposerText }: RewindPortalsProps): ReactNode;
177
- /** The current composer draft, on whichever channel: 0.1.1-rc.2 textarea
178
- * `.value`, 0.1.2-rc.1 contenteditable `textContent`. Empty when the composer
179
- * is absent. Exported as a test seam (the empty-composer guard in
180
- * `retractPending`). */
163
+ /** The current composer draft: the 0.1.2-rc.1 contenteditable `textContent`.
164
+ * Empty when the composer is absent. Exported as a test seam (the
165
+ * empty-composer guard in `retractPending`). */
181
166
  export declare function composerText(): string;
182
167
  /**
183
168
  * Build the slot-entry component for the plugin apply(): a tiny bridge that
@@ -12,10 +12,9 @@
12
12
  *
13
13
  * It neither imports the client settings typed contract nor depends on the
14
14
  * 0.1.2-rc.1-only `mutate` write API: it reads `getSnapshot().value` and writes
15
- * via the `set(field, value)` method present on both 0.1.1-rc.2 and 0.1.2-rc.1,
16
- * and the card receives a tiny structural `CleanupCardApi` supplied by
17
- * `src/client/index.ts` so the component stays harness-agnostic and
18
- * unit-testable in isolation.
15
+ * via the `set(field, value)` method, and the card receives a tiny structural
16
+ * `CleanupCardApi` supplied by `src/client/index.ts` so the component stays
17
+ * harness-agnostic and unit-testable in isolation.
19
18
  *
20
19
  * @module dsh-rewind/client/settings-card
21
20
  */
@@ -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/* Dual-channel note: the harness card corner/border drifted across versions \u2014\n 0.1.1-rc.2 uses border-radius 12px + 1px solid border-l2, while 0.1.2-rc.1\n uses 16px + 0.5px border-l4 (and adds corner-shape: round). We intentionally\n keep the 0.1.1-rc.2 values here for now; if the 0.1.2-rc.1 look is wanted,\n switch these on the host version like the other dual-channel surfaces (see the\n host-version probe in src/client/index.ts). */\n.dsh-rewind-cleanup-card {\n list-style: none;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 12px;\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: 1px 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: 1px 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.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 background: var(--dsw-alias-border-l3);\n cursor: pointer;\n}\n.dsh-rewind-cleanup-switch-on {\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-on .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: 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.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 background: var(--dsw-alias-border-l3);\n cursor: pointer;\n}\n.dsh-rewind-cleanup-switch-on {\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-on .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";
@@ -4,11 +4,17 @@
4
4
  * lives in `src/client/`).
5
5
  *
6
6
  * Rewind mechanism: planning is pure (`src/rewind.ts`); execution appends a
7
- * marker `assistant/message` into the session log whose `surfaceOp` replaces
8
- * every surface node after the target message with the marker. The
9
- * append-only log (and the rendered transcript) is untouched — only the
10
- * model-visible surface is cut, so the next request derives its context from
11
- * the target onward.
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 an EMPTY `user/message`: v2 reserves surface `replace` to a
12
+ * node that cites every shadowed seq (`sourceEventSeqs`), and
13
+ * `assistant/message` can no longer carry those — so the replacement node is
14
+ * a `user/message`, exactly as /compact's checkpoint is. An empty
15
+ * `user/message` derives to itself (a present-but-empty user turn), so the
16
+ * marker stays as the surface-tail cut point rather than vanishing.
17
+ *
12
18
  *
13
19
  * File restore (mode `both`) follows Claude Code's checkpointing: the plugin
14
20
  * backs up each tracked write-class edit BEFORE it happens (at the
@@ -69,6 +69,25 @@ export declare const en: {
69
69
  'cleanup.clearCancelled': string;
70
70
  'cleanup.clearFailed': string;
71
71
  'cleanup.usage': string;
72
+ 'rewindfix.description': string;
73
+ 'rewindfix.inputHint': string;
74
+ 'rewindfix.dryRun': string;
75
+ 'rewindfix.done': string;
76
+ 'rewindfix.cancelled': string;
77
+ 'rewindfix.launcherGuard': string;
78
+ 'rewindfix.counts': string;
79
+ 'rewindfix.writeOk': string;
80
+ 'rewindfix.skip.loaded': string;
81
+ 'rewindfix.skip.noMarkers': string;
82
+ 'rewindfix.skip.noArtifact': string;
83
+ 'rewindfix.skip.loadedNow': string;
84
+ 'rewindfix.fail.unreadable': string;
85
+ 'rewindfix.fail.repair': string;
86
+ 'rewindfix.fail.locked': string;
87
+ 'rewindfix.fail.generic': string;
88
+ 'rewindfix.persistenceUnavailable': string;
89
+ 'rewindfix.sessionStoreUnavailable': string;
90
+ 'rewindfix.locked': string;
72
91
  };
73
92
  /** The host rewind dictionary key union. */
74
93
  export type HostKey = keyof typeof en;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * `/dsh-rewind-fix` — orchestration layer.
3
+ *
4
+ * This module owns the COMMAND semantics and the per-session repair pipeline. It
5
+ * intentionally does NOT re-implement the physical codec (`./session-log-io.ts`)
6
+ * or the A/B→C transform (`./rewind-marker-repair.ts`); it wires those against
7
+ * the harness's own session services (read path = `sessionPersistence` +
8
+ * `ctx.sessions`; write path = self-contained 2-frame zstd rewrite + atomic
9
+ * tmp+rename, because the persistence layer is append-only & repairing a log
10
+ * must rewrite non-tail events).
11
+ *
12
+ * Safety model (settled in the design):
13
+ * - Only NON-loaded (closed) sessions are repaired. "Loaded" = the harness has
14
+ * the session's in-memory `this.log`, which is the seq authority — rewriting
15
+ * that session's file would fork it.
16
+ * - Per-session `.lock` (O_EXCL) guards against two tabs/processes fixing the
17
+ * same closed session concurrently.
18
+ * - Each repair is backed up and rolled back on write/verify failure; an
19
+ * interrupted run is idempotent (already-C / marker-free sessions are no-ops).
20
+ * - Repaired sessions have their snapshots cleared via `store.clearSession`.
21
+ *
22
+ * There is no streaming progress channel (`CommandResult` returns once), so the
23
+ * command is "dry wait" like `/compact`: the client is told it takes minutes and
24
+ * a single final summary is returned via `command/done`.
25
+ *
26
+ * {@link runRewindFix} is the DOMAIN core: it returns structured data (not a
27
+ * rendered string) over a narrow deps interface, so it is unit-testable without a
28
+ * real cordis `Context` and stays locale-agnostic. `renderRewindFixReport`
29
+ * renders that data through the plugin's locale translator `t`, and
30
+ * `registerRewindFix` adapts a real `Context` + `SnapshotStore` + renderer and
31
+ * registers the command.
32
+ */
33
+ import type { Context } from '@deepseek-ai/cordis';
34
+ import type { SessionHeader, SessionId } from '@deepseek-ai/dsh-session';
35
+ /** A locale renderer: dictionary key + optional `{name}` params → text. */
36
+ export type RenderFn = (key: string, params?: Record<string, string | number>) => string;
37
+ /** Structural view of the snapshot store the repair needs (`clearSession` only). */
38
+ interface SnapshotStoreLike {
39
+ clearSession(sessionId: string): Promise<unknown>;
40
+ }
41
+ /** The narrow dependency set {@link runRewindFix} needs (injected; unit-testable). */
42
+ export interface RewindFixDeps {
43
+ listSnapshots(signal?: AbortSignal): Promise<Array<{
44
+ header: SessionHeader;
45
+ }>>;
46
+ readRaw(id: SessionId, signal?: AbortSignal): Promise<{
47
+ content: string;
48
+ } | undefined>;
49
+ locate(header: SessionHeader): {
50
+ path: string;
51
+ } | undefined;
52
+ isSessionLoaded(id: SessionId): boolean;
53
+ clearSession(id: string): Promise<unknown>;
54
+ }
55
+ export interface RewindFixOptions {
56
+ /** `--apply` executes the repair; otherwise dry run (no writes). */
57
+ apply: boolean;
58
+ /** The launcher session itself needs repair (guard warning). */
59
+ launcherHasMarkers: boolean;
60
+ signal?: AbortSignal;
61
+ /**
62
+ * Max sessions processed concurrently. Sessions are independent files, so a
63
+ * bounded pool overlaps zstd decode/encode across them (the dominant cost).
64
+ * Default 4; capped by the session count. Higher means more peak memory.
65
+ */
66
+ concurrency?: number;
67
+ }
68
+ /** Why a session was skipped or failed (drives the localized label). */
69
+ export type OutcomeReason = 'loaded' | 'no-markers' | 'no-artifact' | 'loaded-between' | 'unreadable' | 'repair' | 'locked';
70
+ /** One per-session outcome, carried into the report. */
71
+ export interface SessionOutcome {
72
+ readonly id: string;
73
+ readonly status: 'repaired' | 'skipped' | 'failed';
74
+ readonly a: number;
75
+ readonly b: number;
76
+ readonly c: number;
77
+ readonly reason?: OutcomeReason;
78
+ readonly error?: string;
79
+ }
80
+ /** Structured result of {@link runRewindFix} (locale-agnostic, unit-testable). */
81
+ export interface RewindFixResult {
82
+ readonly launcherHasMarkers: boolean;
83
+ readonly cancelled: boolean;
84
+ readonly scanned: number;
85
+ readonly durationMs: number;
86
+ readonly apply: boolean;
87
+ readonly sessions: SessionOutcome[];
88
+ }
89
+ /**
90
+ * Register the `/dsh-rewind-fix` host command. `store` must be the plugin's
91
+ * `SnapshotStore` so repaired sessions get their snapshots cleared; `render` is
92
+ * the plugin's locale translator.
93
+ */
94
+ export declare function registerRewindFix(ctx: Context, store: SnapshotStoreLike, render: RenderFn): void;
95
+ /**
96
+ * The domain core. Enumerates all persisted sessions, applies the safety rules,
97
+ * repairs each marker-bearing CLOSED session, and returns structured outcome
98
+ * data. It never throws: every per-session failure is recorded and the run
99
+ * continues (idempotent; a rollback restores the original artifact).
100
+ */
101
+ export declare function runRewindFix(deps: RewindFixDeps, opts: RewindFixOptions): Promise<RewindFixResult>;
102
+ /**
103
+ * Render a {@link RewindFixResult} through the locale translator. The RESULT
104
+ * headline is the FIRST line (so the client card's single-line collapsed
105
+ * summary shows it), followed by the per-session detail lines (shown when the
106
+ * card is expanded).
107
+ */
108
+ export declare function renderRewindFixReport(result: RewindFixResult, render: RenderFn): string;
109
+ export {};
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Pure repair core for legacy rewind markers.
3
+ *
4
+ * The `/dsh-rewind-fix` command rewrites the OLD plugin's rewind marker — a
5
+ * bare `assistant/message(turn=N, step=0)` (form A) or a ghost-frame
6
+ * `[step/start][assistant/message][step/end]` inside a closed turn (form B) —
7
+ * into the CURRENT marker shape (form C): an empty-content `user/message` whose
8
+ * `source` cites the plugin and whose `surfaceOp`/`sourceEventSeqs` still carry
9
+ * the surface replace. Form C is what a 0.1.3 harness accepts.
10
+ *
11
+ * This module is PURE: it only recognizes event shapes and rewrites them. It
12
+ * does zero IO and has no dependency on the harness session services. The
13
+ * session-log IO layer (read/write) and the command orchestration live
14
+ * elsewhere; here we export the transform plus the shared C-marker contract so
15
+ * the running plugin and the repair agree on the exact marker data shape.
16
+ *
17
+ * Correctness contract:
18
+ * - A → C is an in-place retype (seq does NOT shift).
19
+ * - B → C deletes the two ghost `step/start`/`step/end` events AND re-denses
20
+ * every surviving event's `seq`, so the whole log is one global compaction —
21
+ * the same mechanism DSH's own compaction/forward-migration uses.
22
+ * - Every reference (surfaceOp/sourceEventSeqs/data.*) is rewritten through a
23
+ * single `oldSeq → newSeq` map, because deleting frames shifts every later
24
+ * event and a later marker's references may cite an earlier marker's seq.
25
+ * - A marker that survives is retargeted to form C and participates in the
26
+ * renumbering, so references INTO it follow the map too.
27
+ *
28
+ * The transform never mutates its inputs: input events are copied and the
29
+ * mutable reference fields are rebuilt onto fresh objects (a decoded event's
30
+ * `data` is deep-frozen, so mutating a range in place would throw).
31
+ */
32
+ import type { SessionEvent } from '@deepseek-ai/dsh-session';
33
+ import type { ContentBlock } from '@deepseek-ai/dsh-llm';
34
+ /** The rewind-marker source the current plugin writes (form C contract). */
35
+ export declare const REWIND_MARKER_SOURCE: {
36
+ readonly kind: 'plugin';
37
+ readonly plugin: 'dsh-rewind';
38
+ };
39
+ /** The rewind-marker message content: empty (closest to "invisible"). */
40
+ export declare const REWIND_MARKER_CONTENT: readonly ContentBlock[];
41
+ /**
42
+ * Build the form-C marker `data` (the `user/message` payload). When `id` is
43
+ * supplied it is preserved verbatim (the repair keeps the original marker's
44
+ * `message.id`); when omitted a fresh id is generated so the running plugin can
45
+ * reuse this contract for its live `buildMarker()`.
46
+ */
47
+ export declare function buildRewindMarkerData(id?: string): Record<string, unknown>;
48
+ /** The surface `replace` op carried by a rewind marker. */
49
+ export interface ReplaceSurfaceOp {
50
+ op: 'replace';
51
+ start: number;
52
+ end: number;
53
+ }
54
+ /** Type guard for the surface `replace` op. */
55
+ export declare function isReplaceSurfaceOp(value: unknown): value is ReplaceSurfaceOp;
56
+ /** Is `event` a legacy rewind marker in either form A or B? */
57
+ export declare function isLegacyRewindMarker(event: Readonly<SessionEvent>): boolean;
58
+ /** Is `event` already a form-C rewind marker? (the target shape) */
59
+ export declare function isFormCMarker(event: Readonly<SessionEvent>): boolean;
60
+ /** Result of {@link repairRewindMarkers}. */
61
+ export interface RepairOutput {
62
+ /** The repaired events, densely seq'd, in log order. */
63
+ events: SessionEvent[];
64
+ /** original event seq → new dense seq for every SURVIVING event. */
65
+ mapSeq: ReadonlyMap<number, number>;
66
+ /** Per-form counts observed in the repaired run. */
67
+ stats: {
68
+ a: number;
69
+ b: number;
70
+ c: number;
71
+ removedGhosts: number;
72
+ };
73
+ }
74
+ /**
75
+ * Rewrite any number of stacked rewind markers (forms A and B, interleaved)
76
+ * into form C in one global compaction.
77
+ *
78
+ * @param input - the decoded event list, in log order. Not mutated.
79
+ * @returns the repaired event list, the oldSeq→newSeq map, and per-form counts.
80
+ * @throws {Error} when a surviving reference points at a seq that was consumed
81
+ * (a removed ghost frame); failing closed avoids writing a corrupt log.
82
+ */
83
+ export declare function repairRewindMarkers(input: ReadonlyArray<SessionEvent | unknown>): RepairOutput;