@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.
@@ -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
- * Clicking an indicator toggles visibility of that line's comments only.
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
- // Find the containing comment/suggestion row
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
- /** Remove all indicator buttons from the DOM. */
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?.originalBody
21
- ? JSON.parse(suggestionCard.dataset.originalBody) : '';
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');
@@ -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: result.data.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.5',
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',
@@ -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.1-codex-mini: Smaller, cost-effective variant for quick scans
25
- * - gpt-5.2-codex: Advanced coding model for everyday reviews, good reasoning/cost balance
26
- * - gpt-5.3-codex: Capable agentic coding model with frontier performance and reasoning
27
- * - gpt-5.4: Latest generation with enhanced reasoning depth
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.1-codex-mini',
32
- name: 'GPT-5.1 Mini',
33
+ id: 'gpt-5.4-nano',
34
+ name: 'GPT-5.4 Nano',
33
35
  tier: 'fast',
34
- tagline: 'Blazing Fast',
35
- description: 'Quick, low-cost reviews for style issues, obvious bugs, and lint-level feedback.',
36
- badge: 'Fastest',
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.2-codex',
41
- name: 'GPT-5.2 Codex',
42
+ id: 'gpt-5.4-mini',
43
+ name: 'GPT-5.4 Mini',
42
44
  tier: 'balanced',
43
45
  tagline: 'Best Balance',
44
- description: 'Strong everyday reviewer—good reasoning and code understanding for PR-sized changes without top-tier cost.',
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: 'Capable agentic coding model—combines frontier coding performance with strong reasoning for cross-file analysis.',
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: 'Latest generation model with enhanced reasoning depth for complex architectural reviews.',
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.2-codex', configOverrides = {}) {
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.2-codex';
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 Feb 2026): claude-haiku-4.5, claude-sonnet-4.6,
25
- * claude-sonnet-4.5, gpt-5.2-codex, gpt-5.3-codex,
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.5',
32
- name: 'Claude Haiku 4.5',
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: 'claude-sonnet-4.5',
51
- name: 'Claude Sonnet 4.5',
52
- tier: 'balanced',
53
- tagline: 'Previous Gen',
54
- description: 'Previous generation Sonnet—strong code understanding with excellent quality-to-cost ratio',
55
- badge: 'Previous Gen',
56
- badgeClass: 'badge-balanced'
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: 'Most capable OpenAI coding model—frontier performance for complex multi-file reviews',
72
+ description: 'Industry-leading coding model—frontier performance for complex multi-file reviews',
73
73
  badge: 'Thorough',
74
74
  badgeClass: 'badge-power'
75
75
  },