@mrclrchtr/supi-review 2.7.0 → 3.0.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.
Files changed (49) hide show
  1. package/README.md +75 -163
  2. package/node_modules/@mrclrchtr/supi-core/README.md +1 -1
  3. package/node_modules/@mrclrchtr/supi-core/package.json +2 -1
  4. package/node_modules/@mrclrchtr/supi-core/src/api.ts +2 -0
  5. package/node_modules/@mrclrchtr/supi-core/src/evidence-badge.ts +40 -0
  6. package/node_modules/@mrclrchtr/supi-core/src/settings/scoped-settings-list.ts +1 -0
  7. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-schema.ts +14 -0
  8. package/node_modules/@mrclrchtr/supi-core/src/settings/settings-submenus.ts +31 -14
  9. package/node_modules/@mrclrchtr/supi-core/src/settings.ts +1 -0
  10. package/package.json +3 -3
  11. package/src/config.ts +64 -0
  12. package/src/git-command.ts +78 -0
  13. package/src/git.ts +306 -505
  14. package/src/history/collect.ts +43 -120
  15. package/src/model.ts +35 -0
  16. package/src/review-path.ts +85 -0
  17. package/src/review-result.ts +43 -92
  18. package/src/review.ts +154 -288
  19. package/src/session/review-plan-store.ts +20 -51
  20. package/src/target/packet.ts +46 -369
  21. package/src/tool/agent-review-schemas.ts +101 -104
  22. package/src/tool/agent-review-tools.ts +269 -301
  23. package/src/tool/child-failure-diagnostics.ts +1 -1
  24. package/src/tool/child-resource-loader.ts +37 -0
  25. package/src/tool/child-session-runner.ts +83 -0
  26. package/src/tool/output-page.ts +47 -0
  27. package/src/tool/planner-runner.ts +69 -0
  28. package/src/tool/review-runner.ts +42 -257
  29. package/src/tool/review-system-prompt.ts +12 -110
  30. package/src/tool/review-tools.ts +119 -0
  31. package/src/tool/review-workflow.ts +309 -0
  32. package/src/tool/runner-helpers.ts +3 -8
  33. package/src/tool/schemas.ts +75 -62
  34. package/src/tui/common.ts +175 -0
  35. package/src/tui/prepare.ts +132 -0
  36. package/src/tui/run.ts +160 -0
  37. package/src/types.ts +153 -275
  38. package/src/history/synthesize.ts +0 -121
  39. package/src/tool/agent-review-workflow.ts +0 -398
  40. package/src/tool/brief-runner.ts +0 -180
  41. package/src/tool/guidance.ts +0 -21
  42. package/src/tool/review-handlers.ts +0 -314
  43. package/src/tool/snapshot-tools.ts +0 -117
  44. package/src/ui/flow.ts +0 -189
  45. package/src/ui/format-content.ts +0 -143
  46. package/src/ui/renderer.ts +0 -274
  47. package/src/ui/review-plan-inspector.ts +0 -391
  48. package/src/ui/review-tool-format.ts +0 -138
  49. package/src/ui/review-tool-renderer.ts +0 -234
