@bachi/pi-coder 1.0.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/CHANGELOG.md +24 -0
- package/LICENSE +21 -0
- package/README.md +162 -0
- package/config/AGENTS.md +100 -0
- package/config/pi-statusline.json +140 -0
- package/config/settings.json +38 -0
- package/config/web-search.json +5 -0
- package/docs/README.md +14 -0
- package/docs/configuration.md +123 -0
- package/docs/development.md +177 -0
- package/docs/extensions.md +292 -0
- package/docs/handbook.zh.md +432 -0
- package/docs/installation.md +124 -0
- package/docs/themes.md +107 -0
- package/extensions/ask-user-question/answers.test.ts +104 -0
- package/extensions/ask-user-question/answers.ts +72 -0
- package/extensions/ask-user-question/dialog.test.ts +180 -0
- package/extensions/ask-user-question/dialog.ts +102 -0
- package/extensions/ask-user-question/index.ts +253 -0
- package/extensions/ask-user-question/model.test.ts +275 -0
- package/extensions/ask-user-question/model.ts +259 -0
- package/extensions/ask-user-question/schema.ts +49 -0
- package/extensions/ask-user-question/types.ts +86 -0
- package/extensions/ask-user-question/validate.test.ts +183 -0
- package/extensions/ask-user-question/validate.ts +110 -0
- package/extensions/ask-user-question/view.ts +262 -0
- package/extensions/auto-default-model/default-model.test.ts +268 -0
- package/extensions/auto-default-model/index.ts +87 -0
- package/extensions/bash-command-collapse.ts +1476 -0
- package/extensions/below-editor-after-statusline.ts +118 -0
- package/extensions/clear-command.ts +29 -0
- package/extensions/cwd-statusline.ts +39 -0
- package/extensions/exit-command.ts +59 -0
- package/extensions/fenceless-code-block/index.test.ts +208 -0
- package/extensions/fenceless-code-block/index.ts +28 -0
- package/extensions/fenceless-code-block/render.test.ts +177 -0
- package/extensions/fenceless-code-block/render.ts +142 -0
- package/extensions/folder-history.ts +197 -0
- package/extensions/init-command.ts +163 -0
- package/extensions/prompt-editor/bash-prompt.test.ts +94 -0
- package/extensions/prompt-editor/bash-prompt.ts +59 -0
- package/extensions/prompt-editor/render.test.ts +283 -0
- package/extensions/prompt-editor.ts +212 -0
- package/extensions/read-path-collapse.ts +474 -0
- package/extensions/recap/index.test.ts +348 -0
- package/extensions/recap/index.ts +462 -0
- package/extensions/recap/subagents.test.ts +144 -0
- package/extensions/recap/subagents.ts +128 -0
- package/extensions/rewind/README.md +229 -0
- package/extensions/rewind/checkpoints.test.ts +560 -0
- package/extensions/rewind/checkpoints.ts +820 -0
- package/extensions/rewind/flow.test.ts +756 -0
- package/extensions/rewind/flow.ts +362 -0
- package/extensions/rewind/index.ts +400 -0
- package/extensions/rewind/picker.ts +135 -0
- package/extensions/rewind/viewport.test.ts +76 -0
- package/extensions/rewind/viewport.ts +48 -0
- package/extensions/simple-task/gap.test.ts +147 -0
- package/extensions/simple-task/gap.ts +122 -0
- package/extensions/simple-task/index.ts +439 -0
- package/extensions/simple-task/types.ts +53 -0
- package/extensions/simple-task/widget.ts +86 -0
- package/extensions/startup-logo/header-guard.test.ts +274 -0
- package/extensions/startup-logo/header-guard.ts +166 -0
- package/extensions/startup-logo/index.test.ts +305 -0
- package/extensions/startup-logo/index.ts +194 -0
- package/extensions/startup-logo/loaded-sections.test.ts +257 -0
- package/extensions/startup-logo/loaded-sections.ts +267 -0
- package/extensions/startup-logo/logo.test.ts +124 -0
- package/extensions/startup-logo/logo.ts +124 -0
- package/extensions/statusline/footer-guard.test.ts +273 -0
- package/extensions/statusline/footer-guard.ts +171 -0
- package/extensions/statusline/git.test.ts +174 -0
- package/extensions/statusline/git.ts +142 -0
- package/extensions/statusline/index.ts +294 -0
- package/extensions/statusline/line.test.ts +316 -0
- package/extensions/statusline/line.ts +201 -0
- package/extensions/subagent-log-guard/filter.test.ts +85 -0
- package/extensions/subagent-log-guard/filter.ts +32 -0
- package/extensions/subagent-log-guard/index.ts +112 -0
- package/extensions/theme-command.ts +263 -0
- package/extensions/thinking-collapse/window.test.ts +321 -0
- package/extensions/thinking-collapse/window.ts +354 -0
- package/extensions/thinking-collapse.ts +60 -0
- package/extensions/tool-diff/title-row.test.ts +254 -0
- package/extensions/tool-diff/title-row.ts +191 -0
- package/extensions/tool-diff.ts +1276 -0
- package/extensions/working-indicator/bash-spinner.test.ts +135 -0
- package/extensions/working-indicator/bash-spinner.ts +114 -0
- package/extensions/working-indicator/index.test.ts +579 -0
- package/extensions/working-indicator/index.ts +940 -0
- package/extensions/working-indicator/spinner-frames.test.ts +219 -0
- package/extensions/working-indicator/spinner-frames.ts +156 -0
- package/extensions/working-indicator/summary-request.test.ts +195 -0
- package/extensions/working-indicator/summary-request.ts +207 -0
- package/extensions/working-indicator/working-summary.test.ts +499 -0
- package/extensions/working-indicator/working-summary.ts +375 -0
- package/package.json +71 -0
- package/themes/ayu.json +97 -0
- package/themes/catppuccin.json +103 -0
- package/themes/summer-night.json +87 -0
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi-rewind flow — the /rewind menu.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors Claude Code's rewind menu: pick one of the prompts you sent during
|
|
5
|
+
* this session, then choose what to roll back.
|
|
6
|
+
*
|
|
7
|
+
* Restore code and conversation → files + session tree back to that point
|
|
8
|
+
* Restore conversation → session tree only (pi puts the prompt
|
|
9
|
+
* text back into the editor, same as
|
|
10
|
+
* Claude Code)
|
|
11
|
+
* Restore code → files only — the worktree changed since
|
|
12
|
+
* the checkpoint, plus the pre-images of
|
|
13
|
+
* files the agent edited outside the
|
|
14
|
+
* project root (or in an ignored path)
|
|
15
|
+
* Summarize from here → compress everything after that point
|
|
16
|
+
* into a branch summary (pi's native
|
|
17
|
+
* navigateTree summarization)
|
|
18
|
+
* Never mind → back out
|
|
19
|
+
*
|
|
20
|
+
* Claude Code's "Summarize up to here" has no pi equivalent (pi can summarize
|
|
21
|
+
* an abandoned branch or compact the whole context, but not "everything before
|
|
22
|
+
* an entry while keeping later messages"), so it is not offered.
|
|
23
|
+
*
|
|
24
|
+
* The checkpoint picker itself lives in picker.ts: pi's built-in
|
|
25
|
+
* `ctx.ui.select()` renders every row at once, which pushes the cursor row out
|
|
26
|
+
* of the viewport once a session has enough checkpoints. `deps.pick` is the
|
|
27
|
+
* seam — index.ts passes the scrolling picker, hosts that cannot render
|
|
28
|
+
* extension components fall back to `ctx.ui.select()` here.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
32
|
+
|
|
33
|
+
import {
|
|
34
|
+
type Checkpoint,
|
|
35
|
+
type CheckpointDiff,
|
|
36
|
+
type ExternalFile,
|
|
37
|
+
type RewindStore,
|
|
38
|
+
captureFileStates,
|
|
39
|
+
createCheckpoint,
|
|
40
|
+
deleteCheckpoint,
|
|
41
|
+
diffExternalFiles,
|
|
42
|
+
diffTrees,
|
|
43
|
+
externalRestorePlan,
|
|
44
|
+
latestSafetyCheckpoint,
|
|
45
|
+
listCheckpoints,
|
|
46
|
+
mergeDiffs,
|
|
47
|
+
restoreCheckpoint,
|
|
48
|
+
snapshotTree,
|
|
49
|
+
summarizeDiff,
|
|
50
|
+
} from "./checkpoints.ts";
|
|
51
|
+
|
|
52
|
+
/** Longest prompt snippet shown in the picker. */
|
|
53
|
+
const PROMPT_SNIPPET_LENGTH = 60;
|
|
54
|
+
/** How many changed paths the confirmation dialog lists before truncating. */
|
|
55
|
+
const MAX_DIFF_LINES = 15;
|
|
56
|
+
/** Title of the checkpoint picker dialog. */
|
|
57
|
+
export const PICKER_TITLE = "Rewind to a checkpoint";
|
|
58
|
+
|
|
59
|
+
/** Values the picker hands back: the two synthetic rows, plus `cp:<index>`. */
|
|
60
|
+
const UNDO_VALUE = "undo";
|
|
61
|
+
const NEVER_MIND_VALUE = "never-mind";
|
|
62
|
+
const CHECKPOINT_VALUE_PREFIX = "cp:";
|
|
63
|
+
|
|
64
|
+
export const UNDO_LABEL = "↩ Undo last rewind (restore files from before it)";
|
|
65
|
+
export const NEVER_MIND_LABEL = "Never mind";
|
|
66
|
+
|
|
67
|
+
export interface RewindFlowDeps {
|
|
68
|
+
store: RewindStore;
|
|
69
|
+
sessionId: string;
|
|
70
|
+
/**
|
|
71
|
+
* Scrollable picker, wired up by index.ts to `pickFromList` (picker.ts).
|
|
72
|
+
* Omitted in tests and on hosts that cannot render extension components —
|
|
73
|
+
* `pickValue` then uses `ctx.ui.select()`, which renders every row at once.
|
|
74
|
+
*/
|
|
75
|
+
pick?: Picker;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** One row of the picker. `value` is what the flow acts on, `label` is shown. */
|
|
79
|
+
export interface PickerItem {
|
|
80
|
+
value: string;
|
|
81
|
+
label: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type PickOutcome =
|
|
85
|
+
| { status: "picked"; value: string }
|
|
86
|
+
| { status: "cancelled" }
|
|
87
|
+
| { status: "unsupported" };
|
|
88
|
+
|
|
89
|
+
export type Picker = (
|
|
90
|
+
ctx: ExtensionCommandContext,
|
|
91
|
+
title: string,
|
|
92
|
+
items: PickerItem[],
|
|
93
|
+
) => Promise<PickOutcome>;
|
|
94
|
+
|
|
95
|
+
/** Labels for the picker, oldest → newest index numbering. */
|
|
96
|
+
export function formatCheckpointLabel(checkpoint: Checkpoint, position: number): string {
|
|
97
|
+
const time = formatTime(checkpoint.timestamp);
|
|
98
|
+
if (checkpoint.kind === "session-start") return `#${position} ${time} Session start`;
|
|
99
|
+
if (checkpoint.kind === "safety") return `#${position} ${time} Before a rewind`;
|
|
100
|
+
const snippet = snippetOf(checkpoint.prompt);
|
|
101
|
+
return `#${position} ${time} ${snippet}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function snippetOf(prompt: string): string {
|
|
105
|
+
const firstLine = (prompt || "").split("\n")[0]?.trim() ?? "";
|
|
106
|
+
if (!firstLine) return "(empty prompt)";
|
|
107
|
+
if (firstLine.length <= PROMPT_SNIPPET_LENGTH) return firstLine;
|
|
108
|
+
return `${firstLine.slice(0, PROMPT_SNIPPET_LENGTH - 1)}…`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function formatTime(timestamp: number): string {
|
|
112
|
+
const date = new Date(timestamp);
|
|
113
|
+
const pad = (value: number) => String(value).padStart(2, "0");
|
|
114
|
+
return `${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Render a diff as the lines shown in the restore confirmation dialog. */
|
|
118
|
+
export function formatDiffPreview(diff: CheckpointDiff): string {
|
|
119
|
+
if (diff.empty) return "No file changes.";
|
|
120
|
+
const lines = diff.entries.slice(0, MAX_DIFF_LINES).map((entry) => ` ${statusGlyph(entry.status)} ${entry.path}`);
|
|
121
|
+
if (diff.entries.length > MAX_DIFF_LINES) {
|
|
122
|
+
lines.push(` … and ${diff.entries.length - MAX_DIFF_LINES} more`);
|
|
123
|
+
}
|
|
124
|
+
lines.unshift(` ${summarizeDiff(diff)}`);
|
|
125
|
+
return lines.join("\n");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function statusGlyph(status: string): string {
|
|
129
|
+
switch (status) {
|
|
130
|
+
case "A":
|
|
131
|
+
return "+";
|
|
132
|
+
case "D":
|
|
133
|
+
return "-";
|
|
134
|
+
default:
|
|
135
|
+
return "~";
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface ActionChoice {
|
|
140
|
+
label: string;
|
|
141
|
+
action: "code-and-conversation" | "conversation" | "code" | "summarize" | "cancel";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Build the action menu for a selected checkpoint. Code options only appear
|
|
146
|
+
* when there are file changes to revert, matching Claude Code.
|
|
147
|
+
*/
|
|
148
|
+
export function buildActions(diff: CheckpointDiff, hasConversationTarget: boolean): ActionChoice[] {
|
|
149
|
+
const choices: ActionChoice[] = [];
|
|
150
|
+
const canRestoreCode = !diff.empty;
|
|
151
|
+
|
|
152
|
+
if (canRestoreCode && hasConversationTarget) {
|
|
153
|
+
choices.push({ label: "Restore code and conversation", action: "code-and-conversation" });
|
|
154
|
+
}
|
|
155
|
+
if (hasConversationTarget) {
|
|
156
|
+
choices.push({ label: "Restore conversation (keep current files)", action: "conversation" });
|
|
157
|
+
}
|
|
158
|
+
if (canRestoreCode) {
|
|
159
|
+
choices.push({ label: "Restore code (keep conversation)", action: "code" });
|
|
160
|
+
}
|
|
161
|
+
if (hasConversationTarget) {
|
|
162
|
+
choices.push({ label: "Summarize from here (frees context)", action: "summarize" });
|
|
163
|
+
}
|
|
164
|
+
choices.push({ label: NEVER_MIND_LABEL, action: "cancel" });
|
|
165
|
+
return choices;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Picker rows, in display order: the undo entry, then every checkpoint newest
|
|
170
|
+
* first (the order the index numbering already implies), then "Never mind".
|
|
171
|
+
* Claude Code's menu ends with that explicit row; Esc cancels too.
|
|
172
|
+
*/
|
|
173
|
+
export function buildPickerItems(targets: Checkpoint[], hasUndo: boolean): PickerItem[] {
|
|
174
|
+
const items: PickerItem[] = [];
|
|
175
|
+
if (hasUndo) items.push({ value: UNDO_VALUE, label: UNDO_LABEL });
|
|
176
|
+
for (let i = targets.length - 1; i >= 0; i -= 1) {
|
|
177
|
+
items.push({
|
|
178
|
+
value: `${CHECKPOINT_VALUE_PREFIX}${i}`,
|
|
179
|
+
label: formatCheckpointLabel(targets[i], i + 1),
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
items.push({ value: NEVER_MIND_VALUE, label: NEVER_MIND_LABEL });
|
|
183
|
+
return items;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Ask for a row. The scrollable picker keeps a long checkpoint list inside the
|
|
188
|
+
* viewport; a host that cannot render extension components (RPC, print) — and
|
|
189
|
+
* the test harness — gets pi's plain `ctx.ui.select()` instead, where every row
|
|
190
|
+
* is rendered at once.
|
|
191
|
+
*/
|
|
192
|
+
async function pickValue(
|
|
193
|
+
ctx: ExtensionCommandContext,
|
|
194
|
+
deps: RewindFlowDeps,
|
|
195
|
+
items: PickerItem[],
|
|
196
|
+
): Promise<string | undefined> {
|
|
197
|
+
const outcome: PickOutcome = deps.pick
|
|
198
|
+
? await deps.pick(ctx, PICKER_TITLE, items)
|
|
199
|
+
: { status: "unsupported" };
|
|
200
|
+
if (outcome.status === "picked") return outcome.value;
|
|
201
|
+
if (outcome.status === "cancelled") return undefined;
|
|
202
|
+
|
|
203
|
+
const label = await ctx.ui.select(
|
|
204
|
+
PICKER_TITLE,
|
|
205
|
+
items.map((item) => item.label),
|
|
206
|
+
);
|
|
207
|
+
if (label === undefined) return undefined;
|
|
208
|
+
return items.find((item) => item.label === label)?.value;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Run the whole rewind flow. Returns a short human-readable outcome so the
|
|
213
|
+
* caller can log it; all user-facing messaging happens here.
|
|
214
|
+
*/
|
|
215
|
+
export async function runRewindFlow(
|
|
216
|
+
ctx: ExtensionCommandContext,
|
|
217
|
+
deps: RewindFlowDeps,
|
|
218
|
+
): Promise<string> {
|
|
219
|
+
const { store, sessionId } = deps;
|
|
220
|
+
// Oldest → newest; the menu renders them newest first.
|
|
221
|
+
const targets = listCheckpoints(store, sessionId).filter((cp) => cp.kind !== "safety");
|
|
222
|
+
|
|
223
|
+
if (targets.length === 0) {
|
|
224
|
+
ctx.ui.notify("No checkpoints yet for this session", "warning");
|
|
225
|
+
return "no checkpoints";
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const safety = latestSafetyCheckpoint(store, sessionId);
|
|
229
|
+
const items = buildPickerItems(targets, Boolean(safety));
|
|
230
|
+
const picked = await pickValue(ctx, deps, items);
|
|
231
|
+
if (picked === undefined || picked === NEVER_MIND_VALUE) return "cancelled";
|
|
232
|
+
|
|
233
|
+
if (picked === UNDO_VALUE) {
|
|
234
|
+
if (!safety) return "cancelled";
|
|
235
|
+
return undoLastRewind(ctx, store, safety);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (!picked.startsWith(CHECKPOINT_VALUE_PREFIX)) return "cancelled";
|
|
239
|
+
const index = Number(picked.slice(CHECKPOINT_VALUE_PREFIX.length));
|
|
240
|
+
if (!Number.isInteger(index) || index < 0 || index >= targets.length) return "cancelled";
|
|
241
|
+
const target = targets[index];
|
|
242
|
+
const position = index + 1;
|
|
243
|
+
|
|
244
|
+
// What would a code restore actually change? The worktree diff covers the
|
|
245
|
+
// project; the pre-image plan covers the files it cannot see.
|
|
246
|
+
const files: ExternalFile[] = externalRestorePlan(targets, index);
|
|
247
|
+
const currentTree = await snapshotTree(store);
|
|
248
|
+
const diff = mergeDiffs(
|
|
249
|
+
await diffTrees(store, target.tree, currentTree),
|
|
250
|
+
diffExternalFiles(store, files),
|
|
251
|
+
);
|
|
252
|
+
const hasConversationTarget = Boolean(target.entryId);
|
|
253
|
+
|
|
254
|
+
const actions = buildActions(diff, hasConversationTarget);
|
|
255
|
+
const actionLabel = await ctx.ui.select(
|
|
256
|
+
`#${position} ${snippetOf(target.prompt)} · ${summarizeDiff(diff)}`,
|
|
257
|
+
actions.map((choice) => choice.label),
|
|
258
|
+
);
|
|
259
|
+
if (!actionLabel) return "cancelled";
|
|
260
|
+
|
|
261
|
+
const choice = actions.find((candidate) => candidate.label === actionLabel);
|
|
262
|
+
if (!choice || choice.action === "cancel") return "cancelled";
|
|
263
|
+
|
|
264
|
+
const restoreCode = choice.action === "code-and-conversation" || choice.action === "code";
|
|
265
|
+
const restoreConversation =
|
|
266
|
+
choice.action === "code-and-conversation" || choice.action === "conversation";
|
|
267
|
+
|
|
268
|
+
if (restoreCode) {
|
|
269
|
+
const confirmed = await ctx.ui.confirm(
|
|
270
|
+
"Restore code",
|
|
271
|
+
`Roll the working tree back to checkpoint #${position}?\n\n${formatDiffPreview(diff)}\n\nFiles created after that point are deleted, and files the agent edited outside the project (or in an ignored path) are put back too. Ignored files nobody touched (node_modules, build output) are kept, and your git history is untouched.`,
|
|
272
|
+
);
|
|
273
|
+
if (!confirmed) return "cancelled";
|
|
274
|
+
|
|
275
|
+
// Undo net: snapshot the state we are about to replace.
|
|
276
|
+
try {
|
|
277
|
+
await createCheckpoint(store, {
|
|
278
|
+
sessionId,
|
|
279
|
+
kind: "safety",
|
|
280
|
+
tree: currentTree,
|
|
281
|
+
files: captureFileStates(store, files),
|
|
282
|
+
});
|
|
283
|
+
} catch {
|
|
284
|
+
// Without a safety checkpoint the rewind still proceeds; only the undo
|
|
285
|
+
// entry is missing.
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
try {
|
|
289
|
+
const result = await restoreCheckpoint(store, target, { files });
|
|
290
|
+
ctx.ui.notify(
|
|
291
|
+
`Restored files to checkpoint #${position} (${summarizeDiff(diff)}${
|
|
292
|
+
result.removed.length > 0 ? `, removed ${result.removed.length} newer file(s)` : ""
|
|
293
|
+
})`,
|
|
294
|
+
"info",
|
|
295
|
+
);
|
|
296
|
+
} catch (error) {
|
|
297
|
+
ctx.ui.notify(`Restore failed: ${errorMessage(error)}`, "error");
|
|
298
|
+
return "restore failed";
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (restoreConversation) {
|
|
303
|
+
const outcome = await rewindConversation(ctx, target.entryId!, false);
|
|
304
|
+
if (outcome !== "ok") return outcome;
|
|
305
|
+
} else if (choice.action === "summarize") {
|
|
306
|
+
const outcome = await rewindConversation(ctx, target.entryId!, true);
|
|
307
|
+
if (outcome !== "ok") return outcome;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (choice.action === "summarize") return "summarized";
|
|
311
|
+
if (restoreCode && restoreConversation) return "restored code and conversation";
|
|
312
|
+
if (restoreCode) return "restored code";
|
|
313
|
+
return "restored conversation";
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/** Navigate the session tree to a checkpoint's user message. */
|
|
317
|
+
async function rewindConversation(
|
|
318
|
+
ctx: ExtensionCommandContext,
|
|
319
|
+
entryId: string,
|
|
320
|
+
summarize: boolean,
|
|
321
|
+
): Promise<string> {
|
|
322
|
+
try {
|
|
323
|
+
const result = await ctx.navigateTree(entryId, { summarize });
|
|
324
|
+
if (result.cancelled) return "cancelled";
|
|
325
|
+
return "ok";
|
|
326
|
+
} catch (error) {
|
|
327
|
+
ctx.ui.notify(`Conversation rewind failed: ${errorMessage(error)}`, "error");
|
|
328
|
+
return "conversation rewind failed";
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Restore the files captured right before the previous rewind. */
|
|
333
|
+
async function undoLastRewind(
|
|
334
|
+
ctx: ExtensionCommandContext,
|
|
335
|
+
store: RewindStore,
|
|
336
|
+
safety: Checkpoint,
|
|
337
|
+
): Promise<string> {
|
|
338
|
+
const confirmed = await ctx.ui.confirm(
|
|
339
|
+
"Undo last rewind",
|
|
340
|
+
"Put the files back the way they were before the last rewind?",
|
|
341
|
+
);
|
|
342
|
+
if (!confirmed) return "cancelled";
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
// The safety checkpoint carries its own pre-images, so the worktree reset
|
|
346
|
+
// and the outside-project files it is meant to restore both come from it.
|
|
347
|
+
const result = await restoreCheckpoint(store, safety);
|
|
348
|
+
await deleteCheckpoint(store, safety);
|
|
349
|
+
ctx.ui.notify(
|
|
350
|
+
`Undid the last rewind${result.removed.length > 0 ? ` (removed ${result.removed.length} file(s))` : ""}`,
|
|
351
|
+
"info",
|
|
352
|
+
);
|
|
353
|
+
return "undid last rewind";
|
|
354
|
+
} catch (error) {
|
|
355
|
+
ctx.ui.notify(`Undo failed: ${errorMessage(error)}`, "error");
|
|
356
|
+
return "undo failed";
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function errorMessage(error: unknown): string {
|
|
361
|
+
return error instanceof Error ? error.message : String(error);
|
|
362
|
+
}
|