@ia-ccun/code-agent-cli 0.0.1

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 (87) hide show
  1. package/README.md +211 -0
  2. package/bin/cli.js +83 -0
  3. package/config/agent/APPEND_SYSTEM.md +48 -0
  4. package/config/agent/SYSTEM.md +33 -0
  5. package/config/agent/bin/fd +0 -0
  6. package/config/agent/extensions/context.ts +578 -0
  7. package/config/agent/extensions/custom-footer.ts +170 -0
  8. package/config/agent/extensions/custom.ts +289 -0
  9. package/config/agent/extensions/review.ts +1281 -0
  10. package/config/agent/extensions/working-msg.ts +96 -0
  11. package/config/agent/help.md +364 -0
  12. package/config/agent/models.json +56 -0
  13. package/config/agent/prompts/feat.md +106 -0
  14. package/config/agent/prompts/git-commit.md +159 -0
  15. package/config/agent/prompts/git-rollback.md +91 -0
  16. package/config/agent/prompts/git-worktree.md +277 -0
  17. package/config/agent/prompts/help.md +10 -0
  18. package/config/agent/prompts/init-project.md +53 -0
  19. package/config/agent/prompts/workflow.md +194 -0
  20. package/config/agent/settings.json +7 -0
  21. package/config/agent/skills/code-review/SKILL.md +50 -0
  22. package/config/agent/skills/commit/SKILL.md +51 -0
  23. package/config/agent/skills/csv-data-summarizer/SKILL.md +149 -0
  24. package/config/agent/skills/csv-data-summarizer/analyze.py +182 -0
  25. package/config/agent/skills/csv-data-summarizer/examples/showcase_financial_pl_data.csv +46 -0
  26. package/config/agent/skills/csv-data-summarizer/requirements.txt +4 -0
  27. package/config/agent/skills/csv-data-summarizer/resources/sample.csv +22 -0
  28. package/config/agent/skills/find-skills/SKILL.md +133 -0
  29. package/config/agent/skills/frontend-design/LICENSE.txt +177 -0
  30. package/config/agent/skills/frontend-design/SKILL.md +42 -0
  31. package/config/agent/skills/github/SKILL.md +47 -0
  32. package/config/agent/skills/hello/SKILL.md +23 -0
  33. package/config/agent/skills/librarian/SKILL.md +195 -0
  34. package/config/agent/skills/markdown-to-html/SKILL.md +62 -0
  35. package/config/agent/skills/pr/SKILL.md +56 -0
  36. package/config/agent/skills/refactor/SKILL.md +37 -0
  37. package/config/agent/skills/skill-creator/LICENSE.txt +202 -0
  38. package/config/agent/skills/skill-creator/SKILL.md +356 -0
  39. package/config/agent/skills/skill-creator/references/output-patterns.md +82 -0
  40. package/config/agent/skills/skill-creator/references/workflows.md +28 -0
  41. package/config/agent/skills/skill-creator/scripts/init_skill.py +303 -0
  42. package/config/agent/skills/skill-creator/scripts/package_skill.py +110 -0
  43. package/config/agent/skills/skill-creator/scripts/quick_validate.py +95 -0
  44. package/config/agent/skills/ui-ux-pro-max/SKILL.md +264 -0
  45. package/config/agent/skills/ui-ux-pro-max/data/charts.csv +26 -0
  46. package/config/agent/skills/ui-ux-pro-max/data/colors.csv +97 -0
  47. package/config/agent/skills/ui-ux-pro-max/data/landing.csv +31 -0
  48. package/config/agent/skills/ui-ux-pro-max/data/products.csv +97 -0
  49. package/config/agent/skills/ui-ux-pro-max/data/prompts.csv +24 -0
  50. package/config/agent/skills/ui-ux-pro-max/data/stacks/flutter.csv +53 -0
  51. package/config/agent/skills/ui-ux-pro-max/data/stacks/html-tailwind.csv +56 -0
  52. package/config/agent/skills/ui-ux-pro-max/data/stacks/nextjs.csv +53 -0
  53. package/config/agent/skills/ui-ux-pro-max/data/stacks/nuxt-ui.csv +51 -0
  54. package/config/agent/skills/ui-ux-pro-max/data/stacks/nuxtjs.csv +59 -0
  55. package/config/agent/skills/ui-ux-pro-max/data/stacks/react-native.csv +52 -0
  56. package/config/agent/skills/ui-ux-pro-max/data/stacks/react.csv +54 -0
  57. package/config/agent/skills/ui-ux-pro-max/data/stacks/svelte.csv +54 -0
  58. package/config/agent/skills/ui-ux-pro-max/data/stacks/swiftui.csv +51 -0
  59. package/config/agent/skills/ui-ux-pro-max/data/stacks/vue.csv +50 -0
  60. package/config/agent/skills/ui-ux-pro-max/data/styles.csv +59 -0
  61. package/config/agent/skills/ui-ux-pro-max/data/typography.csv +58 -0
  62. package/config/agent/skills/ui-ux-pro-max/data/ux-guidelines.csv +100 -0
  63. package/config/agent/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-312.pyc +0 -0
  64. package/config/agent/skills/ui-ux-pro-max/scripts/analyze.py +434 -0
  65. package/config/agent/skills/ui-ux-pro-max/scripts/core.py +238 -0
  66. package/config/agent/skills/ui-ux-pro-max/scripts/search.py +61 -0
  67. package/config/agent/skills/unit-test/SKILL.md +115 -0
  68. package/config/agent/themes/catppuccin-mocha.json +99 -0
  69. package/config.json +6 -0
  70. package/dist/banner.d.ts +10 -0
  71. package/dist/banner.d.ts.map +1 -0
  72. package/dist/banner.js +32 -0
  73. package/dist/banner.js.map +1 -0
  74. package/dist/config-loader.d.ts +17 -0
  75. package/dist/config-loader.d.ts.map +1 -0
  76. package/dist/config-loader.js +60 -0
  77. package/dist/config-loader.js.map +1 -0
  78. package/dist/config.d.ts +23 -0
  79. package/dist/config.d.ts.map +1 -0
  80. package/dist/config.js +12 -0
  81. package/dist/config.js.map +1 -0
  82. package/dist/index.d.ts +11 -0
  83. package/dist/index.d.ts.map +1 -0
  84. package/dist/index.js +14 -0
  85. package/dist/index.js.map +1 -0
  86. package/package.json +69 -0
  87. package/scripts/postinstall.js +197 -0
