dsh-rewind-plugin 0.2.6 → 0.2.8
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.md +78 -175
- package/README.zh.md +77 -162
- package/docs/harness-reference.md +23 -0
- package/docs/release.md +90 -0
- package/docs/troubleshooting.md +39 -0
- package/docs/troubleshooting.zh.md +31 -0
- package/lib/client.js +249 -172
- package/lib/index.js +78 -49
- package/lib/types/client/hidden.d.ts +20 -1
- package/lib/types/client/index.d.ts +14 -18
- package/lib/types/client/popover.d.ts +24 -0
- package/lib/types/client/portals.d.ts +66 -0
- package/lib/types/client/styles.d.ts +0 -2
- package/package.json +7 -2
package/lib/index.js
CHANGED
|
@@ -309,7 +309,7 @@ function mutationPathOf(exec) {
|
|
|
309
309
|
}
|
|
310
310
|
function anchorSeqOf(session, cache) {
|
|
311
311
|
const events = session.events;
|
|
312
|
-
const cached = cache.get(session
|
|
312
|
+
const cached = cache.get(session);
|
|
313
313
|
if (cached !== void 0 && cached.eventsLength === events.length) return cached.anchor;
|
|
314
314
|
let anchor = cached?.anchor;
|
|
315
315
|
for (let i = events.length - 1; i >= (cached?.eventsLength ?? 0); i--) {
|
|
@@ -318,7 +318,7 @@ function anchorSeqOf(session, cache) {
|
|
|
318
318
|
break;
|
|
319
319
|
}
|
|
320
320
|
}
|
|
321
|
-
cache.set(session
|
|
321
|
+
cache.set(session, { anchor, eventsLength: events.length });
|
|
322
322
|
return anchor;
|
|
323
323
|
}
|
|
324
324
|
async function resolveTarget(fs, path, cwd, signal) {
|
|
@@ -342,6 +342,8 @@ async function readTextOrUndefined(fs, target, signal) {
|
|
|
342
342
|
}
|
|
343
343
|
async function captureBefore(fs, exec, pending) {
|
|
344
344
|
if (!TRACKED_TOOLS.has(exec.name)) return;
|
|
345
|
+
const header = exec.agent?.session.header;
|
|
346
|
+
if (header !== void 0 && (header.origin === "subagent" || (header.delegationDepth ?? 0) > 0)) return;
|
|
345
347
|
const path = mutationPathOf(exec);
|
|
346
348
|
if (path === void 0) return;
|
|
347
349
|
const cwd = execSessionCwd(exec, path);
|
|
@@ -388,6 +390,7 @@ function formatPlan(plan, files) {
|
|
|
388
390
|
} else {
|
|
389
391
|
lines.push("\u76EE\u6807\u4E4B\u540E\u6CA1\u6709\u5FEB\u7167\u8BB0\u5F55\u7684\u5199\u7C7B\u53D8\u66F4\uFF0C\u65E0\u9700\u8FD8\u539F\u6587\u4EF6\u3002");
|
|
390
392
|
}
|
|
393
|
+
lines.push(`impact=${files.length}`);
|
|
391
394
|
return lines.join("\n");
|
|
392
395
|
}
|
|
393
396
|
function resolveOrError(events, surface, raw) {
|
|
@@ -402,56 +405,81 @@ function renderFailures(failed) {
|
|
|
402
405
|
return `\uFF1B${failed.length} \u4E2A\u6587\u4EF6\u8FD8\u539F\u5931\u8D25\uFF1A${failed.map((f) => `${f.path}\uFF08${f.message}\uFF09`).join("\u3001")}`;
|
|
403
406
|
}
|
|
404
407
|
async function waitForAgentIdle(agent, signal, timeoutMs = 15e3) {
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
408
|
+
if (signal.aborted) return false;
|
|
409
|
+
let timer;
|
|
410
|
+
let onAbort;
|
|
411
|
+
try {
|
|
412
|
+
await Promise.race([
|
|
413
|
+
agent.whenIdle(),
|
|
414
|
+
new Promise((_resolve, reject) => {
|
|
415
|
+
timer = setTimeout(() => reject(new Error("rewind idle wait timed out")), timeoutMs);
|
|
416
|
+
onAbort = () => reject(new Error("rewind idle wait aborted"));
|
|
417
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
418
|
+
})
|
|
419
|
+
]);
|
|
420
|
+
return true;
|
|
421
|
+
} catch {
|
|
422
|
+
return false;
|
|
423
|
+
} finally {
|
|
424
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
425
|
+
if (onAbort !== void 0) signal.removeEventListener("abort", onAbort);
|
|
409
426
|
}
|
|
410
|
-
return true;
|
|
411
427
|
}
|
|
412
|
-
async function executeRewind(ctx, store, invocation, rawTarget, mode) {
|
|
428
|
+
async function executeRewind(ctx, store, invocation, rawTarget, mode, inflight) {
|
|
413
429
|
const { agent } = invocation;
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
if (!stopped) {
|
|
418
|
-
return { kind: "error", text: "\u65E0\u6CD5\u505C\u6B62\u8FD0\u884C\u4E2D\u7684 agent\uFF0C\u56DE\u9000\u5DF2\u53D6\u6D88\u3002\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002" };
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
let plan;
|
|
422
|
-
try {
|
|
423
|
-
plan = resolveOrError(agent.session.events, agent.session.surface.nodes, rawTarget);
|
|
424
|
-
} catch (error) {
|
|
425
|
-
return rewindErrorResult(error);
|
|
430
|
+
const sessionId = agent.session.id;
|
|
431
|
+
if (inflight.has(sessionId)) {
|
|
432
|
+
return { kind: "error", text: "\u8BE5\u4F1A\u8BDD\u5DF2\u6709\u4E00\u4E2A\u56DE\u9000\u6B63\u5728\u6267\u884C\uFF0C\u8BF7\u7A0D\u5019\u3002" };
|
|
426
433
|
}
|
|
427
|
-
|
|
428
|
-
let event;
|
|
434
|
+
inflight.add(sessionId);
|
|
429
435
|
try {
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
436
|
+
if (agent.status !== "idle") {
|
|
437
|
+
agent.cancel({ kind: "user" });
|
|
438
|
+
const stopped = await waitForAgentIdle(agent, invocation.signal);
|
|
439
|
+
if (!stopped) {
|
|
440
|
+
return { kind: "error", text: "\u65E0\u6CD5\u505C\u6B62\u8FD0\u884C\u4E2D\u7684 agent\uFF0C\u56DE\u9000\u5DF2\u53D6\u6D88\u3002\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002" };
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
if (invocation.signal.aborted) {
|
|
444
|
+
return { kind: "error", text: "\u56DE\u9000\u5DF2\u53D6\u6D88\u3002" };
|
|
445
|
+
}
|
|
446
|
+
let plan;
|
|
447
|
+
try {
|
|
448
|
+
plan = resolveOrError(agent.session.events, agent.session.surface.nodes, rawTarget);
|
|
449
|
+
} catch (error) {
|
|
450
|
+
return rewindErrorResult(error);
|
|
451
|
+
}
|
|
452
|
+
const marker = buildMarker();
|
|
453
|
+
let event;
|
|
454
|
+
try {
|
|
455
|
+
event = agent.session.append("assistant/message", { turn: markerTurnOf(agent.session.events), step: 0, message: marker }, {
|
|
456
|
+
surfaceOp: { op: "replace", start: plan.surfaceStart, end: plan.surfaceEnd },
|
|
457
|
+
sourceEventSeqs: [...plan.shadowedSeqs]
|
|
458
|
+
});
|
|
459
|
+
} catch (error) {
|
|
460
|
+
return {
|
|
461
|
+
kind: "error",
|
|
462
|
+
text: `\u56DE\u9000\u5931\u8D25\uFF1A${error instanceof Error ? error.message : String(error)}\u3002\u4F1A\u8BDD\u672A\u6539\u53D8\u3002`
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
let restore = "";
|
|
466
|
+
if (mode === "both") {
|
|
467
|
+
const outcome = await store.restoreAfter(agent.session.id, plan.targetSeq, (path) => unlink(path));
|
|
468
|
+
const parts = [];
|
|
469
|
+
if (outcome.restored.length > 0) parts.push(`\u8FD8\u539F ${outcome.restored.length} \u4E2A\u6587\u4EF6`);
|
|
470
|
+
if (outcome.deleted.length > 0) parts.push(`\u5220\u9664 ${outcome.deleted.length} \u4E2A\u6587\u4EF6`);
|
|
471
|
+
if (outcome.skipped.length > 0) parts.push(`\u8DF3\u8FC7 ${outcome.skipped.length} \u4E2A\u94FE\u63A5`);
|
|
472
|
+
restore = parts.length > 0 ? `\uFF1B${parts.join("\u3001")}` : "\uFF1B\u76EE\u6807\u4E4B\u540E\u6CA1\u6709\u53EF\u8FD8\u539F\u7684\u5199\u7C7B\u53D8\u66F4";
|
|
473
|
+
restore += renderFailures(outcome.failed);
|
|
474
|
+
}
|
|
435
475
|
return {
|
|
436
|
-
kind: "
|
|
437
|
-
text: `\
|
|
476
|
+
kind: "success",
|
|
477
|
+
text: `\u5DF2\u64A4\u56DE seq ${plan.targetSeq} \u53CA\u4E4B\u540E\u5185\u5BB9\uFF08\u5BF9\u8BDD\u5DF2\u56DE\u5230\u6B64\u524D\uFF09${restore}\u3002`,
|
|
478
|
+
sourceEventSeq: event.seq
|
|
438
479
|
};
|
|
480
|
+
} finally {
|
|
481
|
+
inflight.delete(sessionId);
|
|
439
482
|
}
|
|
440
|
-
let restore = "";
|
|
441
|
-
if (mode === "both") {
|
|
442
|
-
const outcome = await store.restoreAfter(agent.session.id, plan.targetSeq, (path) => unlink(path));
|
|
443
|
-
const parts = [];
|
|
444
|
-
if (outcome.restored.length > 0) parts.push(`\u8FD8\u539F ${outcome.restored.length} \u4E2A\u6587\u4EF6`);
|
|
445
|
-
if (outcome.deleted.length > 0) parts.push(`\u5220\u9664 ${outcome.deleted.length} \u4E2A\u6587\u4EF6`);
|
|
446
|
-
if (outcome.skipped.length > 0) parts.push(`\u8DF3\u8FC7 ${outcome.skipped.length} \u4E2A\u94FE\u63A5`);
|
|
447
|
-
restore = parts.length > 0 ? `\uFF1B${parts.join("\u3001")}` : "\uFF1B\u76EE\u6807\u4E4B\u540E\u6CA1\u6709\u53EF\u8FD8\u539F\u7684\u5199\u7C7B\u53D8\u66F4";
|
|
448
|
-
restore += renderFailures(outcome.failed);
|
|
449
|
-
}
|
|
450
|
-
return {
|
|
451
|
-
kind: "success",
|
|
452
|
-
text: `\u5DF2\u64A4\u56DE seq ${plan.targetSeq} \u53CA\u4E4B\u540E\u5185\u5BB9\uFF08\u5BF9\u8BDD\u5DF2\u56DE\u5230\u6B64\u524D\uFF09${restore}\u3002`,
|
|
453
|
-
sourceEventSeq: event.seq
|
|
454
|
-
};
|
|
455
483
|
}
|
|
456
484
|
function rewindErrorResult(error) {
|
|
457
485
|
if (error instanceof RewindError) {
|
|
@@ -465,7 +493,7 @@ function rewindErrorResult(error) {
|
|
|
465
493
|
}
|
|
466
494
|
throw error;
|
|
467
495
|
}
|
|
468
|
-
async function handleRewind(ctx, store, invocation) {
|
|
496
|
+
async function handleRewind(ctx, store, invocation, inflight) {
|
|
469
497
|
const session = invocation.agent.session;
|
|
470
498
|
const input = invocation.rawInput.trim();
|
|
471
499
|
if (input === "") {
|
|
@@ -473,7 +501,7 @@ async function handleRewind(ctx, store, invocation) {
|
|
|
473
501
|
if (candidates.length === 0) {
|
|
474
502
|
return { kind: "error", text: "\u5F53\u524D\u4F1A\u8BDD\u8FD8\u6CA1\u6709\u53EF\u56DE\u9000\u7684\u7528\u6237\u6D88\u606F\u3002" };
|
|
475
503
|
}
|
|
476
|
-
return executeRewind(ctx, store, invocation, `@${candidates[0].seq}`, "chat");
|
|
504
|
+
return executeRewind(ctx, store, invocation, `@${candidates[0].seq}`, "chat", inflight);
|
|
477
505
|
}
|
|
478
506
|
const parts = input.split(/\s+/);
|
|
479
507
|
if (parts[0] === "preview") {
|
|
@@ -503,17 +531,18 @@ async function handleRewind(ctx, store, invocation) {
|
|
|
503
531
|
/rewind ${target} both \u56DE\u9000\u5BF9\u8BDD\u5E76\u8FD8\u539F\u6587\u4EF6`
|
|
504
532
|
};
|
|
505
533
|
}
|
|
506
|
-
return executeRewind(ctx, store, invocation, target, mode);
|
|
534
|
+
return executeRewind(ctx, store, invocation, target, mode, inflight);
|
|
507
535
|
}
|
|
508
536
|
function apply(ctx, config) {
|
|
509
537
|
const store = new SnapshotStore(config?.snapshotDir);
|
|
510
538
|
const pending = /* @__PURE__ */ new Map();
|
|
511
|
-
const anchorCache = /* @__PURE__ */ new
|
|
539
|
+
const anchorCache = /* @__PURE__ */ new WeakMap();
|
|
540
|
+
const inflight = /* @__PURE__ */ new Set();
|
|
512
541
|
ctx.effect(function* () {
|
|
513
542
|
yield ctx.commands.register({
|
|
514
543
|
name: "rewind",
|
|
515
544
|
description: "\u5728\u540C\u7A97\u53E3\u5185\u5C06\u5BF9\u8BDD\u56DE\u9000\u5230\u66F4\u65E9\u7684\u7528\u6237\u6D88\u606F\uFF08\u53EF\u540C\u65F6\u8FD8\u539F\u6587\u4EF6\uFF09",
|
|
516
|
-
handler: (invocation) => handleRewind(ctx, store, invocation)
|
|
545
|
+
handler: (invocation) => handleRewind(ctx, store, invocation, inflight)
|
|
517
546
|
});
|
|
518
547
|
}, "dsh-rewind command");
|
|
519
548
|
ctx.inject(["fs"], (scope) => {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @module dsh-rewind/client/hidden
|
|
7
7
|
*/
|
|
8
|
-
import type { ChatConversationViewNode } from '@deepseek-ai/dsh-client-runtime/client';
|
|
8
|
+
import type { ChatConversationViewNode, CommandNode } from '@deepseek-ai/dsh-client-runtime/client';
|
|
9
9
|
/** Minimal chat snapshot reader the hiding logic needs. */
|
|
10
10
|
export interface HiddenChat {
|
|
11
11
|
readonly order: readonly string[];
|
|
@@ -15,6 +15,25 @@ export interface HiddenChat {
|
|
|
15
15
|
}
|
|
16
16
|
/** Extract the rewind target from a command outcome text ("已撤回 seq N..."). */
|
|
17
17
|
export declare function targetOfOutcome(text: string | undefined): number | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* True when a `/rewind` command node is an EXECUTED rewind for `seq` — the
|
|
20
|
+
* admission form the popover drives (`@<seq> chat` / `both`) that settled
|
|
21
|
+
* with a marker-carrying success outcome. The composer refill waits for
|
|
22
|
+
* exactly this node after the user confirms, so a history-loaded command can
|
|
23
|
+
* never trigger a fill.
|
|
24
|
+
*/
|
|
25
|
+
export declare function isExecutedRewindCommand(node: CommandNode, seq: number): boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Whether a preview outcome reports tracked file changes — the availability
|
|
28
|
+
* of the "rewind conversation and code" option (Claude Code hides the
|
|
29
|
+
* code-restore options when the checkpoint has no tracked changes).
|
|
30
|
+
*
|
|
31
|
+
* Prefers the machine-readable `impact=<n>` trailer the current host appends
|
|
32
|
+
* to preview text. Older host output (or a history-loaded preview row from
|
|
33
|
+
* before the trailer existed) has none, so it falls back to the human copy
|
|
34
|
+
* ("将影响 …") to keep mixed-version deployments correct.
|
|
35
|
+
*/
|
|
36
|
+
export declare function hasFileImpact(text: string | undefined): boolean;
|
|
18
37
|
/**
|
|
19
38
|
* Anchor seqs that must be hidden from the rendered transcript so the user
|
|
20
39
|
* sees the conversation as the agent sees it: every impact-preview flow node
|
|
@@ -1,23 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* dsh-rewind client half: the
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* dsh-rewind client half: the manual `/rewind` composer guard, the locale
|
|
3
|
+
* registration, and the session-scoped portal bridge that renders the
|
|
4
|
+
* per-message ↶ rewind button (see `portals.tsx` for the button itself).
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* `
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* popover offers the two modes (step two); "rewind conversation and code"
|
|
15
|
-
* first fetches the impact list via `/rewind preview @seq both` and confirms
|
|
16
|
-
* before executing. Execution always goes through `session.command(...)`, the
|
|
17
|
-
* same host path the `/rewind` command uses.
|
|
6
|
+
* The button is NOT injected by hand into the DOM anymore: the plugin
|
|
7
|
+
* registers a bridge into the harness's `conversation.session.header.actions`
|
|
8
|
+
* list slot, and that bridge portals a React button into every user message's
|
|
9
|
+
* IconActions row — the same rendering family as the copy button (a React
|
|
10
|
+
* child of the actions row), without touching any harness source. The
|
|
11
|
+
* registration is typed structurally (see `SlotsLike` in portals.tsx), so the
|
|
12
|
+
* plugin never imports conversation UI types and survives harness version
|
|
13
|
+
* drift.
|
|
18
14
|
*
|
|
19
15
|
* Manual composer input of `/rewind` is deliberately blocked (the guard
|
|
20
|
-
* below): the command exists only as the per-message button's internal
|
|
16
|
+
* below): the command exists only as the per-message ↶ button's internal
|
|
21
17
|
* channel, so any `/rewind` line typed by hand — bare or with arguments — is
|
|
22
18
|
* stopped with a hint pointing at the button.
|
|
23
19
|
*
|
|
@@ -27,7 +23,7 @@ import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
|
|
|
27
23
|
export declare const name = "dsh-rewind";
|
|
28
24
|
export declare const inject: string[];
|
|
29
25
|
/**
|
|
30
|
-
* Client plugin body:
|
|
31
|
-
* @param ctx - client root context carrying `sessions` and `locale`.
|
|
26
|
+
* Client plugin body: composer guard + locale + the portal bridge.
|
|
27
|
+
* @param ctx - client root context carrying `slots`, `sessions` and `locale`.
|
|
32
28
|
*/
|
|
33
29
|
export declare function apply(ctx: ClientContext): void;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* @module dsh-rewind/client/popover
|
|
8
8
|
*/
|
|
9
9
|
import type { SessionFace } from '@deepseek-ai/dsh-client-runtime/client';
|
|
10
|
+
import type { CommandNode } from '@deepseek-ai/dsh-client-runtime/client';
|
|
10
11
|
import type { RewindKey } from './locales.ts';
|
|
11
12
|
type Translate = (key: RewindKey, params?: Record<string, unknown>) => string;
|
|
12
13
|
export interface PopoverOptions {
|
|
@@ -17,9 +18,32 @@ export interface PopoverOptions {
|
|
|
17
18
|
/** The button that opened the popover (outside-click ignore target). */
|
|
18
19
|
readonly anchor: HTMLElement;
|
|
19
20
|
readonly t: Translate;
|
|
21
|
+
/**
|
|
22
|
+
* Execute one rewind in the given mode. The popover closes itself first;
|
|
23
|
+
* the callback owns the command + composer-refill lifecycle (see
|
|
24
|
+
* runRewindAndFill in index.ts).
|
|
25
|
+
*/
|
|
26
|
+
readonly onRewind: (mode: 'chat' | 'both') => void;
|
|
20
27
|
}
|
|
21
28
|
/** Close the current popover, if any. */
|
|
22
29
|
export declare function closePopover(): void;
|
|
30
|
+
/**
|
|
31
|
+
* Seqs of the command nodes currently matching `match`. Sample BEFORE issuing
|
|
32
|
+
* a new command of the same shape so the subsequent wait can exclude them: a
|
|
33
|
+
* repeated preview/rewind of the same target must not settle on the previous
|
|
34
|
+
* command's stale outcome (e.g. an older preview that found file changes,
|
|
35
|
+
* after those changes were already restored).
|
|
36
|
+
*/
|
|
37
|
+
export declare function knownCommandSeqs(session: SessionFace, match: (node: CommandNode) => boolean): Set<number>;
|
|
38
|
+
/**
|
|
39
|
+
* Resolve the outcome of the newest matching rewind command by watching the
|
|
40
|
+
* session snapshot (command/run + command/done land as one CommandNode).
|
|
41
|
+
* @returns the outcome text-bearing node, or null on timeout.
|
|
42
|
+
*/
|
|
43
|
+
export declare function waitForCommand(session: SessionFace, match: (node: CommandNode) => boolean, timeoutMs?: number): Promise<{
|
|
44
|
+
kind: 'success' | 'error';
|
|
45
|
+
text?: string;
|
|
46
|
+
} | null>;
|
|
23
47
|
/** Open the mode-selection popover anchored near the given button. */
|
|
24
48
|
export declare function openPopover(opts: PopoverOptions): void;
|
|
25
49
|
export {};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-rewind portal half: the per-user-message ↶ rewind button, rendered as a
|
|
3
|
+
* React portal inside the message's `MessageIconActions` row.
|
|
4
|
+
*
|
|
5
|
+
* Why portals (aligned with the copy button's own rendering): the copy button
|
|
6
|
+
* is a React child of the actions row, painted in the same commit as the
|
|
7
|
+
* bubble. A pure-DOM `appendChild` (the earlier approach) lands one microtask
|
|
8
|
+
* later and re-runs a full-transcript scan on EVERY mutation, which can push
|
|
9
|
+
* the paint of a newly sent bubble — the "occasional hiccup before the bubble
|
|
10
|
+
* shows". Portals let React own the button lifecycle (mount/unmount with the
|
|
11
|
+
* row, no orphaned buttons, no manual re-attach after harness re-renders),
|
|
12
|
+
* and the target collection is coalesced (one refresh per mutation batch) and
|
|
13
|
+
* diffed (no setState churn when nothing changed).
|
|
14
|
+
*
|
|
15
|
+
* Mount point: the plugin registers a session-scoped bridge into the harness's
|
|
16
|
+
* `conversation.session.header.actions` list slot. The bridge renders NO
|
|
17
|
+
* header UI — it only portals buttons into the user rows of the session the
|
|
18
|
+
* harness mounts it for. That slot is the harness-native way to get a
|
|
19
|
+
* per-session React mount without touching any source; the registration is
|
|
20
|
+
* typed structurally (see `SlotsLike`) so the plugin never imports the
|
|
21
|
+
* conversation UI package's types and survives its version drift.
|
|
22
|
+
*
|
|
23
|
+
* @module dsh-rewind/client/portals
|
|
24
|
+
*/
|
|
25
|
+
import { type ReactNode } from 'react';
|
|
26
|
+
import type { SessionFace } from '@deepseek-ai/dsh-client-runtime/client';
|
|
27
|
+
import type { RewindKey } from './locales.ts';
|
|
28
|
+
type Translate = (key: RewindKey, params?: Record<string, unknown>) => string;
|
|
29
|
+
/** Capabilities the session-scoped bridge receives from the plugin apply(). */
|
|
30
|
+
export interface RewindBridgeDeps {
|
|
31
|
+
readonly sessionOf: (sessionId: string) => SessionFace | undefined;
|
|
32
|
+
readonly currentSessionId: () => string | undefined;
|
|
33
|
+
readonly t: Translate;
|
|
34
|
+
readonly subscribeLocale: (cb: () => void) => () => void;
|
|
35
|
+
}
|
|
36
|
+
/** Structural face of the runtime slot service (see the module doc). */
|
|
37
|
+
export interface SlotsLike {
|
|
38
|
+
inject(key: string, install: () => () => void): () => void;
|
|
39
|
+
register(entry: {
|
|
40
|
+
readonly name: string;
|
|
41
|
+
readonly id: string;
|
|
42
|
+
readonly order: number;
|
|
43
|
+
}, component: (props: {
|
|
44
|
+
readonly sessionId: string;
|
|
45
|
+
}) => ReactNode): () => void;
|
|
46
|
+
}
|
|
47
|
+
interface RewindPortalsProps extends RewindBridgeDeps {
|
|
48
|
+
readonly sessionId: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Session-scoped portal bridge: renders the ↶ button of every user message
|
|
52
|
+
* row of the session the harness mounts it for. The refresh is coalesced
|
|
53
|
+
* (one pass per mutation batch via queueMicrotask) and diffed (setState is
|
|
54
|
+
* skipped when the target set is unchanged), so the plugin never runs a
|
|
55
|
+
* synchronous full-transcript scan inside a commit microtask.
|
|
56
|
+
*/
|
|
57
|
+
export declare function RewindPortals({ sessionId, sessionOf, currentSessionId, t, subscribeLocale }: RewindPortalsProps): ReactNode;
|
|
58
|
+
/**
|
|
59
|
+
* Build the slot-entry component for the plugin apply(): a tiny bridge that
|
|
60
|
+
* injects the apply-time capabilities (session resolution, locale, rewind
|
|
61
|
+
* runner) into the module-level `RewindPortals`.
|
|
62
|
+
*/
|
|
63
|
+
export declare function createRewindBridge(deps: RewindBridgeDeps): (props: {
|
|
64
|
+
readonly sessionId: string;
|
|
65
|
+
}) => ReactNode;
|
|
66
|
+
export {};
|
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @module dsh-rewind/client/styles
|
|
7
7
|
*/
|
|
8
|
-
/** Marker attribute set on a seat row once its rewind button is attached. */
|
|
9
|
-
export declare const REWIND_ATTACHED = "data-dsh-rewind-attached";
|
|
10
8
|
/** Class names shared between the injected DOM and the stylesheet. */
|
|
11
9
|
export declare const CLASS: {
|
|
12
10
|
readonly button: 'dsh-rewind-btn';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-rewind-plugin",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "DeepSeek Harness plugin: in-place conversation rewind in the same session window (Claude Code /rewind semantics) with optional workspace file restore",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|
|
@@ -54,7 +54,8 @@
|
|
|
54
54
|
"client": {
|
|
55
55
|
"inject": [
|
|
56
56
|
"@deepseek-ai/dsh-client-locale",
|
|
57
|
-
"@deepseek-ai/dsh-client-runtime"
|
|
57
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
58
|
+
"@deepseek-ai/dsh-client-ui-conversation"
|
|
58
59
|
],
|
|
59
60
|
"platform": "web"
|
|
60
61
|
}
|
|
@@ -126,7 +127,11 @@
|
|
|
126
127
|
"@deepseek-ai/dsh-session": "^0.1.0-rc.7",
|
|
127
128
|
"@deepseek-ai/dsh-tools": "^0.1.0-rc.7",
|
|
128
129
|
"@types/node": "^24.0.0",
|
|
130
|
+
"@types/react": "^18.3.31",
|
|
131
|
+
"@types/react-dom": "^18.3.7",
|
|
129
132
|
"esbuild": "^0.28.2",
|
|
133
|
+
"react": "^18.3.1",
|
|
134
|
+
"react-dom": "^18.3.1",
|
|
130
135
|
"typescript": "^7.0.2",
|
|
131
136
|
"vitest": "^4.1.10"
|
|
132
137
|
}
|