@ia-ccun/code-agent-cli 0.0.53 → 0.0.54-beta.3

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 (45) hide show
  1. package/bin/cli.js +97 -69
  2. package/config/agent/extensions/aicode-advisor.ts +187 -64
  3. package/config/agent/extensions/aicode-banner.ts +101 -0
  4. package/config/agent/extensions/aicode-btw.ts +619 -182
  5. package/config/agent/extensions/aicode-cd.ts +246 -63
  6. package/config/agent/extensions/aicode-pets.ts +0 -11
  7. package/config/agent/extensions/aicode-plan.ts +2 -0
  8. package/config/agent/extensions/aicode-safety-guard.ts +3 -0
  9. package/config/agent/extensions/aicode-tips.ts +1 -16
  10. package/config/agent/extensions/context-monitor.ts +30 -15
  11. package/config/agent/extensions/context.ts +132 -60
  12. package/config/agent/extensions/custom-footer.ts +0 -5
  13. package/config/agent/extensions/custom.ts +41 -29
  14. package/config/agent/extensions/error-monitor.ts +37 -33
  15. package/config/agent/extensions/goal.ts +8 -6
  16. package/config/agent/extensions/msg-debug.ts +103 -112
  17. package/config/agent/extensions/review.ts +365 -164
  18. package/config/agent/extensions/working-msg.ts +2 -4
  19. package/dist/banner.d.ts.map +1 -1
  20. package/dist/banner.js +8 -1
  21. package/dist/banner.js.map +1 -1
  22. package/dist/config-loader.d.ts.map +1 -1
  23. package/dist/config-loader.js +9 -2
  24. package/dist/config-loader.js.map +1 -1
  25. package/package.json +6 -7
  26. package/scripts/regression/README.md +224 -0
  27. package/scripts/regression/cases/01-version-and-deps.sh +94 -0
  28. package/scripts/regression/cases/02-extension-loading.sh +130 -0
  29. package/scripts/regression/cases/03-rpc-protocol.sh +259 -0
  30. package/scripts/regression/cases/04-prompt-stream.sh +130 -0
  31. package/scripts/regression/cases/05-slash-commands-and-skills.sh +119 -0
  32. package/scripts/regression/cases/06-tool-execution.sh +115 -0
  33. package/scripts/regression/cases/07-pi-new-features.sh +141 -0
  34. package/scripts/regression/lib/common.sh +287 -0
  35. package/scripts/regression/results/.last_pid +1 -0
  36. package/scripts/regression/results/.last_workdir +1 -0
  37. package/scripts/regression/results/02-extension-loading.log +21 -0
  38. package/scripts/regression/results/03-prompt-stream.log +16 -0
  39. package/scripts/regression/results/03-rpc-protocol.log +27 -0
  40. package/scripts/regression/results/04-slash-commands-and-skills.log +11 -0
  41. package/scripts/regression/results/05-tool-execution.log +8 -0
  42. package/scripts/regression/results/06-pi-new-features.log +27 -0
  43. package/scripts/regression/run-all.sh +187 -0
  44. package/config/agent/bin/fd.exe +0 -0
  45. package/config/agent/bin/rg.exe +0 -0
@@ -27,9 +27,18 @@
27
27
  * Note: PR review requires a clean working tree (no uncommitted changes to tracked files).
28
28
  */
29
29
 
