@in-the-loop-labs/pair-review 1.6.0 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@in-the-loop-labs/pair-review",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "Your AI-powered code review partner - Close the feedback loop with AI coding agents",
5
5
  "main": "src/server.js",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pair-review",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "pair-review app integration — Open PRs and local changes in the pair-review web UI, run server-side AI analysis, and address review feedback. Requires the pair-review MCP server.",
5
5
  "author": {
6
6
  "name": "in-the-loop-labs",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-critic",
3
- "version": "1.6.0",
3
+ "version": "1.6.2",
4
4
  "description": "AI-powered code review analysis — Run three-level AI analysis and implement-review-fix loops directly in your coding agent. Works standalone, no server required.",
5
5
  "author": {
6
6
  "name": "in-the-loop-labs",
package/public/css/pr.css CHANGED
@@ -2481,7 +2481,7 @@ tr.newly-expanded .d2h-code-line-ctn {
2481
2481
  background-color: var(--color-bg-secondary);
2482
2482
  }
2483
2483
 
2484
- /* Progress Modal Styles */
2484
+ /* Modal Styles */
2485
2485
  .modal-overlay {
2486
2486
  position: fixed;
2487
2487
  top: 0;
@@ -2556,102 +2556,6 @@ tr.newly-expanded .d2h-code-line-ctn {
2556
2556
  overflow-y: auto;
2557
2557
  }
2558
2558
 
2559
- .progress-levels {
2560
- display: flex;
2561
- flex-direction: column;
2562
- gap: 16px;
2563
- }
2564
-
2565
- .progress-level {
2566
- display: flex;
2567
- align-items: flex-start;
2568
- gap: 12px;
2569
- }
2570
-
2571
- .level-icon {
2572
- width: 20px;
2573
- height: 20px;
2574
- display: flex;
2575
- align-items: center;
2576
- justify-content: center;
2577
- margin-top: 2px;
2578
- }
2579
-
2580
- .level-icon .icon {
2581
- font-size: 14px;
2582
- display: flex;
2583
- align-items: center;
2584
- justify-content: center;
2585
- width: 16px;
2586
- height: 16px;
2587
- }
2588
-
2589
- .level-icon .icon.pending {
2590
- color: #656d76;
2591
- }
2592
-
2593
- .level-icon .icon.active {
2594
- color: var(--ai-primary, #d97706);
2595
- }
2596
-
2597
- .level-icon .icon.completed {
2598
- color: #1a7f37;
2599
- }
2600
-
2601
- .level-icon .icon.error {
2602
- color: #cf222e;
2603
- }
2604
-
2605
- .level-icon .icon.cancelled {
2606
- color: #9e6a03;
2607
- }
2608
-
2609
- .level-content {
2610
- flex: 1;
2611
- min-width: 0;
2612
- }
2613
-
2614
- .level-title {
2615
- font-size: 14px;
2616
- font-weight: 600;
2617
- color: var(--color-text-primary, #24292f);
2618
- margin-bottom: 4px;
2619
- }
2620
-
2621
- .level-status {
2622
- font-size: 13px;
2623
- color: var(--color-text-secondary, #656d76);
2624
- margin-bottom: 6px;
2625
- }
2626
-
2627
- .level-stream-snippet {
2628
- font-family: ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, 'Liberation Mono', monospace;
2629
- font-size: 12px;
2630
- color: var(--color-text-tertiary, #8b949e);
2631
- background: var(--color-bg-subtle, #f6f8fa);
2632
- border-radius: 4px;
2633
- padding: 3px 6px;
2634
- margin-top: 4px;
2635
- max-width: 100%;
2636
- white-space: nowrap;
2637
- overflow: hidden;
2638
- text-overflow: ellipsis;
2639
- }
2640
-
2641
- .level-files {
2642
- display: flex;
2643
- gap: 16px;
2644
- font-size: 13px;
2645
- }
2646
-
2647
- .files-analyzed {
2648
- color: #1a7f37;
2649
- }
2650
-
2651
- .files-remaining {
2652
- color: #656d76;
2653
- }
2654
-
2655
2559
  .modal-footer {
2656
2560
  padding: 16px 24px 24px 24px;
2657
2561
  display: flex;
@@ -5069,6 +4973,12 @@ tr.line-range-start .d2h-code-line-ctn {
5069
4973
  border: 1px solid #bf8700;
5070
4974
  }
5071
4975
 
4976
+ .toast-info {
4977
+ background-color: #0969da;
4978
+ color: white;
4979
+ border: 1px solid #0550ae;
4980
+ }
4981
+
5072
4982
  .toast-content {
5073
4983
  display: flex;
5074
4984
  align-items: center;
@@ -5111,6 +5021,11 @@ tr.line-range-start .d2h-code-line-ctn {
5111
5021
  border-color: #d29922;
5112
5022
  }
5113
5023
 
5024
+ [data-theme="dark"] .toast-info {
5025
+ background-color: #1f6feb;
5026
+ border-color: #58a6ff;
5027
+ }
5028
+
5114
5029
  /* Loading spinner for buttons */
5115
5030
  .loading-spinner-small {
5116
5031
  width: 16px;
@@ -628,6 +628,7 @@ class AdvancedConfigTab {
628
628
 
629
629
  // Dirty state tracking via event delegation
630
630
  panel.addEventListener('change', (e) => {
631
+ if (e.target.id === 'council-selector') return; // council selector has its own clean/dirty logic
631
632
  if (e.target.matches('select, input[type="checkbox"]') || e.target.classList.contains('adv-timeout')) {
632
633
  this._markDirty();
633
634
  }
@@ -7,8 +7,7 @@
7
7
  * - Voice participants as child rows under each level
8
8
  * - Consolidation section at the bottom
9
9
  *
10
- * Replaces ProgressModal when a council analysis is running.
11
- * The existing ProgressModal remains for single-model analysis.
10
+ * Handles both council and single-model analysis modes.
12
11
  */
13
12
  class CouncilProgressModal {
14
13
  constructor() {
@@ -673,6 +672,17 @@ class CouncilProgressModal {
673
672
 
674
673
  // Derive the parent header state from children
675
674
  this._refreshConsolidationHeader(iconEl, statusEl, state);
675
+
676
+ // Show stream event text in the snippet element (mirrors _updateSingleModelLevel logic)
677
+ const snippetEl = section.querySelector('.council-level-snippet');
678
+ if (snippetEl) {
679
+ if (state === 'running' && level4Status.streamEvent?.text) {
680
+ snippetEl.textContent = level4Status.streamEvent.text;
681
+ snippetEl.style.display = 'block';
682
+ } else if (state !== 'running') {
683
+ snippetEl.style.display = 'none';
684
+ }
685
+ }
676
686
  }
677
687
 
678
688
  /**
@@ -1128,6 +1138,7 @@ class CouncilProgressModal {
1128
1138
  <span class="council-level-title">Cross-Reviewer Consolidation</span>
1129
1139
  <span class="council-level-status pending">Pending</span>
1130
1140
  </div>
1141
+ <div class="council-level-snippet" style="display: none;"></div>
1131
1142
  </div>
1132
1143
  `;
1133
1144
 
@@ -1188,6 +1199,7 @@ class CouncilProgressModal {
1188
1199
  <span class="council-level-title">Consolidation</span>
1189
1200
  <span class="council-level-status pending">Pending</span>
1190
1201
  </div>
1202
+ <div class="council-level-snippet" style="display: none;"></div>
1191
1203
  </div>
1192
1204
  `;
1193
1205
 
@@ -1327,6 +1339,7 @@ class CouncilProgressModal {
1327
1339
  <span class="council-level-title">Consolidation</span>
1328
1340
  <span class="council-level-status pending">Pending</span>
1329
1341
  </div>
1342
+ <div class="council-level-snippet" style="display: none;"></div>
1330
1343
  </div>
1331
1344
  `;
1332
1345
  }
@@ -1338,6 +1351,7 @@ class CouncilProgressModal {
1338
1351
  <span class="council-level-title">Consolidation</span>
1339
1352
  <span class="council-level-status pending">Pending</span>
1340
1353
  </div>
1354
+ <div class="council-level-snippet" style="display: none;"></div>
1341
1355
  <div class="council-level-children">
1342
1356
  `;
1343
1357
 
@@ -235,8 +235,8 @@ class StatusIndicator {
235
235
  * Reopen modal from status indicator
236
236
  */
237
237
  reopenModal() {
238
- if (this.currentAnalysisId && window.progressModal) {
239
- window.progressModal.reopenFromBackground();
238
+ if (this.currentAnalysisId && window.councilProgressModal) {
239
+ window.councilProgressModal.reopenFromBackground();
240
240
  }
241
241
  }
242
242
 
@@ -1,7 +1,7 @@
1
1
  // SPDX-License-Identifier: GPL-3.0-or-later
2
2
  /**
3
3
  * Toast Notification Component
4
- * Shows temporary success/error messages at the top of the page
4
+ * Shows temporary success/error/warning/info messages at the top of the page
5
5
  */
6
6
  class Toast {
7
7
  constructor() {
@@ -101,6 +101,27 @@ class Toast {
101
101
  this.showToast(toast, duration);
102
102
  }
103
103
 
104
+ /**
105
+ * Show an info toast
106
+ * @param {string} message - The message to display
107
+ * @param {number} duration - Duration in ms (default: 5000)
108
+ */
109
+ showInfo(message, duration = 5000) {
110
+ const toast = document.createElement('div');
111
+ toast.className = 'toast toast-info';
112
+
113
+ toast.innerHTML = `
114
+ <div class="toast-content">
115
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" class="toast-icon">
116
+ <path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"></path>
117
+ </svg>
118
+ <span class="toast-message">${message}</span>
119
+ </div>
120
+ `;
121
+
122
+ this.showToast(toast, duration);
123
+ }
124
+
104
125
  /**
105
126
  * Show a toast element
106
127
  * @param {HTMLElement} toast - The toast element
@@ -547,6 +547,7 @@ class VoiceCentricConfigTab {
547
547
 
548
548
  // Dirty state tracking
549
549
  panel.addEventListener('change', (e) => {
550
+ if (e.target.id === 'vc-council-selector') return; // council selector has its own clean/dirty logic
550
551
  if (e.target.matches('select, input[type="checkbox"]') || e.target.classList.contains('vc-timeout')) {
551
552
  this._markDirty();
552
553
  }
@@ -859,11 +860,11 @@ class VoiceCentricConfigTab {
859
860
  const orchCustomInstructions = orchInstrInput?.value?.trim() || undefined;
860
861
  const consolidation = orchRow ? {
861
862
  provider: orchRow.querySelector('.voice-provider')?.value || 'claude',
862
- model: orchRow.querySelector('.voice-model')?.value || 'sonnet',
863
+ model: orchRow.querySelector('.voice-model')?.value || 'sonnet-4.6',
863
864
  tier: orchRow.querySelector('.voice-tier')?.value || 'balanced',
864
865
  timeout: orchTimeout,
865
866
  ...(orchCustomInstructions ? { customInstructions: orchCustomInstructions } : {})
866
- } : { provider: 'claude', model: 'sonnet', tier: 'balanced', timeout: VoiceCentricConfigTab.DEFAULT_TIMEOUT };
867
+ } : { provider: 'claude', model: 'sonnet-4.6', tier: 'balanced', timeout: VoiceCentricConfigTab.DEFAULT_TIMEOUT };
867
868
 
868
869
  return { voices, levels, consolidation };
869
870
  }
@@ -308,7 +308,7 @@ class LocalManager {
308
308
  const STALE_TIMEOUT = 2000;
309
309
 
310
310
  if (manager.isAnalyzing) {
311
- manager.reopenProgressModal();
311
+ manager.reopenModal();
312
312
  return;
313
313
  }
314
314
 
@@ -450,18 +450,17 @@ class LocalManager {
450
450
  manager.setButtonAnalyzing(data.analysisId);
451
451
 
452
452
  // Show the appropriate progress modal
453
- if (data.status?.isCouncil && window.councilProgressModal && data.status?.councilConfig) {
453
+ if (window.councilProgressModal) {
454
454
  window.councilProgressModal.setLocalMode(reviewId);
455
455
  window.councilProgressModal.show(
456
456
  data.analysisId,
457
- data.status.councilConfig,
457
+ data.status?.isCouncil ? data.status.councilConfig : null,
458
458
  null,
459
- { configType: data.status.configType || 'advanced' }
459
+ {
460
+ configType: data.status?.isCouncil ? (data.status.configType || 'advanced') : 'single',
461
+ enabledLevels: data.status?.enabledLevels || [1, 2, 3]
462
+ }
460
463
  );
461
- } else if (window.progressModal) {
462
- // Update the SSE endpoint for progress modal
463
- self.patchProgressModalForLocal();
464
- window.progressModal.show(data.analysisId);
465
464
  }
466
465
  }
467
466
  } catch (error) {
@@ -1030,58 +1029,6 @@ class LocalManager {
1030
1029
  console.log('PRManager patched for local mode');
1031
1030
  }
1032
1031
 
1033
- /**
1034
- * Patch ProgressModal to use local SSE endpoint
1035
- */
1036
- patchProgressModalForLocal() {
1037
- const modal = window.progressModal;
1038
- if (!modal) return;
1039
-
1040
- const reviewId = this.reviewId;
1041
- const originalStartMonitoring = modal.startProgressMonitoring.bind(modal);
1042
-
1043
- modal.startProgressMonitoring = function() {
1044
- if (modal.eventSource) {
1045
- modal.eventSource.close();
1046
- }
1047
-
1048
- if (!modal.currentAnalysisId) return;
1049
-
1050
- // Use local SSE endpoint
1051
- modal.eventSource = new EventSource(`/api/local/${reviewId}/ai-suggestions/status`);
1052
-
1053
- modal.eventSource.onopen = () => {
1054
- console.log('Connected to local progress stream');
1055
- };
1056
-
1057
- modal.eventSource.onmessage = (event) => {
1058
- try {
1059
- const data = JSON.parse(event.data);
1060
-
1061
- if (data.type === 'connected') {
1062
- console.log('Local SSE connection established');
1063
- return;
1064
- }
1065
-
1066
- if (data.type === 'progress') {
1067
- modal.updateProgress(data);
1068
-
1069
- if (data.status === 'completed' || data.status === 'failed' || data.status === 'cancelled') {
1070
- modal.stopProgressMonitoring();
1071
- }
1072
- }
1073
- } catch (error) {
1074
- console.error('Error parsing SSE data:', error);
1075
- }
1076
- };
1077
-
1078
- modal.eventSource.onerror = (error) => {
1079
- console.error('SSE connection error:', error);
1080
- modal.fallbackToPolling();
1081
- };
1082
- };
1083
- }
1084
-
1085
1032
  /**
1086
1033
  * Start local AI analysis
1087
1034
  */
@@ -1158,12 +1105,6 @@ class LocalManager {
1158
1105
  enabledLevels: config.enabledLevels || [1, 2, 3]
1159
1106
  }
1160
1107
  );
1161
- } else {
1162
- // Fallback to old progress modal if unified modal not available
1163
- this.patchProgressModalForLocal();
1164
- if (window.progressModal) {
1165
- window.progressModal.show(result.analysisId);
1166
- }
1167
1108
  }
1168
1109
 
1169
1110
  } catch (error) {
@@ -1847,7 +1788,7 @@ class LocalManager {
1847
1788
 
1848
1789
  // Map levels to dot phases
1849
1790
  const phaseMap = {
1850
- 4: 'orchestration', // Orchestration/finalization is level 4 in ProgressModal
1791
+ 4: 'orchestration', // Orchestration/finalization is level 4 in progress modal
1851
1792
  1: 'level1',
1852
1793
  2: 'level2',
1853
1794
  3: 'level3'
@@ -848,8 +848,10 @@ class AnalysisHistoryManager {
848
848
  // Claude models
849
849
  'haiku': 'fast',
850
850
  'sonnet': 'balanced',
851
+ 'sonnet-4.5': 'balanced',
852
+ 'sonnet-4.6': 'balanced',
851
853
  'opus': 'thorough',
852
- 'opus-4.5': 'balanced',
854
+ 'opus-4.5': 'thorough',
853
855
  'opus-4.6-low': 'balanced',
854
856
  'opus-4.6-medium': 'balanced',
855
857
  'opus-4.6-1m': 'balanced',
package/public/js/pr.js CHANGED
@@ -3331,7 +3331,7 @@ class PRManager {
3331
3331
 
3332
3332
  // Map levels to dot phases
3333
3333
  const phaseMap = {
3334
- 4: 'orchestration', // Orchestration/finalization is level 4 in ProgressModal
3334
+ 4: 'orchestration', // Orchestration/finalization is level 4 in progress modal
3335
3335
  1: 'level1',
3336
3336
  2: 'level2',
3337
3337
  3: 'level3'
@@ -3399,18 +3399,17 @@ class PRManager {
3399
3399
  this.setButtonAnalyzing(data.analysisId);
3400
3400
 
3401
3401
  // Show the appropriate progress modal
3402
- if (data.status?.isCouncil && window.councilProgressModal && data.status?.councilConfig) {
3402
+ if (window.councilProgressModal) {
3403
3403
  window.councilProgressModal.setPRMode();
3404
3404
  window.councilProgressModal.show(
3405
3405
  data.analysisId,
3406
- data.status.councilConfig,
3406
+ data.status?.isCouncil ? data.status.councilConfig : null,
3407
3407
  null,
3408
- { configType: data.status.configType || 'advanced' }
3408
+ {
3409
+ configType: data.status?.isCouncil ? (data.status.configType || 'advanced') : 'single',
3410
+ enabledLevels: data.status?.enabledLevels || [1, 2, 3]
3411
+ }
3409
3412
  );
3410
- } else if (window.progressModal) {
3411
- window.progressModal.show(data.analysisId);
3412
- } else {
3413
- console.warn('Progress modal not yet initialized');
3414
3413
  }
3415
3414
  }
3416
3415
  } catch (error) {
@@ -3422,17 +3421,12 @@ class PRManager {
3422
3421
  /**
3423
3422
  * Reopen progress modal when button is clicked during analysis
3424
3423
  */
3425
- reopenProgressModal() {
3424
+ reopenModal() {
3426
3425
  if (!this.currentAnalysisId) return;
3427
3426
 
3428
- // If the council modal was used for this analysis, reopen it
3427
+ // Reopen the progress modal if it was tracking this analysis
3429
3428
  if (window.councilProgressModal && window.councilProgressModal.currentAnalysisId === this.currentAnalysisId) {
3430
3429
  window.councilProgressModal.reopenFromBackground();
3431
- return;
3432
- }
3433
-
3434
- if (window.progressModal) {
3435
- window.progressModal.show(this.currentAnalysisId);
3436
3430
  }
3437
3431
  }
3438
3432
 
@@ -3490,7 +3484,7 @@ class PRManager {
3490
3484
  async triggerAIAnalysis() {
3491
3485
  // If analysis is already running, just reopen the progress modal
3492
3486
  if (this.isAnalyzing) {
3493
- this.reopenProgressModal();
3487
+ this.reopenModal();
3494
3488
  return;
3495
3489
  }
3496
3490
 
@@ -3729,9 +3723,6 @@ class PRManager {
3729
3723
  enabledLevels: config.enabledLevels || [1, 2, 3]
3730
3724
  }
3731
3725
  );
3732
- } else if (window.progressModal) {
3733
- // Fallback to old progress modal if unified modal not available
3734
- window.progressModal.show(result.analysisId);
3735
3726
  }
3736
3727
 
3737
3728
  } catch (error) {
package/public/local.html CHANGED
@@ -486,7 +486,6 @@
486
486
  <script src="/js/components/Toast.js"></script>
487
487
  <script src="/js/components/ConfirmDialog.js"></script>
488
488
  <script src="/js/components/TextInputDialog.js"></script>
489
- <script src="/js/components/ProgressModal.js"></script>
490
489
  <script src="/js/components/AnalysisConfigModal.js"></script>
491
490
  <script src="/js/components/TimeoutSelect.js"></script>
492
491
  <script src="/js/components/VoiceCentricConfigTab.js"></script>
package/public/pr.html CHANGED
@@ -309,7 +309,6 @@
309
309
  <script src="/js/components/Toast.js"></script>
310
310
  <script src="/js/components/ConfirmDialog.js"></script>
311
311
  <script src="/js/components/TextInputDialog.js"></script>
312
- <script src="/js/components/ProgressModal.js"></script>
313
312
  <script src="/js/components/AnalysisConfigModal.js"></script>
314
313
  <script src="/js/components/TimeoutSelect.js"></script>
315
314
  <script src="/js/components/VoiceCentricConfigTab.js"></script>
@@ -3561,8 +3561,8 @@ File-level suggestions should NOT have a line number. They apply to the entire f
3561
3561
  }
3562
3562
  }
3563
3563
 
3564
- // Fallback to claude/sonnet
3565
- return { provider: 'claude', model: 'sonnet', tier: 'balanced' };
3564
+ // Fallback to claude/sonnet-4.6
3565
+ return { provider: 'claude', model: 'sonnet-4.6', tier: 'balanced' };
3566
3566
  }
3567
3567
 
3568
3568
  /**
@@ -3581,7 +3581,7 @@ File-level suggestions should NOT have a line number. They apply to the entire f
3581
3581
  tier: voices[0].tier || 'balanced'
3582
3582
  };
3583
3583
  }
3584
- return { provider: 'claude', model: 'sonnet', tier: 'balanced' };
3584
+ return { provider: 'claude', model: 'sonnet-4.6', tier: 'balanced' };
3585
3585
  }
3586
3586
 
3587
3587
  /**
@@ -30,24 +30,25 @@ const CLAUDE_MODELS = [
30
30
  badgeClass: 'badge-speed'
31
31
  },
32
32
  {
33
- id: 'sonnet',
33
+ id: 'sonnet-4.5',
34
+ cli_model: 'claude-sonnet-4.5',
34
35
  name: 'Sonnet 4.5',
35
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
+ {
43
+ id: 'sonnet-4.6',
44
+ cli_model: 'claude-sonnet-4-6',
45
+ name: 'Sonnet 4.6',
46
+ tier: 'balanced',
36
47
  tagline: 'Best Balance',
37
48
  description: 'Recommended for most reviews',
38
49
  badge: 'Standard',
39
50
  badgeClass: 'badge-recommended'
40
51
  },
41
- {
42
- id: 'opus-4.5',
43
- cli_model: 'claude-opus-4-5-20251101',
44
- name: 'Opus 4.5',
45
- tier: 'balanced',
46
- tagline: 'Deep Thinker',
47
- description: 'Extended thinking for complex analysis',
48
- badge: 'Previous Gen',
49
- badgeClass: 'badge-power'
50
- },
51
52
  {
52
53
  id: 'opus-4.6-low',
53
54
  cli_model: 'opus',
@@ -91,6 +92,16 @@ const CLAUDE_MODELS = [
91
92
  description: 'Opus 4.6 high effort with 1M token context window',
92
93
  badge: 'More Context',
93
94
  badgeClass: 'badge-power'
95
+ },
96
+ {
97
+ id: 'opus-4.5',
98
+ cli_model: 'claude-opus-4-5-20251101',
99
+ name: 'Opus 4.5',
100
+ tier: 'thorough',
101
+ tagline: 'Deep Thinker',
102
+ description: 'Extended thinking for complex analysis',
103
+ badge: 'Previous Gen',
104
+ badgeClass: 'badge-power'
94
105
  }
95
106
  ];
96
107
 
@@ -3,7 +3,7 @@
3
3
  * Stream Parser - Side-channel parser for real-time AI streaming events
4
4
  *
5
5
  * Reads stdout data incrementally from provider processes and emits normalized
6
- * events for display in the ProgressModal. This is a read-only side channel;
6
+ * events for display in the progress modal. This is a read-only side channel;
7
7
  * the existing stdout buffering and final JSON extraction remain untouched.
8
8
  *
9
9
  * Normalized event shape:
@@ -379,7 +379,7 @@ function createProgressCallback(analysisId) {
379
379
  currentStatus.levels[4] = {
380
380
  status: derivedStatus,
381
381
  progress: progressUpdate.progress || (consolidationMatch ? 'Consolidating...' : 'Finalizing results...'),
382
- streamEvent: undefined,
382
+ streamEvent: existing.streamEvent,
383
383
  consolidationStep: step,
384
384
  steps,
385
385
  voices: existingVoices
@@ -1,705 +0,0 @@
1
- // SPDX-License-Identifier: GPL-3.0-or-later
2
- /**
3
- * AI Analysis Progress Modal Component
4
- * Displays three-level progress structure and handles background execution
5
- */
6
- class ProgressModal {
7
- constructor() {
8
- this.modal = null;
9
- this.isVisible = false;
10
- this.currentAnalysisId = null;
11
- this.eventSource = null;
12
- this.statusCheckInterval = null;
13
- this.isRunningInBackground = false;
14
-
15
- this.createModal();
16
- this.setupEventListeners();
17
- }
18
-
19
- /**
20
- * Create the modal DOM structure
21
- */
22
- createModal() {
23
- // Remove existing modal if it exists
24
- const existing = document.getElementById('progress-modal');
25
- if (existing) {
26
- existing.remove();
27
- }
28
-
29
- // Create modal container
30
- const modalContainer = document.createElement('div');
31
- modalContainer.id = 'progress-modal';
32
- modalContainer.className = 'modal-overlay';
33
- modalContainer.style.display = 'none';
34
-
35
- modalContainer.innerHTML = `
36
- <div class="modal-backdrop" onclick="progressModal.hide()"></div>
37
- <div class="modal-container">
38
- <div class="modal-header">
39
- <h3>AI Review Analysis</h3>
40
- <button class="modal-close-btn" onclick="progressModal.hide()" title="Close">
41
- <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
42
- <path d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"/>
43
- </svg>
44
- </button>
45
- </div>
46
-
47
- <div class="modal-body">
48
- <div class="progress-levels">
49
- <div class="progress-level" id="level-1">
50
- <div class="level-icon">
51
- <span class="icon pending">○</span>
52
- </div>
53
- <div class="level-content">
54
- <div class="level-title">Level 1: Analyzing diff</div>
55
- <div class="level-status">Preparing to start...</div>
56
- <div class="progress-bar-container" style="display: none;">
57
- <div class="barbershop-progress-bar">
58
- <div class="barbershop-stripes"></div>
59
- </div>
60
- </div>
61
- <div class="level-stream-snippet" style="display: none;"></div>
62
- </div>
63
- </div>
64
-
65
- <div class="progress-level" id="level-2">
66
- <div class="level-icon">
67
- <span class="icon pending">○</span>
68
- </div>
69
- <div class="level-content">
70
- <div class="level-title">Level 2: File context</div>
71
- <div class="level-status">Pending</div>
72
- <div class="progress-bar-container" style="display: none;">
73
- <div class="barbershop-progress-bar">
74
- <div class="barbershop-stripes"></div>
75
- </div>
76
- </div>
77
- <div class="level-stream-snippet" style="display: none;"></div>
78
- </div>
79
- </div>
80
-
81
- <div class="progress-level" id="level-3">
82
- <div class="level-icon">
83
- <span class="icon pending">○</span>
84
- </div>
85
- <div class="level-content">
86
- <div class="level-title">Level 3: Codebase context</div>
87
- <div class="level-status">Pending</div>
88
- <div class="progress-bar-container" style="display: none;">
89
- <div class="barbershop-progress-bar">
90
- <div class="barbershop-stripes"></div>
91
- </div>
92
- </div>
93
- <div class="level-stream-snippet" style="display: none;"></div>
94
- </div>
95
- </div>
96
-
97
- <div class="progress-level" id="level-4">
98
- <div class="level-icon">
99
- <span class="icon pending">○</span>
100
- </div>
101
- <div class="level-content">
102
- <div class="level-title">Finalizing Results</div>
103
- <div class="level-status">Pending</div>
104
- <div class="progress-bar-container" style="display: none;">
105
- <div class="barbershop-progress-bar">
106
- <div class="barbershop-stripes"></div>
107
- </div>
108
- </div>
109
- <div class="level-stream-snippet" style="display: none;"></div>
110
- </div>
111
- </div>
112
- </div>
113
- </div>
114
-
115
- <div class="modal-footer">
116
- <button class="btn btn-secondary" id="run-background-btn" onclick="progressModal.runInBackground()">
117
- Run in Background
118
- </button>
119
- <button class="btn btn-danger" id="cancel-btn" onclick="progressModal.cancel()">
120
- Cancel
121
- </button>
122
- </div>
123
- </div>
124
- `;
125
-
126
- document.body.appendChild(modalContainer);
127
- this.modal = modalContainer;
128
- }
129
-
130
- /**
131
- * Setup event listeners
132
- */
133
- setupEventListeners() {
134
- // Close modal on Escape key
135
- document.addEventListener('keydown', (e) => {
136
- if (e.key === 'Escape' && this.isVisible) {
137
- this.hide();
138
- }
139
- });
140
- }
141
-
142
- /**
143
- * Show the modal
144
- * @param {string} analysisId - Analysis ID to track
145
- */
146
- show(analysisId) {
147
- this.currentAnalysisId = analysisId;
148
- this.isVisible = true;
149
- this.modal.style.display = 'flex';
150
-
151
- // Reset progress state
152
- this.resetProgress();
153
-
154
- // Start monitoring progress
155
- this.startProgressMonitoring();
156
- }
157
-
158
- /**
159
- * Hide the modal
160
- */
161
- hide() {
162
- this.isVisible = false;
163
- this.modal.style.display = 'none';
164
-
165
- // Don't stop monitoring if running in background
166
- if (!this.isRunningInBackground) {
167
- this.stopProgressMonitoring();
168
- }
169
- }
170
-
171
- /**
172
- * Run analysis in background
173
- */
174
- runInBackground() {
175
- this.isRunningInBackground = true;
176
- this.hide();
177
-
178
- // Button already shows analyzing state, no need for separate status indicator
179
- // The button was set to analyzing state when analysis started
180
- }
181
-
182
- /**
183
- * Cancel the analysis
184
- */
185
- async cancel() {
186
- if (!this.currentAnalysisId) {
187
- this.hide();
188
- return;
189
- }
190
-
191
- try {
192
- // Make cancel request to backend
193
- const response = await fetch(`/api/analyze/cancel/${this.currentAnalysisId}`, {
194
- method: 'POST'
195
- });
196
-
197
- if (response.ok) {
198
- this.updateStatus('Analysis cancelled');
199
- }
200
- } catch (error) {
201
- console.warn('Cancel not available on server:', error.message);
202
- }
203
-
204
- this.stopProgressMonitoring();
205
- this.hide();
206
-
207
- // Reset button
208
- if (window.prManager) {
209
- window.prManager.resetButton();
210
- }
211
-
212
- // Reset AI panel to non-loading state
213
- if (window.aiPanel?.setAnalysisState) {
214
- window.aiPanel.setAnalysisState('unknown');
215
- }
216
- }
217
-
218
- /**
219
- * Reset progress to initial state
220
- */
221
- resetProgress() {
222
- // Reset levels 1-3 to running state
223
- for (let i = 1; i <= 3; i++) {
224
- const level = document.getElementById(`level-${i}`);
225
- if (level) {
226
- const icon = level.querySelector('.icon');
227
- const status = level.querySelector('.level-status');
228
- const progressContainer = level.querySelector('.progress-bar-container');
229
- const snippetEl = level.querySelector('.level-stream-snippet');
230
-
231
- icon.className = 'icon active';
232
- icon.textContent = '▶';
233
- status.textContent = 'Starting...';
234
- status.style.display = 'none';
235
-
236
- // Show progress bar immediately for levels 1-3
237
- if (progressContainer) {
238
- progressContainer.style.display = 'block';
239
- }
240
-
241
- // Clear stream snippet
242
- if (snippetEl) {
243
- snippetEl.style.display = 'none';
244
- snippetEl.textContent = '';
245
- }
246
- }
247
- }
248
-
249
- // Level 4 (orchestration) starts as pending
250
- const level4 = document.getElementById('level-4');
251
- if (level4) {
252
- const icon = level4.querySelector('.icon');
253
- const status = level4.querySelector('.level-status');
254
- const progressContainer = level4.querySelector('.progress-bar-container');
255
- const snippetEl = level4.querySelector('.level-stream-snippet');
256
-
257
- icon.className = 'icon pending';
258
- icon.textContent = '○';
259
- status.textContent = 'Pending';
260
- status.style.display = 'block';
261
-
262
- if (progressContainer) {
263
- progressContainer.style.display = 'none';
264
- }
265
-
266
- if (snippetEl) {
267
- snippetEl.style.display = 'none';
268
- snippetEl.textContent = '';
269
- }
270
- }
271
-
272
- // Reset footer buttons to initial state
273
- const runBackgroundBtn = document.getElementById('run-background-btn');
274
- const cancelBtn = document.getElementById('cancel-btn');
275
-
276
- if (runBackgroundBtn) {
277
- runBackgroundBtn.textContent = 'Run in Background';
278
- runBackgroundBtn.disabled = false;
279
- }
280
- if (cancelBtn) {
281
- cancelBtn.textContent = 'Cancel';
282
- }
283
-
284
- // Reset background running state
285
- this.isRunningInBackground = false;
286
- }
287
-
288
- /**
289
- * Start monitoring progress via Server-Sent Events (SSE)
290
- */
291
- startProgressMonitoring() {
292
- if (this.eventSource) {
293
- this.eventSource.close();
294
- }
295
-
296
- if (!this.currentAnalysisId) return;
297
-
298
- // Connect to SSE endpoint
299
- this.eventSource = new EventSource(`/api/pr/${this.currentAnalysisId}/ai-suggestions/status`);
300
-
301
- this.eventSource.onopen = () => {
302
- console.log('Connected to progress stream');
303
- };
304
-
305
- this.eventSource.onmessage = (event) => {
306
- try {
307
- const data = JSON.parse(event.data);
308
-
309
- if (data.type === 'connected') {
310
- console.log('SSE connection established');
311
- return;
312
- }
313
-
314
- if (data.type === 'progress') {
315
- this.updateProgress(data);
316
-
317
- // Stop monitoring if analysis is complete, failed, or cancelled
318
- if (data.status === 'completed' || data.status === 'failed' || data.status === 'cancelled') {
319
- this.stopProgressMonitoring();
320
- }
321
- }
322
- } catch (error) {
323
- console.error('Error parsing SSE data:', error);
324
- }
325
- };
326
-
327
- this.eventSource.onerror = (error) => {
328
- console.error('SSE connection error:', error);
329
- // Fallback to polling if SSE fails
330
- this.fallbackToPolling();
331
- };
332
- }
333
-
334
- /**
335
- * Fallback to polling if SSE fails
336
- */
337
- fallbackToPolling() {
338
- if (this.eventSource) {
339
- this.eventSource.close();
340
- this.eventSource = null;
341
- }
342
-
343
- if (this.statusCheckInterval) {
344
- clearInterval(this.statusCheckInterval);
345
- }
346
-
347
- this.statusCheckInterval = setInterval(async () => {
348
- if (!this.currentAnalysisId) return;
349
-
350
- try {
351
- const response = await fetch(`/api/analyze/status/${this.currentAnalysisId}`);
352
- if (!response.ok) {
353
- throw new Error('Failed to fetch status');
354
- }
355
-
356
- const status = await response.json();
357
- this.updateProgress(status);
358
-
359
- // Stop monitoring if analysis is complete, failed, or cancelled
360
- if (status.status === 'completed' || status.status === 'failed' || status.status === 'cancelled') {
361
- this.stopProgressMonitoring();
362
- }
363
- } catch (error) {
364
- console.error('Error checking analysis status:', error);
365
- }
366
- }, 1000); // Check every second
367
- }
368
-
369
- /**
370
- * Stop progress monitoring
371
- */
372
- stopProgressMonitoring() {
373
- if (this.statusCheckInterval) {
374
- clearInterval(this.statusCheckInterval);
375
- this.statusCheckInterval = null;
376
- }
377
-
378
- if (this.eventSource) {
379
- this.eventSource.close();
380
- this.eventSource = null;
381
- }
382
- }
383
-
384
- /**
385
- * Update progress based on status
386
- * @param {Object} status - Status object from server
387
- */
388
- updateProgress(status) {
389
- // Validate status structure before accessing properties
390
- if (!status.levels || typeof status.levels !== 'object') {
391
- console.warn('Invalid status structure - missing or malformed levels object:', status);
392
- return;
393
- }
394
-
395
- // Update each level's progress independently from the levels object
396
- for (let level = 1; level <= 4; level++) {
397
- const levelStatus = status.levels[level];
398
- if (levelStatus) {
399
- this.updateLevelProgress(level, levelStatus);
400
- }
401
- }
402
-
403
- // Update overall progress message
404
- this.updateStatus(status.progress || 'Running...');
405
-
406
- // Handle completion, failure, or cancellation
407
- if (status.status === 'completed') {
408
- this.handleCompletion(status);
409
- } else if (status.status === 'failed') {
410
- this.handleFailure(status);
411
- } else if (status.status === 'cancelled') {
412
- this.handleCancellation(status);
413
- }
414
- }
415
-
416
- /**
417
- * Mark a level as completed
418
- * @param {number} level - Level number to mark as completed
419
- */
420
- markLevelAsCompleted(level) {
421
- const levelElement = document.getElementById(`level-${level}`);
422
- if (!levelElement) return;
423
-
424
- const icon = levelElement.querySelector('.icon');
425
- const statusText = levelElement.querySelector('.level-status');
426
- const progressContainer = levelElement.querySelector('.progress-bar-container');
427
-
428
- icon.className = 'icon completed';
429
- icon.textContent = '✓';
430
- statusText.textContent = 'Completed';
431
- statusText.style.display = 'block';
432
-
433
- // Hide progress bar for completed levels
434
- if (progressContainer) {
435
- progressContainer.style.display = 'none';
436
- }
437
- }
438
-
439
- /**
440
- * Update a specific level's progress
441
- * @param {number} level - Level number (1, 2, or 3)
442
- * @param {Object} levelStatus - Level status object with { status, progress }
443
- */
444
- updateLevelProgress(level, levelStatus) {
445
- const levelElement = document.getElementById(`level-${level}`);
446
- if (!levelElement) return;
447
-
448
- const icon = levelElement.querySelector('.icon');
449
- const statusText = levelElement.querySelector('.level-status');
450
- const progressContainer = levelElement.querySelector('.progress-bar-container');
451
- const snippetEl = levelElement.querySelector('.level-stream-snippet');
452
-
453
- // Default: clear snippet. Only the running+streamEvent branch re-shows it.
454
- if (snippetEl) {
455
- snippetEl.style.display = 'none';
456
- snippetEl.textContent = '';
457
- }
458
-
459
- // Update icon and status based on current state
460
- if (levelStatus.status === 'running') {
461
- icon.className = 'icon active';
462
- icon.textContent = '▶';
463
-
464
- // Show progress bar and hide status text for running levels
465
- statusText.style.display = 'none';
466
- if (progressContainer) {
467
- progressContainer.style.display = 'block';
468
- }
469
-
470
- // Show stream snippet if present
471
- if (snippetEl && levelStatus.streamEvent) {
472
- snippetEl.textContent = levelStatus.streamEvent.text;
473
- snippetEl.style.display = 'block';
474
- }
475
-
476
- } else if (levelStatus.status === 'completed') {
477
- icon.className = 'icon completed';
478
- icon.textContent = '✓';
479
- statusText.textContent = 'Completed';
480
- statusText.style.display = 'block';
481
-
482
- if (progressContainer) {
483
- progressContainer.style.display = 'none';
484
- }
485
-
486
- } else if (levelStatus.status === 'failed') {
487
- icon.className = 'icon error';
488
- icon.textContent = '❌';
489
- statusText.textContent = 'Failed';
490
- statusText.style.display = 'block';
491
-
492
- if (progressContainer) {
493
- progressContainer.style.display = 'none';
494
- }
495
-
496
- } else if (levelStatus.status === 'cancelled') {
497
- icon.className = 'icon cancelled';
498
- icon.textContent = '⊘';
499
- statusText.textContent = 'Cancelled';
500
- statusText.style.display = 'block';
501
-
502
- if (progressContainer) {
503
- progressContainer.style.display = 'none';
504
- }
505
-
506
- } else {
507
- // For pending or other states
508
- console.warn('Unexpected level status:', levelStatus.status, 'for level', level);
509
- icon.className = 'icon pending';
510
- icon.textContent = '○';
511
- statusText.textContent = levelStatus.progress || 'Pending';
512
- statusText.style.display = 'block';
513
-
514
- if (progressContainer) {
515
- progressContainer.style.display = 'none';
516
- }
517
- }
518
-
519
- // Update toolbar progress dots (check both PR and local managers)
520
- const manager = window.prManager || window.localManager;
521
- if (manager?.updateProgressDot) {
522
- manager.updateProgressDot(level, levelStatus.status);
523
- }
524
- }
525
-
526
- /**
527
- * Update general status message
528
- * @param {string} message - Status message
529
- */
530
- updateStatus(message) {
531
- // Could add a general status area if needed
532
- console.log('Progress:', message);
533
- }
534
-
535
- /**
536
- * Handle analysis completion
537
- * @param {Object} status - Final status object
538
- */
539
- handleCompletion(status) {
540
- // Levels are already marked as completed by updateProgress
541
- // Just update the UI buttons
542
-
543
- const completedLevel = status.completedLevel || status.level || 3;
544
-
545
- // Update button to show completion
546
- const runBackgroundBtn = document.getElementById('run-background-btn');
547
- const cancelBtn = document.getElementById('cancel-btn');
548
-
549
- if (runBackgroundBtn) {
550
- runBackgroundBtn.textContent = `Analysis Complete`;
551
- runBackgroundBtn.disabled = true;
552
- }
553
- if (cancelBtn) {
554
- cancelBtn.textContent = 'Close';
555
- }
556
-
557
- // Update button to show completion
558
- if (window.prManager) {
559
- window.prManager.setButtonComplete();
560
- }
561
-
562
- // CRITICAL FIX: Automatically reload AI suggestions when analysis completes
563
- console.log('Analysis completed, reloading AI suggestions...');
564
-
565
- // Support both PR mode (prManager) and Local mode (localManager)
566
- const manager = window.prManager || window.localManager;
567
-
568
- if (manager && typeof manager.loadAISuggestions === 'function') {
569
- // Determine whether to switch to the new run:
570
- // - If modal is visible, user was waiting for results -> switch immediately
571
- // - If modal is hidden (running in background), user was viewing older results -> don't switch
572
- const shouldSwitchToNew = this.isVisible;
573
-
574
- // First, refresh the analysis history manager to include the new run
575
- const refreshHistory = async () => {
576
- if (manager.analysisHistoryManager) {
577
- console.log('Refreshing analysis history, switchToNew:', shouldSwitchToNew);
578
- const result = await manager.analysisHistoryManager.refresh({ switchToNew: shouldSwitchToNew });
579
- // Return whether the manager actually switched to the new run
580
- // This can differ from shouldSwitchToNew when it's the first-ever run
581
- // (first run always switches regardless of shouldSwitchToNew)
582
- return result.didSwitch;
583
- }
584
- // No history manager, so we'll load suggestions directly
585
- return true;
586
- };
587
-
588
- refreshHistory()
589
- .then((didSwitch) => {
590
- // Load suggestions if we switched to the new run
591
- // Note: didSwitch may be true even if shouldSwitchToNew was false
592
- // (e.g., first-ever analysis run always switches because there's no previous selection)
593
- if (didSwitch) {
594
- return manager.loadAISuggestions();
595
- }
596
- // Otherwise, just return - the user will load when they select the new run
597
- console.log('New analysis available - user will see indicator on dropdown');
598
- return Promise.resolve();
599
- })
600
- .then(() => {
601
- console.log('AI suggestions reloaded successfully');
602
- // Only auto-close after suggestions have loaded successfully
603
- if (this.isVisible) {
604
- setTimeout(() => {
605
- this.hide();
606
- }, 2000); // Reduced to 2 seconds since loading is complete
607
- }
608
- })
609
- .catch(error => {
610
- console.error('Error reloading AI suggestions:', error);
611
- // Still auto-close even if loading failed, but give more time for user to see error
612
- if (this.isVisible) {
613
- setTimeout(() => {
614
- this.hide();
615
- }, 5000);
616
- }
617
- });
618
- } else {
619
- console.warn('Manager not available for automatic suggestion reload');
620
- // Auto-close after 3 seconds if no manager available
621
- if (this.isVisible) {
622
- setTimeout(() => {
623
- this.hide();
624
- }, 3000);
625
- }
626
- }
627
- }
628
-
629
- /**
630
- * Handle analysis failure
631
- * @param {Object} status - Error status object
632
- */
633
- handleFailure(status) {
634
- // Levels are already marked as failed by updateProgress
635
- // Just update the UI buttons
636
-
637
- // Update buttons
638
- const runBackgroundBtn = document.getElementById('run-background-btn');
639
- const cancelBtn = document.getElementById('cancel-btn');
640
-
641
- if (runBackgroundBtn) {
642
- runBackgroundBtn.textContent = 'Analysis Failed';
643
- runBackgroundBtn.disabled = true;
644
- }
645
- if (cancelBtn) {
646
- cancelBtn.textContent = 'Close';
647
- }
648
-
649
- // Reset button on failure
650
- if (window.prManager) {
651
- window.prManager.resetButton();
652
- }
653
- }
654
-
655
- /**
656
- * Handle analysis cancellation (via SSE status)
657
- * @param {Object} status - Cancellation status object
658
- */
659
- handleCancellation(status) {
660
- // Update buttons to show cancelled state
661
- const runBackgroundBtn = document.getElementById('run-background-btn');
662
- const cancelBtn = document.getElementById('cancel-btn');
663
-
664
- if (runBackgroundBtn) {
665
- runBackgroundBtn.textContent = 'Analysis Cancelled';
666
- runBackgroundBtn.disabled = true;
667
- }
668
- if (cancelBtn) {
669
- cancelBtn.textContent = 'Close';
670
- }
671
-
672
- // Reset the analyze button and AI panel state
673
- if (window.prManager) {
674
- window.prManager.resetButton();
675
- }
676
-
677
- // Reset AI panel to non-loading state
678
- if (window.aiPanel?.setAnalysisState) {
679
- window.aiPanel.setAnalysisState('unknown');
680
- }
681
-
682
- // Hide modal after a brief delay
683
- if (this.isVisible) {
684
- setTimeout(() => {
685
- this.hide();
686
- }, 1500);
687
- }
688
- }
689
-
690
- /**
691
- * Reopen modal from background
692
- */
693
- reopenFromBackground() {
694
- this.isRunningInBackground = false;
695
- this.show(this.currentAnalysisId);
696
-
697
- // Hide status indicator
698
- if (window.statusIndicator) {
699
- window.statusIndicator.hide();
700
- }
701
- }
702
- }
703
-
704
- // Initialize global instance
705
- window.progressModal = new ProgressModal();