@in-the-loop-labs/pair-review 3.0.1 → 3.0.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@in-the-loop-labs/pair-review",
3
- "version": "3.0.1",
3
+ "version": "3.0.2",
4
4
  "description": "Your AI-powered code review partner - Close the feedback loop with AI coding agents",
5
5
  "main": "src/server.js",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pair-review",
3
- "version": "3.0.1",
3
+ "version": "3.0.2",
4
4
  "description": "pair-review app integration — Open PRs and local changes in the pair-review web UI, run server-side AI analysis, and address review feedback. Requires the pair-review MCP server.",
5
5
  "author": {
6
6
  "name": "in-the-loop-labs",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-critic",
3
- "version": "3.0.1",
3
+ "version": "3.0.2",
4
4
  "description": "AI-powered code review analysis — Run three-level AI analysis and implement-review-fix loops directly in your coding agent. Works standalone, no server required.",
5
5
  "author": {
6
6
  "name": "in-the-loop-labs",
@@ -674,6 +674,10 @@ class LocalManager {
674
674
  // User cancelled — keep old diff, early return
675
675
  return;
676
676
  }
677
+ // resolved is the response object — merge branchAvailable into result
678
+ if (resolved.branchAvailable !== undefined) {
679
+ result.branchAvailable = resolved.branchAvailable;
680
+ }
677
681
  }
678
682
  // Branch scope: backend already updated SHA and persisted diff — fall through
679
683
  }
