@docyrus/docyrus 0.0.33 → 0.0.35

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