@in-the-loop-labs/pair-review 1.6.2 → 2.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 (63) hide show
  1. package/README.md +77 -4
  2. package/package.json +1 -1
  3. package/plugin/.claude-plugin/plugin.json +1 -1
  4. package/plugin/skills/review-requests/SKILL.md +4 -1
  5. package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
  6. package/plugin-code-critic/skills/analyze/SKILL.md +4 -3
  7. package/public/css/pr.css +1962 -114
  8. package/public/js/CONVENTIONS.md +16 -0
  9. package/public/js/components/AIPanel.js +66 -0
  10. package/public/js/components/AnalysisConfigModal.js +2 -2
  11. package/public/js/components/ChatPanel.js +2955 -0
  12. package/public/js/components/CouncilProgressModal.js +12 -16
  13. package/public/js/components/KeyboardShortcuts.js +3 -0
  14. package/public/js/components/PanelGroup.js +723 -0
  15. package/public/js/components/PreviewModal.js +3 -8
  16. package/public/js/index.js +8 -0
  17. package/public/js/local.js +17 -615
  18. package/public/js/modules/analysis-history.js +19 -68
  19. package/public/js/modules/comment-manager.js +103 -20
  20. package/public/js/modules/diff-context.js +176 -0
  21. package/public/js/modules/diff-renderer.js +30 -0
  22. package/public/js/modules/file-comment-manager.js +126 -105
  23. package/public/js/modules/file-list-merger.js +64 -0
  24. package/public/js/modules/panel-resizer.js +25 -6
  25. package/public/js/modules/suggestion-manager.js +40 -125
  26. package/public/js/pr.js +1009 -159
  27. package/public/js/repo-settings.js +36 -6
  28. package/public/js/utils/category-emoji.js +44 -0
  29. package/public/js/utils/time.js +32 -0
  30. package/public/local.html +107 -70
  31. package/public/pr.html +107 -70
  32. package/public/repo-settings.html +32 -0
  33. package/src/ai/analyzer.js +5 -1
  34. package/src/ai/copilot-provider.js +39 -9
  35. package/src/ai/cursor-agent-provider.js +45 -11
  36. package/src/ai/gemini-provider.js +17 -4
  37. package/src/ai/prompts/config.js +7 -1
  38. package/src/ai/provider-availability.js +1 -1
  39. package/src/ai/provider.js +25 -37
  40. package/src/chat/CONVENTIONS.md +18 -0
  41. package/src/chat/pi-bridge.js +491 -0
  42. package/src/chat/prompt-builder.js +272 -0
  43. package/src/chat/session-manager.js +619 -0
  44. package/src/config.js +14 -0
  45. package/src/database.js +322 -15
  46. package/src/main.js +4 -17
  47. package/src/routes/analyses.js +721 -0
  48. package/src/routes/chat.js +655 -0
  49. package/src/routes/config.js +29 -8
  50. package/src/routes/context-files.js +274 -0
  51. package/src/routes/local.js +225 -1133
  52. package/src/routes/mcp.js +39 -30
  53. package/src/routes/pr.js +424 -58
  54. package/src/routes/reviews.js +1035 -0
  55. package/src/routes/shared.js +4 -29
  56. package/src/server.js +34 -12
  57. package/src/sse/review-events.js +46 -0
  58. package/src/utils/auto-context.js +88 -0
  59. package/src/utils/category-emoji.js +33 -0
  60. package/src/utils/diff-annotator.js +75 -1
  61. package/src/utils/diff-file-list.js +57 -0
  62. package/src/routes/analysis.js +0 -1600
  63. package/src/routes/comments.js +0 -534
package/src/routes/mcp.js CHANGED
@@ -7,20 +7,24 @@ const { v4: uuidv4 } = require('uuid');
7
7
  const { ReviewRepository, CommentRepository, AnalysisRunRepository, RepoSettingsRepository, PRMetadataRepository, query } = require('../database');
