@mrclrchtr/supi-review 1.5.0 → 1.6.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/ui/flow.ts +133 -47
- package/src/ui/theme-type.ts +16 -0
package/package.json
CHANGED
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
|
+
};
|