@@ -702,7 +706,7 @@ class LocalManager {
702
706
  /**
703
707
  * Handle a non-branch-scope HEAD SHA change.
704
708
  * Shows a 3-option dialog (or auto-updates in silent mode).
705
- * @returns {boolean} true if the session was updated in-place (caller should apply diff),
709
+ * @returns {Object|false} The response data object if the session was updated in-place (caller should apply diff),
706
710
  * false if cancelled or redirecting away (caller should skip _applyRefreshedDiff)
707
711
  */
708
712
  async _resolveHeadChange(result, opts) {
@@ -771,8 +775,9 @@ class LocalManager {
771
775
  return false; // navigating away — caller must not fire _applyRefreshedDiff
772
776
  }
773
777
 
774
- // action === 'updated' — session SHA + diff updated, continue to reload
775
- return true;
778
+ // action === 'updated' — session SHA + diff updated, continue to reload.
779
+ // Return the response data so the caller can extract branchAvailable, etc.
780
+ return data;
776
781
  }
777
782
 
778
783
  /**
@@ -806,6 +811,11 @@ class LocalManager {
806
811
  // refresh must call unconditionally since the manager won't re-fire its callback.
807
812
  await manager.loadAISuggestions(null, manager.selectedRunId);
808
813
 
814
+ // Update branchAvailable on the scope selector if the backend sent an updated value
815
+ if (result.branchAvailable !== undefined && manager.diffOptionsDropdown) {
816
+ manager.diffOptionsDropdown.branchAvailable = result.branchAvailable;
817
+ }
818
+
809
819
  // Clear stale state after successful refresh
810
820
  manager._hideStaleBadge();
811
821
  manager._stalenessPromise = null;
package/src/database.js CHANGED
@@ -2717,6 +2717,7 @@ class ReviewRepository {
2717
2717
  * @param {Object} [updates.reviewData] - Additional review data (will be JSON stringified)
2718
2718
  * @param {string} [updates.customInstructions] - Custom instructions used for AI analysis
2719
2719
  * @param {string} [updates.summary] - AI analysis summary
2720
+ * @param {string} [updates.local_head_sha] - Local HEAD SHA
2720
2721
  * @param {Date|string} [updates.submittedAt] - Submission timestamp
2721
2722
  * @returns {Promise<boolean>} True if record was updated
2722
2723
  */
@@ -2759,6 +2760,11 @@ class ReviewRepository {
2759
2760
  params.push(updates.local_head_branch);
2760
2761
  }
2761
2762
 
2763
+ if (updates.local_head_sha !== undefined) {
2764
+ setClauses.push('local_head_sha = ?');
2765
+ params.push(updates.local_head_sha);
2766
+ }
2767
+
2762
2768
  if (updates.submittedAt !== undefined) {
2763
2769
  setClauses.push('submitted_at = ?');
2764
2770
  const submittedAt = updates.submittedAt instanceof Date
@@ -66,6 +66,34 @@ function deleteLocalReviewDiff(reviewId) {
66
66
  localReviewDiffs.delete(toIntKey(reviewId));
67
67
  }
68
68
 
69
+ /**
70
+ * Check whether branch scope should be selectable in the scope range selector.
71
+ * Returns true when the branch has commits ahead of the base branch.
72
+ * Non-fatal: returns false on any error.
73
+ */
74
+ async function checkBranchAvailable(localPath, branchName, scopeStart, config, repositoryName) {
75
+ if (includesBranch(scopeStart)) return true;
76
+ if (!branchName || branchName === 'HEAD' || branchName === 'unknown' || !localPath) return false;
77
+ try {
78
+ const baseBranch = require('../git/base-branch');
79
+ const depsOverride = getGitHubToken(config) ? { getGitHubToken: () => getGitHubToken(config) } : undefined;
80
+ const detection = await baseBranch.detectBaseBranch(localPath, branchName, {
81
+ repository: repositoryName,
82
+ enableGraphite: config.enable_graphite === true,
83
+ _deps: depsOverride
84
+ });
85
+ if (detection) {
86
+ // Lazy require to ensure testability via vi.spyOn on the module exports
87
+ const localReview = require('../local-review');
88
+ const commitCount = await localReview.getBranchCommitCount(localPath, detection.baseBranch);
89
+ return commitCount > 0;
90
+ }
91
+ } catch {
92
+ // Non-fatal — branch stop stays disabled
93
+ }
94
+ return false;
95
+ }
96
+
69
97
  /**
70
98
  * Delete a local review session and its in-memory diff cache.
71
99
  * Shared by both single-delete and bulk-delete routes.
@@ -575,26 +603,9 @@ router.get('/api/local/:reviewId', async (req, res) => {
575
603
  // Determine if Branch stop should be selectable in the scope range selector.
576
604
  // This is independent of branchInfo (which guards on no uncommitted changes).
577
605
  // Branch is available when: not detached HEAD, not on default branch, and has commits ahead.
578
- let branchAvailable = includesBranch(scopeStart) || Boolean(branchInfo);
579
- if (!branchAvailable && branchName && branchName !== 'HEAD' && branchName !== 'unknown' && review.local_path) {
580
- try {
581
- const { getBranchCommitCount } = require('../local-review');
582
- const { detectBaseBranch } = require('../git/base-branch');
583
- const config = req.app.get('config') || {};
584
- const depsOverride = getGitHubToken(config) ? { getGitHubToken: () => getGitHubToken(config) } : undefined;
585
- const detection = await detectBaseBranch(review.local_path, branchName, {
586
- repository: repositoryName,
587
- enableGraphite: config.enable_graphite === true,
588
- _deps: depsOverride
589
- });
590
- if (detection) {
591
- const commitCount = await getBranchCommitCount(review.local_path, detection.baseBranch);
592
- branchAvailable = commitCount > 0;
593
- }
594
- } catch {
595
- // Non-fatal — branch stop stays disabled
596
- }
597
- }
606
+ const branchAvailable = Boolean(branchInfo) || await checkBranchAvailable(
607
+ review.local_path, branchName, scopeStart, req.app.get('config') || {}, repositoryName
608
+ );
598
609
 
599
610
  // Compute SHA abbreviation length from the repo's git config
600
611
  const shaAbbrevLength = getShaAbbrevLength(review.local_path);
@@ -1364,6 +1375,15 @@ router.post('/api/local/:reviewId/refresh', async (req, res) => {
1364
1375
  logger.warn(`Could not check HEAD SHA: ${headError.message}`);
1365
1376
  }
1366
1377
 
1378
+ // Recompute branchAvailable so the frontend can update the scope selector
1379
+ // (e.g. after a commit creates the first branch-ahead commit).
1380
+ const config = req.app.get('config') || {};
1381
+ let branchName;
1382
+ try { branchName = await getCurrentBranch(localPath); } catch (_) { branchName = review.local_head_branch || null; }
1383
+ const branchAvailable = await checkBranchAvailable(
1384
+ localPath, branchName, scopeStart, config, review.repository
1385
+ );
1386
+
1367
1387
  // Non-branch HEAD change: skip diff computation entirely — the old diff is
1368
1388
  // preserved until the user decides (via resolve-head-change) what to do.
1369
1389
  // The resolve-head-change endpoint will recompute the diff for whichever
@@ -1373,6 +1393,7 @@ router.post('/api/local/:reviewId/refresh', async (req, res) => {
1373
1393
  success: true,
1374
1394
  message: 'HEAD changed — awaiting user decision',
1375
1395
  headShaChanged,
1396
+ branchAvailable,
1376
1397
  previousHeadSha: originalHeadSha,
1377
1398
  currentHeadSha: currentHeadSha || null,
1378
1399
  stats: {}
@@ -1397,6 +1418,7 @@ router.post('/api/local/:reviewId/refresh', async (req, res) => {
1397
1418
  success: true,
1398
1419
  message: 'Diff refreshed successfully',
1399
1420
  headShaChanged,
1421
+ branchAvailable,
1400
1422
  previousHeadSha: originalHeadSha,
1401
1423
  currentHeadSha: currentHeadSha || null,
1402
1424
  stats: {
@@ -1453,17 +1475,24 @@ router.post('/api/local/:reviewId/resolve-head-change', async (req, res) => {
1453
1475
  const scopeEnd = review.local_scope_end || DEFAULT_SCOPE.end;
1454
1476
 
1455
1477
  if (action === 'update') {
1456
- // Check for UNIQUE conflict before updating
1457
- const headBranch = review.local_head_branch || null;
1478
+ // Read live branch may differ from stored value after a checkout.
1479
+ // Lazy require to ensure testability via vi.spyOn on the module exports.
1480
+ let headBranch;
1481
+ try { headBranch = await require('../local-review').getCurrentBranch(localPath); } catch (_) { headBranch = review.local_head_branch || null; }
1482
+
1483
+ // Check for UNIQUE conflict before any mutation.
1484
+ // Use the live branch + new SHA so the conflict check targets the
1485
+ // final identity tuple (localPath, newHeadSha, headBranch).
1458
1486
  const conflict = await reviewRepo.getLocalReview(localPath, newHeadSha, headBranch);
1459
1487
  if (conflict && conflict.id !== reviewId) {
1460
1488
  logger.log('API', `UNIQUE conflict: session #${conflict.id} already exists for this HEAD`, 'yellow');
1461
1489
  return res.json({ success: true, action: 'redirect', sessionId: conflict.id });
1462
1490
  }
1463
1491
 
1464
- // Update SHA
1465
- await reviewRepo.updateLocalHeadSha(reviewId, newHeadSha);
1466
- logger.log('API', `Updated HEAD SHA on session ${reviewId}`, 'cyan');
1492
+ // Persist SHA and branch together in a single write so SQLite only
1493
+ // ever sees the final identity tuple — no transient intermediate state.
1494
+ await reviewRepo.updateReview(reviewId, { local_head_sha: newHeadSha, local_head_branch: headBranch });
1495
+ logger.log('API', `Updated HEAD SHA and branch on session ${reviewId}`, 'cyan');
1467
1496
 
1468
1497
  // Recompute and persist diff
1469
1498
  const scopedResult = await generateScopedDiff(localPath, scopeStart, scopeEnd, review.local_base_branch);
@@ -1475,7 +1504,14 @@ router.post('/api/local/:reviewId/resolve-head-change', async (req, res) => {
1475
1504
  logger.warn(`Could not persist diff to database: ${persistError.message}`);
1476
1505
  }
1477
1506
 
1478
- return res.json({ success: true, action: 'updated' });
1507
+ // Recompute branchAvailable the commit may have created the first
1508
+ // branch-ahead commit, making the Branch scope stop selectable.
1509
+ const config = req.app.get('config') || {};
1510
+ const branchAvailable = await checkBranchAvailable(
1511
+ localPath, headBranch, scopeStart, config, review.repository
1512
+ );
1513
+
1514
+ return res.json({ success: true, action: 'updated', branchAvailable });
1479
1515
  }
1480
1516
 
1481
1517
  // action === 'new-session'