@diegopetrucci/pi-extensions 0.1.73 → 0.1.76
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/LICENSE +2 -0
- package/README.md +9 -4
- package/extensions/agent-workflow-audit/index.ts +1398 -1009
- package/extensions/agent-workflow-audit/package.json +1 -1
- package/extensions/annotate-git-diff/README.md +3 -1
- package/extensions/annotate-git-diff/clipboard.ts +109 -100
- package/extensions/annotate-git-diff/git.ts +853 -767
- package/extensions/annotate-git-diff/glimpseui.d.ts +87 -78
- package/extensions/annotate-git-diff/index.ts +712 -671
- package/extensions/annotate-git-diff/package.json +1 -1
- package/extensions/annotate-git-diff/prompt.ts +45 -45
- package/extensions/annotate-git-diff/quiet-glimpse.ts +161 -155
- package/extensions/annotate-git-diff/review-server.ts +337 -311
- package/extensions/annotate-git-diff/types.ts +132 -132
- package/extensions/annotate-git-diff/ui.ts +27 -27
- package/extensions/annotate-git-diff/watch.ts +74 -74
- package/extensions/annotate-git-diff/web/app.js +1947 -1884
- package/extensions/annotate-git-diff/web/index.html +5 -6
- package/extensions/annotate-last-message/README.md +1 -1
- package/extensions/annotate-last-message/glimpseui.d.ts +87 -78
- package/extensions/annotate-last-message/index.ts +210 -206
- package/extensions/annotate-last-message/package.json +1 -1
- package/extensions/annotate-last-message/prompt.ts +87 -66
- package/extensions/annotate-last-message/quiet-glimpse.ts +161 -155
- package/extensions/annotate-last-message/session.ts +94 -89
- package/extensions/annotate-last-message/theme.ts +199 -193
- package/extensions/annotate-last-message/types.ts +23 -23
- package/extensions/annotate-last-message/ui.ts +27 -25
- package/extensions/annotate-last-message/web/app.js +4 -7
- package/extensions/annotate-last-message/web/md-renderer.js +1 -1
- package/extensions/brrr/README.md +1 -1
- package/extensions/brrr/index.ts +213 -191
- package/extensions/brrr/package.json +1 -1
- package/extensions/claude-fast/README.md +5 -79
- package/extensions/claude-fast/index.ts +31 -289
- package/extensions/claude-fast/package.json +2 -3
- package/extensions/code-reviewer/README.md +1 -1
- package/extensions/code-reviewer/index.ts +1598 -1350
- package/extensions/code-reviewer/package.json +1 -1
- package/extensions/confirm-destructive/README.md +1 -1
- package/extensions/confirm-destructive/index.ts +55 -51
- package/extensions/confirm-destructive/package.json +1 -1
- package/extensions/context-cap/index.ts +312 -304
- package/extensions/context-cap/package.json +1 -1
- package/extensions/context-inspector/index.ts +1211 -1064
- package/extensions/context-inspector/package.json +1 -1
- package/extensions/contrarian/index.ts +1854 -1735
- package/extensions/contrarian/package.json +1 -1
- package/extensions/dirty-repo-guard/index.ts +41 -41
- package/extensions/dynamic-context-pruning/README.md +6 -38
- package/extensions/dynamic-context-pruning/index.ts +2124 -1980
- package/extensions/dynamic-context-pruning/package.json +1 -1
- package/extensions/fast/README.md +71 -21
- package/extensions/fast/index.ts +513 -313
- package/extensions/fast/package.json +1 -1
- package/extensions/git-footer/README.md +1 -1
- package/extensions/git-footer/index.ts +458 -411
- package/extensions/git-footer/package.json +2 -3
- package/extensions/gnosis/index.ts +180 -130
- package/extensions/gnosis/package.json +1 -1
- package/extensions/inline-bash/index.ts +88 -88
- package/extensions/inline-bash/package.json +1 -1
- package/extensions/librarian/index.ts +1669 -1419
- package/extensions/librarian/package.json +1 -1
- package/extensions/minimal-footer/index.ts +994 -1012
- package/extensions/minimal-footer/openai-usage.ts +195 -190
- package/extensions/notify/index.ts +222 -230
- package/extensions/notify/package.json +1 -1
- package/extensions/openai-fast/README.md +5 -73
- package/extensions/openai-fast/index.ts +32 -272
- package/extensions/openai-fast/package.json +2 -3
- package/extensions/oracle/index.ts +1731 -1625
- package/extensions/oracle/package.json +1 -1
- package/extensions/permission-gate/index.ts +658 -608
- package/extensions/permission-gate/package.json +1 -1
- package/extensions/permission-gate/powershell-safety.ts +840 -793
- package/extensions/project-mcp-json/.pi-fleet-tested-version +1 -0
- package/extensions/project-mcp-json/README.md +199 -0
- package/extensions/project-mcp-json/config.ts +564 -0
- package/extensions/project-mcp-json/index.ts +119 -0
- package/extensions/project-mcp-json/package.json +38 -0
- package/extensions/quiet-tools/index.ts +282 -264
- package/extensions/review/README.md +2 -2
- package/extensions/review/index.ts +1745 -1872
- package/extensions/review/package.json +1 -1
- package/extensions/todo/index.ts +284 -265
- package/extensions/triage-comments/README.md +2 -2
- package/extensions/triage-comments/index.ts +2294 -1882
- package/extensions/triage-comments/package.json +1 -1
- package/package.json +6 -3
- package/assets/social-preview.png +0 -0
- package/extensions/claude-fast/claude-fast.example.json +0 -4
- package/extensions/openai-fast/openai-fast.example.json +0 -4
|
@@ -2,54 +2,30 @@
|
|
|
2
2
|
* Adapted from:
|
|
3
3
|
* https://github.com/mitsuhiko/agent-stuff/blob/main/extensions/review.ts
|
|
4
4
|
*
|
|
5
|
-
* Upstream source is licensed under Apache-2.0.
|
|
6
|
-
* the original behavior for tlh/pi compatibility as a standalone package.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Code Review Extension (inspired by Codex's review feature)
|
|
11
|
-
*
|
|
12
|
-
* Provides a `/review` command that prompts the agent to review code changes.
|
|
13
|
-
* Supports multiple review modes:
|
|
14
|
-
* - Review a GitHub pull request (checks out the PR locally)
|
|
15
|
-
* - Review against a base branch (PR style)
|
|
16
|
-
* - Review uncommitted changes
|
|
17
|
-
* - Review a specific commit
|
|
18
|
-
* - Shared custom review instructions (applied to all review modes when configured)
|
|
19
|
-
*
|
|
20
|
-
* Usage:
|
|
21
|
-
* - `/review` - show interactive selector
|
|
22
|
-
* - `/review pr 123` - review PR #123 (checks out locally)
|
|
23
|
-
* - `/review pr https://github.com/owner/repo/pull/123` - review PR from URL
|
|
24
|
-
* - `/review uncommitted` - review uncommitted changes directly
|
|
25
|
-
* - `/review branch main` - review against main branch
|
|
26
|
-
* - `/review commit abc123` - review specific commit
|
|
27
|
-
* - `/review folder src docs` - review specific folders/files (snapshot, not diff)
|
|
28
|
-
* - `/review` selector includes Add/Remove custom review instructions (applies to all modes)
|
|
29
|
-
* - `/review --extra "focus on performance regressions"` - add extra review instruction (works with any mode)
|
|
5
|
+
* Upstream source is licensed under Apache-2.0.
|
|
30
6
|
*
|
|
31
|
-
*
|
|
32
|
-
* -
|
|
33
|
-
* the project config directory, its contents are appended to the review prompt.
|
|
34
|
-
*
|
|
35
|
-
* Note: PR review requires a clean working tree (no uncommitted changes to tracked files).
|
|
7
|
+
* This fork adds loop fixing, persisted custom review instructions, a
|
|
8
|
+
* blocking-findings parser, and /end-review handoff modes.
|
|
36
9
|
*/
|
|
37
10
|
|
|
38
|
-
import type {
|
|
11
|
+
import type {
|
|
12
|
+
ExtensionAPI,
|
|
13
|
+
ExtensionContext,
|
|
14
|
+
ExtensionCommandContext,
|
|
15
|
+
} from "@earendil-works/pi-coding-agent";
|
|
39
16
|
import { CONFIG_DIR_NAME, DynamicBorder, BorderedLoader } from "@earendil-works/pi-coding-agent";
|
|
40
17
|
import {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
18
|
+
Container,
|
|
19
|
+
fuzzyFilter,
|
|
20
|
+
Input,
|
|
21
|
+
type SelectItem,
|
|
22
|
+
SelectList,
|
|
23
|
+
Spacer,
|
|
24
|
+
Text,
|
|
48
25
|
} from "@earendil-works/pi-tui";
|
|
49
26
|
import path from "node:path";
|
|
50
27
|
import { promises as fs } from "node:fs";
|
|
51
28
|
|
|
52
|
-
|
|
53
29
|
// State to track fresh session review (where we branched from).
|
|
54
30
|
// Module-level state means only one review can be active at a time.
|
|
55
31
|
// This is intentional - the UI and /end-review command assume a single active review.
|
|
@@ -60,21 +36,21 @@ let reviewCustomInstructions: string | undefined = undefined;
|
|
|
60
36
|
let reviewLoopInProgress = false;
|
|
61
37
|
|
|
62
38
|
function getReviewRuntimeState() {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
39
|
+
return {
|
|
40
|
+
reviewOriginId,
|
|
41
|
+
endReviewInProgress,
|
|
42
|
+
reviewLoopFixingEnabled,
|
|
43
|
+
reviewCustomInstructions,
|
|
44
|
+
reviewLoopInProgress,
|
|
45
|
+
};
|
|
70
46
|
}
|
|
71
47
|
|
|
72
48
|
function resetReviewRuntimeState() {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
49
|
+
reviewOriginId = undefined;
|
|
50
|
+
endReviewInProgress = false;
|
|
51
|
+
reviewLoopFixingEnabled = false;
|
|
52
|
+
reviewCustomInstructions = undefined;
|
|
53
|
+
reviewLoopInProgress = false;
|
|
78
54
|
}
|
|
79
55
|
|
|
80
56
|
const REVIEW_STATE_TYPE = "review-session";
|
|
@@ -85,341 +61,374 @@ const REVIEW_LOOP_START_TIMEOUT_MS = 15000;
|
|
|
85
61
|
const REVIEW_LOOP_START_POLL_MS = 50;
|
|
86
62
|
|
|
87
63
|
type ReviewSessionState = {
|
|
88
|
-
|
|
89
|
-
|
|
64
|
+
active: boolean;
|
|
65
|
+
originId?: string;
|
|
90
66
|
};
|
|
91
67
|
|
|
92
68
|
type ReviewSettingsState = {
|
|
93
|
-
|
|
94
|
-
|
|
69
|
+
loopFixingEnabled?: boolean;
|
|
70
|
+
customInstructions?: string;
|
|
95
71
|
};
|
|
96
72
|
|
|
97
73
|
type ProjectConfigContext = {
|
|
98
|
-
|
|
99
|
-
|
|
74
|
+
cwd: string;
|
|
75
|
+
isProjectTrusted?: () => boolean;
|
|
100
76
|
};
|
|
101
77
|
|
|
102
78
|
function canReadProjectConfig(ctx: ProjectConfigContext): boolean {
|
|
103
|
-
|
|
79
|
+
return typeof ctx.isProjectTrusted === "function" && ctx.isProjectTrusted();
|
|
104
80
|
}
|
|
105
81
|
|
|
106
82
|
function setReviewWidget(ctx: ExtensionContext, active: boolean) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
83
|
+
if (!ctx.hasUI) return;
|
|
84
|
+
if (!active || ctx.mode !== "tui") {
|
|
85
|
+
ctx.ui.setWidget("review", undefined);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
ctx.ui.setWidget("review", (_tui, theme) => {
|
|
90
|
+
const message = reviewLoopInProgress
|
|
91
|
+
? "Review session active (loop fixing running)"
|
|
92
|
+
: reviewLoopFixingEnabled
|
|
93
|
+
? "Review session active (loop fixing enabled), return with /end-review"
|
|
94
|
+
: "Review session active, return with /end-review";
|
|
95
|
+
const text = new Text(theme.fg("warning", message), 0, 0);
|
|
96
|
+
return {
|
|
97
|
+
render(width: number) {
|
|
98
|
+
return text.render(width);
|
|
99
|
+
},
|
|
100
|
+
invalidate() {
|
|
101
|
+
text.invalidate();
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
});
|
|
129
105
|
}
|
|
130
106
|
|
|
131
107
|
function getReviewState(ctx: ExtensionContext): ReviewSessionState | undefined {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
108
|
+
let state: ReviewSessionState | undefined;
|
|
109
|
+
for (const entry of ctx.sessionManager.getBranch()) {
|
|
110
|
+
if (entry.type === "custom" && entry.customType === REVIEW_STATE_TYPE) {
|
|
111
|
+
state = entry.data as ReviewSessionState | undefined;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return state;
|
|
140
116
|
}
|
|
141
117
|
|
|
142
118
|
function applyReviewState(ctx: ExtensionContext) {
|
|
143
|
-
|
|
119
|
+
const state = getReviewState(ctx);
|
|
144
120
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
121
|
+
if (state?.active && state.originId) {
|
|
122
|
+
reviewOriginId = state.originId;
|
|
123
|
+
setReviewWidget(ctx, true);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
150
126
|
|
|
151
|
-
|
|
152
|
-
|
|
127
|
+
reviewOriginId = undefined;
|
|
128
|
+
setReviewWidget(ctx, false);
|
|
153
129
|
}
|
|
154
130
|
|
|
155
131
|
function getReviewSettings(ctx: ExtensionContext): ReviewSettingsState {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
132
|
+
let state: ReviewSettingsState | undefined;
|
|
133
|
+
for (const entry of ctx.sessionManager.getEntries()) {
|
|
134
|
+
if (entry.type === "custom" && entry.customType === REVIEW_SETTINGS_TYPE) {
|
|
135
|
+
state = entry.data as ReviewSettingsState | undefined;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
loopFixingEnabled: state?.loopFixingEnabled === true,
|
|
141
|
+
customInstructions: state?.customInstructions?.trim() || undefined,
|
|
142
|
+
};
|
|
167
143
|
}
|
|
168
144
|
|
|
169
145
|
function applyReviewSettings(ctx: ExtensionContext) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
146
|
+
const state = getReviewSettings(ctx);
|
|
147
|
+
reviewLoopFixingEnabled = state.loopFixingEnabled === true;
|
|
148
|
+
reviewCustomInstructions = state.customInstructions?.trim() || undefined;
|
|
173
149
|
}
|
|
174
150
|
|
|
175
151
|
function parseMarkdownHeading(line: string): { level: number; title: string } | null {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
152
|
+
const headingMatch = line.match(/^\s*(#{1,6})\s+(.+?)\s*$/);
|
|
153
|
+
if (!headingMatch) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const rawTitle = headingMatch[2].replace(/\s+#+\s*$/, "").trim();
|
|
158
|
+
return {
|
|
159
|
+
level: headingMatch[1].length,
|
|
160
|
+
title: rawTitle,
|
|
161
|
+
};
|
|
186
162
|
}
|
|
187
163
|
|
|
188
164
|
function getFindingsSectionBounds(lines: string[]): { start: number; end: number } | null {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
165
|
+
let start = -1;
|
|
166
|
+
let findingsHeadingLevel: number | null = null;
|
|
167
|
+
|
|
168
|
+
for (let i = 0; i < lines.length; i++) {
|
|
169
|
+
const line = lines[i];
|
|
170
|
+
const heading = parseMarkdownHeading(line);
|
|
171
|
+
if (heading && /^findings\b/i.test(heading.title)) {
|
|
172
|
+
start = i + 1;
|
|
173
|
+
findingsHeadingLevel = heading.level;
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
if (/^\s*findings\s*:?\s*$/i.test(line)) {
|
|
177
|
+
start = i + 1;
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (start < 0) {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let end = lines.length;
|
|
187
|
+
for (let i = start; i < lines.length; i++) {
|
|
188
|
+
const line = lines[i];
|
|
189
|
+
const heading = parseMarkdownHeading(line);
|
|
190
|
+
if (heading) {
|
|
191
|
+
const normalizedTitle = heading.title.replace(/[*_`]/g, "").trim();
|
|
192
|
+
if (
|
|
193
|
+
/^(review scope|verdict|overall verdict|fix queue|constraints(?:\s*&\s*preferences)?)\b:?/i.test(
|
|
194
|
+
normalizedTitle,
|
|
195
|
+
)
|
|
196
|
+
) {
|
|
197
|
+
end = i;
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (/\[P[0-3]\]/i.test(heading.title)) {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (findingsHeadingLevel !== null && heading.level <= findingsHeadingLevel) {
|
|
206
|
+
end = i;
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (
|
|
212
|
+
/^\s*(review scope|verdict|overall verdict|fix queue|constraints(?:\s*&\s*preferences)?)\b:?/i.test(
|
|
213
|
+
line,
|
|
214
|
+
)
|
|
215
|
+
) {
|
|
216
|
+
end = i;
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return { start, end };
|
|
238
222
|
}
|
|
239
223
|
|
|
240
224
|
function isLikelyFindingLine(line: string): boolean {
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
225
|
+
if (!/\[P[0-3]\]/i.test(line)) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (/^\s*(?:[-*+]|(?:\d+)[.)]|#{1,6})\s+priority\s+tag\b/i.test(line)) {
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (
|
|
234
|
+
/^\s*(?:[-*+]|(?:\d+)[.)]|#{1,6})\s+\[P[0-3]\]\s*-\s*(?:drop everything|urgent|normal|low|nice to have)\b/i.test(
|
|
235
|
+
line,
|
|
236
|
+
)
|
|
237
|
+
) {
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const allPriorityTags = line.match(/\[P[0-3]\]/gi) ?? [];
|
|
242
|
+
if (allPriorityTags.length > 1) {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (/^\s*(?:[-*+]|(?:\d+)[.)])\s+/.test(line)) {
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (/^\s*#{1,6}\s+/.test(line)) {
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (/^\s*(?:\*\*|__)?\[P[0-3]\](?:\*\*|__)?(?=\s|:|-)/i.test(line)) {
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return false;
|
|
271
259
|
}
|
|
272
260
|
|
|
273
261
|
function normalizeVerdictValue(value: string): string {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
262
|
+
return value
|
|
263
|
+
.trim()
|
|
264
|
+
.replace(/^[-*+]\s*/, "")
|
|
265
|
+
.replace(/^['"`]+|['"`]+$/g, "")
|
|
266
|
+
.toLowerCase();
|
|
279
267
|
}
|
|
280
268
|
|
|
281
269
|
function isNeedsAttentionVerdictValue(value: string): boolean {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
270
|
+
const normalized = normalizeVerdictValue(value);
|
|
271
|
+
if (!normalized.includes("needs attention")) {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (/\bnot\s+needs\s+attention\b/.test(normalized)) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Reject rubric/choice phrasing like "correct or needs attention", but
|
|
280
|
+
// keep legitimate verdict text that may contain unrelated "or".
|
|
281
|
+
if (/\bcorrect\b/.test(normalized) && /\bor\b/.test(normalized)) {
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return true;
|
|
298
286
|
}
|
|
299
287
|
|
|
300
288
|
function hasNeedsAttentionVerdict(messageText: string): boolean {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
289
|
+
const lines = messageText.split(/\r?\n/);
|
|
290
|
+
|
|
291
|
+
for (const line of lines) {
|
|
292
|
+
const inlineMatch = line.match(/^\s*(?:[*-+]\s*)?(?:overall\s+)?verdict\s*:\s*(.+)$/i);
|
|
293
|
+
if (inlineMatch && isNeedsAttentionVerdictValue(inlineMatch[1])) {
|
|
294
|
+
return true;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
for (let i = 0; i < lines.length; i++) {
|
|
299
|
+
const line = lines[i];
|
|
300
|
+
const heading = parseMarkdownHeading(line);
|
|
301
|
+
|
|
302
|
+
let verdictLevel: number | null = null;
|
|
303
|
+
if (heading) {
|
|
304
|
+
const normalizedHeading = heading.title.replace(/[*_`]/g, "").trim();
|
|
305
|
+
if (!/^(?:overall\s+)?verdict\b/i.test(normalizedHeading)) {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
verdictLevel = heading.level;
|
|
309
|
+
} else if (!/^\s*(?:overall\s+)?verdict\s*:?\s*$/i.test(line)) {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
314
|
+
const verdictLine = lines[j];
|
|
315
|
+
const nextHeading = parseMarkdownHeading(verdictLine);
|
|
316
|
+
if (nextHeading) {
|
|
317
|
+
const normalizedNextHeading = nextHeading.title.replace(/[*_`]/g, "").trim();
|
|
318
|
+
if (verdictLevel === null || nextHeading.level <= verdictLevel) {
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
if (
|
|
322
|
+
/^(review scope|findings|fix queue|constraints(?:\s*&\s*preferences)?)\b:?/i.test(
|
|
323
|
+
normalizedNextHeading,
|
|
324
|
+
)
|
|
325
|
+
) {
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const trimmed = verdictLine.trim();
|
|
331
|
+
if (!trimmed) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (isNeedsAttentionVerdictValue(trimmed)) {
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (/\bcorrect\b/i.test(normalizeVerdictValue(trimmed))) {
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return false;
|
|
354
346
|
}
|
|
355
347
|
|
|
356
348
|
function hasBlockingReviewFindings(messageText: string): boolean {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
349
|
+
const lines = messageText.split(/\r?\n/);
|
|
350
|
+
const bounds = getFindingsSectionBounds(lines);
|
|
351
|
+
const candidateLines = bounds ? lines.slice(bounds.start, bounds.end) : lines;
|
|
352
|
+
|
|
353
|
+
let inCodeFence = false;
|
|
354
|
+
let foundTaggedFinding = false;
|
|
355
|
+
for (const line of candidateLines) {
|
|
356
|
+
if (/^\s*```/.test(line)) {
|
|
357
|
+
inCodeFence = !inCodeFence;
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
if (inCodeFence) {
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (!isLikelyFindingLine(line)) {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
foundTaggedFinding = true;
|
|
369
|
+
if (/\[(P0|P1|P2)\]/i.test(line)) {
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (foundTaggedFinding) {
|
|
375
|
+
return false;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return hasNeedsAttentionVerdict(messageText);
|
|
387
379
|
}
|
|
388
380
|
|
|
389
381
|
// Review target types (matching Codex's approach)
|
|
390
382
|
type ReviewTarget =
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
383
|
+
| { type: "uncommitted" }
|
|
384
|
+
| { type: "baseBranch"; branch: string }
|
|
385
|
+
| { type: "commit"; sha: string; title?: string }
|
|
386
|
+
| { type: "pullRequest"; prNumber: number; baseBranch: string; title: string }
|
|
387
|
+
| { type: "folder"; paths: string[] };
|
|
396
388
|
|
|
397
389
|
// Prompts (adapted from Codex)
|
|
398
390
|
const UNCOMMITTED_PROMPT =
|
|
399
|
-
|
|
391
|
+
"Review the current code changes (staged, unstaged, and untracked files) and provide prioritized findings.";
|
|
400
392
|
|
|
401
393
|
const LOCAL_CHANGES_REVIEW_INSTRUCTIONS =
|
|
402
|
-
|
|
394
|
+
"Also include local working-tree changes (staged, unstaged, and untracked files) from this branch. Use `git status --porcelain`, `git diff`, `git diff --staged`, and `git ls-files --others --exclude-standard` so local fixes are part of this review cycle.";
|
|
403
395
|
|
|
404
396
|
const BASE_BRANCH_PROMPT_WITH_MERGE_BASE =
|
|
405
|
-
|
|
397
|
+
"Review the code changes against the base branch '{baseBranch}'. The merge base commit for this comparison is {mergeBaseSha}. Run `git diff {mergeBaseSha}` to inspect the changes relative to {baseBranch}. Provide prioritized, actionable findings.";
|
|
406
398
|
|
|
407
399
|
const BASE_BRANCH_PROMPT_FALLBACK =
|
|
408
|
-
|
|
400
|
+
'Review the code changes against the base branch \'{branch}\'. Start by finding the merge diff between the current branch and {branch}\'s upstream e.g. (`git merge-base HEAD "$(git rev-parse --abbrev-ref "{branch}@{upstream}")"`), then run `git diff` against that SHA to see what changes we would merge into the {branch} branch. Provide prioritized, actionable findings.';
|
|
409
401
|
|
|
410
402
|
const COMMIT_PROMPT_WITH_TITLE =
|
|
411
|
-
|
|
403
|
+
'Review the code changes introduced by commit {sha} ("{title}"). Provide prioritized, actionable findings.';
|
|
412
404
|
|
|
413
|
-
const COMMIT_PROMPT =
|
|
405
|
+
const COMMIT_PROMPT =
|
|
406
|
+
"Review the code changes introduced by commit {sha}. Provide prioritized, actionable findings.";
|
|
414
407
|
|
|
415
408
|
const PULL_REQUEST_PROMPT =
|
|
416
|
-
|
|
409
|
+
"Review pull request #{prNumber} (\"{title}\") against the base branch '{baseBranch}'. The merge base commit for this comparison is {mergeBaseSha}. Run `git diff {mergeBaseSha}` to inspect the changes that would be merged. Provide prioritized, actionable findings.";
|
|
417
410
|
|
|
418
411
|
const PULL_REQUEST_PROMPT_FALLBACK =
|
|
419
|
-
|
|
412
|
+
"Review pull request #{prNumber} (\"{title}\") against the base branch '{baseBranch}'. Start by finding the merge base between the current branch and {baseBranch} (e.g., `git merge-base HEAD {baseBranch}`), then run `git diff` against that SHA to see the changes that would be merged. Provide prioritized, actionable findings.";
|
|
420
413
|
|
|
421
414
|
const FOLDER_REVIEW_PROMPT =
|
|
422
|
-
|
|
415
|
+
"Review the code in the following paths: {paths}. This is a snapshot review (not a diff). Read the files directly in these paths and provide prioritized, actionable findings.";
|
|
416
|
+
|
|
417
|
+
// Shared by the review rubric and the /end-review summary so the callout list
|
|
418
|
+
// and the "informational, not a fix item" rule cannot drift.
|
|
419
|
+
const HUMAN_REVIEWER_CALLOUTS = `## Human Reviewer Callouts (Non-Blocking)
|
|
420
|
+
|
|
421
|
+
Include only applicable callouts (no yes/no lines):
|
|
422
|
+
|
|
423
|
+
- **This change adds a database migration:** <files/details>
|
|
424
|
+
- **This change introduces a new dependency:** <package(s)/details>
|
|
425
|
+
- **This change changes a dependency (or the lockfile):** <files/package(s)/details>
|
|
426
|
+
- **This change modifies auth/permission behavior:** <what changed and where>
|
|
427
|
+
- **This change introduces backwards-incompatible public schema/API/contract changes:** <what changed and where>
|
|
428
|
+
- **This change includes irreversible or destructive operations:** <operation and scope>
|
|
429
|
+
|
|
430
|
+
These are informational callouts for the human reviewer, not fix items.
|
|
431
|
+
If none apply, write "- (none)".`;
|
|
423
432
|
|
|
424
433
|
// The detailed review rubric (adapted from Codex's review_prompt.md)
|
|
425
434
|
const REVIEW_RUBRIC = `# Review Guidelines
|
|
@@ -485,24 +494,12 @@ When reviewing added or modified error handling, default to fail-fast behavior.
|
|
|
485
494
|
|
|
486
495
|
After findings/verdict, you MUST append this final section:
|
|
487
496
|
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
Include only applicable callouts (no yes/no lines):
|
|
491
|
-
|
|
492
|
-
- **This change adds a database migration:** <files/details>
|
|
493
|
-
- **This change introduces a new dependency:** <package(s)/details>
|
|
494
|
-
- **This change changes a dependency (or the lockfile):** <files/package(s)/details>
|
|
495
|
-
- **This change modifies auth/permission behavior:** <what changed and where>
|
|
496
|
-
- **This change introduces backwards-incompatible public schema/API/contract changes:** <what changed and where>
|
|
497
|
-
- **This change includes irreversible or destructive operations:** <operation and scope>
|
|
497
|
+
${HUMAN_REVIEWER_CALLOUTS}
|
|
498
498
|
|
|
499
499
|
Rules for this section:
|
|
500
|
-
1.
|
|
501
|
-
2.
|
|
502
|
-
3.
|
|
503
|
-
4. Only include callouts that apply to the reviewed change.
|
|
504
|
-
5. Keep each emitted callout bold exactly as written.
|
|
505
|
-
6. If none apply, write "- (none)".
|
|
500
|
+
1. Do not include them in Findings unless there is an independent defect.
|
|
501
|
+
2. These callouts alone must not change the verdict.
|
|
502
|
+
3. Keep each emitted callout bold exactly as written.
|
|
506
503
|
|
|
507
504
|
## Priority levels
|
|
508
505
|
|
|
@@ -526,509 +523,545 @@ Provide your findings in a clear, structured format:
|
|
|
526
523
|
Output all findings the author would fix if they knew about them. If there are no qualifying findings, explicitly state the code looks good. Don't stop at the first finding - list every qualifying issue. Then append the required non-blocking callouts section.`;
|
|
527
524
|
|
|
528
525
|
async function loadProjectReviewGuidelines(cwd: string): Promise<string | null> {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
526
|
+
let currentDir = path.resolve(cwd);
|
|
527
|
+
|
|
528
|
+
while (true) {
|
|
529
|
+
const piDir = path.join(currentDir, CONFIG_DIR_NAME);
|
|
530
|
+
const guidelinesPath = path.join(currentDir, "REVIEW_GUIDELINES.md");
|
|
531
|
+
|
|
532
|
+
const piStats = await fs.stat(piDir).catch(() => null);
|
|
533
|
+
if (piStats?.isDirectory()) {
|
|
534
|
+
const guidelineStats = await fs.stat(guidelinesPath).catch(() => null);
|
|
535
|
+
if (guidelineStats?.isFile()) {
|
|
536
|
+
try {
|
|
537
|
+
const content = await fs.readFile(guidelinesPath, "utf8");
|
|
538
|
+
const trimmed = content.trim();
|
|
539
|
+
return trimmed ? trimmed : null;
|
|
540
|
+
} catch {
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const parentDir = path.dirname(currentDir);
|
|
548
|
+
if (parentDir === currentDir) {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
currentDir = parentDir;
|
|
552
|
+
}
|
|
556
553
|
}
|
|
557
554
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
)
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
return null;
|
|
587
|
-
} catch {
|
|
588
|
-
return null;
|
|
589
|
-
}
|
|
555
|
+
async function getMergeBase(pi: ExtensionAPI, branch: string): Promise<string | null> {
|
|
556
|
+
try {
|
|
557
|
+
const { stdout: upstream, code: upstreamCode } = await pi.exec("git", [
|
|
558
|
+
"rev-parse",
|
|
559
|
+
"--abbrev-ref",
|
|
560
|
+
`${branch}@{upstream}`,
|
|
561
|
+
]);
|
|
562
|
+
|
|
563
|
+
if (upstreamCode === 0 && upstream.trim()) {
|
|
564
|
+
const { stdout: mergeBase, code } = await pi.exec("git", [
|
|
565
|
+
"merge-base",
|
|
566
|
+
"HEAD",
|
|
567
|
+
upstream.trim(),
|
|
568
|
+
]);
|
|
569
|
+
if (code === 0 && mergeBase.trim()) {
|
|
570
|
+
return mergeBase.trim();
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const { stdout: mergeBase, code } = await pi.exec("git", ["merge-base", "HEAD", branch]);
|
|
575
|
+
if (code === 0 && mergeBase.trim()) {
|
|
576
|
+
return mergeBase.trim();
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return null;
|
|
580
|
+
} catch {
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
590
583
|
}
|
|
591
584
|
|
|
592
|
-
/**
|
|
593
|
-
* Get list of local branches
|
|
594
|
-
*/
|
|
595
585
|
async function getLocalBranches(pi: ExtensionAPI): Promise<string[]> {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
586
|
+
const { stdout, code } = await pi.exec("git", ["branch", "--format=%(refname:short)"]);
|
|
587
|
+
if (code !== 0) return [];
|
|
588
|
+
return stdout
|
|
589
|
+
.trim()
|
|
590
|
+
.split("\n")
|
|
591
|
+
.filter((b) => b.trim());
|
|
602
592
|
}
|
|
603
593
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
594
|
+
async function getRecentCommits(
|
|
595
|
+
pi: ExtensionAPI,
|
|
596
|
+
limit: number = 10,
|
|
597
|
+
): Promise<Array<{ sha: string; title: string }>> {
|
|
598
|
+
const { stdout, code } = await pi.exec("git", ["log", `--oneline`, `-n`, `${limit}`]);
|
|
599
|
+
if (code !== 0) return [];
|
|
600
|
+
|
|
601
|
+
return stdout
|
|
602
|
+
.trim()
|
|
603
|
+
.split("\n")
|
|
604
|
+
.filter((line) => line.trim())
|
|
605
|
+
.map((line) => {
|
|
606
|
+
const [sha, ...rest] = line.trim().split(" ");
|
|
607
|
+
return { sha, title: rest.join(" ") };
|
|
608
|
+
});
|
|
619
609
|
}
|
|
620
610
|
|
|
621
|
-
/**
|
|
622
|
-
* Check if there are uncommitted changes (staged, unstaged, or untracked)
|
|
623
|
-
*/
|
|
624
611
|
async function hasUncommittedChanges(pi: ExtensionAPI): Promise<boolean> {
|
|
625
|
-
|
|
626
|
-
|
|
612
|
+
const { stdout, code } = await pi.exec("git", ["status", "--porcelain"]);
|
|
613
|
+
return code === 0 && stdout.trim().length > 0;
|
|
627
614
|
}
|
|
628
615
|
|
|
629
|
-
|
|
630
|
-
* Check if there are changes that would prevent switching branches
|
|
631
|
-
* (staged or unstaged changes to tracked files - untracked files are fine)
|
|
632
|
-
*/
|
|
616
|
+
// Tracked staged/unstaged changes block branch switches. Untracked files (??) do not.
|
|
633
617
|
async function hasPendingChanges(pi: ExtensionAPI): Promise<boolean> {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
618
|
+
const { stdout, code } = await pi.exec("git", ["status", "--porcelain"]);
|
|
619
|
+
if (code !== 0) return false;
|
|
620
|
+
|
|
621
|
+
const lines = stdout
|
|
622
|
+
.trim()
|
|
623
|
+
.split("\n")
|
|
624
|
+
.filter((line) => line.trim());
|
|
625
|
+
const trackedChanges = lines.filter((line) => !line.startsWith("??"));
|
|
626
|
+
return trackedChanges.length > 0;
|
|
642
627
|
}
|
|
643
628
|
|
|
644
|
-
/**
|
|
645
|
-
* Parse a PR reference (URL or number) and return the PR number
|
|
646
|
-
*/
|
|
647
629
|
function parsePrReference(ref: string): number | null {
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
630
|
+
const trimmed = ref.trim();
|
|
631
|
+
|
|
632
|
+
if (/^\d+$/.test(trimmed)) {
|
|
633
|
+
const num = Number(trimmed);
|
|
634
|
+
if (Number.isSafeInteger(num) && num > 0) {
|
|
635
|
+
return num;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Try to extract from GitHub URL
|
|
640
|
+
// Formats: https://github.com/owner/repo/pull/123
|
|
641
|
+
// github.com/owner/repo/pull/123
|
|
642
|
+
const urlMatch = trimmed.match(
|
|
643
|
+
/^(?:https?:\/\/)?(?:www\.)?github\.com\/[^/]+\/[^/]+\/pull\/(\d+)(?:[/?#]|$)/i,
|
|
644
|
+
);
|
|
645
|
+
if (urlMatch) {
|
|
646
|
+
const num = Number(urlMatch[1]);
|
|
647
|
+
return Number.isSafeInteger(num) && num > 0 ? num : null;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return null;
|
|
667
651
|
}
|
|
668
652
|
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
653
|
+
async function getPrInfo(
|
|
654
|
+
pi: ExtensionAPI,
|
|
655
|
+
prNumber: number,
|
|
656
|
+
): Promise<{ baseBranch: string; title: string; headBranch: string } | null> {
|
|
657
|
+
const { stdout, code } = await pi.exec("gh", [
|
|
658
|
+
"pr",
|
|
659
|
+
"view",
|
|
660
|
+
String(prNumber),
|
|
661
|
+
"--json",
|
|
662
|
+
"baseRefName,title,headRefName",
|
|
663
|
+
]);
|
|
664
|
+
|
|
665
|
+
if (code !== 0) return null;
|
|
666
|
+
|
|
667
|
+
try {
|
|
668
|
+
const data = JSON.parse(stdout);
|
|
669
|
+
return {
|
|
670
|
+
baseBranch: data.baseRefName,
|
|
671
|
+
title: data.title,
|
|
672
|
+
headBranch: data.headRefName,
|
|
673
|
+
};
|
|
674
|
+
} catch {
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
690
677
|
}
|
|
691
678
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
679
|
+
async function checkoutPullRequest(
|
|
680
|
+
pi: ExtensionAPI,
|
|
681
|
+
ctx: ExtensionContext,
|
|
682
|
+
ref: string,
|
|
683
|
+
): Promise<ReviewTarget | null> {
|
|
684
|
+
const prNumber = parsePrReference(ref);
|
|
685
|
+
if (!prNumber) {
|
|
686
|
+
ctx.ui.notify("Invalid PR reference. Enter a number or GitHub PR URL.", "error");
|
|
687
|
+
return null;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
ctx.ui.notify(`Fetching PR #${prNumber} info...`, "info");
|
|
691
|
+
const prInfo = await getPrInfo(pi, prNumber);
|
|
692
|
+
if (!prInfo) {
|
|
693
|
+
ctx.ui.notify(
|
|
694
|
+
`Could not find PR #${prNumber}. Make sure gh is authenticated and the PR exists.`,
|
|
695
|
+
"error",
|
|
696
|
+
);
|
|
697
|
+
return null;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (await hasPendingChanges(pi)) {
|
|
701
|
+
ctx.ui.notify(
|
|
702
|
+
"Cannot checkout PR: you have uncommitted changes. Please commit or stash them first.",
|
|
703
|
+
"error",
|
|
704
|
+
);
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
ctx.ui.notify(`Checking out PR #${prNumber}...`, "info");
|
|
709
|
+
const { stdout, stderr, code } = await pi.exec("gh", ["pr", "checkout", String(prNumber)]);
|
|
710
|
+
if (code !== 0) {
|
|
711
|
+
ctx.ui.notify(`Failed to checkout PR: ${stderr || stdout || "Failed to checkout PR"}`, "error");
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
ctx.ui.notify(`Checked out PR #${prNumber} (${prInfo.headBranch})`, "info");
|
|
716
|
+
return {
|
|
717
|
+
type: "pullRequest",
|
|
718
|
+
prNumber,
|
|
719
|
+
baseBranch: prInfo.baseBranch,
|
|
720
|
+
title: prInfo.title,
|
|
721
|
+
};
|
|
703
722
|
}
|
|
704
723
|
|
|
705
|
-
/**
|
|
706
|
-
* Get the current branch name
|
|
707
|
-
*/
|
|
708
724
|
async function getCurrentBranch(pi: ExtensionAPI): Promise<string | null> {
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
725
|
+
const { stdout, code } = await pi.exec("git", ["branch", "--show-current"]);
|
|
726
|
+
if (code === 0 && stdout.trim()) {
|
|
727
|
+
return stdout.trim();
|
|
728
|
+
}
|
|
729
|
+
return null;
|
|
714
730
|
}
|
|
715
731
|
|
|
716
|
-
/**
|
|
717
|
-
* Get the default branch (main or master)
|
|
718
|
-
*/
|
|
719
732
|
async function getDefaultBranch(pi: ExtensionAPI): Promise<string> {
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
733
|
+
const { stdout, code } = await pi.exec("git", [
|
|
734
|
+
"symbolic-ref",
|
|
735
|
+
"refs/remotes/origin/HEAD",
|
|
736
|
+
"--short",
|
|
737
|
+
]);
|
|
738
|
+
if (code === 0 && stdout.trim()) {
|
|
739
|
+
return stdout.trim().replace("origin/", "");
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const branches = await getLocalBranches(pi);
|
|
743
|
+
if (branches.includes("main")) return "main";
|
|
744
|
+
if (branches.includes("master")) return "master";
|
|
745
|
+
|
|
746
|
+
return "main";
|
|
732
747
|
}
|
|
733
748
|
|
|
734
|
-
/**
|
|
735
|
-
* Build the review prompt based on target
|
|
736
|
-
*/
|
|
737
749
|
async function buildReviewPrompt(
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
750
|
+
pi: ExtensionAPI,
|
|
751
|
+
target: ReviewTarget,
|
|
752
|
+
options?: { includeLocalChanges?: boolean },
|
|
741
753
|
): Promise<string> {
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
754
|
+
const includeLocalChanges = options?.includeLocalChanges === true;
|
|
755
|
+
|
|
756
|
+
switch (target.type) {
|
|
757
|
+
case "uncommitted":
|
|
758
|
+
return UNCOMMITTED_PROMPT;
|
|
759
|
+
|
|
760
|
+
case "baseBranch": {
|
|
761
|
+
const mergeBase = await getMergeBase(pi, target.branch);
|
|
762
|
+
const basePrompt = mergeBase
|
|
763
|
+
? BASE_BRANCH_PROMPT_WITH_MERGE_BASE.replace(/{baseBranch}/g, target.branch).replace(
|
|
764
|
+
/{mergeBaseSha}/g,
|
|
765
|
+
mergeBase,
|
|
766
|
+
)
|
|
767
|
+
: BASE_BRANCH_PROMPT_FALLBACK.replace(/{branch}/g, target.branch);
|
|
768
|
+
return includeLocalChanges
|
|
769
|
+
? `${basePrompt} ${LOCAL_CHANGES_REVIEW_INSTRUCTIONS}`
|
|
770
|
+
: basePrompt;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
case "commit":
|
|
774
|
+
if (target.title) {
|
|
775
|
+
return COMMIT_PROMPT_WITH_TITLE.replace("{sha}", target.sha).replace(
|
|
776
|
+
"{title}",
|
|
777
|
+
target.title,
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
return COMMIT_PROMPT.replace("{sha}", target.sha);
|
|
781
|
+
|
|
782
|
+
case "pullRequest": {
|
|
783
|
+
const mergeBase = await getMergeBase(pi, target.baseBranch);
|
|
784
|
+
const basePrompt = mergeBase
|
|
785
|
+
? PULL_REQUEST_PROMPT.replace(/{prNumber}/g, String(target.prNumber))
|
|
786
|
+
.replace(/{title}/g, target.title)
|
|
787
|
+
.replace(/{baseBranch}/g, target.baseBranch)
|
|
788
|
+
.replace(/{mergeBaseSha}/g, mergeBase)
|
|
789
|
+
: PULL_REQUEST_PROMPT_FALLBACK.replace(/{prNumber}/g, String(target.prNumber))
|
|
790
|
+
.replace(/{title}/g, target.title)
|
|
791
|
+
.replace(/{baseBranch}/g, target.baseBranch);
|
|
792
|
+
return includeLocalChanges
|
|
793
|
+
? `${basePrompt} ${LOCAL_CHANGES_REVIEW_INSTRUCTIONS}`
|
|
794
|
+
: basePrompt;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
case "folder":
|
|
798
|
+
return FOLDER_REVIEW_PROMPT.replace("{paths}", target.paths.join(", "));
|
|
799
|
+
}
|
|
780
800
|
}
|
|
781
801
|
|
|
782
|
-
/**
|
|
783
|
-
* Get user-facing hint for the review target
|
|
784
|
-
*/
|
|
785
802
|
type ComposeReviewPromptOptions = {
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
803
|
+
customInstructions?: string;
|
|
804
|
+
extraInstruction?: string;
|
|
805
|
+
projectGuidelines?: string | null;
|
|
789
806
|
};
|
|
790
807
|
|
|
791
808
|
function composeReviewPrompt(prompt: string, options: ComposeReviewPromptOptions = {}): string {
|
|
792
|
-
|
|
809
|
+
let fullPrompt = `${REVIEW_RUBRIC}\n\n---\n\nPlease perform a code review with the following focus:\n\n${prompt}`;
|
|
793
810
|
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
811
|
+
if (options.customInstructions?.trim()) {
|
|
812
|
+
fullPrompt += `\n\nShared custom review instructions (applies to all reviews):\n\n${options.customInstructions.trim()}`;
|
|
813
|
+
}
|
|
797
814
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
815
|
+
if (options.extraInstruction?.trim()) {
|
|
816
|
+
fullPrompt += `\n\nAdditional user-provided review instruction:\n\n${options.extraInstruction.trim()}`;
|
|
817
|
+
}
|
|
801
818
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
819
|
+
if (options.projectGuidelines?.trim()) {
|
|
820
|
+
fullPrompt += `\n\nThis project has additional instructions for code reviews:\n\n${options.projectGuidelines.trim()}`;
|
|
821
|
+
}
|
|
805
822
|
|
|
806
|
-
|
|
823
|
+
return fullPrompt;
|
|
807
824
|
}
|
|
808
825
|
|
|
809
826
|
function getUserFacingHint(target: ReviewTarget): string {
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
827
|
+
switch (target.type) {
|
|
828
|
+
case "uncommitted":
|
|
829
|
+
return "current changes";
|
|
830
|
+
case "baseBranch":
|
|
831
|
+
return `changes against '${target.branch}'`;
|
|
832
|
+
case "commit": {
|
|
833
|
+
const shortSha = target.sha.slice(0, 7);
|
|
834
|
+
return target.title ? `commit ${shortSha}: ${target.title}` : `commit ${shortSha}`;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
case "pullRequest": {
|
|
838
|
+
const shortTitle =
|
|
839
|
+
target.title.length > 30 ? target.title.slice(0, 27) + "..." : target.title;
|
|
840
|
+
return `PR #${target.prNumber}: ${shortTitle}`;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
case "folder": {
|
|
844
|
+
const joined = target.paths.join(", ");
|
|
845
|
+
return joined.length > 40 ? `folders: ${joined.slice(0, 37)}...` : `folders: ${joined}`;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
830
848
|
}
|
|
831
849
|
|
|
832
850
|
type AssistantSnapshot = {
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
851
|
+
id: string;
|
|
852
|
+
text: string;
|
|
853
|
+
stopReason?: string;
|
|
836
854
|
};
|
|
837
855
|
|
|
838
856
|
function extractAssistantTextContent(content: unknown): string {
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
857
|
+
if (typeof content === "string") {
|
|
858
|
+
return content.trim();
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
if (!Array.isArray(content)) {
|
|
862
|
+
return "";
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const textParts = content
|
|
866
|
+
.filter((part): part is { type: "text"; text: string } =>
|
|
867
|
+
Boolean(
|
|
868
|
+
part &&
|
|
869
|
+
typeof part === "object" &&
|
|
870
|
+
"type" in part &&
|
|
871
|
+
part.type === "text" &&
|
|
872
|
+
"text" in part,
|
|
873
|
+
),
|
|
874
|
+
)
|
|
875
|
+
.map((part) => part.text);
|
|
876
|
+
return textParts.join("\n").trim();
|
|
854
877
|
}
|
|
855
878
|
|
|
856
879
|
function getLastAssistantSnapshot(ctx: ExtensionContext): AssistantSnapshot | null {
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
880
|
+
const entries = ctx.sessionManager.getBranch();
|
|
881
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
882
|
+
const entry = entries[i];
|
|
883
|
+
if (entry.type !== "message" || entry.message.role !== "assistant") {
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
const assistantMessage = entry.message as { content?: unknown; stopReason?: string };
|
|
888
|
+
return {
|
|
889
|
+
id: entry.id,
|
|
890
|
+
text: extractAssistantTextContent(assistantMessage.content),
|
|
891
|
+
stopReason: assistantMessage.stopReason,
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
return null;
|
|
873
896
|
}
|
|
874
897
|
|
|
875
898
|
function sleep(ms: number): Promise<void> {
|
|
876
|
-
|
|
899
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
877
900
|
}
|
|
878
901
|
|
|
879
902
|
function tokenizeArgs(value: string): string[] {
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
903
|
+
const tokens: string[] = [];
|
|
904
|
+
let current = "";
|
|
905
|
+
let quote: '"' | "'" | null = null;
|
|
906
|
+
|
|
907
|
+
for (let i = 0; i < value.length; i++) {
|
|
908
|
+
const char = value[i];
|
|
909
|
+
|
|
910
|
+
if (quote) {
|
|
911
|
+
if (char === "\\" && i + 1 < value.length) {
|
|
912
|
+
current += value[i + 1];
|
|
913
|
+
i += 1;
|
|
914
|
+
continue;
|
|
915
|
+
}
|
|
916
|
+
if (char === quote) {
|
|
917
|
+
quote = null;
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
current += char;
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
if (char === '"' || char === "'") {
|
|
925
|
+
quote = char;
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
if (/\s/.test(char)) {
|
|
930
|
+
if (current.length > 0) {
|
|
931
|
+
tokens.push(current);
|
|
932
|
+
current = "";
|
|
933
|
+
}
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
current += char;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
if (current.length > 0) {
|
|
941
|
+
tokens.push(current);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
return tokens;
|
|
922
945
|
}
|
|
923
946
|
|
|
924
947
|
function parseReviewPaths(value: string): string[] {
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
948
|
+
return tokenizeArgs(value)
|
|
949
|
+
.map((item) => item.trim())
|
|
950
|
+
.filter((item) => item.length > 0);
|
|
928
951
|
}
|
|
929
952
|
|
|
930
953
|
type ParsedReviewArgs = {
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
954
|
+
target: ReviewTarget | { type: "pr"; ref: string } | null;
|
|
955
|
+
extraInstruction?: string;
|
|
956
|
+
error?: string;
|
|
934
957
|
};
|
|
935
958
|
|
|
936
959
|
function parseArgs(args: string | undefined): ParsedReviewArgs {
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
960
|
+
if (!args?.trim()) return { target: null };
|
|
961
|
+
|
|
962
|
+
const rawParts = tokenizeArgs(args.trim());
|
|
963
|
+
const parts: string[] = [];
|
|
964
|
+
let extraInstruction: string | undefined;
|
|
965
|
+
|
|
966
|
+
for (let i = 0; i < rawParts.length; i++) {
|
|
967
|
+
const part = rawParts[i];
|
|
968
|
+
if (part === "--extra") {
|
|
969
|
+
const next = rawParts[i + 1];
|
|
970
|
+
if (!next) {
|
|
971
|
+
return { target: null, error: "Missing value for --extra" };
|
|
972
|
+
}
|
|
973
|
+
extraInstruction = next;
|
|
974
|
+
i += 1;
|
|
975
|
+
continue;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
if (part.startsWith("--extra=")) {
|
|
979
|
+
extraInstruction = part.slice("--extra=".length);
|
|
980
|
+
continue;
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
parts.push(part);
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
if (parts.length === 0) {
|
|
987
|
+
return { target: null, extraInstruction };
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
const subcommand = parts[0]?.toLowerCase();
|
|
991
|
+
|
|
992
|
+
switch (subcommand) {
|
|
993
|
+
case "uncommitted":
|
|
994
|
+
return { target: { type: "uncommitted" }, extraInstruction };
|
|
995
|
+
|
|
996
|
+
case "branch": {
|
|
997
|
+
const branch = parts[1];
|
|
998
|
+
if (!branch) return { target: null, extraInstruction };
|
|
999
|
+
return { target: { type: "baseBranch", branch }, extraInstruction };
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
case "commit": {
|
|
1003
|
+
const sha = parts[1];
|
|
1004
|
+
if (!sha) return { target: null, extraInstruction };
|
|
1005
|
+
const title = parts.slice(2).join(" ") || undefined;
|
|
1006
|
+
return { target: { type: "commit", sha, title }, extraInstruction };
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
case "folder": {
|
|
1010
|
+
const paths = parts
|
|
1011
|
+
.slice(1)
|
|
1012
|
+
.map((item) => item.trim())
|
|
1013
|
+
.filter((item) => item.length > 0);
|
|
1014
|
+
if (paths.length === 0) return { target: null, extraInstruction };
|
|
1015
|
+
return { target: { type: "folder", paths }, extraInstruction };
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
case "pr": {
|
|
1019
|
+
const ref = parts[1];
|
|
1020
|
+
if (!ref) return { target: null, extraInstruction };
|
|
1021
|
+
return { target: { type: "pr", ref }, extraInstruction };
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
default:
|
|
1025
|
+
return { target: null, extraInstruction };
|
|
1026
|
+
}
|
|
1001
1027
|
}
|
|
1002
1028
|
|
|
1003
|
-
async function waitForLoopTurnToStart(
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1029
|
+
async function waitForLoopTurnToStart(
|
|
1030
|
+
ctx: ExtensionContext,
|
|
1031
|
+
previousAssistantId?: string,
|
|
1032
|
+
): Promise<boolean> {
|
|
1033
|
+
const deadline = Date.now() + REVIEW_LOOP_START_TIMEOUT_MS;
|
|
1034
|
+
|
|
1035
|
+
while (Date.now() < deadline) {
|
|
1036
|
+
const lastAssistantId = getLastAssistantSnapshot(ctx)?.id;
|
|
1037
|
+
if (
|
|
1038
|
+
!ctx.isIdle() ||
|
|
1039
|
+
ctx.hasPendingMessages() ||
|
|
1040
|
+
(lastAssistantId && lastAssistantId !== previousAssistantId)
|
|
1041
|
+
) {
|
|
1042
|
+
return true;
|
|
1043
|
+
}
|
|
1044
|
+
await sleep(REVIEW_LOOP_START_POLL_MS);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
return false;
|
|
1015
1048
|
}
|
|
1016
1049
|
|
|
1017
1050
|
// Review preset options for the selector (keep this order stable)
|
|
1018
1051
|
const REVIEW_PRESETS = [
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1052
|
+
{ value: "uncommitted", label: "Review uncommitted changes", description: "" },
|
|
1053
|
+
{ value: "baseBranch", label: "Review against a base branch", description: "(local)" },
|
|
1054
|
+
{ value: "commit", label: "Review a commit", description: "" },
|
|
1055
|
+
{ value: "pullRequest", label: "Review a pull request", description: "(GitHub PR)" },
|
|
1056
|
+
{ value: "folder", label: "Review a folder (or more)", description: "(snapshot, not diff)" },
|
|
1024
1057
|
] as const;
|
|
1025
1058
|
|
|
1026
1059
|
const TOGGLE_LOOP_FIXING_VALUE = "toggleLoopFixing" as const;
|
|
1027
1060
|
const TOGGLE_CUSTOM_INSTRUCTIONS_VALUE = "toggleCustomInstructions" as const;
|
|
1028
1061
|
type ReviewPresetValue =
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1062
|
+
| (typeof REVIEW_PRESETS)[number]["value"]
|
|
1063
|
+
| typeof TOGGLE_LOOP_FIXING_VALUE
|
|
1064
|
+
| typeof TOGGLE_CUSTOM_INSTRUCTIONS_VALUE;
|
|
1032
1065
|
|
|
1033
1066
|
const REVIEW_SUMMARY_PROMPT = `We are leaving a code-review branch and returning to the main coding branch.
|
|
1034
1067
|
Create a structured handoff that can be used immediately to implement fixes.
|
|
@@ -1058,899 +1091,724 @@ For EACH finding, include:
|
|
|
1058
1091
|
- Any constraints or preferences mentioned during review
|
|
1059
1092
|
- Or "(none)"
|
|
1060
1093
|
|
|
1061
|
-
|
|
1062
|
-
Include only applicable callouts (no yes/no lines):
|
|
1063
|
-
- **This change adds a database migration:** <files/details>
|
|
1064
|
-
- **This change introduces a new dependency:** <package(s)/details>
|
|
1065
|
-
- **This change changes a dependency (or the lockfile):** <files/package(s)/details>
|
|
1066
|
-
- **This change modifies auth/permission behavior:** <what changed and where>
|
|
1067
|
-
- **This change introduces backwards-incompatible public schema/API/contract changes:** <what changed and where>
|
|
1068
|
-
- **This change includes irreversible or destructive operations:** <operation and scope>
|
|
1069
|
-
|
|
1070
|
-
If none apply, write "- (none)".
|
|
1071
|
-
|
|
1072
|
-
These are informational callouts for humans and are not fix items by themselves.
|
|
1094
|
+
${HUMAN_REVIEWER_CALLOUTS}
|
|
1073
1095
|
|
|
1074
1096
|
Preserve exact file paths, function names, and error messages where available.`;
|
|
1075
1097
|
|
|
1076
1098
|
export const __test__ = {
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1099
|
+
applyReviewSettings,
|
|
1100
|
+
applyReviewState,
|
|
1101
|
+
buildReviewPrompt,
|
|
1102
|
+
composeReviewPrompt,
|
|
1103
|
+
getReviewRuntimeState,
|
|
1104
|
+
getReviewSettings,
|
|
1105
|
+
getReviewState,
|
|
1106
|
+
hasBlockingReviewFindings,
|
|
1107
|
+
hasNeedsAttentionVerdict,
|
|
1108
|
+
getUserFacingHint,
|
|
1109
|
+
parseArgs,
|
|
1110
|
+
parsePrReference,
|
|
1111
|
+
parseReviewPaths,
|
|
1112
|
+
HUMAN_REVIEWER_CALLOUTS,
|
|
1113
|
+
REVIEW_RUBRIC,
|
|
1114
|
+
REVIEW_SUMMARY_PROMPT,
|
|
1115
|
+
loadProjectReviewGuidelines,
|
|
1116
|
+
resetReviewRuntimeState,
|
|
1117
|
+
tokenizeArgs,
|
|
1094
1118
|
};
|
|
1095
1119
|
|
|
1096
1120
|
export default function reviewExtension(pi: ExtensionAPI) {
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
if (finalized !== "ok") {
|
|
1789
|
-
return;
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1792
|
-
ctx.ui.notify("Loop fixing complete: no blocking findings remain.", "info");
|
|
1793
|
-
return;
|
|
1794
|
-
}
|
|
1795
|
-
|
|
1796
|
-
ctx.ui.notify(`Loop fixing pass ${pass}: found blocking findings, returning to fix them...`, "info");
|
|
1797
|
-
|
|
1798
|
-
const fixBaselineAssistantId = getLastAssistantSnapshot(ctx)?.id;
|
|
1799
|
-
const sentFixPrompt = await executeEndReviewAction(ctx, "returnAndFix", {
|
|
1800
|
-
showSummaryLoader: true,
|
|
1801
|
-
notifySuccess: false,
|
|
1802
|
-
});
|
|
1803
|
-
if (sentFixPrompt !== "ok") {
|
|
1804
|
-
return;
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
const fixTurnStarted = await waitForLoopTurnToStart(ctx, fixBaselineAssistantId);
|
|
1808
|
-
if (!fixTurnStarted) {
|
|
1809
|
-
ctx.ui.notify("Loop fixing stopped: fix pass did not start in time.", "error");
|
|
1810
|
-
return;
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
await ctx.waitForIdle();
|
|
1814
|
-
|
|
1815
|
-
const fixSnapshot = getLastAssistantSnapshot(ctx);
|
|
1816
|
-
if (!fixSnapshot || fixSnapshot.id === fixBaselineAssistantId) {
|
|
1817
|
-
ctx.ui.notify("Loop fixing stopped: could not read the fix pass result.", "warning");
|
|
1818
|
-
return;
|
|
1819
|
-
}
|
|
1820
|
-
if (fixSnapshot.stopReason === "aborted") {
|
|
1821
|
-
ctx.ui.notify("Loop fixing stopped: fix pass was aborted.", "warning");
|
|
1822
|
-
return;
|
|
1823
|
-
}
|
|
1824
|
-
if (fixSnapshot.stopReason === "error") {
|
|
1825
|
-
ctx.ui.notify("Loop fixing stopped: fix pass failed with an error.", "error");
|
|
1826
|
-
return;
|
|
1827
|
-
}
|
|
1828
|
-
if (fixSnapshot.stopReason === "length") {
|
|
1829
|
-
ctx.ui.notify("Loop fixing stopped: fix pass output was truncated (stopReason=length).", "warning");
|
|
1830
|
-
return;
|
|
1831
|
-
}
|
|
1832
|
-
}
|
|
1833
|
-
|
|
1834
|
-
ctx.ui.notify(
|
|
1835
|
-
`Loop fixing stopped after ${REVIEW_LOOP_MAX_ITERATIONS} passes (safety limit reached).`,
|
|
1836
|
-
"warning",
|
|
1837
|
-
);
|
|
1838
|
-
} finally {
|
|
1839
|
-
reviewLoopInProgress = false;
|
|
1840
|
-
setReviewWidget(ctx, Boolean(reviewOriginId));
|
|
1841
|
-
}
|
|
1842
|
-
}
|
|
1843
|
-
|
|
1844
|
-
// Register the /review command
|
|
1845
|
-
pi.registerCommand("review", {
|
|
1846
|
-
description: "Review code changes (PR, uncommitted, branch, commit, or folder)",
|
|
1847
|
-
handler: async (args, ctx) => {
|
|
1848
|
-
if (ctx.mode !== "tui") {
|
|
1849
|
-
ctx.ui.notify("Review requires interactive mode", "error");
|
|
1850
|
-
return;
|
|
1851
|
-
}
|
|
1852
|
-
|
|
1853
|
-
if (reviewLoopInProgress) {
|
|
1854
|
-
ctx.ui.notify("Loop fixing review is already running.", "warning");
|
|
1855
|
-
return;
|
|
1856
|
-
}
|
|
1857
|
-
|
|
1858
|
-
// Check if we're already in a review
|
|
1859
|
-
if (reviewOriginId) {
|
|
1860
|
-
ctx.ui.notify("Already in a review. Use /end-review to finish first.", "warning");
|
|
1861
|
-
return;
|
|
1862
|
-
}
|
|
1863
|
-
|
|
1864
|
-
// Check if we're in a git repository
|
|
1865
|
-
const { code } = await pi.exec("git", ["rev-parse", "--git-dir"]);
|
|
1866
|
-
if (code !== 0) {
|
|
1867
|
-
ctx.ui.notify("Not a git repository", "error");
|
|
1868
|
-
return;
|
|
1869
|
-
}
|
|
1870
|
-
|
|
1871
|
-
// Try to parse direct arguments
|
|
1872
|
-
let target: ReviewTarget | null = null;
|
|
1873
|
-
let fromSelector = false;
|
|
1874
|
-
let extraInstruction: string | undefined;
|
|
1875
|
-
const parsed = parseArgs(args);
|
|
1876
|
-
if (parsed.error) {
|
|
1877
|
-
ctx.ui.notify(parsed.error, "error");
|
|
1878
|
-
return;
|
|
1879
|
-
}
|
|
1880
|
-
extraInstruction = parsed.extraInstruction?.trim() || undefined;
|
|
1881
|
-
|
|
1882
|
-
if (parsed.target) {
|
|
1883
|
-
if (parsed.target.type === "pr") {
|
|
1884
|
-
// Handle PR checkout (async operation)
|
|
1885
|
-
target = await handlePrCheckout(ctx, parsed.target.ref);
|
|
1886
|
-
if (!target) {
|
|
1887
|
-
ctx.ui.notify("PR review failed. Returning to review menu.", "warning");
|
|
1888
|
-
}
|
|
1889
|
-
} else {
|
|
1890
|
-
target = parsed.target;
|
|
1891
|
-
}
|
|
1892
|
-
}
|
|
1893
|
-
|
|
1894
|
-
// If no args or invalid args, show selector
|
|
1895
|
-
if (!target) {
|
|
1896
|
-
fromSelector = true;
|
|
1897
|
-
}
|
|
1898
|
-
|
|
1899
|
-
while (true) {
|
|
1900
|
-
if (!target && fromSelector) {
|
|
1901
|
-
target = await showReviewSelector(ctx);
|
|
1902
|
-
}
|
|
1903
|
-
|
|
1904
|
-
if (!target) {
|
|
1905
|
-
ctx.ui.notify("Review cancelled", "info");
|
|
1906
|
-
return;
|
|
1907
|
-
}
|
|
1908
|
-
|
|
1909
|
-
if (reviewLoopFixingEnabled && !isLoopCompatibleTarget(target)) {
|
|
1910
|
-
ctx.ui.notify("Loop mode does not work with commit review.", "error");
|
|
1911
|
-
if (fromSelector) {
|
|
1912
|
-
target = null;
|
|
1913
|
-
continue;
|
|
1914
|
-
}
|
|
1915
|
-
return;
|
|
1916
|
-
}
|
|
1917
|
-
|
|
1918
|
-
if (reviewLoopFixingEnabled) {
|
|
1919
|
-
await runLoopFixingReview(ctx, target, extraInstruction);
|
|
1920
|
-
return;
|
|
1921
|
-
}
|
|
1922
|
-
|
|
1923
|
-
// Determine if we should use fresh session mode
|
|
1924
|
-
// Check if this is a new session (no messages yet)
|
|
1925
|
-
const entries = ctx.sessionManager.getEntries();
|
|
1926
|
-
const messageCount = entries.filter((e) => e.type === "message").length;
|
|
1927
|
-
|
|
1928
|
-
// In an empty session, default to fresh review mode so /end-review works consistently.
|
|
1929
|
-
let useFreshSession = messageCount === 0;
|
|
1930
|
-
|
|
1931
|
-
if (messageCount > 0) {
|
|
1932
|
-
// Existing session - ask user which mode they want
|
|
1933
|
-
const choice = await ctx.ui.select("Start review in:", ["Empty branch", "Current session"]);
|
|
1934
|
-
|
|
1935
|
-
if (choice === undefined) {
|
|
1936
|
-
if (fromSelector) {
|
|
1937
|
-
target = null;
|
|
1938
|
-
continue;
|
|
1939
|
-
}
|
|
1940
|
-
ctx.ui.notify("Review cancelled", "info");
|
|
1941
|
-
return;
|
|
1942
|
-
}
|
|
1943
|
-
|
|
1944
|
-
useFreshSession = choice === "Empty branch";
|
|
1945
|
-
}
|
|
1946
|
-
|
|
1947
|
-
await executeReview(ctx, target, useFreshSession, { extraInstruction });
|
|
1948
|
-
return;
|
|
1949
|
-
}
|
|
1950
|
-
},
|
|
1951
|
-
});
|
|
1952
|
-
|
|
1953
|
-
const REVIEW_FIX_FINDINGS_PROMPT = `Use the latest review summary in this session and implement the review findings now.
|
|
1121
|
+
function persistReviewSettings() {
|
|
1122
|
+
pi.appendEntry(REVIEW_SETTINGS_TYPE, {
|
|
1123
|
+
loopFixingEnabled: reviewLoopFixingEnabled,
|
|
1124
|
+
customInstructions: reviewCustomInstructions,
|
|
1125
|
+
});
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function setReviewLoopFixingEnabled(enabled: boolean) {
|
|
1129
|
+
reviewLoopFixingEnabled = enabled;
|
|
1130
|
+
persistReviewSettings();
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
function setReviewCustomInstructions(instructions: string | undefined) {
|
|
1134
|
+
reviewCustomInstructions = instructions?.trim() || undefined;
|
|
1135
|
+
persistReviewSettings();
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function applyAllReviewState(ctx: ExtensionContext) {
|
|
1139
|
+
applyReviewSettings(ctx);
|
|
1140
|
+
applyReviewState(ctx);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
pi.on("session_start", (_event, ctx) => {
|
|
1144
|
+
applyAllReviewState(ctx);
|
|
1145
|
+
});
|
|
1146
|
+
|
|
1147
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
1148
|
+
applyAllReviewState(ctx);
|
|
1149
|
+
});
|
|
1150
|
+
|
|
1151
|
+
async function getSmartDefault(): Promise<"uncommitted" | "baseBranch" | "commit"> {
|
|
1152
|
+
if (await hasUncommittedChanges(pi)) {
|
|
1153
|
+
return "uncommitted";
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const currentBranch = await getCurrentBranch(pi);
|
|
1157
|
+
const defaultBranch = await getDefaultBranch(pi);
|
|
1158
|
+
if (currentBranch && currentBranch !== defaultBranch) {
|
|
1159
|
+
return "baseBranch";
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
return "commit";
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
async function showReviewSelector(ctx: ExtensionContext): Promise<ReviewTarget | null> {
|
|
1166
|
+
// Determine smart default (but keep the list order stable)
|
|
1167
|
+
const smartDefault = await getSmartDefault();
|
|
1168
|
+
const presetItems: SelectItem[] = REVIEW_PRESETS.map((preset) => ({
|
|
1169
|
+
value: preset.value,
|
|
1170
|
+
label: preset.label,
|
|
1171
|
+
description: preset.description,
|
|
1172
|
+
}));
|
|
1173
|
+
const smartDefaultIndex = presetItems.findIndex((item) => item.value === smartDefault);
|
|
1174
|
+
|
|
1175
|
+
while (true) {
|
|
1176
|
+
const customInstructionsLabel = reviewCustomInstructions
|
|
1177
|
+
? "Remove custom review instructions"
|
|
1178
|
+
: "Add custom review instructions";
|
|
1179
|
+
const customInstructionsDescription = reviewCustomInstructions
|
|
1180
|
+
? "(currently set)"
|
|
1181
|
+
: "(applies to all review modes)";
|
|
1182
|
+
const loopToggleLabel = reviewLoopFixingEnabled
|
|
1183
|
+
? "Disable Loop Fixing"
|
|
1184
|
+
: "Enable Loop Fixing";
|
|
1185
|
+
const loopToggleDescription = reviewLoopFixingEnabled ? "(currently on)" : "(currently off)";
|
|
1186
|
+
const items: SelectItem[] = [
|
|
1187
|
+
...presetItems,
|
|
1188
|
+
{
|
|
1189
|
+
value: TOGGLE_CUSTOM_INSTRUCTIONS_VALUE,
|
|
1190
|
+
label: customInstructionsLabel,
|
|
1191
|
+
description: customInstructionsDescription,
|
|
1192
|
+
},
|
|
1193
|
+
{
|
|
1194
|
+
value: TOGGLE_LOOP_FIXING_VALUE,
|
|
1195
|
+
label: loopToggleLabel,
|
|
1196
|
+
description: loopToggleDescription,
|
|
1197
|
+
},
|
|
1198
|
+
];
|
|
1199
|
+
|
|
1200
|
+
const result = await ctx.ui.custom<ReviewPresetValue | null>((tui, theme, _kb, done) => {
|
|
1201
|
+
const container = new Container();
|
|
1202
|
+
container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
|
|
1203
|
+
container.addChild(new Text(theme.fg("accent", theme.bold("Select a review preset"))));
|
|
1204
|
+
|
|
1205
|
+
const selectList = new SelectList(items, Math.min(items.length, 10), {
|
|
1206
|
+
selectedPrefix: (text) => theme.fg("accent", text),
|
|
1207
|
+
selectedText: (text) => theme.fg("accent", text),
|
|
1208
|
+
description: (text) => theme.fg("muted", text),
|
|
1209
|
+
scrollInfo: (text) => theme.fg("dim", text),
|
|
1210
|
+
noMatch: (text) => theme.fg("warning", text),
|
|
1211
|
+
});
|
|
1212
|
+
|
|
1213
|
+
// Preselect the smart default without reordering the list
|
|
1214
|
+
if (smartDefaultIndex >= 0) {
|
|
1215
|
+
selectList.setSelectedIndex(smartDefaultIndex);
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
selectList.onSelect = (item) => done(item.value as ReviewPresetValue);
|
|
1219
|
+
selectList.onCancel = () => done(null);
|
|
1220
|
+
|
|
1221
|
+
container.addChild(selectList);
|
|
1222
|
+
container.addChild(new Text(theme.fg("dim", "Press enter to confirm or esc to go back")));
|
|
1223
|
+
container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
|
|
1224
|
+
|
|
1225
|
+
return {
|
|
1226
|
+
render(width: number) {
|
|
1227
|
+
return container.render(width);
|
|
1228
|
+
},
|
|
1229
|
+
invalidate() {
|
|
1230
|
+
container.invalidate();
|
|
1231
|
+
},
|
|
1232
|
+
handleInput(data: string) {
|
|
1233
|
+
selectList.handleInput(data);
|
|
1234
|
+
tui.requestRender();
|
|
1235
|
+
},
|
|
1236
|
+
};
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
if (!result) return null;
|
|
1240
|
+
|
|
1241
|
+
if (result === TOGGLE_LOOP_FIXING_VALUE) {
|
|
1242
|
+
const nextEnabled = !reviewLoopFixingEnabled;
|
|
1243
|
+
setReviewLoopFixingEnabled(nextEnabled);
|
|
1244
|
+
ctx.ui.notify(nextEnabled ? "Loop fixing enabled" : "Loop fixing disabled", "info");
|
|
1245
|
+
continue;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
if (result === TOGGLE_CUSTOM_INSTRUCTIONS_VALUE) {
|
|
1249
|
+
if (reviewCustomInstructions) {
|
|
1250
|
+
setReviewCustomInstructions(undefined);
|
|
1251
|
+
ctx.ui.notify("Custom review instructions removed", "info");
|
|
1252
|
+
continue;
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
const customInstructions = await ctx.ui.editor(
|
|
1256
|
+
"Enter custom review instructions (applies to all review modes):",
|
|
1257
|
+
"",
|
|
1258
|
+
);
|
|
1259
|
+
|
|
1260
|
+
if (!customInstructions?.trim()) {
|
|
1261
|
+
ctx.ui.notify("Custom review instructions not changed", "info");
|
|
1262
|
+
continue;
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
setReviewCustomInstructions(customInstructions);
|
|
1266
|
+
ctx.ui.notify("Custom review instructions saved", "info");
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
switch (result) {
|
|
1271
|
+
case "uncommitted":
|
|
1272
|
+
return { type: "uncommitted" };
|
|
1273
|
+
|
|
1274
|
+
case "baseBranch": {
|
|
1275
|
+
const target = await showBranchSelector(ctx);
|
|
1276
|
+
if (target) return target;
|
|
1277
|
+
break;
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
case "commit": {
|
|
1281
|
+
if (reviewLoopFixingEnabled) {
|
|
1282
|
+
ctx.ui.notify("Loop mode does not work with commit review.", "error");
|
|
1283
|
+
break;
|
|
1284
|
+
}
|
|
1285
|
+
const target = await showCommitSelector(ctx);
|
|
1286
|
+
if (target) return target;
|
|
1287
|
+
break;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
case "folder": {
|
|
1291
|
+
const target = await showFolderInput(ctx);
|
|
1292
|
+
if (target) return target;
|
|
1293
|
+
break;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
case "pullRequest": {
|
|
1297
|
+
const target = await showPrInput(ctx);
|
|
1298
|
+
if (target) return target;
|
|
1299
|
+
break;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
default:
|
|
1303
|
+
return null;
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
async function showSearchableSelect<T>(
|
|
1309
|
+
ctx: ExtensionContext,
|
|
1310
|
+
options: {
|
|
1311
|
+
title: string;
|
|
1312
|
+
items: SelectItem[];
|
|
1313
|
+
emptyCopy: string;
|
|
1314
|
+
mapResult: (item: SelectItem) => T | null;
|
|
1315
|
+
},
|
|
1316
|
+
): Promise<T | null> {
|
|
1317
|
+
const { title, items, emptyCopy, mapResult } = options;
|
|
1318
|
+
return ctx.ui.custom<T | null>((tui, theme, keybindings, done) => {
|
|
1319
|
+
const container = new Container();
|
|
1320
|
+
container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
|
|
1321
|
+
container.addChild(new Text(theme.fg("accent", theme.bold(title))));
|
|
1322
|
+
|
|
1323
|
+
const searchInput = new Input();
|
|
1324
|
+
container.addChild(searchInput);
|
|
1325
|
+
container.addChild(new Spacer(1));
|
|
1326
|
+
|
|
1327
|
+
const listContainer = new Container();
|
|
1328
|
+
container.addChild(listContainer);
|
|
1329
|
+
container.addChild(
|
|
1330
|
+
new Text(theme.fg("dim", "Type to filter • enter to select • esc to cancel")),
|
|
1331
|
+
);
|
|
1332
|
+
container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
|
|
1333
|
+
|
|
1334
|
+
let filteredItems = items;
|
|
1335
|
+
let selectList: SelectList | null = null;
|
|
1336
|
+
|
|
1337
|
+
const updateList = () => {
|
|
1338
|
+
listContainer.clear();
|
|
1339
|
+
if (filteredItems.length === 0) {
|
|
1340
|
+
listContainer.addChild(new Text(theme.fg("warning", emptyCopy)));
|
|
1341
|
+
selectList = null;
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
selectList = new SelectList(filteredItems, Math.min(filteredItems.length, 10), {
|
|
1346
|
+
selectedPrefix: (text) => theme.fg("accent", text),
|
|
1347
|
+
selectedText: (text) => theme.fg("accent", text),
|
|
1348
|
+
description: (text) => theme.fg("muted", text),
|
|
1349
|
+
scrollInfo: (text) => theme.fg("dim", text),
|
|
1350
|
+
noMatch: (text) => theme.fg("warning", text),
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
selectList.onSelect = (item) => done(mapResult(item));
|
|
1354
|
+
selectList.onCancel = () => done(null);
|
|
1355
|
+
listContainer.addChild(selectList);
|
|
1356
|
+
};
|
|
1357
|
+
|
|
1358
|
+
const applyFilter = () => {
|
|
1359
|
+
const query = searchInput.getValue();
|
|
1360
|
+
filteredItems = query
|
|
1361
|
+
? fuzzyFilter(
|
|
1362
|
+
items,
|
|
1363
|
+
query,
|
|
1364
|
+
(item) => `${item.label} ${item.value} ${item.description ?? ""}`,
|
|
1365
|
+
)
|
|
1366
|
+
: items;
|
|
1367
|
+
updateList();
|
|
1368
|
+
};
|
|
1369
|
+
|
|
1370
|
+
applyFilter();
|
|
1371
|
+
|
|
1372
|
+
return {
|
|
1373
|
+
render(width: number) {
|
|
1374
|
+
return container.render(width);
|
|
1375
|
+
},
|
|
1376
|
+
invalidate() {
|
|
1377
|
+
container.invalidate();
|
|
1378
|
+
},
|
|
1379
|
+
handleInput(data: string) {
|
|
1380
|
+
if (
|
|
1381
|
+
keybindings.matches(data, "tui.select.up") ||
|
|
1382
|
+
keybindings.matches(data, "tui.select.down") ||
|
|
1383
|
+
keybindings.matches(data, "tui.select.confirm") ||
|
|
1384
|
+
keybindings.matches(data, "tui.select.cancel")
|
|
1385
|
+
) {
|
|
1386
|
+
if (selectList) {
|
|
1387
|
+
selectList.handleInput(data);
|
|
1388
|
+
} else if (keybindings.matches(data, "tui.select.cancel")) {
|
|
1389
|
+
done(null);
|
|
1390
|
+
}
|
|
1391
|
+
tui.requestRender();
|
|
1392
|
+
return;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
searchInput.handleInput(data);
|
|
1396
|
+
applyFilter();
|
|
1397
|
+
tui.requestRender();
|
|
1398
|
+
},
|
|
1399
|
+
};
|
|
1400
|
+
});
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
async function showBranchSelector(ctx: ExtensionContext): Promise<ReviewTarget | null> {
|
|
1404
|
+
const branches = await getLocalBranches(pi);
|
|
1405
|
+
const currentBranch = await getCurrentBranch(pi);
|
|
1406
|
+
const defaultBranch = await getDefaultBranch(pi);
|
|
1407
|
+
|
|
1408
|
+
// Never offer the current branch as a base branch (reviewing against itself is meaningless).
|
|
1409
|
+
const candidateBranches = currentBranch
|
|
1410
|
+
? branches.filter((b) => b !== currentBranch)
|
|
1411
|
+
: branches;
|
|
1412
|
+
|
|
1413
|
+
if (candidateBranches.length === 0) {
|
|
1414
|
+
ctx.ui.notify(
|
|
1415
|
+
currentBranch
|
|
1416
|
+
? `No other branches found (current branch: ${currentBranch})`
|
|
1417
|
+
: "No branches found",
|
|
1418
|
+
"error",
|
|
1419
|
+
);
|
|
1420
|
+
return null;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// Sort branches with default branch first
|
|
1424
|
+
const sortedBranches = candidateBranches.sort((a, b) => {
|
|
1425
|
+
if (a === defaultBranch) return -1;
|
|
1426
|
+
if (b === defaultBranch) return 1;
|
|
1427
|
+
return a.localeCompare(b);
|
|
1428
|
+
});
|
|
1429
|
+
|
|
1430
|
+
const items: SelectItem[] = sortedBranches.map((branch) => ({
|
|
1431
|
+
value: branch,
|
|
1432
|
+
label: branch,
|
|
1433
|
+
description: branch === defaultBranch ? "(default)" : "",
|
|
1434
|
+
}));
|
|
1435
|
+
|
|
1436
|
+
const branch = await showSearchableSelect(ctx, {
|
|
1437
|
+
title: "Select base branch",
|
|
1438
|
+
items,
|
|
1439
|
+
emptyCopy: " No matching branches",
|
|
1440
|
+
mapResult: (item) => item.value,
|
|
1441
|
+
});
|
|
1442
|
+
if (!branch) return null;
|
|
1443
|
+
return { type: "baseBranch", branch };
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
async function showCommitSelector(ctx: ExtensionContext): Promise<ReviewTarget | null> {
|
|
1447
|
+
const commits = await getRecentCommits(pi, 20);
|
|
1448
|
+
|
|
1449
|
+
if (commits.length === 0) {
|
|
1450
|
+
ctx.ui.notify("No commits found", "error");
|
|
1451
|
+
return null;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
const items: SelectItem[] = commits.map((commit) => ({
|
|
1455
|
+
value: commit.sha,
|
|
1456
|
+
label: `${commit.sha.slice(0, 7)} ${commit.title}`,
|
|
1457
|
+
description: "",
|
|
1458
|
+
}));
|
|
1459
|
+
|
|
1460
|
+
const commit = await showSearchableSelect(ctx, {
|
|
1461
|
+
title: "Select commit to review",
|
|
1462
|
+
items,
|
|
1463
|
+
emptyCopy: " No matching commits",
|
|
1464
|
+
mapResult: (item) => commits.find((candidate) => candidate.sha === item.value) ?? null,
|
|
1465
|
+
});
|
|
1466
|
+
if (!commit) return null;
|
|
1467
|
+
return { type: "commit", sha: commit.sha, title: commit.title };
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
async function showFolderInput(ctx: ExtensionContext): Promise<ReviewTarget | null> {
|
|
1471
|
+
const result = await ctx.ui.editor(
|
|
1472
|
+
"Enter folders/files to review (space-separated or one per line):",
|
|
1473
|
+
".",
|
|
1474
|
+
);
|
|
1475
|
+
|
|
1476
|
+
if (!result?.trim()) return null;
|
|
1477
|
+
const paths = parseReviewPaths(result);
|
|
1478
|
+
if (paths.length === 0) return null;
|
|
1479
|
+
|
|
1480
|
+
return { type: "folder", paths };
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
async function showPrInput(ctx: ExtensionContext): Promise<ReviewTarget | null> {
|
|
1484
|
+
const prRef = await ctx.ui.editor(
|
|
1485
|
+
"Enter PR number or URL (e.g. 123 or https://github.com/owner/repo/pull/123):",
|
|
1486
|
+
"",
|
|
1487
|
+
);
|
|
1488
|
+
|
|
1489
|
+
if (!prRef?.trim()) return null;
|
|
1490
|
+
return checkoutPullRequest(pi, ctx, prRef);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
async function executeReview(
|
|
1494
|
+
ctx: ExtensionCommandContext,
|
|
1495
|
+
target: ReviewTarget,
|
|
1496
|
+
useFreshSession: boolean,
|
|
1497
|
+
options?: { includeLocalChanges?: boolean; extraInstruction?: string },
|
|
1498
|
+
): Promise<boolean> {
|
|
1499
|
+
if (reviewOriginId) {
|
|
1500
|
+
ctx.ui.notify("Already in a review. Use /end-review to finish first.", "warning");
|
|
1501
|
+
return false;
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
if (useFreshSession) {
|
|
1505
|
+
// In an empty session there is no leaf yet, so create a lightweight anchor first.
|
|
1506
|
+
let originId = ctx.sessionManager.getLeafId() ?? undefined;
|
|
1507
|
+
if (!originId) {
|
|
1508
|
+
pi.appendEntry(REVIEW_ANCHOR_TYPE, { createdAt: new Date().toISOString() });
|
|
1509
|
+
originId = ctx.sessionManager.getLeafId() ?? undefined;
|
|
1510
|
+
}
|
|
1511
|
+
if (!originId) {
|
|
1512
|
+
ctx.ui.notify("Failed to determine review origin.", "error");
|
|
1513
|
+
return false;
|
|
1514
|
+
}
|
|
1515
|
+
reviewOriginId = originId;
|
|
1516
|
+
|
|
1517
|
+
// Keep a local copy so session_tree events during navigation don't wipe it
|
|
1518
|
+
const lockedOriginId = originId;
|
|
1519
|
+
|
|
1520
|
+
// No user message (brand-new session): stay on the current leaf.
|
|
1521
|
+
const entries = ctx.sessionManager.getEntries();
|
|
1522
|
+
const firstUserMessage = entries.find(
|
|
1523
|
+
(e) => e.type === "message" && e.message.role === "user",
|
|
1524
|
+
);
|
|
1525
|
+
|
|
1526
|
+
if (firstUserMessage) {
|
|
1527
|
+
// Label the new branch so it stays visible in the session tree.
|
|
1528
|
+
try {
|
|
1529
|
+
const result = await ctx.navigateTree(firstUserMessage.id, {
|
|
1530
|
+
summarize: false,
|
|
1531
|
+
label: "code-review",
|
|
1532
|
+
});
|
|
1533
|
+
if (result.cancelled) {
|
|
1534
|
+
reviewOriginId = undefined;
|
|
1535
|
+
return false;
|
|
1536
|
+
}
|
|
1537
|
+
} catch (error) {
|
|
1538
|
+
reviewOriginId = undefined;
|
|
1539
|
+
ctx.ui.notify(
|
|
1540
|
+
`Failed to start review: ${error instanceof Error ? error.message : String(error)}`,
|
|
1541
|
+
"error",
|
|
1542
|
+
);
|
|
1543
|
+
return false;
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
// Clear the editor (navigating to user message fills it with the message text)
|
|
1547
|
+
ctx.ui.setEditorText("");
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
// Restore origin after navigation events (session_tree can reset it)
|
|
1551
|
+
reviewOriginId = lockedOriginId;
|
|
1552
|
+
|
|
1553
|
+
setReviewWidget(ctx, true);
|
|
1554
|
+
|
|
1555
|
+
// Persist review state so tree navigation can restore/reset it
|
|
1556
|
+
pi.appendEntry(REVIEW_STATE_TYPE, { active: true, originId: lockedOriginId });
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
const prompt = await buildReviewPrompt(pi, target, {
|
|
1560
|
+
includeLocalChanges: options?.includeLocalChanges === true,
|
|
1561
|
+
});
|
|
1562
|
+
const hint = getUserFacingHint(target);
|
|
1563
|
+
const projectGuidelines = canReadProjectConfig(ctx)
|
|
1564
|
+
? await loadProjectReviewGuidelines(ctx.cwd)
|
|
1565
|
+
: null;
|
|
1566
|
+
|
|
1567
|
+
const fullPrompt = composeReviewPrompt(prompt, {
|
|
1568
|
+
customInstructions: reviewCustomInstructions,
|
|
1569
|
+
extraInstruction: options?.extraInstruction,
|
|
1570
|
+
projectGuidelines,
|
|
1571
|
+
});
|
|
1572
|
+
|
|
1573
|
+
const modeHint = useFreshSession ? " (fresh session)" : "";
|
|
1574
|
+
ctx.ui.notify(`Starting review: ${hint}${modeHint}`, "info");
|
|
1575
|
+
|
|
1576
|
+
pi.sendUserMessage(fullPrompt);
|
|
1577
|
+
return true;
|
|
1578
|
+
}
|
|
1579
|
+
|
|
1580
|
+
async function runLoopFixingReview(
|
|
1581
|
+
ctx: ExtensionCommandContext,
|
|
1582
|
+
target: ReviewTarget,
|
|
1583
|
+
extraInstruction?: string,
|
|
1584
|
+
): Promise<void> {
|
|
1585
|
+
if (reviewLoopInProgress) {
|
|
1586
|
+
ctx.ui.notify("Loop fixing review is already running.", "warning");
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
reviewLoopInProgress = true;
|
|
1591
|
+
setReviewWidget(ctx, Boolean(reviewOriginId));
|
|
1592
|
+
try {
|
|
1593
|
+
ctx.ui.notify(
|
|
1594
|
+
"Loop fixing enabled: using Empty branch mode and cycling until no blocking findings remain.",
|
|
1595
|
+
"info",
|
|
1596
|
+
);
|
|
1597
|
+
|
|
1598
|
+
for (let pass = 1; pass <= REVIEW_LOOP_MAX_ITERATIONS; pass++) {
|
|
1599
|
+
const reviewBaselineAssistantId = getLastAssistantSnapshot(ctx)?.id;
|
|
1600
|
+
const started = await executeReview(ctx, target, true, {
|
|
1601
|
+
includeLocalChanges: true,
|
|
1602
|
+
extraInstruction,
|
|
1603
|
+
});
|
|
1604
|
+
if (!started) {
|
|
1605
|
+
ctx.ui.notify("Loop fixing stopped before starting the review pass.", "warning");
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
const reviewTurnStarted = await waitForLoopTurnToStart(ctx, reviewBaselineAssistantId);
|
|
1610
|
+
if (!reviewTurnStarted) {
|
|
1611
|
+
ctx.ui.notify("Loop fixing stopped: review pass did not start in time.", "error");
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
await ctx.waitForIdle();
|
|
1616
|
+
|
|
1617
|
+
const reviewSnapshot = getLastAssistantSnapshot(ctx);
|
|
1618
|
+
if (!reviewSnapshot || reviewSnapshot.id === reviewBaselineAssistantId) {
|
|
1619
|
+
ctx.ui.notify("Loop fixing stopped: could not read the review result.", "warning");
|
|
1620
|
+
return;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
if (reviewSnapshot.stopReason === "aborted") {
|
|
1624
|
+
ctx.ui.notify("Loop fixing stopped: review was aborted.", "warning");
|
|
1625
|
+
return;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
if (reviewSnapshot.stopReason === "error") {
|
|
1629
|
+
ctx.ui.notify("Loop fixing stopped: review failed with an error.", "error");
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
if (reviewSnapshot.stopReason === "length") {
|
|
1634
|
+
ctx.ui.notify(
|
|
1635
|
+
"Loop fixing stopped: review output was truncated (stopReason=length).",
|
|
1636
|
+
"warning",
|
|
1637
|
+
);
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
if (!hasBlockingReviewFindings(reviewSnapshot.text)) {
|
|
1642
|
+
const finalized = await executeEndReviewAction(ctx, "returnAndSummarize", {
|
|
1643
|
+
showSummaryLoader: true,
|
|
1644
|
+
notifySuccess: false,
|
|
1645
|
+
});
|
|
1646
|
+
if (finalized !== "ok") {
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
ctx.ui.notify("Loop fixing complete: no blocking findings remain.", "info");
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
ctx.ui.notify(
|
|
1655
|
+
`Loop fixing pass ${pass}: found blocking findings, returning to fix them...`,
|
|
1656
|
+
"info",
|
|
1657
|
+
);
|
|
1658
|
+
|
|
1659
|
+
const fixBaselineAssistantId = getLastAssistantSnapshot(ctx)?.id;
|
|
1660
|
+
const sentFixPrompt = await executeEndReviewAction(ctx, "returnAndFix", {
|
|
1661
|
+
showSummaryLoader: true,
|
|
1662
|
+
notifySuccess: false,
|
|
1663
|
+
});
|
|
1664
|
+
if (sentFixPrompt !== "ok") {
|
|
1665
|
+
return;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
const fixTurnStarted = await waitForLoopTurnToStart(ctx, fixBaselineAssistantId);
|
|
1669
|
+
if (!fixTurnStarted) {
|
|
1670
|
+
ctx.ui.notify("Loop fixing stopped: fix pass did not start in time.", "error");
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
await ctx.waitForIdle();
|
|
1675
|
+
|
|
1676
|
+
const fixSnapshot = getLastAssistantSnapshot(ctx);
|
|
1677
|
+
if (!fixSnapshot || fixSnapshot.id === fixBaselineAssistantId) {
|
|
1678
|
+
ctx.ui.notify("Loop fixing stopped: could not read the fix pass result.", "warning");
|
|
1679
|
+
return;
|
|
1680
|
+
}
|
|
1681
|
+
if (fixSnapshot.stopReason === "aborted") {
|
|
1682
|
+
ctx.ui.notify("Loop fixing stopped: fix pass was aborted.", "warning");
|
|
1683
|
+
return;
|
|
1684
|
+
}
|
|
1685
|
+
if (fixSnapshot.stopReason === "error") {
|
|
1686
|
+
ctx.ui.notify("Loop fixing stopped: fix pass failed with an error.", "error");
|
|
1687
|
+
return;
|
|
1688
|
+
}
|
|
1689
|
+
if (fixSnapshot.stopReason === "length") {
|
|
1690
|
+
ctx.ui.notify(
|
|
1691
|
+
"Loop fixing stopped: fix pass output was truncated (stopReason=length).",
|
|
1692
|
+
"warning",
|
|
1693
|
+
);
|
|
1694
|
+
return;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
ctx.ui.notify(
|
|
1699
|
+
`Loop fixing stopped after ${REVIEW_LOOP_MAX_ITERATIONS} passes (safety limit reached).`,
|
|
1700
|
+
"warning",
|
|
1701
|
+
);
|
|
1702
|
+
} finally {
|
|
1703
|
+
reviewLoopInProgress = false;
|
|
1704
|
+
setReviewWidget(ctx, Boolean(reviewOriginId));
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
pi.registerCommand("review", {
|
|
1709
|
+
description: "Review code changes (PR, uncommitted, branch, commit, or folder)",
|
|
1710
|
+
handler: async (args, ctx) => {
|
|
1711
|
+
if (ctx.mode !== "tui") {
|
|
1712
|
+
ctx.ui.notify("Review requires interactive mode", "error");
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
if (reviewLoopInProgress) {
|
|
1717
|
+
ctx.ui.notify("Loop fixing review is already running.", "warning");
|
|
1718
|
+
return;
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
if (reviewOriginId) {
|
|
1722
|
+
ctx.ui.notify("Already in a review. Use /end-review to finish first.", "warning");
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
const { code } = await pi.exec("git", ["rev-parse", "--git-dir"]);
|
|
1727
|
+
if (code !== 0) {
|
|
1728
|
+
ctx.ui.notify("Not a git repository", "error");
|
|
1729
|
+
return;
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
let target: ReviewTarget | null = null;
|
|
1733
|
+
let fromSelector = false;
|
|
1734
|
+
let extraInstruction: string | undefined;
|
|
1735
|
+
const parsed = parseArgs(args);
|
|
1736
|
+
if (parsed.error) {
|
|
1737
|
+
ctx.ui.notify(parsed.error, "error");
|
|
1738
|
+
return;
|
|
1739
|
+
}
|
|
1740
|
+
extraInstruction = parsed.extraInstruction?.trim() || undefined;
|
|
1741
|
+
|
|
1742
|
+
if (parsed.target) {
|
|
1743
|
+
if (parsed.target.type === "pr") {
|
|
1744
|
+
target = await checkoutPullRequest(pi, ctx, parsed.target.ref);
|
|
1745
|
+
if (!target) {
|
|
1746
|
+
ctx.ui.notify("PR review failed. Returning to review menu.", "warning");
|
|
1747
|
+
}
|
|
1748
|
+
} else {
|
|
1749
|
+
target = parsed.target;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
if (!target) {
|
|
1754
|
+
fromSelector = true;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
while (true) {
|
|
1758
|
+
if (!target && fromSelector) {
|
|
1759
|
+
target = await showReviewSelector(ctx);
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
if (!target) {
|
|
1763
|
+
ctx.ui.notify("Review cancelled", "info");
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
if (reviewLoopFixingEnabled && target.type === "commit") {
|
|
1768
|
+
ctx.ui.notify("Loop mode does not work with commit review.", "error");
|
|
1769
|
+
if (fromSelector) {
|
|
1770
|
+
target = null;
|
|
1771
|
+
continue;
|
|
1772
|
+
}
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
if (reviewLoopFixingEnabled) {
|
|
1777
|
+
await runLoopFixingReview(ctx, target, extraInstruction);
|
|
1778
|
+
return;
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
const entries = ctx.sessionManager.getEntries();
|
|
1782
|
+
const messageCount = entries.filter((e) => e.type === "message").length;
|
|
1783
|
+
|
|
1784
|
+
// In an empty session, default to fresh review mode so /end-review works consistently.
|
|
1785
|
+
let useFreshSession = messageCount === 0;
|
|
1786
|
+
|
|
1787
|
+
if (messageCount > 0) {
|
|
1788
|
+
const choice = await ctx.ui.select("Start review in:", [
|
|
1789
|
+
"Empty branch",
|
|
1790
|
+
"Current session",
|
|
1791
|
+
]);
|
|
1792
|
+
|
|
1793
|
+
if (choice === undefined) {
|
|
1794
|
+
if (fromSelector) {
|
|
1795
|
+
target = null;
|
|
1796
|
+
continue;
|
|
1797
|
+
}
|
|
1798
|
+
ctx.ui.notify("Review cancelled", "info");
|
|
1799
|
+
return;
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
useFreshSession = choice === "Empty branch";
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
await executeReview(ctx, target, useFreshSession, { extraInstruction });
|
|
1806
|
+
return;
|
|
1807
|
+
}
|
|
1808
|
+
},
|
|
1809
|
+
});
|
|
1810
|
+
|
|
1811
|
+
const REVIEW_FIX_FINDINGS_PROMPT = `Use the latest review summary in this session and implement the review findings now.
|
|
1954
1812
|
|
|
1955
1813
|
Instructions:
|
|
1956
1814
|
1. Treat the summary's Findings/Fix Queue as a checklist.
|
|
@@ -1963,232 +1821,247 @@ Instructions:
|
|
|
1963
1821
|
8. Run relevant tests/checks for touched code where practical.
|
|
1964
1822
|
9. End with: fixed items, deferred/skipped items (with reasons), and verification results.`;
|
|
1965
1823
|
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
1824
|
+
type EndReviewAction = "returnOnly" | "returnAndFix" | "returnAndSummarize";
|
|
1825
|
+
type EndReviewActionResult = "ok" | "cancelled" | "error";
|
|
1826
|
+
type EndReviewActionOptions = {
|
|
1827
|
+
showSummaryLoader?: boolean;
|
|
1828
|
+
notifySuccess?: boolean;
|
|
1829
|
+
};
|
|
1830
|
+
|
|
1831
|
+
function getActiveReviewOrigin(ctx: ExtensionContext): string | undefined {
|
|
1832
|
+
if (reviewOriginId) {
|
|
1833
|
+
return reviewOriginId;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
const state = getReviewState(ctx);
|
|
1837
|
+
if (state?.active && state.originId) {
|
|
1838
|
+
reviewOriginId = state.originId;
|
|
1839
|
+
return reviewOriginId;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
if (state?.active) {
|
|
1843
|
+
setReviewWidget(ctx, false);
|
|
1844
|
+
pi.appendEntry(REVIEW_STATE_TYPE, { active: false });
|
|
1845
|
+
ctx.ui.notify("Review state was missing origin info; cleared review status.", "warning");
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
return undefined;
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
function clearReviewState(ctx: ExtensionContext) {
|
|
1852
|
+
setReviewWidget(ctx, false);
|
|
1853
|
+
reviewOriginId = undefined;
|
|
1854
|
+
pi.appendEntry(REVIEW_STATE_TYPE, { active: false });
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
type SummaryNavigationResult =
|
|
1858
|
+
| { status: "success" }
|
|
1859
|
+
| { status: "cancelled" }
|
|
1860
|
+
| { status: "late-success" }
|
|
1861
|
+
| { status: "error"; error: string };
|
|
1862
|
+
|
|
1863
|
+
async function navigateWithSummary(
|
|
1864
|
+
ctx: ExtensionCommandContext,
|
|
1865
|
+
originId: string,
|
|
1866
|
+
showLoader: boolean,
|
|
1867
|
+
): Promise<SummaryNavigationResult> {
|
|
1868
|
+
if (showLoader && ctx.mode === "tui") {
|
|
1869
|
+
return ctx.ui.custom<SummaryNavigationResult>((tui, theme, _kb, done) => {
|
|
1870
|
+
const loader = new BorderedLoader(tui, theme, "Returning and summarizing review branch...");
|
|
1871
|
+
let finished = false;
|
|
1872
|
+
let abortRequested = false;
|
|
1873
|
+
|
|
1874
|
+
const finish = (result: SummaryNavigationResult) => {
|
|
1875
|
+
if (finished) return;
|
|
1876
|
+
finished = true;
|
|
1877
|
+
done(result);
|
|
1878
|
+
};
|
|
1879
|
+
|
|
1880
|
+
const navigationPromise = ctx
|
|
1881
|
+
.navigateTree(originId, {
|
|
1882
|
+
summarize: true,
|
|
1883
|
+
customInstructions: REVIEW_SUMMARY_PROMPT,
|
|
1884
|
+
replaceInstructions: true,
|
|
1885
|
+
})
|
|
1886
|
+
.then((result) => {
|
|
1887
|
+
finish(
|
|
1888
|
+
result.cancelled
|
|
1889
|
+
? { status: "cancelled" }
|
|
1890
|
+
: abortRequested
|
|
1891
|
+
? { status: "late-success" }
|
|
1892
|
+
: { status: "success" },
|
|
1893
|
+
);
|
|
1894
|
+
})
|
|
1895
|
+
.catch((err) => {
|
|
1896
|
+
finish({ status: "error", error: err instanceof Error ? err.message : String(err) });
|
|
1897
|
+
});
|
|
1898
|
+
|
|
1899
|
+
loader.onAbort = () => {
|
|
1900
|
+
if (abortRequested) return;
|
|
1901
|
+
abortRequested = true;
|
|
1902
|
+
void Promise.resolve()
|
|
1903
|
+
.then(() => ctx.abort())
|
|
1904
|
+
.catch(() => undefined)
|
|
1905
|
+
.then(() => navigationPromise)
|
|
1906
|
+
.catch(() => undefined);
|
|
1907
|
+
};
|
|
1908
|
+
|
|
1909
|
+
return loader;
|
|
1910
|
+
});
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
try {
|
|
1914
|
+
const result = await ctx.navigateTree(originId, {
|
|
1915
|
+
summarize: true,
|
|
1916
|
+
customInstructions: REVIEW_SUMMARY_PROMPT,
|
|
1917
|
+
replaceInstructions: true,
|
|
1918
|
+
});
|
|
1919
|
+
return result.cancelled ? { status: "cancelled" } : { status: "success" };
|
|
1920
|
+
} catch (error) {
|
|
1921
|
+
return { status: "error", error: error instanceof Error ? error.message : String(error) };
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
async function executeEndReviewAction(
|
|
1926
|
+
ctx: ExtensionCommandContext,
|
|
1927
|
+
action: EndReviewAction,
|
|
1928
|
+
options: EndReviewActionOptions = {},
|
|
1929
|
+
): Promise<EndReviewActionResult> {
|
|
1930
|
+
const originId = getActiveReviewOrigin(ctx);
|
|
1931
|
+
if (!originId) {
|
|
1932
|
+
if (!getReviewState(ctx)?.active) {
|
|
1933
|
+
ctx.ui.notify(
|
|
1934
|
+
"Not in a review branch (use /review first, or review was started in current session mode)",
|
|
1935
|
+
"info",
|
|
1936
|
+
);
|
|
1937
|
+
}
|
|
1938
|
+
return "error";
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
const notifySuccess = options.notifySuccess ?? true;
|
|
1942
|
+
|
|
1943
|
+
if (action === "returnOnly") {
|
|
1944
|
+
try {
|
|
1945
|
+
const result = await ctx.navigateTree(originId, { summarize: false });
|
|
1946
|
+
if (result.cancelled) {
|
|
1947
|
+
ctx.ui.notify("Navigation cancelled. Use /end-review to try again.", "info");
|
|
1948
|
+
return "cancelled";
|
|
1949
|
+
}
|
|
1950
|
+
} catch (error) {
|
|
1951
|
+
ctx.ui.notify(
|
|
1952
|
+
`Failed to return: ${error instanceof Error ? error.message : String(error)}`,
|
|
1953
|
+
"error",
|
|
1954
|
+
);
|
|
1955
|
+
return "error";
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
clearReviewState(ctx);
|
|
1959
|
+
if (notifySuccess) {
|
|
1960
|
+
ctx.ui.notify("Review complete! Returned to original position.", "info");
|
|
1961
|
+
}
|
|
1962
|
+
return "ok";
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
const summaryResult = await navigateWithSummary(
|
|
1966
|
+
ctx,
|
|
1967
|
+
originId,
|
|
1968
|
+
options.showSummaryLoader ?? false,
|
|
1969
|
+
);
|
|
1970
|
+
if (summaryResult.status === "error") {
|
|
1971
|
+
ctx.ui.notify(`Summarization failed: ${summaryResult.error}`, "error");
|
|
1972
|
+
return "error";
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
if (summaryResult.status === "cancelled") {
|
|
1976
|
+
ctx.ui.notify("Navigation cancelled. Use /end-review to try again.", "info");
|
|
1977
|
+
return "cancelled";
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
clearReviewState(ctx);
|
|
1981
|
+
if (summaryResult.status === "late-success") {
|
|
1982
|
+
ctx.ui.notify(
|
|
1983
|
+
"Cancellation arrived too late; review already completed, and no follow-up was queued.",
|
|
1984
|
+
"warning",
|
|
1985
|
+
);
|
|
1986
|
+
return "ok";
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
if (action === "returnAndSummarize") {
|
|
1990
|
+
if (!ctx.ui.getEditorText().trim()) {
|
|
1991
|
+
ctx.ui.setEditorText("Act on the review findings");
|
|
1992
|
+
}
|
|
1993
|
+
if (notifySuccess) {
|
|
1994
|
+
ctx.ui.notify("Review complete! Returned and summarized.", "info");
|
|
1995
|
+
}
|
|
1996
|
+
return "ok";
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
pi.sendUserMessage(REVIEW_FIX_FINDINGS_PROMPT, { deliverAs: "followUp" });
|
|
2000
|
+
if (notifySuccess) {
|
|
2001
|
+
ctx.ui.notify("Review complete! Returned and queued a follow-up to fix findings.", "info");
|
|
2002
|
+
}
|
|
2003
|
+
return "ok";
|
|
2004
|
+
}
|
|
2005
|
+
|
|
2006
|
+
async function runEndReview(ctx: ExtensionCommandContext): Promise<void> {
|
|
2007
|
+
if (!ctx.hasUI) {
|
|
2008
|
+
ctx.ui.notify("End-review requires interactive mode", "error");
|
|
2009
|
+
return;
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
if (reviewLoopInProgress) {
|
|
2013
|
+
ctx.ui.notify("Loop fixing review is running. Wait for it to finish.", "info");
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
if (!getActiveReviewOrigin(ctx)) {
|
|
2018
|
+
if (!getReviewState(ctx)?.active) {
|
|
2019
|
+
ctx.ui.notify(
|
|
2020
|
+
"Not in a review branch (use /review first, or review was started in current session mode)",
|
|
2021
|
+
"info",
|
|
2022
|
+
);
|
|
2023
|
+
}
|
|
2024
|
+
return;
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
if (endReviewInProgress) {
|
|
2028
|
+
ctx.ui.notify("/end-review is already running", "info");
|
|
2029
|
+
return;
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
endReviewInProgress = true;
|
|
2033
|
+
try {
|
|
2034
|
+
const choice = await ctx.ui.select("Finish review:", [
|
|
2035
|
+
"Return only",
|
|
2036
|
+
"Return and fix findings",
|
|
2037
|
+
"Return and summarize",
|
|
2038
|
+
]);
|
|
2039
|
+
|
|
2040
|
+
if (choice === undefined) {
|
|
2041
|
+
ctx.ui.notify("Cancelled. Use /end-review to try again.", "info");
|
|
2042
|
+
return;
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
const action: EndReviewAction =
|
|
2046
|
+
choice === "Return and fix findings"
|
|
2047
|
+
? "returnAndFix"
|
|
2048
|
+
: choice === "Return and summarize"
|
|
2049
|
+
? "returnAndSummarize"
|
|
2050
|
+
: "returnOnly";
|
|
2051
|
+
|
|
2052
|
+
await executeEndReviewAction(ctx, action, {
|
|
2053
|
+
showSummaryLoader: true,
|
|
2054
|
+
notifySuccess: true,
|
|
2055
|
+
});
|
|
2056
|
+
} finally {
|
|
2057
|
+
endReviewInProgress = false;
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
pi.registerCommand("end-review", {
|
|
2062
|
+
description: "Complete review and return to original position",
|
|
2063
|
+
handler: async (_args, ctx) => {
|
|
2064
|
+
await runEndReview(ctx);
|
|
2065
|
+
},
|
|
2066
|
+
});
|
|
2194
2067
|
}
|