@oh-my-pi/pi-coding-agent 15.8.0 → 15.8.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.
@@ -7,7 +7,7 @@
7
7
  * 3. Review a specific commit
8
8
  * 4. Custom review instructions
9
9
  *
10
- * Runs git diff upfront, parses results, filters noise, and provides
10
+ * Runs VCS diffs upfront, parses results, filters noise, and provides
11
11
  * rich context for the orchestrating agent to distribute work across
12
12
  * multiple reviewer agents based on diff weight and locality.
13
13
  */
@@ -18,6 +18,7 @@ import reviewCustomRequestTemplate from "../../../../prompts/review-custom-reque
18
18
  import reviewHeadlessRequestTemplate from "../../../../prompts/review-headless-request.md" with { type: "text" };
19
19
  import reviewRequestTemplate from "../../../../prompts/review-request.md" with { type: "text" };
20
20
  import * as git from "../../../../utils/git";
21
+ import * as jj from "../../../../utils/jj";
21
22
 
22
23
  // ─────────────────────────────────────────────────────────────────────────────
23
24
  // Types
@@ -37,6 +38,13 @@ interface DiffStats {
37
38
  excluded: { path: string; reason: string; linesAdded: number; linesRemoved: number }[];
38
39
  }
39
40
 
41
+ interface CurrentReviewDiff {
42
+ diffInstruction: string;
43
+ diffText: string;
44
+ emptyMessage?: string;
45
+ mode: string;
46
+ }
47
+
40
48
  // ─────────────────────────────────────────────────────────────────────────────
41
49
  // Exclusion patterns for noise files
42
50
  // ─────────────────────────────────────────────────────────────────────────────
@@ -195,11 +203,20 @@ function getDiffPreview(hunks: string, maxLines: number): string {
195
203
  // Thresholds for diff inclusion
196
204
  const MAX_DIFF_CHARS = 50_000; // Don't include diff above this
197
205
  const MAX_FILES_FOR_INLINE_DIFF = 20; // Don't include diff if more files than this
206
+ const DEFAULT_LARGE_DIFF_INSTRUCTION = "MUST run `git diff`/`git show` for assigned files";
207
+ const GIT_UNCOMMITTED_DIFF_INSTRUCTION =
208
+ "MUST run both `git diff -- <path>` and `git diff --cached -- <path>` for assigned files";
209
+ const JJ_UNCOMMITTED_DIFF_INSTRUCTION = "MUST run `jj --ignore-working-copy diff --git -- <path>` for assigned files";
198
210
 
199
211
  /**
200
212
  * Build the full review prompt with diff stats and distribution guidance.
201
213
  */
202
- function buildReviewPrompt(mode: string, stats: DiffStats, rawDiff: string, additionalInstructions?: string): string {
214
+ function buildReviewPrompt(
215
+ mode: string,
216
+ stats: DiffStats,
217
+ rawDiff: string,
218
+ options: { additionalInstructions?: string; diffInstruction?: string } = {},
219
+ ): string {
203
220
  const agentCount = getRecommendedAgentCount(stats);
204
221
  const skipDiff = rawDiff.length > MAX_DIFF_CHARS || stats.files.length > MAX_FILES_FOR_INLINE_DIFF;
205
222
  const totalLines = stats.totalAdded + stats.totalRemoved;
@@ -223,7 +240,8 @@ function buildReviewPrompt(mode: string, stats: DiffStats, rawDiff: string, addi
223
240
  skipDiff,
224
241
  rawDiff: rawDiff.trim(),
225
242
  linesPerFile,
226
- additionalInstructions,
243
+ additionalInstructions: options.additionalInstructions,
244
+ diffInstruction: options.diffInstruction ?? DEFAULT_LARGE_DIFF_INSTRUCTION,
227
245
  });
228
246
  }
229
247
 
@@ -305,49 +323,32 @@ export class ReviewCommand implements CustomCommand {
305
323
  `Reviewing changes between \`${baseBranch}\` and \`${currentBranch}\` (PR-style)`,
306
324
  stats,
307
325
  diffText,
308
- extraInstructions,
326
+ { additionalInstructions: extraInstructions },
309
327
  );
310
328
  }
311
329
 
