@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": "@in-the-loop-labs/pair-review",
3
- "version": "2.4.2",
3
+ "version": "2.4.4",
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": "2.4.2",
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.2",
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-x: auto;
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: auto; /* Allow horizontal scroll for long code lines */
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 table when collapsed */
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-y: auto;
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
- /* Override flex: 1 so the file name doesn't push the badge away */
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: none;
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 */
@@ -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
- // Lazy-load GitHub collection tabs on first switch
1215
- if (tabId === 'review-requests-tab' && !reviewRequestsState.loaded) {
1216
- loadCollectionPrs('review-requests', 'review-requests-container', reviewRequestsState);
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' && !myPrsState.loaded) {
1219
- loadCollectionPrs('my-prs', 'my-prs-container', myPrsState);
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 it immediately
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
- wrapper.appendChild(table);
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
- tbody.dataset.lineStart = contextFile.line_start;
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
- const lineEnd = Math.min(contextFile.line_end, data.lines.length);
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 (lineStart > 1) {
4516
- const gapAboveSize = lineStart - 1;
4550
+ if (effectiveStart > 1) {
4551
+ const gapAboveSize = effectiveStart - 1;
4517
4552
  const gapAbove = window.HunkParser.createGapRowElement(
4518
4553
  contextFile.file,
4519
- 1, // startLine (old coords)
4520
- lineStart - 1, // endLine (old coords)
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 // startLineNew (same as old for context files — no diff offset)
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 = lineStart; i <= clampedEnd; 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
- wrapper.appendChild(table);
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
- lineStartVal = lineStart;
4878
- lineEndVal = lineStart + 49;
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