30
- import type { ExtensionAPI, ExtensionContext, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
30
+ import type {
31
+ ExtensionAPI,
32
+ ExtensionContext,
33
+ ExtensionCommandContext,
34
+ } from "@earendil-works/pi-coding-agent";
31
35
  import { DynamicBorder, BorderedLoader } from "@earendil-works/pi-coding-agent";
32
- import { Container, type SelectItem, SelectList, Text } from "@earendil-works/pi-tui";
36
+ import {
37
+ Container,
38
+ type SelectItem,
39
+ SelectList,
40
+ Text,
41
+ } from "@earendil-works/pi-tui";
33
42
  import path from "node:path";
34
43
  import { promises as fs } from "node:fs";
35
44
 
@@ -54,7 +63,11 @@ function setReviewWidget(ctx: ExtensionContext, active: boolean) {
54
63
  }
55
64
 
56
65
  ctx.ui.setWidget("review", (_tui, theme) => {
57
- const text = new Text(theme.fg("warning", "Review session active, return with /end-review"), 0, 0);
66
+ const text = new Text(
67
+ theme.fg("warning", "Review session active, return with /end-review"),
68
+ 0,
69
+ 0,
70
+ );
58
71
  return {
59
72
  render(width: number) {
60
73
  return text.render(width);
@@ -107,18 +120,19 @@ const BASE_BRANCH_PROMPT_WITH_MERGE_BASE =
107
120
  "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.";
108
121
 
109
122
  const BASE_BRANCH_PROMPT_FALLBACK =
110
- "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.";
123
+ '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.';
111
124
 
112
125
  const COMMIT_PROMPT_WITH_TITLE =
113
126
  'Review the code changes introduced by commit {sha} ("{title}"). Provide prioritized, actionable findings.';
114
127
 
115
- const COMMIT_PROMPT = "Review the code changes introduced by commit {sha}. Provide prioritized, actionable findings.";
128
+ const COMMIT_PROMPT =
129
+ "Review the code changes introduced by commit {sha}. Provide prioritized, actionable findings.";
116
130
 
117
131
  const PULL_REQUEST_PROMPT =
118
- '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.';
132
+ "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.";
119
133
 
120
134
  const PULL_REQUEST_PROMPT_FALLBACK =
121
- '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.';
135
+ "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.";
122
136
 
123
137
  const FOLDER_REVIEW_PROMPT =
124
138
  "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.";
@@ -192,7 +206,9 @@ Provide your findings in a clear, structured format:
192
206
 
193
207
  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.`;
194
208
 
195
- async function loadProjectReviewGuidelines(cwd: string): Promise<string | null> {
209
+ async function loadProjectReviewGuidelines(
210
+ cwd: string,
211
+ ): Promise<string | null> {
196
212
  let currentDir = path.resolve(cwd);
197
213
 
198
214
  while (true) {
@@ -238,14 +254,22 @@ async function getMergeBase(
238
254
  ]);
239
255
 
240
256
  if (upstreamCode === 0 && upstream.trim()) {
241
- const { stdout: mergeBase, code } = await pi.exec("git", ["merge-base", "HEAD", upstream.trim()]);
257
+ const { stdout: mergeBase, code } = await pi.exec("git", [
258
+ "merge-base",
259
+ "HEAD",
260
+ upstream.trim(),
261
+ ]);
242
262
  if (code === 0 && mergeBase.trim()) {
243
263
  return mergeBase.trim();
244
264
  }
245
265
  }
246
266
 
247
267
  // Fall back to using the branch directly
248
- const { stdout: mergeBase, code } = await pi.exec("git", ["merge-base", "HEAD", branch]);
268
+ const { stdout: mergeBase, code } = await pi.exec("git", [
269
+ "merge-base",
270
+ "HEAD",
271
+ branch,
272
+ ]);
249
273
  if (code === 0 && mergeBase.trim()) {
250
274
  return mergeBase.trim();
251
275
  }
@@ -260,7 +284,10 @@ async function getMergeBase(
260
284
  * Get list of local branches
261
285
  */
262
286
  async function getLocalBranches(pi: ExtensionAPI): Promise<string[]> {
263
- const { stdout, code } = await pi.exec("git", ["branch", "--format=%(refname:short)"]);
287
+ const { stdout, code } = await pi.exec("git", [
288
+ "branch",
289
+ "--format=%(refname:short)",
290
+ ]);
264
291
  if (code !== 0) return [];
265
292
  return stdout
266
293
  .trim()
@@ -271,8 +298,16 @@ async function getLocalBranches(pi: ExtensionAPI): Promise<string[]> {
271
298
  /**
272
299
  * Get list of recent commits
273
300
  */
274
- async function getRecentCommits(pi: ExtensionAPI, limit: number = 10): Promise<Array<{ sha: string; title: string }>> {
275
- const { stdout, code } = await pi.exec("git", ["log", `--oneline`, `-n`, `${limit}`]);
301
+ async function getRecentCommits(
302
+ pi: ExtensionAPI,
303
+ limit: number = 10,
304
+ ): Promise<Array<{ sha: string; title: string }>> {
305
+ const { stdout, code } = await pi.exec("git", [
306
+ "log",
307
+ `--oneline`,
308
+ `-n`,
309
+ `${limit}`,
310
+ ]);
276
311
  if (code !== 0) return [];
277
312
 
278
313
  return stdout
@@ -303,7 +338,10 @@ async function hasPendingChanges(pi: ExtensionAPI): Promise<boolean> {
303
338
  if (code !== 0) return false;
304
339
 
305
340
  // Filter out untracked files (lines starting with ??)
306
- const lines = stdout.trim().split("\n").filter((line) => line.trim());
341
+ const lines = stdout
342
+ .trim()
343
+ .split("\n")
344
+ .filter((line) => line.trim());
307
345
  const trackedChanges = lines.filter((line) => !line.startsWith("??"));
308
346
  return trackedChanges.length > 0;
309
347
  }
@@ -334,10 +372,16 @@ function parsePrReference(ref: string): number | null {
334
372
  /**
335
373
  * Get PR information from GitHub CLI
336
374
  */
337
- async function getPrInfo(pi: ExtensionAPI, prNumber: number): Promise<{ baseBranch: string; title: string; headBranch: string } | null> {
375
+ async function getPrInfo(
376
+ pi: ExtensionAPI,
377
+ prNumber: number,
378
+ ): Promise<{ baseBranch: string; title: string; headBranch: string } | null> {
338
379
  const { stdout, code } = await pi.exec("gh", [
339
- "pr", "view", String(prNumber),
340
- "--json", "baseRefName,title,headRefName",
380
+ "pr",
381
+ "view",
382
+ String(prNumber),
383
+ "--json",
384
+ "baseRefName,title,headRefName",
341
385
  ]);
342
386
 
343
387
  if (code !== 0) return null;
@@ -357,8 +401,15 @@ async function getPrInfo(pi: ExtensionAPI, prNumber: number): Promise<{ baseBran
357
401
  /**
358
402
  * Checkout a PR using GitHub CLI
359
403
  */
360
- async function checkoutPr(pi: ExtensionAPI, prNumber: number): Promise<{ success: boolean; error?: string }> {
361
- const { stdout, stderr, code } = await pi.exec("gh", ["pr", "checkout", String(prNumber)]);
404
+ async function checkoutPr(
405
+ pi: ExtensionAPI,
406
+ prNumber: number,
407
+ ): Promise<{ success: boolean; error?: string }> {
408
+ const { stdout, stderr, code } = await pi.exec("gh", [
409
+ "pr",
410
+ "checkout",
411
+ String(prNumber),
412
+ ]);
362
413
 
363
414
  if (code !== 0) {
364
415
  return { success: false, error: stderr || stdout || "Failed to checkout PR" };
@@ -383,7 +434,11 @@ async function getCurrentBranch(pi: ExtensionAPI): Promise<string | null> {
383
434
  */
384
435
  async function getDefaultBranch(pi: ExtensionAPI): Promise<string> {
385
436
  // Try to get from remote HEAD
386
- const { stdout, code } = await pi.exec("git", ["symbolic-ref", "refs/remotes/origin/HEAD", "--short"]);
437
+ const { stdout, code } = await pi.exec("git", [
438
+ "symbolic-ref",
439
+ "refs/remotes/origin/HEAD",
440
+ "--short",
441
+ ]);
387
442
  if (code === 0 && stdout.trim()) {
388
443
  return stdout.trim().replace("origin/", "");
389
444
  }
@@ -399,7 +454,10 @@ async function getDefaultBranch(pi: ExtensionAPI): Promise<string> {
399
454
  /**
400
455
  * Build the review prompt based on target
401
456
  */
402
- async function buildReviewPrompt(pi: ExtensionAPI, target: ReviewTarget): Promise<string> {
457
+ async function buildReviewPrompt(
458
+ pi: ExtensionAPI,
459
+ target: ReviewTarget,
460
+ ): Promise<string> {
403
461
  switch (target.type) {
404
462
  case "uncommitted":
405
463
  return UNCOMMITTED_PROMPT;
@@ -407,17 +465,20 @@ async function buildReviewPrompt(pi: ExtensionAPI, target: ReviewTarget): Promis
407
465
  case "baseBranch": {
408
466
  const mergeBase = await getMergeBase(pi, target.branch);
409
467
  if (mergeBase) {
410
- return BASE_BRANCH_PROMPT_WITH_MERGE_BASE.replace(/{baseBranch}/g, target.branch).replace(
411
- /{mergeBaseSha}/g,
412
- mergeBase,
413
- );
468
+ return BASE_BRANCH_PROMPT_WITH_MERGE_BASE.replace(
469
+ /{baseBranch}/g,
470
+ target.branch,
471
+ ).replace(/{mergeBaseSha}/g, mergeBase);
414
472
  }
415
473
  return BASE_BRANCH_PROMPT_FALLBACK.replace(/{branch}/g, target.branch);
416
474
  }
417
475
 
418
476
  case "commit":
419
477
  if (target.title) {
420
- return COMMIT_PROMPT_WITH_TITLE.replace("{sha}", target.sha).replace("{title}", target.title);
478
+ return COMMIT_PROMPT_WITH_TITLE.replace("{sha}", target.sha).replace(
479
+ "{title}",
480
+ target.title,
481
+ );
421
482
  }
422
483
  return COMMIT_PROMPT.replace("{sha}", target.sha);
423
484
 
@@ -427,14 +488,15 @@ async function buildReviewPrompt(pi: ExtensionAPI, target: ReviewTarget): Promis
427
488
  case "pullRequest": {
428
489
  const mergeBase = await getMergeBase(pi, target.baseBranch);
429
490
  if (mergeBase) {
430
- return PULL_REQUEST_PROMPT
431
- .replace(/{prNumber}/g, String(target.prNumber))
491
+ return PULL_REQUEST_PROMPT.replace(/{prNumber}/g, String(target.prNumber))
432
492
  .replace(/{title}/g, target.title)
433
493
  .replace(/{baseBranch}/g, target.baseBranch)
434
494
  .replace(/{mergeBaseSha}/g, mergeBase);
435
495
  }
436
- return PULL_REQUEST_PROMPT_FALLBACK
437
- .replace(/{prNumber}/g, String(target.prNumber))
496
+ return PULL_REQUEST_PROMPT_FALLBACK.replace(
497
+ /{prNumber}/g,
498
+ String(target.prNumber),
499
+ )
438
500
  .replace(/{title}/g, target.title)
439
501
  .replace(/{baseBranch}/g, target.baseBranch);
440
502
  }
@@ -455,19 +517,26 @@ function getUserFacingHint(target: ReviewTarget): string {
455
517
  return `changes against '${target.branch}'`;
456
518
  case "commit": {
457
519
  const shortSha = target.sha.slice(0, 7);
458
- return target.title ? `commit ${shortSha}: ${target.title}` : `commit ${shortSha}`;
520
+ return target.title
521
+ ? `commit ${shortSha}: ${target.title}`
522
+ : `commit ${shortSha}`;
459
523
  }
460
524
  case "custom":
461
- return target.instructions.length > 40 ? target.instructions.slice(0, 37) + "..." : target.instructions;
525
+ return target.instructions.length > 40
526
+ ? target.instructions.slice(0, 37) + "..."
527
+ : target.instructions;
462
528
 
463
529
  case "pullRequest": {
464
- const shortTitle = target.title.length > 30 ? target.title.slice(0, 27) + "..." : target.title;
530
+ const shortTitle =
531
+ target.title.length > 30 ? target.title.slice(0, 27) + "..." : target.title;
465
532
  return `PR #${target.prNumber}: ${shortTitle}`;
466
533
  }
467
534
 
468
535
  case "folder": {
469
536
  const joined = target.paths.join(", ");
470
- return joined.length > 40 ? `folders: ${joined.slice(0, 37)}...` : `folders: ${joined}`;
537
+ return joined.length > 40
538
+ ? `folders: ${joined.slice(0, 37)}...`
539
+ : `folders: ${joined}`;
471
540
  }
472
541
  }
473
542
  }
@@ -475,10 +544,22 @@ function getUserFacingHint(target: ReviewTarget): string {
475
544
  // Review preset options for the selector (keep this order stable)
476
545
  const REVIEW_PRESETS = [
477
546
  { value: "uncommitted", label: "Review uncommitted changes", description: "" },
478
- { value: "baseBranch", label: "Review against a base branch", description: "(local)" },
547
+ {
548
+ value: "baseBranch",
549
+ label: "Review against a base branch",
550
+ description: "(local)",
551
+ },
479
552
  { value: "commit", label: "Review a commit", description: "" },
480
- { value: "pullRequest", label: "Review a pull request", description: "(GitHub PR)" },
481
- { value: "folder", label: "Review a folder (or more)", description: "(snapshot, not diff)" },
553
+ {
554
+ value: "pullRequest",
555
+ label: "Review a pull request",
556
+ description: "(GitHub PR)",
557
+ },
558
+ {
559
+ value: "folder",
560
+ label: "Review a folder (or more)",
561
+ description: "(snapshot, not diff)",
562
+ },
482
563
  { value: "custom", label: "Custom review instructions", description: "" },
483
564
  ] as const;
484
565
 
@@ -487,10 +568,10 @@ export default function reviewExtension(pi: ExtensionAPI) {
487
568
  applyReviewState(ctx);
488
569
  });
489
570
 
490
- pi.on("session_switch", (_event, ctx) => {
491
- applyReviewState(ctx);
571
+ // pi 1.1.0: session_switch 改名为 session_before_switch
572
+ pi.on("session_before_switch", (_event, _ctx) => {
573
+ applyReviewState(_ctx);
492
574
  });
493
-
494
575
  pi.on("session_tree", (_event, ctx) => {
495
576
  applyReviewState(ctx);
496
577
  });
@@ -498,7 +579,9 @@ export default function reviewExtension(pi: ExtensionAPI) {
498
579
  /**
499
580
  * Determine the smart default review type based on git state
500
581
  */
501
- async function getSmartDefault(): Promise<"uncommitted" | "baseBranch" | "commit"> {
582
+ async function getSmartDefault(): Promise<
583
+ "uncommitted" | "baseBranch" | "commit"
584
+ > {
502
585
  // Priority 1: If there are uncommitted changes, default to reviewing them
503
586
  if (await hasUncommittedChanges(pi)) {
504
587
  return "uncommitted";
@@ -518,7 +601,9 @@ export default function reviewExtension(pi: ExtensionAPI) {
518
601
  /**
519
602
  * Show the review preset selector
520
603
  */
521
- async function showReviewSelector(ctx: ExtensionContext): Promise<ReviewTarget | null> {
604
+ async function showReviewSelector(
605
+ ctx: ExtensionContext,
606
+ ): Promise<ReviewTarget | null> {
522
607
  // Determine smart default (but keep the list order stable)
523
608
  const smartDefault = await getSmartDefault();
524
609
  const items: SelectItem[] = REVIEW_PRESETS.map((preset) => ({
@@ -526,47 +611,55 @@ export default function reviewExtension(pi: ExtensionAPI) {
526
611
  label: preset.label,
527
612
  description: preset.description,
528
613
  }));
529
- const smartDefaultIndex = items.findIndex((item) => item.value === smartDefault);
614
+ const smartDefaultIndex = items.findIndex(
615
+ (item) => item.value === smartDefault,
616
+ );
530
617
 
531
618
  while (true) {
532
- const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
533
- const container = new Container();
534
- container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
535
- container.addChild(new Text(theme.fg("accent", theme.bold("Select a review preset"))));
536
-
537
- const selectList = new SelectList(items, Math.min(items.length, 10), {
538
- selectedPrefix: (text) => theme.fg("accent", text),
539
- selectedText: (text) => theme.fg("accent", text),
540
- description: (text) => theme.fg("muted", text),
541
- scrollInfo: (text) => theme.fg("dim", text),
542
- noMatch: (text) => theme.fg("warning", text),
543
- });
544
-
545
- // Preselect the smart default without reordering the list
546
- if (smartDefaultIndex >= 0) {
547
- selectList.setSelectedIndex(smartDefaultIndex);
548
- }
549
-
550
- selectList.onSelect = (item) => done(item.value);
551
- selectList.onCancel = () => done(null);
552
-
553
- container.addChild(selectList);
554
- container.addChild(new Text(theme.fg("dim", "Press enter to confirm or esc to go back")));
555
- container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
619
+ const result = await ctx.ui.custom<string | null>(
620
+ (tui, theme, _kb, done) => {
621
+ const container = new Container();
622
+ container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
623
+ container.addChild(
624
+ new Text(theme.fg("accent", theme.bold("Select a review preset"))),
625
+ );
626
+
627
+ const selectList = new SelectList(items, Math.min(items.length, 10), {
628
+ selectedPrefix: (text) => theme.fg("accent", text),
629
+ selectedText: (text) => theme.fg("accent", text),
630
+ description: (text) => theme.fg("muted", text),
631
+ scrollInfo: (text) => theme.fg("dim", text),
632
+ noMatch: (text) => theme.fg("warning", text),
633
+ });
634
+
635
+ // Preselect the smart default without reordering the list
636
+ if (smartDefaultIndex >= 0) {
637
+ selectList.setSelectedIndex(smartDefaultIndex);
638
+ }
556
639
 
557
- return {
558
- render(width: number) {
559
- return container.render(width);
560
- },
561
- invalidate() {
562
- container.invalidate();
563
- },
564
- handleInput(data: string) {
565
- selectList.handleInput(data);
566
- tui.requestRender();
567
- },
568
- };
569
- });
640
+ selectList.onSelect = (item) => done(item.value);
641
+ selectList.onCancel = () => done(null);
642
+
643
+ container.addChild(selectList);
644
+ container.addChild(
645
+ new Text(theme.fg("dim", "Press enter to confirm or esc to go back")),
646
+ );
647
+ container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
648
+
649
+ return {
650
+ render(width: number) {
651
+ return container.render(width);
652
+ },
653
+ invalidate() {
654
+ container.invalidate();
655
+ },
656
+ handleInput(data: string) {
657
+ selectList.handleInput(data);
658
+ tui.requestRender();
659
+ },
660
+ };
661
+ },
662
+ );
570
663
 
571
664
  if (!result) return null;
572
665
 
@@ -614,17 +707,23 @@ export default function reviewExtension(pi: ExtensionAPI) {
614
707
  /**
615
708
  * Show branch selector for base branch review
616
709
  */
617
- async function showBranchSelector(ctx: ExtensionContext): Promise<ReviewTarget | null> {
710
+ async function showBranchSelector(
711
+ ctx: ExtensionContext,
712
+ ): Promise<ReviewTarget | null> {
618
713
  const branches = await getLocalBranches(pi);
619
714
  const currentBranch = await getCurrentBranch(pi);
620
715
  const defaultBranch = await getDefaultBranch(pi);
621
716
 
622
717
  // Never offer the current branch as a base branch (reviewing against itself is meaningless).
623
- const candidateBranches = currentBranch ? branches.filter((b) => b !== currentBranch) : branches;
718
+ const candidateBranches = currentBranch
719
+ ? branches.filter((b) => b !== currentBranch)
720
+ : branches;
624
721
 
625
722
  if (candidateBranches.length === 0) {
626
723
  ctx.ui.notify(
627
- currentBranch ? `No other branches found (current branch: ${currentBranch})` : "No branches found",
724
+ currentBranch
725
+ ? `No other branches found (current branch: ${currentBranch})`
726
+ : "No branches found",
628
727
  "error",
629
728
  );
630
729
  return null;
@@ -646,7 +745,9 @@ export default function reviewExtension(pi: ExtensionAPI) {
646
745
  const result = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
647
746
  const container = new Container();
648
747
  container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
649
- container.addChild(new Text(theme.fg("accent", theme.bold("Select base branch"))));
748
+ container.addChild(
749
+ new Text(theme.fg("accent", theme.bold("Select base branch"))),
750
+ );
650
751
 
651
752
  const selectList = new SelectList(items, Math.min(items.length, 10), {
652
753
  selectedPrefix: (text) => theme.fg("accent", text),
@@ -656,14 +757,15 @@ export default function reviewExtension(pi: ExtensionAPI) {
656
757
  noMatch: (text) => theme.fg("warning", text),
657
758
  });
658
759
 
659
- // Enable search
660
- selectList.searchable = true;
661
-
662
760
  selectList.onSelect = (item) => done(item.value);
663
761
  selectList.onCancel = () => done(null);
664
762
 
665
763
  container.addChild(selectList);
666
- container.addChild(new Text(theme.fg("dim", "Type to filter • enter to select • esc to cancel")));
764
+ container.addChild(
765
+ new Text(
766
+ theme.fg("dim", "Type to filter • enter to select • esc to cancel"),
767
+ ),
768
+ );
667
769
  container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
668
770
 
669
771
  return {
@@ -687,7 +789,9 @@ export default function reviewExtension(pi: ExtensionAPI) {
687
789
  /**
688
790
  * Show commit selector
689
791
  */
690
- async function showCommitSelector(ctx: ExtensionContext): Promise<ReviewTarget | null> {
792
+ async function showCommitSelector(
793
+ ctx: ExtensionContext,
794
+ ): Promise<ReviewTarget | null> {
691
795
  const commits = await getRecentCommits(pi, 20);
692
796
 
693
797
  if (commits.length === 0) {
@@ -701,49 +805,54 @@ export default function reviewExtension(pi: ExtensionAPI) {
701
805
  description: "",
702
806
  }));
703
807
 
704
- const result = await ctx.ui.custom<{ sha: string; title: string } | null>((tui, theme, _kb, done) => {
705
- const container = new Container();
706
- container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
707
- container.addChild(new Text(theme.fg("accent", theme.bold("Select commit to review"))));
708
-
709
- const selectList = new SelectList(items, Math.min(items.length, 10), {
710
- selectedPrefix: (text) => theme.fg("accent", text),
711
- selectedText: (text) => theme.fg("accent", text),
712
- description: (text) => theme.fg("muted", text),
713
- scrollInfo: (text) => theme.fg("dim", text),
714
- noMatch: (text) => theme.fg("warning", text),
715
- });
808
+ const result = await ctx.ui.custom<{ sha: string; title: string } | null>(
809
+ (tui, theme, _kb, done) => {
810
+ const container = new Container();
811
+ container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
812
+ container.addChild(
813
+ new Text(theme.fg("accent", theme.bold("Select commit to review"))),
814
+ );
716
815
 
717
- // Enable search
718
- selectList.searchable = true;
816
+ const selectList = new SelectList(items, Math.min(items.length, 10), {
817
+ selectedPrefix: (text) => theme.fg("accent", text),
818
+ selectedText: (text) => theme.fg("accent", text),
819
+ description: (text) => theme.fg("muted", text),
820
+ scrollInfo: (text) => theme.fg("dim", text),
821
+ noMatch: (text) => theme.fg("warning", text),
822
+ });
719
823
 
720
- selectList.onSelect = (item) => {
721
- const commit = commits.find((c) => c.sha === item.value);
722
- if (commit) {
723
- done(commit);
724
- } else {
725
- done(null);
726
- }
727
- };
728
- selectList.onCancel = () => done(null);
824
+ selectList.onSelect = (item) => {
825
+ const commit = commits.find((c) => c.sha === item.value);
826
+ if (commit) {
827
+ done(commit);
828
+ } else {
829
+ done(null);
830
+ }
831
+ };
832
+ selectList.onCancel = () => done(null);
729
833
 
730
- container.addChild(selectList);
731
- container.addChild(new Text(theme.fg("dim", "Type to filter • enter to select • esc to cancel")));
732
- container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
834
+ container.addChild(selectList);
835
+ container.addChild(
836
+ new Text(
837
+ theme.fg("dim", "Type to filter • enter to select • esc to cancel"),
838
+ ),
839
+ );
840
+ container.addChild(new DynamicBorder((str) => theme.fg("accent", str)));
733
841
 
734
- return {
735
- render(width: number) {
736
- return container.render(width);
737
- },
738
- invalidate() {
739
- container.invalidate();
740
- },
741
- handleInput(data: string) {
742
- selectList.handleInput(data);
743
- tui.requestRender();
744
- },
745
- };
746
- });
842
+ return {
843
+ render(width: number) {
844
+ return container.render(width);
845
+ },
846
+ invalidate() {
847
+ container.invalidate();
848
+ },
849
+ handleInput(data: string) {
850
+ selectList.handleInput(data);
851
+ tui.requestRender();
852
+ },
853
+ };
854
+ },
855
+ );
747
856
 
748
857
  if (!result) return null;
749
858
  return { type: "commit", sha: result.sha, title: result.title };
@@ -752,7 +861,9 @@ export default function reviewExtension(pi: ExtensionAPI) {
752
861
  /**
753
862
  * Show custom instructions input
754
863
  */
755
- async function showCustomInput(ctx: ExtensionContext): Promise<ReviewTarget | null> {
864
+ async function showCustomInput(
865
+ ctx: ExtensionContext,
866
+ ): Promise<ReviewTarget | null> {
756
867
  const result = await ctx.ui.editor(
757
868
  "Enter review instructions:",
758
869
  "Review the code for security vulnerabilities and potential bugs...",
@@ -772,7 +883,9 @@ export default function reviewExtension(pi: ExtensionAPI) {
772
883
  /**
773
884
  * Show folder input
774
885
  */
775
- async function showFolderInput(ctx: ExtensionContext): Promise<ReviewTarget | null> {
886
+ async function showFolderInput(
887
+ ctx: ExtensionContext,
888
+ ): Promise<ReviewTarget | null> {
776
889
  const result = await ctx.ui.editor(
777
890
  "Enter folders/files to review (space-separated or one per line):",
778
891
  ".",
@@ -788,10 +901,15 @@ export default function reviewExtension(pi: ExtensionAPI) {
788
901
  /**
789
902
  * Show PR input and handle checkout
790
903
  */
791
- async function showPrInput(ctx: ExtensionContext): Promise<ReviewTarget | null> {
904
+ async function showPrInput(
905
+ ctx: ExtensionContext,
906
+ ): Promise<ReviewTarget | null> {
792
907
  // First check for pending changes that would prevent branch switching
793
908
  if (await hasPendingChanges(pi)) {
794
- ctx.ui.notify("Cannot checkout PR: you have uncommitted changes. Please commit or stash them first.", "error");
909
+ ctx.ui.notify(
910
+ "Cannot checkout PR: you have uncommitted changes. Please commit or stash them first.",
911
+ "error",
912
+ );
795
913
  return null;
796
914
  }
797
915
 
@@ -805,7 +923,10 @@ export default function reviewExtension(pi: ExtensionAPI) {
805
923
 
806
924
  const prNumber = parsePrReference(prRef);
807
925
  if (!prNumber) {
808
- ctx.ui.notify("Invalid PR reference. Enter a number or GitHub PR URL.", "error");
926
+ ctx.ui.notify(
927
+ "Invalid PR reference. Enter a number or GitHub PR URL.",
928
+ "error",
929
+ );
809
930
  return null;
810
931
  }
811
932
 
@@ -814,13 +935,19 @@ export default function reviewExtension(pi: ExtensionAPI) {
814
935
  const prInfo = await getPrInfo(pi, prNumber);
815
936
 
816
937
  if (!prInfo) {
817
- ctx.ui.notify(`Could not find PR #${prNumber}. Make sure gh is authenticated and the PR exists.`, "error");
938
+ ctx.ui.notify(
939
+ `Could not find PR #${prNumber}. Make sure gh is authenticated and the PR exists.`,
940
+ "error",
941
+ );
818
942
  return null;
819
943
  }
820
944
 
821
945
  // Check again for pending changes (in case something changed)
822
946
  if (await hasPendingChanges(pi)) {
823
- ctx.ui.notify("Cannot checkout PR: you have uncommitted changes. Please commit or stash them first.", "error");
947
+ ctx.ui.notify(
948
+ "Cannot checkout PR: you have uncommitted changes. Please commit or stash them first.",
949
+ "error",
950
+ );
824
951
  return null;
825
952
  }
826
953
 
@@ -846,10 +973,17 @@ export default function reviewExtension(pi: ExtensionAPI) {
846
973
  /**
847
974
  * Execute the review
848
975
  */
849
- async function executeReview(ctx: ExtensionCommandContext, target: ReviewTarget, useFreshSession: boolean): Promise<void> {
976
+ async function executeReview(
977
+ ctx: ExtensionCommandContext,
978
+ target: ReviewTarget,
979
+ useFreshSession: boolean,
980
+ ): Promise<void> {
850
981
  // Check if we're already in a review
851
982
  if (reviewOriginId) {
852
- ctx.ui.notify("Already in a review. Use /end-review to finish first.", "warning");
983
+ ctx.ui.notify(
984
+ "Already in a review. Use /end-review to finish first.",
985
+ "warning",
986
+ );
853
987
  return;
854
988
  }
855
989
 
@@ -858,7 +992,10 @@ export default function reviewExtension(pi: ExtensionAPI) {
858
992
  // Store current position (where we'll return to)
859
993
  const originId = ctx.sessionManager.getLeafId() ?? undefined;
860
994
  if (!originId) {
861
- ctx.ui.notify("Failed to determine review origin. Try again from a session with messages.", "error");
995
+ ctx.ui.notify(
996
+ "Failed to determine review origin. Try again from a session with messages.",
997
+ "error",
998
+ );
862
999
  return;
863
1000
  }
864
1001
  reviewOriginId = originId;
@@ -881,7 +1018,10 @@ export default function reviewExtension(pi: ExtensionAPI) {
881
1018
  // Navigate to first user message to create a new branch from that point
882
1019
  // Label it as "code-review" so it's visible in the tree
883
1020
  try {
884
- const result = await ctx.navigateTree(firstUserMessage.id, { summarize: false, label: "code-review" });
1021
+ const result = await ctx.navigateTree(firstUserMessage.id, {
1022
+ summarize: false,
1023
+ label: "code-review",
1024
+ });
885
1025
  if (result.cancelled) {
886
1026
  reviewOriginId = undefined;
887
1027
  return;
@@ -889,7 +1029,10 @@ export default function reviewExtension(pi: ExtensionAPI) {
889
1029
  } catch (error) {
890
1030
  // Clean up state if navigation fails
891
1031
  reviewOriginId = undefined;
892
- ctx.ui.notify(`Failed to start review: ${error instanceof Error ? error.message : String(error)}`, "error");
1032
+ ctx.ui.notify(
1033
+ `Failed to start review: ${error instanceof Error ? error.message : String(error)}`,
1034
+ "error",
1035
+ );
893
1036
  return;
894
1037
  }
895
1038
 
@@ -903,7 +1046,10 @@ export default function reviewExtension(pi: ExtensionAPI) {
903
1046
  setReviewWidget(ctx, true);
904
1047
 
905
1048
  // Persist review state so tree navigation can restore/reset it
906
- pi.appendEntry(REVIEW_STATE_TYPE, { active: true, originId: lockedOriginId });
1049
+ pi.appendEntry(REVIEW_STATE_TYPE, {
1050
+ active: true,
1051
+ originId: lockedOriginId,
1052
+ });
907
1053
  }
908
1054
 
909
1055
  const prompt = await buildReviewPrompt(pi, target);
@@ -931,7 +1077,9 @@ export default function reviewExtension(pi: ExtensionAPI) {
931
1077
  * Parse command arguments for direct invocation
932
1078
  * Returns the target or a special marker for PR that needs async handling
933
1079
  */
934
- function parseArgs(args: string | undefined): ReviewTarget | { type: "pr"; ref: string } | null {
1080
+ function parseArgs(
1081
+ args: string | undefined,
1082
+ ): ReviewTarget | { type: "pr"; ref: string } | null {
935
1083
  if (!args?.trim()) return null;
936
1084
 
937
1085
  const parts = args.trim().split(/\s+/);
@@ -980,16 +1128,25 @@ export default function reviewExtension(pi: ExtensionAPI) {
980
1128
  /**
981
1129
  * Handle PR checkout and return a ReviewTarget (or null on failure)
982
1130
  */
983
- async function handlePrCheckout(ctx: ExtensionContext, ref: string): Promise<ReviewTarget | null> {
1131
+ async function handlePrCheckout(
1132
+ ctx: ExtensionContext,
1133
+ ref: string,
1134
+ ): Promise<ReviewTarget | null> {
984
1135
  // First check for pending changes
985
1136
  if (await hasPendingChanges(pi)) {
986
- ctx.ui.notify("Cannot checkout PR: you have uncommitted changes. Please commit or stash them first.", "error");
1137
+ ctx.ui.notify(
1138
+ "Cannot checkout PR: you have uncommitted changes. Please commit or stash them first.",
1139
+ "error",
1140
+ );
987
1141
  return null;
988
1142
  }
989
1143
 
990
1144
  const prNumber = parsePrReference(ref);
991
1145
  if (!prNumber) {
992
- ctx.ui.notify("Invalid PR reference. Enter a number or GitHub PR URL.", "error");
1146
+ ctx.ui.notify(
1147
+ "Invalid PR reference. Enter a number or GitHub PR URL.",
1148
+ "error",
1149
+ );
993
1150
  return null;
994
1151
  }
995
1152
 
@@ -998,7 +1155,10 @@ export default function reviewExtension(pi: ExtensionAPI) {
998
1155
  const prInfo = await getPrInfo(pi, prNumber);
999
1156
 
1000
1157
  if (!prInfo) {
1001
- ctx.ui.notify(`Could not find PR #${prNumber}. Make sure gh is authenticated and the PR exists.`, "error");
1158
+ ctx.ui.notify(
1159
+ `Could not find PR #${prNumber}. Make sure gh is authenticated and the PR exists.`,
1160
+ "error",
1161
+ );
1002
1162
  return null;
1003
1163
  }
1004
1164
 
@@ -1023,7 +1183,8 @@ export default function reviewExtension(pi: ExtensionAPI) {
1023
1183
 
1024
1184
  // Register the /review command
1025
1185
  pi.registerCommand("review", {
1026
- description: "Review code changes (PR, uncommitted, branch, commit, folder, or custom)",
1186
+ description:
1187
+ "Review code changes (PR, uncommitted, branch, commit, folder, or custom)",
1027
1188
  handler: async (args, ctx) => {
1028
1189
  if (!ctx.hasUI) {
1029
1190
  ctx.ui.notify("Review requires interactive mode", "error");
@@ -1032,7 +1193,10 @@ export default function reviewExtension(pi: ExtensionAPI) {
1032
1193
 
1033
1194
  // Check if we're already in a review
1034
1195
  if (reviewOriginId) {
1035
- ctx.ui.notify("Already in a review. Use /end-review to finish first.", "warning");
1196
+ ctx.ui.notify(
1197
+ "Already in a review. Use /end-review to finish first.",
1198
+ "warning",
1199
+ );
1036
1200
  return;
1037
1201
  }
1038
1202
 
@@ -1084,7 +1248,10 @@ export default function reviewExtension(pi: ExtensionAPI) {
1084
1248
 
1085
1249
  if (messageCount > 0) {
1086
1250
  // Existing session - ask user which mode they want
1087
- const choice = await ctx.ui.select("Start review in:", ["Empty branch", "Current session"]);
1251
+ const choice = await ctx.ui.select("Start review in:", [
1252
+ "Empty branch",
1253
+ "Current session",
1254
+ ]);
1088
1255
 
1089
1256
  if (choice === undefined) {
1090
1257
  if (fromSelector) {
@@ -1161,7 +1328,10 @@ Instructions:
1161
1328
  if (state?.active) {
1162
1329
  setReviewWidget(ctx, false);
1163
1330
  pi.appendEntry(REVIEW_STATE_TYPE, { active: false });
1164
- ctx.ui.notify("Review state was missing origin info; cleared review status.", "warning");
1331
+ ctx.ui.notify(
1332
+ "Review state was missing origin info; cleared review status.",
1333
+ "warning",
1334
+ );
1165
1335
  }
1166
1336
 
1167
1337
  return undefined;
@@ -1187,7 +1357,10 @@ Instructions:
1187
1357
  const originId = getActiveReviewOrigin(ctx);
1188
1358
  if (!originId) {
1189
1359
  if (!getReviewState(ctx)?.active) {
1190
- ctx.ui.notify("Not in a review branch (use /review first, or review was started in current session mode)", "info");
1360
+ ctx.ui.notify(
1361
+ "Not in a review branch (use /review first, or review was started in current session mode)",
1362
+ "info",
1363
+ );
1191
1364
  }
1192
1365
  return;
1193
1366
  }
@@ -1215,11 +1388,17 @@ Instructions:
1215
1388
  try {
1216
1389
  const result = await ctx.navigateTree(originId, { summarize: false });
1217
1390
  if (result.cancelled) {
1218
- ctx.ui.notify("Navigation cancelled. Use /end-review to try again.", "info");
1391
+ ctx.ui.notify(
1392
+ "Navigation cancelled. Use /end-review to try again.",
1393
+ "info",
1394
+ );
1219
1395
  return;
1220
1396
  }
1221
1397
  } catch (error) {
1222
- ctx.ui.notify(`Failed to return: ${error instanceof Error ? error.message : String(error)}`, "error");
1398
+ ctx.ui.notify(
1399
+ `Failed to return: ${error instanceof Error ? error.message : String(error)}`,
1400
+ "error",
1401
+ );
1223
1402
  return;
1224
1403
  }
1225
1404
 
@@ -1228,23 +1407,39 @@ Instructions:
1228
1407
  return;
1229
1408
  }
1230
1409
 
1231
- const summaryResult = await ctx.ui.custom<{ cancelled: boolean; error?: string } | null>((tui, theme, _kb, done) => {
1232
- const loader = new BorderedLoader(tui, theme, "Returning and summarizing review branch...");
1410
+ const summaryResult = await ctx.ui.custom<{
1411
+ cancelled: boolean;
1412
+ error?: string;
1413
+ } | null>((tui, theme, _kb, done) => {
1414
+ const loader = new BorderedLoader(
1415
+ tui,
1416
+ theme,
1417
+ "Returning and summarizing review branch...",
1418
+ );
1233
1419
  loader.onAbort = () => done(null);
1234
1420
 
1235
- ctx.navigateTree(originId, {
1236
- summarize: true,
1237
- customInstructions: REVIEW_SUMMARY_PROMPT,
1238
- replaceInstructions: true,
1239
- })
1421
+ ctx
1422
+ .navigateTree(originId, {
1423
+ summarize: true,
1424
+ customInstructions: REVIEW_SUMMARY_PROMPT,
1425
+ replaceInstructions: true,
1426
+ })
1240
1427
  .then(done)
1241
- .catch((err) => done({ cancelled: false, error: err instanceof Error ? err.message : String(err) }));
1428
+ .catch((err) =>
1429
+ done({
1430
+ cancelled: false,
1431
+ error: err instanceof Error ? err.message : String(err),
1432
+ }),
1433
+ );
1242
1434
 
1243
1435
  return loader;
1244
1436
  });
1245
1437
 
1246
1438
  if (summaryResult === null) {
1247
- ctx.ui.notify("Summarization cancelled. Use /end-review to try again.", "info");
1439
+ ctx.ui.notify(
1440
+ "Summarization cancelled. Use /end-review to try again.",
1441
+ "info",
1442
+ );
1248
1443
  return;
1249
1444
  }
1250
1445
 
@@ -1254,7 +1449,10 @@ Instructions:
1254
1449
  }
1255
1450
 
1256
1451
  if (summaryResult.cancelled) {
1257
- ctx.ui.notify("Navigation cancelled. Use /end-review to try again.", "info");
1452
+ ctx.ui.notify(
1453
+ "Navigation cancelled. Use /end-review to try again.",
1454
+ "info",
1455
+ );
1258
1456
  return;
1259
1457
  }
1260
1458
 
@@ -1269,7 +1467,10 @@ Instructions:
1269
1467
  }
1270
1468
 
1271
1469
  pi.sendUserMessage(REVIEW_FIX_FINDINGS_PROMPT, { deliverAs: "followUp" });
1272
- ctx.ui.notify("Review complete! Returned and queued a follow-up to fix findings.", "info");
1470
+ ctx.ui.notify(
1471
+ "Review complete! Returned and queued a follow-up to fix findings.",
1472
+ "info",
1473
+ );
1273
1474
  } finally {
1274
1475
  endReviewInProgress = false;
1275
1476
  }