@in-the-loop-labs/pair-review 3.1.3 → 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/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/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/session-manager.js +1 -0
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
|
}
|
|
@@ -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');
|
|
@@ -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');
|
|
@@ -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
|
},
|
|
@@ -20,6 +20,16 @@ const BIN_DIR = path.join(__dirname, '..', '..', 'bin');
|
|
|
20
20
|
* Gemini model definitions with tier mappings
|
|
21
21
|
*/
|
|
22
22
|
const GEMINI_MODELS = [
|
|
23
|
+
{
|
|
24
|
+
id: 'gemini-3.1-flash-lite-preview',
|
|
25
|
+
aliases: ['gemini-3.1-flash-lite'],
|
|
26
|
+
name: '3.1 Flash Lite',
|
|
27
|
+
tier: 'fast',
|
|
28
|
+
tagline: 'Cheapest',
|
|
29
|
+
description: 'Ultra-efficient model for high-volume cost-conscious scans',
|
|
30
|
+
badge: 'Cheapest',
|
|
31
|
+
badgeClass: 'badge-speed'
|
|
32
|
+
},
|
|
23
33
|
{
|
|
24
34
|
id: 'gemini-3-flash-preview',
|
|
25
35
|
aliases: ['gemini-3-flash'],
|
package/src/ai/pi-provider.js
CHANGED
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
* for cross-provider switching, which translates to `--provider <provider> --model <model>`.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
+
const crypto = require('crypto');
|
|
19
20
|
const path = require('path');
|
|
21
|
+
const os = require('os');
|
|
22
|
+
const fs = require('fs');
|
|
20
23
|
const { spawn } = require('child_process');
|
|
21
24
|
const { AIProvider, registerProvider, quoteShellArgs } = require('./provider');
|
|
22
25
|
const logger = require('../utils/logger');
|
|
@@ -244,19 +247,23 @@ class PiProvider extends AIProvider {
|
|
|
244
247
|
|
|
245
248
|
const levelPrefix = logPrefix || `[Level ${level}]`;
|
|
246
249
|
logger.info(`${levelPrefix} Executing Pi CLI...`);
|
|
247
|
-
logger.info(`${levelPrefix}
|
|
250
|
+
logger.info(`${levelPrefix} Prompt: ${prompt.length} bytes`);
|
|
251
|
+
|
|
252
|
+
// Write prompt to a temp file and use Pi's @file syntax as a positional arg.
|
|
253
|
+
// This bypasses devx stdin interference that breaks --mode json output.
|
|
254
|
+
const tmpFile = path.join(os.tmpdir(), `pair-review-prompt-${Date.now()}-${process.pid}-${crypto.randomUUID()}.txt`);
|
|
255
|
+
fs.writeFileSync(tmpFile, prompt);
|
|
256
|
+
const cleanupTmpFile = () => { try { fs.unlinkSync(tmpFile); } catch { /* ignore */ } };
|
|
248
257
|
|
|
249
|
-
// Use stdin for prompt instead of CLI argument (avoids shell escaping issues)
|
|
250
|
-
// Pi reads from stdin when using -p with no positional message arguments
|
|
251
258
|
let fullCommand;
|
|
252
259
|
let fullArgs;
|
|
253
260
|
|
|
254
261
|
if (this.useShell) {
|
|
255
|
-
fullCommand = `${this.piCmd} ${quoteShellArgs(this.baseArgs).join(' ')}`;
|
|
262
|
+
fullCommand = `${this.piCmd} ${quoteShellArgs([...this.baseArgs, `@${tmpFile}`]).join(' ')}`;
|
|
256
263
|
fullArgs = [];
|
|
257
264
|
} else {
|
|
258
265
|
fullCommand = this.piCmd;
|
|
259
|
-
fullArgs = [...this.baseArgs];
|
|
266
|
+
fullArgs = [...this.baseArgs, `@${tmpFile}`];
|
|
260
267
|
}
|
|
261
268
|
|
|
262
269
|
const pi = spawn(fullCommand, fullArgs, {
|
|
@@ -269,6 +276,10 @@ class PiProvider extends AIProvider {
|
|
|
269
276
|
shell: this.useShell
|
|
270
277
|
});
|
|
271
278
|
|
|
279
|
+
// Close stdin immediately — prompt is delivered via @file, but some
|
|
280
|
+
// wrappers (e.g., devx) keep the process alive until stdin is closed.
|
|
281
|
+
pi.stdin.end();
|
|
282
|
+
|
|
272
283
|
const pid = pi.pid;
|
|
273
284
|
logger.debug(`${levelPrefix} Pi CLI command: ${fullCommand} ${fullArgs.join(' ')}`);
|
|
274
285
|
logger.info(`${levelPrefix} Spawned Pi CLI process: PID ${pid}`);
|
|
@@ -340,6 +351,7 @@ class PiProvider extends AIProvider {
|
|
|
340
351
|
|
|
341
352
|
// Handle completion
|
|
342
353
|
pi.on('close', (code) => {
|
|
354
|
+
cleanupTmpFile();
|
|
343
355
|
if (settled) return; // Already settled by timeout or error
|
|
344
356
|
|
|
345
357
|
// Flush any remaining stream parser buffer
|
|
@@ -413,7 +425,7 @@ class PiProvider extends AIProvider {
|
|
|
413
425
|
|
|
414
426
|
// Use async IIFE to handle the async LLM extraction
|
|
415
427
|
(async () => {
|
|
416
|
-
// Guard: if already settled (by timeout,
|
|
428
|
+
// Guard: if already settled (by timeout, process error, or cancellation),
|
|
417
429
|
// skip the LLM extraction entirely to avoid misleading log output
|
|
418
430
|
if (settled) return;
|
|
419
431
|
|
|
@@ -437,6 +449,7 @@ class PiProvider extends AIProvider {
|
|
|
437
449
|
|
|
438
450
|
// Handle errors
|
|
439
451
|
pi.on('error', (error) => {
|
|
452
|
+
cleanupTmpFile();
|
|
440
453
|
if (error.code === 'ENOENT') {
|
|
441
454
|
logger.error(`${levelPrefix} Pi CLI not found. Please ensure Pi CLI is installed.`);
|
|
442
455
|
settle(reject, new Error(`${levelPrefix} Pi CLI not found. ${PiProvider.getInstallInstructions()}`));
|
|
@@ -445,21 +458,6 @@ class PiProvider extends AIProvider {
|
|
|
445
458
|
settle(reject, error);
|
|
446
459
|
}
|
|
447
460
|
});
|
|
448
|
-
|
|
449
|
-
// Handle stdin errors (e.g., EPIPE if process exits before write completes)
|
|
450
|
-
pi.stdin.on('error', (err) => {
|
|
451
|
-
logger.error(`${levelPrefix} stdin error: ${err.message}`);
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
// Send the prompt to stdin (Pi reads from stdin when using -p with no args)
|
|
455
|
-
pi.stdin.write(prompt, (err) => {
|
|
456
|
-
if (err) {
|
|
457
|
-
logger.error(`${levelPrefix} Failed to write prompt to stdin: ${err}`);
|
|
458
|
-
pi.kill('SIGTERM');
|
|
459
|
-
settle(reject, new Error(`${levelPrefix} Failed to write prompt to stdin: ${err}`));
|
|
460
|
-
}
|
|
461
|
-
});
|
|
462
|
-
pi.stdin.end();
|
|
463
461
|
});
|
|
464
462
|
}
|
|
465
463
|
|
|
@@ -740,14 +738,13 @@ class PiProvider extends AIProvider {
|
|
|
740
738
|
// Build args consistently using the shared method, applying provider and model extra_args
|
|
741
739
|
const args = this.buildArgsForModel(model);
|
|
742
740
|
|
|
743
|
-
//
|
|
744
|
-
// Pi reads from stdin when using -p with no positional message arguments
|
|
741
|
+
// Use @file syntax for prompt delivery (bypasses devx stdin interference)
|
|
745
742
|
if (useShell) {
|
|
746
743
|
return {
|
|
747
744
|
command: `${piCmd} ${quoteShellArgs(args).join(' ')}`,
|
|
748
745
|
args: [],
|
|
749
746
|
useShell: true,
|
|
750
|
-
|
|
747
|
+
promptViaFile: true,
|
|
751
748
|
env: this.extraEnv
|
|
752
749
|
};
|
|
753
750
|
}
|
|
@@ -755,7 +752,7 @@ class PiProvider extends AIProvider {
|
|
|
755
752
|
command: piCmd,
|
|
756
753
|
args,
|
|
757
754
|
useShell: false,
|
|
758
|
-
|
|
755
|
+
promptViaFile: true,
|
|
759
756
|
env: this.extraEnv
|
|
760
757
|
};
|
|
761
758
|
}
|
package/src/ai/provider.js
CHANGED
|
@@ -6,7 +6,10 @@
|
|
|
6
6
|
* and provides a factory function to create provider instances.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
const crypto = require('crypto');
|
|
9
10
|
const path = require('path');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const fs = require('fs');
|
|
10
13
|
const { spawn } = require('child_process');
|
|
11
14
|
const logger = require('../utils/logger');
|
|
12
15
|
const { extractJSON } = require('../utils/json-extractor');
|
|
@@ -181,6 +184,7 @@ class AIProvider {
|
|
|
181
184
|
* @property {string[]} args - Arguments (prompt will be appended if promptViaStdin is false)
|
|
182
185
|
* @property {boolean} useShell - Whether to use shell mode
|
|
183
186
|
* @property {boolean} promptViaStdin - If true, send prompt to stdin; if false, append to args
|
|
187
|
+
* @property {boolean} promptViaFile - If true, write prompt to a temp file and pass @filepath as a positional arg (Pi-specific @file syntax; currently only used by PiProvider)
|
|
184
188
|
*/
|
|
185
189
|
getExtractionConfig(model) {
|
|
186
190
|
// Default: extraction not supported
|
|
@@ -213,7 +217,7 @@ class AIProvider {
|
|
|
213
217
|
};
|
|
214
218
|
}
|
|
215
219
|
|
|
216
|
-
const { command, args, useShell, promptViaStdin, env: configEnv } = config;
|
|
220
|
+
const { command, args, useShell, promptViaStdin, promptViaFile, env: configEnv } = config;
|
|
217
221
|
const prompt = `Extract the JSON object from the following text. Return ONLY the valid JSON, nothing else. Do not include any explanation, markdown formatting, or code blocks - just the raw JSON.
|
|
218
222
|
|
|
219
223
|
=== BEGIN INPUT TEXT ===
|
|
@@ -222,7 +226,21 @@ ${rawResponse}
|
|
|
222
226
|
|
|
223
227
|
return new Promise((resolve) => {
|
|
224
228
|
// Build final command and args based on prompt delivery method
|
|
225
|
-
|
|
229
|
+
// promptViaFile: write to temp file, pass @filepath as positional arg (Pi @file syntax)
|
|
230
|
+
// promptViaStdin: write to process stdin after spawn
|
|
231
|
+
// default: pass prompt as positional CLI arg
|
|
232
|
+
let tmpFile = null;
|
|
233
|
+
let cleanupTmpFile = () => {};
|
|
234
|
+
let finalArgs;
|
|
235
|
+
|
|
236
|
+
if (promptViaFile) {
|
|
237
|
+
tmpFile = path.join(os.tmpdir(), `pair-review-extract-${Date.now()}-${process.pid}-${crypto.randomUUID()}.txt`);
|
|
238
|
+
fs.writeFileSync(tmpFile, prompt);
|
|
239
|
+
cleanupTmpFile = () => { try { fs.unlinkSync(tmpFile); } catch { /* ignore */ } };
|
|
240
|
+
finalArgs = [...args, `@${tmpFile}`];
|
|
241
|
+
} else {
|
|
242
|
+
finalArgs = promptViaStdin ? args : [...args, prompt];
|
|
243
|
+
}
|
|
226
244
|
|
|
227
245
|
logger.info(`${levelPrefix} Attempting LLM-based JSON extraction with ${extractionModel}...`);
|
|
228
246
|
|
|
@@ -269,6 +287,7 @@ ${rawResponse}
|
|
|
269
287
|
});
|
|
270
288
|
|
|
271
289
|
proc.on('close', (code) => {
|
|
290
|
+
cleanupTmpFile();
|
|
272
291
|
if (settled) return;
|
|
273
292
|
|
|
274
293
|
if (code !== 0) {
|
|
@@ -295,11 +314,12 @@ ${rawResponse}
|
|
|
295
314
|
});
|
|
296
315
|
|
|
297
316
|
proc.on('error', (error) => {
|
|
317
|
+
cleanupTmpFile();
|
|
298
318
|
logger.warn(`${levelPrefix} LLM extraction process error: ${error.message}`);
|
|
299
319
|
settle({ success: false, error: error.message });
|
|
300
320
|
});
|
|
301
321
|
|
|
302
|
-
//
|
|
322
|
+
// Deliver prompt based on config method
|
|
303
323
|
if (promptViaStdin) {
|
|
304
324
|
// Handle stdin errors (e.g., EPIPE if process exits before write completes)
|
|
305
325
|
proc.stdin.on('error', (err) => {
|
|
@@ -314,6 +334,9 @@ ${rawResponse}
|
|
|
314
334
|
}
|
|
315
335
|
});
|
|
316
336
|
proc.stdin.end();
|
|
337
|
+
} else if (promptViaFile) {
|
|
338
|
+
// Prompt delivered via @file arg — close stdin so wrappers (e.g., devx) don't hang
|
|
339
|
+
proc.stdin.end();
|
|
317
340
|
}
|
|
318
341
|
});
|
|
319
342
|
}
|
package/src/chat/pi-bridge.js
CHANGED
|
@@ -35,6 +35,7 @@ class PiBridge extends EventEmitter {
|
|
|
35
35
|
* @param {boolean} [options.useShell] - Use shell mode for multi-word commands
|
|
36
36
|
* @param {string[]} [options.skills] - Array of skill file paths to load via --skill
|
|
37
37
|
* @param {string[]} [options.extensions] - Array of extension directory paths to load via -e
|
|
38
|
+
* @param {string[]} [options.extraArgs] - Extra CLI args to append (e.g., from config extra_args)
|
|
38
39
|
* @param {string} [options.sessionPath] - Path to a session file for resumption
|
|
39
40
|
*/
|
|
40
41
|
constructor(options = {}) {
|
|
@@ -49,6 +50,7 @@ class PiBridge extends EventEmitter {
|
|
|
49
50
|
this.useShell = options.useShell || false;
|
|
50
51
|
this.skills = options.skills || [];
|
|
51
52
|
this.extensions = options.extensions || [];
|
|
53
|
+
this.extraArgs = options.extraArgs || [];
|
|
52
54
|
this.sessionPath = options.sessionPath || null;
|
|
53
55
|
|
|
54
56
|
this._process = null;
|
|
@@ -288,6 +290,12 @@ class PiBridge extends EventEmitter {
|
|
|
288
290
|
args.push('-e', ext);
|
|
289
291
|
}
|
|
290
292
|
|
|
293
|
+
// Append extra args from provider config (e.g., extra_args in chat_providers).
|
|
294
|
+
// These go last so they can override earlier flags if needed.
|
|
295
|
+
if (this.extraArgs.length > 0) {
|
|
296
|
+
args.push(...this.extraArgs);
|
|
297
|
+
}
|
|
298
|
+
|
|
291
299
|
return args;
|
|
292
300
|
}
|
|
293
301
|
|