@in-the-loop-labs/pair-review 1.0.0

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.
Files changed (91) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +371 -0
  3. package/bin/git-diff-lines +146 -0
  4. package/bin/pair-review.js +49 -0
  5. package/package.json +71 -0
  6. package/public/css/ai-summary-modal.css +183 -0
  7. package/public/css/pr.css +8698 -0
  8. package/public/css/repo-settings.css +891 -0
  9. package/public/css/styles.css +479 -0
  10. package/public/favicon.png +0 -0
  11. package/public/index.html +1104 -0
  12. package/public/js/components/AIPanel.js +1639 -0
  13. package/public/js/components/AISummaryModal.js +278 -0
  14. package/public/js/components/AnalysisConfigModal.js +684 -0
  15. package/public/js/components/ConfirmDialog.js +227 -0
  16. package/public/js/components/PreviewModal.js +344 -0
  17. package/public/js/components/ProgressModal.js +678 -0
  18. package/public/js/components/ReviewModal.js +531 -0
  19. package/public/js/components/SplitButton.js +382 -0
  20. package/public/js/components/StatusIndicator.js +265 -0
  21. package/public/js/components/SuggestionNavigator.js +489 -0
  22. package/public/js/components/Toast.js +166 -0
  23. package/public/js/local.js +1580 -0
  24. package/public/js/modules/analysis-history.js +940 -0
  25. package/public/js/modules/comment-manager.js +643 -0
  26. package/public/js/modules/diff-renderer.js +585 -0
  27. package/public/js/modules/file-comment-manager.js +1242 -0
  28. package/public/js/modules/gap-coordinates.js +190 -0
  29. package/public/js/modules/hunk-parser.js +358 -0
  30. package/public/js/modules/line-tracker.js +386 -0
  31. package/public/js/modules/panel-resizer.js +228 -0
  32. package/public/js/modules/storage-cleanup.js +36 -0
  33. package/public/js/modules/suggestion-manager.js +692 -0
  34. package/public/js/pr.js +3503 -0
  35. package/public/js/repo-settings.js +691 -0
  36. package/public/js/utils/file-order.js +87 -0
  37. package/public/js/utils/markdown.js +97 -0
  38. package/public/js/utils/suggestion-ui.js +55 -0
  39. package/public/js/utils/tier-icons.js +25 -0
  40. package/public/local.html +460 -0
  41. package/public/pr.html +329 -0
  42. package/public/repo-settings.html +243 -0
  43. package/src/ai/analyzer.js +2592 -0
  44. package/src/ai/claude-cli.js +153 -0
  45. package/src/ai/claude-provider.js +261 -0
  46. package/src/ai/codex-provider.js +361 -0
  47. package/src/ai/copilot-provider.js +345 -0
  48. package/src/ai/gemini-provider.js +375 -0
  49. package/src/ai/index.js +47 -0
  50. package/src/ai/prompts/baseline/_meta.json +14 -0
  51. package/src/ai/prompts/baseline/level1/balanced.js +239 -0
  52. package/src/ai/prompts/baseline/level1/fast.js +194 -0
  53. package/src/ai/prompts/baseline/level1/thorough.js +319 -0
  54. package/src/ai/prompts/baseline/level2/balanced.js +248 -0
  55. package/src/ai/prompts/baseline/level2/fast.js +201 -0
  56. package/src/ai/prompts/baseline/level2/thorough.js +367 -0
  57. package/src/ai/prompts/baseline/level3/balanced.js +280 -0
  58. package/src/ai/prompts/baseline/level3/fast.js +220 -0
  59. package/src/ai/prompts/baseline/level3/thorough.js +459 -0
  60. package/src/ai/prompts/baseline/orchestration/balanced.js +259 -0
  61. package/src/ai/prompts/baseline/orchestration/fast.js +213 -0
  62. package/src/ai/prompts/baseline/orchestration/thorough.js +446 -0
  63. package/src/ai/prompts/config.js +52 -0
  64. package/src/ai/prompts/index.js +267 -0
  65. package/src/ai/prompts/shared/diff-instructions.js +50 -0
  66. package/src/ai/prompts/shared/output-schema.js +179 -0
  67. package/src/ai/prompts/shared/valid-files.js +37 -0
  68. package/src/ai/provider.js +260 -0
  69. package/src/config.js +139 -0
  70. package/src/database.js +2284 -0
  71. package/src/git/gitattributes.js +207 -0
  72. package/src/git/worktree.js +688 -0
  73. package/src/github/client.js +893 -0
  74. package/src/github/parser.js +247 -0
  75. package/src/local-review.js +691 -0
  76. package/src/main.js +987 -0
  77. package/src/routes/analysis.js +897 -0
  78. package/src/routes/comments.js +534 -0
  79. package/src/routes/config.js +250 -0
  80. package/src/routes/local.js +1728 -0
  81. package/src/routes/pr.js +1164 -0
  82. package/src/routes/shared.js +218 -0
  83. package/src/routes/worktrees.js +500 -0
  84. package/src/server.js +295 -0
  85. package/src/utils/diff-annotator.js +414 -0
  86. package/src/utils/instructions.js +33 -0
  87. package/src/utils/json-extractor.js +107 -0
  88. package/src/utils/line-validation.js +183 -0
  89. package/src/utils/logger.js +142 -0
  90. package/src/utils/paths.js +161 -0
  91. package/src/utils/stats-calculator.js +86 -0
