@in-the-loop-labs/pair-review 3.1.2 → 3.1.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/css/pr.css +86 -2
- package/public/js/components/AIPanel.js +7 -4
- package/public/js/components/ChatPanel.js +34 -4
- package/public/js/components/CouncilProgressModal.js +3 -0
- package/public/js/components/DiffOptionsDropdown.js +93 -19
- package/public/js/components/SuggestionNavigator.js +2 -0
- package/public/js/modules/comment-manager.js +7 -0
- package/public/js/modules/comment-minimizer.js +151 -4
- package/public/js/modules/file-comment-manager.js +66 -2
- package/public/js/modules/suggestion-manager.js +2 -1
- package/public/js/pr.js +13 -0
- package/src/ai/analyzer.js +21 -3
- package/src/ai/claude-provider.js +1 -11
- package/src/ai/codex-provider.js +18 -16
- package/src/ai/copilot-provider.js +21 -21
- package/src/ai/gemini-provider.js +10 -0
- package/src/ai/pi-provider.js +22 -25
- package/src/ai/provider.js +26 -3
- package/src/chat/pi-bridge.js +8 -0
- package/src/chat/prompt-builder.js +2 -3
- package/src/chat/session-manager.js +1 -0
- package/src/config.js +1 -0
- package/src/database.js +31 -1
- package/src/local-review.js +21 -30
- package/src/local-scope.js +31 -23
- package/src/routes/executable-analysis.js +11 -9
- package/src/routes/local.js +52 -50
- package/src/routes/setup.js +4 -3
- package/src/setup/local-setup.js +8 -6
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pair-review",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.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": "3.1.
|
|
3
|
+
"version": "3.1.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
|
@@ -7468,6 +7468,90 @@ body.resizing * {
|
|
|
7468
7468
|
display: table-row;
|
|
7469
7469
|
}
|
|
7470
7470
|
|
|
7471
|
+
/* When minimize mode is active, hide file-level comment cards */
|
|
7472
|
+
.comments-minimized .file-comment-card {
|
|
7473
|
+
display: none;
|
|
7474
|
+
}
|
|
7475
|
+
|
|
7476
|
+
/* Per-file expansion override — clicking a file-header indicator reveals that file's cards */
|
|
7477
|
+
.comments-minimized .file-comments-zone.file-comments-expanded .file-comment-card {
|
|
7478
|
+
display: block;
|
|
7479
|
+
}
|
|
7480
|
+
|
|
7481
|
+
/* File-header comment indicator — matches header button sizing, only visible when minimized */
|
|
7482
|
+
.file-comment-indicator {
|
|
7483
|
+
display: none;
|
|
7484
|
+
}
|
|
7485
|
+
|
|
7486
|
+
.comments-minimized .file-comment-indicator {
|
|
7487
|
+
display: inline-flex;
|
|
7488
|
+
align-items: center;
|
|
7489
|
+
justify-content: center;
|
|
7490
|
+
gap: 3px;
|
|
7491
|
+
width: 28px;
|
|
7492
|
+
height: 28px;
|
|
7493
|
+
padding: 0;
|
|
7494
|
+
background: transparent;
|
|
7495
|
+
border: 1px solid transparent;
|
|
7496
|
+
border-radius: 6px;
|
|
7497
|
+
cursor: pointer;
|
|
7498
|
+
color: var(--color-text-secondary, #656d76);
|
|
7499
|
+
transition: background-color 0.15s, color 0.15s, border-color 0.15s;
|
|
7500
|
+
flex-shrink: 0;
|
|
7501
|
+
font-size: 11px;
|
|
7502
|
+
line-height: 1;
|
|
7503
|
+
}
|
|
7504
|
+
|
|
7505
|
+
.comments-minimized .file-comment-indicator:has(.indicator-user):hover,
|
|
7506
|
+
.comments-minimized .file-comment-indicator:has(.indicator-adopted):hover {
|
|
7507
|
+
background-color: rgba(130, 80, 223, 0.1);
|
|
7508
|
+
border-color: var(--comment-primary, #8250df);
|
|
7509
|
+
}
|
|
7510
|
+
|
|
7511
|
+
.comments-minimized .file-comment-indicator:has(.indicator-ai):hover {
|
|
7512
|
+
background-color: rgba(217, 119, 6, 0.1);
|
|
7513
|
+
border-color: var(--color-accent-ai, #d97706);
|
|
7514
|
+
}
|
|
7515
|
+
|
|
7516
|
+
.comments-minimized .file-comment-indicator:has(.indicator-user),
|
|
7517
|
+
.comments-minimized .file-comment-indicator:has(.indicator-adopted) {
|
|
7518
|
+
border-color: var(--comment-primary, #8250df);
|
|
7519
|
+
color: var(--comment-primary, #8250df);
|
|
7520
|
+
}
|
|
7521
|
+
|
|
7522
|
+
.comments-minimized .file-comment-indicator:has(.indicator-ai) {
|
|
7523
|
+
border-color: var(--color-accent-ai, #d97706);
|
|
7524
|
+
color: var(--color-accent-ai, #d97706);
|
|
7525
|
+
}
|
|
7526
|
+
|
|
7527
|
+
.comments-minimized .file-comment-indicator.expanded {
|
|
7528
|
+
border-width: 2px;
|
|
7529
|
+
}
|
|
7530
|
+
|
|
7531
|
+
[data-theme="dark"] .comments-minimized .file-comment-indicator:has(.indicator-user),
|
|
7532
|
+
[data-theme="dark"] .comments-minimized .file-comment-indicator:has(.indicator-adopted) {
|
|
7533
|
+
border-color: var(--comment-primary, #a371f7);
|
|
7534
|
+
color: var(--comment-primary, #a371f7);
|
|
7535
|
+
}
|
|
7536
|
+
|
|
7537
|
+
[data-theme="dark"] .comments-minimized .file-comment-indicator:has(.indicator-ai) {
|
|
7538
|
+
border-color: var(--color-accent-ai, #fbbf24);
|
|
7539
|
+
color: var(--color-accent-ai, #fbbf24);
|
|
7540
|
+
}
|
|
7541
|
+
|
|
7542
|
+
[data-theme="dark"] .comments-minimized .file-comment-indicator:has(.indicator-user):hover,
|
|
7543
|
+
[data-theme="dark"] .comments-minimized .file-comment-indicator:has(.indicator-adopted):hover {
|
|
7544
|
+
background-color: rgba(163, 113, 247, 0.15);
|
|
7545
|
+
}
|
|
7546
|
+
|
|
7547
|
+
[data-theme="dark"] .comments-minimized .file-comment-indicator:has(.indicator-ai):hover {
|
|
7548
|
+
background-color: rgba(251, 191, 36, 0.15);
|
|
7549
|
+
}
|
|
7550
|
+
|
|
7551
|
+
[data-theme="dark"] .comments-minimized .file-comment-indicator.expanded {
|
|
7552
|
+
border-width: 2px;
|
|
7553
|
+
}
|
|
7554
|
+
|
|
7471
7555
|
/* Indicator button on the right edge of diff code cells */
|
|
7472
7556
|
.comment-indicator {
|
|
7473
7557
|
position: absolute;
|
|
@@ -7530,11 +7614,11 @@ body.resizing * {
|
|
|
7530
7614
|
}
|
|
7531
7615
|
|
|
7532
7616
|
.comment-indicator .indicator-ai {
|
|
7533
|
-
color: var(--
|
|
7617
|
+
color: var(--color-accent-ai, #d97706);
|
|
7534
7618
|
}
|
|
7535
7619
|
|
|
7536
7620
|
.comment-indicator:has(.indicator-ai) {
|
|
7537
|
-
border-color: var(--
|
|
7621
|
+
border-color: var(--color-accent-ai, #d97706);
|
|
7538
7622
|
background: rgba(217, 119, 6, 0.06);
|
|
7539
7623
|
}
|
|
7540
7624
|
|
|
@@ -807,8 +807,9 @@ class AIPanel {
|
|
|
807
807
|
const finding = this.findings.find(f => f.id === findingId);
|
|
808
808
|
if (finding) {
|
|
809
809
|
suggestionContext = {
|
|
810
|
+
suggestionId: findingId ? String(findingId) : null,
|
|
810
811
|
title: finding.title || title,
|
|
811
|
-
body: finding.body || '',
|
|
812
|
+
body: finding.formattedBody || finding.body || '',
|
|
812
813
|
type: finding.type || '',
|
|
813
814
|
file: finding.file || file,
|
|
814
815
|
line_start: finding.line_start || null,
|
|
@@ -1002,6 +1003,8 @@ class AIPanel {
|
|
|
1002
1003
|
if (targetSuggestion) {
|
|
1003
1004
|
const minimizer = window.prManager?.commentMinimizer;
|
|
1004
1005
|
if (minimizer?.active) {
|
|
1006
|
+
// Expand file-level comments so the target becomes visible
|
|
1007
|
+
minimizer.expandForElement(targetSuggestion);
|
|
1005
1008
|
// Comments are minimized — scroll to the parent diff line instead
|
|
1006
1009
|
const diffRow = minimizer.findDiffRowFor(targetSuggestion);
|
|
1007
1010
|
(diffRow || targetSuggestion).scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
@@ -1069,9 +1072,9 @@ class AIPanel {
|
|
|
1069
1072
|
|
|
1070
1073
|
if (targetElement) {
|
|
1071
1074
|
const minimizer = window.prManager?.commentMinimizer;
|
|
1072
|
-
if (minimizer?.active
|
|
1073
|
-
|
|
1074
|
-
const diffRow = minimizer.findDiffRowFor(targetElement);
|
|
1075
|
+
if (minimizer?.active) {
|
|
1076
|
+
minimizer.expandForElement(targetElement);
|
|
1077
|
+
const diffRow = isFileLevel ? null : minimizer.findDiffRowFor(targetElement);
|
|
1075
1078
|
(diffRow || targetElement).scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
1076
1079
|
} else {
|
|
1077
1080
|
targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
@@ -32,6 +32,7 @@ class ChatPanel {
|
|
|
32
32
|
this._pendingContext = [];
|
|
33
33
|
this._pendingContextData = [];
|
|
34
34
|
this._pendingDiffStateNotifications = [];
|
|
35
|
+
this._pendingUserActionHints = [];
|
|
35
36
|
this._contextSource = null; // 'suggestion' or 'user' — set when opened with context
|
|
36
37
|
this._contextItemId = null; // suggestion ID or comment ID from context
|
|
37
38
|
this._contextLineMeta = null; // { file, line_start, line_end } — set when opened with line context
|
|
@@ -667,6 +668,7 @@ class ChatPanel {
|
|
|
667
668
|
this._pendingContext = [];
|
|
668
669
|
this._pendingContextData = [];
|
|
669
670
|
this._pendingDiffStateNotifications = [];
|
|
671
|
+
this._pendingUserActionHints = [];
|
|
670
672
|
this._contextSource = null;
|
|
671
673
|
this._contextItemId = null;
|
|
672
674
|
this._contextLineMeta = null;
|
|
@@ -1030,6 +1032,7 @@ class ChatPanel {
|
|
|
1030
1032
|
this._pendingContext = [];
|
|
1031
1033
|
this._pendingContextData = [];
|
|
1032
1034
|
this._pendingDiffStateNotifications = [];
|
|
1035
|
+
this._pendingUserActionHints = [];
|
|
1033
1036
|
this._contextSource = null;
|
|
1034
1037
|
this._contextItemId = null;
|
|
1035
1038
|
this._contextLineMeta = null;
|
|
@@ -1269,12 +1272,28 @@ class ChatPanel {
|
|
|
1269
1272
|
this._pendingDiffStateNotifications = [];
|
|
1270
1273
|
}
|
|
1271
1274
|
|
|
1275
|
+
// Snapshot user-action-hints queue for error recovery (invisible to user, no UI cards)
|
|
1276
|
+
const savedUserActionHints = this._pendingUserActionHints.slice();
|
|
1277
|
+
let userActionPrefix = '';
|
|
1278
|
+
if (this._pendingUserActionHints.length > 0) {
|
|
1279
|
+
userActionPrefix = '[User Action Hints]\n' + this._pendingUserActionHints.join('\n');
|
|
1280
|
+
this._pendingUserActionHints = [];
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// Combine invisible prefixes (diff state + user action hints)
|
|
1284
|
+
let invisiblePrefix = '';
|
|
1285
|
+
if (diffStatePrefix && userActionPrefix) {
|
|
1286
|
+
invisiblePrefix = diffStatePrefix + '\n\n' + userActionPrefix;
|
|
1287
|
+
} else {
|
|
1288
|
+
invisiblePrefix = diffStatePrefix || userActionPrefix;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1272
1291
|
const savedContext = this._pendingContext;
|
|
1273
1292
|
const savedContextData = this._pendingContextData;
|
|
1274
1293
|
if (this._pendingContext.length > 0) {
|
|
1275
1294
|
const userContext = this._pendingContext.join('\n\n');
|
|
1276
|
-
payload.context =
|
|
1277
|
-
?
|
|
1295
|
+
payload.context = invisiblePrefix
|
|
1296
|
+
? invisiblePrefix + '\n\n' + userContext
|
|
1278
1297
|
: userContext;
|
|
1279
1298
|
payload.contextData = this._pendingContextData;
|
|
1280
1299
|
this._pendingContext = [];
|
|
@@ -1287,8 +1306,8 @@ class ChatPanel {
|
|
|
1287
1306
|
if (btn) btn.remove();
|
|
1288
1307
|
delete card.dataset.contextIndex;
|
|
1289
1308
|
});
|
|
1290
|
-
} else if (
|
|
1291
|
-
payload.context =
|
|
1309
|
+
} else if (invisiblePrefix) {
|
|
1310
|
+
payload.context = invisiblePrefix;
|
|
1292
1311
|
}
|
|
1293
1312
|
|
|
1294
1313
|
// Lock analysis context card (not indexed, handled separately from pending context)
|
|
@@ -1353,6 +1372,7 @@ class ChatPanel {
|
|
|
1353
1372
|
this._pendingContext = savedContext;
|
|
1354
1373
|
this._pendingContextData = savedContextData;
|
|
1355
1374
|
this._pendingDiffStateNotifications = [...savedDiffState, ...this._pendingDiffStateNotifications];
|
|
1375
|
+
this._pendingUserActionHints = [...savedUserActionHints, ...this._pendingUserActionHints];
|
|
1356
1376
|
// Restore removability on context cards that were locked before the failed send
|
|
1357
1377
|
this._restoreRemovableCards();
|
|
1358
1378
|
console.error('[ChatPanel] Error sending message:', error);
|
|
@@ -1371,6 +1391,16 @@ class ChatPanel {
|
|
|
1371
1391
|
this._pendingDiffStateNotifications.push(message);
|
|
1372
1392
|
}
|
|
1373
1393
|
|
|
1394
|
+
/**
|
|
1395
|
+
* Queue an invisible user-action hint for the chat agent.
|
|
1396
|
+
* Like diff-state notifications, these do NOT render UI cards and survive panel close.
|
|
1397
|
+
* Drained into the context parameter on the next sendMessage() call.
|
|
1398
|
+
* @param {string} message - Description of the user action (e.g., "[User Action: adopted suggestion 42]")
|
|
1399
|
+
*/
|
|
1400
|
+
queueUserActionHint(message) {
|
|
1401
|
+
this._pendingUserActionHints.push(message);
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1374
1404
|
/**
|
|
1375
1405
|
* Store pending context and render a compact context card in the UI.
|
|
1376
1406
|
* Called when the user clicks "Ask about this" on a suggestion.
|
|
@@ -1312,6 +1312,9 @@ class CouncilProgressModal {
|
|
|
1312
1312
|
<span class="council-voice-icon pending">\u25CB</span>
|
|
1313
1313
|
<span class="council-voice-label">Consolidation</span>
|
|
1314
1314
|
<span class="council-voice-status pending">Pending</span>
|
|
1315
|
+
<div class="council-voice-detail">
|
|
1316
|
+
<div class="council-voice-snippet" style="display: none;"></div>
|
|
1317
|
+
</div>
|
|
1315
1318
|
</div>
|
|
1316
1319
|
`;
|
|
1317
1320
|
}
|
|
@@ -57,11 +57,29 @@ class DiffOptionsDropdown {
|
|
|
57
57
|
this._outsideClickHandler = null;
|
|
58
58
|
this._escapeHandler = null;
|
|
59
59
|
|
|
60
|
-
// Scope state
|
|
61
|
-
|
|
60
|
+
// Scope state — resolve LocalScope with inline fallback so the scope selector
|
|
61
|
+
// renders even if window.LocalScope failed to load (race condition guard).
|
|
62
|
+
const FALLBACK_STOPS = ['branch', 'staged', 'unstaged', 'untracked']; // Keep in sync with local-scope.js:STOPS
|
|
63
|
+
const FALLBACK_DEFAULT = { start: 'unstaged', end: 'untracked' };
|
|
64
|
+
this._localScope = window.LocalScope || {
|
|
65
|
+
STOPS: FALLBACK_STOPS,
|
|
66
|
+
DEFAULT_SCOPE: FALLBACK_DEFAULT,
|
|
67
|
+
isValidScope: (s, e) => {
|
|
68
|
+
const si = FALLBACK_STOPS.indexOf(s);
|
|
69
|
+
const ei = FALLBACK_STOPS.indexOf(e);
|
|
70
|
+
return si !== -1 && ei !== -1 && si <= ei;
|
|
71
|
+
},
|
|
72
|
+
scopeIncludes: (s, e, stop) => {
|
|
73
|
+
const si = FALLBACK_STOPS.indexOf(s);
|
|
74
|
+
const ei = FALLBACK_STOPS.indexOf(e);
|
|
75
|
+
const ti = FALLBACK_STOPS.indexOf(stop);
|
|
76
|
+
return ti !== -1 && ti >= si && ti <= ei;
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const LS = this._localScope;
|
|
62
80
|
this._branchAvailable = Boolean(branchAvailable);
|
|
63
|
-
this._scopeStart = (initialScope && initialScope.start) ||
|
|
64
|
-
this._scopeEnd = (initialScope && initialScope.end) ||
|
|
81
|
+
this._scopeStart = (initialScope && initialScope.start) || LS.DEFAULT_SCOPE.start;
|
|
82
|
+
this._scopeEnd = (initialScope && initialScope.end) || LS.DEFAULT_SCOPE.end;
|
|
65
83
|
this._scopeStops = [];
|
|
66
84
|
this._scopeTrackEl = null;
|
|
67
85
|
this._scopeDebounceTimer = null;
|
|
@@ -144,7 +162,7 @@ class DiffOptionsDropdown {
|
|
|
144
162
|
/** Programmatically set scope. */
|
|
145
163
|
set scope(val) {
|
|
146
164
|
if (!val) return;
|
|
147
|
-
const LS =
|
|
165
|
+
const LS = this._localScope;
|
|
148
166
|
if (LS && !LS.isValidScope(val.start, val.end)) return;
|
|
149
167
|
this._scopeStart = val.start;
|
|
150
168
|
this._scopeEnd = val.end;
|
|
@@ -188,8 +206,11 @@ class DiffOptionsDropdown {
|
|
|
188
206
|
popover.style.zIndex = '1100';
|
|
189
207
|
popover.style.transition = 'opacity 0.15s ease, transform 0.15s ease';
|
|
190
208
|
|
|
191
|
-
// Scope selector first — only in local mode
|
|
192
|
-
|
|
209
|
+
// Scope selector first — only in local mode.
|
|
210
|
+
// Belt-and-suspenders: also render when scope callbacks were explicitly provided,
|
|
211
|
+
// in case a race condition prevents the globals from being set in time.
|
|
212
|
+
const hasLocalScope = (window.PAIR_REVIEW_LOCAL_MODE && window.LocalScope) || this._onScopeChange;
|
|
213
|
+
if (hasLocalScope) {
|
|
193
214
|
this._renderScopeSelector(popover);
|
|
194
215
|
|
|
195
216
|
// Divider between scope selector and whitespace checkbox
|
|
@@ -261,7 +282,7 @@ class DiffOptionsDropdown {
|
|
|
261
282
|
}
|
|
262
283
|
|
|
263
284
|
_renderScopeSelector(popover) {
|
|
264
|
-
const LS =
|
|
285
|
+
const LS = this._localScope;
|
|
265
286
|
|
|
266
287
|
// Section container — generous horizontal padding so dots/labels breathe
|
|
267
288
|
const section = document.createElement('div');
|
|
@@ -362,13 +383,43 @@ class DiffOptionsDropdown {
|
|
|
362
383
|
stopEl.appendChild(dot);
|
|
363
384
|
stopEl.appendChild(labelEl);
|
|
364
385
|
|
|
386
|
+
// Custom tooltip element (positioned above the dot, hidden by default)
|
|
387
|
+
const tooltipEl = document.createElement('div');
|
|
388
|
+
tooltipEl.style.position = 'absolute';
|
|
389
|
+
tooltipEl.style.bottom = '100%';
|
|
390
|
+
tooltipEl.style.left = '50%';
|
|
391
|
+
tooltipEl.style.transform = 'translateX(-50%)';
|
|
392
|
+
tooltipEl.style.marginBottom = '6px';
|
|
393
|
+
tooltipEl.style.padding = '4px 8px';
|
|
394
|
+
tooltipEl.style.fontSize = '11px';
|
|
395
|
+
tooltipEl.style.lineHeight = '1.3';
|
|
396
|
+
tooltipEl.style.color = 'var(--color-text-on-emphasis, #ffffff)';
|
|
397
|
+
tooltipEl.style.background = 'var(--color-neutral-emphasis, #24292f)';
|
|
398
|
+
tooltipEl.style.borderRadius = '4px';
|
|
399
|
+
tooltipEl.style.whiteSpace = 'nowrap';
|
|
400
|
+
tooltipEl.style.pointerEvents = 'none';
|
|
401
|
+
tooltipEl.style.opacity = '0';
|
|
402
|
+
tooltipEl.style.transition = 'opacity 0.12s ease';
|
|
403
|
+
tooltipEl.style.zIndex = '2';
|
|
404
|
+
stopEl.style.position = 'relative';
|
|
405
|
+
stopEl.appendChild(tooltipEl);
|
|
406
|
+
|
|
407
|
+
stopEl.addEventListener('mouseenter', () => {
|
|
408
|
+
if (tooltipEl.textContent) {
|
|
409
|
+
tooltipEl.style.opacity = '1';
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
stopEl.addEventListener('mouseleave', () => {
|
|
413
|
+
tooltipEl.style.opacity = '0';
|
|
414
|
+
});
|
|
415
|
+
|
|
365
416
|
stopEl.addEventListener('click', (e) => {
|
|
366
417
|
e.stopPropagation();
|
|
367
418
|
this._handleStopClick(stop, e);
|
|
368
419
|
});
|
|
369
420
|
|
|
370
421
|
stopsRow.appendChild(stopEl);
|
|
371
|
-
this._scopeStops.push({ stop, dotEl: dot, labelEl, containerEl: stopEl });
|
|
422
|
+
this._scopeStops.push({ stop, dotEl: dot, labelEl, containerEl: stopEl, tooltipEl });
|
|
372
423
|
});
|
|
373
424
|
|
|
374
425
|
trackContainer.appendChild(stopsRow);
|
|
@@ -382,7 +433,7 @@ class DiffOptionsDropdown {
|
|
|
382
433
|
}
|
|
383
434
|
|
|
384
435
|
_handleStopClick(clickedStop, event) {
|
|
385
|
-
const LS =
|
|
436
|
+
const LS = this._localScope;
|
|
386
437
|
if (!LS) return;
|
|
387
438
|
|
|
388
439
|
// Branch disabled? Ignore.
|
|
@@ -396,10 +447,14 @@ class DiffOptionsDropdown {
|
|
|
396
447
|
let newStart = this._scopeStart;
|
|
397
448
|
let newEnd = this._scopeEnd;
|
|
398
449
|
|
|
399
|
-
//
|
|
450
|
+
// 'unstaged' is always included — the AI reads files from the working
|
|
451
|
+
// tree, so the diff must always cover at least the unstaged state.
|
|
452
|
+
const ui = stops.indexOf('unstaged');
|
|
453
|
+
|
|
454
|
+
// Alt/Option-click: select this stop with minimum scope including unstaged
|
|
400
455
|
if (event && event.altKey) {
|
|
401
|
-
newStart =
|
|
402
|
-
newEnd =
|
|
456
|
+
newStart = stops[Math.min(ci, ui)];
|
|
457
|
+
newEnd = stops[Math.max(ci, ui)];
|
|
403
458
|
} else {
|
|
404
459
|
// Checkbox-like toggle with contiguity constraint
|
|
405
460
|
const included = ci >= si && ci <= ei;
|
|
@@ -407,6 +462,7 @@ class DiffOptionsDropdown {
|
|
|
407
462
|
if (included) {
|
|
408
463
|
// Toggling OFF — only allowed at boundaries, and range must have >1 stop
|
|
409
464
|
if (si === ei) return;
|
|
465
|
+
if (clickedStop === 'unstaged') return; // unstaged is mandatory
|
|
410
466
|
if (ci === si) {
|
|
411
467
|
newStart = stops[si + 1];
|
|
412
468
|
} else if (ci === ei) {
|
|
@@ -457,30 +513,48 @@ class DiffOptionsDropdown {
|
|
|
457
513
|
}
|
|
458
514
|
|
|
459
515
|
_updateScopeUI() {
|
|
460
|
-
const LS =
|
|
516
|
+
const LS = this._localScope;
|
|
461
517
|
if (!LS || !this._scopeStops.length) return;
|
|
462
518
|
|
|
463
519
|
const stops = LS.STOPS;
|
|
464
520
|
const si = stops.indexOf(this._scopeStart);
|
|
465
521
|
const ei = stops.indexOf(this._scopeEnd);
|
|
466
522
|
|
|
467
|
-
this._scopeStops.forEach(({ stop, dotEl, labelEl, containerEl }, i) => {
|
|
523
|
+
this._scopeStops.forEach(({ stop, dotEl, labelEl, containerEl, tooltipEl }, i) => {
|
|
468
524
|
const included = LS.scopeIncludes(this._scopeStart, this._scopeEnd, stop);
|
|
469
525
|
const isBranch = stop === 'branch';
|
|
470
526
|
const disabled = isBranch && !this._branchAvailable;
|
|
471
527
|
|
|
472
|
-
// Determine if clicking this stop would do anything (for cursor hint)
|
|
473
|
-
|
|
528
|
+
// Determine if clicking this stop would do anything (for cursor hint).
|
|
529
|
+
// 'unstaged' is mandatory and cannot be toggled off, so it is never a
|
|
530
|
+
// clickable boundary even when it sits at a range edge.
|
|
531
|
+
const isMandatory = stop === 'unstaged';
|
|
532
|
+
const atRangeEdge = included && (i === si || i === ei) && si !== ei;
|
|
533
|
+
const isBoundary = atRangeEdge && !isMandatory;
|
|
474
534
|
const isAdjacent = !included && (i === si - 1 || i === ei + 1);
|
|
475
535
|
const clickable = !disabled && (isBoundary || isAdjacent);
|
|
476
536
|
|
|
537
|
+
// Tooltip for disabled branch stop
|
|
538
|
+
containerEl.title = disabled ? 'No feature branch detected' : '';
|
|
539
|
+
|
|
540
|
+
// Mandatory stop sitting at a range edge — user might expect to toggle
|
|
541
|
+
// it off but can't. Show not-allowed cursor and explanatory tooltip.
|
|
542
|
+
const mandatoryEdge = isMandatory && atRangeEdge;
|
|
543
|
+
|
|
544
|
+
// Update tooltip text (empty string hides the tooltip on hover)
|
|
545
|
+
if (tooltipEl) {
|
|
546
|
+
tooltipEl.textContent = mandatoryEdge
|
|
547
|
+
? 'Unstaged changes are always included \u2014 the agent reads from your working tree'
|
|
548
|
+
: '';
|
|
549
|
+
}
|
|
550
|
+
|
|
477
551
|
if (disabled) {
|
|
478
552
|
// Disabled state
|
|
479
553
|
dotEl.style.background = 'var(--color-bg-tertiary, #f6f8fa)';
|
|
480
554
|
dotEl.style.borderColor = 'var(--color-border-secondary, #e1e4e8)';
|
|
481
555
|
dotEl.style.boxShadow = 'none';
|
|
482
556
|
labelEl.style.color = 'var(--color-text-tertiary, #8b949e)';
|
|
483
|
-
containerEl.style.cursor = '
|
|
557
|
+
containerEl.style.cursor = 'default';
|
|
484
558
|
containerEl.style.opacity = '0.5';
|
|
485
559
|
} else if (included) {
|
|
486
560
|
// Included (filled) state
|
|
@@ -489,7 +563,7 @@ class DiffOptionsDropdown {
|
|
|
489
563
|
dotEl.style.boxShadow = '0 0 0 2px rgba(139, 92, 246, 0.2)';
|
|
490
564
|
labelEl.style.color = 'var(--color-text-primary, #24292f)';
|
|
491
565
|
labelEl.style.fontWeight = '600';
|
|
492
|
-
containerEl.style.cursor = clickable ? 'pointer' : 'default';
|
|
566
|
+
containerEl.style.cursor = clickable ? 'pointer' : (mandatoryEdge ? 'not-allowed' : 'default');
|
|
493
567
|
containerEl.style.opacity = '1';
|
|
494
568
|
} else {
|
|
495
569
|
// Excluded (empty) state
|
|
@@ -371,6 +371,8 @@ class SuggestionNavigator {
|
|
|
371
371
|
if (suggestionEl) {
|
|
372
372
|
const minimizer = window.prManager?.commentMinimizer;
|
|
373
373
|
if (minimizer?.active) {
|
|
374
|
+
// Expand file-level comments so the target becomes visible
|
|
375
|
+
minimizer.expandForElement(suggestionEl);
|
|
374
376
|
// Comments are minimized — scroll to the parent diff line instead
|
|
375
377
|
const diffRow = minimizer.findDiffRowFor(suggestionEl);
|
|
376
378
|
if (diffRow) {
|
|
@@ -503,8 +503,15 @@ class CommentManager {
|
|
|
503
503
|
// Refresh minimize-mode indicators so the new comment is reflected
|
|
504
504
|
if (window.prManager?.commentMinimizer) {
|
|
505
505
|
window.prManager.commentMinimizer.refreshIndicators();
|
|
506
|
+
// Auto-expand so the new comment stays visible in minimize mode
|
|
507
|
+
const newRow = document.querySelector(`.user-comment-row[data-comment-id="${commentData.id}"]`);
|
|
508
|
+
if (newRow) {
|
|
509
|
+
window.prManager.commentMinimizer.expandForElement(newRow);
|
|
510
|
+
}
|
|
506
511
|
}
|
|
507
512
|
|
|
513
|
+
window.chatPanel?.queueUserActionHint(`[User Action: created comment ${result.commentId}]`);
|
|
514
|
+
|
|
508
515
|
} catch (error) {
|
|
509
516
|
console.error('Error saving comment:', error);
|
|
510
517
|
alert('Failed to save comment');
|