dsh-rewind-plugin 0.18.0 → 0.19.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/README.en.md +6 -14
- package/README.md +5 -10
- package/lib/client.js +494 -338
- package/lib/index.js +2 -2
- package/lib/types/client/candidates.d.ts +4 -6
- package/lib/types/client/hidden.d.ts +30 -36
- package/lib/types/client/index.d.ts +1 -7
- package/lib/types/client/portals.d.ts +3 -65
- package/lib/types/client/styles.d.ts +1 -1
- package/lib/types/client/visibility.d.ts +89 -0
- package/lib/types/index.d.ts +2 -2
- package/lib/types/rewind.d.ts +7 -7
- package/package.json +37 -37
package/lib/index.js
CHANGED
|
@@ -28,7 +28,7 @@ var en = {
|
|
|
28
28
|
"storeUnsupported": "file restore unavailable: these snapshots use a newer store format (v{version}) than this plugin understands; nothing was changed. Update the plugin or clear this session's snapshots.",
|
|
29
29
|
"success": "Withdrawn seq {targetSeq} and everything after it (conversation returned to earlier){restore}.",
|
|
30
30
|
"noUserMessages": "This session has no rewindable user messages yet.",
|
|
31
|
-
"command.description": "Rewind the conversation and/or
|
|
31
|
+
"command.description": "Rewind the conversation and/or files to a previous point",
|
|
32
32
|
"command.undoDescription": "Alias of /rewind",
|
|
33
33
|
"engine.description": "dsh-rewind internal command",
|
|
34
34
|
"engine.rejected": "Internal command: use /rewind or /undo.",
|
|
@@ -74,7 +74,7 @@ var zh = {
|
|
|
74
74
|
"storeUnsupported": "\u6587\u4EF6\u8FD8\u539F\u4E0D\u53EF\u7528\uFF1A\u8FD9\u4E9B\u5FEB\u7167\u4F7F\u7528\u4E86\u6BD4\u672C\u63D2\u4EF6\u66F4\u65B0\u7684\u5B58\u50A8\u683C\u5F0F\uFF08v{version}\uFF09\uFF1B\u5DE5\u4F5C\u533A\u672A\u505A\u4EFB\u4F55\u6539\u52A8\u3002\u8BF7\u66F4\u65B0\u63D2\u4EF6\uFF0C\u6216\u6E05\u9664\u8BE5\u4F1A\u8BDD\u7684\u5FEB\u7167\u3002",
|
|
75
75
|
"success": "\u5DF2\u64A4\u56DE seq {targetSeq} \u53CA\u4E4B\u540E\u5185\u5BB9\uFF08\u5BF9\u8BDD\u5DF2\u56DE\u5230\u6B64\u524D\uFF09{restore}\u3002",
|
|
76
76
|
"noUserMessages": "\u5F53\u524D\u4F1A\u8BDD\u8FD8\u6CA1\u6709\u53EF\u56DE\u9000\u7684\u7528\u6237\u6D88\u606F\u3002",
|
|
77
|
-
"command.description": "\
|
|
77
|
+
"command.description": "\u628A\u5BF9\u8BDD\u548C/\u6216\u6587\u4EF6\u56DE\u9000\u5230\u4E4B\u524D\u7684\u67D0\u6761\u6D88\u606F",
|
|
78
78
|
"command.undoDescription": "/rewind \u7684\u522B\u540D",
|
|
79
79
|
"engine.description": "dsh-rewind \u5185\u90E8\u547D\u4EE4",
|
|
80
80
|
"engine.rejected": "\u5185\u90E8\u547D\u4EE4\uFF1A\u8BF7\u4F7F\u7528 /rewind \u6216 /undo\u3002",
|
|
@@ -43,7 +43,6 @@ export interface CandidateChat {
|
|
|
43
43
|
/** A user/steering row subset; only fields the listing reads are real. */
|
|
44
44
|
export interface CandidateUserNode {
|
|
45
45
|
readonly kind: string;
|
|
46
|
-
readonly anchorSeq?: number;
|
|
47
46
|
readonly data: {
|
|
48
47
|
readonly seq: number;
|
|
49
48
|
readonly time: number;
|
|
@@ -64,15 +63,14 @@ export declare function messagePreviewOf(message: {
|
|
|
64
63
|
export declare function formatCandidateTime(time: number): string;
|
|
65
64
|
/**
|
|
66
65
|
* List the selectable rewind candidates of a session chat snapshot: user and
|
|
67
|
-
* steering rows still on the surface (
|
|
68
|
-
* newest `limit` kept, newest first — the top row is the default highlight,
|
|
66
|
+
* steering rows still on the surface (withdrawn rows are absent from `order`),
|
|
67
|
+
* the newest `limit` kept, newest first — the top row is the default highlight,
|
|
69
68
|
* i.e. the most recent message and the most common rewind target.
|
|
70
69
|
* @param snap - the session chat snapshot.
|
|
71
|
-
* @param hidden - anchor seqs withdrawn by rewinds (from `hiddenSeqsOf`).
|
|
72
70
|
* @param limit - maximum number of candidates to return.
|
|
73
71
|
*/
|
|
74
|
-
export declare function rewindCandidatesOf(snap: CandidateChat,
|
|
75
|
-
/** The candidates of a live chat snapshot
|
|
72
|
+
export declare function rewindCandidatesOf(snap: CandidateChat, limit?: number): RewindCandidate[];
|
|
73
|
+
/** The candidates of a live chat snapshot; withdrawn rows are already excluded from `order`. */
|
|
76
74
|
export declare function rewindCandidatesOfChat(snap: CandidateChat): RewindCandidate[];
|
|
77
75
|
/**
|
|
78
76
|
* Map typed candidates to popupSelect rows. The list comes from the host over
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pure
|
|
3
|
-
* transcript. Extracted from the client plugin (`src/client/index.ts`) so the
|
|
4
|
-
* multi-rewind cut logic stays unit-testable without a DOM.
|
|
2
|
+
* Pure Chat-snapshot readers and executed-rewind cut derivation.
|
|
5
3
|
*
|
|
6
4
|
* @module dsh-rewind/client/hidden
|
|
7
5
|
*/
|
|
@@ -15,35 +13,39 @@ export declare const ENGINE_COMMAND = "rewind-plugin";
|
|
|
15
13
|
export declare const LEGACY_ENGINE_COMMAND = "rewind";
|
|
16
14
|
/** Engine command names the client recognizes. */
|
|
17
15
|
export declare const ENGINE_COMMANDS: ReadonlySet<string>;
|
|
18
|
-
/** A
|
|
19
|
-
* The harness's view node shape plus the `anchorSeq` the chat snapshot carries
|
|
20
|
-
* (the plugin reads `anchorSeq` off each node; it is not declared on the
|
|
21
|
-
* harness's `ConversationViewNode`). */
|
|
16
|
+
/** A Chat-snapshot view node as the client readers see it. */
|
|
22
17
|
export interface ChatConversationViewNode {
|
|
23
18
|
readonly key: string;
|
|
24
19
|
readonly kind?: string;
|
|
25
20
|
readonly data?: unknown;
|
|
26
21
|
readonly anchorSeq: number;
|
|
27
|
-
/** Owning Turn
|
|
28
|
-
* (see `addWithdrawnTurnChrome`). */
|
|
22
|
+
/** Owning Turn, when the node has one (used by the visibility summary pass). */
|
|
29
23
|
readonly location?: {
|
|
30
24
|
readonly turn?: {
|
|
31
25
|
readonly turn?: unknown;
|
|
32
26
|
readonly start?: unknown;
|
|
33
27
|
};
|
|
34
28
|
};
|
|
29
|
+
/** Chat-only row visibility; absent on older test doubles. */
|
|
30
|
+
readonly visibility?: 'visible' | 'hidden';
|
|
35
31
|
}
|
|
36
|
-
/** Minimal
|
|
32
|
+
/** Minimal Chat-snapshot reader the client helpers need. */
|
|
37
33
|
export interface HiddenChat {
|
|
38
34
|
readonly order: readonly string[];
|
|
39
35
|
readonly nodes: {
|
|
40
36
|
get(key: string): ChatConversationViewNode | undefined;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
readonly locations?: {
|
|
44
|
-
getTurn(turn: number): readonly string[];
|
|
37
|
+
/** Every materialized node, including hidden ones. Absent on older snapshots. */
|
|
38
|
+
values?(): readonly ChatConversationViewNode[];
|
|
45
39
|
};
|
|
46
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Every materialized Chat node, preferring the store's own values (which also
|
|
43
|
+
* includes rows the visibility layer hid) and falling back to the visible
|
|
44
|
+
* order for older snapshots / test doubles.
|
|
45
|
+
* @param chat - the live chat snapshot, when available.
|
|
46
|
+
* @returns all nodes in store insertion order.
|
|
47
|
+
*/
|
|
48
|
+
export declare function chatNodesOf(chat: HiddenChat | undefined): readonly ChatConversationViewNode[];
|
|
47
49
|
/**
|
|
48
50
|
* Reader for one session's live chat snapshot: the chat is served by the
|
|
49
51
|
* `uiConversation` service's named "chat" view (contributed by
|
|
@@ -71,7 +73,7 @@ export declare function resolveChatWatch(resolveView: (sessionId: string) => {
|
|
|
71
73
|
/**
|
|
72
74
|
* Resolve the chat snapshot from the `uiConversation` "chat" view. The view's
|
|
73
75
|
* `getSnapshot()` returns undefined until the named view is registered, which
|
|
74
|
-
* degrades to `undefined` (no targets
|
|
76
|
+
* degrades to `undefined` (no targets — never a crash).
|
|
75
77
|
*/
|
|
76
78
|
export declare function chatSnapshotOf(chatView: {
|
|
77
79
|
getSnapshot(): unknown;
|
|
@@ -131,26 +133,18 @@ export declare function targetSeqOfArgs(args: string | null | undefined): number
|
|
|
131
133
|
* never trigger a fill.
|
|
132
134
|
*/
|
|
133
135
|
export declare function isExecutedRewindCommand(node: CommandNode, seq: number): boolean;
|
|
136
|
+
/** One executed rewind's cut: the target seq through the marker's seq, inclusive. */
|
|
137
|
+
export interface RewindCut {
|
|
138
|
+
/** First withdrawn anchor seq. */
|
|
139
|
+
readonly target: number;
|
|
140
|
+
/** Marker log seq, the last withdrawn anchor seq. */
|
|
141
|
+
readonly marker: number;
|
|
142
|
+
}
|
|
134
143
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Anchor seqs that must be hidden from the rendered transcript so the user sees
|
|
142
|
-
* the conversation as the agent sees it: every legacy probe flow node (it only
|
|
143
|
-
* exists to feed the popover) and every SUCCESSFUL executed `/rewind` command
|
|
144
|
-
* row, plus every message a rewind withdrew — the target, everything after it,
|
|
145
|
-
* and the (unrendered) marker.
|
|
146
|
-
*
|
|
147
|
-
* Each executed rewind cuts ONE span `[target, marker]`, and callers must keep
|
|
148
|
-
* the spans SEPARATE: collapsing them to `[min target, max marker]` would hide a
|
|
149
|
-
* still-on-surface gap of new traffic between an earlier marker and a later
|
|
150
|
-
* target. Endpoints come from the command nodes (`sourceEventSeq` is the
|
|
151
|
-
* marker's log seq; the outcome text carries the target seq).
|
|
152
|
-
*
|
|
153
|
-
* A Turn whose rows were ALL withdrawn also contributes its Turn-process summary
|
|
154
|
-
* row, whose own anchor can precede the target (see `addWithdrawnTurnChrome`).
|
|
144
|
+
* Read every executed rewind cut from a node set. Internal probes never
|
|
145
|
+
* contribute; a success without a marker rewound nothing; a command whose
|
|
146
|
+
* args carry no `@<seq>` target has no recoverable span.
|
|
147
|
+
* @param nodes - every materialized Chat node.
|
|
148
|
+
* @returns one cut per marker-carrying successful engine command, in input order.
|
|
155
149
|
*/
|
|
156
|
-
export declare function
|
|
150
|
+
export declare function rewindCutsOf(nodes: Iterable<ChatConversationViewNode>): RewindCut[];
|
|
@@ -9,8 +9,7 @@
|
|
|
9
9
|
* The command decoration (`ctx.commandUi.decorate`) makes a bare `/rewind` (or
|
|
10
10
|
* its alias `/undo`) open the harness's own popupSelect instead of executing;
|
|
11
11
|
* the parameterized `@<seq>` form stays the execution channel, while the
|
|
12
|
-
* popover's impact and candidate reads go over the host RPC channel.
|
|
13
|
-
* exported contract is specified in `docs/contract/client-contract.md`.
|
|
12
|
+
* popover's impact and candidate reads go over the host RPC channel.
|
|
14
13
|
*
|
|
15
14
|
* @module dsh-rewind/client
|
|
16
15
|
*/
|
|
@@ -46,8 +45,3 @@ export interface ClientContext {
|
|
|
46
45
|
* @param ctx - client root context carrying `slots`, `sessions`, `locale` and `commandUi`.
|
|
47
46
|
*/
|
|
48
47
|
export declare function apply(ctx: ClientContext): void;
|
|
49
|
-
/**
|
|
50
|
-
* Public contract — rewind visibility. Stable, semver-protected; the rest of
|
|
51
|
-
* this module is internal. See `docs/contract/client-contract.md`.
|
|
52
|
-
*/
|
|
53
|
-
export { hiddenSeqsOf, targetSeqOfArgs, type HiddenChat } from './hidden.ts';
|
|
@@ -170,22 +170,6 @@ export declare function writeComposer(text: string, facade: ComposerDraftWriter
|
|
|
170
170
|
* after switching sessions or restarting dsh.
|
|
171
171
|
*/
|
|
172
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>;
|
|
173
|
-
/**
|
|
174
|
-
* The snapshot seq a rendered seat stands for.
|
|
175
|
-
*
|
|
176
|
-
* A seat carries two keys: `data-chat-anchor-key` (the FLOW key, which for a
|
|
177
|
-
* step/process part is the composite `["<nodeKey>", "<part>"]`) and
|
|
178
|
-
* `data-chat-node-key` (always the plain node key). Resolving only the flow key
|
|
179
|
-
* made every grouped process row — the "正在分析/已完成分析" step rows, tool
|
|
180
|
-
* groups — unresolvable, so a withdrawn one could never be hidden and stayed on
|
|
181
|
-
* screen after a rewind. This mirrors the official viewport's own identity read
|
|
182
|
-
* (`anchorKey === key || nodeKey === key`).
|
|
183
|
-
*
|
|
184
|
-
* @param element - one rendered seat.
|
|
185
|
-
* @param chat - the live chat snapshot the seat belongs to.
|
|
186
|
-
* @returns the node's `anchorSeq`, or undefined when the seat has no resolvable node.
|
|
187
|
-
*/
|
|
188
|
-
export declare function resolveSeatAnchorSeq(element: HTMLElement, chat: HiddenChat | undefined): number | undefined;
|
|
189
173
|
/**
|
|
190
174
|
* Locate the actions container of a user/steering seat row — the element the
|
|
191
175
|
* ↶ button portals into (the copy/branch IconActions row).
|
|
@@ -203,57 +187,12 @@ export declare function resolveSeatAnchorSeq(element: HTMLElement, chat: HiddenC
|
|
|
203
187
|
* both durable and pending row shapes without a full React portal render.
|
|
204
188
|
*/
|
|
205
189
|
export declare function actionsContainerOf(row: HTMLElement | undefined): HTMLElement | undefined;
|
|
206
|
-
/**
|
|
207
|
-
* Hide every rendered seat a rewind withdrew, plus the step/process GROUP shells
|
|
208
|
-
* those seats empty.
|
|
209
|
-
*
|
|
210
|
-
* A seat's seq comes from `resolveSeatAnchorSeq`: `data-chat-anchor-key` (the
|
|
211
|
-
* FLOW key, composite for a step/process part) with `data-chat-node-key` (always
|
|
212
|
-
* the plain node key) as the fallback. A group shell is different: it carries
|
|
213
|
-
* the composite `group:<json>` anchor, which resolves to no chat node — the
|
|
214
|
-
* group is a view construct, not a message. It is therefore judged by its
|
|
215
|
-
* members, which THIS batch already holds: a shell whose every member seat is
|
|
216
|
-
* withdrawn has no live content left, so it is hidden too. That is what stops a
|
|
217
|
-
* fully withdrawn "analysis completed" / "command executed" group from staying
|
|
218
|
-
* on screen as an expandable empty row. A shell keeps one live member only if
|
|
219
|
-
* that member is genuinely on the surface, in which case the shell stays; a
|
|
220
|
-
* shell the batch carries no member of is left alone.
|
|
221
|
-
*
|
|
222
|
-
* A MEMBER is a real chat node seat — `ChatNodeSeat` is the one shape that
|
|
223
|
-
* writes `data-chat-node-key` beside the anchor key. An anchor-only descendant
|
|
224
|
-
* (the tool call tree's own `call:<callId>` row, or a third-party anchor) is
|
|
225
|
-
* nested decoration: it resolves to no chat node, so it does NOT vote on the
|
|
226
|
-
* shell's emptiness. Counting such a row as "still live" is what left a fully
|
|
227
|
-
* withdrawn "command executed" tool group on screen as an empty expandable row
|
|
228
|
-
* after every member below it had been hidden.
|
|
229
|
-
*
|
|
230
|
-
* Every hidden element carries `data-dsh-rewind-hidden` (see
|
|
231
|
-
* `docs/contract/client-contract.md`).
|
|
232
|
-
*
|
|
233
|
-
* @param chat - the live chat snapshot the seats belong to.
|
|
234
|
-
* @param seats - this refresh's seat batch (every `[data-chat-anchor-key]`).
|
|
235
|
-
* @param hiddenSeqs - anchor seqs withdrawn by the executed rewinds.
|
|
236
|
-
* @param hidden - elements this pass has hidden, so a re-created row is re-judged.
|
|
237
|
-
*/
|
|
238
|
-
export declare function hideWithdrawnSeats(chat: HiddenChat | undefined, seats: Iterable<HTMLElement>, hiddenSeqs: ReadonlySet<number>, hidden: Set<HTMLElement>): void;
|
|
239
|
-
/**
|
|
240
|
-
* Undo every hide the given set recorded, so a live disable (or a session-slot
|
|
241
|
-
* collapse) cannot leave the transcript truncated.
|
|
242
|
-
*
|
|
243
|
-
* The hide is the plugin's OWN mutation of host rows ({@link hideWithdrawnSeats}),
|
|
244
|
-
* and nothing else would ever revert it: a disposed bridge runs no further
|
|
245
|
-
* refresh, so rows it hid would stay hidden with no affordance to bring them
|
|
246
|
-
* back.
|
|
247
|
-
*
|
|
248
|
-
* @param hidden - the elements this bridge currently hides; emptied.
|
|
249
|
-
*/
|
|
250
|
-
export declare function showHiddenSeats(hidden: Set<HTMLElement>): void;
|
|
251
190
|
/**
|
|
252
191
|
* Collect the portal targets of one session: user rows × snapshot nodes.
|
|
253
192
|
* Exported as a test seam — the DOM→targets pairing that drives the ↶ button
|
|
254
193
|
* is otherwise only reachable through a full React portal render.
|
|
255
194
|
*/
|
|
256
|
-
export declare function collectTargets(chat: HiddenChat
|
|
195
|
+
export declare function collectTargets(chat: HiddenChat): readonly PortalTarget[];
|
|
257
196
|
/**
|
|
258
197
|
* The session-kind slice the durable-target gate reads — the session
|
|
259
198
|
* snapshot's `subagent` cell, the Harness's own runtime signal for a
|
|
@@ -285,9 +224,8 @@ export declare function isRewindInertSession(snapshot: SessionKindLike): boolean
|
|
|
285
224
|
* pure, directly testable decision.
|
|
286
225
|
* @param snapshot - the session snapshot carrying the `subagent` cell.
|
|
287
226
|
* @param chat - the session's chat snapshot, or undefined while unavailable.
|
|
288
|
-
* @param hiddenSeqs - anchor seqs withdrawn by previous rewinds.
|
|
289
227
|
*/
|
|
290
|
-
export declare function collectDurableTargets(snapshot: SessionKindLike, chat: HiddenChat | undefined
|
|
228
|
+
export declare function collectDurableTargets(snapshot: SessionKindLike, chat: HiddenChat | undefined): readonly PortalTarget[];
|
|
291
229
|
/**
|
|
292
230
|
* The pending bubble row's compared text and its message-attachment block
|
|
293
231
|
* count, read from a CLONE (the live row is never touched).
|
|
@@ -320,7 +258,7 @@ interface RewindPortalsProps extends RewindBridgeDeps {
|
|
|
320
258
|
* synchronous full-transcript scan inside a commit microtask.
|
|
321
259
|
*
|
|
322
260
|
* The refresh is driven by the three inputs its targets are made of: the chat
|
|
323
|
-
* snapshot
|
|
261
|
+
* snapshot's visible order, the session's `inbox` projection (the pending
|
|
324
262
|
* occurrences — see `syncInboxSubscription`), and the DOM (row/actions mounts,
|
|
325
263
|
* via the scoped observer).
|
|
326
264
|
*/
|
|
@@ -32,4 +32,4 @@ export declare const CLASS: {
|
|
|
32
32
|
*/
|
|
33
33
|
export declare const REWIND_ICON_SVG: string;
|
|
34
34
|
/** One injected stylesheet (scoped under `.dsh-rewind-*`). */
|
|
35
|
-
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
|
|
35
|
+
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@media (hover: hover) {\n .dsh-rewind-btn:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n color: var(--dsw-alias-label-secondary);\n }\n}\n\n.dsh-rewind-popover {\n position: fixed;\n z-index: 1100;\n box-sizing: border-box;\n width: min(300px, calc(100vw - 24px));\n padding: 16px;\n border: 0;\n border-radius: var(--dsw-radius-lg);\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n backdrop-filter: var(--dsw-menu-backdrop-filter);\n isolation: isolate;\n --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);\n /* Elevated surface: match the menu cards' l2 scrollbar tokens. */\n --dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);\n --dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);\n box-shadow: var(--dsw-elevation-prominent);\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n}\n.dsh-rewind-popover-title {\n font-size: 12px;\n font-weight: 500;\n line-height: 18px;\n}\n.dsh-rewind-popover-target {\n margin: 2px 0 10px;\n font-size: 12px;\n line-height: 18px;\n color: var(--dsw-alias-label-tertiary);\n word-break: break-all;\n /* The full message sits above the popover; two lines are enough context. */\n display: -webkit-box;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n overflow: hidden;\n}\n.dsh-rewind-popover-option {\n display: flex;\n flex-direction: column;\n gap: 2px;\n width: 100%;\n margin: 0 0 2px;\n padding: 6px 8px;\n border: 0;\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.4;\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-note {\n margin-top: 6px;\n font-size: 11px;\n line-height: 15px;\n color: var(--dsw-alias-label-tertiary);\n}\n.dsh-rewind-popover-actions {\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n margin-top: 8px;\n}\n.dsh-rewind-popover-primary,\n.dsh-rewind-popover-ghost {\n height: 28px;\n padding: 0 10px;\n border: none;\n border-radius: 8px;\n font: inherit;\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.4;\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.dsh-rewind-popover-ghost:active {\n background: var(--dsw-alias-interactive-bg-active);\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: 0;\n border-radius: var(--dsw-radius-md);\n background: var(--dsw-specific-menu, var(--dsw-alias-bg-layer-3));\n backdrop-filter: var(--dsw-menu-backdrop-filter);\n isolation: isolate;\n --dsw-elevation-stroke-color: var(--dsw-alias-border-l1);\n box-shadow: var(--dsw-elevation-panel);\n font-size: 13px;\n line-height: 18px;\n color: var(--dsw-alias-label-primary);\n pointer-events: none;\n}\n\n/* ---- Plugin configuration form ---- */\n/* Only the card's own controls: the two switch rows, their hints, and the divider. */\n.dsh-rewind-settings-divider {\n border-top: 1px solid var(--dsw-alias-border-l2);\n}\n.dsh-rewind-settings-section {\n display: grid;\n gap: 6px;\n padding: 12px 0;\n}\n.dsh-rewind-settings-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-settings-toggle-label {\n flex: 1;\n min-width: 0;\n}\n.dsh-rewind-settings-hint {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n";
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot-level rewind visibility.
|
|
3
|
+
*
|
|
4
|
+
* DSH's Chat builder already supports hidden rows: a node stays in the node
|
|
5
|
+
* store (so the composer can read its text/images) but leaves `order`,
|
|
6
|
+
* `locations`, Turn navigation, and grouping whenever its `visibility` is
|
|
7
|
+
* `'hidden'`. This module wraps the registered `chat` view builder and marks
|
|
8
|
+
* the nodes inside an executed rewind's `[target, marker]` span hidden before
|
|
9
|
+
* the builder sees them.
|
|
10
|
+
*
|
|
11
|
+
* The seam is private: `uiConversation.views` is a first-party registry and
|
|
12
|
+
* `definition.create()` is not an extension point. Its shape is checked and it
|
|
13
|
+
* is restored on unload.
|
|
14
|
+
*
|
|
15
|
+
* @module dsh-rewind/client/visibility
|
|
16
|
+
*/
|
|
17
|
+
import { type ChatConversationViewNode } from './hidden.ts';
|
|
18
|
+
interface BuilderLike {
|
|
19
|
+
readonly empty: unknown;
|
|
20
|
+
replace(input: BuilderInput): unknown;
|
|
21
|
+
apply(input: BuilderApplyInput): unknown;
|
|
22
|
+
groupInput?(): unknown;
|
|
23
|
+
publish?(): void;
|
|
24
|
+
}
|
|
25
|
+
interface BuilderInput {
|
|
26
|
+
readonly nodes: readonly ChatConversationViewNode[];
|
|
27
|
+
readonly timeline: unknown;
|
|
28
|
+
readonly changedTurns?: readonly number[];
|
|
29
|
+
}
|
|
30
|
+
interface BuilderApplyInput {
|
|
31
|
+
readonly upserts: readonly ChatConversationViewNode[];
|
|
32
|
+
readonly timeline: unknown;
|
|
33
|
+
readonly changedTurns?: readonly number[];
|
|
34
|
+
}
|
|
35
|
+
interface SessionsLike {
|
|
36
|
+
readonly list?: {
|
|
37
|
+
getSnapshot(): {
|
|
38
|
+
readonly ids?: readonly string[];
|
|
39
|
+
readonly byId?: Readonly<Record<string, {
|
|
40
|
+
readonly retainedBy?: Readonly<Record<string, number>>;
|
|
41
|
+
}>>;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/** Context subset the installer needs; structurally satisfied by the client root. */
|
|
46
|
+
export interface VisibilityContext {
|
|
47
|
+
get(name: string): unknown;
|
|
48
|
+
effect?(execute: () => (() => void) | void, label?: string): unknown;
|
|
49
|
+
inject?(names: readonly string[], callback: (scope: VisibilityContext) => void): unknown;
|
|
50
|
+
readonly sessions?: SessionsLike;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Wrap the registered `chat` view builder so rewind spans enter the builder as
|
|
54
|
+
* `visibility: 'hidden'`. Idempotent across registry changes and HMR.
|
|
55
|
+
* @param ctx - client root context.
|
|
56
|
+
*/
|
|
57
|
+
export declare function installChatViewVisibility(ctx: VisibilityContext): void;
|
|
58
|
+
/**
|
|
59
|
+
* Structural wrapper over the registered Chat builder. It forwards every
|
|
60
|
+
* builder method, keeps the original node objects, and only changes
|
|
61
|
+
* `visibility`. A full replacement is used whenever the cut set changes;
|
|
62
|
+
* otherwise it stays on the builder's incremental `apply` path.
|
|
63
|
+
*/
|
|
64
|
+
export declare class RewindVisibilityBuilder implements BuilderLike {
|
|
65
|
+
private readonly inner;
|
|
66
|
+
private originals;
|
|
67
|
+
private commands;
|
|
68
|
+
private byTurn;
|
|
69
|
+
private turnProcess;
|
|
70
|
+
private derived;
|
|
71
|
+
private cuts;
|
|
72
|
+
private cutSignature;
|
|
73
|
+
private lastSignature;
|
|
74
|
+
/** @param inner - the real Chat builder. */
|
|
75
|
+
constructor(inner: BuilderLike);
|
|
76
|
+
get empty(): unknown;
|
|
77
|
+
replace(input: BuilderInput): unknown;
|
|
78
|
+
apply(input: BuilderApplyInput): unknown;
|
|
79
|
+
groupInput(): unknown;
|
|
80
|
+
publish(): void;
|
|
81
|
+
private setAll;
|
|
82
|
+
private setOriginal;
|
|
83
|
+
private unindex;
|
|
84
|
+
private refreshCuts;
|
|
85
|
+
private deriveAll;
|
|
86
|
+
private deriveOne;
|
|
87
|
+
private deriveTurnProcess;
|
|
88
|
+
}
|
|
89
|
+
export {};
|
package/lib/types/index.d.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
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` whose `surfaceOp` replaces
|
|
8
|
-
*
|
|
7
|
+
* marker `user/message` whose `surfaceOp` replaces the target and every surface
|
|
8
|
+
* node after it with itself, cutting only the model-visible surface
|
|
9
9
|
* (`docs/architecture.md`).
|
|
10
10
|
*
|
|
11
11
|
* File restore (mode `both`) follows Claude Code's checkpointing: unless
|
package/lib/types/rewind.d.ts
CHANGED
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
* and the ordered surface, so this module stays unit-testable.
|
|
5
5
|
*
|
|
6
6
|
* Rewind semantics: rewinding to a user message appends a marker node into the
|
|
7
|
-
* session log whose `surfaceOp` replaces every surface node
|
|
8
|
-
* with itself. The log (audit trail, rendered transcript) is
|
|
9
|
-
* the model-visible surface is cut, so the next request
|
|
10
|
-
* from the target message
|
|
7
|
+
* session log whose `surfaceOp` replaces the target and every surface node
|
|
8
|
+
* after it with itself. The log (audit trail, rendered transcript) is
|
|
9
|
+
* untouched; only the model-visible surface is cut, so the next request
|
|
10
|
+
* derives its context from before the target message. The marker is a `user/message` carrying a
|
|
11
11
|
* replace `surfaceOp` and the producer-owned source `{ kind: 'dsh-rewind' }`
|
|
12
12
|
* (see `marker.ts`); the shape's invariants and history are documented in
|
|
13
13
|
* `docs/architecture.md`.
|
|
@@ -51,13 +51,13 @@ export interface RewindCandidate {
|
|
|
51
51
|
}
|
|
52
52
|
/** A validated rewind: the target plus the exact surface range to shadow. */
|
|
53
53
|
export interface RewindPlan {
|
|
54
|
-
/** Target user message seq (
|
|
54
|
+
/** Target user message seq (withdrawn with the surface range). */
|
|
55
55
|
readonly targetSeq: number;
|
|
56
56
|
/** The target's ordered position in the surface. */
|
|
57
57
|
readonly targetIndex: number;
|
|
58
|
-
/** Ordered surface node seqs the rewind shadows (everything after
|
|
58
|
+
/** Ordered surface node seqs the rewind shadows (the target and everything after it). */
|
|
59
59
|
readonly shadowedSeqs: readonly number[];
|
|
60
|
-
/**
|
|
60
|
+
/** The target — the replace range start (inclusive). */
|
|
61
61
|
readonly surfaceStart: number;
|
|
62
62
|
/** Last surface node — the replace range end (inclusive). */
|
|
63
63
|
readonly surfaceEnd: number;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-rewind-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "In-window conversation rewind with workspace file restore · 同窗口内对话回退并可还原工作区文件",
|
|
5
5
|
"icon": "./assets/icon.svg",
|
|
6
6
|
"keywords": [
|
|
@@ -152,42 +152,42 @@
|
|
|
152
152
|
"devDependencies": {
|
|
153
153
|
"@deepseek-ai/cordis": "4.0.5",
|
|
154
154
|
"@deepseek-ai/cosmokit": "1.8.6",
|
|
155
|
-
"@deepseek-ai/dsh-agent": "0.2.1-rc.
|
|
156
|
-
"@deepseek-ai/dsh-api-session-controller": "0.2.1-rc.
|
|
157
|
-
"@deepseek-ai/dsh-attachment": "0.2.1-rc.
|
|
158
|
-
"@deepseek-ai/dsh-client-connection": "0.2.1-rc.
|
|
159
|
-
"@deepseek-ai/dsh-client-locale": "0.2.1-rc.
|
|
160
|
-
"@deepseek-ai/dsh-client-store": "0.2.1-rc.
|
|
161
|
-
"@deepseek-ai/dsh-client-ui-commands": "0.2.1-rc.
|
|
162
|
-
"@deepseek-ai/dsh-client-ui-conversation": "0.2.1-rc.
|
|
163
|
-
"@deepseek-ai/dsh-client-ui-input-trigger": "0.2.1-rc.
|
|
164
|
-
"@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.1-rc.
|
|
165
|
-
"@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.
|
|
166
|
-
"@deepseek-ai/dsh-client-ui-session": "0.2.1-rc.
|
|
167
|
-
"@deepseek-ai/dsh-client-ui-settings": "0.2.1-rc.
|
|
168
|
-
"@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.
|
|
169
|
-
"@deepseek-ai/dsh-command-compact": "0.2.1-rc.
|
|
170
|
-
"@deepseek-ai/dsh-commands": "0.2.1-rc.
|
|
171
|
-
"@deepseek-ai/dsh-compaction": "0.2.1-rc.
|
|
172
|
-
"@deepseek-ai/dsh-compaction-basic": "0.2.1-rc.
|
|
173
|
-
"@deepseek-ai/dsh-compaction-image-offload": "0.2.1-rc.
|
|
174
|
-
"@deepseek-ai/dsh-fs": "0.2.1-rc.
|
|
175
|
-
"@deepseek-ai/dsh-goal": "0.2.1-rc.
|
|
176
|
-
"@deepseek-ai/dsh-home-paths": "0.2.1-rc.
|
|
177
|
-
"@deepseek-ai/dsh-llm": "0.2.1-rc.
|
|
178
|
-
"@deepseek-ai/dsh-plan-mode": "0.2.1-rc.
|
|
179
|
-
"@deepseek-ai/dsh-sandbox": "0.2.1-rc.
|
|
180
|
-
"@deepseek-ai/dsh-scope": "0.2.1-rc.
|
|
181
|
-
"@deepseek-ai/dsh-session": "0.2.1-rc.
|
|
182
|
-
"@deepseek-ai/dsh-session-projection": "0.2.1-rc.
|
|
183
|
-
"@deepseek-ai/dsh-session-stats": "0.2.1-rc.
|
|
184
|
-
"@deepseek-ai/dsh-session-title": "0.2.1-rc.
|
|
185
|
-
"@deepseek-ai/dsh-settings": "0.2.1-rc.
|
|
186
|
-
"@deepseek-ai/dsh-token-meter": "0.2.1-rc.
|
|
187
|
-
"@deepseek-ai/dsh-tools": "0.2.1-rc.
|
|
188
|
-
"@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.
|
|
189
|
-
"@deepseek-ai/dsh-user-questions": "0.2.1-rc.
|
|
190
|
-
"@deepseek-ai/dsh-working-directory": "0.2.1-rc.
|
|
155
|
+
"@deepseek-ai/dsh-agent": "0.2.1-rc.3",
|
|
156
|
+
"@deepseek-ai/dsh-api-session-controller": "0.2.1-rc.3",
|
|
157
|
+
"@deepseek-ai/dsh-attachment": "0.2.1-rc.3",
|
|
158
|
+
"@deepseek-ai/dsh-client-connection": "0.2.1-rc.3",
|
|
159
|
+
"@deepseek-ai/dsh-client-locale": "0.2.1-rc.3",
|
|
160
|
+
"@deepseek-ai/dsh-client-store": "0.2.1-rc.3",
|
|
161
|
+
"@deepseek-ai/dsh-client-ui-commands": "0.2.1-rc.3",
|
|
162
|
+
"@deepseek-ai/dsh-client-ui-conversation": "0.2.1-rc.3",
|
|
163
|
+
"@deepseek-ai/dsh-client-ui-input-trigger": "0.2.1-rc.3",
|
|
164
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager": "0.2.1-rc.3",
|
|
165
|
+
"@deepseek-ai/dsh-client-ui-primitives": "0.2.1-rc.3",
|
|
166
|
+
"@deepseek-ai/dsh-client-ui-session": "0.2.1-rc.3",
|
|
167
|
+
"@deepseek-ai/dsh-client-ui-settings": "0.2.1-rc.3",
|
|
168
|
+
"@deepseek-ai/dsh-client-ui-slots": "0.2.1-rc.3",
|
|
169
|
+
"@deepseek-ai/dsh-command-compact": "0.2.1-rc.3",
|
|
170
|
+
"@deepseek-ai/dsh-commands": "0.2.1-rc.3",
|
|
171
|
+
"@deepseek-ai/dsh-compaction": "0.2.1-rc.3",
|
|
172
|
+
"@deepseek-ai/dsh-compaction-basic": "0.2.1-rc.3",
|
|
173
|
+
"@deepseek-ai/dsh-compaction-image-offload": "0.2.1-rc.3",
|
|
174
|
+
"@deepseek-ai/dsh-fs": "0.2.1-rc.3",
|
|
175
|
+
"@deepseek-ai/dsh-goal": "0.2.1-rc.3",
|
|
176
|
+
"@deepseek-ai/dsh-home-paths": "0.2.1-rc.3",
|
|
177
|
+
"@deepseek-ai/dsh-llm": "0.2.1-rc.3",
|
|
178
|
+
"@deepseek-ai/dsh-plan-mode": "0.2.1-rc.3",
|
|
179
|
+
"@deepseek-ai/dsh-sandbox": "0.2.1-rc.3",
|
|
180
|
+
"@deepseek-ai/dsh-scope": "0.2.1-rc.3",
|
|
181
|
+
"@deepseek-ai/dsh-session": "0.2.1-rc.3",
|
|
182
|
+
"@deepseek-ai/dsh-session-projection": "0.2.1-rc.3",
|
|
183
|
+
"@deepseek-ai/dsh-session-stats": "0.2.1-rc.3",
|
|
184
|
+
"@deepseek-ai/dsh-session-title": "0.2.1-rc.3",
|
|
185
|
+
"@deepseek-ai/dsh-settings": "0.2.1-rc.3",
|
|
186
|
+
"@deepseek-ai/dsh-token-meter": "0.2.1-rc.3",
|
|
187
|
+
"@deepseek-ai/dsh-tools": "0.2.1-rc.3",
|
|
188
|
+
"@deepseek-ai/dsh-typert-protocol": "0.2.1-rc.3",
|
|
189
|
+
"@deepseek-ai/dsh-user-questions": "0.2.1-rc.3",
|
|
190
|
+
"@deepseek-ai/dsh-working-directory": "0.2.1-rc.3",
|
|
191
191
|
"@deepseek-ai/schemastery": "3.18.5",
|
|
192
192
|
"@types/node": "^24.0.0",
|
|
193
193
|
"@types/react": "^18.3.31",
|