pi-rewind-hook 1.8.2 → 1.8.4
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/CHANGELOG.md +14 -0
- package/README.md +12 -0
- package/index.test.ts +161 -12
- package/index.ts +55 -15
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.8.4] - 2026-04-25
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- Added `pi-custom-compaction` compatibility: custom message nodes can now receive exact Rewind checkpoints via the `rewind:checkpoint-entry` extension event.
|
|
11
|
+
|
|
12
|
+
### Documentation
|
|
13
|
+
- Documented seamless `pi-custom-compaction` compatibility and the `pi install npm:pi-custom-compaction` install command.
|
|
14
|
+
|
|
15
|
+
## [1.8.3] - 2026-04-16
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- `session_before_tree` now auto-keeps current files during [pi-boomerang](https://github.com/nicobailon/pi-boomerang) collapses instead of prompting with restore options.
|
|
19
|
+
- Headless boomerang-triggered tree collapses now preserve rewind metadata instead of skipping the keep-current-files snapshot path.
|
|
20
|
+
|
|
7
21
|
## [1.8.2] - 2026-04-05
|
|
8
22
|
|
|
9
23
|
### Changed
|
package/README.md
CHANGED
|
@@ -29,6 +29,18 @@ This will:
|
|
|
29
29
|
4. Remove legacy hooks/explicit rewind extension entries from `settings.json` if present
|
|
30
30
|
5. Clean up old `hooks/rewind` directory (if present)
|
|
31
31
|
|
|
32
|
+
## Works with pi-custom-compaction
|
|
33
|
+
|
|
34
|
+
Rewind works seamlessly with [`pi-custom-compaction`](https://github.com/nicobailon/pi-custom-compaction). When `pi-custom-compaction` writes visible background-summary markers, it can ask Rewind to bind an exact file checkpoint to that marker’s own session-tree node. That means `/tree` can offer the normal `Restore files to that point` option directly on the custom compaction marker instead of falling back to a nearby parent node.
|
|
35
|
+
|
|
36
|
+
Install the companion extension with:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pi install npm:pi-custom-compaction
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
This is optional. Pi’s default compaction and Rewind’s native compaction checkpoints continue to work without `pi-custom-compaction`.
|
|
43
|
+
|
|
32
44
|
## Configuration
|
|
33
45
|
|
|
34
46
|
Optionally add settings to `~/.pi/agent/settings.json`. For example:
|
package/index.test.ts
CHANGED
|
@@ -7,13 +7,15 @@ import os from "node:os";
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { promisify } from "node:util";
|
|
9
9
|
|
|
10
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
11
|
+
|
|
10
12
|
import rewindExtension from "./index.ts";
|
|
11
13
|
|
|
12
14
|
const execFileAsync = promisify(execFile);
|
|
13
15
|
const STORE_REF = "refs/pi-rewind/store";
|
|
14
16
|
|
|
15
17
|
type RewindEntry = Record<string, unknown>;
|
|
16
|
-
type EventHandler = (event:
|
|
18
|
+
type EventHandler = (event: unknown, ctx: unknown) => Promise<unknown> | unknown;
|
|
17
19
|
|
|
18
20
|
class SessionManagerStub {
|
|
19
21
|
private readonly header: { type: "session"; version: number; id: string; timestamp: string; cwd: string; parentSession?: string };
|
|
@@ -97,11 +99,14 @@ async function runGit(repoRoot: string, args: string[]): Promise<{ stdout: strin
|
|
|
97
99
|
try {
|
|
98
100
|
const { stdout, stderr } = await execFileAsync("git", args, { cwd: repoRoot });
|
|
99
101
|
return { stdout, stderr, code: 0 };
|
|
100
|
-
} catch (error:
|
|
102
|
+
} catch (error: unknown) {
|
|
103
|
+
const execError = error && typeof error === "object"
|
|
104
|
+
? error as Partial<{ stdout: string; stderr: string; message: string; code: number }>
|
|
105
|
+
: undefined;
|
|
101
106
|
return {
|
|
102
|
-
stdout:
|
|
103
|
-
stderr:
|
|
104
|
-
code:
|
|
107
|
+
stdout: execError?.stdout ?? "",
|
|
108
|
+
stderr: execError?.stderr ?? execError?.message ?? "",
|
|
109
|
+
code: execError?.code ?? 1,
|
|
105
110
|
};
|
|
106
111
|
}
|
|
107
112
|
}
|
|
@@ -161,7 +166,7 @@ async function createHarness(options: {
|
|
|
161
166
|
await runGitChecked(repoRoot, ["config", "user.email", "rewind@example.com"]);
|
|
162
167
|
|
|
163
168
|
const handlers = new Map<string, EventHandler>();
|
|
164
|
-
const eventHandlers = new Map<string, (data:
|
|
169
|
+
const eventHandlers = new Map<string, (data: unknown) => void>();
|
|
165
170
|
const execCalls: string[][] = [];
|
|
166
171
|
const notifications: Array<{ message: string; level: string }> = [];
|
|
167
172
|
const statusUpdates: Array<{ key: string; value: string | undefined }> = [];
|
|
@@ -200,15 +205,15 @@ async function createHarness(options: {
|
|
|
200
205
|
handlers.set(eventName, handler);
|
|
201
206
|
},
|
|
202
207
|
events: {
|
|
203
|
-
on: (eventName: string, handler: (data:
|
|
208
|
+
on: (eventName: string, handler: (data: unknown) => void) => {
|
|
204
209
|
eventHandlers.set(eventName, handler);
|
|
205
210
|
},
|
|
206
211
|
},
|
|
207
|
-
}
|
|
212
|
+
} satisfies Pick<ExtensionAPI, "exec" | "appendEntry" | "on" | "events">;
|
|
208
213
|
|
|
209
|
-
rewindExtension(api);
|
|
214
|
+
rewindExtension(api as ExtensionAPI);
|
|
210
215
|
|
|
211
|
-
function createContext(sessionManager: SessionManagerStub, hasUI = true)
|
|
216
|
+
function createContext(sessionManager: SessionManagerStub, hasUI = true) {
|
|
212
217
|
return {
|
|
213
218
|
cwd: repoRoot,
|
|
214
219
|
hasUI,
|
|
@@ -259,7 +264,7 @@ async function createHarness(options: {
|
|
|
259
264
|
entries: options.entries,
|
|
260
265
|
});
|
|
261
266
|
},
|
|
262
|
-
async invoke(eventName: string, event:
|
|
267
|
+
async invoke(eventName: string, event: unknown, sessionManager = activeSession, hasUI = true) {
|
|
263
268
|
const handler = handlers.get(eventName);
|
|
264
269
|
assert.ok(handler, `missing handler for ${eventName}`);
|
|
265
270
|
activeSession = sessionManager;
|
|
@@ -433,6 +438,151 @@ test("session_before_tree gracefully cancels when restore fails", async () => {
|
|
|
433
438
|
}
|
|
434
439
|
});
|
|
435
440
|
|
|
441
|
+
test("session_before_tree restores directly from a custom message with a checkpoint", async () => {
|
|
442
|
+
const harness = await createHarness({
|
|
443
|
+
settings: { rewind: { silentCheckpoints: true } },
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
try {
|
|
447
|
+
await harness.writeRepoFile("notes.txt", "target state\n");
|
|
448
|
+
const targetCommit = await harness.captureSnapshot();
|
|
449
|
+
await harness.writeRepoFile("notes.txt", "current state\n");
|
|
450
|
+
|
|
451
|
+
harness.currentSession.replaceEntries([
|
|
452
|
+
{
|
|
453
|
+
type: "custom_message",
|
|
454
|
+
id: "marker-1",
|
|
455
|
+
parentId: null,
|
|
456
|
+
timestamp: new Date().toISOString(),
|
|
457
|
+
customType: "pi-custom-compaction.virtual-summary-marker",
|
|
458
|
+
content: "Older context was summarized in the background.",
|
|
459
|
+
display: true,
|
|
460
|
+
},
|
|
461
|
+
{
|
|
462
|
+
type: "custom",
|
|
463
|
+
id: "rewind-op-1",
|
|
464
|
+
parentId: "marker-1",
|
|
465
|
+
timestamp: new Date().toISOString(),
|
|
466
|
+
customType: "rewind-op",
|
|
467
|
+
data: { v: 2, snapshots: [targetCommit], bindings: [["marker-1", 0]] },
|
|
468
|
+
},
|
|
469
|
+
]);
|
|
470
|
+
|
|
471
|
+
await harness.invoke("session_start", {});
|
|
472
|
+
harness.enqueueSelection("Restore files to that point");
|
|
473
|
+
|
|
474
|
+
const result = await harness.invoke("session_before_tree", { preparation: { targetId: "marker-1" } });
|
|
475
|
+
assert.equal(result, undefined);
|
|
476
|
+
assert.equal(harness.readRepoFile("notes.txt"), "target state\n");
|
|
477
|
+
assert.deepEqual(harness.selectCalls[0]?.options, ["Keep current files", "Restore files to that point", "Cancel navigation"]);
|
|
478
|
+
} finally {
|
|
479
|
+
await harness.cleanup();
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test("rewind:checkpoint-entry binds the current tree to a custom message", async () => {
|
|
484
|
+
const harness = await createHarness({
|
|
485
|
+
settings: { rewind: { silentCheckpoints: true } },
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
try {
|
|
489
|
+
await harness.writeRepoFile("notes.txt", "target state\n");
|
|
490
|
+
harness.currentSession.replaceEntries([
|
|
491
|
+
{
|
|
492
|
+
type: "custom_message",
|
|
493
|
+
id: "marker-1",
|
|
494
|
+
parentId: null,
|
|
495
|
+
timestamp: new Date().toISOString(),
|
|
496
|
+
customType: "pi-custom-compaction.virtual-summary-marker",
|
|
497
|
+
content: "Older context was summarized in the background.",
|
|
498
|
+
display: true,
|
|
499
|
+
},
|
|
500
|
+
]);
|
|
501
|
+
|
|
502
|
+
await harness.invoke("session_start", {});
|
|
503
|
+
harness.eventHandlers.get("rewind:checkpoint-entry")?.({ source: "pi-custom-compaction", entryId: "marker-1" });
|
|
504
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
505
|
+
await harness.writeRepoFile("notes.txt", "current state\n");
|
|
506
|
+
|
|
507
|
+
harness.enqueueSelection("Restore files to that point");
|
|
508
|
+
const result = await harness.invoke("session_before_tree", { preparation: { targetId: "marker-1" } });
|
|
509
|
+
assert.equal(result, undefined);
|
|
510
|
+
assert.equal(harness.readRepoFile("notes.txt"), "target state\n");
|
|
511
|
+
} finally {
|
|
512
|
+
await harness.cleanup();
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
test("session_before_tree auto-keeps current files during boomerang collapse", async () => {
|
|
517
|
+
const harness = await createHarness({
|
|
518
|
+
settings: { rewind: { silentCheckpoints: true } },
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
try {
|
|
522
|
+
await harness.writeRepoFile("notes.txt", "current state\n");
|
|
523
|
+
|
|
524
|
+
harness.currentSession.replaceEntries([
|
|
525
|
+
{
|
|
526
|
+
type: "message",
|
|
527
|
+
id: "user-1",
|
|
528
|
+
parentId: null,
|
|
529
|
+
timestamp: new Date().toISOString(),
|
|
530
|
+
message: { role: "user", content: [{ type: "text", text: "Tree target" }] },
|
|
531
|
+
},
|
|
532
|
+
]);
|
|
533
|
+
|
|
534
|
+
await harness.invoke("session_start", {});
|
|
535
|
+
(globalThis as typeof globalThis & { __boomerangCollapseInProgress?: boolean }).__boomerangCollapseInProgress = true;
|
|
536
|
+
|
|
537
|
+
const result = await harness.invoke("session_before_tree", { preparation: { targetId: "user-1" } });
|
|
538
|
+
assert.equal(result, undefined);
|
|
539
|
+
assert.equal(harness.selectCalls.length, 0);
|
|
540
|
+
|
|
541
|
+
await harness.invoke("session_tree", { summaryEntry: { id: "summary-1" } });
|
|
542
|
+
|
|
543
|
+
const rewindOps = harness.currentSession.getEntries().filter((entry) => entry.type === "custom" && entry.customType === "rewind-op");
|
|
544
|
+
assert.equal(rewindOps.length, 1);
|
|
545
|
+
} finally {
|
|
546
|
+
delete (globalThis as typeof globalThis & { __boomerangCollapseInProgress?: boolean }).__boomerangCollapseInProgress;
|
|
547
|
+
await harness.cleanup();
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
test("session_before_tree auto-keeps current files during headless boomerang collapse", async () => {
|
|
552
|
+
const harness = await createHarness({
|
|
553
|
+
settings: { rewind: { silentCheckpoints: true } },
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
await harness.writeRepoFile("notes.txt", "current state\n");
|
|
558
|
+
|
|
559
|
+
harness.currentSession.replaceEntries([
|
|
560
|
+
{
|
|
561
|
+
type: "message",
|
|
562
|
+
id: "user-1",
|
|
563
|
+
parentId: null,
|
|
564
|
+
timestamp: new Date().toISOString(),
|
|
565
|
+
message: { role: "user", content: [{ type: "text", text: "Tree target" }] },
|
|
566
|
+
},
|
|
567
|
+
]);
|
|
568
|
+
|
|
569
|
+
await harness.invoke("session_start", {});
|
|
570
|
+
(globalThis as typeof globalThis & { __boomerangCollapseInProgress?: boolean }).__boomerangCollapseInProgress = true;
|
|
571
|
+
|
|
572
|
+
const result = await harness.invoke("session_before_tree", { preparation: { targetId: "user-1" } }, harness.currentSession, false);
|
|
573
|
+
assert.equal(result, undefined);
|
|
574
|
+
assert.equal(harness.selectCalls.length, 0);
|
|
575
|
+
|
|
576
|
+
await harness.invoke("session_tree", { summaryEntry: { id: "summary-1" } }, harness.currentSession, false);
|
|
577
|
+
|
|
578
|
+
const rewindOps = harness.currentSession.getEntries().filter((entry) => entry.type === "custom" && entry.customType === "rewind-op");
|
|
579
|
+
assert.equal(rewindOps.length, 1);
|
|
580
|
+
} finally {
|
|
581
|
+
delete (globalThis as typeof globalThis & { __boomerangCollapseInProgress?: boolean }).__boomerangCollapseInProgress;
|
|
582
|
+
await harness.cleanup();
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
|
|
436
586
|
test("first mutating turn creates a reachable store ref even when retention is omitted", async () => {
|
|
437
587
|
const harness = await createHarness({
|
|
438
588
|
settings: { rewind: { silentCheckpoints: true } },
|
|
@@ -600,4 +750,3 @@ test("retention rewrites the keepalive ref when a live snapshot exists", async (
|
|
|
600
750
|
await harness.cleanup();
|
|
601
751
|
}
|
|
602
752
|
});
|
|
603
|
-
|
package/index.ts
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Rewind Extension - session-ledger based exact file restoration for pi branching
|
|
3
|
-
*
|
|
4
|
-
* Rewind v2 stores exact rewind metadata in hidden session custom entries and keeps
|
|
5
|
-
* snapshot commits reachable through a single repo-local store ref.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
1
|
import { getAgentDir, type ExtensionAPI, type ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
9
2
|
import { exec as execCb } from "child_process";
|
|
10
3
|
import { existsSync, readFileSync, realpathSync } from "fs";
|
|
@@ -18,6 +11,7 @@ const execAsync = promisify(execCb);
|
|
|
18
11
|
const STORE_REF = "refs/pi-rewind/store";
|
|
19
12
|
const STATUS_KEY = "rewind";
|
|
20
13
|
const FORK_PREFERENCE_SOURCE_ALLOWLIST = new Set(["fork-from-first"]);
|
|
14
|
+
const CHECKPOINT_SOURCE_ALLOWLIST = new Set(["pi-custom-compaction"]);
|
|
21
15
|
const LEGACY_ZERO_SHA = "0000000000000000000000000000000000000000";
|
|
22
16
|
const RETENTION_SWEEP_THRESHOLD = 50;
|
|
23
17
|
const RETENTION_VERSION = 2;
|
|
@@ -323,7 +317,7 @@ function isRestorableTreeEntry(entry: SessionLikeEntry | undefined): boolean {
|
|
|
323
317
|
if (entry.type === "message") {
|
|
324
318
|
return entry.message.role === "user" || entry.message.role === "assistant";
|
|
325
319
|
}
|
|
326
|
-
return entry.type === "branch_summary" || entry.type === "compaction";
|
|
320
|
+
return entry.type === "branch_summary" || entry.type === "compaction" || entry.type === "custom_message";
|
|
327
321
|
}
|
|
328
322
|
|
|
329
323
|
function isAssistantMessageEntry(entry: SessionLikeEntry): entry is SessionLikeMessageEntry {
|
|
@@ -783,14 +777,19 @@ export default function rewindExtension(pi: ExtensionAPI) {
|
|
|
783
777
|
const lines = content.split("\n").filter(Boolean);
|
|
784
778
|
|
|
785
779
|
for (const line of lines) {
|
|
786
|
-
let entry:
|
|
780
|
+
let entry: SessionLikeGenericEntry | null = null;
|
|
787
781
|
try {
|
|
788
|
-
|
|
782
|
+
const parsed = JSON.parse(line);
|
|
783
|
+
if (parsed && typeof parsed === "object") {
|
|
784
|
+
entry = parsed as SessionLikeGenericEntry;
|
|
785
|
+
}
|
|
789
786
|
} catch {
|
|
790
787
|
// Ignore malformed JSONL lines; retention discovery is best-effort.
|
|
791
788
|
continue;
|
|
792
789
|
}
|
|
793
790
|
|
|
791
|
+
if (!entry) continue;
|
|
792
|
+
|
|
794
793
|
if (entry?.type === "session") {
|
|
795
794
|
ledger.sessionId = entry.id;
|
|
796
795
|
ledger.cwd = entry.cwd;
|
|
@@ -1090,7 +1089,27 @@ export default function rewindExtension(pi: ExtensionAPI) {
|
|
|
1090
1089
|
updateStatus(ctx);
|
|
1091
1090
|
}
|
|
1092
1091
|
|
|
1092
|
+
let activeContext: ExtensionContext | undefined;
|
|
1093
|
+
|
|
1094
|
+
async function checkpointEntry(ctx: ExtensionContext, entryId: string) {
|
|
1095
|
+
syncSessionIdentity(ctx);
|
|
1096
|
+
if (!isGitRepo) return;
|
|
1097
|
+
const entry = ctx.sessionManager.getEntry(entryId) as SessionLikeEntry | undefined;
|
|
1098
|
+
if (!isRestorableTreeEntry(entry)) return;
|
|
1099
|
+
if (entryToCommit.has(entryId)) return;
|
|
1100
|
+
|
|
1101
|
+
const currentCommitSha = await ensureSnapshotForCurrentWorktree();
|
|
1102
|
+
appendRewindOp(ctx, {
|
|
1103
|
+
v: RETENTION_VERSION,
|
|
1104
|
+
snapshots: [currentCommitSha],
|
|
1105
|
+
bindings: [[entryId, 0]],
|
|
1106
|
+
});
|
|
1107
|
+
await reconstructState(ctx);
|
|
1108
|
+
updateStatus(ctx);
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1093
1111
|
async function initializeForSession(ctx: ExtensionContext) {
|
|
1112
|
+
activeContext = ctx;
|
|
1094
1113
|
resetState();
|
|
1095
1114
|
syncSessionIdentity(ctx);
|
|
1096
1115
|
|
|
@@ -1115,14 +1134,28 @@ export default function rewindExtension(pi: ExtensionAPI) {
|
|
|
1115
1134
|
});
|
|
1116
1135
|
}
|
|
1117
1136
|
|
|
1118
|
-
pi.events.on("rewind:fork-preference", (data
|
|
1119
|
-
if (data
|
|
1120
|
-
if (
|
|
1137
|
+
pi.events.on("rewind:fork-preference", (data) => {
|
|
1138
|
+
if (!data || typeof data !== "object") return;
|
|
1139
|
+
if (!("mode" in data) || data.mode !== "conversation-only") return;
|
|
1140
|
+
if (!("source" in data) || typeof data.source !== "string") return;
|
|
1121
1141
|
if (!FORK_PREFERENCE_SOURCE_ALLOWLIST.has(data.source)) return;
|
|
1122
1142
|
forceConversationOnlyOnNextFork = true;
|
|
1123
1143
|
forceConversationOnlySource = data.source;
|
|
1124
1144
|
});
|
|
1125
1145
|
|
|
1146
|
+
pi.events.on("rewind:checkpoint-entry", (data) => {
|
|
1147
|
+
if (!data || typeof data !== "object") return;
|
|
1148
|
+
if (!("source" in data) || typeof data.source !== "string") return;
|
|
1149
|
+
if (!CHECKPOINT_SOURCE_ALLOWLIST.has(data.source)) return;
|
|
1150
|
+
if (!("entryId" in data) || typeof data.entryId !== "string") return;
|
|
1151
|
+
const ctx = activeContext;
|
|
1152
|
+
if (!ctx) return;
|
|
1153
|
+
|
|
1154
|
+
checkpointEntry(ctx, data.entryId).catch((error) => {
|
|
1155
|
+
notify(ctx, `Rewind: failed to checkpoint ${data.entryId} (${error instanceof Error ? error.message : String(error)})`, "warning");
|
|
1156
|
+
});
|
|
1157
|
+
});
|
|
1158
|
+
|
|
1126
1159
|
pi.on("before_agent_start", async (event) => {
|
|
1127
1160
|
activePromptText = event.prompt;
|
|
1128
1161
|
});
|
|
@@ -1342,9 +1375,16 @@ export default function rewindExtension(pi: ExtensionAPI) {
|
|
|
1342
1375
|
});
|
|
1343
1376
|
|
|
1344
1377
|
pi.on("session_before_tree", async (event, ctx) => {
|
|
1345
|
-
if (!isGitRepo
|
|
1378
|
+
if (!isGitRepo) return;
|
|
1346
1379
|
|
|
1347
1380
|
try {
|
|
1381
|
+
if ((globalThis as typeof globalThis & { __boomerangCollapseInProgress?: boolean }).__boomerangCollapseInProgress === true) {
|
|
1382
|
+
pendingTreeState = { currentCommitSha: await ensureSnapshotForCurrentWorktree() };
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
if (!ctx.hasUI) return;
|
|
1387
|
+
|
|
1348
1388
|
const targetEntry = ctx.sessionManager.getEntry(event.preparation.targetId) as SessionLikeEntry | undefined;
|
|
1349
1389
|
const targetCommitSha = isRestorableTreeEntry(targetEntry)
|
|
1350
1390
|
? await resolveEntrySnapshotWithLineage(event.preparation.targetId, currentSessionFile)
|
|
@@ -1386,7 +1426,7 @@ export default function rewindExtension(pi: ExtensionAPI) {
|
|
|
1386
1426
|
}
|
|
1387
1427
|
|
|
1388
1428
|
if (!targetCommitSha) {
|
|
1389
|
-
notify(ctx, "Exact file rewind is only available for user, assistant, compaction, and summary nodes", "error");
|
|
1429
|
+
notify(ctx, "Exact file rewind is only available for user, assistant, custom message, compaction, and summary nodes", "error");
|
|
1390
1430
|
return { cancel: true };
|
|
1391
1431
|
}
|
|
1392
1432
|
|