@in-the-loop-labs/pair-review 3.1.3 → 3.2.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.
- 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 +980 -3
- package/public/js/components/AIPanel.js +7 -4
- package/public/js/components/ChatPanel.js +34 -4
- package/public/js/components/CouncilProgressModal.js +11 -0
- package/public/js/components/NotificationDropdown.js +257 -0
- package/public/js/components/StackAnalysisDialog.js +313 -0
- package/public/js/components/StackProgressModal.js +475 -0
- package/public/js/components/StatusIndicator.js +1 -0
- package/public/js/components/SuggestionNavigator.js +2 -0
- package/public/js/modules/comment-manager.js +7 -0
- package/public/js/modules/comment-minimizer.js +151 -4
- package/public/js/modules/file-comment-manager.js +66 -2
- package/public/js/modules/suggestion-manager.js +2 -1
- package/public/js/pr.js +433 -2
- package/public/js/utils/notification-sounds.js +62 -0
- package/public/local.html +10 -0
- package/public/pr.html +12 -0
- package/public/setup.html +4 -0
- package/src/ai/claude-provider.js +1 -11
- package/src/ai/codex-provider.js +18 -16
- package/src/ai/copilot-provider.js +21 -21
- package/src/ai/gemini-provider.js +10 -0
- package/src/ai/pi-provider.js +22 -25
- package/src/ai/provider.js +26 -3
- package/src/chat/pi-bridge.js +8 -0
- package/src/chat/session-manager.js +1 -0
- package/src/git/base-branch.js +1 -51
- package/src/git/worktree-lock.js +88 -0
- package/src/git/worktree.js +64 -0
- package/src/github/stack-walker.js +196 -0
- package/src/routes/local.js +12 -8
- package/src/routes/pr.js +139 -26
- package/src/routes/sound.js +49 -0
- package/src/routes/stack-analysis.js +886 -0
- package/src/server.js +4 -0
- package/src/setup/stack-setup.js +77 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
// Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Stack Progress Modal Component
|
|
4
|
+
* Displays per-PR progress during stack analysis.
|
|
5
|
+
* Subscribes to WebSocket for live updates and shows level detail for
|
|
6
|
+
* the currently-running PR.
|
|
7
|
+
*/
|
|
8
|
+
class StackProgressModal {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.modal = null;
|
|
11
|
+
this.isVisible = false;
|
|
12
|
+
this.isRunningInBackground = false;
|
|
13
|
+
this.stackAnalysisId = null;
|
|
14
|
+
this.prList = [];
|
|
15
|
+
this.owner = null;
|
|
16
|
+
this.repo = null;
|
|
17
|
+
this._wsStackUnsub = null;
|
|
18
|
+
this._wsAnalysisUnsubs = new Map();
|
|
19
|
+
this._onReconnect = null;
|
|
20
|
+
this._prStatuses = [];
|
|
21
|
+
this._onComplete = null;
|
|
22
|
+
|
|
23
|
+
this._createModal();
|
|
24
|
+
this._setupEventListeners();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Lifecycle
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Show the modal and begin tracking stack analysis progress.
|
|
33
|
+
* @param {string} stackAnalysisId - The stack analysis tracking ID
|
|
34
|
+
* @param {Array<{prNumber: number, title?: string}>} prList - Ordered list of PRs being analyzed
|
|
35
|
+
* @param {Object} context - Additional context
|
|
36
|
+
* @param {string} context.owner - Repository owner
|
|
37
|
+
* @param {string} context.repo - Repository name
|
|
38
|
+
*/
|
|
39
|
+
open(stackAnalysisId, prList, context = {}) {
|
|
40
|
+
this.stackAnalysisId = stackAnalysisId;
|
|
41
|
+
this.prList = prList;
|
|
42
|
+
this.owner = context.owner || null;
|
|
43
|
+
this.repo = context.repo || null;
|
|
44
|
+
this._onComplete = context.onComplete || null;
|
|
45
|
+
this._prStatuses = prList.map(pr => ({
|
|
46
|
+
prNumber: pr.prNumber,
|
|
47
|
+
title: pr.title || `PR #${pr.prNumber}`,
|
|
48
|
+
status: 'pending',
|
|
49
|
+
analysisId: null,
|
|
50
|
+
suggestionsCount: null,
|
|
51
|
+
error: null
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
this.isVisible = true;
|
|
55
|
+
this.isRunningInBackground = false;
|
|
56
|
+
|
|
57
|
+
this._rebuildBody();
|
|
58
|
+
this._updateFooter('running');
|
|
59
|
+
this.modal.style.display = 'flex';
|
|
60
|
+
|
|
61
|
+
this._startMonitoring();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Hide the modal. Keeps subscriptions alive if analysis is still running
|
|
66
|
+
* so it can be reopened later.
|
|
67
|
+
*/
|
|
68
|
+
hide() {
|
|
69
|
+
this.isVisible = false;
|
|
70
|
+
this.isRunningInBackground = !!this._wsStackUnsub;
|
|
71
|
+
this.modal.style.display = 'none';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Run the analysis in the background (same as hide — kept for API clarity).
|
|
76
|
+
*/
|
|
77
|
+
runInBackground() {
|
|
78
|
+
this.hide();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Reopen the progress modal after it was hidden/backgrounded.
|
|
83
|
+
*/
|
|
84
|
+
reopenFromBackground() {
|
|
85
|
+
if (this.stackAnalysisId) {
|
|
86
|
+
this.isRunningInBackground = false;
|
|
87
|
+
this.isVisible = true;
|
|
88
|
+
this.modal.style.display = 'flex';
|
|
89
|
+
// Re-fetch status in case we missed updates
|
|
90
|
+
this._fetchStatus();
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Whether a stack analysis is actively running (for external callers).
|
|
96
|
+
*/
|
|
97
|
+
get isActive() {
|
|
98
|
+
return !!this.stackAnalysisId && (this.isVisible || this.isRunningInBackground);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Cancel the stack analysis.
|
|
103
|
+
*/
|
|
104
|
+
async cancel() {
|
|
105
|
+
if (!this.stackAnalysisId) {
|
|
106
|
+
this.hide();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
await fetch(`/api/analyses/stack/${this.stackAnalysisId}/cancel`, { method: 'POST' });
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.warn('Stack cancel request failed:', error.message);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
this._stopMonitoring();
|
|
117
|
+
this.stackAnalysisId = null;
|
|
118
|
+
this.isRunningInBackground = false;
|
|
119
|
+
if (this._onComplete) {
|
|
120
|
+
this._onComplete('cancelled');
|
|
121
|
+
}
|
|
122
|
+
this.isVisible = false;
|
|
123
|
+
this.modal.style.display = 'none';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// DOM creation
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
_createModal() {
|
|
131
|
+
const existing = document.getElementById('stack-progress-modal');
|
|
132
|
+
if (existing) existing.remove();
|
|
133
|
+
|
|
134
|
+
const overlay = document.createElement('div');
|
|
135
|
+
overlay.id = 'stack-progress-modal';
|
|
136
|
+
overlay.className = 'stack-progress-overlay';
|
|
137
|
+
overlay.style.display = 'none';
|
|
138
|
+
|
|
139
|
+
overlay.innerHTML = `
|
|
140
|
+
<div class="stack-progress-backdrop" data-action="close"></div>
|
|
141
|
+
<div class="stack-progress-modal">
|
|
142
|
+
<div class="stack-progress-header">
|
|
143
|
+
<h3>Stack Analysis Progress</h3>
|
|
144
|
+
<button class="modal-close-btn" data-action="close" title="Close">
|
|
145
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
146
|
+
<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"/>
|
|
147
|
+
</svg>
|
|
148
|
+
</button>
|
|
149
|
+
</div>
|
|
150
|
+
<div class="stack-progress-body">
|
|
151
|
+
<!-- Rebuilt dynamically -->
|
|
152
|
+
</div>
|
|
153
|
+
<div class="stack-progress-footer">
|
|
154
|
+
<button class="btn btn-danger stack-progress-cancel-btn" data-action="cancel">Cancel</button>
|
|
155
|
+
<button class="btn btn-secondary stack-progress-bg-btn" data-action="background">Run in Background</button>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
`;
|
|
159
|
+
|
|
160
|
+
document.body.appendChild(overlay);
|
|
161
|
+
this.modal = overlay;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
_setupEventListeners() {
|
|
165
|
+
document.addEventListener('keydown', (e) => {
|
|
166
|
+
if (e.key === 'Escape' && this.isVisible) {
|
|
167
|
+
this.hide();
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
document.addEventListener('click', (e) => {
|
|
172
|
+
if (!this.modal) return;
|
|
173
|
+
|
|
174
|
+
const actionEl = e.target.closest('#stack-progress-modal [data-action]');
|
|
175
|
+
if (!actionEl) return;
|
|
176
|
+
|
|
177
|
+
const action = actionEl.dataset.action;
|
|
178
|
+
if (action === 'close') {
|
|
179
|
+
this.hide();
|
|
180
|
+
} else if (action === 'cancel') {
|
|
181
|
+
this.cancel();
|
|
182
|
+
} else if (action === 'background') {
|
|
183
|
+
this.runInBackground();
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Handle clicks on completed PR links
|
|
188
|
+
document.addEventListener('click', (e) => {
|
|
189
|
+
const link = e.target.closest('#stack-progress-modal .stack-progress-pr-link');
|
|
190
|
+
if (link) {
|
|
191
|
+
const href = link.dataset.href;
|
|
192
|
+
if (href) {
|
|
193
|
+
window.location.href = href;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
_rebuildBody() {
|
|
200
|
+
const body = this.modal?.querySelector('.stack-progress-body');
|
|
201
|
+
if (!body) return;
|
|
202
|
+
|
|
203
|
+
let html = '<div class="stack-progress-pr-list">';
|
|
204
|
+
|
|
205
|
+
for (const pr of this._prStatuses) {
|
|
206
|
+
html += this._renderPRRow(pr);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
html += '</div>';
|
|
210
|
+
body.innerHTML = html;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
_renderPRRow(pr) {
|
|
214
|
+
const statusIcon = this._getStatusIcon(pr.status);
|
|
215
|
+
const statusClass = `status-${pr.status}`;
|
|
216
|
+
const detailText = this._getDetailText(pr);
|
|
217
|
+
|
|
218
|
+
return `
|
|
219
|
+
<div class="stack-progress-pr-row ${statusClass}" data-pr-number="${pr.prNumber}">
|
|
220
|
+
<span class="stack-progress-status-icon ${statusClass}">${statusIcon}</span>
|
|
221
|
+
<span class="stack-progress-pr-label">
|
|
222
|
+
PR #${pr.prNumber}: ${this._escapeHtml(pr.title)}
|
|
223
|
+
</span>
|
|
224
|
+
<span class="stack-progress-pr-detail">${detailText}</span>
|
|
225
|
+
</div>
|
|
226
|
+
`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
_getStatusIcon(status) {
|
|
230
|
+
switch (status) {
|
|
231
|
+
case 'completed': return '\u2713';
|
|
232
|
+
case 'running': return '<span class="council-spinner"></span>';
|
|
233
|
+
case 'setting_up': return '<span class="council-spinner"></span>';
|
|
234
|
+
case 'pending': return '\u25CB';
|
|
235
|
+
case 'failed': return '\u2717';
|
|
236
|
+
case 'cancelled': return '\u2298';
|
|
237
|
+
default: return '\u25CB';
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
_getDetailText(pr) {
|
|
242
|
+
switch (pr.status) {
|
|
243
|
+
case 'completed':
|
|
244
|
+
return pr.suggestionsCount != null ? `${pr.suggestionsCount} suggestions` : 'Complete';
|
|
245
|
+
case 'running':
|
|
246
|
+
return 'Analyzing...';
|
|
247
|
+
case 'setting_up':
|
|
248
|
+
return 'Setting up...';
|
|
249
|
+
case 'pending':
|
|
250
|
+
return 'Pending';
|
|
251
|
+
case 'failed':
|
|
252
|
+
return pr.error ? this._escapeHtml(pr.error) : 'Failed';
|
|
253
|
+
case 'cancelled':
|
|
254
|
+
return 'Cancelled';
|
|
255
|
+
default:
|
|
256
|
+
return '';
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
_updatePRRow(prNumber) {
|
|
261
|
+
const pr = this._prStatuses.find(p => p.prNumber === prNumber);
|
|
262
|
+
if (!pr) return;
|
|
263
|
+
|
|
264
|
+
const row = this.modal?.querySelector(`.stack-progress-pr-row[data-pr-number="${prNumber}"]`);
|
|
265
|
+
if (!row) return;
|
|
266
|
+
|
|
267
|
+
const iconEl = row.querySelector('.stack-progress-status-icon');
|
|
268
|
+
const detailEl = row.querySelector('.stack-progress-pr-detail');
|
|
269
|
+
|
|
270
|
+
// Update status class
|
|
271
|
+
row.className = `stack-progress-pr-row status-${pr.status}`;
|
|
272
|
+
|
|
273
|
+
// Update icon
|
|
274
|
+
if (iconEl) {
|
|
275
|
+
iconEl.className = `stack-progress-status-icon status-${pr.status}`;
|
|
276
|
+
iconEl.innerHTML = this._getStatusIcon(pr.status);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Update detail text
|
|
280
|
+
if (detailEl) {
|
|
281
|
+
detailEl.innerHTML = this._getDetailText(pr);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Make completed PRs clickable links
|
|
285
|
+
if (pr.status === 'completed' && this.owner && this.repo) {
|
|
286
|
+
const labelEl = row.querySelector('.stack-progress-pr-label');
|
|
287
|
+
if (labelEl && !labelEl.classList.contains('stack-progress-pr-link')) {
|
|
288
|
+
const href = `/pr/${this.owner}/${this.repo}/${prNumber}`;
|
|
289
|
+
labelEl.classList.add('stack-progress-pr-link');
|
|
290
|
+
labelEl.dataset.href = href;
|
|
291
|
+
labelEl.title = 'Click to view this PR\'s review';
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
_updateFooter(overallStatus) {
|
|
297
|
+
const cancelBtn = this.modal?.querySelector('.stack-progress-cancel-btn');
|
|
298
|
+
const bgBtn = this.modal?.querySelector('.stack-progress-bg-btn');
|
|
299
|
+
|
|
300
|
+
if (!cancelBtn || !bgBtn) return;
|
|
301
|
+
|
|
302
|
+
const isTerminal = ['completed', 'failed', 'cancelled'].includes(overallStatus);
|
|
303
|
+
|
|
304
|
+
if (isTerminal) {
|
|
305
|
+
cancelBtn.style.display = 'none';
|
|
306
|
+
bgBtn.textContent = 'Close';
|
|
307
|
+
bgBtn.dataset.action = 'close';
|
|
308
|
+
bgBtn.className = 'btn btn-secondary stack-progress-bg-btn';
|
|
309
|
+
} else {
|
|
310
|
+
cancelBtn.style.display = '';
|
|
311
|
+
bgBtn.textContent = 'Run in Background';
|
|
312
|
+
bgBtn.dataset.action = 'background';
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
// WebSocket monitoring
|
|
318
|
+
// ---------------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
_startMonitoring() {
|
|
321
|
+
this._stopMonitoring();
|
|
322
|
+
if (!this.stackAnalysisId) return;
|
|
323
|
+
|
|
324
|
+
window.wsClient.connect();
|
|
325
|
+
|
|
326
|
+
// Subscribe to stack-level progress
|
|
327
|
+
this._wsStackUnsub = window.wsClient.subscribe(
|
|
328
|
+
`stack-analysis:${this.stackAnalysisId}`,
|
|
329
|
+
(msg) => this._handleStackProgress(msg)
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
// Fetch initial status via HTTP (covers startup race)
|
|
333
|
+
this._fetchStatus();
|
|
334
|
+
|
|
335
|
+
// Listen for reconnects
|
|
336
|
+
this._onReconnect = () => { this._fetchStatus(); };
|
|
337
|
+
window.addEventListener('wsReconnected', this._onReconnect);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
_stopMonitoring() {
|
|
341
|
+
if (this._wsStackUnsub) {
|
|
342
|
+
this._wsStackUnsub();
|
|
343
|
+
this._wsStackUnsub = null;
|
|
344
|
+
}
|
|
345
|
+
if (this._wsAnalysisUnsubs) {
|
|
346
|
+
for (const unsub of this._wsAnalysisUnsubs.values()) {
|
|
347
|
+
unsub();
|
|
348
|
+
}
|
|
349
|
+
this._wsAnalysisUnsubs.clear();
|
|
350
|
+
}
|
|
351
|
+
if (this._onReconnect) {
|
|
352
|
+
window.removeEventListener('wsReconnected', this._onReconnect);
|
|
353
|
+
this._onReconnect = null;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
_handleStackProgress(msg) {
|
|
358
|
+
if (msg.type !== 'stack-progress') return;
|
|
359
|
+
|
|
360
|
+
// Update internal state from the server message
|
|
361
|
+
if (msg.prStatuses && Array.isArray(msg.prStatuses)) {
|
|
362
|
+
for (const serverPR of msg.prStatuses) {
|
|
363
|
+
const local = this._prStatuses.find(p => p.prNumber === serverPR.prNumber);
|
|
364
|
+
if (local) {
|
|
365
|
+
local.status = serverPR.status || local.status;
|
|
366
|
+
local.analysisId = serverPR.analysisId || local.analysisId;
|
|
367
|
+
if (serverPR.suggestionsCount != null) {
|
|
368
|
+
local.suggestionsCount = serverPR.suggestionsCount;
|
|
369
|
+
}
|
|
370
|
+
if (serverPR.error) {
|
|
371
|
+
local.error = serverPR.error;
|
|
372
|
+
}
|
|
373
|
+
this._updatePRRow(local.prNumber);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Track per-PR analysis subscriptions for all running PRs
|
|
379
|
+
this._subscribeToRunningPRs(msg.prStatuses);
|
|
380
|
+
|
|
381
|
+
// Update footer based on overall status
|
|
382
|
+
this._updateFooter(msg.status || 'running');
|
|
383
|
+
|
|
384
|
+
// Handle terminal states
|
|
385
|
+
if (['completed', 'failed', 'cancelled'].includes(msg.status)) {
|
|
386
|
+
this.isRunningInBackground = false;
|
|
387
|
+
this._stopMonitoring();
|
|
388
|
+
if (this._onComplete) {
|
|
389
|
+
this._onComplete(msg.status);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Subscribe to analysis WebSocket topics for all currently running PRs,
|
|
396
|
+
* so we can show inline level progress for each.
|
|
397
|
+
*/
|
|
398
|
+
_subscribeToRunningPRs(prStatuses) {
|
|
399
|
+
if (!prStatuses || !window.wsClient) return;
|
|
400
|
+
|
|
401
|
+
const runningPRs = prStatuses.filter(p => p.status === 'running' && p.analysisId);
|
|
402
|
+
const runningAnalysisIds = new Set(runningPRs.map(p => p.analysisId));
|
|
403
|
+
|
|
404
|
+
// Unsubscribe from analyses no longer running
|
|
405
|
+
for (const [analysisId, unsub] of this._wsAnalysisUnsubs) {
|
|
406
|
+
if (!runningAnalysisIds.has(analysisId)) {
|
|
407
|
+
unsub();
|
|
408
|
+
this._wsAnalysisUnsubs.delete(analysisId);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Subscribe to new running analyses
|
|
413
|
+
for (const pr of runningPRs) {
|
|
414
|
+
if (!this._wsAnalysisUnsubs.has(pr.analysisId)) {
|
|
415
|
+
const unsub = window.wsClient.subscribe(
|
|
416
|
+
`analysis:${pr.analysisId}`,
|
|
417
|
+
(msg) => this._handleAnalysisProgress(pr.prNumber, msg)
|
|
418
|
+
);
|
|
419
|
+
this._wsAnalysisUnsubs.set(pr.analysisId, unsub);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Handle individual PR analysis progress (placeholder for future detail).
|
|
426
|
+
* Level-by-level detail (L1/L2/L3) was removed as it cluttered the UI
|
|
427
|
+
* without adding value in the stack context.
|
|
428
|
+
*/
|
|
429
|
+
_handleAnalysisProgress(_prNumber, _msg) {
|
|
430
|
+
// No-op: the stack-level progress handler already shows status per PR.
|
|
431
|
+
// Per-analysis subscriptions are kept for potential future use (e.g. %).
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Fetch stack analysis status via HTTP for initial state / reconnect.
|
|
436
|
+
*/
|
|
437
|
+
_fetchStatus() {
|
|
438
|
+
if (!this.stackAnalysisId) return;
|
|
439
|
+
|
|
440
|
+
fetch(`/api/analyses/stack/${this.stackAnalysisId}`)
|
|
441
|
+
.then(r => r.ok ? r.json() : null)
|
|
442
|
+
.then(data => {
|
|
443
|
+
if (data) {
|
|
444
|
+
this._handleStackProgress({
|
|
445
|
+
type: 'stack-progress',
|
|
446
|
+
...data
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
})
|
|
450
|
+
.catch(err => {
|
|
451
|
+
console.warn('Failed to fetch stack analysis status:', err);
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// ---------------------------------------------------------------------------
|
|
456
|
+
// Utilities
|
|
457
|
+
// ---------------------------------------------------------------------------
|
|
458
|
+
|
|
459
|
+
_escapeHtml(str) {
|
|
460
|
+
if (!str) return '';
|
|
461
|
+
const div = document.createElement('div');
|
|
462
|
+
div.textContent = str;
|
|
463
|
+
return div.innerHTML;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Export for use in other modules
|
|
468
|
+
if (typeof window !== 'undefined') {
|
|
469
|
+
window.StackProgressModal = StackProgressModal;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Export for Node.js/test environments
|
|
473
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
474
|
+
module.exports = { StackProgressModal };
|
|
475
|
+
}
|
|
@@ -110,6 +110,7 @@ class StatusIndicator {
|
|
|
110
110
|
* @param {string} message - Completion message
|
|
111
111
|
*/
|
|
112
112
|
showComplete(message = 'Analysis complete') {
|
|
113
|
+
if (window.notificationSounds) window.notificationSounds.playIfEnabled('analysis');
|
|
113
114
|
this.updateText(message);
|
|
114
115
|
this.showCheckmark();
|
|
115
116
|
this.stopDotsAnimation();
|
|
@@ -371,6 +371,8 @@ class SuggestionNavigator {
|
|
|
371
371
|
if (suggestionEl) {
|
|
372
372
|
const minimizer = window.prManager?.commentMinimizer;
|
|
373
373
|
if (minimizer?.active) {
|
|
374
|
+
// Expand file-level comments so the target becomes visible
|
|
375
|
+
minimizer.expandForElement(suggestionEl);
|
|
374
376
|
// Comments are minimized — scroll to the parent diff line instead
|
|
375
377
|
const diffRow = minimizer.findDiffRowFor(suggestionEl);
|
|
376
378
|
if (diffRow) {
|
|
@@ -503,8 +503,15 @@ class CommentManager {
|
|
|
503
503
|
// Refresh minimize-mode indicators so the new comment is reflected
|
|
504
504
|
if (window.prManager?.commentMinimizer) {
|
|
505
505
|
window.prManager.commentMinimizer.refreshIndicators();
|
|
506
|
+
// Auto-expand so the new comment stays visible in minimize mode
|
|
507
|
+
const newRow = document.querySelector(`.user-comment-row[data-comment-id="${commentData.id}"]`);
|
|
508
|
+
if (newRow) {
|
|
509
|
+
window.prManager.commentMinimizer.expandForElement(newRow);
|
|
510
|
+
}
|
|
506
511
|
}
|
|
507
512
|
|
|
513
|
+
window.chatPanel?.queueUserActionHint(`[User Action: created comment ${result.commentId}]`);
|
|
514
|
+
|
|
508
515
|
} catch (error) {
|
|
509
516
|
console.error('Error saving comment:', error);
|
|
510
517
|
alert('Failed to save comment');
|