@@ -0,0 +1,678 @@
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>
62
+ </div>
63
+
64
+ <div class="progress-level" id="level-2">
65
+ <div class="level-icon">
66
+ <span class="icon pending">○</span>
67
+ </div>
68
+ <div class="level-content">
69
+ <div class="level-title">Level 2: File context</div>
70
+ <div class="level-status">Pending</div>
71
+ <div class="progress-bar-container" style="display: none;">
72
+ <div class="barbershop-progress-bar">
73
+ <div class="barbershop-stripes"></div>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ </div>
78
+
79
+ <div class="progress-level" id="level-3">
80
+ <div class="level-icon">
81
+ <span class="icon pending">○</span>
82
+ </div>
83
+ <div class="level-content">
84
+ <div class="level-title">Level 3: Codebase context</div>
85
+ <div class="level-status">Pending</div>
86
+ <div class="progress-bar-container" style="display: none;">
87
+ <div class="barbershop-progress-bar">
88
+ <div class="barbershop-stripes"></div>
89
+ </div>
90
+ </div>
91
+ </div>
92
+ </div>
93
+
94
+ <div class="progress-level" id="level-4">
95
+ <div class="level-icon">
96
+ <span class="icon pending">○</span>
97
+ </div>
98
+ <div class="level-content">
99
+ <div class="level-title">Finalizing Results</div>
100
+ <div class="level-status">Pending</div>
101
+ <div class="progress-bar-container" style="display: none;">
102
+ <div class="barbershop-progress-bar">
103
+ <div class="barbershop-stripes"></div>
104
+ </div>
105
+ </div>
106
+ </div>
107
+ </div>
108
+ </div>
109
+ </div>
110
+
111
+ <div class="modal-footer">
112
+ <button class="btn btn-secondary" id="run-background-btn" onclick="progressModal.runInBackground()">
113
+ Run in Background
114
+ </button>
115
+ <button class="btn btn-danger" id="cancel-btn" onclick="progressModal.cancel()">
116
+ Cancel
117
+ </button>
118
+ </div>
119
+ </div>
120
+ `;
121
+
122
+ document.body.appendChild(modalContainer);
123
+ this.modal = modalContainer;
124
+ }
125
+
126
+ /**
127
+ * Setup event listeners
128
+ */
129
+ setupEventListeners() {
130
+ // Close modal on Escape key
131
+ document.addEventListener('keydown', (e) => {
132
+ if (e.key === 'Escape' && this.isVisible) {
133
+ this.hide();
134
+ }
135
+ });
136
+ }
137
+
138
+ /**
139
+ * Show the modal
140
+ * @param {string} analysisId - Analysis ID to track
141
+ */
142
+ show(analysisId) {
143
+ this.currentAnalysisId = analysisId;
144
+ this.isVisible = true;
145
+ this.modal.style.display = 'flex';
146
+
147
+ // Reset progress state
148
+ this.resetProgress();
149
+
150
+ // Start monitoring progress
151
+ this.startProgressMonitoring();
152
+ }
153
+
154
+ /**
155
+ * Hide the modal
156
+ */
157
+ hide() {
158
+ this.isVisible = false;
159
+ this.modal.style.display = 'none';
160
+
161
+ // Don't stop monitoring if running in background
162
+ if (!this.isRunningInBackground) {
163
+ this.stopProgressMonitoring();
164
+ }
165
+ }
166
+
167
+ /**
168
+ * Run analysis in background
169
+ */
170
+ runInBackground() {
171
+ this.isRunningInBackground = true;
172
+ this.hide();
173
+
174
+ // Button already shows analyzing state, no need for separate status indicator
175
+ // The button was set to analyzing state when analysis started
176
+ }
177
+
178
+ /**
179
+ * Cancel the analysis
180
+ */
181
+ async cancel() {
182
+ if (!this.currentAnalysisId) {
183
+ this.hide();
184
+ return;
185
+ }
186
+
187
+ try {
188
+ // Make cancel request to backend
189
+ const response = await fetch(`/api/analyze/cancel/${this.currentAnalysisId}`, {
190
+ method: 'POST'
191
+ });
192
+
193
+ if (response.ok) {
194
+ this.updateStatus('Analysis cancelled');
195
+ }
196
+ } catch (error) {
197
+ console.warn('Cancel not available on server:', error.message);
198
+ }
199
+
200
+ this.stopProgressMonitoring();
201
+ this.hide();
202
+
203
+ // Reset button
204
+ if (window.prManager) {
205
+ window.prManager.resetButton();
206
+ }
207
+
208
+ // Reset AI panel to non-loading state
209
+ if (window.aiPanel?.setAnalysisState) {
210
+ window.aiPanel.setAnalysisState('unknown');
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Reset progress to initial state
216
+ */
217
+ resetProgress() {
218
+ // Reset levels 1-3 to running state
219
+ for (let i = 1; i <= 3; i++) {
220
+ const level = document.getElementById(`level-${i}`);
221
+ if (level) {
222
+ const icon = level.querySelector('.icon');
223
+ const status = level.querySelector('.level-status');
224
+ const progressContainer = level.querySelector('.progress-bar-container');
225
+
226
+ icon.className = 'icon active';
227
+ icon.textContent = '▶';
228
+ status.textContent = 'Starting...';
229
+ status.style.display = 'none';
230
+
231
+ // Show progress bar immediately for levels 1-3
232
+ if (progressContainer) {
233
+ progressContainer.style.display = 'block';
234
+ }
235
+ }
236
+ }
237
+
238
+ // Level 4 (orchestration) starts as pending
239
+ const level4 = document.getElementById('level-4');
240
+ if (level4) {
241
+ const icon = level4.querySelector('.icon');
242
+ const status = level4.querySelector('.level-status');
243
+ const progressContainer = level4.querySelector('.progress-bar-container');
244
+
245
+ icon.className = 'icon pending';
246
+ icon.textContent = '○';
247
+ status.textContent = 'Pending';
248
+ status.style.display = 'block';
249
+
250
+ if (progressContainer) {
251
+ progressContainer.style.display = 'none';
252
+ }
253
+ }
254
+
255
+ // Reset footer buttons to initial state
256
+ const runBackgroundBtn = document.getElementById('run-background-btn');
257
+ const cancelBtn = document.getElementById('cancel-btn');
258
+
259
+ if (runBackgroundBtn) {
260
+ runBackgroundBtn.textContent = 'Run in Background';
261
+ runBackgroundBtn.disabled = false;
262
+ }
263
+ if (cancelBtn) {
264
+ cancelBtn.textContent = 'Cancel';
265
+ }
266
+
267
+ // Reset background running state
268
+ this.isRunningInBackground = false;
269
+ }
270
+
271
+ /**
272
+ * Start monitoring progress via Server-Sent Events (SSE)
273
+ */
274
+ startProgressMonitoring() {
275
+ if (this.eventSource) {
276
+ this.eventSource.close();
277
+ }
278
+
279
+ if (!this.currentAnalysisId) return;
280
+
281
+ // Connect to SSE endpoint
282
+ this.eventSource = new EventSource(`/api/pr/${this.currentAnalysisId}/ai-suggestions/status`);
283
+
284
+ this.eventSource.onopen = () => {
285
+ console.log('Connected to progress stream');
286
+ };
287
+
288
+ this.eventSource.onmessage = (event) => {
289
+ try {
290
+ const data = JSON.parse(event.data);
291
+
292
+ if (data.type === 'connected') {
293
+ console.log('SSE connection established');
294
+ return;
295
+ }
296
+
297
+ if (data.type === 'progress') {
298
+ this.updateProgress(data);
299
+
300
+ // Stop monitoring if analysis is complete, failed, or cancelled
301
+ if (data.status === 'completed' || data.status === 'failed' || data.status === 'cancelled') {
302
+ this.stopProgressMonitoring();
303
+ }
304
+ }
305
+ } catch (error) {
306
+ console.error('Error parsing SSE data:', error);
307
+ }
308
+ };
309
+
310
+ this.eventSource.onerror = (error) => {
311
+ console.error('SSE connection error:', error);
312
+ // Fallback to polling if SSE fails
313
+ this.fallbackToPolling();
314
+ };
315
+ }
316
+
317
+ /**
318
+ * Fallback to polling if SSE fails
319
+ */
320
+ fallbackToPolling() {
321
+ if (this.eventSource) {
322
+ this.eventSource.close();
323
+ this.eventSource = null;
324
+ }
325
+
326
+ if (this.statusCheckInterval) {
327
+ clearInterval(this.statusCheckInterval);
328
+ }
329
+
330
+ this.statusCheckInterval = setInterval(async () => {
331
+ if (!this.currentAnalysisId) return;
332
+
333
+ try {
334
+ const response = await fetch(`/api/analyze/status/${this.currentAnalysisId}`);
335
+ if (!response.ok) {
336
+ throw new Error('Failed to fetch status');
337
+ }
338
+
339
+ const status = await response.json();
340
+ this.updateProgress(status);
341
+
342
+ // Stop monitoring if analysis is complete, failed, or cancelled
343
+ if (status.status === 'completed' || status.status === 'failed' || status.status === 'cancelled') {
344
+ this.stopProgressMonitoring();
345
+ }
346
+ } catch (error) {
347
+ console.error('Error checking analysis status:', error);
348
+ }
349
+ }, 1000); // Check every second
350
+ }
351
+
352
+ /**
353
+ * Stop progress monitoring
354
+ */
355
+ stopProgressMonitoring() {
356
+ if (this.statusCheckInterval) {
357
+ clearInterval(this.statusCheckInterval);
358
+ this.statusCheckInterval = null;
359
+ }
360
+
361
+ if (this.eventSource) {
362
+ this.eventSource.close();
363
+ this.eventSource = null;
364
+ }
365
+ }
366
+
367
+ /**
368
+ * Update progress based on status
369
+ * @param {Object} status - Status object from server
370
+ */
371
+ updateProgress(status) {
372
+ // Validate status structure before accessing properties
373
+ if (!status.levels || typeof status.levels !== 'object') {
374
+ console.warn('Invalid status structure - missing or malformed levels object:', status);
375
+ return;
376
+ }
377
+
378
+ // Update each level's progress independently from the levels object
379
+ for (let level = 1; level <= 4; level++) {
380
+ const levelStatus = status.levels[level];
381
+ if (levelStatus) {
382
+ this.updateLevelProgress(level, levelStatus);
383
+ }
384
+ }
385
+
386
+ // Update overall progress message
387
+ this.updateStatus(status.progress || 'Running...');
388
+
389
+ // Handle completion, failure, or cancellation
390
+ if (status.status === 'completed') {
391
+ this.handleCompletion(status);
392
+ } else if (status.status === 'failed') {
393
+ this.handleFailure(status);
394
+ } else if (status.status === 'cancelled') {
395
+ this.handleCancellation(status);
396
+ }
397
+ }
398
+
399
+ /**
400
+ * Mark a level as completed
401
+ * @param {number} level - Level number to mark as completed
402
+ */
403
+ markLevelAsCompleted(level) {
404
+ const levelElement = document.getElementById(`level-${level}`);
405
+ if (!levelElement) return;
406
+
407
+ const icon = levelElement.querySelector('.icon');
408
+ const statusText = levelElement.querySelector('.level-status');
409
+ const progressContainer = levelElement.querySelector('.progress-bar-container');
410
+
411
+ icon.className = 'icon completed';
412
+ icon.textContent = '✓';
413
+ statusText.textContent = 'Completed';
414
+ statusText.style.display = 'block';
415
+
416
+ // Hide progress bar for completed levels
417
+ if (progressContainer) {
418
+ progressContainer.style.display = 'none';
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Update a specific level's progress
424
+ * @param {number} level - Level number (1, 2, or 3)
425
+ * @param {Object} levelStatus - Level status object with { status, progress }
426
+ */
427
+ updateLevelProgress(level, levelStatus) {
428
+ const levelElement = document.getElementById(`level-${level}`);
429
+ if (!levelElement) return;
430
+
431
+ const icon = levelElement.querySelector('.icon');
432
+ const statusText = levelElement.querySelector('.level-status');
433
+ const progressContainer = levelElement.querySelector('.progress-bar-container');
434
+
435
+ // Update icon and status based on current state
436
+ if (levelStatus.status === 'running') {
437
+ icon.className = 'icon active';
438
+ icon.textContent = '▶';
439
+
440
+ // Show progress bar and hide status text for running levels
441
+ statusText.style.display = 'none';
442
+ if (progressContainer) {
443
+ progressContainer.style.display = 'block';
444
+ }
445
+
446
+ } else if (levelStatus.status === 'completed') {
447
+ icon.className = 'icon completed';
448
+ icon.textContent = '✓';
449
+ statusText.textContent = 'Completed';
450
+ statusText.style.display = 'block';
451
+
452
+ // Hide progress bar for completed levels
453
+ if (progressContainer) {
454
+ progressContainer.style.display = 'none';
455
+ }
456
+
457
+ } else if (levelStatus.status === 'failed') {
458
+ icon.className = 'icon error';
459
+ icon.textContent = '❌';
460
+ statusText.textContent = 'Failed';
461
+ statusText.style.display = 'block';
462
+
463
+ // Hide progress bar for failed levels
464
+ if (progressContainer) {
465
+ progressContainer.style.display = 'none';
466
+ }
467
+
468
+ } else if (levelStatus.status === 'cancelled') {
469
+ icon.className = 'icon cancelled';
470
+ icon.textContent = '⊘';
471
+ statusText.textContent = 'Cancelled';
472
+ statusText.style.display = 'block';
473
+
474
+ // Hide progress bar for cancelled levels
475
+ if (progressContainer) {
476
+ progressContainer.style.display = 'none';
477
+ }
478
+
479
+ } else {
480
+ // For pending or other states
481
+ console.warn('Unexpected level status:', levelStatus.status, 'for level', level);
482
+ icon.className = 'icon pending';
483
+ icon.textContent = '○';
484
+ statusText.textContent = levelStatus.progress || 'Pending';
485
+ statusText.style.display = 'block';
486
+
487
+ if (progressContainer) {
488
+ progressContainer.style.display = 'none';
489
+ }
490
+ }
491
+
492
+ // Update toolbar progress dots (check both PR and local managers)
493
+ const manager = window.prManager || window.localManager;
494
+ if (manager?.updateProgressDot) {
495
+ manager.updateProgressDot(level, levelStatus.status);
496
+ }
497
+ }
498
+
499
+ /**
500
+ * Update general status message
501
+ * @param {string} message - Status message
502
+ */
503
+ updateStatus(message) {
504
+ // Could add a general status area if needed
505
+ console.log('Progress:', message);
506
+ }
507
+
508
+ /**
509
+ * Handle analysis completion
510
+ * @param {Object} status - Final status object
511
+ */
512
+ handleCompletion(status) {
513
+ // Levels are already marked as completed by updateProgress
514
+ // Just update the UI buttons
515
+
516
+ const completedLevel = status.completedLevel || status.level || 3;
517
+
518
+ // Update button to show completion
519
+ const runBackgroundBtn = document.getElementById('run-background-btn');
520
+ const cancelBtn = document.getElementById('cancel-btn');
521
+
522
+ if (runBackgroundBtn) {
523
+ runBackgroundBtn.textContent = `Analysis Complete`;
524
+ runBackgroundBtn.disabled = true;
525
+ }
526
+ if (cancelBtn) {
527
+ cancelBtn.textContent = 'Close';
528
+ }
529
+
530
+ // Update button to show completion
531
+ if (window.prManager) {
532
+ window.prManager.setButtonComplete();
533
+ }
534
+
535
+ // CRITICAL FIX: Automatically reload AI suggestions when analysis completes
536
+ console.log('Analysis completed, reloading AI suggestions...');
537
+
538
+ // Support both PR mode (prManager) and Local mode (localManager)
539
+ const manager = window.prManager || window.localManager;
540
+
541
+ if (manager && typeof manager.loadAISuggestions === 'function') {
542
+ // Determine whether to switch to the new run:
543
+ // - If modal is visible, user was waiting for results -> switch immediately
544
+ // - If modal is hidden (running in background), user was viewing older results -> don't switch
545
+ const shouldSwitchToNew = this.isVisible;
546
+
547
+ // First, refresh the analysis history manager to include the new run
548
+ const refreshHistory = async () => {
549
+ if (manager.analysisHistoryManager) {
550
+ console.log('Refreshing analysis history, switchToNew:', shouldSwitchToNew);
551
+ const result = await manager.analysisHistoryManager.refresh({ switchToNew: shouldSwitchToNew });
552
+ // Return whether the manager actually switched to the new run
553
+ // This can differ from shouldSwitchToNew when it's the first-ever run
554
+ // (first run always switches regardless of shouldSwitchToNew)
555
+ return result.didSwitch;
556
+ }
557
+ // No history manager, so we'll load suggestions directly
558
+ return true;
559
+ };
560
+
561
+ refreshHistory()
562
+ .then((didSwitch) => {
563
+ // Load suggestions if we switched to the new run
564
+ // Note: didSwitch may be true even if shouldSwitchToNew was false
565
+ // (e.g., first-ever analysis run always switches because there's no previous selection)
566
+ if (didSwitch) {
567
+ return manager.loadAISuggestions();
568
+ }
569
+ // Otherwise, just return - the user will load when they select the new run
570
+ console.log('New analysis available - user will see indicator on dropdown');
571
+ return Promise.resolve();
572
+ })
573
+ .then(() => {
574
+ console.log('AI suggestions reloaded successfully');
575
+ // Only auto-close after suggestions have loaded successfully
576
+ if (this.isVisible) {
577
+ setTimeout(() => {
578
+ this.hide();
579
+ }, 2000); // Reduced to 2 seconds since loading is complete
580
+ }
581
+ })
582
+ .catch(error => {
583
+ console.error('Error reloading AI suggestions:', error);
584
+ // Still auto-close even if loading failed, but give more time for user to see error
585
+ if (this.isVisible) {
586
+ setTimeout(() => {
587
+ this.hide();
588
+ }, 5000);
589
+ }
590
+ });
591
+ } else {
592
+ console.warn('Manager not available for automatic suggestion reload');
593
+ // Auto-close after 3 seconds if no manager available
594
+ if (this.isVisible) {
595
+ setTimeout(() => {
596
+ this.hide();
597
+ }, 3000);
598
+ }
599
+ }
600
+ }
601
+
602
+ /**
603
+ * Handle analysis failure
604
+ * @param {Object} status - Error status object
605
+ */
606
+ handleFailure(status) {
607
+ // Levels are already marked as failed by updateProgress
608
+ // Just update the UI buttons
609
+
610
+ // Update buttons
611
+ const runBackgroundBtn = document.getElementById('run-background-btn');
612
+ const cancelBtn = document.getElementById('cancel-btn');
613
+
614
+ if (runBackgroundBtn) {
615
+ runBackgroundBtn.textContent = 'Analysis Failed';
616
+ runBackgroundBtn.disabled = true;
617
+ }
618
+ if (cancelBtn) {
619
+ cancelBtn.textContent = 'Close';
620
+ }
621
+
622
+ // Reset button on failure
623
+ if (window.prManager) {
624
+ window.prManager.resetButton();
625
+ }
626
+ }
627
+
628
+ /**
629
+ * Handle analysis cancellation (via SSE status)
630
+ * @param {Object} status - Cancellation status object
631
+ */
632
+ handleCancellation(status) {
633
+ // Update buttons to show cancelled state
634
+ const runBackgroundBtn = document.getElementById('run-background-btn');
635
+ const cancelBtn = document.getElementById('cancel-btn');
636
+
637
+ if (runBackgroundBtn) {
638
+ runBackgroundBtn.textContent = 'Analysis Cancelled';
639
+ runBackgroundBtn.disabled = true;
640
+ }
641
+ if (cancelBtn) {
642
+ cancelBtn.textContent = 'Close';
643
+ }
644
+
645
+ // Reset the analyze button and AI panel state
646
+ if (window.prManager) {
647
+ window.prManager.resetButton();
648
+ }
649
+
650
+ // Reset AI panel to non-loading state
651
+ if (window.aiPanel?.setAnalysisState) {
652
+ window.aiPanel.setAnalysisState('unknown');
653
+ }
654
+
655
+ // Hide modal after a brief delay
656
+ if (this.isVisible) {
657
+ setTimeout(() => {
658
+ this.hide();
659
+ }, 1500);
660
+ }
661
+ }
662
+
663
+ /**
664
+ * Reopen modal from background
665
+ */
666
+ reopenFromBackground() {
667
+ this.isRunningInBackground = false;
668
+ this.show(this.currentAnalysisId);
669
+
670
+ // Hide status indicator
671
+ if (window.statusIndicator) {
672
+ window.statusIndicator.hide();
673
+ }
674
+ }
675
+ }
676
+
677
+ // Initialize global instance
678
+ window.progressModal = new ProgressModal();