@in-the-loop-labs/pair-review 2.4.2 → 2.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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pair-review",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.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": "2.4.
|
|
3
|
+
"version": "2.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/css/pr.css
CHANGED
|
@@ -1188,8 +1188,7 @@
|
|
|
1188
1188
|
background: var(--color-bg-primary);
|
|
1189
1189
|
border: 1px solid var(--color-border-primary);
|
|
1190
1190
|
border-radius: 8px;
|
|
1191
|
-
overflow
|
|
1192
|
-
overflow-y: visible;
|
|
1191
|
+
overflow: visible; /* Must be visible so sticky file headers work relative to .diff-view */
|
|
1193
1192
|
position: relative;
|
|
1194
1193
|
min-width: 0;
|
|
1195
1194
|
padding-bottom: 1px;
|
|
@@ -1335,7 +1334,7 @@
|
|
|
1335
1334
|
/* Diff styling improvements */
|
|
1336
1335
|
.d2h-file-wrapper[data-file-name] {
|
|
1337
1336
|
margin-bottom: 24px;
|
|
1338
|
-
overflow-x:
|
|
1337
|
+
overflow-x: visible; /* Must be visible for sticky file headers to work */
|
|
1339
1338
|
max-width: 100%;
|
|
1340
1339
|
}
|
|
1341
1340
|
|
|
@@ -1348,6 +1347,9 @@
|
|
|
1348
1347
|
border-bottom: 1px solid var(--color-border-primary);
|
|
1349
1348
|
font-weight: 600;
|
|
1350
1349
|
font-size: 14px;
|
|
1350
|
+
position: sticky;
|
|
1351
|
+
top: var(--toolbar-height, 0px);
|
|
1352
|
+
z-index: 4;
|
|
1351
1353
|
}
|
|
1352
1354
|
|
|
1353
1355
|
.d2h-file-name {
|
|
@@ -1540,7 +1542,8 @@
|
|
|
1540
1542
|
color: #f85149;
|
|
1541
1543
|
}
|
|
1542
1544
|
|
|
1543
|
-
/* Hide diff
|
|
1545
|
+
/* Hide diff content when collapsed */
|
|
1546
|
+
.d2h-file-wrapper.collapsed .d2h-file-body,
|
|
1544
1547
|
.d2h-file-wrapper.collapsed .d2h-diff-table {
|
|
1545
1548
|
display: none;
|
|
1546
1549
|
}
|
|
@@ -1560,6 +1563,12 @@
|
|
|
1560
1563
|
cursor: pointer;
|
|
1561
1564
|
}
|
|
1562
1565
|
|
|
1566
|
+
/* Scrollable wrapper for diff tables — provides per-file horizontal scroll
|
|
1567
|
+
now that .d2h-file-wrapper and .diff-container use overflow:visible for sticky headers */
|
|
1568
|
+
.d2h-file-body {
|
|
1569
|
+
overflow-x: auto;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1563
1572
|
.d2h-diff-table {
|
|
1564
1573
|
width: 100%;
|
|
1565
1574
|
border-collapse: collapse;
|
|
@@ -6288,6 +6297,9 @@ body:not([data-theme="dark"]) .theme-icon-light {
|
|
|
6288
6297
|
background: var(--color-bg-primary);
|
|
6289
6298
|
border-bottom: 1px solid var(--color-border-primary);
|
|
6290
6299
|
flex-shrink: 0;
|
|
6300
|
+
position: sticky;
|
|
6301
|
+
top: 0;
|
|
6302
|
+
z-index: 5;
|
|
6291
6303
|
}
|
|
6292
6304
|
|
|
6293
6305
|
.diff-toolbar .sidebar-toggle-collapsed {
|
|
@@ -6759,11 +6771,10 @@ body:not([data-theme="dark"]) .theme-icon-light {
|
|
|
6759
6771
|
}
|
|
6760
6772
|
|
|
6761
6773
|
.main-layout .diff-container {
|
|
6762
|
-
flex: 1;
|
|
6774
|
+
flex: 1 0 auto; /* Grow to fill, but don't shrink below content so .diff-view scrolls */
|
|
6763
6775
|
min-width: 0; /* Prevent flex item from expanding beyond container */
|
|
6764
6776
|
padding: 16px;
|
|
6765
|
-
overflow
|
|
6766
|
-
overflow-x: auto; /* Allow scroll as fallback for unbreakable content */
|
|
6777
|
+
overflow: visible; /* Must be visible so sticky file headers work relative to .diff-view */
|
|
6767
6778
|
}
|
|
6768
6779
|
|
|
6769
6780
|
/* --------------------------------------------------------------------------
|
|
@@ -12277,9 +12288,12 @@ body.resizing * {
|
|
|
12277
12288
|
font-size: 14px;
|
|
12278
12289
|
}
|
|
12279
12290
|
|
|
12280
|
-
/*
|
|
12291
|
+
/* Don't grow (keeps badge next to text), shrink for long paths, clip from the left so the filename stays visible */
|
|
12281
12292
|
.context-file-header .d2h-file-name {
|
|
12282
|
-
flex:
|
|
12293
|
+
flex: 0 1 auto;
|
|
12294
|
+
direction: rtl;
|
|
12295
|
+
unicode-bidi: plaintext;
|
|
12296
|
+
text-overflow: ellipsis;
|
|
12283
12297
|
}
|
|
12284
12298
|
|
|
12285
12299
|
/* Push viewed checkbox (and everything after it) to the right */
|
package/public/js/index.js
CHANGED
|
@@ -402,11 +402,6 @@
|
|
|
402
402
|
state.fetchedAt = data.fetched_at;
|
|
403
403
|
|
|
404
404
|
renderCollectionTable(container, state, collection);
|
|
405
|
-
|
|
406
|
-
// Auto-refresh on first load if cache is empty
|
|
407
|
-
if (state.prs.length === 0 && !state.fetchedAt) {
|
|
408
|
-
refreshCollectionPrs(collection, containerId, state);
|
|
409
|
-
}
|
|
410
405
|
} catch (error) {
|
|
411
406
|
console.error('Error loading ' + collection + ':', error);
|
|
412
407
|
container.innerHTML =
|
|
@@ -1164,11 +1159,11 @@
|
|
|
1164
1159
|
if (collectionRow && !event.target.closest('a')) {
|
|
1165
1160
|
var prUrl = collectionRow.dataset.prUrl;
|
|
1166
1161
|
if (prUrl) {
|
|
1167
|
-
// Switch to PR tab to show loading state
|
|
1162
|
+
// Switch to PR tab to show loading state (do NOT persist to
|
|
1163
|
+
// localStorage – the user's intentional tab choice should be preserved)
|
|
1168
1164
|
var tabBar = document.getElementById('unified-tab-bar');
|
|
1169
1165
|
var prTabBtn = tabBar.querySelector('[data-tab="pr-tab"]');
|
|
1170
1166
|
switchTab(tabBar, prTabBtn);
|
|
1171
|
-
localStorage.setItem(TAB_STORAGE_KEY, 'pr-tab');
|
|
1172
1167
|
|
|
1173
1168
|
// Populate input and submit the form programmatically
|
|
1174
1169
|
var input = document.getElementById('pr-url-input');
|
|
@@ -1204,19 +1199,25 @@
|
|
|
1204
1199
|
const unifiedTabBtn = event.target.closest('#unified-tab-bar .tab-btn');
|
|
1205
1200
|
if (unifiedTabBtn) {
|
|
1206
1201
|
const tabBar = document.getElementById('unified-tab-bar');
|
|
1207
|
-
switchTab(tabBar, unifiedTabBtn, function (tabId) {
|
|
1202
|
+
switchTab(tabBar, unifiedTabBtn, async function (tabId) {
|
|
1208
1203
|
// Persist tab choice
|
|
1209
1204
|
localStorage.setItem(TAB_STORAGE_KEY, tabId);
|
|
1210
1205
|
// Lazy-load local reviews on first switch
|
|
1211
1206
|
if (tabId === 'local-tab' && !localReviewsPagination.loaded) {
|
|
1212
1207
|
loadLocalReviews();
|
|
1213
1208
|
}
|
|
1214
|
-
//
|
|
1215
|
-
if (tabId === 'review-requests-tab'
|
|
1216
|
-
|
|
1209
|
+
// Load cached data on first switch, then always refresh from GitHub
|
|
1210
|
+
if (tabId === 'review-requests-tab') {
|
|
1211
|
+
if (!reviewRequestsState.loaded) {
|
|
1212
|
+
await loadCollectionPrs('review-requests', 'review-requests-container', reviewRequestsState);
|
|
1213
|
+
}
|
|
1214
|
+
refreshCollectionPrs('review-requests', 'review-requests-container', reviewRequestsState);
|
|
1217
1215
|
}
|
|
1218
|
-
if (tabId === 'my-prs-tab'
|
|
1219
|
-
|
|
1216
|
+
if (tabId === 'my-prs-tab') {
|
|
1217
|
+
if (!myPrsState.loaded) {
|
|
1218
|
+
await loadCollectionPrs('my-prs', 'my-prs-container', myPrsState);
|
|
1219
|
+
}
|
|
1220
|
+
refreshCollectionPrs('my-prs', 'my-prs-container', myPrsState);
|
|
1220
1221
|
}
|
|
1221
1222
|
});
|
|
1222
1223
|
return;
|
|
@@ -1248,12 +1249,14 @@
|
|
|
1248
1249
|
loadLocalReviews();
|
|
1249
1250
|
}
|
|
1250
1251
|
|
|
1251
|
-
// If a GitHub collection tab is active, load
|
|
1252
|
+
// If a GitHub collection tab is active, load cached data then refresh from GitHub
|
|
1252
1253
|
if (savedTab === 'review-requests-tab') {
|
|
1253
|
-
loadCollectionPrs('review-requests', 'review-requests-container', reviewRequestsState)
|
|
1254
|
+
loadCollectionPrs('review-requests', 'review-requests-container', reviewRequestsState)
|
|
1255
|
+
.then(function () { refreshCollectionPrs('review-requests', 'review-requests-container', reviewRequestsState); });
|
|
1254
1256
|
}
|
|
1255
1257
|
if (savedTab === 'my-prs-tab') {
|
|
1256
|
-
loadCollectionPrs('my-prs', 'my-prs-container', myPrsState)
|
|
1258
|
+
loadCollectionPrs('my-prs', 'my-prs-container', myPrsState)
|
|
1259
|
+
.then(function () { refreshCollectionPrs('my-prs', 'my-prs-container', myPrsState); });
|
|
1257
1260
|
}
|
|
1258
1261
|
|
|
1259
1262
|
// Set up start review form handler
|
package/public/js/pr.js
CHANGED
|
@@ -180,6 +180,9 @@ class PRManager {
|
|
|
180
180
|
this.initAnalysisConfigModal();
|
|
181
181
|
this.initKeyboardShortcuts();
|
|
182
182
|
|
|
183
|
+
// Track toolbar height for sticky file headers (they sit below the sticky toolbar)
|
|
184
|
+
this._initToolbarHeightTracking();
|
|
185
|
+
|
|
183
186
|
// Initialize diff options dropdown (gear icon for whitespace toggle).
|
|
184
187
|
// Must happen before init() so the persisted hideWhitespace state is
|
|
185
188
|
// applied before the first loadAndDisplayFiles() call.
|
|
@@ -238,6 +241,27 @@ class PRManager {
|
|
|
238
241
|
};
|
|
239
242
|
}
|
|
240
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Keep --toolbar-height CSS variable in sync with the actual toolbar size
|
|
246
|
+
* so sticky file headers can position themselves below the sticky toolbar.
|
|
247
|
+
*/
|
|
248
|
+
_initToolbarHeightTracking() {
|
|
249
|
+
const toolbar = document.querySelector('.diff-toolbar');
|
|
250
|
+
if (!toolbar) return;
|
|
251
|
+
|
|
252
|
+
const update = () => {
|
|
253
|
+
document.documentElement.style.setProperty(
|
|
254
|
+
'--toolbar-height', toolbar.offsetHeight + 'px'
|
|
255
|
+
);
|
|
256
|
+
};
|
|
257
|
+
update();
|
|
258
|
+
|
|
259
|
+
// Re-measure when toolbar resizes (e.g. analysis dots appear/disappear)
|
|
260
|
+
if (typeof ResizeObserver !== 'undefined') {
|
|
261
|
+
new ResizeObserver(update).observe(toolbar);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
241
265
|
/**
|
|
242
266
|
* Set up event handlers
|
|
243
267
|
*/
|
|
@@ -1182,7 +1206,13 @@ class PRManager {
|
|
|
1182
1206
|
}
|
|
1183
1207
|
|
|
1184
1208
|
table.appendChild(tbody);
|
|
1185
|
-
|
|
1209
|
+
|
|
1210
|
+
// Wrap table in a scrollable container for horizontal scroll of long code lines
|
|
1211
|
+
// (parent elements use overflow:visible to support sticky file headers)
|
|
1212
|
+
const fileBody = document.createElement('div');
|
|
1213
|
+
fileBody.className = 'd2h-file-body';
|
|
1214
|
+
fileBody.appendChild(table);
|
|
1215
|
+
wrapper.appendChild(fileBody);
|
|
1186
1216
|
|
|
1187
1217
|
return wrapper;
|
|
1188
1218
|
}
|
|
@@ -4480,7 +4510,16 @@ class PRManager {
|
|
|
4480
4510
|
const tbody = document.createElement('tbody');
|
|
4481
4511
|
tbody.className = 'd2h-diff-tbody context-chunk';
|
|
4482
4512
|
tbody.dataset.contextId = contextFile.id;
|
|
4483
|
-
|
|
4513
|
+
|
|
4514
|
+
// Compute effective display range, shifting for end-of-file
|
|
4515
|
+
const clampedEnd = Math.min(contextFile.line_end, data.lines.length);
|
|
4516
|
+
const intendedSize = contextFile.line_end - contextFile.line_start + 1;
|
|
4517
|
+
let effectiveStart = contextFile.line_start;
|
|
4518
|
+
const actualSize = clampedEnd - effectiveStart + 1;
|
|
4519
|
+
if (actualSize < intendedSize && effectiveStart > 1) {
|
|
4520
|
+
effectiveStart = Math.max(1, effectiveStart - (intendedSize - actualSize));
|
|
4521
|
+
}
|
|
4522
|
+
tbody.dataset.lineStart = effectiveStart;
|
|
4484
4523
|
|
|
4485
4524
|
// Chunk header row with range label and per-chunk dismiss button
|
|
4486
4525
|
const headerRow = document.createElement('tr');
|
|
@@ -4493,8 +4532,7 @@ class PRManager {
|
|
|
4493
4532
|
contentTd.colSpan = 3;
|
|
4494
4533
|
const rangeLabel = document.createElement('span');
|
|
4495
4534
|
rangeLabel.className = 'context-range-label';
|
|
4496
|
-
|
|
4497
|
-
rangeLabel.textContent = `Lines ${contextFile.line_start}\u2013${lineEnd}`;
|
|
4535
|
+
rangeLabel.textContent = `Lines ${effectiveStart}\u2013${clampedEnd}`;
|
|
4498
4536
|
contentTd.appendChild(rangeLabel);
|
|
4499
4537
|
const chunkDismiss = document.createElement('button');
|
|
4500
4538
|
chunkDismiss.className = 'context-chunk-dismiss';
|
|
@@ -4508,25 +4546,22 @@ class PRManager {
|
|
|
4508
4546
|
headerRow.appendChild(contentTd);
|
|
4509
4547
|
tbody.appendChild(headerRow);
|
|
4510
4548
|
|
|
4511
|
-
const lineStart = contextFile.line_start;
|
|
4512
|
-
const clampedEnd = Math.min(contextFile.line_end, data.lines.length);
|
|
4513
|
-
|
|
4514
4549
|
// Add expand-up gap row if there are lines above the context range
|
|
4515
|
-
if (
|
|
4516
|
-
const gapAboveSize =
|
|
4550
|
+
if (effectiveStart > 1) {
|
|
4551
|
+
const gapAboveSize = effectiveStart - 1;
|
|
4517
4552
|
const gapAbove = window.HunkParser.createGapRowElement(
|
|
4518
4553
|
contextFile.file,
|
|
4519
|
-
1,
|
|
4520
|
-
|
|
4554
|
+
1, // startLine (old coords)
|
|
4555
|
+
effectiveStart - 1, // endLine (old coords)
|
|
4521
4556
|
gapAboveSize,
|
|
4522
4557
|
'above',
|
|
4523
4558
|
this.expandGapContext.bind(this),
|
|
4524
|
-
1
|
|
4559
|
+
1 // startLineNew (same as old for context files — no diff offset)
|
|
4525
4560
|
);
|
|
4526
4561
|
tbody.appendChild(gapAbove);
|
|
4527
4562
|
}
|
|
4528
4563
|
|
|
4529
|
-
for (let i =
|
|
4564
|
+
for (let i = effectiveStart; i <= clampedEnd; i++) {
|
|
4530
4565
|
const lineData = {
|
|
4531
4566
|
type: 'context',
|
|
4532
4567
|
oldNumber: i,
|
|
@@ -4779,7 +4814,11 @@ class PRManager {
|
|
|
4779
4814
|
table.className = 'd2h-diff-table';
|
|
4780
4815
|
const tbody = this._buildContextChunkTbody(data, contextFile);
|
|
4781
4816
|
table.appendChild(tbody);
|
|
4782
|
-
|
|
4817
|
+
|
|
4818
|
+
const fileBody = document.createElement('div');
|
|
4819
|
+
fileBody.className = 'd2h-file-body';
|
|
4820
|
+
fileBody.appendChild(table);
|
|
4821
|
+
wrapper.appendChild(fileBody);
|
|
4783
4822
|
|
|
4784
4823
|
// Insert in sorted path order among existing file wrappers
|
|
4785
4824
|
const allWrappers = [...diffContainer.querySelectorAll('.d2h-file-wrapper')];
|
|
@@ -4874,8 +4913,9 @@ class PRManager {
|
|
|
4874
4913
|
lineStartVal = 1;
|
|
4875
4914
|
lineEndVal = 100;
|
|
4876
4915
|
} else if (lineEnd == null) {
|
|
4877
|
-
|
|
4878
|
-
|
|
4916
|
+
// Center a ~21-line window around the target line (±10 lines)
|
|
4917
|
+
lineStartVal = Math.max(1, lineStart - 10);
|
|
4918
|
+
lineEndVal = lineStartVal + 20;
|
|
4879
4919
|
} else {
|
|
4880
4920
|
lineStartVal = lineStart;
|
|
4881
4921
|
lineEndVal = Math.min(lineEnd, lineStart + 499);
|
|
@@ -465,6 +465,14 @@ class ClaudeCodeBridge extends EventEmitter {
|
|
|
465
465
|
toolName: name,
|
|
466
466
|
status: 'start',
|
|
467
467
|
});
|
|
468
|
+
} else if (event.content_block && event.content_block.type === 'text') {
|
|
469
|
+
// When a new text block starts and we already have accumulated text
|
|
470
|
+
// from a previous block, inject paragraph separation so the markdown
|
|
471
|
+
// renderer doesn't smash the blocks together (e.g., "diff.The").
|
|
472
|
+
if (this._accumulatedText) {
|
|
473
|
+
this._accumulatedText += '\n\n';
|
|
474
|
+
this.emit('delta', { text: '\n\n' });
|
|
475
|
+
}
|
|
468
476
|
}
|
|
469
477
|
break;
|
|
470
478
|
|