@in-the-loop-labs/pair-review 1.4.2 → 1.4.4
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 +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/public/js/local.js +1 -1
- package/public/js/modules/comment-manager.js +10 -5
- package/public/js/modules/diff-renderer.js +58 -0
- package/public/js/pr.js +17 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pair-review",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.4",
|
|
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.
|
|
3
|
+
"version": "1.4.4",
|
|
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",
|
package/public/js/local.js
CHANGED
|
@@ -650,7 +650,7 @@ class LocalManager {
|
|
|
650
650
|
data-file="${fileName}"
|
|
651
651
|
data-line="${lineStart}"
|
|
652
652
|
data-line-end="${lineEnd}"
|
|
653
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1459
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
1557
|
-
//
|
|
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
|
-
|
|
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>
|