@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
|
@@ -7,7 +7,10 @@
|
|
|
7
7
|
* are injected on the right edge of each diff line that has comments, showing
|
|
8
8
|
* a person icon (user comments) or sparkles icon (AI suggestions).
|
|
9
9
|
*
|
|
10
|
-
*
|
|
10
|
+
* File-level comments (.file-comment-card inside .file-comments-zone) are also
|
|
11
|
+
* hidden, with an indicator button injected into the file header bar.
|
|
12
|
+
*
|
|
13
|
+
* Clicking an indicator toggles visibility of that line's or file's comments.
|
|
11
14
|
*/
|
|
12
15
|
|
|
13
16
|
class CommentMinimizer {
|
|
@@ -24,6 +27,8 @@ class CommentMinimizer {
|
|
|
24
27
|
this._active = false;
|
|
25
28
|
// Track which diff lines have been expanded by the user (Set of diff row elements)
|
|
26
29
|
this._expandedLines = new Set();
|
|
30
|
+
// Track which file-comments-zones have been expanded (Set of zone elements)
|
|
31
|
+
this._expandedFiles = new Set();
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
/** @returns {boolean} Whether minimize mode is active */
|
|
@@ -38,6 +43,7 @@ class CommentMinimizer {
|
|
|
38
43
|
setMinimized(minimized) {
|
|
39
44
|
this._active = minimized;
|
|
40
45
|
this._expandedLines.clear();
|
|
46
|
+
this._expandedFiles.clear();
|
|
41
47
|
|
|
42
48
|
const diffContainer = document.getElementById('diff-container');
|
|
43
49
|
if (!diffContainer) return;
|
|
@@ -50,6 +56,8 @@ class CommentMinimizer {
|
|
|
50
56
|
this._removeAllIndicators();
|
|
51
57
|
// Remove any per-line expansion overrides
|
|
52
58
|
document.querySelectorAll('.comment-expanded').forEach(el => el.classList.remove('comment-expanded'));
|
|
59
|
+
// Remove any per-file expansion overrides
|
|
60
|
+
document.querySelectorAll('.file-comments-expanded').forEach(el => el.classList.remove('file-comments-expanded'));
|
|
53
61
|
}
|
|
54
62
|
}
|
|
55
63
|
|
|
@@ -103,10 +111,13 @@ class CommentMinimizer {
|
|
|
103
111
|
lineMap.set(diffRow, entry);
|
|
104
112
|
}
|
|
105
113
|
|
|
106
|
-
// Inject indicators
|
|
114
|
+
// Inject line-level indicators
|
|
107
115
|
for (const [diffRow, info] of lineMap) {
|
|
108
116
|
this._injectIndicator(diffRow, info);
|
|
109
117
|
}
|
|
118
|
+
|
|
119
|
+
// Scan file-comments-zones and inject file-header indicators
|
|
120
|
+
this._refreshFileIndicators();
|
|
110
121
|
}
|
|
111
122
|
|
|
112
123
|
/**
|
|
@@ -266,7 +277,22 @@ class CommentMinimizer {
|
|
|
266
277
|
expandForElement(element) {
|
|
267
278
|
if (!this._active) return;
|
|
268
279
|
|
|
269
|
-
//
|
|
280
|
+
// Check if this element is inside a file-comments-zone (file-level comment)
|
|
281
|
+
const zone = element.closest('.file-comments-zone');
|
|
282
|
+
if (zone) {
|
|
283
|
+
if (this._expandedFiles.has(zone)) return; // already expanded
|
|
284
|
+
this._expandedFiles.add(zone);
|
|
285
|
+
zone.classList.add('file-comments-expanded');
|
|
286
|
+
// Update the file-header indicator button
|
|
287
|
+
const wrapper = zone.closest('.d2h-file-wrapper');
|
|
288
|
+
const btn = wrapper?.querySelector('.d2h-file-header .file-comment-indicator');
|
|
289
|
+
if (btn) {
|
|
290
|
+
btn.classList.add('expanded');
|
|
291
|
+
}
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Line-level: find the containing comment/suggestion row
|
|
270
296
|
const commentRow = element.closest('.user-comment-row, .ai-suggestion-row') || element;
|
|
271
297
|
if (!commentRow.classList.contains('user-comment-row') && !commentRow.classList.contains('ai-suggestion-row')) {
|
|
272
298
|
return;
|
|
@@ -290,9 +316,130 @@ class CommentMinimizer {
|
|
|
290
316
|
}
|
|
291
317
|
}
|
|
292
318
|
|
|
293
|
-
|
|
319
|
+
// ---------------------------------------------------------------------------
|
|
320
|
+
// File-level comment indicators
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Scan all file-comments-zones and inject indicator buttons into file headers.
|
|
325
|
+
*/
|
|
326
|
+
_refreshFileIndicators() {
|
|
327
|
+
const zones = document.querySelectorAll('.file-comments-zone');
|
|
328
|
+
for (const zone of zones) {
|
|
329
|
+
const cards = zone.querySelectorAll('.file-comment-card');
|
|
330
|
+
if (cards.length === 0) continue;
|
|
331
|
+
|
|
332
|
+
// Count comment types
|
|
333
|
+
const info = { hasUser: false, hasAI: false, hasAdopted: false, userCount: 0, aiCount: 0, adoptedCount: 0 };
|
|
334
|
+
for (const card of cards) {
|
|
335
|
+
// Skip collapsed cards (adopted/dismissed originals remain in DOM)
|
|
336
|
+
if (card.classList.contains('collapsed')) continue;
|
|
337
|
+
|
|
338
|
+
if (card.classList.contains('ai-suggestion')) {
|
|
339
|
+
info.hasAI = true;
|
|
340
|
+
info.aiCount++;
|
|
341
|
+
} else if (card.classList.contains('user-comment')) {
|
|
342
|
+
if (card.classList.contains('adopted-comment')) {
|
|
343
|
+
info.hasAdopted = true;
|
|
344
|
+
info.adoptedCount++;
|
|
345
|
+
} else {
|
|
346
|
+
info.hasUser = true;
|
|
347
|
+
info.userCount++;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (info.userCount + info.aiCount + info.adoptedCount === 0) continue;
|
|
353
|
+
|
|
354
|
+
// Find the file header — zone and header are siblings inside .d2h-file-wrapper
|
|
355
|
+
const wrapper = zone.closest('.d2h-file-wrapper');
|
|
356
|
+
const header = wrapper?.querySelector('.d2h-file-header');
|
|
357
|
+
if (!header) continue;
|
|
358
|
+
|
|
359
|
+
this._injectFileIndicator(header, zone, info);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Inject an indicator button into a file header, positioned before the comment button.
|
|
365
|
+
* @param {HTMLElement} header - The .d2h-file-header element
|
|
366
|
+
* @param {HTMLElement} zone - The .file-comments-zone element
|
|
367
|
+
* @param {Object} info - { hasUser, hasAI, hasAdopted, userCount, aiCount, adoptedCount }
|
|
368
|
+
*/
|
|
369
|
+
_injectFileIndicator(header, zone, info) {
|
|
370
|
+
// Don't double-inject
|
|
371
|
+
if (header.querySelector('.file-comment-indicator')) return;
|
|
372
|
+
|
|
373
|
+
const btn = document.createElement('button');
|
|
374
|
+
btn.className = 'file-comment-indicator';
|
|
375
|
+
btn.type = 'button';
|
|
376
|
+
|
|
377
|
+
// Build icon — pick the dominant type icon
|
|
378
|
+
const icons = [];
|
|
379
|
+
if (info.hasUser) {
|
|
380
|
+
icons.push(`<span class="indicator-icon indicator-user">${CommentMinimizer.PERSON_ICON}</span>`);
|
|
381
|
+
}
|
|
382
|
+
if (info.hasAdopted) {
|
|
383
|
+
icons.push(`<span class="indicator-icon indicator-adopted">${CommentMinimizer.AI_COMMENT_ICON}</span>`);
|
|
384
|
+
}
|
|
385
|
+
if (info.hasAI) {
|
|
386
|
+
icons.push(`<span class="indicator-icon indicator-ai">${CommentMinimizer.SPARKLES_ICON}</span>`);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const total = info.userCount + info.adoptedCount + info.aiCount;
|
|
390
|
+
const countBadge = total > 1 ? `<span class="indicator-count">${total}</span>` : '';
|
|
391
|
+
|
|
392
|
+
btn.innerHTML = icons.join('') + countBadge;
|
|
393
|
+
|
|
394
|
+
const totalLabel = [];
|
|
395
|
+
if (info.userCount) totalLabel.push(`${info.userCount} file comment${info.userCount !== 1 ? 's' : ''}`);
|
|
396
|
+
if (info.adoptedCount) totalLabel.push(`${info.adoptedCount} adopted`);
|
|
397
|
+
if (info.aiCount) totalLabel.push(`${info.aiCount} suggestion${info.aiCount !== 1 ? 's' : ''}`);
|
|
398
|
+
btn.title = totalLabel.join(', ');
|
|
399
|
+
|
|
400
|
+
// Restore expanded state
|
|
401
|
+
if (this._expandedFiles.has(zone)) {
|
|
402
|
+
btn.classList.add('expanded');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
btn.addEventListener('click', (e) => {
|
|
406
|
+
e.stopPropagation();
|
|
407
|
+
e.preventDefault();
|
|
408
|
+
this._toggleFileComments(zone, btn);
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
// Insert before the file-header-comment-btn if present, otherwise append
|
|
412
|
+
const commentBtn = header.querySelector('.file-header-comment-btn');
|
|
413
|
+
if (commentBtn) {
|
|
414
|
+
header.insertBefore(btn, commentBtn);
|
|
415
|
+
} else {
|
|
416
|
+
header.appendChild(btn);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Toggle visibility of file-level comments for a specific file.
|
|
422
|
+
* @param {HTMLElement} zone - The .file-comments-zone element
|
|
423
|
+
* @param {HTMLElement} btn - The indicator button
|
|
424
|
+
*/
|
|
425
|
+
_toggleFileComments(zone, btn) {
|
|
426
|
+
const isExpanded = this._expandedFiles.has(zone);
|
|
427
|
+
|
|
428
|
+
if (isExpanded) {
|
|
429
|
+
this._expandedFiles.delete(zone);
|
|
430
|
+
btn.classList.remove('expanded');
|
|
431
|
+
zone.classList.remove('file-comments-expanded');
|
|
432
|
+
} else {
|
|
433
|
+
this._expandedFiles.add(zone);
|
|
434
|
+
btn.classList.add('expanded');
|
|
435
|
+
zone.classList.add('file-comments-expanded');
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/** Remove all indicator buttons (both line-level and file-level) from the DOM. */
|
|
294
440
|
_removeAllIndicators() {
|
|
295
441
|
document.querySelectorAll('.comment-indicator').forEach(btn => btn.remove());
|
|
442
|
+
document.querySelectorAll('.file-comment-indicator').forEach(btn => btn.remove());
|
|
296
443
|
}
|
|
297
444
|
}
|
|
298
445
|
|
|
@@ -17,12 +17,15 @@ class FileCommentManager {
|
|
|
17
17
|
if (chatBtn && window.chatPanel) {
|
|
18
18
|
e.stopPropagation();
|
|
19
19
|
const suggestionCard = chatBtn.closest('.ai-suggestion');
|
|
20
|
-
const bodyText = suggestionCard?.dataset?.
|
|
21
|
-
? JSON.parse(suggestionCard.dataset.
|
|
20
|
+
const bodyText = suggestionCard?.dataset?.formattedBody
|
|
21
|
+
? JSON.parse(suggestionCard.dataset.formattedBody)
|
|
22
|
+
: suggestionCard?.dataset?.originalBody
|
|
23
|
+
? JSON.parse(suggestionCard.dataset.originalBody) : '';
|
|
22
24
|
window.chatPanel.open({
|
|
23
25
|
reviewId: this.prManager?.currentPR?.id,
|
|
24
26
|
suggestionId: chatBtn.dataset.suggestionId,
|
|
25
27
|
suggestionContext: {
|
|
28
|
+
suggestionId: chatBtn.dataset.suggestionId || null,
|
|
26
29
|
title: chatBtn.dataset.title || '',
|
|
27
30
|
body: bodyText,
|
|
28
31
|
type: suggestionCard?.querySelector('.ai-suggestion-badge')?.dataset?.type || '',
|
|
@@ -312,6 +315,16 @@ class FileCommentManager {
|
|
|
312
315
|
// Update count badge
|
|
313
316
|
this.updateCommentCount(zone);
|
|
314
317
|
|
|
318
|
+
// Refresh minimize-mode indicators so file-header counts stay current
|
|
319
|
+
if (this.prManager?.commentMinimizer) {
|
|
320
|
+
this.prManager.commentMinimizer.refreshIndicators();
|
|
321
|
+
// Auto-expand so the new comment stays visible in minimize mode
|
|
322
|
+
const newCard = zone.querySelector(`.file-comment-card[data-comment-id="${commentData.id}"]`);
|
|
323
|
+
if (newCard) {
|
|
324
|
+
this.prManager.commentMinimizer.expandForElement(newCard);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
315
328
|
// Notify AI Panel if available
|
|
316
329
|
if (window.aiPanel?.addComment) {
|
|
317
330
|
window.aiPanel.addComment(commentData);
|
|
@@ -322,6 +335,8 @@ class FileCommentManager {
|
|
|
322
335
|
this.prManager.updateCommentCount();
|
|
323
336
|
}
|
|
324
337
|
|
|
338
|
+
window.chatPanel?.queueUserActionHint(`[User Action: created comment ${result.commentId}]`);
|
|
339
|
+
|
|
325
340
|
} catch (error) {
|
|
326
341
|
console.error('Error saving file-level comment:', error);
|
|
327
342
|
if (window.toast) {
|
|
@@ -432,6 +447,7 @@ class FileCommentManager {
|
|
|
432
447
|
// Store original markdown body for adopt functionality via extractSuggestionData
|
|
433
448
|
// Use JSON.stringify to preserve newlines and special characters (matches line-level suggestions)
|
|
434
449
|
card.dataset.originalBody = JSON.stringify(suggestion.body || '');
|
|
450
|
+
card.dataset.formattedBody = JSON.stringify(suggestion.formattedBody || '');
|
|
435
451
|
|
|
436
452
|
// Store target info on the card for reliable retrieval in getFileAndLineInfo
|
|
437
453
|
// File-level suggestions don't have line numbers, just the file name
|
|
@@ -604,6 +620,16 @@ class FileCommentManager {
|
|
|
604
620
|
this.displayUserComment(zone, commentData);
|
|
605
621
|
this.updateCommentCount(zone);
|
|
606
622
|
|
|
623
|
+
// Refresh minimize-mode indicators so file-header counts stay current
|
|
624
|
+
if (this.prManager?.commentMinimizer) {
|
|
625
|
+
this.prManager.commentMinimizer.refreshIndicators();
|
|
626
|
+
// Auto-expand so the new comment stays visible in minimize mode
|
|
627
|
+
const newCard = zone.querySelector(`.file-comment-card[data-comment-id="${commentData.id}"]`);
|
|
628
|
+
if (newCard) {
|
|
629
|
+
this.prManager.commentMinimizer.expandForElement(newCard);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
607
633
|
// Update parent comment count for Preview button
|
|
608
634
|
if (this.prManager?.updateCommentCount) {
|
|
609
635
|
this.prManager.updateCommentCount();
|
|
@@ -619,6 +645,8 @@ class FileCommentManager {
|
|
|
619
645
|
window.aiPanel.updateFindingStatus(suggestion.id, 'adopted');
|
|
620
646
|
}
|
|
621
647
|
|
|
648
|
+
window.chatPanel?.queueUserActionHint(`[User Action: adopted suggestion ${suggestion.id}]`);
|
|
649
|
+
|
|
622
650
|
} catch (error) {
|
|
623
651
|
console.error('Error adopting suggestion:', error);
|
|
624
652
|
if (window.toast) {
|
|
@@ -657,11 +685,18 @@ class FileCommentManager {
|
|
|
657
685
|
|
|
658
686
|
this.updateCommentCount(zone);
|
|
659
687
|
|
|
688
|
+
// Refresh minimize-mode indicators so file-header counts stay current
|
|
689
|
+
if (this.prManager?.commentMinimizer) {
|
|
690
|
+
this.prManager.commentMinimizer.refreshIndicators();
|
|
691
|
+
}
|
|
692
|
+
|
|
660
693
|
// Update finding status in AI Panel (mark suggestion as dismissed)
|
|
661
694
|
if (window.aiPanel?.updateFindingStatus) {
|
|
662
695
|
window.aiPanel.updateFindingStatus(suggestionId, 'dismissed');
|
|
663
696
|
}
|
|
664
697
|
|
|
698
|
+
window.chatPanel?.queueUserActionHint(`[User Action: dismissed suggestion ${suggestionId}]`);
|
|
699
|
+
|
|
665
700
|
} catch (error) {
|
|
666
701
|
console.error('Error dismissing suggestion:', error);
|
|
667
702
|
if (window.toast) {
|
|
@@ -703,6 +738,13 @@ class FileCommentManager {
|
|
|
703
738
|
// Update comment count (for consistency with dismissAISuggestion)
|
|
704
739
|
this.updateCommentCount(zone);
|
|
705
740
|
|
|
741
|
+
// Refresh minimize-mode indicators so file-header counts stay current
|
|
742
|
+
if (this.prManager?.commentMinimizer) {
|
|
743
|
+
this.prManager.commentMinimizer.refreshIndicators();
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
window.chatPanel?.queueUserActionHint(`[User Action: restored suggestion ${suggestionId}]`);
|
|
747
|
+
|
|
706
748
|
} catch (error) {
|
|
707
749
|
console.error('Error restoring suggestion:', error);
|
|
708
750
|
if (window.toast) {
|
|
@@ -848,6 +890,16 @@ class FileCommentManager {
|
|
|
848
890
|
this.displayUserComment(zone, commentData);
|
|
849
891
|
this.updateCommentCount(zone);
|
|
850
892
|
|
|
893
|
+
// Refresh minimize-mode indicators so file-header counts stay current
|
|
894
|
+
if (this.prManager?.commentMinimizer) {
|
|
895
|
+
this.prManager.commentMinimizer.refreshIndicators();
|
|
896
|
+
// Auto-expand so the new comment stays visible in minimize mode
|
|
897
|
+
const newCard = zone.querySelector(`.file-comment-card[data-comment-id="${commentData.id}"]`);
|
|
898
|
+
if (newCard) {
|
|
899
|
+
this.prManager.commentMinimizer.expandForElement(newCard);
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
851
903
|
// Update parent comment count for Preview button
|
|
852
904
|
if (this.prManager?.updateCommentCount) {
|
|
853
905
|
this.prManager.updateCommentCount();
|
|
@@ -863,6 +915,8 @@ class FileCommentManager {
|
|
|
863
915
|
window.aiPanel.updateFindingStatus(suggestion.id, 'adopted');
|
|
864
916
|
}
|
|
865
917
|
|
|
918
|
+
window.chatPanel?.queueUserActionHint(`[User Action: adopted suggestion ${suggestion.id}]`);
|
|
919
|
+
|
|
866
920
|
} catch (error) {
|
|
867
921
|
console.error('Error adopting suggestion with edit:', error);
|
|
868
922
|
if (window.toast) {
|
|
@@ -1012,6 +1066,11 @@ class FileCommentManager {
|
|
|
1012
1066
|
|
|
1013
1067
|
this.updateCommentCount(zone);
|
|
1014
1068
|
|
|
1069
|
+
// Refresh minimize-mode indicators so file-header counts stay current
|
|
1070
|
+
if (this.prManager?.commentMinimizer) {
|
|
1071
|
+
this.prManager.commentMinimizer.refreshIndicators();
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1015
1074
|
// Update parent comment count
|
|
1016
1075
|
if (this.prManager?.updateCommentCount) {
|
|
1017
1076
|
this.prManager.updateCommentCount();
|
|
@@ -1029,6 +1088,11 @@ class FileCommentManager {
|
|
|
1029
1088
|
window.aiPanel.updateFindingStatus(apiResult.dismissedSuggestionId, 'dismissed');
|
|
1030
1089
|
}
|
|
1031
1090
|
|
|
1091
|
+
if (apiResult.dismissedSuggestionId) {
|
|
1092
|
+
window.chatPanel?.queueUserActionHint(`[User Action: dismissed suggestion ${apiResult.dismissedSuggestionId}]`);
|
|
1093
|
+
}
|
|
1094
|
+
window.chatPanel?.queueUserActionHint(`[User Action: dismissed comment ${commentId}]`);
|
|
1095
|
+
|
|
1032
1096
|
} catch (error) {
|
|
1033
1097
|
console.error('Error deleting comment:', error);
|
|
1034
1098
|
if (window.toast) {
|
|
@@ -24,8 +24,9 @@ class SuggestionManager {
|
|
|
24
24
|
reviewId: this.prManager?.currentPR?.id,
|
|
25
25
|
suggestionId: chatBtn.dataset.suggestionId,
|
|
26
26
|
suggestionContext: {
|
|
27
|
+
suggestionId: chatBtn.dataset.suggestionId || null,
|
|
27
28
|
title: chatBtn.dataset.title || suggestionData.suggestionTitle || '',
|
|
28
|
-
body: suggestionData.suggestionText || '',
|
|
29
|
+
body: suggestionData.formattedBody || suggestionData.suggestionText || '',
|
|
29
30
|
type: suggestionData.suggestionType || '',
|
|
30
31
|
file: chatBtn.dataset.file || '',
|
|
31
32
|
line_start: suggestionDiv?.dataset?.lineNumber ? parseInt(suggestionDiv.dataset.lineNumber) : null,
|
package/public/js/pr.js
CHANGED
|
@@ -2542,6 +2542,11 @@ class PRManager {
|
|
|
2542
2542
|
if (window.toast) {
|
|
2543
2543
|
window.toast.showSuccess('Comment dismissed');
|
|
2544
2544
|
}
|
|
2545
|
+
|
|
2546
|
+
if (apiResult.dismissedSuggestionId) {
|
|
2547
|
+
window.chatPanel?.queueUserActionHint(`[User Action: dismissed suggestion ${apiResult.dismissedSuggestionId}]`);
|
|
2548
|
+
}
|
|
2549
|
+
window.chatPanel?.queueUserActionHint(`[User Action: dismissed comment ${commentId}]`);
|
|
2545
2550
|
} catch (error) {
|
|
2546
2551
|
console.error('Error deleting comment:', error);
|
|
2547
2552
|
if (window.toast) {
|
|
@@ -2570,6 +2575,8 @@ class PRManager {
|
|
|
2570
2575
|
if (window.toast) {
|
|
2571
2576
|
window.toast.showSuccess('Comment restored');
|
|
2572
2577
|
}
|
|
2578
|
+
|
|
2579
|
+
window.chatPanel?.queueUserActionHint(`[User Action: restored comment ${commentId}]`);
|
|
2573
2580
|
} catch (error) {
|
|
2574
2581
|
console.error('Error restoring comment:', error);
|
|
2575
2582
|
if (window.toast) {
|
|
@@ -3050,6 +3057,7 @@ class PRManager {
|
|
|
3050
3057
|
});
|
|
3051
3058
|
this.displayUserComment(newComment, suggestionRow);
|
|
3052
3059
|
this._notifyAdoption(suggestionId, newComment);
|
|
3060
|
+
window.chatPanel?.queueUserActionHint(`[User Action: adopted suggestion ${suggestionId}]`);
|
|
3053
3061
|
} catch (error) {
|
|
3054
3062
|
console.error('Error saving edited suggestion:', error);
|
|
3055
3063
|
alert(`Failed to save suggestion: ${error.message}`);
|
|
@@ -3095,6 +3103,7 @@ class PRManager {
|
|
|
3095
3103
|
|
|
3096
3104
|
this.displayUserComment(result.newComment, result.suggestionRow);
|
|
3097
3105
|
this._notifyAdoption(suggestionId, result.newComment);
|
|
3106
|
+
window.chatPanel?.queueUserActionHint(`[User Action: adopted suggestion ${suggestionId}]`);
|
|
3098
3107
|
} catch (error) {
|
|
3099
3108
|
console.error('Error adopting suggestion:', error);
|
|
3100
3109
|
alert(`Failed to adopt suggestion: ${error.message}`);
|
|
@@ -3158,6 +3167,8 @@ class PRManager {
|
|
|
3158
3167
|
if (this.commentMinimizer) {
|
|
3159
3168
|
this.commentMinimizer.refreshIndicators();
|
|
3160
3169
|
}
|
|
3170
|
+
|
|
3171
|
+
window.chatPanel?.queueUserActionHint(`[User Action: dismissed suggestion ${suggestionId}]`);
|
|
3161
3172
|
} catch (error) {
|
|
3162
3173
|
console.error('Error dismissing suggestion:', error);
|
|
3163
3174
|
alert('Failed to dismiss suggestion');
|
|
@@ -3213,6 +3224,8 @@ class PRManager {
|
|
|
3213
3224
|
if (this.commentMinimizer) {
|
|
3214
3225
|
this.commentMinimizer.refreshIndicators();
|
|
3215
3226
|
}
|
|
3227
|
+
|
|
3228
|
+
window.chatPanel?.queueUserActionHint(`[User Action: restored suggestion ${suggestionId}]`);
|
|
3216
3229
|
} catch (error) {
|
|
3217
3230
|
console.error('Error restoring suggestion:', error);
|
|
3218
3231
|
alert('Failed to restore suggestion');
|
package/src/ai/analyzer.js
CHANGED
|
@@ -126,8 +126,8 @@ async function runExecutableVoice(voiceProvider, reviewId, worktreePath, prMetad
|
|
|
126
126
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'pair-review-exec-'));
|
|
127
127
|
try {
|
|
128
128
|
const executableContext = {
|
|
129
|
-
title: prMetadata.title ||
|
|
130
|
-
description: prMetadata.description ||
|
|
129
|
+
title: prMetadata.title || null,
|
|
130
|
+
description: prMetadata.description || null,
|
|
131
131
|
cwd: worktreePath,
|
|
132
132
|
outputDir: tmpDir,
|
|
133
133
|
model: voiceProvider.resolvedModel !== undefined ? voiceProvider.resolvedModel : (voiceProvider.model || null),
|
|
@@ -176,11 +176,16 @@ async function runExecutableVoice(voiceProvider, reviewId, worktreePath, prMetad
|
|
|
176
176
|
});
|
|
177
177
|
|
|
178
178
|
if (!result?.success || !result?.data) {
|
|
179
|
+
if (progressCallback) {
|
|
180
|
+
progressCallback({ level: 'exec', status: 'failed', progress: 'External tool returned no data' });
|
|
181
|
+
}
|
|
179
182
|
throw new Error(`${logPrefix || ''}Executable provider returned no data`);
|
|
180
183
|
}
|
|
181
184
|
|
|
185
|
+
const suggestions = result.data.suggestions || [];
|
|
186
|
+
|
|
182
187
|
return {
|
|
183
|
-
suggestions
|
|
188
|
+
suggestions,
|
|
184
189
|
summary: result.data.summary || ''
|
|
185
190
|
};
|
|
186
191
|
} finally {
|
|
@@ -2948,6 +2953,10 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
2948
2953
|
const finalSuggestions = this.validateAndFinalizeSuggestions(result.suggestions, fileLineCountMap, validFiles);
|
|
2949
2954
|
await this.storeSuggestions(reviewId, parentRunId, finalSuggestions, null, validFiles);
|
|
2950
2955
|
|
|
2956
|
+
if (progressCallback) {
|
|
2957
|
+
progressCallback({ level: 'exec', status: 'completed', progress: `External tool complete: ${finalSuggestions.length} suggestions` });
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2951
2960
|
try {
|
|
2952
2961
|
await analysisRunRepo.update(parentRunId, {
|
|
2953
2962
|
status: 'completed',
|
|
@@ -3074,6 +3083,10 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
3074
3083
|
// Store validated voice suggestions
|
|
3075
3084
|
await commentRepo.bulkInsertAISuggestions(reviewId, childRunId, validatedSuggestions, null);
|
|
3076
3085
|
|
|
3086
|
+
if (voiceProgressCallback) {
|
|
3087
|
+
voiceProgressCallback({ level: 'exec', status: 'completed', progress: `External tool complete: ${validatedSuggestions.length} suggestions` });
|
|
3088
|
+
}
|
|
3089
|
+
|
|
3077
3090
|
const validatedResult = { ...result, suggestions: validatedSuggestions };
|
|
3078
3091
|
return { voiceKey, reviewerLabel, childRunId, result: validatedResult, provider: voice.provider, model: voice.model, isExecutable: true, customInstructions: voice.customInstructions || null };
|
|
3079
3092
|
}
|
|
@@ -3121,6 +3134,11 @@ File-level suggestions should NOT have a line number. They apply to the entire f
|
|
|
3121
3134
|
} catch (err) {
|
|
3122
3135
|
logger.warn(`[ReviewerCouncil] Failed to update child run ${childRunId}: ${err.message}`);
|
|
3123
3136
|
}
|
|
3137
|
+
// Notify the progress dialog so the voice row stops spinning
|
|
3138
|
+
if (isExecutable && voiceProgressCallback) {
|
|
3139
|
+
const terminalStatus = error.isCancellation ? 'cancelled' : 'failed';
|
|
3140
|
+
voiceProgressCallback({ level: 'exec', status: terminalStatus, progress: error.message || 'Voice failed' });
|
|
3141
|
+
}
|
|
3124
3142
|
throw error;
|
|
3125
3143
|
}
|
|
3126
3144
|
});
|
|
@@ -22,23 +22,13 @@ const BIN_DIR = path.join(__dirname, '..', '..', 'bin');
|
|
|
22
22
|
const CLAUDE_MODELS = [
|
|
23
23
|
{
|
|
24
24
|
id: 'haiku',
|
|
25
|
-
name: 'Haiku 4.
|
|
25
|
+
name: 'Haiku 4.6',
|
|
26
26
|
tier: 'fast',
|
|
27
27
|
tagline: 'Lightning Fast',
|
|
28
28
|
description: 'Quick analysis for simple changes',
|
|
29
29
|
badge: 'Fastest',
|
|
30
30
|
badgeClass: 'badge-speed'
|
|
31
31
|
},
|
|
32
|
-
{
|
|
33
|
-
id: 'sonnet-4.5',
|
|
34
|
-
cli_model: 'claude-sonnet-4.5',
|
|
35
|
-
name: 'Sonnet 4.5',
|
|
36
|
-
tier: 'balanced',
|
|
37
|
-
tagline: 'Previous Gen',
|
|
38
|
-
description: 'Sonnet 4.5 — previous generation balanced model',
|
|
39
|
-
badge: 'Previous Gen',
|
|
40
|
-
badgeClass: 'badge-balanced'
|
|
41
|
-
},
|
|
42
32
|
{
|
|
43
33
|
id: 'sonnet-4.6',
|
|
44
34
|
cli_model: 'claude-sonnet-4-6',
|
package/src/ai/codex-provider.js
CHANGED
|
@@ -21,27 +21,29 @@ const BIN_DIR = path.join(__dirname, '..', '..', 'bin');
|
|
|
21
21
|
* Codex model definitions with tier mappings
|
|
22
22
|
*
|
|
23
23
|
* Based on OpenAI Codex Models guide (developers.openai.com/codex/models)
|
|
24
|
-
* - gpt-5.
|
|
25
|
-
* - gpt-5.
|
|
26
|
-
* - gpt-5.
|
|
27
|
-
* - gpt-5.
|
|
24
|
+
* - gpt-5.4-nano: Cheapest model ($0.20/$1.25 per MTok), good for surface scans
|
|
25
|
+
* - gpt-5.4-mini: Fast with 400k context ($0.75/$4.50 per MTok)
|
|
26
|
+
* - gpt-5.4: Flagship model combining coding, reasoning, and agentic workflows
|
|
27
|
+
* - gpt-5.3-codex: Industry-leading coding model for complex engineering tasks
|
|
28
|
+
*
|
|
29
|
+
* Deprecated (April 2026): gpt-5.1-codex-mini, gpt-5.1-codex-max, gpt-5.1-codex
|
|
28
30
|
*/
|
|
29
31
|
const CODEX_MODELS = [
|
|
30
32
|
{
|
|
31
|
-
id: 'gpt-5.
|
|
32
|
-
name: 'GPT-5.
|
|
33
|
+
id: 'gpt-5.4-nano',
|
|
34
|
+
name: 'GPT-5.4 Nano',
|
|
33
35
|
tier: 'fast',
|
|
34
|
-
tagline: '
|
|
35
|
-
description: '
|
|
36
|
-
badge: '
|
|
36
|
+
tagline: 'Cheapest',
|
|
37
|
+
description: 'Ultra-low-cost surface scans for style issues, obvious bugs, and lint-level feedback.',
|
|
38
|
+
badge: 'Cheapest',
|
|
37
39
|
badgeClass: 'badge-speed'
|
|
38
40
|
},
|
|
39
41
|
{
|
|
40
|
-
id: 'gpt-5.
|
|
41
|
-
name: 'GPT-5.
|
|
42
|
+
id: 'gpt-5.4-mini',
|
|
43
|
+
name: 'GPT-5.4 Mini',
|
|
42
44
|
tier: 'balanced',
|
|
43
45
|
tagline: 'Best Balance',
|
|
44
|
-
description: '
|
|
46
|
+
description: 'Fast reviews with 400k context—good balance of speed and capability for everyday PR review.',
|
|
45
47
|
badge: 'Recommended',
|
|
46
48
|
badgeClass: 'badge-recommended',
|
|
47
49
|
default: true
|
|
@@ -51,7 +53,7 @@ const CODEX_MODELS = [
|
|
|
51
53
|
name: 'GPT-5.3 Codex',
|
|
52
54
|
tier: 'thorough',
|
|
53
55
|
tagline: 'Deep Review',
|
|
54
|
-
description: '
|
|
56
|
+
description: 'Industry-leading coding model—frontier performance with strong reasoning for cross-file analysis.',
|
|
55
57
|
badge: 'Thorough',
|
|
56
58
|
badgeClass: 'badge-power'
|
|
57
59
|
},
|
|
@@ -60,7 +62,7 @@ const CODEX_MODELS = [
|
|
|
60
62
|
name: 'GPT-5.4',
|
|
61
63
|
tier: 'thorough',
|
|
62
64
|
tagline: 'Latest Gen',
|
|
63
|
-
description: '
|
|
65
|
+
description: 'Flagship model combining coding, reasoning, and agentic workflows for complex architectural reviews.',
|
|
64
66
|
badge: 'Most Thorough',
|
|
65
67
|
badgeClass: 'badge-power'
|
|
66
68
|
}
|
|
@@ -76,7 +78,7 @@ class CodexProvider extends AIProvider {
|
|
|
76
78
|
* @param {Object} configOverrides.env - Additional environment variables
|
|
77
79
|
* @param {Object[]} configOverrides.models - Custom model definitions
|
|
78
80
|
*/
|
|
79
|
-
constructor(model = 'gpt-5.
|
|
81
|
+
constructor(model = 'gpt-5.4-mini', configOverrides = {}) {
|
|
80
82
|
super(model);
|
|
81
83
|
|
|
82
84
|
// Command precedence: ENV > config > default
|
|
@@ -698,7 +700,7 @@ class CodexProvider extends AIProvider {
|
|
|
698
700
|
}
|
|
699
701
|
|
|
700
702
|
static getDefaultModel() {
|
|
701
|
-
return 'gpt-5.
|
|
703
|
+
return 'gpt-5.4-mini';
|
|
702
704
|
}
|
|
703
705
|
|
|
704
706
|
static getInstallInstructions() {
|
|
@@ -21,21 +21,30 @@ const BIN_DIR = path.join(__dirname, '..', '..', 'bin');
|
|
|
21
21
|
*
|
|
22
22
|
* GitHub Copilot CLI supports multiple AI models including OpenAI,
|
|
23
23
|
* Anthropic, and Google models via the --model flag.
|
|
24
|
-
* Available models (as of
|
|
25
|
-
* claude-sonnet-4.5, gpt-5.
|
|
24
|
+
* Available models (as of April 2026): claude-haiku-4.6, claude-sonnet-4.6,
|
|
25
|
+
* claude-sonnet-4.5, gpt-5.4, gpt-5.4-mini, gpt-5.3-codex,
|
|
26
26
|
* claude-opus-4.5, claude-opus-4.6, claude-opus-4.6-fast.
|
|
27
27
|
* Default is claude-sonnet-4.6.
|
|
28
28
|
*/
|
|
29
29
|
const COPILOT_MODELS = [
|
|
30
30
|
{
|
|
31
|
-
id: 'claude-haiku-4.
|
|
32
|
-
name: 'Claude Haiku 4.
|
|
31
|
+
id: 'claude-haiku-4.6',
|
|
32
|
+
name: 'Claude Haiku 4.6',
|
|
33
33
|
tier: 'fast',
|
|
34
34
|
tagline: 'Quick Scan',
|
|
35
35
|
description: 'Rapid feedback for obvious issues, style checks, and simple logic errors',
|
|
36
36
|
badge: 'Speedy',
|
|
37
37
|
badgeClass: 'badge-speed'
|
|
38
38
|
},
|
|
39
|
+
{
|
|
40
|
+
id: 'gpt-5.4-mini',
|
|
41
|
+
name: 'GPT-5.4 Mini',
|
|
42
|
+
tier: 'fast',
|
|
43
|
+
tagline: 'Fast & Cheap',
|
|
44
|
+
description: 'Low-cost fast reviews with solid reasoning—included at no premium cost',
|
|
45
|
+
badge: 'Fast',
|
|
46
|
+
badgeClass: 'badge-speed'
|
|
47
|
+
},
|
|
39
48
|
{
|
|
40
49
|
id: 'claude-sonnet-4.6',
|
|
41
50
|
name: 'Claude Sonnet 4.6',
|
|
@@ -47,29 +56,20 @@ const COPILOT_MODELS = [
|
|
|
47
56
|
default: true
|
|
48
57
|
},
|
|
49
58
|
{
|
|
50
|
-
id: '
|
|
51
|
-
name: '
|
|
52
|
-
tier: '
|
|
53
|
-
tagline: '
|
|
54
|
-
description: '
|
|
55
|
-
badge: '
|
|
56
|
-
badgeClass: 'badge-
|
|
57
|
-
},
|
|
58
|
-
{
|
|
59
|
-
id: 'gpt-5.2-codex',
|
|
60
|
-
name: 'GPT-5.2 Codex',
|
|
61
|
-
tier: 'balanced',
|
|
62
|
-
tagline: 'Alternative View',
|
|
63
|
-
description: 'OpenAI code-specialized model—different perspective for cross-file analysis',
|
|
64
|
-
badge: 'Balanced',
|
|
65
|
-
badgeClass: 'badge-balanced'
|
|
59
|
+
id: 'gpt-5.4',
|
|
60
|
+
name: 'GPT-5.4',
|
|
61
|
+
tier: 'thorough',
|
|
62
|
+
tagline: 'Latest OpenAI',
|
|
63
|
+
description: 'Flagship OpenAI model combining coding, reasoning, and agentic workflows',
|
|
64
|
+
badge: 'Latest',
|
|
65
|
+
badgeClass: 'badge-power'
|
|
66
66
|
},
|
|
67
67
|
{
|
|
68
68
|
id: 'gpt-5.3-codex',
|
|
69
69
|
name: 'GPT-5.3 Codex',
|
|
70
70
|
tier: 'thorough',
|
|
71
71
|
tagline: 'Deep Code Analysis',
|
|
72
|
-
description: '
|
|
72
|
+
description: 'Industry-leading coding model—frontier performance for complex multi-file reviews',
|
|
73
73
|
badge: 'Thorough',
|
|
74
74
|
badgeClass: 'badge-power'
|
|
75
75
|
},
|