312
330
  case 2: {
313
- // Uncommitted changes - combine staged and unstaged
314
- const status = await getGitStatus(this.api);
315
- if (!status.trim()) {
316
- ctx.ui.notify("No uncommitted changes found", "warning");
317
- return undefined;
318
- }
319
-
320
- let unstagedDiff: string;
321
- let stagedDiff: string;
322
- try {
323
- [unstagedDiff, stagedDiff] = await Promise.all([
324
- git.diff(this.api.cwd),
325
- git.diff(this.api.cwd, { cached: true }),
326
- ]);
327
- } catch (err) {
331
+ const reviewDiff = await getUncommittedReviewDiff(this.api).catch(err => {
328
332
  ctx.ui.notify(`Failed to get diff: ${err instanceof Error ? err.message : String(err)}`, "error");
329
333
  return undefined;
330
- }
334
+ });
335
+ if (!reviewDiff) return undefined;
331
336
 
332
- const combinedDiff = [unstagedDiff, stagedDiff].filter(Boolean).join("\n");
333
-
334
- if (!combinedDiff.trim()) {
335
- ctx.ui.notify("No diff content found", "warning");
337
+ if (!reviewDiff.diffText.trim()) {
338
+ ctx.ui.notify(reviewDiff.emptyMessage ?? "No diff content found", "warning");
336
339
  return undefined;
337
340
  }
338
341
 
339
- const stats = parseDiff(combinedDiff);
342
+ const stats = parseDiff(reviewDiff.diffText);
340
343
  if (stats.files.length === 0) {
341
344
  ctx.ui.notify("No reviewable files (all changes filtered out)", "warning");
342
345
  return undefined;
343
346
  }
344
347
 
345
- return buildReviewPrompt(
346
- "Reviewing uncommitted changes (staged + unstaged)",
347
- stats,
348
- combinedDiff,
349
- extraInstructions,
350
- );
348
+ return buildReviewPrompt(reviewDiff.mode, stats, reviewDiff.diffText, {
349
+ additionalInstructions: extraInstructions,
350
+ diffInstruction: reviewDiff.diffInstruction,
351
+ });
351
352
  }
352
353
 
353
354
  case 3: {
@@ -383,11 +384,13 @@ export class ReviewCommand implements CustomCommand {
383
384
  return undefined;
384
385
  }
385
386
 
386
- return buildReviewPrompt(`Reviewing commit \`${hash}\``, stats, diffText, extraInstructions);
387
+ return buildReviewPrompt(`Reviewing commit \`${hash}\``, stats, diffText, {
388
+ additionalInstructions: extraInstructions,
389
+ });
387
390
  }
388
391
 
389
392
  case 4: {
390
- // Custom instructions - still uses the old approach since user provides context
393
+ // Custom instructions with opportunistic current-diff context.
391
394
  const instructions = await ctx.ui.editor(
392
395
  "Enter custom review instructions",
393
396
  "Review the following:\n\n",
@@ -396,23 +399,19 @@ export class ReviewCommand implements CustomCommand {
396
399
  );
397
400
  if (!instructions?.trim()) return undefined;
398
401
 
399
- // For custom, we still try to get current diff for context
400
- let diffText: string | undefined;
401
- try {
402
- diffText = await git.diff(this.api.cwd, { base: "HEAD" });
403
- } catch {
404
- diffText = undefined;
405
- }
406
- const reviewDiff = diffText?.trim();
402
+ const reviewDiff = await getUncommittedReviewDiff(this.api).catch(() => undefined);
407
403
 
408
- if (reviewDiff) {
409
- const stats = parseDiff(reviewDiff);
404
+ if (reviewDiff?.diffText.trim()) {
405
+ const stats = parseDiff(reviewDiff.diffText);
410
406
  // Even if all files filtered, include the custom instructions
411
407
  return buildReviewPrompt(
412
408
  `Custom review: ${instructions.split("\n")[0].slice(0, 60)}…`,
413
409
  stats,
414
- reviewDiff,
415
- instructions,
410
+ reviewDiff.diffText,
411
+ {
412
+ additionalInstructions: instructions,
413
+ diffInstruction: reviewDiff.diffInstruction,
414
+ },
416
415
  );
417
416
  }
418
417
 
@@ -449,6 +448,36 @@ async function getGitStatus(api: CustomCommandAPI): Promise<string> {
449
448
  }
450
449
  }
451
450
 
451
+ async function getUncommittedReviewDiff(api: CustomCommandAPI): Promise<CurrentReviewDiff> {
452
+ if (await jj.repo.is(api.cwd)) {
453
+ return {
454
+ diffText: await jj.diff(api.cwd),
455
+ diffInstruction: JJ_UNCOMMITTED_DIFF_INSTRUCTION,
456
+ emptyMessage: "No uncommitted changes found",
457
+ mode: "Reviewing JJ working-copy changes",
458
+ };
459
+ }
460
+
461
+ const status = await getGitStatus(api);
462
+ if (!status.trim()) {
463
+ return {
464
+ diffText: "",
465
+ diffInstruction: GIT_UNCOMMITTED_DIFF_INSTRUCTION,
466
+ emptyMessage: "No uncommitted changes found",
467
+ mode: "Reviewing uncommitted changes (staged + unstaged)",
468
+ };
469
+ }
470
+
471
+ const [unstagedDiff, stagedDiff] = await Promise.all([git.diff(api.cwd), git.diff(api.cwd, { cached: true })]);
472
+ const combinedDiff = [unstagedDiff, stagedDiff].filter(Boolean).join("\n");
473
+ return {
474
+ diffText: combinedDiff,
475
+ diffInstruction: GIT_UNCOMMITTED_DIFF_INSTRUCTION,
476
+ emptyMessage: "No diff content found",
477
+ mode: "Reviewing uncommitted changes (staged + unstaged)",
478
+ };
479
+ }
480
+
452
481
  async function getRecentCommits(api: CustomCommandAPI, count: number): Promise<string[]> {
453
482
  try {
454
483
  return await git.log.onelines(api.cwd, count);