@mrclrchtr/supi-review 1.5.0 → 1.7.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/package.json +1 -1
- package/src/review.ts +53 -13
- package/src/target/packet.ts +121 -3
- package/src/tool/brief-runner.ts +1 -1
- package/src/tool/review-runner.ts +20 -3
- package/src/ui/flow.ts +133 -47
- package/src/ui/theme-type.ts +16 -0
package/package.json
CHANGED
package/src/review.ts
CHANGED
|
@@ -260,20 +260,60 @@ function maybeQueueReviewFollowUp(pi: ExtensionAPI, result: ReviewResult): void
|
|
|
260
260
|
function buildReviewFollowUpInstruction(
|
|
261
261
|
result: Extract<ReviewResult, { kind: "success" }>,
|
|
262
262
|
): string {
|
|
263
|
-
const findings = result.output
|
|
264
|
-
|
|
265
|
-
);
|
|
263
|
+
const { findings, overall_correctness } = result.output;
|
|
264
|
+
const criticalCount = findings.filter((f) => f.priority === 3).length;
|
|
265
|
+
const majorCount = findings.filter((f) => f.priority === 2).length;
|
|
266
|
+
|
|
267
|
+
const findingList = findings.map((finding, index) => `- #${index + 1}: ${finding.title}`);
|
|
266
268
|
|
|
267
|
-
|
|
268
|
-
"A code review just completed and the result is available in the preceding `supi-review` message."
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
"If the user chooses Verify findings, verify the findings first and then ask again whether to Fix all or Fix selected.",
|
|
269
|
+
const header =
|
|
270
|
+
"A code review just completed and the result is available in the preceding `supi-review` message.";
|
|
271
|
+
const noFixing = "Do not start fixing code immediately.";
|
|
272
|
+
const useAskUser = "If the `ask_user` tool is available, use it for this decision.";
|
|
273
|
+
|
|
274
|
+
const appendFindings = (lines: string[]): string[] => [
|
|
275
|
+
...lines,
|
|
275
276
|
"",
|
|
276
277
|
"Current findings:",
|
|
277
|
-
...
|
|
278
|
-
]
|
|
278
|
+
...findingList,
|
|
279
|
+
];
|
|
280
|
+
|
|
281
|
+
if (criticalCount > 0) {
|
|
282
|
+
return appendFindings([
|
|
283
|
+
header,
|
|
284
|
+
`⚠️ ${criticalCount} critical finding(s) found. Urge the user to fix before merging.`,
|
|
285
|
+
noFixing,
|
|
286
|
+
useAskUser,
|
|
287
|
+
"Offer these options: Fix all, Fix critical only, Done.",
|
|
288
|
+
]).join("\n");
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const contradictionNote =
|
|
292
|
+
overall_correctness === "patch is correct" && findings.length > 0
|
|
293
|
+
? "The reviewer marked the patch as correct but found issues — verify the verdict before acting."
|
|
294
|
+
: undefined;
|
|
295
|
+
|
|
296
|
+
if (majorCount > 0) {
|
|
297
|
+
const lines: string[] = [header];
|
|
298
|
+
if (contradictionNote) lines.push(contradictionNote);
|
|
299
|
+
lines.push(
|
|
300
|
+
noFixing,
|
|
301
|
+
useAskUser,
|
|
302
|
+
"Offer exactly these options: Done, Fix all, Fix selected, Verify findings.",
|
|
303
|
+
"If the user chooses Fix selected, ask a follow-up question listing the findings by number/title.",
|
|
304
|
+
"If the user chooses Verify findings, verify the findings first and then ask again whether to Fix all or Fix selected.",
|
|
305
|
+
);
|
|
306
|
+
return appendFindings(lines).join("\n");
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Only minor/info findings
|
|
310
|
+
return appendFindings([
|
|
311
|
+
header,
|
|
312
|
+
...(contradictionNote
|
|
313
|
+
? [contradictionNote]
|
|
314
|
+
: ["Only minor/info suggestions — no blocking issues."]),
|
|
315
|
+
noFixing,
|
|
316
|
+
useAskUser,
|
|
317
|
+
"Offer these options: Apply suggestions, Skip.",
|
|
318
|
+
]).join("\n");
|
|
279
319
|
}
|
package/src/target/packet.ts
CHANGED
|
@@ -5,9 +5,11 @@ import type {
|
|
|
5
5
|
SynthesizedReviewBrief,
|
|
6
6
|
} from "../types.ts";
|
|
7
7
|
|
|
8
|
-
interface DiffSection {
|
|
8
|
+
export interface DiffSection {
|
|
9
9
|
file: string;
|
|
10
10
|
text: string;
|
|
11
|
+
additions: number;
|
|
12
|
+
deletions: number;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
/** Build the final prompt packet for the reviewer child session. */
|
|
@@ -49,6 +51,8 @@ export function buildReviewPacket(
|
|
|
49
51
|
...snapshot.changedFiles.map((file) => `- ${file}`),
|
|
50
52
|
];
|
|
51
53
|
|
|
54
|
+
baseParts.push("", buildFileOverviewTable(snapshot.changedFiles, sections));
|
|
55
|
+
|
|
52
56
|
if (preamble.trim()) {
|
|
53
57
|
baseParts.push("", "## Snapshot notes", truncate(preamble.trim(), 1_500));
|
|
54
58
|
}
|
|
@@ -115,7 +119,21 @@ export function getPacketCharBudget(model: ReviewModelSelection): number {
|
|
|
115
119
|
return tokenBudget * 4;
|
|
116
120
|
}
|
|
117
121
|
|
|
118
|
-
function
|
|
122
|
+
function countDiffLines(lines: string[]): { additions: number; deletions: number } {
|
|
123
|
+
let additions = 0;
|
|
124
|
+
let deletions = 0;
|
|
125
|
+
for (const line of lines) {
|
|
126
|
+
// Skip diff header lines (e.g. "--- a/file", "+++ b/file").
|
|
127
|
+
// The trailing space ensures we do not skip content lines whose first
|
|
128
|
+
// characters happen to be "+++" or "---".
|
|
129
|
+
if (line.startsWith("--- ") || line.startsWith("+++ ")) continue;
|
|
130
|
+
if (line.startsWith("+")) additions++;
|
|
131
|
+
else if (line.startsWith("-")) deletions++;
|
|
132
|
+
}
|
|
133
|
+
return { additions, deletions };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function splitDiffSections(text: string): { preamble: string; sections: DiffSection[] } {
|
|
119
137
|
const lines = text.split("\n");
|
|
120
138
|
const preamble: string[] = [];
|
|
121
139
|
const sections: DiffSection[] = [];
|
|
@@ -128,7 +146,13 @@ function splitDiffSections(text: string): { preamble: string; sections: DiffSect
|
|
|
128
146
|
currentFile = undefined;
|
|
129
147
|
return;
|
|
130
148
|
}
|
|
131
|
-
|
|
149
|
+
const stats = countDiffLines(current);
|
|
150
|
+
sections.push({
|
|
151
|
+
file: currentFile,
|
|
152
|
+
text: current.join("\n").trimEnd(),
|
|
153
|
+
additions: stats.additions,
|
|
154
|
+
deletions: stats.deletions,
|
|
155
|
+
});
|
|
132
156
|
current = [];
|
|
133
157
|
currentFile = undefined;
|
|
134
158
|
};
|
|
@@ -206,6 +230,100 @@ function toPathTokens(path: string): string[] {
|
|
|
206
230
|
.filter((part) => part.length >= 3);
|
|
207
231
|
}
|
|
208
232
|
|
|
233
|
+
/** Categorize a file path for skip-list annotation, or undefined if it should be reviewed. */
|
|
234
|
+
export function classifySkipCategory(file: string): string | undefined {
|
|
235
|
+
const lockfiles = new Set([
|
|
236
|
+
"package-lock.json",
|
|
237
|
+
"yarn.lock",
|
|
238
|
+
"pnpm-lock.yaml",
|
|
239
|
+
"Gemfile.lock",
|
|
240
|
+
"Cargo.lock",
|
|
241
|
+
"poetry.lock",
|
|
242
|
+
"composer.lock",
|
|
243
|
+
]);
|
|
244
|
+
|
|
245
|
+
const name = file.split("/").pop() ?? file;
|
|
246
|
+
if (lockfiles.has(name)) return "lockfile";
|
|
247
|
+
if (name.startsWith("CHANGELOG") || name.startsWith("CHANGES") || name === "CHANGE_LOG") {
|
|
248
|
+
return "changelog";
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (file.includes("__snapshots__/") || name.endsWith(".snap")) {
|
|
252
|
+
return "snapshot";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (
|
|
256
|
+
file.includes("dist/") ||
|
|
257
|
+
file.includes("build/") ||
|
|
258
|
+
file.includes(".next/") ||
|
|
259
|
+
file.includes("__generated__/")
|
|
260
|
+
) {
|
|
261
|
+
return "generated";
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (file.includes("vendor/") || file.includes("third_party/")) {
|
|
265
|
+
return "vendored";
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (name.endsWith(".min.js") || name.endsWith(".min.css")) return "generated";
|
|
269
|
+
|
|
270
|
+
return undefined;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function formatOverviewRow(
|
|
274
|
+
file: string,
|
|
275
|
+
statsMap: Map<string, { additions: number; deletions: number }>,
|
|
276
|
+
binaryFiles: Set<string>,
|
|
277
|
+
): string {
|
|
278
|
+
const stats = statsMap.get(file);
|
|
279
|
+
const skipCategory = classifySkipCategory(file);
|
|
280
|
+
const annotations: string[] = [];
|
|
281
|
+
|
|
282
|
+
if (!stats) {
|
|
283
|
+
// File in the changed-files manifest but not in the parsed diff sections.
|
|
284
|
+
// This happens for untracked files in working-tree snapshots, which
|
|
285
|
+
// have no diff section to parse. Do not guess 0/0 or mark as trivial.
|
|
286
|
+
if (skipCategory) annotations.push(`skip — ${skipCategory}`);
|
|
287
|
+
const annotation = annotations.length > 0 ? ` (${annotations.join(", ")})` : "";
|
|
288
|
+
return `| ${file} | ? | ?${annotation} |`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (binaryFiles.has(file)) {
|
|
292
|
+
// Binary diffs have no diffable +/- lines. Show unknown stats.
|
|
293
|
+
if (skipCategory) annotations.push(`skip — ${skipCategory}`);
|
|
294
|
+
annotations.push("binary");
|
|
295
|
+
const annotation = annotations.length > 0 ? ` (${annotations.join(", ")})` : "";
|
|
296
|
+
return `| ${file} | ? | ?${annotation} |`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const total = stats.additions + stats.deletions;
|
|
300
|
+
if (total < 5) annotations.push("trivial");
|
|
301
|
+
if (skipCategory) annotations.push(`skip — ${skipCategory}`);
|
|
302
|
+
const annotation = annotations.length > 0 ? ` (${annotations.join(", ")})` : "";
|
|
303
|
+
return `| ${file} | ${stats.additions} | ${stats.deletions}${annotation} |`;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function buildFileOverviewTable(changedFiles: string[], sections: DiffSection[]): string {
|
|
307
|
+
const statsMap = new Map<string, { additions: number; deletions: number }>();
|
|
308
|
+
const binaryFiles = new Set<string>();
|
|
309
|
+
for (const section of sections) {
|
|
310
|
+
const existing = statsMap.get(section.file);
|
|
311
|
+
statsMap.set(section.file, {
|
|
312
|
+
additions: (existing?.additions ?? 0) + section.additions,
|
|
313
|
+
deletions: (existing?.deletions ?? 0) + section.deletions,
|
|
314
|
+
});
|
|
315
|
+
if (section.text.includes("Binary files ")) {
|
|
316
|
+
binaryFiles.add(section.file);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const header = "| File | +Add | -Del |";
|
|
321
|
+
const separator = "|---|---|---|";
|
|
322
|
+
const rows = changedFiles.map((file) => formatOverviewRow(file, statsMap, binaryFiles));
|
|
323
|
+
|
|
324
|
+
return [`## File overview`, "", header, separator, ...rows].join("\n");
|
|
325
|
+
}
|
|
326
|
+
|
|
209
327
|
function toBullets(items: string[], fallback: string): string[] {
|
|
210
328
|
return items.length > 0 ? items.map((item) => `- ${item}`) : [fallback];
|
|
211
329
|
}
|
package/src/tool/brief-runner.ts
CHANGED
|
@@ -69,7 +69,7 @@ async function createBriefSession(
|
|
|
69
69
|
cwd: invocation.cwd,
|
|
70
70
|
model: invocation.model,
|
|
71
71
|
modelRegistry: invocation.modelRegistry,
|
|
72
|
-
thinkingLevel: clampThinkingLevel(invocation.model, "
|
|
72
|
+
thinkingLevel: clampThinkingLevel(invocation.model, "xhigh"),
|
|
73
73
|
tools: ["submit_review_brief"],
|
|
74
74
|
customTools: [submitBriefTool],
|
|
75
75
|
resourceLoader,
|
|
@@ -65,9 +65,21 @@ export function buildReviewerSystemPrompt(): string {
|
|
|
65
65
|
"- Title: concise and specific.",
|
|
66
66
|
"- Body: explain the issue, why it matters, and a concrete fix direction.",
|
|
67
67
|
"- code_location: 1-based inclusive line range.",
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
"
|
|
68
|
+
"",
|
|
69
|
+
"--- Finding calibration ---",
|
|
70
|
+
"Priority:",
|
|
71
|
+
" 0 (info): style nits, naming suggestions, non-functional improvements without real impact",
|
|
72
|
+
" 1 (minor): unlikely edge cases, minor perf concerns without benchmarks, missing comments",
|
|
73
|
+
" 2 (major): logic errors, incorrect error handling, API contract violations, race conditions",
|
|
74
|
+
" 3 (critical): security vulnerabilities, data loss/corruption, crashes, breaking changes",
|
|
75
|
+
"Confidence:",
|
|
76
|
+
" 0.8-1.0: you verified the issue by reading surrounding code or grepping the codebase",
|
|
77
|
+
" 0.5-0.8: suspected from the diff, plausible but not fully verified",
|
|
78
|
+
" <0.5: do not report — too uncertain; either verify further or drop it",
|
|
79
|
+
"overall_correctness:",
|
|
80
|
+
" 'patch is correct': no issues found, or only info-level suggestions",
|
|
81
|
+
" 'mostly correct': minor issues present, safe to merge with fixes",
|
|
82
|
+
" 'patch is incorrect': major/critical issues that should block merge",
|
|
71
83
|
"",
|
|
72
84
|
"--- Tool strategy ---",
|
|
73
85
|
"- Use read to inspect full files when the inline diff lacks context.",
|
|
@@ -75,6 +87,11 @@ export function buildReviewerSystemPrompt(): string {
|
|
|
75
87
|
"- Use find to locate related files quickly.",
|
|
76
88
|
"- Use ls when you need a quick directory overview.",
|
|
77
89
|
"",
|
|
90
|
+
"--- Skipped files ---",
|
|
91
|
+
"- Skip reviewing: lockfiles, generated/bundled code (dist/, .next/, __generated__/),",
|
|
92
|
+
" vendored dependencies, changelogs, snapshot files, minified bundles, and binary files.",
|
|
93
|
+
"- Focus on application source and test code.",
|
|
94
|
+
"",
|
|
78
95
|
"Do NOT output JSON directly — call submit_review with the structured result.",
|
|
79
96
|
].join("\n");
|
|
80
97
|
}
|
package/src/ui/flow.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { DynamicBorder, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Container, type SelectItem, SelectList, Text } from "@earendil-works/pi-tui";
|
|
2
|
+
import { Container, type SelectItem, SelectList, Spacer, Text } from "@earendil-works/pi-tui";
|
|
3
3
|
import { getLocalBranches, getRecentCommits } from "../git.ts";
|
|
4
4
|
import { getSelectableReviewModels } from "../model.ts";
|
|
5
5
|
import type { ReviewModelSelection, ReviewPlan, ReviewTargetSpec } from "../types.ts";
|
|
6
|
+
import type { ReviewTheme } from "./theme-type.ts";
|
|
6
7
|
|
|
7
8
|
interface SelectFromListOptions<T> {
|
|
8
9
|
items: SelectItem[];
|
|
@@ -132,9 +133,138 @@ export async function collectReviewNote(ctx: ExtensionContext): Promise<string |
|
|
|
132
133
|
return value.trim();
|
|
133
134
|
}
|
|
134
135
|
|
|
135
|
-
/** Show the synthesized brief
|
|
136
|
+
/** Show the synthesized brief, the actual reviewer prompt preview, and ask for approval. */
|
|
136
137
|
export function previewReviewPlan(ctx: ExtensionContext, plan: ReviewPlan): Promise<boolean> {
|
|
137
|
-
return ctx.ui.
|
|
138
|
+
return ctx.ui.custom<boolean>((_tui, theme, _kb, done) => {
|
|
139
|
+
const container = buildReviewPlanContainer(theme, plan);
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
render: (width) => container.render(width),
|
|
143
|
+
invalidate: () => container.invalidate(),
|
|
144
|
+
handleInput: (data) => {
|
|
145
|
+
if (data === "\r" || data === "\n" || data === "y" || data === "Y") {
|
|
146
|
+
done(true);
|
|
147
|
+
} else if (data === "\x1b" || data === "n" || data === "N") {
|
|
148
|
+
done(false);
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Build the review plan preview container with all styled sections. */
|
|
156
|
+
function buildReviewPlanContainer(theme: ReviewTheme, plan: ReviewPlan): Container {
|
|
157
|
+
const { model, snapshot, brief, packet } = plan;
|
|
158
|
+
const container = new Container();
|
|
159
|
+
|
|
160
|
+
const accent = (s: string) => theme.fg("accent", s);
|
|
161
|
+
const dim = (s: string) => theme.fg("dim", s);
|
|
162
|
+
const bold = (s: string) => theme.bold(s);
|
|
163
|
+
|
|
164
|
+
// ── Top border ──
|
|
165
|
+
container.addChild(new DynamicBorder((s: string) => accent(s)));
|
|
166
|
+
container.addChild(new Spacer(1));
|
|
167
|
+
|
|
168
|
+
// ── Title ──
|
|
169
|
+
container.addChild(new Text(accent(bold(" Review Plan")), 1, 0));
|
|
170
|
+
container.addChild(new Spacer(1));
|
|
171
|
+
|
|
172
|
+
// ── Metadata section ──
|
|
173
|
+
const kind = snapshot.target.kind;
|
|
174
|
+
const targetLabel =
|
|
175
|
+
kind === "working-tree"
|
|
176
|
+
? "Working tree"
|
|
177
|
+
: kind === "branch"
|
|
178
|
+
? `${snapshot.target.base} \u2190 current`
|
|
179
|
+
: `commit ${snapshot.target.sha.slice(0, 7)}`;
|
|
180
|
+
|
|
181
|
+
container.addChild(new Text(accent(bold(" \u2500\u2500 Metadata \u2500\u2500")), 1, 0));
|
|
182
|
+
container.addChild(
|
|
183
|
+
new Text(
|
|
184
|
+
[
|
|
185
|
+
` ${dim("Model:")} ${model.canonicalId}`,
|
|
186
|
+
` ${dim("Target:")} ${snapshot.title}`,
|
|
187
|
+
` ${dim("Kind:")} ${targetLabel}`,
|
|
188
|
+
` ${dim("Files:")} ${snapshot.changedFiles.length} changed ${theme.fg("toolDiffAdded", `+${snapshot.stats.additions}`)}/${theme.fg("toolDiffRemoved", `-${snapshot.stats.deletions}`)}`,
|
|
189
|
+
].join("\n"),
|
|
190
|
+
1,
|
|
191
|
+
0,
|
|
192
|
+
),
|
|
193
|
+
);
|
|
194
|
+
container.addChild(new Spacer(1));
|
|
195
|
+
|
|
196
|
+
// ── Brief section ──
|
|
197
|
+
container.addChild(
|
|
198
|
+
new Text(accent(bold(" \u2500\u2500 Session-derived Brief \u2500\u2500")), 1, 0),
|
|
199
|
+
);
|
|
200
|
+
const briefParts = [
|
|
201
|
+
` ${dim("Summary:")} ${brief.summary}`,
|
|
202
|
+
` ${dim("Outcome:")} ${brief.intendedOutcome}`,
|
|
203
|
+
];
|
|
204
|
+
if (brief.constraints.length > 0) {
|
|
205
|
+
briefParts.push(` ${dim("Constraints:")} ${brief.constraints.join("; ")}`);
|
|
206
|
+
}
|
|
207
|
+
if (brief.focusAreas.length > 0) {
|
|
208
|
+
briefParts.push(` ${dim("Focus:")} ${brief.focusAreas.join("; ")}`);
|
|
209
|
+
}
|
|
210
|
+
if (brief.riskyFiles.length > 0) {
|
|
211
|
+
briefParts.push(` ${dim("Risky:")} ${brief.riskyFiles.join(", ")}`);
|
|
212
|
+
}
|
|
213
|
+
if (brief.unresolvedQuestions.length > 0) {
|
|
214
|
+
briefParts.push(` ${dim("Questions:")} ${brief.unresolvedQuestions.join("; ")}`);
|
|
215
|
+
}
|
|
216
|
+
container.addChild(new Text(briefParts.join("\n"), 1, 0));
|
|
217
|
+
container.addChild(new Spacer(1));
|
|
218
|
+
|
|
219
|
+
// ── Reviewer Prompt preview ──
|
|
220
|
+
const totalChars = packet.prompt.length;
|
|
221
|
+
const maxPreview = 2000;
|
|
222
|
+
const previewText =
|
|
223
|
+
totalChars > maxPreview
|
|
224
|
+
? `${packet.prompt.slice(0, maxPreview)}\n\n${theme.fg("warning", `[Preview truncated \u2014 showing ${maxPreview.toLocaleString()} of ${totalChars.toLocaleString()} total chars]`)}`
|
|
225
|
+
: packet.prompt;
|
|
226
|
+
|
|
227
|
+
container.addChild(
|
|
228
|
+
new Text(
|
|
229
|
+
accent(
|
|
230
|
+
bold(` \u2500\u2500 Reviewer Prompt (${totalChars.toLocaleString()} chars) \u2500\u2500`),
|
|
231
|
+
),
|
|
232
|
+
1,
|
|
233
|
+
0,
|
|
234
|
+
),
|
|
235
|
+
);
|
|
236
|
+
container.addChild(new Text(previewText, 1, 0));
|
|
237
|
+
container.addChild(new Spacer(1));
|
|
238
|
+
|
|
239
|
+
// ── File coverage line ──
|
|
240
|
+
container.addChild(
|
|
241
|
+
new Text(
|
|
242
|
+
theme.fg(
|
|
243
|
+
"dim",
|
|
244
|
+
` Included diffs: ${packet.includedFiles.length} file${packet.includedFiles.length === 1 ? "" : "s"}` +
|
|
245
|
+
` \u2022 Omitted: ${packet.omittedFiles.length} file${packet.omittedFiles.length === 1 ? "" : "s"}` +
|
|
246
|
+
` \u2022 Budget: ${(packet.charBudget / 1000).toFixed(0)}K chars`,
|
|
247
|
+
),
|
|
248
|
+
1,
|
|
249
|
+
0,
|
|
250
|
+
),
|
|
251
|
+
);
|
|
252
|
+
container.addChild(new Spacer(1));
|
|
253
|
+
|
|
254
|
+
// ── Confirm / Cancel hints ──
|
|
255
|
+
container.addChild(
|
|
256
|
+
new Text(
|
|
257
|
+
` ${dim("Enter")} ${theme.fg("success", "Run review")} ${dim("\u2022")} ${dim("Esc")} ${theme.fg("muted", "Cancel")} ${dim("\u2022 y/n")}`,
|
|
258
|
+
1,
|
|
259
|
+
0,
|
|
260
|
+
),
|
|
261
|
+
);
|
|
262
|
+
container.addChild(new Spacer(1));
|
|
263
|
+
|
|
264
|
+
// ── Bottom border ──
|
|
265
|
+
container.addChild(new DynamicBorder((s: string) => accent(s)));
|
|
266
|
+
|
|
267
|
+
return container;
|
|
138
268
|
}
|
|
139
269
|
|
|
140
270
|
export async function selectBranch(ctx: ExtensionContext): Promise<string | undefined> {
|
|
@@ -170,47 +300,3 @@ export async function selectCommit(ctx: ExtensionContext): Promise<string | unde
|
|
|
170
300
|
onSelect: (item) => item.value,
|
|
171
301
|
});
|
|
172
302
|
}
|
|
173
|
-
|
|
174
|
-
function formatPlanPreview(plan: ReviewPlan): string {
|
|
175
|
-
const { model, snapshot, brief } = plan;
|
|
176
|
-
const parts: string[] = [
|
|
177
|
-
`Model: ${model.canonicalId}`,
|
|
178
|
-
`Snapshot: ${snapshot.title}`,
|
|
179
|
-
`Files changed: ${snapshot.changedFiles.length}`,
|
|
180
|
-
`Inline diff files: ${plan.packet.includedFiles.length}`,
|
|
181
|
-
`Omitted files: ${plan.packet.omittedFiles.length}`,
|
|
182
|
-
"",
|
|
183
|
-
"Summary:",
|
|
184
|
-
brief.summary,
|
|
185
|
-
"",
|
|
186
|
-
"Intended outcome:",
|
|
187
|
-
brief.intendedOutcome,
|
|
188
|
-
];
|
|
189
|
-
|
|
190
|
-
if (brief.constraints.length > 0) {
|
|
191
|
-
parts.push("", "Constraints:", ...brief.constraints.map((item) => `- ${item}`));
|
|
192
|
-
}
|
|
193
|
-
if (brief.focusAreas.length > 0) {
|
|
194
|
-
parts.push("", "Focus areas:", ...brief.focusAreas.map((item) => `- ${item}`));
|
|
195
|
-
}
|
|
196
|
-
if (brief.riskyFiles.length > 0) {
|
|
197
|
-
parts.push("", "Risky files:", ...brief.riskyFiles.map((item) => `- ${item}`));
|
|
198
|
-
}
|
|
199
|
-
if (brief.unresolvedQuestions.length > 0) {
|
|
200
|
-
parts.push(
|
|
201
|
-
"",
|
|
202
|
-
"Unresolved questions:",
|
|
203
|
-
...brief.unresolvedQuestions.map((item) => `- ${item}`),
|
|
204
|
-
);
|
|
205
|
-
}
|
|
206
|
-
if (plan.packet.omittedFiles.length > 0) {
|
|
207
|
-
parts.push(
|
|
208
|
-
"",
|
|
209
|
-
"Prompt coverage:",
|
|
210
|
-
`Included: ${plan.packet.includedFiles.join(", ") || "none"}`,
|
|
211
|
-
`Omitted: ${plan.packet.omittedFiles.join(", ")}`,
|
|
212
|
-
);
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return parts.join("\n");
|
|
216
|
-
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Theme-like interface consumed by review plan preview helpers.
|
|
3
|
+
*
|
|
4
|
+
* pi's `Theme` type is not publicly re-exported from
|
|
5
|
+
* `@earendil-works/pi-coding-agent`, so we define the subset of
|
|
6
|
+
* methods we use. A full `Theme` object is assignable to this type
|
|
7
|
+
* because `Theme.fg` accepts a superset of the color names listed
|
|
8
|
+
* here (contravariance: wider parameter type accepts narrower args).
|
|
9
|
+
*/
|
|
10
|
+
export type ReviewTheme = {
|
|
11
|
+
fg: (
|
|
12
|
+
color: "accent" | "dim" | "success" | "muted" | "warning" | "toolDiffAdded" | "toolDiffRemoved",
|
|
13
|
+
text: string,
|
|
14
|
+
) => string;
|
|
15
|
+
bold: (text: string) => string;
|
|
16
|
+
};
|