@@ -1,314 +0,0 @@
1
- import {
2
- type AgentSession,
3
- type AgentSessionEvent,
4
- defineTool,
5
- } from "@earendil-works/pi-coding-agent";
6
- import type {
7
- ChildFailureDiagnostics,
8
- RawReviewResult,
9
- ReviewInvocation,
10
- ReviewOutputEvent,
11
- ReviewProgress,
12
- } from "../types.ts";
13
- import type { ChildLifecycleHostMarker } from "./child-lifecycle-trace.ts";
14
- import { buildProgressTokens } from "./runner-helpers.ts";
15
- import { reviewOutputSchema } from "./schemas.ts";
16
-
17
- const STEER_SUBMIT_MESSAGE =
18
- "You stopped without calling submit_review. Call submit_review now with your findings.";
19
- const DEFAULT_TIMEOUT_MS = 60 * 60 * 1_000;
20
-
21
- // ---------------------------------------------------------------------------
22
- // Runner-specific context combining lifecycle-managed fields with
23
- // runner-private mutable state.
24
- // ---------------------------------------------------------------------------
25
- export interface RunnerContext {
26
- progress: ReviewProgress;
27
- session: AgentSession;
28
- resolve: (result: RawReviewResult) => void;
29
- cleanup: (result: RawReviewResult) => RawReviewResult;
30
- state: { settled: boolean; aborting: boolean };
31
- resultHolder: { value: ReviewOutputEvent | undefined };
32
- invocation: ReviewInvocation;
33
- submitSteered: boolean;
34
- timeoutSteered: boolean;
35
- graceTurnsRemaining: number | undefined;
36
- getFailureDiagnostics: () => ChildFailureDiagnostics;
37
- recordHostMarker: (marker: ChildLifecycleHostMarker) => void;
38
- /** Accumulated per-tool-label execution counts. */
39
- toolCounts: Record<string, number>;
40
- /** Set of distinct file paths inspected via read_snapshot_diff / read_snapshot_file. */
41
- inspectedFiles: Set<string>;
42
- /** Timestamp (ms) when the runner started, for elapsed-time display. */
43
- startTime: number;
44
- }
45
-
46
- // ---------------------------------------------------------------------------
47
- // Tool and progress helpers
48
- // ---------------------------------------------------------------------------
49
-
50
- /** Maps tool names to compact display labels for the progress line. */
51
- function toolNameToLabel(name: string): string {
52
- const map: Record<string, string> = {
53
- read: "reads",
54
- grep: "greps",
55
- find: "finds",
56
- ls: "ls",
57
- submit_review: "submits",
58
- read_snapshot_diff: "diffs",
59
- read_snapshot_file: "file-reads",
60
- };
61
- return map[name] ?? name;
62
- }
63
-
64
- /** Maps tool names to focus labels for the progress narrative line. */
65
- function toolNameToFocusLabel(name: string): string {
66
- const map: Record<string, string> = {
67
- read: "Reading",
68
- grep: "Searching",
69
- find: "Finding",
70
- ls: "Listing",
71
- submit_review: "Submitting review",
72
- read_snapshot_diff: "Reading",
73
- read_snapshot_file: "Reading",
74
- };
75
- return map[name] ?? name;
76
- }
77
-
78
- /** Extract the basename from a file path, or return the path unchanged. */
79
- function extractBasename(file: string): string {
80
- const lastSlash = file.lastIndexOf("/");
81
- return lastSlash >= 0 ? file.slice(lastSlash + 1) : file;
82
- }
83
-
84
- /** Truncate a focus detail string to a display-friendly length. */
85
- function truncateFocusDetail(value: string): string {
86
- return value.length > 40 ? `${value.slice(0, 40)}…` : value;
87
- }
88
-
89
- /** Extract focus detail from a read-type tool (read, read_snapshot_diff, read_snapshot_file). */
90
- function tryExtractReadDetail(toolName: string, a: Record<string, unknown>): string | undefined {
91
- const file = (a.file ?? a.path) as string | undefined;
92
- if (!file) return undefined;
93
- const name = extractBasename(file);
94
- if (toolName === "read_snapshot_diff") return `${name} (diff)`;
95
- if (toolName === "read_snapshot_file") return `${name} (full)`;
96
- return name;
97
- }
98
-
99
- /** Extract a human-readable detail string from tool args for the focus display. */
100
- function tryExtractFocusDetail(toolName: string, args: unknown): string | undefined {
101
- if (typeof args !== "object" || args === null) return undefined;
102
- const a = args as Record<string, unknown>;
103
-
104
- // submit_review is self-explanatory — no detail needed
105
- if (toolName === "submit_review") return undefined;
106
-
107
- // Read-type tools: extract file path, show basename only
108
- if (
109
- toolName === "read" ||
110
- toolName === "read_snapshot_diff" ||
111
- toolName === "read_snapshot_file"
112
- ) {
113
- return tryExtractReadDetail(toolName, a);
114
- }
115
-
116
- // grep: show the search pattern
117
- if (toolName === "grep") {
118
- const pattern = a.pattern as string | undefined;
119
- return pattern ? truncateFocusDetail(pattern) : undefined;
120
- }
121
-
122
- // find: show the glob / pattern
123
- if (toolName === "find") {
124
- const pattern = (a.pattern ?? a.glob) as string | undefined;
125
- return pattern ? truncateFocusDetail(pattern) : undefined;
126
- }
127
-
128
- // ls: show the listing path
129
- if (toolName === "ls") {
130
- const path = a.path as string | undefined;
131
- return path ? truncateFocusDetail(path) : undefined;
132
- }
133
-
134
- return undefined;
135
- }
136
-
137
- /** Extract a file path from tool args when the tool is file-inspecting (for inspectedFiles tracking). */
138
- function tryExtractFileArg(toolName: string, args: unknown): string | undefined {
139
- if (toolName !== "read_snapshot_diff" && toolName !== "read_snapshot_file") return undefined;
140
- if (typeof args !== "object" || args === null) return undefined;
141
- const file = (args as Record<string, unknown>).file;
142
- return typeof file === "string" ? file : undefined;
143
- }
144
-
145
- export function createSubmitReviewTool(resultHolder: {
146
- value: ReviewOutputEvent | undefined;
147
- }): ReturnType<typeof defineTool> {
148
- return defineTool({
149
- name: "submit_review",
150
- label: "Submit Review",
151
- description:
152
- "Submit the final structured review result after you finish investigating the changes.",
153
- parameters: reviewOutputSchema,
154
- execute: async (_toolCallId, args) => {
155
- resultHolder.value = args as ReviewOutputEvent;
156
- return {
157
- content: [{ type: "text" as const, text: "Review submitted successfully." }],
158
- details: args,
159
- terminate: true,
160
- };
161
- },
162
- });
163
- }
164
-
165
- export function emitProgress(ctx: RunnerContext): void {
166
- ctx.progress.tokens = buildProgressTokens(() => ctx.session.getSessionStats());
167
- ctx.progress.toolCounts = { ...ctx.toolCounts };
168
- ctx.progress.filesInspected = ctx.inspectedFiles.size;
169
- ctx.progress.filesTotal = ctx.invocation.snapshot.stats.files;
170
- ctx.progress.elapsedMs = Date.now() - ctx.startTime;
171
- ctx.invocation.onProgress?.(ctx.progress);
172
- }
173
-
174
- // ---------------------------------------------------------------------------
175
- // Event handlers
176
- // ---------------------------------------------------------------------------
177
-
178
- export function handleTurnEnd(ctx: RunnerContext): void {
179
- ctx.progress.turns++;
180
-
181
- if (!ctx.state.settled && ctx.timeoutSteered && ctx.graceTurnsRemaining !== undefined) {
182
- ctx.graceTurnsRemaining--;
183
- if (ctx.graceTurnsRemaining <= 0) {
184
- ctx.state.aborting = true;
185
- doFinalAbort(ctx);
186
- }
187
- }
188
-
189
- emitProgress(ctx);
190
- }
191
-
192
- export function handleToolStart(
193
- event: Extract<AgentSessionEvent, { type: "tool_execution_start" }>,
194
- ctx: RunnerContext,
195
- ): void {
196
- ctx.progress.toolUses++;
197
-
198
- const label = toolNameToLabel(event.toolName);
199
- ctx.toolCounts[label] = (ctx.toolCounts[label] ?? 0) + 1;
200
-
201
- const file = tryExtractFileArg(event.toolName, event.args);
202
- if (file) ctx.inspectedFiles.add(file);
203
-
204
- const focusDetail = tryExtractFocusDetail(event.toolName, event.args);
205
- ctx.progress.currentFocus = {
206
- label: toolNameToFocusLabel(event.toolName),
207
- detail: focusDetail ?? "",
208
- };
209
-
210
- ctx.invocation.onToolActivity?.({ toolName: event.toolName, phase: "start" });
211
- emitProgress(ctx);
212
- }
213
-
214
- export function handleToolEnd(
215
- event: Extract<AgentSessionEvent, { type: "tool_execution_end" }>,
216
- ctx: RunnerContext,
217
- ): void {
218
- ctx.progress.currentFocus = undefined;
219
- ctx.invocation.onToolActivity?.({ toolName: event.toolName, phase: "end" });
220
- emitProgress(ctx);
221
- }
222
-
223
- export function handleMessageEnd(
224
- event: Extract<AgentSessionEvent, { type: "message_end" }>,
225
- ctx: RunnerContext,
226
- ): void {
227
- if (ctx.state.settled || ctx.submitSteered || ctx.resultHolder.value) return;
228
-
229
- const msg = event.message as { role?: string; stopReason?: string };
230
- if (msg.role !== "assistant" || msg.stopReason !== "stop") return;
231
-
232
- ctx.submitSteered = true;
233
- ctx.recordHostMarker({ type: "steer_requested", reason: "submit" });
234
- ctx.session.steer(STEER_SUBMIT_MESSAGE).catch(() => {});
235
- }
236
-
237
- /**
238
- * Finalize only after Pi has no retry, compaction recovery, or queued continuation left.
239
- * `agent_end` is a low-level boundary and is not terminal for a managed session.
240
- */
241
- export function handleAgentSettled(ctx: RunnerContext): void {
242
- if (ctx.state.settled || ctx.state.aborting) return;
243
-
244
- if (ctx.resultHolder.value) {
245
- ctx.resolve(
246
- ctx.cleanup({
247
- kind: "success",
248
- output: ctx.resultHolder.value,
249
- snapshot: ctx.invocation.snapshot,
250
- brief: ctx.invocation.brief,
251
- modelId: ctx.invocation.model.canonicalId,
252
- }),
253
- );
254
- return;
255
- }
256
-
257
- ctx.resolve(
258
- ctx.cleanup({
259
- kind: "failed",
260
- failureCode: "missing-structured-output",
261
- snapshot: ctx.invocation.snapshot,
262
- brief: ctx.invocation.brief,
263
- modelId: ctx.invocation.model.canonicalId,
264
- diagnostics: ctx.getFailureDiagnostics(),
265
- }),
266
- );
267
- }
268
-
269
- export function handleSessionEvent(event: AgentSessionEvent, ctx: RunnerContext): void {
270
- switch (event.type) {
271
- case "turn_end":
272
- handleTurnEnd(ctx);
273
- break;
274
- case "tool_execution_start":
275
- handleToolStart(event, ctx);
276
- break;
277
- case "tool_execution_end":
278
- handleToolEnd(event, ctx);
279
- break;
280
- case "message_end": {
281
- handleMessageEnd(event, ctx);
282
- break;
283
- }
284
- case "agent_settled":
285
- handleAgentSettled(ctx);
286
- break;
287
- default:
288
- break;
289
- }
290
- }
291
-
292
- // ---------------------------------------------------------------------------
293
- // Hard-abort helper (called from handleTurnEnd when grace turns expire)
294
- // ---------------------------------------------------------------------------
295
-
296
- function doFinalAbort(ctx: RunnerContext): void {
297
- emitProgress(ctx);
298
- ctx.recordHostMarker({ type: "abort_requested", reason: "timeout" });
299
- void ctx.session
300
- .abort()
301
- .catch(() => {})
302
- .finally(() => {
303
- ctx.resolve(
304
- ctx.cleanup({
305
- kind: "timeout" as const,
306
- snapshot: ctx.invocation.snapshot,
307
- timeoutMs: ctx.invocation.timeoutMs ?? DEFAULT_TIMEOUT_MS,
308
- brief: ctx.invocation.brief,
309
- modelId: ctx.invocation.model.canonicalId,
310
- diagnostics: ctx.getFailureDiagnostics(),
311
- }),
312
- );
313
- });
314
- }
@@ -1,117 +0,0 @@
1
- import { StringEnum } from "@earendil-works/pi-ai";
2
- import { defineTool } from "@earendil-works/pi-coding-agent";
3
- import { Type } from "typebox";
4
- import { getSnapshotFileContent, getSnapshotFileDiff } from "../git.ts";
5
- import type { ReviewSnapshot } from "../types.ts";
6
-
7
- /** Create the read_snapshot_diff custom tool for the reviewer child session. */
8
- export function createSnapshotDiffTool(
9
- cwd: string,
10
- snapshot: ReviewSnapshot,
11
- ): ReturnType<typeof defineTool> {
12
- return defineTool({
13
- name: "read_snapshot_diff",
14
- label: "Read Snapshot Diff",
15
- description:
16
- "Read the exact diff for a single changed file in the selected review snapshot. " +
17
- "The file must be in the snapshot's changed-files list.",
18
- parameters: Type.Object({
19
- file: Type.String(),
20
- }),
21
- execute: async (_toolCallId, args) => {
22
- const file = (args as { file: string }).file;
23
- if (!snapshot.changedFiles.includes(file)) {
24
- return {
25
- content: [
26
- {
27
- type: "text" as const,
28
- text:
29
- `Error: "${file}" is not in the snapshot's changed-files list. ` +
30
- "Use the changed-file manifest from the review task to pick a valid file.",
31
- },
32
- ],
33
- details: null,
34
- };
35
- }
36
-
37
- const diff = await getSnapshotFileDiff(cwd, snapshot, file);
38
- if (!diff.trim()) {
39
- return {
40
- content: [
41
- {
42
- type: "text" as const,
43
- text:
44
- `No diff available for "${file}" in this snapshot. ` +
45
- "The file may be untracked; use read_snapshot_file to inspect its current content.",
46
- },
47
- ],
48
- details: null,
49
- };
50
- }
51
-
52
- return {
53
- content: [{ type: "text" as const, text: diff }],
54
- details: null,
55
- };
56
- },
57
- });
58
- }
59
-
60
- /** Create the read_snapshot_file custom tool for the reviewer child session. */
61
- export function createSnapshotFileTool(
62
- cwd: string,
63
- snapshot: ReviewSnapshot,
64
- ): ReturnType<typeof defineTool> {
65
- return defineTool({
66
- name: "read_snapshot_file",
67
- label: "Read Snapshot File",
68
- description:
69
- "Read the before or after content of a single changed file in the selected review snapshot. " +
70
- '"before" shows the file before the change (HEAD for working tree, base for branch, parent for commit). ' +
71
- '"after" shows the file after the change. The file must be in the snapshot\'s changed-files list.',
72
- parameters: Type.Object({
73
- file: Type.String(),
74
- side: StringEnum(["before", "after"] as const),
75
- }),
76
- execute: async (_toolCallId, args) => {
77
- const { file, side } = args as { file: string; side: "before" | "after" };
78
-
79
- if (!snapshot.changedFiles.includes(file)) {
80
- return {
81
- content: [
82
- {
83
- type: "text" as const,
84
- text:
85
- `Error: "${file}" is not in the snapshot's changed-files list. ` +
86
- "Use the changed-file manifest from the review task to pick a valid file.",
87
- },
88
- ],
89
- details: null,
90
- };
91
- }
92
-
93
- const content = await getSnapshotFileContent(cwd, snapshot, file, side);
94
-
95
- if (content === undefined) {
96
- const hint =
97
- side === "before"
98
- ? "The file may be newly added or renamed (use read_snapshot_diff to check for renames)."
99
- : "The file may have been deleted.";
100
- return {
101
- content: [
102
- {
103
- type: "text" as const,
104
- text: `Content for "${file}" (${side}) is not available. ${hint}`,
105
- },
106
- ],
107
- details: null,
108
- };
109
- }
110
-
111
- return {
112
- content: [{ type: "text" as const, text: content }],
113
- details: null,
114
- };
115
- },
116
- });
117
- }
package/src/ui/flow.ts DELETED
@@ -1,189 +0,0 @@
1
- import { DynamicBorder, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import { Container, type SelectItem, SelectList, Text, visibleWidth } from "@earendil-works/pi-tui";
3
- import { getLocalBranches, getRecentCommits } from "../git.ts";
4
- import { getSelectableReviewModels } from "../model.ts";
5
- import type { ReviewModelSelection, ReviewPlan, ReviewTargetSpec } from "../types.ts";
6
- import { ReviewPlanPreviewComponent } from "./review-plan-inspector.ts";
7
-
8
- interface SelectFromListOptions<T> {
9
- items: SelectItem[];
10
- title: string;
11
- maxHeight: number;
12
- onSelect: (item: SelectItem) => T | undefined;
13
- initialIndex?: number;
14
- }
15
-
16
- /** Shared TUI helper for bordered select lists used throughout the review flow. */
17
- function selectFromList<T>(
18
- ctx: ExtensionContext,
19
- options: SelectFromListOptions<T>,
20
- ): Promise<T | undefined> {
21
- const { items, title, maxHeight, onSelect, initialIndex } = options;
22
- const widestLabel = items.reduce((w, item) => Math.max(w, visibleWidth(item.label)), 0);
23
- const maxPrimaryColumnWidth = Math.max(widestLabel + 4, 32);
24
-
25
- return ctx.ui.custom<T | undefined>((tui, theme, _kb, done) => {
26
- const container = new Container();
27
- container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
28
- container.addChild(new Text(theme.fg("accent", title), 1, 0));
29
-
30
- const selectList = new SelectList(
31
- items,
32
- Math.min(items.length, maxHeight),
33
- {
34
- selectedPrefix: (text) => theme.fg("accent", text),
35
- selectedText: (text) => theme.fg("accent", text),
36
- description: (text) => theme.fg("muted", text),
37
- scrollInfo: (text) => theme.fg("dim", text),
38
- noMatch: (text) => theme.fg("warning", text),
39
- },
40
- { maxPrimaryColumnWidth },
41
- );
42
- if (initialIndex !== undefined) {
43
- selectList.setSelectedIndex(initialIndex);
44
- }
45
- selectList.onSelect = (item) => done(onSelect(item));
46
- selectList.onCancel = () => done(undefined);
47
-
48
- container.addChild(selectList);
49
- container.addChild(new Text(theme.fg("dim", "↑↓ navigate • enter select • esc cancel"), 1, 0));
50
- container.addChild(new DynamicBorder((text: string) => theme.fg("accent", text)));
51
-
52
- return {
53
- render: (width) => container.render(width),
54
- invalidate: () => container.invalidate(),
55
- handleInput: (data) => {
56
- selectList.handleInput(data);
57
- tui.requestRender();
58
- },
59
- };
60
- });
61
- }
62
-
63
- /** Prompt for the concrete git target to review. */
64
- export async function selectTarget(ctx: ExtensionContext): Promise<ReviewTargetSpec | undefined> {
65
- const preset = await selectFromList(ctx, {
66
- items: [
67
- {
68
- value: "working-tree",
69
- label: "Review working tree",
70
- description: "Staged, unstaged, and untracked changes in the current checkout",
71
- },
72
- {
73
- value: "branch",
74
- label: "Review changes vs base branch",
75
- description: "Compare the current branch against another local branch",
76
- },
77
- {
78
- value: "commit",
79
- label: "Review a commit",
80
- description: "Review a single recent commit",
81
- },
82
- ],
83
- title: "Select review target",
84
- maxHeight: 8,
85
- onSelect: (item) => item.value,
86
- });
87
-
88
- if (!preset) return undefined;
89
-
90
- switch (preset) {
91
- case "working-tree":
92
- return { kind: "working-tree" };
93
- case "branch": {
94
- const base = await selectBranch(ctx);
95
- return base ? { kind: "branch", base } : undefined;
96
- }
97
- case "commit": {
98
- const sha = await selectCommit(ctx);
99
- return sha ? { kind: "commit", sha } : undefined;
100
- }
101
- default:
102
- return undefined;
103
- }
104
- }
105
-
106
- /** Prompt for the explicit reviewer model used by both synthesis and review. */
107
- export async function selectModel(
108
- ctx: Pick<ExtensionContext, "cwd" | "modelRegistry" | "model" | "ui">,
109
- ): Promise<ReviewModelSelection | undefined> {
110
- const models = getSelectableReviewModels(ctx);
111
- if (models.length === 0) {
112
- ctx.ui.notify(
113
- "No scoped models are enabled for /supi-review. Configure enabledModels or use /scoped-models.",
114
- "error",
115
- );
116
- return undefined;
117
- }
118
-
119
- const currentIndex = models.findIndex((model) => model.isCurrent);
120
-
121
- return selectFromList(ctx as ExtensionContext, {
122
- items: models.map((model) => ({
123
- value: model.canonicalId,
124
- label: model.isCurrent ? `${model.canonicalId} [current]` : model.canonicalId,
125
- description: model.label !== model.canonicalId ? model.label : undefined,
126
- })),
127
- title: "Select scoped reviewer model",
128
- maxHeight: 12,
129
- initialIndex: currentIndex >= 0 ? currentIndex : 0,
130
- onSelect: (item) => models.find((model) => model.canonicalId === item.value),
131
- });
132
- }
133
-
134
- /** Collect an optional freeform note for the generated review brief. */
135
- export async function collectReviewNote(ctx: ExtensionContext): Promise<string | undefined> {
136
- const value = await ctx.ui.input(
137
- "Optional review note",
138
- "Anything specific to watch for? Leave empty to skip.",
139
- );
140
- if (value === undefined) return undefined;
141
- return value.trim();
142
- }
143
-
144
- /** Show the synthesized brief, the actual reviewer prompt preview, and ask for approval. */
145
- export function previewReviewPlan(ctx: ExtensionContext, plan: ReviewPlan): Promise<boolean> {
146
- return ctx.ui.custom<boolean>(
147
- (tui, theme, _kb, done) =>
148
- new ReviewPlanPreviewComponent({
149
- plan,
150
- theme,
151
- onDone: done,
152
- requestRender: () => tui.requestRender(),
153
- }),
154
- );
155
- }
156
-
157
- export async function selectBranch(ctx: ExtensionContext): Promise<string | undefined> {
158
- const branches = await getLocalBranches(ctx.cwd);
159
- if (branches.length === 0) {
160
- ctx.ui.notify("No local branches found", "warning");
161
- return undefined;
162
- }
163
-
164
- return selectFromList(ctx, {
165
- items: branches.map((branch) => ({ value: branch, label: branch })),
166
- title: "Select base branch",
167
- maxHeight: 15,
168
- onSelect: (item) => item.value,
169
- });
170
- }
171
-
172
- export async function selectCommit(ctx: ExtensionContext): Promise<string | undefined> {
173
- const commits = await getRecentCommits(ctx.cwd, 30);
174
- if (commits.length === 0) {
175
- ctx.ui.notify("No recent commits found", "warning");
176
- return undefined;
177
- }
178
-
179
- return selectFromList(ctx, {
180
- items: commits.map((commit) => ({
181
- value: commit.sha,
182
- label: `${commit.sha.slice(0, 7)} ${commit.subject}`,
183
- description: commit.sha,
184
- })),
185
- title: "Select commit to review",
186
- maxHeight: 15,
187
- onSelect: (item) => item.value,
188
- });
189
- }