@gavin-hu/my-pi-agent 0.11.4 → 0.13.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.md +38 -26
- package/README.zh-CN.md +33 -23
- package/extensions/bytefly/README.md +381 -225
- package/extensions/bytefly/chrome.ts +7 -2
- package/extensions/bytefly/doctor/register.ts +6 -0
- package/extensions/bytefly/doctor/runtime.ts +52 -6
- package/extensions/bytefly/doctor/tools.ts +1 -3
- package/extensions/bytefly/footer/collect/snapshot.ts +12 -2
- package/extensions/bytefly/footer/config.ts +7 -1
- package/extensions/bytefly/footer/delegation.ts +100 -0
- package/extensions/bytefly/footer/price/config.ts +54 -15
- package/extensions/bytefly/footer/price/price.ts +20 -7
- package/extensions/bytefly/footer/price/types.ts +13 -7
- package/extensions/bytefly/footer/register.ts +13 -2
- package/extensions/bytefly/footer/render/format.ts +3 -2
- package/extensions/bytefly/footer/render/lines.ts +2 -0
- package/extensions/bytefly/footer/render/segments.ts +30 -2
- package/extensions/bytefly/footer/types.ts +12 -0
- package/extensions/bytefly/header/component.ts +21 -6
- package/extensions/bytefly/header/config.ts +3 -0
- package/extensions/bytefly/header/register.ts +34 -3
- package/extensions/bytefly/index.ts +15 -4
- package/extensions/bytefly/notify/config.ts +2 -0
- package/extensions/bytefly/notify/format.ts +19 -1
- package/extensions/bytefly/notify/register.ts +37 -13
- package/extensions/bytefly/notify/types.ts +2 -0
- package/extensions/bytefly/panel/commands.ts +34 -13
- package/extensions/bytefly/panel/settings-runtime.ts +71 -28
- package/extensions/bytefly/panel/settings.ts +293 -48
- package/extensions/bytefly/panel/tui/entries.ts +58 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +86 -0
- package/extensions/bytefly/panel/tui/frame.ts +71 -0
- package/extensions/bytefly/panel/tui/nav.ts +75 -0
- package/extensions/bytefly/panel/tui/settings-view.ts +115 -0
- package/extensions/bytefly/panel/tui.ts +205 -319
- package/extensions/bytefly/picker/runtime.ts +61 -2
- package/extensions/bytefly/picker/toggle.ts +56 -0
- package/extensions/bytefly/update/cache.ts +73 -0
- package/extensions/bytefly/update/check.ts +59 -0
- package/extensions/bytefly/update/format.ts +87 -0
- package/extensions/bytefly/update/register.ts +158 -0
- package/extensions/bytefly/update/state.ts +40 -0
- package/extensions/bytefly/update/version.ts +83 -0
- package/extensions/document/README.md +24 -14
- package/extensions/document/config.ts +4 -0
- package/extensions/document/deadline.ts +61 -0
- package/extensions/document/tool.ts +14 -5
- package/extensions/file-browse/README.md +12 -8
- package/extensions/file-browse/client.ts +6 -7
- package/extensions/file-browse/files.ts +0 -4
- package/extensions/file-browse/git.ts +9 -10
- package/extensions/file-browse/html.ts +17 -7
- package/extensions/file-browse/router.ts +24 -15
- package/extensions/file-open/README.md +30 -15
- package/extensions/file-open/index.ts +4 -3
- package/extensions/file-open/open.ts +52 -21
- package/extensions/goal/README.md +45 -13
- package/extensions/goal/clarify-prompt.ts +5 -36
- package/extensions/goal/clarify.ts +55 -86
- package/extensions/goal/commands.ts +41 -35
- package/extensions/goal/format.ts +12 -4
- package/extensions/goal/schema.ts +6 -2
- package/extensions/goal/tools.ts +28 -8
- package/extensions/goal/tui.ts +1 -1
- package/extensions/goal/types.ts +6 -2
- package/extensions/job/README.md +16 -12
- package/extensions/job/runtime.ts +23 -6
- package/extensions/job/status.ts +4 -2
- package/extensions/memory/README.md +56 -14
- package/extensions/memory/commands.ts +20 -2
- package/extensions/memory/doctor.ts +137 -0
- package/extensions/memory/format.ts +24 -7
- package/extensions/memory/runtime.ts +49 -0
- package/extensions/memory/store.ts +31 -0
- package/extensions/memory/tui.ts +136 -12
- package/extensions/plan/README.md +83 -36
- package/extensions/plan/blocks.ts +1 -35
- package/extensions/plan/commands.ts +84 -19
- package/extensions/plan/index.ts +35 -11
- package/extensions/plan/plan-index.ts +234 -0
- package/extensions/plan/plan-text.ts +95 -0
- package/extensions/plan/plans.ts +91 -2
- package/extensions/plan/policy.ts +4 -3
- package/extensions/plan/review.ts +246 -0
- package/extensions/plan/runtime.ts +73 -24
- package/extensions/plan/steps.ts +60 -39
- package/extensions/plan/todos.ts +29 -60
- package/extensions/plan/tools.ts +188 -244
- package/extensions/plan/tui.ts +18 -31
- package/extensions/plan/types.ts +12 -0
- package/extensions/plan/web-client.ts +51 -18
- package/extensions/plan/web-page.ts +7 -2
- package/extensions/plan/web-router.ts +11 -3
- package/extensions/plan/web.ts +106 -41
- package/extensions/question/README.md +1 -1
- package/extensions/question/tools.ts +1 -1
- package/extensions/rewind/README.md +28 -8
- package/extensions/rewind/commands.ts +1 -1
- package/extensions/rewind/format.ts +4 -4
- package/extensions/rewind/git.ts +56 -18
- package/extensions/rewind/index.ts +3 -5
- package/extensions/rewind/path-picker.ts +198 -0
- package/extensions/rewind/paths.ts +128 -0
- package/extensions/rewind/restore.ts +69 -28
- package/extensions/rewind/rewind.ts +80 -3
- package/extensions/rewind/runtime.ts +13 -5
- package/extensions/rewind/snapshot-run.ts +5 -2
- package/extensions/rewind/types.ts +2 -0
- package/extensions/subagent/README.md +7 -1
- package/extensions/subagent/accounting.ts +34 -0
- package/extensions/subagent/index.ts +23 -3
- package/extensions/subagent/stream.ts +2 -0
- package/extensions/subagent/types.ts +2 -0
- package/extensions/todo/README.md +19 -11
- package/extensions/todo/index.ts +5 -3
- package/extensions/todo/nudge.ts +58 -9
- package/extensions/todo/runtime.ts +28 -17
- package/extensions/watch/README.md +8 -3
- package/extensions/watch/index.ts +19 -0
- package/extensions/web-access/README.md +46 -16
- package/extensions/web-access/charset.ts +58 -0
- package/extensions/web-access/fetch/find.ts +11 -7
- package/extensions/web-access/fetch/format.ts +64 -18
- package/extensions/web-access/fetch/page.ts +5 -1
- package/extensions/web-access/fetch/tool.ts +20 -21
- package/extensions/web-access/http.ts +49 -8
- package/extensions/web-access/search/facts.ts +38 -0
- package/extensions/web-access/search/tool.ts +14 -3
- package/extensions/web-access/text.ts +50 -11
- package/extensions/wechat/README.md +5 -4
- package/extensions/wechat/tools.ts +0 -2
- package/extensions/worktree/README.md +74 -15
- package/extensions/worktree/config.ts +11 -0
- package/extensions/worktree/exit.ts +35 -3
- package/extensions/worktree/gaps.ts +74 -0
- package/extensions/worktree/guard.ts +50 -4
- package/extensions/worktree/handoff.ts +55 -0
- package/extensions/worktree/index.ts +12 -2
- package/extensions/worktree/runtime.ts +20 -0
- package/extensions/worktree/status.ts +15 -2
- package/extensions/worktree/tools.ts +3 -2
- package/lib/README.md +12 -9
- package/lib/config.ts +43 -1
- package/lib/env.ts +12 -0
- package/lib/events.ts +86 -1
- package/lib/goal-state.ts +95 -0
- package/lib/requirements.ts +340 -46
- package/lib/todo-state.ts +62 -0
- package/lib/tui/README.md +91 -0
- package/lib/tui/glyphs.ts +2 -0
- package/lib/web/layout.ts +12 -2
- package/package.json +5 -3
- package/themes/bytefly-dark.json +1 -1
- package/assets/bytefly.schema.json +0 -785
- package/assets/chrome.png +0 -0
- package/assets/file-browse.png +0 -0
- package/assets/job.png +0 -0
- package/assets/plan-review.png +0 -0
- package/assets/question.png +0 -0
- package/assets/rewind.png +0 -0
- package/assets/todo.png +0 -0
- package/assets/tool-renderings.png +0 -0
- package/assets/tools/ask_user_question-cancelled.png +0 -0
- package/assets/tools/ask_user_question.png +0 -0
- package/assets/tools/bash.png +0 -0
- package/assets/tools/edit.png +0 -0
- package/assets/tools/enter_plan_mode.png +0 -0
- package/assets/tools/enter_worktree.png +0 -0
- package/assets/tools/exit_plan_mode.png +0 -0
- package/assets/tools/exit_worktree.png +0 -0
- package/assets/tools/find.png +0 -0
- package/assets/tools/goal.png +0 -0
- package/assets/tools/grep.png +0 -0
- package/assets/tools/job.png +0 -0
- package/assets/tools/list_worktrees.png +0 -0
- package/assets/tools/ls.png +0 -0
- package/assets/tools/memory-long-subject.png +0 -0
- package/assets/tools/memory.png +0 -0
- package/assets/tools/notice.png +0 -0
- package/assets/tools/open_path.png +0 -0
- package/assets/tools/prune_worktrees.png +0 -0
- package/assets/tools/read.png +0 -0
- package/assets/tools/read_doc-running.png +0 -0
- package/assets/tools/read_doc.png +0 -0
- package/assets/tools/run_doctor.png +0 -0
- package/assets/tools/send_wechat.png +0 -0
- package/assets/tools/set_plan_steps.png +0 -0
- package/assets/tools/subagent.png +0 -0
- package/assets/tools/todo.png +0 -0
- package/assets/tools/turn-rule.png +0 -0
- package/assets/tools/user-message.png +0 -0
- package/assets/tools/watch.png +0 -0
- package/assets/tools/web_fetch-rejected.png +0 -0
- package/assets/tools/web_fetch.png +0 -0
- package/assets/tools/web_search.png +0 -0
- package/assets/tools/write.png +0 -0
- package/assets/tools/write_plan.png +0 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The path checklist a partial rewind opens: every changed path, grouped by
|
|
3
|
+
* directory, each one togglable.
|
|
4
|
+
*
|
|
5
|
+
* The screen is the timeline's sibling — same dock frame, same cursor, same
|
|
6
|
+
* width-safe row grammar — so a partial restore reads as one more step in
|
|
7
|
+
* `/rewind` rather than a second product. Rows are selected by default, which
|
|
8
|
+
* makes `Enter` here reproduce the whole-tree restore exactly.
|
|
9
|
+
*
|
|
10
|
+
* The component owns only the selection: paths in, a chosen subset out through
|
|
11
|
+
* `onClose`, and the restore itself stays behind the confirmation that follows.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { Key, matchesKey, type Component, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
16
|
+
import { fitRows, formatRange, isCloseKey, ListCursor, navigateList, scrollList } from "../../lib/tui/cursor.ts";
|
|
17
|
+
import { dockListFrame, screenHint, type ViewportRowsSource } from "../../lib/tui/chrome.ts";
|
|
18
|
+
import { clip } from "../../lib/tui/columns.ts";
|
|
19
|
+
import { SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
20
|
+
import { checkbox, listRow } from "../../lib/tui/rows.ts";
|
|
21
|
+
import { titleRule } from "../../lib/tui/rules.ts";
|
|
22
|
+
import { paint } from "../../lib/tui/tokens.ts";
|
|
23
|
+
import { baseName, expandGroup, groupPaths, SELECTION_MARKS, type ChangedPath, type PathGroup } from "./paths.ts";
|
|
24
|
+
|
|
25
|
+
export interface PathPickerOptions {
|
|
26
|
+
/** The changed paths the snapshot differs on. */
|
|
27
|
+
paths: readonly ChangedPath[];
|
|
28
|
+
theme: Theme;
|
|
29
|
+
/** Called once when the screen closes: the selected paths, or undefined when cancelled. */
|
|
30
|
+
onClose: (paths?: string[]) => void;
|
|
31
|
+
requestRender: () => void;
|
|
32
|
+
viewportRows?: ViewportRowsSource;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Rows the screen shows when the terminal height is unknown. */
|
|
36
|
+
const SCREEN_DEFAULT_ITEMS = 12;
|
|
37
|
+
/** Header, summary, blanks, and footer rows around the list. */
|
|
38
|
+
const SCREEN_CHROME_ROWS = 6;
|
|
39
|
+
|
|
40
|
+
/** One rendered row: a directory header or a file under one. */
|
|
41
|
+
type Row = { kind: "group"; group: PathGroup } | { kind: "file"; path: ChangedPath; dir: string };
|
|
42
|
+
|
|
43
|
+
/** The checklist of changed paths. */
|
|
44
|
+
export class PathPickerComponent implements Component {
|
|
45
|
+
private readonly cursor: ListCursor;
|
|
46
|
+
private readonly groups: PathGroup[];
|
|
47
|
+
private readonly rows: Row[];
|
|
48
|
+
private readonly chosen = new Set<string>();
|
|
49
|
+
|
|
50
|
+
constructor(private readonly options: PathPickerOptions) {
|
|
51
|
+
this.cursor = new ListCursor(options.requestRender);
|
|
52
|
+
this.groups = groupPaths(options.paths);
|
|
53
|
+
// Every path starts selected, so confirming without touching anything means
|
|
54
|
+
// "restore it all" — the behavior `/rewind` had before the checklist existed.
|
|
55
|
+
for (const group of this.groups) for (const path of expandGroup(group)) this.chosen.add(path);
|
|
56
|
+
this.rows = this.groups.flatMap((group): Row[] => [
|
|
57
|
+
{ kind: "group", group },
|
|
58
|
+
...group.paths.map((path): Row => ({ kind: "file", path, dir: group.dir })),
|
|
59
|
+
]);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Whether every file under `group` is selected. */
|
|
63
|
+
private groupOn(group: PathGroup): boolean {
|
|
64
|
+
return group.paths.every((path) => this.chosen.has(path.path));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Toggle every file under `group` to `on`. */
|
|
68
|
+
private setGroup(group: PathGroup, on: boolean): void {
|
|
69
|
+
for (const path of expandGroup(group)) {
|
|
70
|
+
if (on) this.chosen.add(path);
|
|
71
|
+
else this.chosen.delete(path);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Apply a key press. Exposed for the tests that drive the screen directly. */
|
|
76
|
+
handleInput(data: string): void {
|
|
77
|
+
if (isCloseKey(data)) {
|
|
78
|
+
this.options.onClose(undefined);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (navigateList(data, this.cursor, this.rows.length)) return;
|
|
82
|
+
if (data === " ") {
|
|
83
|
+
const row = this.rows[this.cursor.selected];
|
|
84
|
+
if (row?.kind === "group") this.setGroup(row.group, !this.groupOn(row.group));
|
|
85
|
+
else if (row) {
|
|
86
|
+
if (this.chosen.has(row.path.path)) this.chosen.delete(row.path.path);
|
|
87
|
+
else this.chosen.add(row.path.path);
|
|
88
|
+
}
|
|
89
|
+
this.options.requestRender();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (data === "a") {
|
|
93
|
+
for (const path of this.options.paths) this.chosen.add(path.path);
|
|
94
|
+
this.options.requestRender();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (data === "n") {
|
|
98
|
+
this.chosen.clear();
|
|
99
|
+
this.options.requestRender();
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (matchesKey(data, Key.enter)) this.options.onClose([...this.chosen]);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Wheel scrolling in fullscreen moves the cursor; regular mode leaves the wheel to the terminal. */
|
|
106
|
+
handleMouse(event: TuiMouseEvent): TuiMouseEventResult | undefined {
|
|
107
|
+
return scrollList(event, this.cursor, this.rows.length);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
dispose(): void {}
|
|
111
|
+
|
|
112
|
+
invalidate(): void {}
|
|
113
|
+
|
|
114
|
+
// The shared dock frame, like every other list screen.
|
|
115
|
+
render(width: number): string[] {
|
|
116
|
+
const w = Math.max(1, width);
|
|
117
|
+
const header = titleRule(this.theme, w, "Restore paths");
|
|
118
|
+
const cursor = this.cursor;
|
|
119
|
+
cursor.visible = fitRows(this.options.viewportRows, this.rows.length, SCREEN_CHROME_ROWS, SCREEN_DEFAULT_ITEMS);
|
|
120
|
+
const { start, end } = cursor.window(this.rows.length);
|
|
121
|
+
|
|
122
|
+
const body: string[] = [];
|
|
123
|
+
for (let i = start; i < end; i++) body.push(this.rowLine(w, this.rows[i], i === cursor.selected));
|
|
124
|
+
|
|
125
|
+
const total = this.options.paths.length;
|
|
126
|
+
const selected = this.chosen.size;
|
|
127
|
+
return dockListFrame({
|
|
128
|
+
header,
|
|
129
|
+
summary: clip(
|
|
130
|
+
` ${paint(this.theme, "meta", `${selected} of ${total} selected${SEPARATORS.item}Space toggles a file or a directory`)}`,
|
|
131
|
+
w,
|
|
132
|
+
),
|
|
133
|
+
body,
|
|
134
|
+
range: cursor.clipped(this.rows.length)
|
|
135
|
+
? formatRange(this.theme, w, { start, end, total: this.rows.length })
|
|
136
|
+
: undefined,
|
|
137
|
+
detail: this.detail(w),
|
|
138
|
+
hint: this.hint(w),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private get theme(): Theme {
|
|
143
|
+
return this.options.theme;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** One row: a directory header with its count, or a file with its change mark. */
|
|
147
|
+
private rowLine(width: number, row: Row, selected: boolean): string {
|
|
148
|
+
const tone = selected ? "accent" : row.kind === "group" ? "meta" : "label";
|
|
149
|
+
if (row.kind === "group") {
|
|
150
|
+
const count = row.group.paths.length;
|
|
151
|
+
return listRow(this.theme, width, {
|
|
152
|
+
selected,
|
|
153
|
+
leading: checkbox(this.theme, this.groupOn(row.group)),
|
|
154
|
+
label: `${row.group.dir === "" ? "./" : `${row.group.dir}/`}`,
|
|
155
|
+
labelRole: tone,
|
|
156
|
+
trailing: `${count} file${count === 1 ? "" : "s"}`,
|
|
157
|
+
trailingRole: "meta",
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return listRow(this.theme, width, {
|
|
161
|
+
selected,
|
|
162
|
+
indent: 2,
|
|
163
|
+
// The header already carries the directory's checkbox, so the file rows
|
|
164
|
+
// keep the column with a blank rather than repeating it.
|
|
165
|
+
leading: " ",
|
|
166
|
+
label: baseName(row.path.path),
|
|
167
|
+
labelRole: tone,
|
|
168
|
+
trailing: SELECTION_MARKS[row.path.change],
|
|
169
|
+
trailingRole: row.path.change === "added" ? "warn" : "meta",
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** What the focused row will do, in words. */
|
|
174
|
+
private detail(width: number): string[] | undefined {
|
|
175
|
+
const row = this.rows[this.cursor.selected];
|
|
176
|
+
if (!row) return undefined;
|
|
177
|
+
const text =
|
|
178
|
+
row.kind === "group"
|
|
179
|
+
? `Space selects or clears all ${row.group.paths.length} file${row.group.paths.length === 1 ? "" : "s"} in this directory.`
|
|
180
|
+
: row.path.change === "added"
|
|
181
|
+
? "A — created after the snapshot; restoring removes it."
|
|
182
|
+
: row.path.change === "deleted"
|
|
183
|
+
? "D — deleted since the snapshot; restoring brings it back."
|
|
184
|
+
: "M — modified since the snapshot; restoring rewrites it.";
|
|
185
|
+
return [clip(` ${paint(this.theme, "meta", text)}`, width)];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The key-hint row, with the primary action in the accent role. */
|
|
189
|
+
private hint(width: number): string {
|
|
190
|
+
return screenHint(this.theme, width, [
|
|
191
|
+
"Esc cancel",
|
|
192
|
+
{ text: "Enter restore", color: "accent" },
|
|
193
|
+
"Space toggle",
|
|
194
|
+
"a all",
|
|
195
|
+
"n none",
|
|
196
|
+
]);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The changed paths a rewind can restore, and how the picker groups them.
|
|
3
|
+
*
|
|
4
|
+
* `git diff --name-status <snapshot> <working tree>` is the one source of truth:
|
|
5
|
+
* the preview, the checklist, and the apply step all read this list, so they
|
|
6
|
+
* cannot disagree about what changed. Pure: parsing, grouping, marks, and the
|
|
7
|
+
* counts `RestoreSummary` reports.
|
|
8
|
+
*
|
|
9
|
+
* A rename arrives as `R100\told\tnew`. It is expanded into two entries — the old
|
|
10
|
+
* path as deleted and the new one as added — because that is exactly what the
|
|
11
|
+
* restore does to each: selecting the old path brings it back, selecting the new
|
|
12
|
+
* one removes it. Both rows appear in the checklist rather than one opaque one.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
/** What a restore does to one path. */
|
|
16
|
+
export type PathChange = "modified" | "deleted" | "added";
|
|
17
|
+
|
|
18
|
+
/** One path the snapshot differs on, and how. */
|
|
19
|
+
export interface ChangedPath {
|
|
20
|
+
/** Repository-relative path, as git reports it. */
|
|
21
|
+
path: string;
|
|
22
|
+
change: PathChange;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The trailing mark for each change in the checklist. */
|
|
26
|
+
export const SELECTION_MARKS: Readonly<Record<PathChange, string>> = {
|
|
27
|
+
modified: "M",
|
|
28
|
+
deleted: "D",
|
|
29
|
+
added: "A",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** One directory's files, for the checklist's grouping. */
|
|
33
|
+
export interface PathGroup {
|
|
34
|
+
/** Directory relative to the root; `""` for files at the root. */
|
|
35
|
+
dir: string;
|
|
36
|
+
/** The group's files, sorted by path. */
|
|
37
|
+
paths: ChangedPath[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The status letter → change kind, for the single-path statuses. */
|
|
41
|
+
function changeOf(letter: string): PathChange | undefined {
|
|
42
|
+
switch (letter) {
|
|
43
|
+
case "M":
|
|
44
|
+
case "T": // a type change (file ↔ symlink) rewrites the path
|
|
45
|
+
return "modified";
|
|
46
|
+
case "D":
|
|
47
|
+
return "deleted";
|
|
48
|
+
case "A":
|
|
49
|
+
return "added";
|
|
50
|
+
default:
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Parse `git diff --name-status` output. A rename expands to both of its paths
|
|
57
|
+
* (as described in the module comment) and a copy to its destination only — the
|
|
58
|
+
* source is untouched, so it is not a changed path. An unknown status letter is
|
|
59
|
+
* skipped rather than guessed at.
|
|
60
|
+
*/
|
|
61
|
+
export function parseNameStatus(stdout: string): ChangedPath[] {
|
|
62
|
+
const changed: ChangedPath[] = [];
|
|
63
|
+
for (const line of stdout.split("\n")) {
|
|
64
|
+
if (line.trim() === "") continue;
|
|
65
|
+
const [status, first, second] = line.split("\t");
|
|
66
|
+
if (!status || !first) continue;
|
|
67
|
+
const letter = status[0] ?? "";
|
|
68
|
+
if (letter === "R" || letter === "C") {
|
|
69
|
+
if (!second) continue;
|
|
70
|
+
if (letter === "R") changed.push({ path: first, change: "deleted" });
|
|
71
|
+
changed.push({ path: second, change: "added" });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const kind = changeOf(letter);
|
|
75
|
+
if (kind) changed.push({ path: first, change: kind });
|
|
76
|
+
}
|
|
77
|
+
return changed;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The directory part of a path, or `""` for the root. */
|
|
81
|
+
function dirOf(path: string): string {
|
|
82
|
+
const slash = path.lastIndexOf("/");
|
|
83
|
+
return slash === -1 ? "" : path.slice(0, slash);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The file name within its directory. */
|
|
87
|
+
export function baseName(path: string): string {
|
|
88
|
+
const slash = path.lastIndexOf("/");
|
|
89
|
+
return slash === -1 ? path : path.slice(slash + 1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Group changed paths by directory: the root first, then directories in
|
|
94
|
+
* lexicographic order, files sorted inside each group. A stable order is what
|
|
95
|
+
* makes the checklist's cursor predictable and the golden renders testable.
|
|
96
|
+
*/
|
|
97
|
+
export function groupPaths(paths: readonly ChangedPath[]): PathGroup[] {
|
|
98
|
+
const groups = new Map<string, ChangedPath[]>();
|
|
99
|
+
for (const entry of paths) {
|
|
100
|
+
const dir = dirOf(entry.path);
|
|
101
|
+
const group = groups.get(dir);
|
|
102
|
+
if (group) group.push(entry);
|
|
103
|
+
else groups.set(dir, [entry]);
|
|
104
|
+
}
|
|
105
|
+
return [...groups.entries()]
|
|
106
|
+
.map(([dir, entries]) => ({ dir, paths: [...entries].sort((a, b) => a.path.localeCompare(b.path)) }))
|
|
107
|
+
.sort((a, b) => (a.dir === "" ? -1 : b.dir === "" ? 1 : a.dir.localeCompare(b.dir)));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The paths a directory toggle selects. */
|
|
111
|
+
export function expandGroup(group: PathGroup): string[] {
|
|
112
|
+
return group.paths.map((entry) => entry.path);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The counts the plan and the summary report: `changed` is modified plus deleted
|
|
117
|
+
* (files rewritten from the snapshot), and `removed` is the paths created after
|
|
118
|
+
* it, which the restore deletes.
|
|
119
|
+
*/
|
|
120
|
+
export function summarize(paths: readonly ChangedPath[]): { changed: number; removed: number } {
|
|
121
|
+
let changed = 0;
|
|
122
|
+
let removed = 0;
|
|
123
|
+
for (const entry of paths) {
|
|
124
|
+
if (entry.change === "added") removed++;
|
|
125
|
+
else changed++;
|
|
126
|
+
}
|
|
127
|
+
return { changed, removed };
|
|
128
|
+
}
|
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Rewind: restore the working tree and index to a snapshot without moving HEAD.
|
|
3
3
|
*
|
|
4
|
-
* `read-tree --reset -u`
|
|
5
|
-
* modifications and deletions and removing
|
|
6
|
-
* created after the snapshot and never
|
|
7
|
-
* because `read-tree` leaves untracked files
|
|
8
|
-
* are untouched.
|
|
4
|
+
* The whole-tree restore runs `read-tree --reset -u` against the snapshot, which
|
|
5
|
+
* brings tracked files back (restoring modifications and deletions and removing
|
|
6
|
+
* paths the snapshot never had). Files created after the snapshot and never
|
|
7
|
+
* tracked are then deleted explicitly, because `read-tree` leaves untracked files
|
|
8
|
+
* alone. HEAD, branches, and reflogs are untouched.
|
|
9
|
+
*
|
|
10
|
+
* A **partial** restore takes the same shape over a subset: `read-tree` accepts no
|
|
11
|
+
* pathspec, so the selected paths are checked out of the snapshot instead, and
|
|
12
|
+
* only the selected additions are deleted. Everything else is left exactly as it
|
|
13
|
+
* is, and the same temporary index is used, so the real index stays untouched
|
|
14
|
+
* either way.
|
|
9
15
|
*/
|
|
10
16
|
|
|
11
17
|
import { lstatSync, rmSync } from "node:fs";
|
|
12
18
|
import { resolve, sep } from "node:path";
|
|
13
19
|
import {
|
|
14
|
-
addedPaths,
|
|
15
20
|
busyGitState,
|
|
16
|
-
changedCount,
|
|
17
21
|
diffStat,
|
|
22
|
+
nameStatus,
|
|
18
23
|
readTreeReset,
|
|
24
|
+
restorePaths,
|
|
19
25
|
treeFromWorkingTree,
|
|
20
26
|
type RunGit,
|
|
21
27
|
} from "./git.ts";
|
|
28
|
+
import { summarize, type ChangedPath } from "./paths.ts";
|
|
22
29
|
import type { Snapshot, RestoreSummary } from "./types.ts";
|
|
23
30
|
|
|
24
31
|
export interface RestoreDeps {
|
|
@@ -32,6 +39,8 @@ export interface RestoreInput {
|
|
|
32
39
|
indexFile: string;
|
|
33
40
|
/** Snapshot to restore. */
|
|
34
41
|
target: Snapshot;
|
|
42
|
+
/** Restore only these paths; omitted means the whole tree. */
|
|
43
|
+
paths?: readonly string[];
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
export interface RestorePlan {
|
|
@@ -41,6 +50,8 @@ export interface RestorePlan {
|
|
|
41
50
|
changed: number;
|
|
42
51
|
/** Paths created since the snapshot that the restore removes. */
|
|
43
52
|
removed: number;
|
|
53
|
+
/** Every changed path, with how the restore treats it. */
|
|
54
|
+
paths: ChangedPath[];
|
|
44
55
|
}
|
|
45
56
|
|
|
46
57
|
export type PlanResult = { ok: true; plan: RestorePlan } | { ok: false; reason: string };
|
|
@@ -51,26 +62,32 @@ export async function planRestore(deps: RestoreDeps, input: RestoreInput): Promi
|
|
|
51
62
|
if (busy) {
|
|
52
63
|
return { ok: false, reason: `a git ${busy} is in progress; finish or abort it before restoring.` };
|
|
53
64
|
}
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
65
|
+
const cur = await stagedTree(deps, input);
|
|
66
|
+
const paths = await changedPaths(deps, input, cur);
|
|
67
|
+
const diff = await diffStat(deps.runGit, input.root, input.target.commit, cur, selectionOf(paths));
|
|
68
|
+
return { ok: true, plan: { diff, ...summarize(paths), paths } };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The selection narrowed to paths the plan actually found, or undefined for all. */
|
|
72
|
+
function selectionOf(paths: readonly ChangedPath[]): string[] | undefined {
|
|
73
|
+
return paths.length > 0 ? paths.map((entry) => entry.path) : undefined;
|
|
57
74
|
}
|
|
58
75
|
|
|
59
76
|
/**
|
|
60
|
-
* Stage the working tree into the temporary index
|
|
61
|
-
*
|
|
62
|
-
*
|
|
77
|
+
* Stage the working tree into the temporary index, so previews compare
|
|
78
|
+
* tree-to-tree: untracked files captured in the snapshot are not mistaken for
|
|
79
|
+
* deletions against the real index.
|
|
63
80
|
*/
|
|
64
|
-
async function
|
|
65
|
-
deps
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
return
|
|
81
|
+
async function stagedTree(deps: RestoreDeps, input: RestoreInput): Promise<string> {
|
|
82
|
+
return treeFromWorkingTree(deps.runGit, input.root, input.indexFile, input.target.includeUntracked);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Every changed path, narrowed to the caller's selection when one is given. */
|
|
86
|
+
async function changedPaths(deps: RestoreDeps, input: RestoreInput, cur: string): Promise<ChangedPath[]> {
|
|
87
|
+
const all = await nameStatus(deps.runGit, input.root, input.target.commit, cur);
|
|
88
|
+
if (!input.paths) return all;
|
|
89
|
+
const selected = new Set(input.paths);
|
|
90
|
+
return all.filter((entry) => selected.has(entry.path));
|
|
74
91
|
}
|
|
75
92
|
|
|
76
93
|
/** Delete a path under `root`, refusing to escape it or remove a directory. */
|
|
@@ -85,12 +102,36 @@ function deleteFile(root: string, relative: string): void {
|
|
|
85
102
|
}
|
|
86
103
|
}
|
|
87
104
|
|
|
88
|
-
/**
|
|
105
|
+
/**
|
|
106
|
+
* Apply the restore. Recomputes the changed set immediately before mutating, so a
|
|
107
|
+
* file edited while the confirmation was open is still handled correctly.
|
|
108
|
+
*/
|
|
89
109
|
export async function applyRestore(deps: RestoreDeps, input: RestoreInput): Promise<RestoreSummary> {
|
|
90
|
-
const
|
|
110
|
+
const cur = await stagedTree(deps, input);
|
|
111
|
+
const paths = await changedPaths(deps, input, cur);
|
|
112
|
+
const { changed, removed } = summarize(paths);
|
|
113
|
+
const rewritten = paths.filter((entry) => entry.change !== "added").map((entry) => entry.path);
|
|
114
|
+
const created = paths.filter((entry) => entry.change === "added").map((entry) => entry.path);
|
|
115
|
+
|
|
116
|
+
if (input.paths) {
|
|
117
|
+
await restorePaths(deps.runGit, input.root, input.target.commit, input.indexFile, rewritten);
|
|
118
|
+
} else {
|
|
119
|
+
// The whole tree in one call: this also removes files the snapshot lacks,
|
|
120
|
+
// which a pathspec checkout of every changed path would not.
|
|
121
|
+
await readTreeReset(deps.runGit, input.root, input.target.commit, input.indexFile);
|
|
122
|
+
}
|
|
123
|
+
for (const path of created) deleteFile(input.root, path);
|
|
91
124
|
|
|
92
|
-
|
|
93
|
-
|
|
125
|
+
// A subset notice says "2 of 5 files", so it needs the whole diff's size; the
|
|
126
|
+
// all-path query is already in hand when nothing was narrowed.
|
|
127
|
+
const of = input.paths ? (await nameStatus(deps.runGit, input.root, input.target.commit, cur)).length : paths.length;
|
|
94
128
|
|
|
95
|
-
return {
|
|
129
|
+
return {
|
|
130
|
+
id: input.target.id,
|
|
131
|
+
commit: input.target.commit,
|
|
132
|
+
root: input.root,
|
|
133
|
+
changed,
|
|
134
|
+
removed,
|
|
135
|
+
...(input.paths ? { of } : {}),
|
|
136
|
+
};
|
|
96
137
|
}
|
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { withRailsSuppressed } from "../../lib/rails.ts";
|
|
13
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
14
|
import { formatChangeSummary, formatRestoreText, previewDiffText } from "./format.ts";
|
|
15
|
+
import { PathPickerComponent } from "./path-picker.ts";
|
|
16
|
+
import type { ChangedPath } from "./paths.ts";
|
|
13
17
|
import type { RewindRuntime } from "./runtime.ts";
|
|
14
18
|
import { rewindScopes, type RewindPoint, type RewindScope } from "./timeline.ts";
|
|
15
19
|
|
|
@@ -47,19 +51,67 @@ async function chooseScope(
|
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
/** Build the combined confirm body, including the diff preview when code moves. */
|
|
50
|
-
function confirmBody(
|
|
54
|
+
function confirmBody(
|
|
55
|
+
scope: RewindScope,
|
|
56
|
+
changeSummary: string | undefined,
|
|
57
|
+
diff: string | undefined,
|
|
58
|
+
subset?: { selected: number; total: number },
|
|
59
|
+
): string {
|
|
51
60
|
const lines = [scopeDescription(scope)];
|
|
61
|
+
if (subset) lines.push(`Restoring ${subset.selected} of ${subset.total} changed files.`);
|
|
52
62
|
if (changeSummary) lines.push(`${changeSummary} since the snapshot. HEAD is untouched.`);
|
|
53
63
|
if (diff) lines.push("", previewDiffText(diff));
|
|
54
64
|
if (scope !== "code") lines.push("", "This prompt returns to the editor so you can edit and resubmit.");
|
|
55
65
|
return lines.join("\n");
|
|
56
66
|
}
|
|
57
67
|
|
|
68
|
+
/** What the path step decided: the whole tree, a chosen subset, or nothing. */
|
|
69
|
+
export type PathChoice = { kind: "all" } | { kind: "some"; paths: string[] } | { kind: "cancel" };
|
|
70
|
+
|
|
71
|
+
/** The two answers the path step offers before the checklist opens. */
|
|
72
|
+
const ALL_LABEL = (count: number): string => `Everything (${count} file${count === 1 ? "" : "s"})`;
|
|
73
|
+
const SOME_LABEL = "Choose paths…";
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Ask which paths to restore. The checklist needs a list only a TUI can mount, so
|
|
77
|
+
* every other mode keeps the whole-tree restore rather than inventing a typed
|
|
78
|
+
* syntax with its own parser and preview.
|
|
79
|
+
*/
|
|
80
|
+
export async function choosePaths(
|
|
81
|
+
pi: ExtensionAPI,
|
|
82
|
+
ctx: ExtensionCommandContext,
|
|
83
|
+
paths: readonly ChangedPath[],
|
|
84
|
+
): Promise<PathChoice> {
|
|
85
|
+
if (ctx.mode !== "tui" || paths.length === 0) return { kind: "all" };
|
|
86
|
+
|
|
87
|
+
const choice = await ctx.ui.select("Restore what?", [ALL_LABEL(paths.length), SOME_LABEL]);
|
|
88
|
+
if (choice === undefined) return { kind: "cancel" };
|
|
89
|
+
if (choice !== SOME_LABEL) return { kind: "all" };
|
|
90
|
+
|
|
91
|
+
const selected = await withRailsSuppressed(pi, () =>
|
|
92
|
+
ctx.ui.custom<string[] | undefined>(
|
|
93
|
+
(tui, theme, _keybindings, done) =>
|
|
94
|
+
new PathPickerComponent({
|
|
95
|
+
paths,
|
|
96
|
+
theme,
|
|
97
|
+
onClose: (subset) => done(subset),
|
|
98
|
+
requestRender: () => tui.requestRender(),
|
|
99
|
+
viewportRows: () => tui.terminal?.rows,
|
|
100
|
+
}),
|
|
101
|
+
),
|
|
102
|
+
);
|
|
103
|
+
if (!selected) return { kind: "cancel" };
|
|
104
|
+
// An empty checklist is a choice, not a cancel: it restores no files and still
|
|
105
|
+
// moves the conversation, which is what the confirm describes.
|
|
106
|
+
return { kind: "some", paths: selected };
|
|
107
|
+
}
|
|
108
|
+
|
|
58
109
|
/**
|
|
59
110
|
* Rewind to `point` with the chosen scope. Interactive only: callers guard on
|
|
60
111
|
* `ctx.hasUI`. Returns true when something was applied.
|
|
61
112
|
*/
|
|
62
113
|
export async function rewindTo(
|
|
114
|
+
pi: ExtensionAPI,
|
|
63
115
|
runtime: RewindRuntime,
|
|
64
116
|
ctx: ExtensionCommandContext,
|
|
65
117
|
root: string | undefined,
|
|
@@ -81,6 +133,9 @@ export async function rewindTo(
|
|
|
81
133
|
}
|
|
82
134
|
|
|
83
135
|
let plan;
|
|
136
|
+
/** The subset to restore, when the path step narrowed the plan. */
|
|
137
|
+
let selection: string[] | undefined;
|
|
138
|
+
let total = 0;
|
|
84
139
|
if (snapshot && root) {
|
|
85
140
|
const result = await runtime.plan(root, snapshot);
|
|
86
141
|
if (!result.ok) {
|
|
@@ -88,12 +143,34 @@ export async function rewindTo(
|
|
|
88
143
|
return false;
|
|
89
144
|
}
|
|
90
145
|
plan = result.plan;
|
|
146
|
+
total = plan.paths.length;
|
|
147
|
+
if (plan.paths.length > 0) {
|
|
148
|
+
const choice = await choosePaths(pi, ctx, plan.paths);
|
|
149
|
+
if (choice.kind === "cancel") {
|
|
150
|
+
ctx.ui.notify("Rewind cancelled.", "info");
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
if (choice.kind === "some") {
|
|
154
|
+
selection = choice.paths;
|
|
155
|
+
const narrowed = await runtime.plan(root, snapshot, selection);
|
|
156
|
+
if (!narrowed.ok) {
|
|
157
|
+
ctx.ui.notify(narrowed.reason, "warning");
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
plan = narrowed.plan;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
91
163
|
}
|
|
92
164
|
|
|
93
165
|
const label = point.summary || point.prompt || "this prompt";
|
|
94
166
|
const approved = await ctx.ui.confirm(
|
|
95
167
|
`Rewind to "${label}"?`,
|
|
96
|
-
confirmBody(
|
|
168
|
+
confirmBody(
|
|
169
|
+
scope,
|
|
170
|
+
plan ? formatChangeSummary(plan.changed, plan.removed) : undefined,
|
|
171
|
+
plan?.diff,
|
|
172
|
+
selection ? { selected: plan?.paths.length ?? 0, total } : undefined,
|
|
173
|
+
),
|
|
97
174
|
);
|
|
98
175
|
if (!approved) {
|
|
99
176
|
ctx.ui.notify("Rewind cancelled.", "info");
|
|
@@ -103,7 +180,7 @@ export async function rewindTo(
|
|
|
103
180
|
const notes: string[] = [];
|
|
104
181
|
if (snapshot && root) {
|
|
105
182
|
try {
|
|
106
|
-
const restored = await runtime.restore(ctx, root, snapshot, runtime.configFor(root));
|
|
183
|
+
const restored = await runtime.restore(ctx, root, snapshot, runtime.configFor(root), selection);
|
|
107
184
|
await runtime.setStatus(ctx);
|
|
108
185
|
notes.push(formatRestoreText(restored));
|
|
109
186
|
} catch (error) {
|
|
@@ -33,8 +33,15 @@ export interface RewindRuntime {
|
|
|
33
33
|
/** Create and store a snapshot of the current working tree. */
|
|
34
34
|
snapshot(ctx: ExtensionContext, options: SnapshotOptions): Promise<Snapshot>;
|
|
35
35
|
list(root: string): Promise<Snapshot[]>;
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
/** Preview a restore; `paths` narrows it to a subset of the changed paths. */
|
|
37
|
+
plan(root: string, target: Snapshot, paths?: readonly string[]): Promise<PlanResult>;
|
|
38
|
+
restore(
|
|
39
|
+
ctx: ExtensionContext,
|
|
40
|
+
root: string,
|
|
41
|
+
target: Snapshot,
|
|
42
|
+
config: RewindConfig,
|
|
43
|
+
paths?: readonly string[],
|
|
44
|
+
): Promise<RestoreSummary>;
|
|
38
45
|
/** Repaint the `↺ N` chip from the prompt count on the active branch (`/rewind` list size). */
|
|
39
46
|
setStatus(ctx: ExtensionContext): Promise<void>;
|
|
40
47
|
clearStatus(ctx: ExtensionContext): void;
|
|
@@ -110,10 +117,10 @@ export function createRuntime(pi: ExtensionAPI): RewindRuntime {
|
|
|
110
117
|
const list = (root: string): Promise<Snapshot[]> =>
|
|
111
118
|
listSnapshots(runGit, root, configFor(root).refNamespace, { root });
|
|
112
119
|
|
|
113
|
-
const plan = (root: string, target: Snapshot): Promise<PlanResult> =>
|
|
120
|
+
const plan = (root: string, target: Snapshot, paths?: readonly string[]): Promise<PlanResult> =>
|
|
114
121
|
queue.enqueue(async () => {
|
|
115
122
|
if (!belongsToRoot(target, root)) return { ok: false, reason: foreignRootReason(target, root) };
|
|
116
|
-
return planRestore({ runGit }, { root, indexFile: await indexStore.pathFor(root), target });
|
|
123
|
+
return planRestore({ runGit }, { root, indexFile: await indexStore.pathFor(root), target, paths });
|
|
117
124
|
});
|
|
118
125
|
|
|
119
126
|
const restore = (
|
|
@@ -121,11 +128,12 @@ export function createRuntime(pi: ExtensionAPI): RewindRuntime {
|
|
|
121
128
|
root: string,
|
|
122
129
|
target: Snapshot,
|
|
123
130
|
config: RewindConfig,
|
|
131
|
+
paths?: readonly string[],
|
|
124
132
|
): Promise<RestoreSummary> =>
|
|
125
133
|
queue.enqueue(async () => {
|
|
126
134
|
if (!belongsToRoot(target, root)) throw new Error(foreignRootReason(target, root));
|
|
127
135
|
const indexFile = await indexStore.pathFor(root);
|
|
128
|
-
const input: RestoreInput = { root, indexFile, target };
|
|
136
|
+
const input: RestoreInput = { root, indexFile, target, paths };
|
|
129
137
|
let safety: string | undefined;
|
|
130
138
|
if (config.safetySnapshot) {
|
|
131
139
|
const before = await createSnapshot(
|
|
@@ -15,8 +15,11 @@ import { createSnapshotPolicy, type SnapshotPolicy } from "./policy.ts";
|
|
|
15
15
|
import type { RewindRuntime } from "./runtime.ts";
|
|
16
16
|
|
|
17
17
|
export interface SnapshotRun {
|
|
18
|
-
/**
|
|
19
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Start a run for `prompt` — `undefined` when a custom message started it: reset
|
|
20
|
+
* run state and resolve the snapshot decision.
|
|
21
|
+
*/
|
|
22
|
+
beginRun(prompt: string | undefined, ctx: ExtensionContext): Promise<void>;
|
|
20
23
|
/** Take the run's single snapshot before the first mutating call, at most once. */
|
|
21
24
|
onToolCall(toolName: string, ctx: ExtensionContext): Promise<void>;
|
|
22
25
|
/** Drop run-scoped state (on session shutdown). */
|
|
@@ -53,6 +53,8 @@ export interface RestoreSummary {
|
|
|
53
53
|
changed: number;
|
|
54
54
|
/** Files deleted because they were created after the snapshot. */
|
|
55
55
|
removed: number;
|
|
56
|
+
/** Paths the whole diff touched, present only when a subset was restored. */
|
|
57
|
+
of?: number;
|
|
56
58
|
/** Id of the pre-restore safety snapshot, when one was taken. */
|
|
57
59
|
safety?: string;
|
|
58
60
|
}
|