@@ -0,0 +1,1281 @@
1
+ /**
2
+ * Code Review Extension (inspired by Codex's review feature)
3
+ *
4
+ * Provides a `/review` command that prompts the agent to review code changes.
5
+ * Supports multiple review modes:
6
+ * - Review a GitHub pull request (checks out the PR locally)
7
+ * - Review against a base branch (PR style)
8
+ * - Review uncommitted changes
9
+ * - Review a specific commit
10
+ * - Custom review instructions
11
+ *
12
+ * Usage:
13
+ * - `/review` - show interactive selector
14
+ * - `/review pr 123` - review PR #123 (checks out locally)
15
+ * - `/review pr https://github.com/owner/repo/pull/123` - review PR from URL
16
+ * - `/review uncommitted` - review uncommitted changes directly
17
+ * - `/review branch main` - review against main branch
18
+ * - `/review commit abc123` - review specific commit
19
+ * - `/review folder src docs` - review specific folders/files (snapshot, not diff)
20
+ * - `/review custom "check for security issues"` - custom instructions
21
+ *
22
+ * Project-specific review guidelines:
23
+ * - If a REVIEW_GUIDELINES.md file exists in the same directory as .pi,
24
+ * its contents are appended to the review prompt.
25
+ *
26
+ * Note: PR review requires a clean working tree (no uncommitted changes to tracked files).
27
+ */
28
+
29
+ import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext } from "@mariozechner/pi-coding-agent";
30
+ import { DynamicBorder, BorderedLoader } from "@mariozechner/pi-coding-agent";
31
+ import { Container, type SelectItem, SelectList, Text } from "@mariozechner/pi-tui";
32
+ import path from "node:path";
33
+ import { promises as fs } from "node:fs";
34
+
35
+ // State to track fresh session review (where we branched from).
36
+ // Module-level state means only one review can be active at a time.
37
+ // This is intentional - the UI and /end-review command assume a single active review.
38
+ let reviewOriginId: string | undefined = undefined;
39
+ let endReviewInProgress = false;
40
+
41
+ const REVIEW_STATE_TYPE = "review-session";
42
+
43
+ type ReviewSessionState = {
44
+ active: boolean;
45
+ originId?: string;
46
+ };
47
+
48
+ function setReviewWidget(ctx: ExtensionContext, active: boolean) {
49
+ if (!ctx.hasUI) return;
50
+ if (!active) {
51
+ ctx.ui.setWidget("review", undefined);
52
+ return;
53
+ }
54
+
55
+ ctx.ui.setWidget("review", (_tui, theme) => {
56
+ const text = new Text(theme.fg("warning", "Review session active, return with /end-review"), 0, 0);
57
+ return {
58
+ render(width: number) {
59
+ return text.render(width);
60
+ },
61
+ invalidate() {
62
+ text.invalidate();
63
+ },
64
+ };
65
+ });
66
+ }
67
+
68
+ function getReviewState(ctx: ExtensionContext): ReviewSessionState | undefined {
69
+ let state: ReviewSessionState | undefined;
70
+ for (const entry of ctx.sessionManager.getBranch()) {
71
+ if (entry.type === "custom" && entry.customType === REVIEW_STATE_TYPE) {
72
+ state = entry.data as ReviewSessionState | undefined;
73
+ }
74
+ }
75
+
76
+ return state;
77
+ }
78
+
79
+ function applyReviewState(ctx: ExtensionContext) {
80
+ const state = getReviewState(ctx);
81
+
82
+ if (state?.active && state.originId) {
83
+ reviewOriginId = state.originId;
84
+ setReviewWidget(ctx, true);
85
+ return;
86
+ }
87
+
88
+ reviewOriginId = undefined;
89
+ setReviewWidget(ctx, false);
90
+ }
91
+
92
+ // Review target types (matching Codex's approach)
93
+ type ReviewTarget =
94
+ | { type: "uncommitted" }
95
+ | { type: "baseBranch"; branch: string }
96
+ | { type: "commit"; sha: string; title?: string }
97
+ | { type: "custom"; instructions: string }
98
+ | { type: "pullRequest"; prNumber: number; baseBranch: string; title: string }
99
+ | { type: "folder"; paths: string[] };
100
+
101
+ // Prompts (adapted from Codex)
102
+ const UNCOMMITTED_PROMPT =
103
+ "Review the current code changes (staged, unstaged, and untracked files) and provide prioritized findings.";
104
+
105
+ const BASE_BRANCH_PROMPT_WITH_MERGE_BASE =
106
+ "Review the code changes against the base branch '{baseBranch}'. The merge base commit for this comparison is {mergeBaseSha}. Run `git diff {mergeBaseSha}` to inspect the changes relative to {baseBranch}. Provide prioritized, actionable findings.";
107
+
108
+ const BASE_BRANCH_PROMPT_FALLBACK =
109
+ "Review the code changes against the base branch '{branch}'. Start by finding the merge diff between the current branch and {branch}'s upstream e.g. (`git merge-base HEAD \"$(git rev-parse --abbrev-ref \"{branch}@{upstream}\")\"`), then run `git diff` against that SHA to see what changes we would merge into the {branch} branch. Provide prioritized, actionable findings.";
110
+
111
+ const COMMIT_PROMPT_WITH_TITLE =
112
+ 'Review the code changes introduced by commit {sha} ("{title}"). Provide prioritized, actionable findings.';
113
+
114
+ const COMMIT_PROMPT = "Review the code changes introduced by commit {sha}. Provide prioritized, actionable findings.";
115
+
116
+ const PULL_REQUEST_PROMPT =
117
+ 'Review pull request #{prNumber} ("{title}") against the base branch \'{baseBranch}\'. The merge base commit for this comparison is {mergeBaseSha}. Run `git diff {mergeBaseSha}` to inspect the changes that would be merged. Provide prioritized, actionable findings.';
118
+
119
+ const PULL_REQUEST_PROMPT_FALLBACK =
120
+ 'Review pull request #{prNumber} ("{title}") against the base branch \'{baseBranch}\'. Start by finding the merge base between the current branch and {baseBranch} (e.g., `git merge-base HEAD {baseBranch}`), then run `git diff` against that SHA to see the changes that would be merged. Provide prioritized, actionable findings.';
121
+
122
+ const FOLDER_REVIEW_PROMPT =
123
+ "Review the code in the following paths: {paths}. This is a snapshot review (not a diff). Read the files directly in these paths and provide prioritized, actionable findings.";
124
+
125
+ // The detailed review rubric (adapted from Codex's review_prompt.md)
126
+ const REVIEW_RUBRIC = `# Review Guidelines
127
+
128
+ You are acting as a code reviewer for a proposed code change made by another engineer.
129
+
130
+ Below are default guidelines for determining what to flag. These are not the final word — if you encounter more specific guidelines elsewhere (in a developer message, user message, file, or project review guidelines appended below), those override these general instructions.
131
+
132
+ ## Determining what to flag
133
+
134
+ Flag issues that:
135
+ 1. Meaningfully impact the accuracy, performance, security, or maintainability of the code.
136
+ 2. Are discrete and actionable (not general issues or multiple combined issues).
137
+ 3. Don't demand rigor inconsistent with the rest of the codebase.
138
+ 4. Were introduced in the changes being reviewed (not pre-existing bugs).
139
+ 5. The author would likely fix if aware of them.
140
+ 6. Don't rely on unstated assumptions about the codebase or author's intent.
141
+ 7. Have provable impact on other parts of the code — it is not enough to speculate that a change may disrupt another part, you must identify the parts that are provably affected.
142
+ 8. Are clearly not intentional changes by the author.
143
+ 9. Be particularly careful with untrusted user input and follow the specific guidelines to review.
144
+
145
+ ## Untrusted User Input
146
+
147
+ 1. Be careful with open redirects, they must always be checked to only go to trusted domains (?next_page=...)
148
+ 2. Always flag SQL that is not parametrized
149
+ 3. In systems with user supplied URL input, http fetches always need to be protected against access to local resources (intercept DNS resolver!)
150
+ 4. Escape, don't sanitize if you have the option (eg: HTML escaping)
151
+
152
+ ## Comment guidelines
153
+
154
+ 1. Be clear about why the issue is a problem.
155
+ 2. Communicate severity appropriately - don't exaggerate.
156
+ 3. Be brief - at most 1 paragraph.
157
+ 4. Keep code snippets under 3 lines, wrapped in inline code or code blocks.
158
+ 5. Use \`\`\`suggestion blocks ONLY for concrete replacement code (minimal lines; no commentary inside the block). Preserve the exact leading whitespace of the replaced lines.
159
+ 6. Explicitly state scenarios/environments where the issue arises.
160
+ 7. Use a matter-of-fact tone - helpful AI assistant, not accusatory.
161
+ 8. Write for quick comprehension without close reading.
162
+ 9. Avoid excessive flattery or unhelpful phrases like "Great job...".
163
+
164
+ ## Review priorities
165
+
166
+ 1. Call out newly added dependencies explicitly and explain why they're needed.
167
+ 2. Prefer simple, direct solutions over wrappers or abstractions without clear value.
168
+ 3. Favor fail-fast behavior; avoid logging-and-continue patterns that hide errors.
169
+ 4. Prefer predictable production behavior; crashing is better than silent degradation.
170
+ 5. Treat back pressure handling as critical to system stability.
171
+ 6. Apply system-level thinking; flag changes that increase operational risk or on-call wakeups.
172
+ 7. Ensure that errors are always checked against codes or stable identifiers, never error messages.
173
+
174
+ ## Priority levels
175
+
176
+ Tag each finding with a priority level in the title:
177
+ - [P0] - Drop everything to fix. Blocking release/operations. Only for universal issues that do not depend on assumptions about inputs.
178
+ - [P1] - Urgent. Should be addressed in the next cycle.
179
+ - [P2] - Normal. To be fixed eventually.
180
+ - [P3] - Low. Nice to have.
181
+
182
+ ## Output format
183
+
184
+ Provide your findings in a clear, structured format:
185
+ 1. List each finding with its priority tag, file location, and explanation.
186
+ 2. Findings must reference locations that overlap with the actual diff — don't flag pre-existing code.
187
+ 3. Keep line references as short as possible (avoid ranges over 5-10 lines; pick the most suitable subrange).
188
+ 4. At the end, provide an overall verdict: "correct" (no blocking issues) or "needs attention" (has blocking issues).
189
+ 5. Ignore trivial style issues unless they obscure meaning or violate documented standards.
190
+ 6. Do not generate a full PR fix — only flag issues and optionally provide short suggestion blocks.
191
+
192
+ 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.`;
193
+
194
+ async function loadProjectReviewGuidelines(cwd: string): Promise<string | null> {
195
+ let currentDir = path.resolve(cwd);
196
+
197
+ while (true) {
198
+ const piDir = path.join(currentDir, ".pi");
199
+ const guidelinesPath = path.join(currentDir, "REVIEW_GUIDELINES.md");
200
+
201
+ const piStats = await fs.stat(piDir).catch(() => null);
202
+ if (piStats?.isDirectory()) {
203
+ const guidelineStats = await fs.stat(guidelinesPath).catch(() => null);
204
+ if (guidelineStats?.isFile()) {
205
+ try {
206
+ const content = await fs.readFile(guidelinesPath, "utf8");
207
+ const trimmed = content.trim();
208
+ return trimmed ? trimmed : null;
209
+ } catch {
210
+ return null;
211
+ }
212
+ }
213
+ return null;
214
+ }
215
+
216
+ const parentDir = path.dirname(currentDir);
217
+ if (parentDir === currentDir) {
218
+ return null;
219
+ }
220
+ currentDir = parentDir;
221
+ }
222
+ }
223
+
224
+ /**
225
+ * Get the merge base between HEAD and a branch
226
+ */
227
+ async function getMergeBase(
228
+ pi: ExtensionAPI,
229
+ branch: string,
230
+ ): Promise<string | null> {
231
+ try {
232
+ // First try to get the upstream tracking branch
233
+ const { stdout: upstream, code: upstreamCode } = await pi.exec("git", [
234
+ "rev-parse",
235
+ "--abbrev-ref",
236
+ `${branch}@{upstream}`,
237
+ ]);
238
+
239
+ if (upstreamCode === 0 && upstream.trim()) {
240
+ const { stdout: mergeBase, code } = await pi.exec("git", ["merge-base", "HEAD", upstream.trim()]);
241
+ if (code === 0 && mergeBase.trim()) {
242
+ return mergeBase.trim();
243
+ }
244
+ }
245
+
246
+ // Fall back to using the branch directly
247
+ const { stdout: mergeBase, code } = await pi.exec("git", ["merge-base", "HEAD", branch]);
248
+ if (code === 0 && mergeBase.trim()) {
249
+ return mergeBase.trim();
250
+ }
251
+
252
+ return null;
253
+ } catch {
254
+ return null;
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Get list of local branches
260
+ */
261
+ async function getLocalBranches(pi: ExtensionAPI): Promise<string[]> {
262
+ const { stdout, code } = await pi.exec("git", ["branch", "--format=%(refname:short)"]);
263
+ if (code !== 0) return [];
264
+ return stdout
265
+ .trim()
266
+ .split("\n")
267
+ .filter((b) => b.trim());
268
+ }
269
+
270
+ /**
271
+ * Get list of recent commits
272
+ */
273
+ async function getRecentCommits(pi: ExtensionAPI, limit: number = 10): Promise<Array<{ sha: string; title: string }>> {
274
+ const { stdout, code } = await pi.exec("git", ["log", `--oneline`, `-n`, `${limit}`]);
275
+ if (code !== 0) return [];
276
+
277
+ return stdout
278
+ .trim()
279
+ .split("\n")
280
+ .filter((line) => line.trim())
281
+ .map((line) => {
282
+ const [sha, ...rest] = line.trim().split(" ");
283
+ return { sha, title: rest.join(" ") };
284
+ });
285
+ }
286
+
287
+ /**
288
+ * Check if there are uncommitted changes (staged, unstaged, or untracked)
289
+ */
290
+ async function hasUncommittedChanges(pi: ExtensionAPI): Promise<boolean> {
291
+ const { stdout, code } = await pi.exec("git", ["status", "--porcelain"]);
292
+ return code === 0 && stdout.trim().length > 0;
293
+ }
294
+
295
+ /**
296
+ * Check if there are changes that would prevent switching branches
297
+ * (staged or unstaged changes to tracked files - untracked files are fine)
298
+ */
299
+ async function hasPendingChanges(pi: ExtensionAPI): Promise<boolean> {
300
+ // Check for staged or unstaged changes to tracked files
301
+ const { stdout, code } = await pi.exec("git", ["status", "--porcelain"]);
302
+ if (code !== 0) return false;
303
+
304
+ // Filter out untracked files (lines starting with ??)
305
+ const lines = stdout.trim().split("\n").filter((line) => line.trim());
306
+ const trackedChanges = lines.filter((line) => !line.startsWith("??"));
307
+ return trackedChanges.length > 0;
308
+ }
309
+
310
+ /**
311
+ * Parse a PR reference (URL or number) and return the PR number
312
+ */
313
+ function parsePrReference(ref: string): number | null {
314
+ const trimmed = ref.trim();
315
+
316
+ // Try as a number first
317
+ const num = parseInt(trimmed, 10);
318
+ if (!isNaN(num) && num > 0) {
319
+ return num;
320
+ }
321
+
322
+ // Try to extract from GitHub URL
323
+ // Formats: https://github.com/owner/repo/pull/123
324
+ // github.com/owner/repo/pull/123
325
+ const urlMatch = trimmed.match(/github\.com\/[^/]+\/[^/]+\/pull\/(\d+)/);
326
+ if (urlMatch) {
327
+ return parseInt(urlMatch[1], 10);
328
+ }
329
+
330
+ return null;
331
+ }
332
+
333
+ /**
334
+ * Get PR information from GitHub CLI
335
+ */
336
+ async function getPrInfo(pi: ExtensionAPI, prNumber: number): Promise<{ baseBranch: string; title: string; headBranch: string } | null> {
337
+ const { stdout, code } = await pi.exec("gh", [
338
+ "pr", "view", String(prNumber),
339
+ "--json", "baseRefName,title,headRefName",
340
+ ]);
341
+
342
+ if (code !== 0) return null;
343
+
344
+ try {
345
+ const data = JSON.parse(stdout);
346
+ return {
347
+ baseBranch: data.baseRefName,
348
+ title: data.title,
349
+ headBranch: data.headRefName,
350
+ };
351
+ } catch {
352
+ return null;
353
+ }
354
+ }
355
+
356
+ /**
357
+ * Checkout a PR using GitHub CLI
358
+ */
359
+ async function checkoutPr(pi: ExtensionAPI, prNumber: number): Promise<{ success: boolean; error?: string }> {
360
+ const { stdout, stderr, code } = await pi.exec("gh", ["pr", "checkout", String(prNumber)]);
361
+
362
+ if (code !== 0) {
363
+ return { success: false, error: stderr || stdout || "Failed to checkout PR" };
364
+ }
365
+
366
+ return { success: true };
367
+ }
368
+
369
+ /**
370
+ * Get the current branch name
371
+ */
372
+ async function getCurrentBranch(pi: ExtensionAPI): Promise<string | null> {
373
+ const { stdout, code } = await pi.exec("git", ["branch", "--show-current"]);
374
+ if (code === 0 && stdout.trim()) {
375
+ return stdout.trim();
376
+ }
377
+ return null;
378
+ }
379
+
380
+ /**
381
+ * Get the default branch (main or master)
382
+ */
383
+ async function getDefaultBranch(pi: ExtensionAPI): Promise<string> {
384
+ // Try to get from remote HEAD
385
+ const { stdout, code } = await pi.exec("git", ["symbolic-ref", "refs/remotes/origin/HEAD", "--short"]);
386
+ if (code === 0 && stdout.trim()) {
387
+ return stdout.trim().replace("origin/", "");
388
+ }
389
+
390
+ // Fall back to checking if main or master exists
391
+ const branches = await getLocalBranches(pi);
392
+ if (branches.includes("main")) return "main";
393
+ if (branches.includes("master")) return "master";
394
+
395
+ return "main"; // Default fallback
396
+ }
397
+
398
+ /**
399
+ * Build the review prompt based on target
400
+ */
401
+ async function buildReviewPrompt(pi: ExtensionAPI, target: ReviewTarget): Promise<string> {
402
+ switch (target.type) {
403
+ case "uncommitted":
404
+ return UNCOMMITTED_PROMPT;
405
+
406
+ case "baseBranch": {
407
+ const mergeBase = await getMergeBase(pi, target.branch);
408
+ if (mergeBase) {
409
+ return BASE_BRANCH_PROMPT_WITH_MERGE_BASE.replace(/{baseBranch}/g, target.branch).replace(
410
+ /{mergeBaseSha}/g,
411
+ mergeBase,
412
+ );
413
+ }
414
+ return BASE_BRANCH_PROMPT_FALLBACK.replace(/{branch}/g, target.branch);
415
+ }
416
+
417
+ case "commit":
418
+ if (target.title) {
419
+ return COMMIT_PROMPT_WITH_TITLE.replace("{sha}", target.sha).replace("{title}", target.title);
420
+ }
421
+ return COMMIT_PROMPT.replace("{sha}", target.sha);
422
+
423
+ case "custom":
424
+ return target.instructions;
425
+
426
+ case "pullRequest": {
427
+ const mergeBase = await getMergeBase(pi, target.baseBranch);
428
+ if (mergeBase) {
429
+ return PULL_REQUEST_PROMPT
430
+ .replace(/{prNumber}/g, String(target.prNumber))
431
+ .replace(/{title}/g, target.title)
432
+ .replace(/{baseBranch}/g, target.baseBranch)
433
+ .replace(/{mergeBaseSha}/g, mergeBase);
434
+ }
435
+ return PULL_REQUEST_PROMPT_FALLBACK
436
+ .replace(/{prNumber}/g, String(target.prNumber))
437
+ .replace(/{title}/g, target.title)
438
+ .replace(/{baseBranch}/g, target.baseBranch);
439
+ }
440
+
441
+ case "folder":
442
+ return FOLDER_REVIEW_PROMPT.replace("{paths}", target.paths.join(", "));
443
+ }
444
+ }
445
+
446
+ /**
447
+ * Get user-facing hint for the review target
448
+ */
449
+ function getUserFacingHint(target: ReviewTarget): string {
450
+ switch (target.type) {
451
+ case "uncommitted":
452
+ return "current changes";
453
+ case "baseBranch":
454
+ return `changes against '${target.branch}'`;
455
+ case "commit": {
456
+ const shortSha = target.sha.slice(0, 7);
457
+ return target.title ? `commit ${shortSha}: ${target.title}` : `commit ${shortSha}`;
458
+ }
459
+ case "custom":
460
+ return target.instructions.length > 40 ? target.instructions.slice(0, 37) + "..." : target.instructions;
461
+
462
+ case "pullRequest": {
463
+ const shortTitle = target.title.length > 30 ? target.title.slice(0, 27) + "..." : target.title;
464
+ return `PR #${target.prNumber}: ${shortTitle}`;
465
+ }
466
+
467
+ case "folder": {
468
+ const joined = target.paths.join(", ");
469
+ return joined.length > 40 ? `folders: ${joined.slice(0, 37)}...` : `folders: ${joined}`;
470
+ }
471
+ }
472
+ }
473
+
474
+ // Review preset options for the selector (keep this order stable)
475
+ const REVIEW_PRESETS = [
476
+ { value: "uncommitted", label: "Review uncommitted changes", description: "" },
477
+ { value: "baseBranch", label: "Review against a base branch", description: "(local)" },
478
+ { value: "commit", label: "Review a commit", description: "" },
479
+ { value: "pullRequest", label: "Review a pull request", description: "(GitHub PR)" },
480
+ { value: "folder", label: "Review a folder (or more)", description: "(snapshot, not diff)" },
481
+ { value: "custom", label: "Custom review instructions", description: "" },
482
+ ] as const;
483
+
484
+ export default function reviewExtension(pi: ExtensionAPI) {
485
+ pi.on("session_start", (_event, ctx) => {
486
+ applyReviewState(ctx);
487
+ });
488
+
489
+ pi.on("session_switch", (_event, ctx) => {
490
+ applyReviewState(ctx);
491
+ });
492
+
493
+ pi.on("session_tree", (_event, ctx) => {
494
+ applyReviewState(ctx);
495
+ });
496
+
497
+ /**
498
+ * Determine the smart default review type based on git state
499
+ */
500
+ async function getSmartDefault(): Promise<"uncommitted" | "baseBranch" | "commit"> {
501
+ // Priority 1: If there are uncommitted changes, default to reviewing them
502
+ if (await hasUncommittedChanges(pi)) {
503
+ return "uncommitted";
504
+ }
505
+
506
+ // Priority 2: If on a feature branch (not the default branch), default to PR-style review
507
+ const currentBranch = await getCurrentBranch(pi);
508
+ const defaultBranch = await getDefaultBranch(pi);
509
+ if (currentBranch && currentBranch !== defaultBranch) {
510
+ return "baseBranch";
511
+ }
512
+
513
+ // Priority 3: Default to reviewing a specific commit
514
+ return "commit";
515
+ }
516
+
517
+ /**
518
+ * Show the review preset selector
519
+ */
520
+ async function showReviewSelector(ctx: ExtensionContext): Promise<ReviewTarget | null> {
521
+ // Determine smart default (but keep the list order stable)
522
+ const smartDefault = await getSmartDefault();
523
+ const items: SelectItem[] = REVIEW_PRESETS.map((preset) => ({
524
+ value: preset.value,
525
+ label: preset.label,
526
+ description: preset.description,
527
+ }));
528
+ const smartDefaultIndex = items.findIndex((item) => item.value === smartDefault);
529
+
530
+ while (true) {
531
+ const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
532
+ const container = new Container();
533
+ container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
534
+ container.addChild(new Text(theme.fg("accent", theme.bold("Select a review preset"))));
535
+
536
+ const selectList = new SelectList(items, Math.min(items.length, 10), {
537
+ selectedPrefix: (text) => theme.fg("accent", text),
538
+ selectedText: (text) => theme.fg("accent", text),
539
+ description: (text) => theme.fg("muted", text),
540
+ scrollInfo: (text) => theme.fg("dim", text),
541
+ noMatch: (text) => theme.fg("warning", text),
542
+ });
543
+
544
+ // Preselect the smart default without reordering the list
545
+ if (smartDefaultIndex >= 0) {
546
+ selectList.setSelectedIndex(smartDefaultIndex);
547
+ }
548
+
549
+ selectList.onSelect = (item) => done(item.value);
550
+ selectList.onCancel = () => done(null);
551
+
552
+ container.addChild(selectList);
553
+ container.addChild(new Text(theme.fg("dim", "Press enter to confirm or esc to go back")));
554
+ container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
555
+
556
+ return {
557
+ render(width: number) {
558
+ return container.render(width);
559
+ },
560
+ invalidate() {
561
+ container.invalidate();
562
+ },
563
+ handleInput(data: string) {
564
+ selectList.handleInput(data);
565
+ tui.requestRender();
566
+ },
567
+ };
568
+ });
569
+
570
+ if (!result) return null;
571
+
572
+ // Handle each preset type
573
+ switch (result) {
574
+ case "uncommitted":
575
+ return { type: "uncommitted" };
576
+
577
+ case "baseBranch": {
578
+ const target = await showBranchSelector(ctx);
579
+ if (target) return target;
580
+ break;
581
+ }
582
+
583
+ case "commit": {
584
+ const target = await showCommitSelector(ctx);
585
+ if (target) return target;
586
+ break;
587
+ }
588
+
589
+ case "custom": {
590
+ const target = await showCustomInput(ctx);
591
+ if (target) return target;
592
+ break;
593
+ }
594
+
595
+ case "folder": {
596
+ const target = await showFolderInput(ctx);
597
+ if (target) return target;
598
+ break;
599
+ }
600
+
601
+ case "pullRequest": {
602
+ const target = await showPrInput(ctx);
603
+ if (target) return target;
604
+ break;
605
+ }
606
+
607
+ default:
608
+ return null;
609
+ }
610
+ }
611
+ }
612
+
613
+ /**
614
+ * Show branch selector for base branch review
615
+ */
616
+ async function showBranchSelector(ctx: ExtensionContext): Promise<ReviewTarget | null> {
617
+ const branches = await getLocalBranches(pi);
618
+ const currentBranch = await getCurrentBranch(pi);
619
+ const defaultBranch = await getDefaultBranch(pi);
620
+
621
+ // Never offer the current branch as a base branch (reviewing against itself is meaningless).
622
+ const candidateBranches = currentBranch ? branches.filter((b) => b !== currentBranch) : branches;
623
+
624
+ if (candidateBranches.length === 0) {
625
+ ctx.ui.notify(
626
+ currentBranch ? `No other branches found (current branch: ${currentBranch})` : "No branches found",
627
+ "error",
628
+ );
629
+ return null;
630
+ }
631
+
632
+ // Sort branches with default branch first
633
+ const sortedBranches = candidateBranches.sort((a, b) => {
634
+ if (a === defaultBranch) return -1;
635
+ if (b === defaultBranch) return 1;
636
+ return a.localeCompare(b);
637
+ });
638
+
639
+ const items: SelectItem[] = sortedBranches.map((branch) => ({
640
+ value: branch,
641
+ label: branch,
642
+ description: branch === defaultBranch ? "(default)" : "",
643
+ }));
644
+
645
+ const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
646
+ const container = new Container();
647
+ container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
648
+ container.addChild(new Text(theme.fg("accent", theme.bold("Select base branch"))));
649
+
650
+ const selectList = new SelectList(items, Math.min(items.length, 10), {
651
+ selectedPrefix: (text) => theme.fg("accent", text),
652
+ selectedText: (text) => theme.fg("accent", text),
653
+ description: (text) => theme.fg("muted", text),
654
+ scrollInfo: (text) => theme.fg("dim", text),
655
+ noMatch: (text) => theme.fg("warning", text),
656
+ });
657
+
658
+ // Enable search
659
+ selectList.searchable = true;
660
+
661
+ selectList.onSelect = (item) => done(item.value);
662
+ selectList.onCancel = () => done(null);
663
+
664
+ container.addChild(selectList);
665
+ container.addChild(new Text(theme.fg("dim", "Type to filter • enter to select • esc to cancel")));
666
+ container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
667
+
668
+ return {
669
+ render(width: number) {
670
+ return container.render(width);
671
+ },
672
+ invalidate() {
673
+ container.invalidate();
674
+ },
675
+ handleInput(data: string) {
676
+ selectList.handleInput(data);
677
+ tui.requestRender();
678
+ },
679
+ };
680
+ });
681
+
682
+ if (!result) return null;
683
+ return { type: "baseBranch", branch: result };
684
+ }
685
+
686
+ /**
687
+ * Show commit selector
688
+ */
689
+ async function showCommitSelector(ctx: ExtensionContext): Promise<ReviewTarget | null> {
690
+ const commits = await getRecentCommits(pi, 20);
691
+
692
+ if (commits.length === 0) {
693
+ ctx.ui.notify("No commits found", "error");
694
+ return null;
695
+ }
696
+
697
+ const items: SelectItem[] = commits.map((commit) => ({
698
+ value: commit.sha,
699
+ label: `${commit.sha.slice(0, 7)} ${commit.title}`,
700
+ description: "",
701
+ }));
702
+
703
+ const result = await ctx.ui.custom<{ sha: string; title: string } | null>((tui, theme, _kb, done) => {
704
+ const container = new Container();
705
+ container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
706
+ container.addChild(new Text(theme.fg("accent", theme.bold("Select commit to review"))));
707
+
708
+ const selectList = new SelectList(items, Math.min(items.length, 10), {
709
+ selectedPrefix: (text) => theme.fg("accent", text),
710
+ selectedText: (text) => theme.fg("accent", text),
711
+ description: (text) => theme.fg("muted", text),
712
+ scrollInfo: (text) => theme.fg("dim", text),
713
+ noMatch: (text) => theme.fg("warning", text),
714
+ });
715
+
716
+ // Enable search
717
+ selectList.searchable = true;
718
+
719
+ selectList.onSelect = (item) => {
720
+ const commit = commits.find((c) => c.sha === item.value);
721
+ if (commit) {
722
+ done(commit);
723
+ } else {
724
+ done(null);
725
+ }
726
+ };
727
+ selectList.onCancel = () => done(null);
728
+
729
+ container.addChild(selectList);
730
+ container.addChild(new Text(theme.fg("dim", "Type to filter • enter to select • esc to cancel")));
731
+ container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
732
+
733
+ return {
734
+ render(width: number) {
735
+ return container.render(width);
736
+ },
737
+ invalidate() {
738
+ container.invalidate();
739
+ },
740
+ handleInput(data: string) {
741
+ selectList.handleInput(data);
742
+ tui.requestRender();
743
+ },
744
+ };
745
+ });
746
+
747
+ if (!result) return null;
748
+ return { type: "commit", sha: result.sha, title: result.title };
749
+ }
750
+
751
+ /**
752
+ * Show custom instructions input
753
+ */
754
+ async function showCustomInput(ctx: ExtensionContext): Promise<ReviewTarget | null> {
755
+ const result = await ctx.ui.editor(
756
+ "Enter review instructions:",
757
+ "Review the code for security vulnerabilities and potential bugs...",
758
+ );
759
+
760
+ if (!result?.trim()) return null;
761
+ return { type: "custom", instructions: result.trim() };
762
+ }
763
+
764
+ function parseReviewPaths(value: string): string[] {
765
+ return value
766
+ .split(/\s+/)
767
+ .map((item) => item.trim())
768
+ .filter((item) => item.length > 0);
769
+ }
770
+
771
+ /**
772
+ * Show folder input
773
+ */
774
+ async function showFolderInput(ctx: ExtensionContext): Promise<ReviewTarget | null> {
775
+ const result = await ctx.ui.editor(
776
+ "Enter folders/files to review (space-separated or one per line):",
777
+ ".",
778
+ );
779
+
780
+ if (!result?.trim()) return null;
781
+ const paths = parseReviewPaths(result);
782
+ if (paths.length === 0) return null;
783
+
784
+ return { type: "folder", paths };
785
+ }
786
+
787
+ /**
788
+ * Show PR input and handle checkout
789
+ */
790
+ async function showPrInput(ctx: ExtensionContext): Promise<ReviewTarget | null> {
791
+ // First check for pending changes that would prevent branch switching
792
+ if (await hasPendingChanges(pi)) {
793
+ ctx.ui.notify("Cannot checkout PR: you have uncommitted changes. Please commit or stash them first.", "error");
794
+ return null;
795
+ }
796
+
797
+ // Get PR reference from user
798
+ const prRef = await ctx.ui.editor(
799
+ "Enter PR number or URL (e.g. 123 or https://github.com/owner/repo/pull/123):",
800
+ "",
801
+ );
802
+
803
+ if (!prRef?.trim()) return null;
804
+
805
+ const prNumber = parsePrReference(prRef);
806
+ if (!prNumber) {
807
+ ctx.ui.notify("Invalid PR reference. Enter a number or GitHub PR URL.", "error");
808
+ return null;
809
+ }
810
+
811
+ // Get PR info from GitHub
812
+ ctx.ui.notify(`Fetching PR #${prNumber} info...`, "info");
813
+ const prInfo = await getPrInfo(pi, prNumber);
814
+
815
+ if (!prInfo) {
816
+ ctx.ui.notify(`Could not find PR #${prNumber}. Make sure gh is authenticated and the PR exists.`, "error");
817
+ return null;
818
+ }
819
+
820
+ // Check again for pending changes (in case something changed)
821
+ if (await hasPendingChanges(pi)) {
822
+ ctx.ui.notify("Cannot checkout PR: you have uncommitted changes. Please commit or stash them first.", "error");
823
+ return null;
824
+ }
825
+
826
+ // Checkout the PR
827
+ ctx.ui.notify(`Checking out PR #${prNumber}...`, "info");
828
+ const checkoutResult = await checkoutPr(pi, prNumber);
829
+
830
+ if (!checkoutResult.success) {
831
+ ctx.ui.notify(`Failed to checkout PR: ${checkoutResult.error}`, "error");
832
+ return null;
833
+ }
834
+
835
+ ctx.ui.notify(`Checked out PR #${prNumber} (${prInfo.headBranch})`, "info");
836
+
837
+ return {
838
+ type: "pullRequest",
839
+ prNumber,
840
+ baseBranch: prInfo.baseBranch,
841
+ title: prInfo.title,
842
+ };
843
+ }
844
+
845
+ /**
846
+ * Execute the review
847
+ */
848
+ async function executeReview(ctx: ExtensionCommandContext, target: ReviewTarget, useFreshSession: boolean): Promise<void> {
849
+ // Check if we're already in a review
850
+ if (reviewOriginId) {
851
+ ctx.ui.notify("Already in a review. Use /end-review to finish first.", "warning");
852
+ return;
853
+ }
854
+
855
+ // Handle fresh session mode
856
+ if (useFreshSession) {
857
+ // Store current position (where we'll return to)
858
+ const originId = ctx.sessionManager.getLeafId() ?? undefined;
859
+ if (!originId) {
860
+ ctx.ui.notify("Failed to determine review origin. Try again from a session with messages.", "error");
861
+ return;
862
+ }
863
+ reviewOriginId = originId;
864
+
865
+ // Keep a local copy so session_tree events during navigation don't wipe it
866
+ const lockedOriginId = originId;
867
+
868
+ // Find the first user message in the session
869
+ const entries = ctx.sessionManager.getEntries();
870
+ const firstUserMessage = entries.find(
871
+ (e) => e.type === "message" && e.message.role === "user",
872
+ );
873
+
874
+ if (!firstUserMessage) {
875
+ ctx.ui.notify("No user message found in session", "error");
876
+ reviewOriginId = undefined;
877
+ return;
878
+ }
879
+
880
+ // Navigate to first user message to create a new branch from that point
881
+ // Label it as "code-review" so it's visible in the tree
882
+ try {
883
+ const result = await ctx.navigateTree(firstUserMessage.id, { summarize: false, label: "code-review" });
884
+ if (result.cancelled) {
885
+ reviewOriginId = undefined;
886
+ return;
887
+ }
888
+ } catch (error) {
889
+ // Clean up state if navigation fails
890
+ reviewOriginId = undefined;
891
+ ctx.ui.notify(`Failed to start review: ${error instanceof Error ? error.message : String(error)}`, "error");
892
+ return;
893
+ }
894
+
895
+ // Restore origin after navigation events (session_tree can reset it)
896
+ reviewOriginId = lockedOriginId;
897
+
898
+ // Clear the editor (navigating to user message fills it with the message text)
899
+ ctx.ui.setEditorText("");
900
+
901
+ // Show widget indicating review is active
902
+ setReviewWidget(ctx, true);
903
+
904
+ // Persist review state so tree navigation can restore/reset it
905
+ pi.appendEntry(REVIEW_STATE_TYPE, { active: true, originId: lockedOriginId });
906
+ }
907
+
908
+ const prompt = await buildReviewPrompt(pi, target);
909
+ const hint = getUserFacingHint(target);
910
+ const projectGuidelines = await loadProjectReviewGuidelines(ctx.cwd);
911
+
912
+ // Combine the review rubric with the specific prompt
913
+ let fullPrompt = `${REVIEW_RUBRIC}\n\n---\n\nPlease perform a code review with the following focus:\n\n${prompt}`;
914
+
915
+ if (projectGuidelines) {
916
+ fullPrompt += `\n\nThis project has additional instructions for code reviews:\n\n${projectGuidelines}`;
917
+ }
918
+
919
+ const modeHint = useFreshSession ? " (fresh session)" : "";
920
+ ctx.ui.notify(`Starting review: ${hint}${modeHint}`, "info");
921
+
922
+ // Send as a user message that triggers a turn
923
+ pi.sendUserMessage(fullPrompt);
924
+ }
925
+
926
+ /**
927
+ * Parse command arguments for direct invocation
928
+ * Returns the target or a special marker for PR that needs async handling
929
+ */
930
+ function parseArgs(args: string | undefined): ReviewTarget | { type: "pr"; ref: string } | null {
931
+ if (!args?.trim()) return null;
932
+
933
+ const parts = args.trim().split(/\s+/);
934
+ const subcommand = parts[0]?.toLowerCase();
935
+
936
+ switch (subcommand) {
937
+ case "uncommitted":
938
+ return { type: "uncommitted" };
939
+
940
+ case "branch": {
941
+ const branch = parts[1];
942
+ if (!branch) return null;
943
+ return { type: "baseBranch", branch };
944
+ }
945
+
946
+ case "commit": {
947
+ const sha = parts[1];
948
+ if (!sha) return null;
949
+ const title = parts.slice(2).join(" ") || undefined;
950
+ return { type: "commit", sha, title };
951
+ }
952
+
953
+ case "custom": {
954
+ const instructions = parts.slice(1).join(" ");
955
+ if (!instructions) return null;
956
+ return { type: "custom", instructions };
957
+ }
958
+
959
+ case "folder": {
960
+ const paths = parseReviewPaths(parts.slice(1).join(" "));
961
+ if (paths.length === 0) return null;
962
+ return { type: "folder", paths };
963
+ }
964
+
965
+ case "pr": {
966
+ const ref = parts[1];
967
+ if (!ref) return null;
968
+ return { type: "pr", ref };
969
+ }
970
+
971
+ default:
972
+ return null;
973
+ }
974
+ }
975
+
976
+ /**
977
+ * Handle PR checkout and return a ReviewTarget (or null on failure)
978
+ */
979
+ async function handlePrCheckout(ctx: ExtensionContext, ref: string): Promise<ReviewTarget | null> {
980
+ // First check for pending changes
981
+ if (await hasPendingChanges(pi)) {
982
+ ctx.ui.notify("Cannot checkout PR: you have uncommitted changes. Please commit or stash them first.", "error");
983
+ return null;
984
+ }
985
+
986
+ const prNumber = parsePrReference(ref);
987
+ if (!prNumber) {
988
+ ctx.ui.notify("Invalid PR reference. Enter a number or GitHub PR URL.", "error");
989
+ return null;
990
+ }
991
+
992
+ // Get PR info
993
+ ctx.ui.notify(`Fetching PR #${prNumber} info...`, "info");
994
+ const prInfo = await getPrInfo(pi, prNumber);
995
+
996
+ if (!prInfo) {
997
+ ctx.ui.notify(`Could not find PR #${prNumber}. Make sure gh is authenticated and the PR exists.`, "error");
998
+ return null;
999
+ }
1000
+
1001
+ // Checkout the PR
1002
+ ctx.ui.notify(`Checking out PR #${prNumber}...`, "info");
1003
+ const checkoutResult = await checkoutPr(pi, prNumber);
1004
+
1005
+ if (!checkoutResult.success) {
1006
+ ctx.ui.notify(`Failed to checkout PR: ${checkoutResult.error}`, "error");
1007
+ return null;
1008
+ }
1009
+
1010
+ ctx.ui.notify(`Checked out PR #${prNumber} (${prInfo.headBranch})`, "info");
1011
+
1012
+ return {
1013
+ type: "pullRequest",
1014
+ prNumber,
1015
+ baseBranch: prInfo.baseBranch,
1016
+ title: prInfo.title,
1017
+ };
1018
+ }
1019
+
1020
+ // Register the /review command
1021
+ pi.registerCommand("review", {
1022
+ description: "Review code changes (PR, uncommitted, branch, commit, folder, or custom)",
1023
+ handler: async (args, ctx) => {
1024
+ if (!ctx.hasUI) {
1025
+ ctx.ui.notify("Review requires interactive mode", "error");
1026
+ return;
1027
+ }
1028
+
1029
+ // Check if we're already in a review
1030
+ if (reviewOriginId) {
1031
+ ctx.ui.notify("Already in a review. Use /end-review to finish first.", "warning");
1032
+ return;
1033
+ }
1034
+
1035
+ // Check if we're in a git repository
1036
+ const { code } = await pi.exec("git", ["rev-parse", "--git-dir"]);
1037
+ if (code !== 0) {
1038
+ ctx.ui.notify("Not a git repository", "error");
1039
+ return;
1040
+ }
1041
+
1042
+ // Try to parse direct arguments
1043
+ let target: ReviewTarget | null = null;
1044
+ let fromSelector = false;
1045
+ const parsed = parseArgs(args);
1046
+
1047
+ if (parsed) {
1048
+ if (parsed.type === "pr") {
1049
+ // Handle PR checkout (async operation)
1050
+ target = await handlePrCheckout(ctx, parsed.ref);
1051
+ if (!target) {
1052
+ ctx.ui.notify("PR review failed. Returning to review menu.", "warning");
1053
+ }
1054
+ } else {
1055
+ target = parsed;
1056
+ }
1057
+ }
1058
+
1059
+ // If no args or invalid args, show selector
1060
+ if (!target) {
1061
+ fromSelector = true;
1062
+ }
1063
+
1064
+ while (true) {
1065
+ if (!target && fromSelector) {
1066
+ target = await showReviewSelector(ctx);
1067
+ }
1068
+
1069
+ if (!target) {
1070
+ ctx.ui.notify("Review cancelled", "info");
1071
+ return;
1072
+ }
1073
+
1074
+ // Determine if we should use fresh session mode
1075
+ // Check if this is a new session (no messages yet)
1076
+ const entries = ctx.sessionManager.getEntries();
1077
+ const messageCount = entries.filter((e) => e.type === "message").length;
1078
+
1079
+ let useFreshSession = false;
1080
+
1081
+ if (messageCount > 0) {
1082
+ // Existing session - ask user which mode they want
1083
+ const choice = await ctx.ui.select("Start review in:", ["Empty branch", "Current session"]);
1084
+
1085
+ if (choice === undefined) {
1086
+ if (fromSelector) {
1087
+ target = null;
1088
+ continue;
1089
+ }
1090
+ ctx.ui.notify("Review cancelled", "info");
1091
+ return;
1092
+ }
1093
+
1094
+ useFreshSession = choice === "Empty branch";
1095
+ }
1096
+ // If messageCount === 0, useFreshSession stays false (current session mode)
1097
+
1098
+ await executeReview(ctx, target, useFreshSession);
1099
+ return;
1100
+ }
1101
+ },
1102
+ });
1103
+
1104
+ // Custom prompt for review summaries - focuses on preserving actionable findings
1105
+ const REVIEW_SUMMARY_PROMPT = `We are leaving a code-review branch and returning to the main coding branch.
1106
+ Create a structured handoff that can be used immediately to implement fixes.
1107
+
1108
+ You MUST summarize the review that happened in this branch so findings can be acted on.
1109
+ Do not omit findings: include every actionable issue that was identified.
1110
+
1111
+ Required sections (in order):
1112
+
1113
+ ## Review Scope
1114
+ - What was reviewed (files/paths, changes, and scope)
1115
+
1116
+ ## Verdict
1117
+ - "correct" or "needs attention"
1118
+
1119
+ ## Findings
1120
+ For EACH finding, include:
1121
+ - Priority tag ([P0]..[P3]) and short title
1122
+ - File location (\`path/to/file.ext:line\`)
1123
+ - Why it matters (brief)
1124
+ - What should change (brief, actionable)
1125
+
1126
+ ## Fix Queue
1127
+ 1. Ordered implementation checklist (highest priority first)
1128
+
1129
+ ## Constraints & Preferences
1130
+ - Any constraints or preferences mentioned during review
1131
+ - Or "(none)"
1132
+
1133
+ Preserve exact file paths, function names, and error messages where available.`;
1134
+
1135
+ const REVIEW_FIX_FINDINGS_PROMPT = `Use the latest review summary in this session and implement the review findings now.
1136
+
1137
+ Instructions:
1138
+ 1. Treat the summary's Findings/Fix Queue as a checklist.
1139
+ 2. Fix in priority order: P0, P1, then P2 (include P3 if quick and safe).
1140
+ 3. If a finding is invalid/already fixed/not possible right now, briefly explain why and continue.
1141
+ 4. Run relevant tests/checks for touched code where practical.
1142
+ 5. End with: fixed items, deferred/skipped items (with reasons), and verification results.`;
1143
+
1144
+ type EndReviewAction = "returnOnly" | "returnAndFix" | "returnAndSummarize";
1145
+
1146
+ function getActiveReviewOrigin(ctx: ExtensionContext): string | undefined {
1147
+ if (reviewOriginId) {
1148
+ return reviewOriginId;
1149
+ }
1150
+
1151
+ const state = getReviewState(ctx);
1152
+ if (state?.active && state.originId) {
1153
+ reviewOriginId = state.originId;
1154
+ return reviewOriginId;
1155
+ }
1156
+
1157
+ if (state?.active) {
1158
+ setReviewWidget(ctx, false);
1159
+ pi.appendEntry(REVIEW_STATE_TYPE, { active: false });
1160
+ ctx.ui.notify("Review state was missing origin info; cleared review status.", "warning");
1161
+ }
1162
+
1163
+ return undefined;
1164
+ }
1165
+
1166
+ function clearReviewState(ctx: ExtensionContext) {
1167
+ setReviewWidget(ctx, false);
1168
+ reviewOriginId = undefined;
1169
+ pi.appendEntry(REVIEW_STATE_TYPE, { active: false });
1170
+ }
1171
+
1172
+ async function runEndReview(ctx: ExtensionCommandContext): Promise<void> {
1173
+ if (!ctx.hasUI) {
1174
+ ctx.ui.notify("End-review requires interactive mode", "error");
1175
+ return;
1176
+ }
1177
+
1178
+ if (endReviewInProgress) {
1179
+ ctx.ui.notify("/end-review is already running", "info");
1180
+ return;
1181
+ }
1182
+
1183
+ const originId = getActiveReviewOrigin(ctx);
1184
+ if (!originId) {
1185
+ if (!getReviewState(ctx)?.active) {
1186
+ ctx.ui.notify("Not in a review branch (use /review first, or review was started in current session mode)", "info");
1187
+ }
1188
+ return;
1189
+ }
1190
+
1191
+ endReviewInProgress = true;
1192
+ try {
1193
+ const choice = await ctx.ui.select("Finish review:", [
1194
+ "Return only",
1195
+ "Return and fix findings",
1196
+ "Return and summarize",
1197
+ ]);
1198
+
1199
+ if (choice === undefined) {
1200
+ ctx.ui.notify("Cancelled. Use /end-review to try again.", "info");
1201
+ return;
1202
+ }
1203
+
1204
+ const action: EndReviewAction =
1205
+ choice === "Return and fix findings"
1206
+ ? "returnAndFix"
1207
+ : choice === "Return and summarize"
1208
+ ? "returnAndSummarize"
1209
+ : "returnOnly";
1210
+ if (action === "returnOnly") {
1211
+ try {
1212
+ const result = await ctx.navigateTree(originId, { summarize: false });
1213
+ if (result.cancelled) {
1214
+ ctx.ui.notify("Navigation cancelled. Use /end-review to try again.", "info");
1215
+ return;
1216
+ }
1217
+ } catch (error) {
1218
+ ctx.ui.notify(`Failed to return: ${error instanceof Error ? error.message : String(error)}`, "error");
1219
+ return;
1220
+ }
1221
+
1222
+ clearReviewState(ctx);
1223
+ ctx.ui.notify("Review complete! Returned to original position.", "info");
1224
+ return;
1225
+ }
1226
+
1227
+ const summaryResult = await ctx.ui.custom<{ cancelled: boolean; error?: string } | null>((tui, theme, _kb, done) => {
1228
+ const loader = new BorderedLoader(tui, theme, "Returning and summarizing review branch...");
1229
+ loader.onAbort = () => done(null);
1230
+
1231
+ ctx.navigateTree(originId, {
1232
+ summarize: true,
1233
+ customInstructions: REVIEW_SUMMARY_PROMPT,
1234
+ replaceInstructions: true,
1235
+ })
1236
+ .then(done)
1237
+ .catch((err) => done({ cancelled: false, error: err instanceof Error ? err.message : String(err) }));
1238
+
1239
+ return loader;
1240
+ });
1241
+
1242
+ if (summaryResult === null) {
1243
+ ctx.ui.notify("Summarization cancelled. Use /end-review to try again.", "info");
1244
+ return;
1245
+ }
1246
+
1247
+ if (summaryResult.error) {
1248
+ ctx.ui.notify(`Summarization failed: ${summaryResult.error}`, "error");
1249
+ return;
1250
+ }
1251
+
1252
+ if (summaryResult.cancelled) {
1253
+ ctx.ui.notify("Navigation cancelled. Use /end-review to try again.", "info");
1254
+ return;
1255
+ }
1256
+
1257
+ clearReviewState(ctx);
1258
+
1259
+ if (action === "returnAndSummarize") {
1260
+ if (!ctx.ui.getEditorText().trim()) {
1261
+ ctx.ui.setEditorText("Act on the review findings");
1262
+ }
1263
+ ctx.ui.notify("Review complete! Returned and summarized.", "info");
1264
+ return;
1265
+ }
1266
+
1267
+ pi.sendUserMessage(REVIEW_FIX_FINDINGS_PROMPT, { deliverAs: "followUp" });
1268
+ ctx.ui.notify("Review complete! Returned and queued a follow-up to fix findings.", "info");
1269
+ } finally {
1270
+ endReviewInProgress = false;
1271
+ }
1272
+ }
1273
+
1274
+ // Register the /end-review command
1275
+ pi.registerCommand("end-review", {
1276
+ description: "Complete review and return to original position",
1277
+ handler: async (_args, ctx) => {
1278
+ await runEndReview(ctx);
1279
+ },
1280
+ });
1281
+ }