@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,382 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ /**
3
+ * Split Button Component
4
+ * A button with a main action area and a dropdown menu for additional actions
5
+ */
6
+ class SplitButton {
7
+ // localStorage key for persisting default action preference
8
+ static STORAGE_KEY = 'pair-review-split-button-default-action';
9
+
10
+ constructor(options = {}) {
11
+ this.container = null;
12
+ this.dropdown = null;
13
+ this.isOpen = false;
14
+ this.commentCount = 0;
15
+ // In local mode, default to preview since submit is hidden
16
+ const isLocalMode = window.PAIR_REVIEW_LOCAL_MODE === true;
17
+ this.hideSubmit = isLocalMode || options.hideSubmit === true;
18
+
19
+ // Determine default action: local mode always uses preview,
20
+ // otherwise check localStorage for saved preference
21
+ if (isLocalMode) {
22
+ this.defaultAction = 'preview';
23
+ } else {
24
+ const savedAction = this.loadSavedAction();
25
+ this.defaultAction = savedAction || options.defaultAction || 'submit';
26
+ }
27
+ this.onSubmit = options.onSubmit || (() => {});
28
+ this.onPreview = options.onPreview || (() => {});
29
+ this.onClear = options.onClear || (() => {});
30
+ this.onSetDefault = options.onSetDefault || (() => {});
31
+
32
+ // Bind methods
33
+ this.handleMainClick = this.handleMainClick.bind(this);
34
+ this.handleDropdownClick = this.handleDropdownClick.bind(this);
35
+ this.handleOutsideClick = this.handleOutsideClick.bind(this);
36
+ this.handleMenuItemClick = this.handleMenuItemClick.bind(this);
37
+ }
38
+
39
+ /**
40
+ * Create and return the split button element
41
+ * @returns {HTMLElement} The split button container element
42
+ */
43
+ render() {
44
+ // Create container
45
+ this.container = document.createElement('div');
46
+ this.container.className = 'split-button-container';
47
+ this.container.id = 'comment-split-button';
48
+
49
+ // Create main button
50
+ const mainButton = document.createElement('button');
51
+ mainButton.className = 'split-button-main';
52
+ mainButton.id = 'split-button-main';
53
+ mainButton.type = 'button';
54
+ mainButton.addEventListener('click', this.handleMainClick);
55
+
56
+ // Create button text span
57
+ const buttonText = document.createElement('span');
58
+ buttonText.className = 'split-button-text';
59
+ buttonText.id = 'split-button-text';
60
+ buttonText.textContent = this.getButtonText();
61
+ mainButton.appendChild(buttonText);
62
+
63
+ // Create dropdown toggle button
64
+ const dropdownToggle = document.createElement('button');
65
+ dropdownToggle.className = 'split-button-dropdown-toggle';
66
+ dropdownToggle.id = 'split-button-dropdown-toggle';
67
+ dropdownToggle.type = 'button';
68
+ dropdownToggle.setAttribute('aria-label', 'Open comment actions menu');
69
+ dropdownToggle.setAttribute('aria-haspopup', 'true');
70
+ dropdownToggle.setAttribute('aria-expanded', 'false');
71
+ dropdownToggle.addEventListener('click', this.handleDropdownClick);
72
+
73
+ // Add dropdown arrow icon
74
+ dropdownToggle.innerHTML = `
75
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
76
+ <path d="M4.427 7.427l3.396 3.396a.25.25 0 00.354 0l3.396-3.396A.25.25 0 0011.396 7H4.604a.25.25 0 00-.177.427z"/>
77
+ </svg>
78
+ `;
79
+
80
+ // Create dropdown menu
81
+ this.dropdown = document.createElement('div');
82
+ this.dropdown.className = 'split-button-dropdown';
83
+ this.dropdown.id = 'split-button-dropdown';
84
+ this.dropdown.setAttribute('role', 'menu');
85
+ this.dropdown.style.display = 'none';
86
+
87
+ this.updateDropdownMenu();
88
+
89
+ // Assemble the split button
90
+ this.container.appendChild(mainButton);
91
+ this.container.appendChild(dropdownToggle);
92
+ this.container.appendChild(this.dropdown);
93
+
94
+ return this.container;
95
+ }
96
+
97
+ /**
98
+ * Update the dropdown menu items based on current state
99
+ */
100
+ updateDropdownMenu() {
101
+ if (!this.dropdown) return;
102
+
103
+ const isSubmitDefault = this.defaultAction === 'submit';
104
+ const isPreviewDefault = this.defaultAction === 'preview';
105
+
106
+ // Build menu items - conditionally include Submit Review
107
+ let menuItems = '';
108
+
109
+ if (!this.hideSubmit) {
110
+ menuItems += `
111
+ <button class="split-button-menu-item" data-action="submit" role="menuitem">
112
+ <span class="menu-item-check">${isSubmitDefault ? '&#10003;' : ''}</span>
113
+ <span class="menu-item-text">Submit Review</span>
114
+ </button>`;
115
+ }
116
+
117
+ menuItems += `
118
+ <button class="split-button-menu-item" data-action="preview" role="menuitem">
119
+ <span class="menu-item-check">${isPreviewDefault ? '&#10003;' : ''}</span>
120
+ <span class="menu-item-text">Preview</span>
121
+ </button>
122
+ <div class="split-button-menu-separator"></div>
123
+ <button class="split-button-menu-item split-button-menu-item-danger" data-action="clear" role="menuitem" ${this.commentCount === 0 ? 'disabled' : ''}>
124
+ <span class="menu-item-check"></span>
125
+ <span class="menu-item-text">Clear All</span>
126
+ </button>
127
+ `;
128
+
129
+ this.dropdown.innerHTML = menuItems;
130
+
131
+ // Add click handlers to menu items
132
+ this.dropdown.querySelectorAll('.split-button-menu-item').forEach(item => {
133
+ item.addEventListener('click', this.handleMenuItemClick);
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Handle click on the main button area
139
+ */
140
+ handleMainClick() {
141
+ if (this.defaultAction === 'submit') {
142
+ this.onSubmit();
143
+ } else {
144
+ this.onPreview();
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Handle click on the dropdown toggle
150
+ * @param {Event} event - Click event
151
+ */
152
+ handleDropdownClick(event) {
153
+ event.stopPropagation();
154
+ this.toggleDropdown();
155
+ }
156
+
157
+ /**
158
+ * Handle click on a menu item
159
+ * @param {Event} event - Click event
160
+ */
161
+ handleMenuItemClick(event) {
162
+ const button = event.currentTarget;
163
+ if (button.disabled) return;
164
+
165
+ const action = button.dataset.action;
166
+
167
+ switch (action) {
168
+ case 'submit':
169
+ if (this.defaultAction !== 'submit') {
170
+ this.setDefaultAction('submit');
171
+ }
172
+ this.onSubmit();
173
+ break;
174
+ case 'preview':
175
+ if (this.defaultAction !== 'preview') {
176
+ this.setDefaultAction('preview');
177
+ }
178
+ this.onPreview();
179
+ break;
180
+ case 'clear':
181
+ this.onClear();
182
+ break;
183
+ }
184
+
185
+ this.closeDropdown();
186
+ }
187
+
188
+ /**
189
+ * Toggle dropdown open/close state
190
+ */
191
+ toggleDropdown() {
192
+ if (this.isOpen) {
193
+ this.closeDropdown();
194
+ } else {
195
+ this.openDropdown();
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Open the dropdown menu
201
+ */
202
+ openDropdown() {
203
+ if (!this.dropdown) return;
204
+
205
+ this.isOpen = true;
206
+ this.dropdown.style.display = 'block';
207
+
208
+ const toggleButton = this.container?.querySelector('#split-button-dropdown-toggle');
209
+ if (toggleButton) {
210
+ toggleButton.setAttribute('aria-expanded', 'true');
211
+ }
212
+
213
+ this.container?.classList.add('split-button-open');
214
+
215
+ // Add outside click listener
216
+ setTimeout(() => {
217
+ document.addEventListener('click', this.handleOutsideClick);
218
+ }, 0);
219
+ }
220
+
221
+ /**
222
+ * Close the dropdown menu
223
+ */
224
+ closeDropdown() {
225
+ if (!this.dropdown) return;
226
+
227
+ this.isOpen = false;
228
+ this.dropdown.style.display = 'none';
229
+
230
+ const toggleButton = this.container?.querySelector('#split-button-dropdown-toggle');
231
+ if (toggleButton) {
232
+ toggleButton.setAttribute('aria-expanded', 'false');
233
+ }
234
+
235
+ this.container?.classList.remove('split-button-open');
236
+
237
+ // Remove outside click listener
238
+ document.removeEventListener('click', this.handleOutsideClick);
239
+ }
240
+
241
+ /**
242
+ * Handle clicks outside the dropdown to close it
243
+ * @param {Event} event - Click event
244
+ */
245
+ handleOutsideClick(event) {
246
+ if (this.container && !this.container.contains(event.target)) {
247
+ this.closeDropdown();
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Set the default action for the main button
253
+ * @param {string} action - 'submit' or 'preview'
254
+ */
255
+ setDefaultAction(action) {
256
+ if (action !== 'submit' && action !== 'preview') return;
257
+
258
+ // In local mode (hideSubmit), don't allow setting submit as default
259
+ if (this.hideSubmit && action === 'submit') {
260
+ action = 'preview';
261
+ }
262
+
263
+ this.defaultAction = action;
264
+ this.updateDropdownMenu();
265
+ this.updateButtonText();
266
+ this.onSetDefault(action);
267
+
268
+ // Persist to localStorage (only in PR mode where submit is available)
269
+ if (!this.hideSubmit) {
270
+ this.saveAction(action);
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Load saved action preference from localStorage
276
+ * @returns {string|null} Saved action or null if not found
277
+ */
278
+ loadSavedAction() {
279
+ try {
280
+ const saved = localStorage.getItem(SplitButton.STORAGE_KEY);
281
+ if (saved === 'submit' || saved === 'preview') {
282
+ return saved;
283
+ }
284
+ } catch {
285
+ // localStorage may be unavailable (private browsing, etc.)
286
+ }
287
+ return null;
288
+ }
289
+
290
+ /**
291
+ * Save action preference to localStorage
292
+ * @param {string} action - 'submit' or 'preview'
293
+ */
294
+ saveAction(action) {
295
+ try {
296
+ localStorage.setItem(SplitButton.STORAGE_KEY, action);
297
+ } catch {
298
+ // localStorage may be unavailable (private browsing, etc.)
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Get the button text based on default action and comment count
304
+ * @returns {string} Button text
305
+ */
306
+ getButtonText() {
307
+ const actionText = this.defaultAction === 'submit' ? 'Submit Review' : 'Preview';
308
+ if (this.commentCount > 0) {
309
+ return `${actionText} (${this.commentCount})`;
310
+ }
311
+ return actionText;
312
+ }
313
+
314
+ /**
315
+ * Update the button text display
316
+ */
317
+ updateButtonText() {
318
+ const textSpan = this.container?.querySelector('#split-button-text');
319
+ if (textSpan) {
320
+ textSpan.textContent = this.getButtonText();
321
+ }
322
+ }
323
+
324
+ /**
325
+ * Update the comment count display
326
+ * @param {number} count - Number of comments
327
+ */
328
+ updateCommentCount(count) {
329
+ this.commentCount = count;
330
+ this.updateButtonText();
331
+
332
+ // Update button styling based on count
333
+ const mainButton = this.container?.querySelector('#split-button-main');
334
+ const dropdownToggle = this.container?.querySelector('#split-button-dropdown-toggle');
335
+
336
+ if (mainButton) {
337
+ if (count > 0) {
338
+ mainButton.classList.add('has-comments');
339
+ } else {
340
+ mainButton.classList.remove('has-comments');
341
+ }
342
+ }
343
+
344
+ if (dropdownToggle) {
345
+ if (count > 0) {
346
+ dropdownToggle.classList.add('has-comments');
347
+ } else {
348
+ dropdownToggle.classList.remove('has-comments');
349
+ }
350
+ }
351
+
352
+ // Update the Clear All menu item disabled state
353
+ this.updateDropdownMenu();
354
+ }
355
+
356
+ /**
357
+ * Get the current comment count
358
+ * @returns {number} Current comment count
359
+ */
360
+ getCommentCount() {
361
+ return this.commentCount;
362
+ }
363
+
364
+ /**
365
+ * Destroy the component and clean up event listeners
366
+ */
367
+ destroy() {
368
+ document.removeEventListener('click', this.handleOutsideClick);
369
+
370
+ if (this.container) {
371
+ this.container.remove();
372
+ this.container = null;
373
+ }
374
+
375
+ this.dropdown = null;
376
+ }
377
+ }
378
+
379
+ // Export for use in other modules
380
+ if (typeof window !== 'undefined') {
381
+ window.SplitButton = SplitButton;
382
+ }
@@ -0,0 +1,265 @@
1
+ // SPDX-License-Identifier: GPL-3.0-or-later
2
+ /**
3
+ * Status Indicator Component
4
+ * Shows AI analysis status in the toolbar when running in background
5
+ */
6
+ class StatusIndicator {
7
+ constructor() {
8
+ this.indicator = null;
9
+ this.currentAnalysisId = null;
10
+ this.animationDots = 0;
11
+ this.dotsInterval = null;
12
+
13
+ this.createIndicator();
14
+ }
15
+
16
+ /**
17
+ * Create the status indicator DOM structure
18
+ */
19
+ createIndicator() {
20
+ // Remove existing indicator if it exists
21
+ const existing = document.getElementById('status-indicator');
22
+ if (existing) {
23
+ existing.remove();
24
+ }
25
+
26
+ // Create status indicator element
27
+ const indicator = document.createElement('div');
28
+ indicator.id = 'status-indicator';
29
+ indicator.className = 'status-indicator';
30
+ indicator.style.display = 'none';
31
+
32
+ indicator.innerHTML = `
33
+ <div class="status-content" onclick="statusIndicator.reopenModal()">
34
+ <span class="status-icon">
35
+ <svg class="spinner" width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
36
+ <path d="M8 0a8 8 0 0 0-8 8h2a6 6 0 0 1 6-6V0z"/>
37
+ </svg>
38
+ </span>
39
+ <span class="status-text">AI analyzing</span>
40
+ <span class="status-dots">...</span>
41
+ </div>
42
+ <button class="status-close" onclick="statusIndicator.hide()" title="Dismiss">
43
+ <svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
44
+ <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"/>
45
+ </svg>
46
+ </button>
47
+ `;
48
+
49
+ // Insert indicator into the PR actions area
50
+ const prActions = document.querySelector('.pr-actions');
51
+ if (prActions) {
52
+ // Insert before the first button (theme toggle)
53
+ const firstButton = prActions.querySelector('button');
54
+ if (firstButton) {
55
+ prActions.insertBefore(indicator, firstButton);
56
+ } else {
57
+ prActions.appendChild(indicator);
58
+ }
59
+ } else {
60
+ // Fallback: append to body
61
+ document.body.appendChild(indicator);
62
+ }
63
+
64
+ this.indicator = indicator;
65
+ }
66
+
67
+ /**
68
+ * Show the status indicator
69
+ * @param {string} analysisId - Analysis ID being tracked
70
+ * @param {string} text - Initial status text
71
+ */
72
+ show(analysisId, text = 'AI analyzing') {
73
+ if (!this.indicator) {
74
+ this.createIndicator();
75
+ }
76
+
77
+ this.currentAnalysisId = analysisId;
78
+ this.indicator.style.display = 'flex';
79
+
80
+ // Set initial state
81
+ this.updateText(text);
82
+ this.showSpinner();
83
+ this.startDotsAnimation();
84
+ }
85
+
86
+ /**
87
+ * Hide the status indicator
88
+ */
89
+ hide() {
90
+ if (this.indicator) {
91
+ this.indicator.style.display = 'none';
92
+ }
93
+ this.stopDotsAnimation();
94
+ this.currentAnalysisId = null;
95
+ }
96
+
97
+ /**
98
+ * Update status text
99
+ * @param {string} text - New status text
100
+ */
101
+ updateText(text) {
102
+ const statusText = this.indicator?.querySelector('.status-text');
103
+ if (statusText) {
104
+ statusText.textContent = text;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Show completion state
110
+ * @param {string} message - Completion message
111
+ */
112
+ showComplete(message = 'Analysis complete') {
113
+ this.updateText(message);
114
+ this.showCheckmark();
115
+ this.stopDotsAnimation();
116
+
117
+ // CRITICAL FIX: Automatically reload AI suggestions when background analysis completes
118
+ console.log('Background analysis completed, reloading AI suggestions...');
119
+ if (window.prManager && typeof window.prManager.loadAISuggestions === 'function') {
120
+ window.prManager.loadAISuggestions()
121
+ .then(() => {
122
+ console.log('AI suggestions reloaded successfully after background analysis');
123
+ // Auto-hide after suggestions have loaded successfully (shorter delay)
124
+ setTimeout(() => {
125
+ this.hide();
126
+ }, 3000);
127
+ })
128
+ .catch(error => {
129
+ console.error('Error reloading AI suggestions after background analysis:', error);
130
+ // Auto-hide with longer delay if loading failed
131
+ setTimeout(() => {
132
+ this.hide();
133
+ }, 7000);
134
+ });
135
+ } else {
136
+ console.warn('PRManager not available for automatic suggestion reload after background analysis');
137
+ // Auto-hide after 5 seconds if no PR manager available
138
+ setTimeout(() => {
139
+ this.hide();
140
+ }, 5000);
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Show error state
146
+ * @param {string} message - Error message
147
+ */
148
+ showError(message = 'Analysis failed') {
149
+ this.updateText(message);
150
+ this.showErrorIcon();
151
+ this.stopDotsAnimation();
152
+
153
+ // Auto-hide after 10 seconds
154
+ setTimeout(() => {
155
+ this.hide();
156
+ }, 10000);
157
+ }
158
+
159
+ /**
160
+ * Show spinner icon
161
+ */
162
+ showSpinner() {
163
+ const statusIcon = this.indicator?.querySelector('.status-icon');
164
+ if (statusIcon) {
165
+ statusIcon.innerHTML = `
166
+ <svg class="spinner" width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
167
+ <path d="M8 0a8 8 0 0 0-8 8h2a6 6 0 0 1 6-6V0z"/>
168
+ </svg>
169
+ `;
170
+ statusIcon.className = 'status-icon spinning';
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Show checkmark icon
176
+ */
177
+ showCheckmark() {
178
+ const statusIcon = this.indicator?.querySelector('.status-icon');
179
+ if (statusIcon) {
180
+ statusIcon.innerHTML = `
181
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" class="checkmark">
182
+ <path d="M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z"/>
183
+ </svg>
184
+ `;
185
+ statusIcon.className = 'status-icon success';
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Show error icon
191
+ */
192
+ showErrorIcon() {
193
+ const statusIcon = this.indicator?.querySelector('.status-icon');
194
+ if (statusIcon) {
195
+ statusIcon.innerHTML = `
196
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" class="error-icon">
197
+ <path d="M2.343 13.657A8 8 0 1113.657 2.343 8 8 0 012.343 13.657zM6.03 4.97a.75.75 0 00-1.06 1.06L6.94 8 4.97 9.97a.75.75 0 101.06 1.06L8 9.06l1.97 1.97a.75.75 0 101.06-1.06L9.06 8l1.97-1.97a.75.75 0 10-1.06-1.06L8 6.94 6.03 4.97z"/>
198
+ </svg>
199
+ `;
200
+ statusIcon.className = 'status-icon error';
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Start animated dots
206
+ */
207
+ startDotsAnimation() {
208
+ this.stopDotsAnimation(); // Clear any existing interval
209
+
210
+ this.dotsInterval = setInterval(() => {
211
+ const dotsElement = this.indicator?.querySelector('.status-dots');
212
+ if (!dotsElement) return;
213
+
214
+ this.animationDots = (this.animationDots + 1) % 4;
215
+ dotsElement.textContent = '.'.repeat(this.animationDots);
216
+ }, 500);
217
+ }
218
+
219
+ /**
220
+ * Stop animated dots
221
+ */
222
+ stopDotsAnimation() {
223
+ if (this.dotsInterval) {
224
+ clearInterval(this.dotsInterval);
225
+ this.dotsInterval = null;
226
+ }
227
+
228
+ const dotsElement = this.indicator?.querySelector('.status-dots');
229
+ if (dotsElement) {
230
+ dotsElement.textContent = '';
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Reopen modal from status indicator
236
+ */
237
+ reopenModal() {
238
+ if (this.currentAnalysisId && window.progressModal) {
239
+ window.progressModal.reopenFromBackground();
240
+ }
241
+ }
242
+
243
+ /**
244
+ * Ensure indicator is properly positioned in header
245
+ */
246
+ repositionInHeader() {
247
+ // This method can be called after the header is re-rendered
248
+ // to ensure the indicator is in the correct position
249
+ const prActions = document.querySelector('.pr-actions');
250
+ const existingIndicator = document.getElementById('status-indicator');
251
+
252
+ if (prActions && existingIndicator && !prActions.contains(existingIndicator)) {
253
+ // Move the indicator to the correct position
254
+ const firstButton = prActions.querySelector('button');
255
+ if (firstButton) {
256
+ prActions.insertBefore(existingIndicator, firstButton);
257
+ } else {
258
+ prActions.appendChild(existingIndicator);
259
+ }
260
+ }
261
+ }
262
+ }
263
+
264
+ // Initialize global instance
265
+ window.statusIndicator = new StatusIndicator();