8
8
  const { renderPromptForSkill } = require('../ai/prompts/render-for-skill');
9
9
  const Analyzer = require('../ai/analyzer');
10
+ const { getTierForModel } = require('../ai/provider');
11
+ const { TIERS, TIER_ALIASES, resolveTier } = require('../ai/prompts/config');
10
12
  const { GitWorktreeManager } = require('../git/worktree');
11
13
  const path = require('path');
12
14
  const { normalizeRepository } = require('../utils/paths');
13
15
  const logger = require('../utils/logger');
16
+ const { broadcastReviewEvent } = require('../sse/review-events');
14
17
  const {
15
18
  activeAnalyses,
16
- prToAnalysisId,
17
- localReviewToAnalysisId,
18
- getLocalReviewKey,
19
+ reviewToAnalysisId,
19
20
  determineCompletionInfo,
20
21
  broadcastProgress,
21
22
  createProgressCallback
22
23
  } = require('./shared');
23
24
 
25
+ // All valid tier values: canonical tiers + aliases (for Zod enum validation)
26
+ const ALL_TIER_VALUES = /** @type {[string, ...string[]]} */ ([...TIERS, ...Object.keys(TIER_ALIASES)]);
27
+
24
28
  const router = express.Router();
25
29
 
26
30
  /**
@@ -61,6 +65,9 @@ async function handleAnalysisCompletion(analysisId, runId, result, savePersisten
61
65
  };
62
66
  activeAnalyses.set(analysisId, completedStatus);
63
67
  broadcastProgress(analysisId, completedStatus);
68
+ if (currentStatus.reviewId) {
69
+ broadcastReviewEvent(currentStatus.reviewId, { type: 'review:analysis_completed' });
70
+ }
64
71
 
65
72
  // Auto-cleanup after 30 minutes
66
73
  setTimeout(() => activeAnalyses.delete(analysisId), 30 * 60 * 1000);
@@ -195,14 +202,15 @@ function createMCPServer(db, options = {}) {
195
202
  {
196
203
  promptType: z.enum(['level1', 'level2', 'level3', 'orchestration'])
197
204
  .describe('Analysis level'),
198
- tier: z.enum(['fast', 'balanced', 'thorough']).default('balanced')
205
+ tier: z.enum(ALL_TIER_VALUES).default('balanced')
199
206
  .describe('Prompt tier — fast (surface), balanced (standard), or thorough (deep)'),
200
207
  customInstructions: z.string().max(5000).optional()
201
208
  .describe('Optional repo or user-specific review instructions to include'),
202
209
  },
203
210
  async (args) => {
204
211
  try {
205
- const rendered = renderPromptForSkill(args.promptType, args.tier, {
212
+ const tier = resolveTier(args.tier);
213
+ const rendered = renderPromptForSkill(args.promptType, tier, {
206
214
  customInstructions: args.customInstructions,
207
215
  });
208
216
  return { content: [{ type: 'text', text: rendered }] };
@@ -300,6 +308,8 @@ function createMCPServer(db, options = {}) {
300
308
  id: r.id,
301
309
  provider: r.provider,
302
310
  model: r.model,
311
+ // Enrich historical runs that predate the tier column (migration 22)
312
+ tier: r.tier ?? (r.provider && r.model ? getTierForModel(r.provider, r.model) : null),
303
313
  status: r.status,
304
314
  summary: r.summary,
305
315
  head_sha: r.head_sha,
@@ -439,14 +449,13 @@ function createMCPServer(db, options = {}) {
439
449
  .describe('Optional repo or user-specific review instructions'),
440
450
  skipLevel3: z.boolean().default(false)
441
451
  .describe('Whether to skip Level 3 (codebase context) analysis'),
442
- tier: z.enum(['fast', 'balanced', 'thorough']).default('balanced')
452
+ tier: z.enum(ALL_TIER_VALUES).default('balanced')
443
453
  .describe('Analysis tier: fast (surface), balanced (standard), or thorough (deep)'),
444
454
  },
445
455
  async (args) => {
446
- // Track analysisId and key for cleanup in catch block (must be outside try scope)
456
+ // Track analysisId and reviewId for cleanup in catch block (must be outside try scope)
447
457
  let analysisId = null;
448
- let trackingKey = null;
449
- let trackingMap = null;
458
+ let trackingReviewId = null;
450
459
 
451
460
  try {
452
461
  const reviewRepo = new ReviewRepository(db);
@@ -514,8 +523,7 @@ function createMCPServer(db, options = {}) {
514
523
  });
515
524
 
516
525
  // Concurrent analysis guard: check if one is already running
517
- const reviewKey = getLocalReviewKey(reviewId);
518
- const existingAnalysisId = localReviewToAnalysisId.get(reviewKey);
526
+ const existingAnalysisId = reviewToAnalysisId.get(reviewId);
519
527
  if (existingAnalysisId && activeAnalyses.get(existingAnalysisId)?.status === 'running') {
520
528
  return {
521
529
  content: [{
@@ -540,8 +548,7 @@ function createMCPServer(db, options = {}) {
540
548
  // Create unified run/analysis ID and DB record immediately
541
549
  const runId = uuidv4();
542
550
  analysisId = runId;
543
- trackingKey = reviewKey;
544
- trackingMap = localReviewToAnalysisId;
551
+ trackingReviewId = reviewId;
545
552
 
546
553
  const requestInstructions = args.customInstructions?.trim() || null;
547
554
  const repoInstructions = repoSettings?.default_instructions || null;
@@ -566,10 +573,11 @@ function createMCPServer(db, options = {}) {
566
573
  };
567
574
  activeAnalyses.set(analysisId, initialStatus);
568
575
 
569
- // Store local review to analysis ID mapping
570
- localReviewToAnalysisId.set(reviewKey, analysisId);
576
+ // Store review to analysis ID mapping (unified map)
577
+ reviewToAnalysisId.set(reviewId, analysisId);
571
578
 
572
579
  broadcastProgress(analysisId, initialStatus);
580
+ broadcastReviewEvent(reviewId, { type: 'review:analysis_started', analysisId });
573
581
 
574
582
  // Create analyzer and launch asynchronously
575
583
  const analyzer = new Analyzer(db, model, provider);
@@ -592,7 +600,7 @@ function createMCPServer(db, options = {}) {
592
600
  }
593
601
 
594
602
  const progressCallback = createProgressCallback(analysisId);
595
- const tier = args.tier;
603
+ const tier = resolveTier(args.tier);
596
604
 
597
605
  logger.log('MCP', `Starting local analysis: review #${reviewId}, runId=${runId}`, 'magenta');
598
606
 
@@ -604,6 +612,7 @@ function createMCPServer(db, options = {}) {
604
612
  reviewId,
605
613
  provider,
606
614
  model,
615
+ tier,
607
616
  repoInstructions,
608
617
  requestInstructions,
609
618
  headSha: localHeadSha
@@ -618,7 +627,7 @@ function createMCPServer(db, options = {}) {
618
627
  }))
619
628
  .catch(error => handleAnalysisFailure(analysisId, error, `local review #${reviewId}`))
620
629
  .finally(() => {
621
- localReviewToAnalysisId.delete(reviewKey);
630
+ reviewToAnalysisId.delete(reviewId);
622
631
  });
623
632
 
624
633
  return {
@@ -645,18 +654,16 @@ function createMCPServer(db, options = {}) {
645
654
  const repository = normalizeRepository(owner, repo);
646
655
 
647
656
  // Concurrent analysis guard: check if one is already running
648
- // Use normalized repository to ensure case-insensitive matching
649
- const prKey = `${repository}/${prNumber}`;
650
- const existingAnalysisId = prToAnalysisId.get(prKey);
657
+ // First need the review to get the integer reviewId for the unified map
658
+ const existingReviewForGuard = await reviewRepo.getReviewByPR(prNumber, repository);
659
+ const existingAnalysisId = existingReviewForGuard ? reviewToAnalysisId.get(existingReviewForGuard.id) : null;
651
660
  if (existingAnalysisId && activeAnalyses.get(existingAnalysisId)?.status === 'running') {
652
- // Look up the review to return its ID
653
- const existingReview = await reviewRepo.getReviewByPR(prNumber, repository);
654
661
  return {
655
662
  content: [{
656
663
  type: 'text',
657
664
  text: JSON.stringify({
658
665
  analysisId: existingAnalysisId,
659
- reviewId: existingReview?.id || null,
666
+ reviewId: existingReviewForGuard?.id || null,
660
667
  status: 'already_running',
661
668
  message: 'An analysis is already running for this PR'
662
669
  }, null, 2)
@@ -689,8 +696,7 @@ function createMCPServer(db, options = {}) {
689
696
  // Create unified run/analysis ID and DB record immediately
690
697
  const runId = uuidv4();
691
698
  analysisId = runId;
692
- trackingKey = prKey;
693
- trackingMap = prToAnalysisId;
699
+ trackingReviewId = review.id;
694
700
 
695
701
  // Save custom instructions if provided
696
702
  const requestInstructions = args.customInstructions?.trim() || null;
@@ -702,6 +708,7 @@ function createMCPServer(db, options = {}) {
702
708
 
703
709
  const initialStatus = {
704
710
  id: analysisId,
711
+ reviewId: review.id,
705
712
  prNumber,
706
713
  repository,
707
714
  reviewType: 'pr',
@@ -719,13 +726,14 @@ function createMCPServer(db, options = {}) {
719
726
  };
720
727
  activeAnalyses.set(analysisId, initialStatus);
721
728
 
722
- prToAnalysisId.set(prKey, analysisId);
729
+ reviewToAnalysisId.set(review.id, analysisId);
723
730
 
724
731
  broadcastProgress(analysisId, initialStatus);
732
+ broadcastReviewEvent(review.id, { type: 'review:analysis_started', analysisId });
725
733
 
726
734
  const analyzer = new Analyzer(db, model, provider);
727
735
  const progressCallback = createProgressCallback(analysisId);
728
- const tier = args.tier;
736
+ const tier = resolveTier(args.tier);
729
737
 
730
738
  logger.log('MCP', `Starting PR analysis: PR #${prNumber} in ${repository}, runId=${runId}`, 'magenta');
731
739
 
@@ -737,6 +745,7 @@ function createMCPServer(db, options = {}) {
737
745
  reviewId: review.id,
738
746
  provider,
739
747
  model,
748
+ tier,
740
749
  repoInstructions,
741
750
  requestInstructions,
742
751
  headSha: prMetadata.head_sha || null
@@ -752,7 +761,7 @@ function createMCPServer(db, options = {}) {
752
761
  }))
753
762
  .catch(error => handleAnalysisFailure(analysisId, error, `PR #${prNumber}`))
754
763
  .finally(() => {
755
- prToAnalysisId.delete(prKey);
764
+ reviewToAnalysisId.delete(review.id);
756
765
  });
757
766
 
758
767
  return {
@@ -790,8 +799,8 @@ function createMCPServer(db, options = {}) {
790
799
  await analysisRunRepo.update(analysisId, { status: 'failed' });
791
800
  } catch (_) { /* record may not exist yet */ }
792
801
  }
793
- if (trackingKey && trackingMap) {
794
- trackingMap.delete(trackingKey);
802
+ if (trackingReviewId != null) {
803
+ reviewToAnalysisId.delete(trackingReviewId);
795
804
  }
796
805
 
797
806
  logger.error(`MCP start_analysis error: ${error.message}`);