@in-the-loop-labs/pair-review 2.0.0 → 2.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.
@@ -26,7 +26,7 @@ async function main() {
26
26
  : 'inherit';
27
27
 
28
28
  // Spawn the main process with arguments
29
- const app = spawn('node', [mainPath, ...args], {
29
+ const app = spawn(process.execPath, [mainPath, ...args], {
30
30
  stdio: stdioOption
31
31
  });
32
32
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@in-the-loop-labs/pair-review",
3
- "version": "2.0.0",
3
+ "version": "2.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": "2.0.0",
3
+ "version": "2.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": "2.0.0",
3
+ "version": "2.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",
package/public/css/pr.css CHANGED
@@ -6362,12 +6362,29 @@ body:not([data-theme="dark"]) .theme-icon-light {
6362
6362
  }
6363
6363
 
6364
6364
  /* Small icon buttons in toolbar have reduced dimensions */
6365
- .toolbar-actions .btn-sm.btn-icon {
6365
+ .toolbar-actions .btn-sm.btn-icon,
6366
+ .toolbar-meta .btn-sm.btn-icon {
6366
6367
  padding: 6px;
6367
6368
  width: 32px;
6368
6369
  height: 32px;
6369
6370
  }
6370
6371
 
6372
+ /* Icon buttons inside toolbar-meta need the same visual treatment as toolbar-actions */
6373
+ .toolbar-meta .btn-icon {
6374
+ background: var(--color-bg-secondary);
6375
+ border: 1px solid var(--color-border-primary);
6376
+ color: var(--color-text-secondary);
6377
+ border-radius: var(--radius-sm);
6378
+ cursor: pointer;
6379
+ transition: all var(--transition-fast);
6380
+ }
6381
+
6382
+ .toolbar-meta .btn-icon:hover {
6383
+ background: var(--color-bg-tertiary);
6384
+ color: var(--color-text-primary);
6385
+ border-color: var(--color-border-secondary);
6386
+ }
6387
+
6371
6388
  /* ============================================
6372
6389
  Analysis Progress Dots
6373
6390
  ============================================ */
@@ -6962,17 +6979,96 @@ body.resizing * {
6962
6979
  }
6963
6980
 
6964
6981
  /* Dark theme toolbar button overrides */
6965
- [data-theme="dark"] .toolbar-actions .btn-icon {
6982
+ [data-theme="dark"] .toolbar-actions .btn-icon,
6983
+ [data-theme="dark"] .toolbar-meta .btn-icon {
6966
6984
  background: var(--color-bg-tertiary);
6967
6985
  border-color: var(--color-border-secondary);
6968
6986
  color: var(--color-text-secondary);
6969
6987
  }
6970
6988
 
6971
- [data-theme="dark"] .toolbar-actions .btn-icon:hover {
6989
+ [data-theme="dark"] .toolbar-actions .btn-icon:hover,
6990
+ [data-theme="dark"] .toolbar-meta .btn-icon:hover {
6972
6991
  background: var(--color-bg-elevated);
6973
6992
  color: var(--color-text-primary);
6974
6993
  }
6975
6994
 
6995
+ /* --------------------------------------------------------------------------
6996
+ Diff Options Popover (gear icon dropdown for whitespace toggle, etc.)
6997
+ -------------------------------------------------------------------------- */
6998
+ .diff-options-popover {
6999
+ position: fixed;
7000
+ z-index: 1100;
7001
+ background: var(--color-bg-primary);
7002
+ border: 1px solid var(--color-border-primary);
7003
+ border-radius: 8px;
7004
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08), 0 8px 24px rgba(0, 0, 0, 0.12);
7005
+ padding: 4px 0;
7006
+ opacity: 0;
7007
+ transform: translateY(-4px);
7008
+ transition: opacity 0.15s ease, transform 0.15s ease;
7009
+ pointer-events: none;
7010
+ }
7011
+
7012
+ .diff-options-popover.visible {
7013
+ opacity: 1;
7014
+ transform: translateY(0);
7015
+ pointer-events: auto;
7016
+ }
7017
+
7018
+ .diff-options-popover label {
7019
+ display: flex;
7020
+ align-items: center;
7021
+ gap: 8px;
7022
+ padding: 8px 12px;
7023
+ cursor: pointer;
7024
+ font-size: 0.8125rem;
7025
+ color: var(--color-text-primary);
7026
+ white-space: nowrap;
7027
+ user-select: none;
7028
+ transition: background-color 0.1s ease;
7029
+ }
7030
+
7031
+ .diff-options-popover label:hover {
7032
+ background: var(--color-bg-tertiary);
7033
+ }
7034
+
7035
+ .diff-options-popover input[type="checkbox"] {
7036
+ margin: 0;
7037
+ cursor: pointer;
7038
+ }
7039
+
7040
+ /* Active state for the diff-options gear button when a filter is applied */
7041
+ #diff-options-btn.active {
7042
+ color: var(--color-accent-primary);
7043
+ border-color: var(--color-accent-primary);
7044
+ background: var(--color-accent-light);
7045
+ }
7046
+
7047
+ #diff-options-btn.active:hover {
7048
+ background: var(--color-accent-lighter);
7049
+ }
7050
+
7051
+ /* Dark theme */
7052
+ [data-theme="dark"] .diff-options-popover {
7053
+ background: var(--color-bg-secondary);
7054
+ border-color: var(--color-border-secondary);
7055
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3), 0 8px 24px rgba(0, 0, 0, 0.4);
7056
+ }
7057
+
7058
+ [data-theme="dark"] .diff-options-popover label:hover {
7059
+ background: var(--color-bg-tertiary);
7060
+ }
7061
+
7062
+ [data-theme="dark"] #diff-options-btn.active {
7063
+ color: #58a6ff;
7064
+ border-color: #58a6ff;
7065
+ background: rgba(88, 166, 255, 0.1);
7066
+ }
7067
+
7068
+ [data-theme="dark"] #diff-options-btn.active:hover {
7069
+ background: rgba(88, 166, 255, 0.15);
7070
+ }
7071
+
6976
7072
  .ai-panel-header {
6977
7073
  display: flex;
6978
7074
  align-items: center;
@@ -11198,14 +11294,78 @@ body.resizing * {
11198
11294
  opacity: 0.6;
11199
11295
  }
11200
11296
 
11201
- /* Highlight flash when navigating to a line from a chat file link */
11202
- .chat-file-link--highlight {
11203
- animation: chat-file-link-flash 2s ease-out;
11297
+ /* Unified line highlight for navigating to lines from chat/context links.
11298
+ Applied to <tr> elements; styles target <td> children since box-shadow
11299
+ and background-color animations don't render on <tr> in most browsers. */
11300
+ .chat-line-highlight td {
11301
+ animation: chat-line-highlight-bg 3.5s ease-out forwards !important;
11302
+ }
11303
+
11304
+ .chat-line-highlight > td:first-child {
11305
+ animation: chat-line-highlight-border 3.5s ease-out forwards !important;
11306
+ }
11307
+
11308
+ @keyframes chat-line-highlight-bg {
11309
+ 0% {
11310
+ background-color: rgba(227, 179, 65, 0.20);
11311
+ }
11312
+ 57% {
11313
+ background-color: rgba(227, 179, 65, 0.08);
11314
+ }
11315
+ 100% {
11316
+ background-color: transparent;
11317
+ }
11204
11318
  }
11205
11319
 
11206
- @keyframes chat-file-link-flash {
11207
- 0% { background-color: rgba(227, 179, 65, 0.25); }
11208
- 100% { background-color: transparent; }
11320
+ @keyframes chat-line-highlight-border {
11321
+ 0% {
11322
+ box-shadow: inset 5px 0 0 #e3b341;
11323
+ background-color: rgba(227, 179, 65, 0.20);
11324
+ }
11325
+ 57% {
11326
+ /* ~2s: border still solid, background fading */
11327
+ box-shadow: inset 5px 0 0 #e3b341;
11328
+ background-color: rgba(227, 179, 65, 0.08);
11329
+ }
11330
+ 100% {
11331
+ box-shadow: inset 5px 0 0 transparent;
11332
+ background-color: transparent;
11333
+ }
11334
+ }
11335
+
11336
+ [data-theme="dark"] .chat-line-highlight td {
11337
+ animation: chat-line-highlight-bg-dark 3.5s ease-out forwards !important;
11338
+ }
11339
+
11340
+ [data-theme="dark"] .chat-line-highlight > td:first-child {
11341
+ animation: chat-line-highlight-border-dark 3.5s ease-out forwards !important;
11342
+ }
11343
+
11344
+ @keyframes chat-line-highlight-bg-dark {
11345
+ 0% {
11346
+ background-color: rgba(227, 179, 65, 0.15);
11347
+ }
11348
+ 57% {
11349
+ background-color: rgba(227, 179, 65, 0.06);
11350
+ }
11351
+ 100% {
11352
+ background-color: transparent;
11353
+ }
11354
+ }
11355
+
11356
+ @keyframes chat-line-highlight-border-dark {
11357
+ 0% {
11358
+ box-shadow: inset 5px 0 0 #e3b341;
11359
+ background-color: rgba(227, 179, 65, 0.15);
11360
+ }
11361
+ 57% {
11362
+ box-shadow: inset 5px 0 0 #e3b341;
11363
+ background-color: rgba(227, 179, 65, 0.06);
11364
+ }
11365
+ 100% {
11366
+ box-shadow: inset 5px 0 0 transparent;
11367
+ background-color: transparent;
11368
+ }
11209
11369
  }
11210
11370
 
11211
11371
  .chat-file-link--loading {
@@ -11227,28 +11387,6 @@ body.resizing * {
11227
11387
  to { transform: rotate(360deg); }
11228
11388
  }
11229
11389
 
11230
- /* Gutter arrow for "already here" micro-feedback */
11231
- .chat-gutter-arrow {
11232
- position: absolute;
11233
- left: -2px;
11234
- top: 50%;
11235
- transform: translateY(-50%);
11236
- color: #e3b341;
11237
- font-size: 20px;
11238
- font-weight: bold;
11239
- pointer-events: none;
11240
- transition: opacity 0.3s ease;
11241
- z-index: 2;
11242
- }
11243
-
11244
- .chat-gutter-arrow--fade {
11245
- opacity: 0;
11246
- }
11247
-
11248
- [data-theme="dark"] .chat-gutter-arrow {
11249
- color: #e3b341;
11250
- }
11251
-
11252
11390
  /* Error bubble */
11253
11391
  .chat-panel__error-bubble {
11254
11392
  display: flex;
@@ -11362,6 +11500,7 @@ body.resizing * {
11362
11500
 
11363
11501
  /* Chat Panel Context Card */
11364
11502
  .chat-panel__context-card {
11503
+ position: relative;
11365
11504
  display: flex;
11366
11505
  align-items: center;
11367
11506
  gap: 6px;
@@ -11391,6 +11530,7 @@ body.resizing * {
11391
11530
  overflow: hidden;
11392
11531
  text-overflow: ellipsis;
11393
11532
  white-space: nowrap;
11533
+ min-width: 0;
11394
11534
  color: var(--color-text-secondary);
11395
11535
  }
11396
11536
 
@@ -11405,9 +11545,12 @@ body.resizing * {
11405
11545
  display: none;
11406
11546
  align-items: center;
11407
11547
  justify-content: center;
11548
+ position: absolute;
11549
+ right: 6px;
11550
+ top: 50%;
11551
+ transform: translateY(-50%);
11408
11552
  width: 16px;
11409
11553
  height: 16px;
11410
- margin-left: auto;
11411
11554
  padding: 0;
11412
11555
  border: none;
11413
11556
  border-radius: 50%;
@@ -11416,7 +11559,6 @@ body.resizing * {
11416
11559
  font-size: 12px;
11417
11560
  line-height: 1;
11418
11561
  cursor: pointer;
11419
- flex-shrink: 0;
11420
11562
  transition: background 0.15s ease, color 0.15s ease;
11421
11563
  }
11422
11564
 
@@ -11938,20 +12080,6 @@ body.resizing * {
11938
12080
  color: var(--color-text-secondary);
11939
12081
  }
11940
12082
 
11941
- /* Highlight flash animation for scroll-to-line targeting */
11942
- .highlight-flash {
11943
- animation: highlightFlash 1.5s ease-out;
11944
- }
11945
-
11946
- @keyframes highlightFlash {
11947
- 0% {
11948
- background-color: var(--color-selection, #fff8c5);
11949
- }
11950
- 100% {
11951
- background-color: transparent;
11952
- }
11953
- }
11954
-
11955
12083
  /* Dark theme overrides for context files */
11956
12084
  [data-theme="dark"] .d2h-file-wrapper.context-file {
11957
12085
  border-left-color: var(--color-accent-primary, #58a6ff);
@@ -2135,6 +2135,7 @@ class ChatPanel {
2135
2135
  const bubble = streamingMsg.querySelector('.chat-panel__bubble');
2136
2136
  if (bubble) {
2137
2137
  bubble.innerHTML = this.renderMarkdown(text) + '<span class="chat-panel__cursor"></span>';
2138
+ this._linkifyFileReferences(bubble);
2138
2139
  }
2139
2140
  this.scrollToBottom();
2140
2141
  }
@@ -2530,6 +2531,7 @@ class ChatPanel {
2530
2531
  if (contextEl) {
2531
2532
  // Context file — find the right chunk by line number or use first chunk
2532
2533
  let contextFileId = contextEl.dataset?.contextId; // legacy: on wrapper itself
2534
+ let lineFoundInChunk = !!contextFileId; // legacy mode assumes line is present
2533
2535
  if (!contextFileId && lineStart) {
2534
2536
  // Merged wrapper: find chunk tbody containing this line
2535
2537
  const chunks = [...contextEl.querySelectorAll('tbody.context-chunk[data-context-id]')];
@@ -2537,27 +2539,32 @@ class ChatPanel {
2537
2539
  const row = chunk.querySelector(`tr[data-line-number="${lineStart}"]`);
2538
2540
  if (row) {
2539
2541
  contextFileId = chunk.dataset.contextId;
2542
+ lineFoundInChunk = true;
2540
2543
  break;
2541
2544
  }
2542
2545
  }
2543
2546
  }
2544
- if (!contextFileId) {
2545
- // Fallback: use first chunk's context ID
2546
- const firstChunk = contextEl.querySelector('tbody.context-chunk[data-context-id]');
2547
- if (firstChunk) contextFileId = firstChunk.dataset.contextId;
2548
- }
2549
- if (window.prManager?.scrollToContextFile) {
2550
- window.prManager.scrollToContextFile(file, lineStart, contextFileId);
2547
+
2548
+ if (lineFoundInChunk || !lineStart) {
2549
+ if (!contextFileId) {
2550
+ const firstChunk = contextEl.querySelector('tbody.context-chunk[data-context-id]');
2551
+ if (firstChunk) contextFileId = firstChunk.dataset.contextId;
2552
+ }
2553
+ if (window.prManager?.scrollToContextFile) {
2554
+ window.prManager.scrollToContextFile(file, lineStart, contextFileId);
2555
+ }
2556
+ return;
2551
2557
  }
2558
+ // Line not found in any existing chunk — fall through to add new range
2552
2559
  } else {
2553
2560
  // Diff file
2554
2561
  if (lineStart) {
2555
- this._scrollToLine(file, lineStart);
2562
+ await this._scrollToLine(file, lineStart, lineEnd);
2556
2563
  } else if (window.prManager?.scrollToFile) {
2557
2564
  window.prManager.scrollToFile(file);
2558
2565
  }
2566
+ return;
2559
2567
  }
2560
- return;
2561
2568
  }
2562
2569
 
2563
2570
  // File not in DOM — try to add as context file
@@ -2574,7 +2581,7 @@ class ChatPanel {
2574
2581
 
2575
2582
  if (result.type === 'diff') {
2576
2583
  if (lineStart) {
2577
- this._scrollToLine(file, lineStart);
2584
+ await this._scrollToLine(file, lineStart, lineEnd);
2578
2585
  } else if (window.prManager?.scrollToFile) {
2579
2586
  window.prManager.scrollToFile(file);
2580
2587
  }
@@ -2594,13 +2601,17 @@ class ChatPanel {
2594
2601
  }
2595
2602
 
2596
2603
  /**
2597
- * Scroll to a specific line within a file wrapper, with micro-feedback
2598
- * when the target is already visible.
2604
+ * Scroll to a specific line within a file wrapper, applying a bold
2605
+ * left-border + background highlight that fades over ~3.5s.
2606
+ * Supports line ranges: if lineEnd is provided, all rows from
2607
+ * lineStart to lineEnd are highlighted. If the line is in a collapsed
2608
+ * diff chunk, expands the chunk first via ensureLinesVisible().
2599
2609
  * @param {string} file - File path
2600
- * @param {number} lineStart - Target line number
2610
+ * @param {number} lineStart - Target line number (start of range)
2611
+ * @param {number|null} [lineEnd] - End of target line range (used for expansion)
2601
2612
  * @param {HTMLElement} [fileWrapper] - Pre-resolved file wrapper element
2602
2613
  */
2603
- _scrollToLine(file, lineStart, fileWrapper) {
2614
+ async _scrollToLine(file, lineStart, lineEnd, fileWrapper) {
2604
2615
  if (!fileWrapper) {
2605
2616
  const escaped = CSS.escape(file);
2606
2617
  fileWrapper = document.querySelector(`[data-file-name="${escaped}"]`) ||
@@ -2608,68 +2619,60 @@ class ChatPanel {
2608
2619
  }
2609
2620
  if (!fileWrapper) return;
2610
2621
 
2611
- // Find the target row by line number
2612
- const lineNums = fileWrapper.querySelectorAll('.line-num2');
2613
- let targetRow = null;
2614
- for (const ln of lineNums) {
2615
- if (ln.textContent.trim() === String(lineStart)) {
2616
- targetRow = ln.closest('tr');
2617
- break;
2618
- }
2622
+ // Collect all target rows (single line or range)
2623
+ const end = lineEnd || lineStart;
2624
+ let targetRows = this._findLineRows(fileWrapper, lineStart, end);
2625
+
2626
+ // If not found, try expanding the collapsed diff context
2627
+ if (targetRows.length === 0 && window.prManager?.ensureLinesVisible) {
2628
+ await window.prManager.ensureLinesVisible([
2629
+ { file, line_start: lineStart, line_end: end, side: 'RIGHT' }
2630
+ ]);
2631
+ targetRows = this._findLineRows(fileWrapper, lineStart, end);
2619
2632
  }
2620
- if (!targetRow) return;
2633
+ if (targetRows.length === 0) return;
2634
+
2635
+ const primaryRow = targetRows[0];
2621
2636
 
2622
- // Check if the target row is already visible in the viewport
2623
- const rect = targetRow.getBoundingClientRect();
2637
+ // Check if the primary target row is already visible in the viewport
2638
+ const rect = primaryRow.getBoundingClientRect();
2624
2639
  const isVisible = rect.top >= 0 && rect.bottom <= window.innerHeight;
2625
2640
 
2626
- if (isVisible) {
2627
- // Already visible provide micro-feedback instead of scrolling
2628
- this._showLineFeedback(targetRow, lineStart);
2629
- } else {
2630
- // Scroll to the row, then apply highlight
2631
- targetRow.scrollIntoView({ behavior: 'smooth', block: 'center' });
2632
- targetRow.classList.add('chat-file-link--highlight');
2633
- setTimeout(() => targetRow.classList.remove('chat-file-link--highlight'), 2000);
2634
- }
2635
- }
2636
-
2637
- /**
2638
- * Show micro-feedback when a target line is already visible:
2639
- * 1. A brief hop animation (scroll nudge)
2640
- * 2. A temporary gutter arrow indicator
2641
- * @param {HTMLElement} row - The target table row
2642
- * @param {number} lineNum - The line number
2643
- */
2644
- _showLineFeedback(row, lineNum) {
2645
- // Highlight the row
2646
- row.classList.add('chat-file-link--highlight');
2647
- setTimeout(() => row.classList.remove('chat-file-link--highlight'), 2000);
2648
-
2649
- // Inject a temporary gutter arrow
2650
- const lineNumCell = row.querySelector('.d2h-code-linenumber');
2651
- if (lineNumCell) {
2652
- const arrow = document.createElement('span');
2653
- arrow.className = 'chat-gutter-arrow';
2654
- arrow.textContent = '\u2192'; // →
2655
- lineNumCell.appendChild(arrow);
2656
- // Fade out and remove after 1.5s
2657
- setTimeout(() => {
2658
- arrow.classList.add('chat-gutter-arrow--fade');
2659
- arrow.addEventListener('transitionend', () => arrow.remove(), { once: true });
2660
- // Safety cleanup in case transitionend doesn't fire
2661
- setTimeout(() => arrow.remove(), 500);
2662
- }, 1500);
2663
- }
2664
-
2665
- // Hop animation — small vertical nudge
2666
- const scrollContainer = document.getElementById('diff-container') ||
2667
- row.closest('.d2h-wrapper') || document.documentElement;
2668
- const currentScroll = scrollContainer.scrollTop;
2669
- scrollContainer.scrollTo({ top: currentScroll - 30, behavior: 'smooth' });
2670
- setTimeout(() => {
2671
- scrollContainer.scrollTo({ top: currentScroll, behavior: 'smooth' });
2672
- }, 150);
2641
+ if (!isVisible) {
2642
+ primaryRow.scrollIntoView({ behavior: 'smooth', block: 'center' });
2643
+ }
2644
+
2645
+ // Apply the highlight to all target rows
2646
+ for (const row of targetRows) {
2647
+ // Remove any existing highlight first (in case of rapid re-clicks)
2648
+ row.classList.remove('chat-line-highlight');
2649
+ // Force reflow so re-adding the class restarts the animation
2650
+ void row.offsetWidth;
2651
+ row.classList.add('chat-line-highlight');
2652
+ row.addEventListener('animationend', () => {
2653
+ row.classList.remove('chat-line-highlight');
2654
+ }, { once: true });
2655
+ }
2656
+ }
2657
+
2658
+ /**
2659
+ * Find all table rows matching a line range within a file wrapper.
2660
+ * @param {HTMLElement} fileWrapper - The file wrapper element
2661
+ * @param {number} lineStart - Start line number
2662
+ * @param {number} lineEnd - End line number (inclusive)
2663
+ * @returns {HTMLElement[]} Matching rows
2664
+ */
2665
+ _findLineRows(fileWrapper, lineStart, lineEnd) {
2666
+ const rows = [];
2667
+ const lineNums = fileWrapper.querySelectorAll('.line-num2');
2668
+ for (const ln of lineNums) {
2669
+ const num = parseInt(ln.textContent.trim(), 10);
2670
+ if (!isNaN(num) && num >= lineStart && num <= lineEnd) {
2671
+ const row = ln.closest('tr');
2672
+ if (row) rows.push(row);
2673
+ }
2674
+ }
2675
+ return rows;
2673
2676
  }
2674
2677
 
2675
2678
  /**