@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-review",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "SuPi Review extension — structured code review via /supi-review command",
5
5
  "license": "MIT",
6
6
  "repository": {
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 and packet coverage, then ask for approval. */
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.confirm("Run generated review?", formatPlanPreview(plan));
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
+ };