@gavin-hu/my-pi-agent 0.14.0 → 0.15.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 +55 -33
- package/README.zh-CN.md +44 -27
- package/extensions/bytefly/README.md +193 -140
- package/extensions/bytefly/block/component.ts +7 -15
- package/extensions/bytefly/block/register.ts +6 -1
- package/extensions/bytefly/doctor/register.ts +24 -5
- package/extensions/bytefly/doctor/tools.ts +4 -0
- package/extensions/bytefly/footer/collect/snapshot.ts +6 -1
- package/extensions/bytefly/footer/config.ts +10 -6
- package/extensions/bytefly/footer/price/price.ts +5 -3
- package/extensions/bytefly/footer/price/rates.ts +63 -26
- package/extensions/bytefly/footer/render/status.ts +3 -3
- package/extensions/bytefly/header/component.ts +3 -2
- package/extensions/bytefly/index.ts +3 -3
- package/extensions/bytefly/notice/component.ts +8 -3
- package/extensions/bytefly/notify/config.ts +6 -11
- package/extensions/bytefly/notify/dispatch.ts +13 -5
- package/extensions/bytefly/panel/commands.ts +21 -1
- package/extensions/bytefly/panel/format.ts +5 -2
- package/extensions/bytefly/panel/settings-runtime.ts +26 -6
- package/extensions/bytefly/panel/settings.ts +41 -4
- package/extensions/bytefly/panel/themes.ts +32 -0
- package/extensions/bytefly/panel/tui/editing.ts +77 -0
- package/extensions/bytefly/panel/tui/entries.ts +24 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +13 -8
- package/extensions/bytefly/panel/tui/frame.ts +121 -4
- package/extensions/bytefly/panel/tui/settings-view.ts +54 -15
- package/extensions/bytefly/panel/tui.ts +94 -152
- package/extensions/bytefly/picker/discovery.ts +1 -0
- package/extensions/bytefly/picker/types.ts +2 -0
- package/extensions/bytefly/separator/component.ts +14 -4
- package/extensions/bytefly/update/cache.ts +58 -19
- package/extensions/bytefly/update/check.ts +35 -0
- package/extensions/bytefly/update/format.ts +76 -35
- package/extensions/bytefly/update/register.ts +144 -55
- package/extensions/bytefly/update/state.ts +4 -2
- package/extensions/bytefly/update/suppress.ts +66 -0
- package/extensions/bytefly/update/types.ts +15 -0
- package/extensions/check/README.md +253 -0
- package/extensions/check/commands.ts +128 -0
- package/extensions/check/config.ts +39 -0
- package/extensions/check/detect.ts +269 -0
- package/extensions/check/failures.ts +230 -0
- package/extensions/check/format.ts +135 -0
- package/extensions/check/index.ts +26 -0
- package/extensions/check/render.ts +185 -0
- package/extensions/check/runner.ts +176 -0
- package/extensions/check/runtime.ts +244 -0
- package/extensions/check/source.ts +80 -0
- package/extensions/check/tools.ts +173 -0
- package/extensions/check/tui.ts +195 -0
- package/extensions/check/types.ts +99 -0
- package/extensions/document/README.md +1 -1
- package/extensions/document/extract/pdf.ts +8 -16
- package/extensions/document/formats.ts +2 -2
- package/extensions/file-browse/README.md +66 -32
- package/extensions/file-browse/client.ts +95 -32
- package/extensions/file-browse/config.ts +9 -9
- package/extensions/file-browse/git.ts +37 -47
- package/extensions/file-browse/html.ts +39 -58
- package/extensions/file-browse/markdown.ts +110 -92
- package/extensions/file-browse/raw-html.ts +478 -0
- package/extensions/file-browse/router.ts +32 -70
- package/extensions/file-browse/server.ts +18 -56
- package/extensions/file-browse/urls.ts +80 -0
- package/extensions/goal/README.md +103 -47
- package/extensions/goal/clarify-parse.ts +4 -9
- package/extensions/goal/clarify.ts +27 -4
- package/extensions/goal/commands.ts +24 -11
- package/extensions/goal/format.ts +41 -10
- package/extensions/goal/index.ts +39 -16
- package/extensions/goal/plan-route.ts +51 -0
- package/extensions/goal/schema.ts +6 -23
- package/extensions/goal/state.ts +39 -57
- package/extensions/goal/tools.ts +26 -20
- package/extensions/goal/types.ts +11 -1
- package/extensions/job/README.md +20 -6
- package/extensions/job/format.ts +18 -68
- package/extensions/job/index.ts +26 -41
- package/extensions/job/log-writer.ts +208 -0
- package/extensions/job/logs.ts +3 -2
- package/extensions/job/process.ts +6 -99
- package/extensions/job/runtime.ts +42 -181
- package/extensions/job/schema.ts +1 -4
- package/extensions/job/tools.ts +1 -15
- package/extensions/job/tui.ts +117 -307
- package/extensions/job/ui.ts +7 -33
- package/extensions/memory/README.md +1 -1
- package/extensions/memory/errors.ts +5 -6
- package/extensions/memory/tui.ts +14 -24
- package/extensions/plan/README.md +94 -18
- package/extensions/plan/commands.ts +9 -5
- package/extensions/plan/format.ts +87 -0
- package/extensions/plan/index.ts +31 -15
- package/extensions/plan/plans.ts +2 -3
- package/extensions/plan/policy.ts +0 -6
- package/extensions/plan/runtime.ts +4 -2
- package/extensions/plan/steps.ts +37 -8
- package/extensions/plan/todos.ts +44 -3
- package/extensions/plan/tools/enter.ts +94 -0
- package/extensions/plan/tools/exit.ts +296 -0
- package/extensions/plan/tools/nested.ts +21 -0
- package/extensions/plan/tools/steps.ts +136 -0
- package/extensions/plan/tools/todo-seed.ts +76 -0
- package/extensions/plan/tools/write.ts +127 -0
- package/extensions/plan/tools.ts +18 -703
- package/extensions/plan/web-client.ts +6 -22
- package/extensions/plan/web-page.ts +19 -30
- package/extensions/plan/web-router.ts +34 -71
- package/extensions/plan/web.ts +13 -40
- package/extensions/question/answers.ts +3 -8
- package/extensions/question/tui.ts +6 -6
- package/extensions/rewind/README.md +95 -22
- package/extensions/rewind/checklist.ts +185 -0
- package/extensions/rewind/commands.ts +8 -0
- package/extensions/rewind/config.ts +3 -11
- package/extensions/rewind/format.ts +220 -2
- package/extensions/rewind/git.ts +74 -7
- package/extensions/rewind/hunk-picker.ts +114 -0
- package/extensions/rewind/patch.ts +88 -0
- package/extensions/rewind/path-picker.ts +67 -100
- package/extensions/rewind/paths.ts +93 -19
- package/extensions/rewind/restore.ts +53 -10
- package/extensions/rewind/review-tui.ts +320 -0
- package/extensions/rewind/review.ts +397 -0
- package/extensions/rewind/rewind-tui.ts +4 -3
- package/extensions/rewind/runtime.ts +94 -17
- package/extensions/subagent/README.md +61 -6
- package/extensions/subagent/background-tui.ts +208 -0
- package/extensions/subagent/background.ts +470 -0
- package/extensions/subagent/commands.ts +80 -0
- package/extensions/subagent/config.ts +4 -0
- package/extensions/subagent/format.ts +24 -1
- package/extensions/subagent/index.ts +91 -2
- package/extensions/subagent/render.ts +13 -0
- package/extensions/subagent/run.ts +17 -16
- package/extensions/subagent/schema.ts +12 -0
- package/extensions/subagent/types.ts +11 -0
- package/extensions/subagent/ui.ts +50 -0
- package/extensions/todo/README.md +18 -13
- package/extensions/todo/format.ts +3 -2
- package/extensions/todo/runtime.ts +3 -2
- package/extensions/todo/schema.ts +8 -84
- package/extensions/todo/state.ts +5 -46
- package/extensions/todo/tools.ts +2 -1
- package/extensions/todo/types.ts +6 -15
- package/extensions/watch/README.md +14 -6
- package/extensions/watch/child.ts +150 -0
- package/extensions/watch/format.ts +8 -48
- package/extensions/watch/index.ts +42 -58
- package/extensions/watch/runtime.ts +49 -128
- package/extensions/watch/schema.ts +1 -4
- package/extensions/watch/tools.ts +1 -11
- package/extensions/watch/tui.ts +103 -273
- package/extensions/watch/ui.ts +5 -32
- package/extensions/web-access/README.md +1 -1
- package/extensions/web-access/fetch/page.ts +1 -1
- package/extensions/web-access/fetch/tool.ts +6 -2
- package/extensions/web-access/search/search.ts +2 -5
- package/extensions/wechat/README.md +1 -1
- package/extensions/wechat/client.ts +23 -0
- package/extensions/wechat/commands.ts +57 -42
- package/extensions/wechat/deliver.ts +5 -9
- package/extensions/wechat/errors.ts +3 -6
- package/extensions/wechat/inbound.ts +2 -5
- package/extensions/wechat/index.ts +2 -8
- package/extensions/wechat/outbound.ts +2 -4
- package/extensions/wechat/peer-worker.ts +3 -8
- package/extensions/wechat/poll.ts +10 -9
- package/extensions/wechat/presence.ts +35 -62
- package/extensions/wechat/reply-pipeline.ts +3 -1
- package/extensions/wechat/status-screen.ts +1 -1
- package/extensions/wechat/timer.ts +57 -0
- package/extensions/worktree/config.ts +8 -20
- package/extensions/worktree/enter.ts +7 -12
- package/extensions/worktree/git.ts +38 -69
- package/extensions/worktree/index.ts +5 -4
- package/extensions/worktree/registry.ts +7 -7
- package/extensions/worktree/root-tools.ts +2 -2
- package/extensions/worktree/status.ts +3 -2
- package/lib/README.md +22 -8
- package/lib/config.ts +50 -3
- package/lib/errors.ts +12 -0
- package/lib/format.ts +50 -0
- package/lib/git/read.ts +8 -0
- package/lib/goal-state.ts +23 -8
- package/lib/http/headers.ts +54 -0
- package/lib/http/index.ts +13 -0
- package/lib/http/response.ts +46 -0
- package/lib/http/router.ts +52 -0
- package/lib/http/server.ts +91 -0
- package/lib/lines.ts +77 -0
- package/lib/pdf.ts +82 -0
- package/lib/plan-entry.ts +71 -0
- package/lib/plan-state.ts +34 -0
- package/lib/report.ts +99 -0
- package/lib/requirements.ts +25 -19
- package/lib/spawn.ts +170 -0
- package/lib/todo-state.ts +110 -23
- package/lib/tool-names.ts +7 -0
- package/lib/tool-result.ts +33 -0
- package/lib/tui/README.md +68 -3
- package/lib/tui/chrome.ts +28 -4
- package/lib/tui/columns.ts +6 -1
- package/lib/tui/confirm.ts +65 -0
- package/lib/tui/dock-screen.ts +410 -0
- package/lib/tui/glyphs.ts +2 -1
- package/lib/tui/indent.ts +1 -1
- package/lib/tui/index.ts +2 -0
- package/lib/tui/rows.ts +21 -0
- package/lib/tui/status-controller.ts +35 -1
- package/lib/tui/tool.ts +59 -1
- package/lib/web/components.ts +20 -1
- package/lib/web/markup.ts +32 -0
- package/lib/web/runtime.ts +50 -0
- package/package.json +22 -5
- package/skills/init/SKILL.md +165 -0
- package/skills/init/references/agents-md-quality.md +103 -0
- package/skills/init/references/interview-questions.md +81 -0
- package/skills/refactor/SKILL.md +141 -0
- package/extensions/watch/process.ts +0 -63
- package/extensions/web-access/fetch/pdf.ts +0 -45
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `/review` orchestration.
|
|
3
|
+
*
|
|
4
|
+
* The latest prompt with an `auto` snapshot on the active branch is the review
|
|
5
|
+
* target. `runtime.plan` already knows how to diff that snapshot against the
|
|
6
|
+
* working tree and how to restore a subset, so this module only coordinates:
|
|
7
|
+
* the file list, the per-file patch, the confirmations, the restore, and the
|
|
8
|
+
* refresh afterwards. No state is persisted; every open re-derives the review
|
|
9
|
+
* from git and the session branch.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { messageOf } from "../../lib/errors.ts";
|
|
14
|
+
import { sanitizeLine } from "../../lib/format.ts";
|
|
15
|
+
import { isPlanModeEnabled } from "../../lib/plan-state.ts";
|
|
16
|
+
import { withRailsSuppressed } from "../../lib/rails.ts";
|
|
17
|
+
import {
|
|
18
|
+
boundDiff,
|
|
19
|
+
formatHunkRevertBody,
|
|
20
|
+
formatHunkRevertText,
|
|
21
|
+
formatRevertAllBody,
|
|
22
|
+
formatRevertOneBody,
|
|
23
|
+
formatRevertSelectedBody,
|
|
24
|
+
formatReviewListText,
|
|
25
|
+
formatReviewRevertText,
|
|
26
|
+
} from "./format.ts";
|
|
27
|
+
import { HunkPickerComponent } from "./hunk-picker.ts";
|
|
28
|
+
import { buildHunkPatch, parseFilePatch } from "./patch.ts";
|
|
29
|
+
import { entryPaths, toReviewEntries, type ReviewEntry } from "./paths.ts";
|
|
30
|
+
import {
|
|
31
|
+
ReviewDiffComponent,
|
|
32
|
+
ReviewListComponent,
|
|
33
|
+
type ReviewDiffAction,
|
|
34
|
+
type ReviewListResult,
|
|
35
|
+
} from "./review-tui.ts";
|
|
36
|
+
import type { RestorePlan } from "./restore.ts";
|
|
37
|
+
import type { RewindRuntime } from "./runtime.ts";
|
|
38
|
+
import { buildRewindPoints, type RewindPoint } from "./timeline.ts";
|
|
39
|
+
import type { Snapshot, RestoreSummary } from "./types.ts";
|
|
40
|
+
|
|
41
|
+
/** One reviewable view of the working tree. */
|
|
42
|
+
interface ReviewView {
|
|
43
|
+
plan: RestorePlan;
|
|
44
|
+
entries: ReviewEntry[];
|
|
45
|
+
/** Tree id of HEAD when the view was read, or undefined without a commit. */
|
|
46
|
+
headTree?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Whether the working tree is clean against HEAD, so the diff is all committed. */
|
|
50
|
+
function isCommitted(view: ReviewView): boolean {
|
|
51
|
+
return view.headTree !== undefined && view.plan.currentTree === view.headTree;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** A loaded review: where it is, which prompt it follows, and what changed. */
|
|
55
|
+
interface LoadedReview {
|
|
56
|
+
root: string;
|
|
57
|
+
point: RewindPoint;
|
|
58
|
+
snapshot: Snapshot;
|
|
59
|
+
view: ReviewView;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type LoadResult = { ok: true; review: LoadedReview } | { ok: false; kind: "info" | "warning"; message: string };
|
|
63
|
+
|
|
64
|
+
type RefreshResult = { kind: "ok"; view: ReviewView } | { kind: "empty" } | { kind: "error"; message: string };
|
|
65
|
+
|
|
66
|
+
type RevertResult =
|
|
67
|
+
| { kind: "ok"; summary: RestoreSummary; next: ReviewView }
|
|
68
|
+
| { kind: "empty"; summary: RestoreSummary }
|
|
69
|
+
| { kind: "error"; message: string };
|
|
70
|
+
|
|
71
|
+
/** The latest prompt on the branch with a code snapshot, or a reason there is none. */
|
|
72
|
+
async function loadReview(runtime: RewindRuntime, ctx: ExtensionCommandContext): Promise<LoadResult> {
|
|
73
|
+
if (!ctx.isIdle()) await ctx.waitForIdle();
|
|
74
|
+
if (isPlanModeEnabled(ctx.sessionManager.getBranch())) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
kind: "warning",
|
|
78
|
+
message: "Plan mode is read-only. Approve the plan before reviewing and reverting its changes.",
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const root = await runtime.rootFor(ctx);
|
|
83
|
+
if (!root) return { ok: false, kind: "warning", message: "Review needs a git repository." };
|
|
84
|
+
|
|
85
|
+
const snapshots = await runtime.list(root);
|
|
86
|
+
const point = buildRewindPoints(ctx.sessionManager.getBranch(), snapshots, ctx.sessionManager.getSessionId()).find(
|
|
87
|
+
(candidate) => candidate.snapshot,
|
|
88
|
+
);
|
|
89
|
+
if (!point?.snapshot) {
|
|
90
|
+
return { ok: false, kind: "info", message: "No prompt with a code snapshot to review yet." };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const planned = await runtime.plan(root, point.snapshot);
|
|
94
|
+
if (!planned.ok) return { ok: false, kind: "warning", message: planned.reason };
|
|
95
|
+
if (planned.plan.paths.length === 0) {
|
|
96
|
+
const label = point.summary || "this prompt";
|
|
97
|
+
return { ok: false, kind: "info", message: `No changes since the snapshot for "${label}".` };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const stats = await runtime.numStat(root, point.snapshot, planned.plan.currentTree);
|
|
101
|
+
const headTree = await runtime.headTree(root);
|
|
102
|
+
return {
|
|
103
|
+
ok: true,
|
|
104
|
+
review: {
|
|
105
|
+
root,
|
|
106
|
+
point,
|
|
107
|
+
snapshot: point.snapshot,
|
|
108
|
+
view: { plan: planned.plan, entries: toReviewEntries(planned.plan.paths, stats), headTree },
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Re-plan after a revert, so the list and counts reflect the tree that is left. */
|
|
114
|
+
async function refreshView(runtime: RewindRuntime, root: string, snapshot: Snapshot): Promise<RefreshResult> {
|
|
115
|
+
const planned = await runtime.plan(root, snapshot);
|
|
116
|
+
if (!planned.ok) return { kind: "error", message: planned.reason };
|
|
117
|
+
if (planned.plan.paths.length === 0) return { kind: "empty" };
|
|
118
|
+
const stats = await runtime.numStat(root, snapshot, planned.plan.currentTree);
|
|
119
|
+
const headTree = await runtime.headTree(root);
|
|
120
|
+
return { kind: "ok", view: { plan: planned.plan, entries: toReviewEntries(planned.plan.paths, stats), headTree } };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Repaint the status chip, then re-read the review. A repaint failure is never
|
|
125
|
+
* reported as a restore failure, because the restore has already happened.
|
|
126
|
+
*/
|
|
127
|
+
async function refreshAfterMutation(
|
|
128
|
+
runtime: RewindRuntime,
|
|
129
|
+
ctx: ExtensionCommandContext,
|
|
130
|
+
review: LoadedReview,
|
|
131
|
+
): Promise<RefreshResult> {
|
|
132
|
+
try {
|
|
133
|
+
await runtime.setStatus(ctx);
|
|
134
|
+
} catch {
|
|
135
|
+
// The status chip is best-effort; a failed repaint must not report the
|
|
136
|
+
// restore itself as failed.
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
return await refreshView(runtime, review.root, review.snapshot);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
return { kind: "error", message: `Reverted, but could not refresh the review: ${messageOf(error)}` };
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Restore one or more paths, then re-read the review. */
|
|
146
|
+
async function revertPaths(
|
|
147
|
+
runtime: RewindRuntime,
|
|
148
|
+
ctx: ExtensionCommandContext,
|
|
149
|
+
review: LoadedReview,
|
|
150
|
+
paths: readonly string[],
|
|
151
|
+
): Promise<RevertResult> {
|
|
152
|
+
let summary: RestoreSummary;
|
|
153
|
+
try {
|
|
154
|
+
summary = await runtime.restore(ctx, review.root, review.snapshot, runtime.configFor(review.root), paths);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
return { kind: "error", message: `Revert failed: ${messageOf(error)}` };
|
|
157
|
+
}
|
|
158
|
+
const refreshed = await refreshAfterMutation(runtime, ctx, review);
|
|
159
|
+
if (refreshed.kind === "error") return { kind: "error", message: refreshed.message };
|
|
160
|
+
if (refreshed.kind === "empty") return { kind: "empty", summary };
|
|
161
|
+
return { kind: "ok", summary, next: refreshed.view };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Ask before reverting one file. */
|
|
165
|
+
async function confirmRevertOne(
|
|
166
|
+
ctx: ExtensionCommandContext,
|
|
167
|
+
review: LoadedReview,
|
|
168
|
+
entry: ReviewEntry,
|
|
169
|
+
safety: boolean,
|
|
170
|
+
committed: boolean,
|
|
171
|
+
): Promise<boolean> {
|
|
172
|
+
return ctx.ui.confirm(
|
|
173
|
+
`Revert ${sanitizeLine(entry.path)}?`,
|
|
174
|
+
formatRevertOneBody(entry, review.point.prompt, safety, committed),
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Ask before reverting one or more selected files. */
|
|
179
|
+
async function confirmRevertSelected(
|
|
180
|
+
ctx: ExtensionCommandContext,
|
|
181
|
+
review: LoadedReview,
|
|
182
|
+
entries: readonly ReviewEntry[],
|
|
183
|
+
safety: boolean,
|
|
184
|
+
committed: boolean,
|
|
185
|
+
): Promise<boolean> {
|
|
186
|
+
const files = `${entries.length} selected file${entries.length === 1 ? "" : "s"}`;
|
|
187
|
+
return ctx.ui.confirm(`Revert ${files}?`, formatRevertSelectedBody(entries, review.point.prompt, safety, committed));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Ask before reverting chosen hunks of one file. */
|
|
191
|
+
async function confirmHunks(
|
|
192
|
+
ctx: ExtensionCommandContext,
|
|
193
|
+
review: LoadedReview,
|
|
194
|
+
entry: ReviewEntry,
|
|
195
|
+
selected: number,
|
|
196
|
+
total: number,
|
|
197
|
+
safety: boolean,
|
|
198
|
+
committed: boolean,
|
|
199
|
+
): Promise<boolean> {
|
|
200
|
+
return ctx.ui.confirm(
|
|
201
|
+
`Revert ${selected} hunk${selected === 1 ? "" : "s"} in ${sanitizeLine(entry.path)}?`,
|
|
202
|
+
formatHunkRevertBody(entry, selected, total, review.point.prompt, safety, committed),
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** Ask before reverting every changed path in the review. */
|
|
207
|
+
async function confirmRevertAll(
|
|
208
|
+
ctx: ExtensionCommandContext,
|
|
209
|
+
review: LoadedReview,
|
|
210
|
+
entries: readonly ReviewEntry[],
|
|
211
|
+
safety: boolean,
|
|
212
|
+
committed: boolean,
|
|
213
|
+
): Promise<boolean> {
|
|
214
|
+
const files = `${entries.length} file${entries.length === 1 ? "" : "s"}`;
|
|
215
|
+
return ctx.ui.confirm(`Revert all ${files}?`, formatRevertAllBody(entries, review.point.prompt, safety, committed));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** The interactive loop: list, diff, confirm, restore, refresh. */
|
|
219
|
+
async function reviewLoop(runtime: RewindRuntime, ctx: ExtensionCommandContext, review: LoadedReview): Promise<void> {
|
|
220
|
+
const label = review.point.summary || review.point.prompt || "this prompt";
|
|
221
|
+
const safety = runtime.configFor(review.root).safetySnapshot;
|
|
222
|
+
let view = review.view;
|
|
223
|
+
let index = 0;
|
|
224
|
+
let selected: string[] = [];
|
|
225
|
+
|
|
226
|
+
/** Take a refreshed view: the selection is gone and the focus is re-clamped. */
|
|
227
|
+
const adopt = (next: ReviewView): void => {
|
|
228
|
+
view = next;
|
|
229
|
+
selected = [];
|
|
230
|
+
index = Math.min(index, view.entries.length - 1);
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/** Fold one revert into the loop's state; `"done"` means the loop should leave. */
|
|
234
|
+
const settleRevert = (result: RevertResult): "next" | "done" => {
|
|
235
|
+
if (result.kind === "error") {
|
|
236
|
+
ctx.ui.notify(result.message, "error");
|
|
237
|
+
return "done";
|
|
238
|
+
}
|
|
239
|
+
const remaining = result.kind === "ok" ? result.next.entries.length : 0;
|
|
240
|
+
ctx.ui.notify(formatReviewRevertText(result.summary, remaining), "info");
|
|
241
|
+
if (result.kind === "empty") return "done";
|
|
242
|
+
adopt(result.next);
|
|
243
|
+
return "next";
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
while (true) {
|
|
247
|
+
const committed = isCommitted(view);
|
|
248
|
+
const screen = await ctx.ui.custom<ReviewListResult | undefined>(
|
|
249
|
+
(tui, theme, _keybindings, done) =>
|
|
250
|
+
new ReviewListComponent({
|
|
251
|
+
entries: view.entries,
|
|
252
|
+
prompt: label,
|
|
253
|
+
theme,
|
|
254
|
+
initialIndex: index,
|
|
255
|
+
initialSelected: selected,
|
|
256
|
+
committed,
|
|
257
|
+
onClose: (result) => done(result),
|
|
258
|
+
requestRender: () => tui.requestRender(),
|
|
259
|
+
viewportRows: () => tui.terminal?.rows,
|
|
260
|
+
}),
|
|
261
|
+
);
|
|
262
|
+
if (!screen) return;
|
|
263
|
+
selected = screen.selected;
|
|
264
|
+
const action = screen.action;
|
|
265
|
+
if (!action) return;
|
|
266
|
+
|
|
267
|
+
if (action.kind === "revertAll") {
|
|
268
|
+
if (!(await confirmRevertAll(ctx, review, view.entries, safety, committed))) continue;
|
|
269
|
+
const paths = view.plan.paths.map((entry) => entry.path);
|
|
270
|
+
if (settleRevert(await revertPaths(runtime, ctx, review, paths)) === "done") return;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (action.kind === "revert") {
|
|
275
|
+
const entries = action.paths
|
|
276
|
+
.map((path) => view.entries.find((candidate) => candidate.path === path))
|
|
277
|
+
.filter((entry): entry is ReviewEntry => entry !== undefined);
|
|
278
|
+
if (entries.length === 0) continue;
|
|
279
|
+
const paths = entries.map((entry) => entry.path);
|
|
280
|
+
const approved =
|
|
281
|
+
entries.length === 1
|
|
282
|
+
? await confirmRevertOne(ctx, review, entries[0], safety, committed)
|
|
283
|
+
: await confirmRevertSelected(ctx, review, entries, safety, committed);
|
|
284
|
+
if (!approved) continue;
|
|
285
|
+
if (settleRevert(await revertPaths(runtime, ctx, review, paths)) === "done") return;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const entry = view.entries.find((candidate) => candidate.path === action.path);
|
|
290
|
+
if (!entry) continue;
|
|
291
|
+
let patch: string;
|
|
292
|
+
try {
|
|
293
|
+
patch = await runtime.patch(review.root, review.snapshot, view.plan.currentTree, entryPaths(entry));
|
|
294
|
+
} catch (error) {
|
|
295
|
+
ctx.ui.notify(`Could not read the diff: ${messageOf(error)}`, "error");
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
const filePatch = parseFilePatch(patch);
|
|
299
|
+
const canRevertHunks = filePatch !== undefined && filePatch.hunks.length > 0;
|
|
300
|
+
const decision = await ctx.ui.custom<ReviewDiffAction | undefined>(
|
|
301
|
+
(tui, theme, _keybindings, done) =>
|
|
302
|
+
new ReviewDiffComponent({
|
|
303
|
+
entry,
|
|
304
|
+
prompt: label,
|
|
305
|
+
lines: boundDiff(patch),
|
|
306
|
+
canRevertHunks,
|
|
307
|
+
committed,
|
|
308
|
+
theme,
|
|
309
|
+
onClose: (chosen) => done(chosen),
|
|
310
|
+
requestRender: () => tui.requestRender(),
|
|
311
|
+
viewportRows: () => tui.terminal?.rows,
|
|
312
|
+
}),
|
|
313
|
+
);
|
|
314
|
+
if (decision === "hunks") {
|
|
315
|
+
if (!filePatch) continue;
|
|
316
|
+
const chosen = await ctx.ui.custom<number[] | undefined>(
|
|
317
|
+
(tui, theme, _keybindings, done) =>
|
|
318
|
+
new HunkPickerComponent({
|
|
319
|
+
path: entry.path,
|
|
320
|
+
hunks: filePatch.hunks,
|
|
321
|
+
theme,
|
|
322
|
+
onClose: (selection) => done(selection),
|
|
323
|
+
requestRender: () => tui.requestRender(),
|
|
324
|
+
viewportRows: () => tui.terminal?.rows,
|
|
325
|
+
}),
|
|
326
|
+
);
|
|
327
|
+
if (chosen === undefined) continue;
|
|
328
|
+
if (chosen.length === 0) {
|
|
329
|
+
ctx.ui.notify("No hunks selected.", "info");
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
if (!(await confirmHunks(ctx, review, entry, chosen.length, filePatch.hunks.length, safety, committed))) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
let safetyId: string | undefined;
|
|
336
|
+
try {
|
|
337
|
+
const reverted = await runtime.revertHunks(
|
|
338
|
+
ctx,
|
|
339
|
+
review.root,
|
|
340
|
+
review.snapshot,
|
|
341
|
+
runtime.configFor(review.root),
|
|
342
|
+
buildHunkPatch(filePatch, chosen),
|
|
343
|
+
);
|
|
344
|
+
safetyId = reverted.safety;
|
|
345
|
+
} catch (error) {
|
|
346
|
+
ctx.ui.notify(`Revert failed: ${messageOf(error)}`, "error");
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const refreshed = await refreshAfterMutation(runtime, ctx, review);
|
|
350
|
+
if (refreshed.kind === "error") {
|
|
351
|
+
ctx.ui.notify(refreshed.message, "error");
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const remaining = refreshed.kind === "ok" ? refreshed.view.entries.length : 0;
|
|
355
|
+
ctx.ui.notify(formatHunkRevertText(entry.path, chosen.length, safetyId, remaining), "info");
|
|
356
|
+
if (refreshed.kind === "empty") return;
|
|
357
|
+
adopt(refreshed.view);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (decision !== "revert") continue;
|
|
361
|
+
if (!(await confirmRevertOne(ctx, review, entry, safety, committed))) continue;
|
|
362
|
+
if (settleRevert(await revertPaths(runtime, ctx, review, [entry.path])) === "done") return;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Open `/review`: the latest turn's changes, with per-file diffs and reverts.
|
|
368
|
+
*
|
|
369
|
+
* The TUI gets the two screens. RPC, print, and JSON print the changed-file
|
|
370
|
+
* list, because custom components cannot be forwarded; that keeps the command
|
|
371
|
+
* useful without pretending the review actions exist there.
|
|
372
|
+
*/
|
|
373
|
+
export async function openReview(
|
|
374
|
+
pi: ExtensionAPI,
|
|
375
|
+
runtime: RewindRuntime,
|
|
376
|
+
ctx: ExtensionCommandContext,
|
|
377
|
+
): Promise<void> {
|
|
378
|
+
let loaded: LoadResult;
|
|
379
|
+
try {
|
|
380
|
+
loaded = await loadReview(runtime, ctx);
|
|
381
|
+
} catch (error) {
|
|
382
|
+
ctx.ui.notify(`Review failed: ${messageOf(error)}`, "error");
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (!loaded.ok) {
|
|
386
|
+
ctx.ui.notify(loaded.message, loaded.kind);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
if (ctx.mode !== "tui") {
|
|
390
|
+
ctx.ui.notify(
|
|
391
|
+
formatReviewListText(loaded.review.view.entries, loaded.review.point.prompt, isCommitted(loaded.review.view)),
|
|
392
|
+
"info",
|
|
393
|
+
);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
await withRailsSuppressed(pi, () => reviewLoop(runtime, ctx, loaded.review));
|
|
397
|
+
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { Key, matchesKey, type Component, type TuiMouseEvent, type TuiMouseEventResult } from "@earendil-works/pi-tui";
|
|
13
13
|
import { fitRows, formatRange, isCloseKey, ListCursor, navigateList, scrollList } from "../../lib/tui/cursor.ts";
|
|
14
|
-
import { dockListFrame, screenHint, type ViewportRowsSource } from "../../lib/tui/chrome.ts";
|
|
14
|
+
import { detailBlockRows, dockListFrame, screenHint, type ViewportRowsSource } from "../../lib/tui/chrome.ts";
|
|
15
15
|
import { clip } from "../../lib/tui/columns.ts";
|
|
16
16
|
import { SEPARATORS } from "../../lib/tui/glyphs.ts";
|
|
17
17
|
import { emptyState, listRow } from "../../lib/tui/rows.ts";
|
|
@@ -89,10 +89,11 @@ export class RewindListComponent implements Component {
|
|
|
89
89
|
const cursor = this.cursor;
|
|
90
90
|
const focused = this.points[cursor.selected];
|
|
91
91
|
const detail = focused ? formatRewindDetail(Boolean(focused.snapshot), focused.snapshot?.id) : "";
|
|
92
|
+
const detailLines = detail ? [clip(` ${paint(this.theme, "meta", detail)}`, w)] : undefined;
|
|
92
93
|
cursor.visible = fitRows(
|
|
93
94
|
this.options.viewportRows,
|
|
94
95
|
this.points.length,
|
|
95
|
-
SCREEN_CHROME_ROWS + (
|
|
96
|
+
SCREEN_CHROME_ROWS + detailBlockRows(detailLines),
|
|
96
97
|
SCREEN_DEFAULT_ITEMS,
|
|
97
98
|
);
|
|
98
99
|
|
|
@@ -127,7 +128,7 @@ export class RewindListComponent implements Component {
|
|
|
127
128
|
range: cursor.clipped(this.points.length)
|
|
128
129
|
? formatRange(this.theme, w, { start, end, total: this.points.length })
|
|
129
130
|
: undefined,
|
|
130
|
-
detail:
|
|
131
|
+
detail: detailLines,
|
|
131
132
|
hint: this.hint(w),
|
|
132
133
|
});
|
|
133
134
|
}
|
|
@@ -11,9 +11,18 @@
|
|
|
11
11
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { resolveEffectiveCwd } from "../../lib/env.ts";
|
|
13
13
|
import { loadConfig, type RewindConfig } from "./config.ts";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
applyPatchReverse,
|
|
16
|
+
createRewindRunner,
|
|
17
|
+
diffNumStat,
|
|
18
|
+
diffPatch,
|
|
19
|
+
repoRoot,
|
|
20
|
+
revParse,
|
|
21
|
+
type RunGit,
|
|
22
|
+
} from "./git.ts";
|
|
15
23
|
import { createOperationQueue } from "./queue.ts";
|
|
16
24
|
import { applyRestore, planRestore, type PlanResult, type RestoreInput } from "./restore.ts";
|
|
25
|
+
import type { PathStat } from "./paths.ts";
|
|
17
26
|
import { createSnapshot } from "./snapshot.ts";
|
|
18
27
|
import { createStatusChip } from "./status.ts";
|
|
19
28
|
import { belongsToRoot, listSnapshots, pruneSnapshots } from "./store.ts";
|
|
@@ -26,6 +35,12 @@ export interface SnapshotOptions {
|
|
|
26
35
|
prompt?: string;
|
|
27
36
|
}
|
|
28
37
|
|
|
38
|
+
/** What a hunk revert did beyond the file it touched. */
|
|
39
|
+
export interface HunkRevertResult {
|
|
40
|
+
/** Id of the pre-restore safety snapshot, when one was taken. */
|
|
41
|
+
safety?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
29
44
|
export interface RewindRuntime {
|
|
30
45
|
/** Repository (or worktree) root, or undefined outside a repository. */
|
|
31
46
|
rootFor(ctx: ExtensionContext): Promise<string | undefined>;
|
|
@@ -35,6 +50,12 @@ export interface RewindRuntime {
|
|
|
35
50
|
list(root: string): Promise<Snapshot[]>;
|
|
36
51
|
/** Preview a restore; `paths` narrows it to a subset of the changed paths. */
|
|
37
52
|
plan(root: string, target: Snapshot, paths?: readonly string[]): Promise<PlanResult>;
|
|
53
|
+
/** Unified patch between a snapshot and the working-tree tree `tree`. Read-only. */
|
|
54
|
+
patch(root: string, target: Snapshot, tree: string, paths?: readonly string[]): Promise<string>;
|
|
55
|
+
/** Per-path line counts for the same comparison. Read-only. */
|
|
56
|
+
numStat(root: string, target: Snapshot, tree: string, paths?: readonly string[]): Promise<Map<string, PathStat>>;
|
|
57
|
+
/** Tree id of HEAD, or undefined when the repository has no commit. Read-only. */
|
|
58
|
+
headTree(root: string): Promise<string | undefined>;
|
|
38
59
|
restore(
|
|
39
60
|
ctx: ExtensionContext,
|
|
40
61
|
root: string,
|
|
@@ -42,6 +63,14 @@ export interface RewindRuntime {
|
|
|
42
63
|
config: RewindConfig,
|
|
43
64
|
paths?: readonly string[],
|
|
44
65
|
): Promise<RestoreSummary>;
|
|
66
|
+
/** Reverse-apply a prepared hunk patch, after the same safety snapshot as a restore. */
|
|
67
|
+
revertHunks(
|
|
68
|
+
ctx: ExtensionContext,
|
|
69
|
+
root: string,
|
|
70
|
+
target: Snapshot,
|
|
71
|
+
config: RewindConfig,
|
|
72
|
+
patch: string,
|
|
73
|
+
): Promise<HunkRevertResult>;
|
|
45
74
|
/** Repaint the `↺ N` chip from the prompt count on the active branch (`/rewind` list size). */
|
|
46
75
|
setStatus(ctx: ExtensionContext): Promise<void>;
|
|
47
76
|
clearStatus(ctx: ExtensionContext): void;
|
|
@@ -123,6 +152,50 @@ export function createRuntime(pi: ExtensionAPI): RewindRuntime {
|
|
|
123
152
|
return planRestore({ runGit }, { root, indexFile: await indexStore.pathFor(root), target, paths });
|
|
124
153
|
});
|
|
125
154
|
|
|
155
|
+
const patch = (root: string, target: Snapshot, tree: string, paths?: readonly string[]): Promise<string> =>
|
|
156
|
+
diffPatch(runGit, root, target.commit, tree, paths);
|
|
157
|
+
|
|
158
|
+
const numStat = (
|
|
159
|
+
root: string,
|
|
160
|
+
target: Snapshot,
|
|
161
|
+
tree: string,
|
|
162
|
+
paths?: readonly string[],
|
|
163
|
+
): Promise<Map<string, PathStat>> => diffNumStat(runGit, root, target.commit, tree, paths);
|
|
164
|
+
|
|
165
|
+
const headTree = async (root: string): Promise<string | undefined> => {
|
|
166
|
+
try {
|
|
167
|
+
return await revParse(runGit, root, "HEAD^{tree}");
|
|
168
|
+
} catch {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Snapshot the current state before a restore, returning its id. The ref is
|
|
175
|
+
* not pruned here: the next automatic snapshot applies `max`, so the snapshot
|
|
176
|
+
* being restored from stays available for a follow-up restore.
|
|
177
|
+
*/
|
|
178
|
+
const safetySnapshot = async (
|
|
179
|
+
ctx: ExtensionContext,
|
|
180
|
+
root: string,
|
|
181
|
+
config: RewindConfig,
|
|
182
|
+
indexFile: string,
|
|
183
|
+
): Promise<string> => {
|
|
184
|
+
const before = await createSnapshot(
|
|
185
|
+
{ runGit },
|
|
186
|
+
{
|
|
187
|
+
root,
|
|
188
|
+
indexFile,
|
|
189
|
+
namespace: config.refNamespace,
|
|
190
|
+
reason: "pre-restore",
|
|
191
|
+
includeUntracked: config.includeUntracked,
|
|
192
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
193
|
+
entryId: lastUserEntryId(ctx.sessionManager.getBranch()),
|
|
194
|
+
},
|
|
195
|
+
);
|
|
196
|
+
return before.id;
|
|
197
|
+
};
|
|
198
|
+
|
|
126
199
|
const restore = (
|
|
127
200
|
ctx: ExtensionContext,
|
|
128
201
|
root: string,
|
|
@@ -134,26 +207,26 @@ export function createRuntime(pi: ExtensionAPI): RewindRuntime {
|
|
|
134
207
|
if (!belongsToRoot(target, root)) throw new Error(foreignRootReason(target, root));
|
|
135
208
|
const indexFile = await indexStore.pathFor(root);
|
|
136
209
|
const input: RestoreInput = { root, indexFile, target, paths };
|
|
137
|
-
|
|
138
|
-
if (config.safetySnapshot) {
|
|
139
|
-
const before = await createSnapshot(
|
|
140
|
-
{ runGit },
|
|
141
|
-
{
|
|
142
|
-
root,
|
|
143
|
-
indexFile,
|
|
144
|
-
namespace: config.refNamespace,
|
|
145
|
-
reason: "pre-restore",
|
|
146
|
-
includeUntracked: config.includeUntracked,
|
|
147
|
-
sessionId: ctx.sessionManager.getSessionId(),
|
|
148
|
-
entryId: lastUserEntryId(ctx.sessionManager.getBranch()),
|
|
149
|
-
},
|
|
150
|
-
);
|
|
151
|
-
safety = before.id;
|
|
152
|
-
}
|
|
210
|
+
const safety = config.safetySnapshot ? await safetySnapshot(ctx, root, config, indexFile) : undefined;
|
|
153
211
|
const summary = await applyRestore({ runGit }, input);
|
|
154
212
|
return { ...summary, safety };
|
|
155
213
|
});
|
|
156
214
|
|
|
215
|
+
const revertHunks = (
|
|
216
|
+
ctx: ExtensionContext,
|
|
217
|
+
root: string,
|
|
218
|
+
target: Snapshot,
|
|
219
|
+
config: RewindConfig,
|
|
220
|
+
patch: string,
|
|
221
|
+
): Promise<HunkRevertResult> =>
|
|
222
|
+
queue.enqueue(async () => {
|
|
223
|
+
if (!belongsToRoot(target, root)) throw new Error(foreignRootReason(target, root));
|
|
224
|
+
const indexFile = await indexStore.pathFor(root);
|
|
225
|
+
const safety = config.safetySnapshot ? await safetySnapshot(ctx, root, config, indexFile) : undefined;
|
|
226
|
+
await applyPatchReverse(runGit, root, patch);
|
|
227
|
+
return { safety };
|
|
228
|
+
});
|
|
229
|
+
|
|
157
230
|
const sweepStaleIndexes = async (ctx: ExtensionContext): Promise<void> => {
|
|
158
231
|
try {
|
|
159
232
|
const root = await rootFor(ctx);
|
|
@@ -169,7 +242,11 @@ export function createRuntime(pi: ExtensionAPI): RewindRuntime {
|
|
|
169
242
|
snapshot,
|
|
170
243
|
list,
|
|
171
244
|
plan,
|
|
245
|
+
patch,
|
|
246
|
+
numStat,
|
|
247
|
+
headTree,
|
|
172
248
|
restore,
|
|
249
|
+
revertHunks,
|
|
173
250
|
setStatus: status.set,
|
|
174
251
|
clearStatus: status.clear,
|
|
175
252
|
releaseIndexes: indexStore.release,
|