@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.
- package/bin/pair-review.js +1 -1
- 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/css/pr.css +175 -47
- package/public/js/components/ChatPanel.js +75 -72
- package/public/js/components/DiffOptionsDropdown.js +207 -0
- package/public/js/components/ReviewModal.js +8 -3
- package/public/js/local.js +31 -1
- package/public/js/modules/comment-manager.js +46 -1
- package/public/js/pr.js +115 -12
- package/public/local.html +6 -0
- package/public/pr.html +6 -0
- package/src/ai/cursor-agent-provider.js +9 -4
- package/src/chat/prompt-builder.js +11 -1
- package/src/database.js +5 -3
- package/src/github/client.js +80 -20
- package/src/local-review.js +5 -4
- package/src/routes/context-files.js +51 -0
- package/src/routes/local.js +17 -1
- package/src/routes/pr.js +74 -17
- package/src/utils/auto-context.js +1 -1
- package/src/utils/diff-annotator.js +75 -1
package/bin/pair-review.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pair-review",
|
|
3
|
-
"version": "2.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.
|
|
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
|
-
/*
|
|
11202
|
-
|
|
11203
|
-
|
|
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-
|
|
11207
|
-
0% {
|
|
11208
|
-
|
|
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
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
window.prManager
|
|
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,
|
|
2598
|
-
*
|
|
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
|
-
//
|
|
2612
|
-
const
|
|
2613
|
-
let
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
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
|
/**
|