@in-the-loop-labs/pair-review 1.4.2 → 1.4.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@in-the-loop-labs/pair-review",
3
- "version": "1.4.2",
3
+ "version": "1.4.3",
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": "1.4.2",
3
+ "version": "1.4.3",
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": "1.4.2",
3
+ "version": "1.4.3",
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",
@@ -650,7 +650,7 @@ class LocalManager {
650
650
  data-file="${fileName}"
651
651
  data-line="${lineStart}"
652
652
  data-line-end="${lineEnd}"
653
- ${side ? `data-side="${side}"` : ''}
653
+ data-side="${side || 'RIGHT'}"
654
654
  >${manager.escapeHtml(currentText)}</textarea>
655
655
  <div class="comment-edit-actions">
656
656
  <button class="btn btn-sm btn-primary save-edit-btn">Save</button>
@@ -245,12 +245,14 @@ class CommentManager {
245
245
  const rows = targetWrapper.querySelectorAll('tr[data-line-number]');
246
246
  const codeLines = [];
247
247
 
248
+ // Always filter by side to prevent including both OLD and NEW versions of modified lines.
249
+ // Default to 'RIGHT' because suggestions target the NEW version of code.
250
+ // This is the definitive fix: even if callers fail to propagate side, we never return both versions.
251
+ const effectiveSide = side || 'RIGHT';
252
+
248
253
  for (const row of rows) {
249
254
  const lineNum = parseInt(row.dataset.lineNumber, 10);
250
- // Filter by line number, file name, and side (if provided)
251
- // Side filtering prevents including both deleted and added versions of modified lines
252
- const matchesSide = !side || row.dataset.side === side;
253
- if (lineNum >= startLine && lineNum <= endLine && row.dataset.fileName === fileName && matchesSide) {
255
+ if (lineNum >= startLine && lineNum <= endLine && row.dataset.fileName === fileName && row.dataset.side === effectiveSide) {
254
256
  // Get the code content cell
255
257
  const codeCell = row.querySelector('.d2h-code-line-ctn');
256
258
  if (codeCell) {
@@ -279,6 +281,9 @@ class CommentManager {
279
281
  const startLine = parseInt(textarea.dataset.line, 10);
280
282
  const endLine = parseInt(textarea.dataset.lineEnd, 10) || startLine;
281
283
  const side = textarea.dataset.side;
284
+ if (!side) {
285
+ console.warn('[Suggestion] textarea missing data-side attribute, defaulting to RIGHT');
286
+ }
282
287
 
283
288
  // Get the code from the selected lines (pass side to avoid including both deleted and added lines)
284
289
  const code = this.getCodeFromLines(fileName, startLine, endLine, side);
@@ -587,7 +592,7 @@ class CommentManager {
587
592
  data-file="${comment.file}"
588
593
  data-line="${comment.line_start}"
589
594
  data-line-end="${comment.line_end || comment.line_start}"
590
- ${comment.side ? `data-side="${comment.side}"` : ''}
595
+ data-side="${comment.side || 'RIGHT'}"
591
596
  >${escapeHtml(comment.body)}</textarea>
592
597
  <div class="comment-edit-actions">
593
598
  <button class="btn btn-sm btn-primary save-edit-btn">
@@ -517,6 +517,8 @@ class DiffRenderer {
517
517
 
518
518
  const headerContent = functionContext
519
519
  ? `<span class="hunk-context-icon" aria-label="Function context">f</span><span class="hunk-context-text">${DiffRenderer.escapeHtml(functionContext)}</span>`
520
+ // "..." dividers act as visual separators between non-contiguous code
521
+ // sections when git couldn't identify a surrounding function/class scope.
520
522
  : '<span class="hunk-divider" aria-label="Code section divider">...</span>';
521
523
 
522
524
  headerRow.innerHTML = `<td colspan="2" class="d2h-info">${headerContent}</td>`;
@@ -554,6 +556,62 @@ class DiffRenderer {
554
556
  trimmedLine.includes(trimmedContext + ' ');
555
557
  }
556
558
 
559
+ /**
560
+ * Handle hunk header rows that are no longer at a gap boundary.
561
+ * A hunk header (d2h-info row) should only be visible when:
562
+ * 1. It is the first row in the tbody (file-level context), OR
563
+ * 2. Its immediately preceding sibling is a gap row (context-expand-row)
564
+ *
565
+ * When a header with function context (_f_ marker) becomes stranded after
566
+ * upward expansion, it is relocated to the nearest gap row above within the
567
+ * same hunk section. This preserves the function context indicator for the
568
+ * remaining gap, whether or not the function definition is now visible below.
569
+ *
570
+ * Headers without function context (... dividers) or those with no reachable
571
+ * gap above are removed.
572
+ * @param {HTMLElement} tbody - The table body containing the diff
573
+ */
574
+ static removeStrandedHunkHeaders(tbody) {
575
+ if (!tbody) return;
576
+
577
+ const infoRows = tbody.querySelectorAll('tr.d2h-info');
578
+ for (const row of infoRows) {
579
+ const prev = row.previousElementSibling;
580
+ // Keep if this is the first row in the tbody (provides file-level context)
581
+ if (!prev) continue;
582
+ // Keep if preceded by a gap row (the header marks the boundary)
583
+ if (prev.classList.contains('context-expand-row')) continue;
584
+
585
+ // Header is stranded between visible code lines.
586
+ // If it has function context, try to relocate it to the nearest gap above
587
+ // so the gap retains its function context indicator.
588
+ if (row.dataset.functionContext) {
589
+ let sibling = row.previousElementSibling;
590
+ let nearestGap = null;
591
+ while (sibling) {
592
+ if (sibling.classList.contains('context-expand-row')) {
593
+ nearestGap = sibling;
594
+ break;
595
+ }
596
+ // Stop at another hunk header - don't cross hunk boundaries.
597
+ // (This also means only the nearest stranded header in DOM order
598
+ // gets relocated to a given gap; subsequent ones are removed.)
599
+ if (sibling.classList.contains('d2h-info')) break;
600
+ sibling = sibling.previousElementSibling;
601
+ }
602
+
603
+ if (nearestGap) {
604
+ // Relocate: move the header to right after the gap row
605
+ nearestGap.insertAdjacentElement('afterend', row);
606
+ continue;
607
+ }
608
+ }
609
+
610
+ // No gap found above or no function context - remove the stranded header
611
+ row.remove();
612
+ }
613
+ }
614
+
557
615
  /**
558
616
  * Update function context visibility for all hunk headers in a file's tbody
559
617
  * Called once after any expansion in a file - checks all headers efficiently
package/public/js/pr.js CHANGED
@@ -1455,9 +1455,10 @@ class PRManager {
1455
1455
  gapRow.parentNode.insertBefore(fragment, gapRow);
1456
1456
  gapRow.remove();
1457
1457
 
1458
- // Check all function context markers in this file and remove any whose
1459
- // function definitions are now visible
1458
+ // Remove hunk headers that are no longer at a gap boundary,
1459
+ // then check remaining headers for visible function definitions
1460
1460
  if (window.DiffRenderer) {
1461
+ window.DiffRenderer.removeStrandedHunkHeaders(tbody);
1461
1462
  window.DiffRenderer.updateFunctionContextVisibility(tbody);
1462
1463
  }
1463
1464
 
@@ -1478,6 +1479,7 @@ class PRManager {
1478
1479
  const hasExplicitEndLineNew = !isNaN(parseInt(controls.dataset.endLineNew));
1479
1480
 
1480
1481
  const fileName = controls.dataset.fileName;
1482
+ const position = controls.dataset.position || 'between';
1481
1483
  const tbody = gapRow.closest('tbody');
1482
1484
 
1483
1485
  if (!tbody) return;
@@ -1488,6 +1490,13 @@ class PRManager {
1488
1490
 
1489
1491
  const fragment = document.createDocumentFragment();
1490
1492
 
1493
+ // Compute positions for each remnant based on file boundary proximity.
1494
+ // The upper remnant keeps 'above' only if the original gap was at the file start;
1495
+ // the lower remnant keeps 'below' only if the original gap was at the file end.
1496
+ // Inner remnants become 'between' since they're sandwiched between visible content.
1497
+ const gapAbovePosition = position === 'above' ? 'above' : 'between';
1498
+ const gapBelowPosition = position === 'below' ? 'below' : 'between';
1499
+
1491
1500
  // Create gap above if needed
1492
1501
  const gapAboveSize = expandStart - gapStart;
1493
1502
  if (gapAboveSize > 0) {
@@ -1496,7 +1505,7 @@ class PRManager {
1496
1505
  gapStart,
1497
1506
  expandStart - 1,
1498
1507
  gapAboveSize,
1499
- 'above',
1508
+ gapAbovePosition,
1500
1509
  (controls, dir, cnt) => this.expandGapContext(controls, dir, cnt),
1501
1510
  gapStartNew // Preserve the NEW line number offset
1502
1511
  );
@@ -1537,7 +1546,7 @@ class PRManager {
1537
1546
  expandEnd + 1,
1538
1547
  gapEnd,
1539
1548
  gapBelowSize,
1540
- 'below',
1549
+ gapBelowPosition,
1541
1550
  (controls, dir, cnt) => this.expandGapContext(controls, dir, cnt),
1542
1551
  belowGapStartNew // Updated NEW line number for gap below
1543
1552
  );
@@ -1553,9 +1562,10 @@ class PRManager {
1553
1562
  gapRow.parentNode.insertBefore(fragment, gapRow);
1554
1563
  gapRow.remove();
1555
1564
 
1556
- // Check all function context markers in this file and remove any whose
1557
- // function definitions are now visible
1565
+ // Remove hunk headers that are no longer at a gap boundary,
1566
+ // then check remaining headers for visible function definitions
1558
1567
  if (window.DiffRenderer) {
1568
+ window.DiffRenderer.removeStrandedHunkHeaders(tbody);
1559
1569
  window.DiffRenderer.updateFunctionContextVisibility(tbody);
1560
1570
  }
1561
1571
 
@@ -1718,10 +1728,6 @@ class PRManager {
1718
1728
  this.commentManager.updateSuggestionButtonState(textarea, button);
1719
1729
  }
1720
1730
 
1721
- getCodeFromLines(fileName, startLine, endLine) {
1722
- return this.commentManager.getCodeFromLines(fileName, startLine, endLine);
1723
- }
1724
-
1725
1731
  insertSuggestionBlock(textarea, button) {
1726
1732
  this.commentManager.insertSuggestionBlock(textarea, button);
1727
1733
  }
@@ -1783,7 +1789,7 @@ class PRManager {
1783
1789
  data-file="${fileName}"
1784
1790
  data-line="${lineStart}"
1785
1791
  data-line-end="${lineEnd}"
1786
- ${side ? `data-side="${side}"` : ''}
1792
+ data-side="${side || 'RIGHT'}"
1787
1793
  >${this.escapeHtml(currentText)}</textarea>
1788
1794
  <div class="comment-edit-actions">
1789
1795
  <button class="btn btn-sm btn-primary save-edit-btn">Save</button>