@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 +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 +12 -97
- package/public/js/components/AdvancedConfigTab.js +1 -0
- package/public/js/components/CouncilProgressModal.js +16 -2
- package/public/js/components/StatusIndicator.js +2 -2
- package/public/js/components/Toast.js +22 -1
- package/public/js/components/VoiceCentricConfigTab.js +3 -2
- package/public/js/local.js +8 -67
- package/public/js/modules/analysis-history.js +3 -1
- package/public/js/pr.js +10 -19
- package/public/local.html +0 -1
- package/public/pr.html +0 -1
- package/src/ai/analyzer.js +3 -3
- package/src/ai/claude-provider.js +22 -11
- package/src/ai/stream-parser.js +1 -1
- package/src/routes/shared.js +1 -1
- package/public/js/components/ProgressModal.js +0 -705
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pair-review",
|
|
3
|
-
"version": "1.6.
|
|
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.
|
|
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
|
-
/*
|
|
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
|
-
*
|
|
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.
|
|
239
|
-
window.
|
|
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
|
}
|
package/public/js/local.js
CHANGED
|
@@ -308,7 +308,7 @@ class LocalManager {
|
|
|
308
308
|
const STALE_TIMEOUT = 2000;
|
|
309
309
|
|
|
310
310
|
if (manager.isAnalyzing) {
|
|
311
|
-
manager.
|
|
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 (
|
|
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
|
-
{
|
|
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
|
|
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': '
|
|
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
|
|
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 (
|
|
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
|
-
{
|
|
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
|
-
|
|
3424
|
+
reopenModal() {
|
|
3426
3425
|
if (!this.currentAnalysisId) return;
|
|
3427
3426
|
|
|
3428
|
-
//
|
|
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.
|
|
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>
|
package/src/ai/analyzer.js
CHANGED
|
@@ -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
|
|
package/src/ai/stream-parser.js
CHANGED
|
@@ -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
|
|
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:
|
package/src/routes/shared.js
CHANGED
|
@@ -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:
|
|
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();
|