@in-the-loop-labs/pair-review 1.4.3 → 1.5.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/skills/review-requests/SKILL.md +54 -0
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/public/css/pr.css +1081 -54
- package/public/css/repo-settings.css +452 -140
- package/public/js/components/AdvancedConfigTab.js +1364 -0
- package/public/js/components/AnalysisConfigModal.js +488 -112
- package/public/js/components/CouncilProgressModal.js +1416 -0
- package/public/js/components/TextInputDialog.js +231 -0
- package/public/js/components/TimeoutSelect.js +367 -0
- package/public/js/components/VoiceCentricConfigTab.js +1334 -0
- package/public/js/local.js +162 -83
- package/public/js/modules/analysis-history.js +185 -11
- package/public/js/modules/comment-manager.js +13 -0
- package/public/js/modules/file-comment-manager.js +28 -0
- package/public/js/pr.js +233 -115
- package/public/js/repo-settings.js +575 -106
- package/public/local.html +11 -1
- package/public/pr.html +6 -1
- package/public/repo-settings.html +28 -21
- package/public/setup.html +8 -2
- package/src/ai/analyzer.js +1262 -111
- package/src/ai/claude-cli.js +2 -2
- package/src/ai/claude-provider.js +6 -6
- package/src/ai/codex-provider.js +6 -6
- package/src/ai/copilot-provider.js +3 -3
- package/src/ai/cursor-agent-provider.js +6 -6
- package/src/ai/gemini-provider.js +6 -6
- package/src/ai/opencode-provider.js +6 -6
- package/src/ai/pi-provider.js +6 -6
- package/src/ai/prompts/baseline/consolidation/balanced.js +208 -0
- package/src/ai/prompts/baseline/consolidation/fast.js +175 -0
- package/src/ai/prompts/baseline/consolidation/thorough.js +283 -0
- package/src/ai/prompts/config.js +1 -1
- package/src/ai/prompts/index.js +26 -2
- package/src/ai/provider.js +4 -2
- package/src/database.js +417 -14
- package/src/main.js +1 -1
- package/src/routes/analysis.js +495 -10
- package/src/routes/config.js +36 -15
- package/src/routes/councils.js +351 -0
- package/src/routes/local.js +33 -11
- package/src/routes/mcp.js +9 -2
- package/src/routes/setup.js +12 -2
- package/src/routes/shared.js +126 -13
- package/src/server.js +34 -4
- package/src/utils/stats-calculator.js +2 -0
|
@@ -346,6 +346,14 @@ class CommentManager {
|
|
|
346
346
|
return;
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
// Prevent duplicate saves from rapid clicks or Cmd+Enter
|
|
350
|
+
const saveBtn = formRow?.querySelector('.save-comment-btn');
|
|
351
|
+
if (saveBtn?.dataset.saving === 'true') {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
if (saveBtn) saveBtn.dataset.saving = 'true';
|
|
355
|
+
if (saveBtn) saveBtn.disabled = true;
|
|
356
|
+
|
|
349
357
|
try {
|
|
350
358
|
const reviewId = this.prManager?.currentPR?.id;
|
|
351
359
|
const headSha = this.prManager?.currentPR?.head_sha;
|
|
@@ -407,6 +415,11 @@ class CommentManager {
|
|
|
407
415
|
} catch (error) {
|
|
408
416
|
console.error('Error saving comment:', error);
|
|
409
417
|
alert('Failed to save comment');
|
|
418
|
+
// Re-enable save button on failure so the user can retry
|
|
419
|
+
if (saveBtn) {
|
|
420
|
+
saveBtn.dataset.saving = 'false';
|
|
421
|
+
saveBtn.disabled = false;
|
|
422
|
+
}
|
|
410
423
|
}
|
|
411
424
|
}
|
|
412
425
|
|
|
@@ -243,6 +243,15 @@ class FileCommentManager {
|
|
|
243
243
|
* @param {string} body - The comment body
|
|
244
244
|
*/
|
|
245
245
|
async saveFileComment(zone, fileName, body) {
|
|
246
|
+
// Prevent duplicate saves from rapid clicks or Cmd+Enter
|
|
247
|
+
const container = zone.querySelector('.file-comments-container');
|
|
248
|
+
const submitBtn = container?.querySelector('.file-comment-form .submit-btn');
|
|
249
|
+
if (submitBtn?.dataset.saving === 'true') {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
if (submitBtn) submitBtn.dataset.saving = 'true';
|
|
253
|
+
if (submitBtn) submitBtn.disabled = true;
|
|
254
|
+
|
|
246
255
|
try {
|
|
247
256
|
const { endpoint, requestBody } = this._getFileCommentEndpoint('create', {
|
|
248
257
|
file: fileName,
|
|
@@ -297,6 +306,11 @@ class FileCommentManager {
|
|
|
297
306
|
if (window.toast) {
|
|
298
307
|
window.toast.showError('Failed to save file-level comment');
|
|
299
308
|
}
|
|
309
|
+
// Re-enable save button on failure so the user can retry
|
|
310
|
+
if (submitBtn) {
|
|
311
|
+
submitBtn.dataset.saving = 'false';
|
|
312
|
+
submitBtn.disabled = false;
|
|
313
|
+
}
|
|
300
314
|
}
|
|
301
315
|
}
|
|
302
316
|
|
|
@@ -910,6 +924,15 @@ class FileCommentManager {
|
|
|
910
924
|
* @param {HTMLElement} bodyEl - The body element to update
|
|
911
925
|
*/
|
|
912
926
|
async saveEditedComment(zone, commentId, newBody, bodyEl) {
|
|
927
|
+
// Prevent duplicate saves from rapid clicks or Cmd+Enter
|
|
928
|
+
const editForm = bodyEl?.closest('.file-comment-card')?.querySelector('.file-comment-edit-form');
|
|
929
|
+
const saveBtn = editForm?.querySelector('.submit-btn');
|
|
930
|
+
if (saveBtn?.dataset.saving === 'true') {
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
if (saveBtn) saveBtn.dataset.saving = 'true';
|
|
934
|
+
if (saveBtn) saveBtn.disabled = true;
|
|
935
|
+
|
|
913
936
|
try {
|
|
914
937
|
const { endpoint, requestBody } = this._getFileCommentEndpoint('update', {
|
|
915
938
|
commentId: commentId,
|
|
@@ -936,6 +959,11 @@ class FileCommentManager {
|
|
|
936
959
|
if (window.toast) {
|
|
937
960
|
window.toast.showError('Failed to update comment');
|
|
938
961
|
}
|
|
962
|
+
// Re-enable save button on failure so the user can retry
|
|
963
|
+
if (saveBtn) {
|
|
964
|
+
saveBtn.dataset.saving = 'false';
|
|
965
|
+
saveBtn.disabled = false;
|
|
966
|
+
}
|
|
939
967
|
}
|
|
940
968
|
}
|
|
941
969
|
|
package/public/js/pr.js
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
* - CommentManager: Comment forms and editing
|
|
9
9
|
* - SuggestionManager: AI suggestion handling
|
|
10
10
|
*/
|
|
11
|
+
// Timeout (ms) for stale check — git commands can hang on locked repos
|
|
12
|
+
const STALE_TIMEOUT = 2000;
|
|
13
|
+
|
|
11
14
|
class PRManager {
|
|
12
15
|
// Forward static constants from modules for backward compatibility
|
|
13
16
|
static get CATEGORY_EMOJI_MAP() {
|
|
@@ -273,7 +276,12 @@ class PRManager {
|
|
|
273
276
|
const pathMatch = window.location.pathname.match(/^\/pr\/([^\/]+)\/([^\/]+)\/(\d+)$/);
|
|
274
277
|
if (pathMatch) {
|
|
275
278
|
const [, owner, repo, number] = pathMatch;
|
|
276
|
-
|
|
279
|
+
const prNumber = parseInt(number);
|
|
280
|
+
await this.loadPR(owner, repo, prNumber);
|
|
281
|
+
|
|
282
|
+
// Auto-trigger analysis if ?analyze=true is present
|
|
283
|
+
await this._maybeAutoAnalyze(owner, repo, prNumber);
|
|
284
|
+
|
|
277
285
|
return;
|
|
278
286
|
}
|
|
279
287
|
|
|
@@ -292,14 +300,40 @@ class PRManager {
|
|
|
292
300
|
throw new Error('Invalid PR reference format. Expected: owner/repo/number');
|
|
293
301
|
}
|
|
294
302
|
|
|
295
|
-
const [owner, repo,
|
|
296
|
-
|
|
303
|
+
const [owner, repo, numberStr] = parts;
|
|
304
|
+
const prNumber = parseInt(numberStr);
|
|
305
|
+
await this.loadPR(owner, repo, prNumber);
|
|
306
|
+
|
|
307
|
+
// Auto-trigger analysis if ?analyze=true is present
|
|
308
|
+
await this._maybeAutoAnalyze(owner, repo, prNumber);
|
|
297
309
|
} catch (error) {
|
|
298
310
|
console.error('Error initializing PR viewer:', error);
|
|
299
311
|
this.showError(error.message);
|
|
300
312
|
}
|
|
301
313
|
}
|
|
302
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Auto-trigger analysis if ?analyze=true is present in the URL.
|
|
317
|
+
* Cleans up the query parameter afterwards regardless of success or failure.
|
|
318
|
+
* @param {string} owner - Repository owner
|
|
319
|
+
* @param {string} repo - Repository name
|
|
320
|
+
* @param {number} prNumber - PR number
|
|
321
|
+
*/
|
|
322
|
+
async _maybeAutoAnalyze(owner, repo, prNumber) {
|
|
323
|
+
const autoAnalyze = new URLSearchParams(window.location.search).get('analyze');
|
|
324
|
+
if (autoAnalyze === 'true' && !this.isAnalyzing) {
|
|
325
|
+
this._autoAnalyzeRequested = true;
|
|
326
|
+
try {
|
|
327
|
+
await this.startAnalysis(owner, repo, prNumber, null, {});
|
|
328
|
+
} finally {
|
|
329
|
+
this._autoAnalyzeRequested = false;
|
|
330
|
+
const cleanUrl = new URL(window.location);
|
|
331
|
+
cleanUrl.searchParams.delete('analyze');
|
|
332
|
+
history.replaceState(null, '', cleanUrl);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
303
337
|
/**
|
|
304
338
|
* Load PR data from the API
|
|
305
339
|
* @param {string} owner - Repository owner
|
|
@@ -1837,6 +1871,15 @@ class PRManager {
|
|
|
1837
1871
|
* Save edited user comment
|
|
1838
1872
|
*/
|
|
1839
1873
|
async saveEditedUserComment(commentId) {
|
|
1874
|
+
// Prevent duplicate saves from rapid clicks or Cmd+Enter
|
|
1875
|
+
const editForm = document.querySelector(`#edit-comment-${commentId}`)?.closest('.user-comment-edit-form');
|
|
1876
|
+
const saveBtn = editForm?.querySelector('.save-edit-btn');
|
|
1877
|
+
if (saveBtn?.dataset.saving === 'true') {
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
if (saveBtn) saveBtn.dataset.saving = 'true';
|
|
1881
|
+
if (saveBtn) saveBtn.disabled = true;
|
|
1882
|
+
|
|
1840
1883
|
try {
|
|
1841
1884
|
const textarea = document.getElementById(`edit-comment-${commentId}`);
|
|
1842
1885
|
const editedText = textarea.value.trim();
|
|
@@ -1844,6 +1887,10 @@ class PRManager {
|
|
|
1844
1887
|
if (!editedText) {
|
|
1845
1888
|
alert('Comment cannot be empty');
|
|
1846
1889
|
textarea.focus();
|
|
1890
|
+
if (saveBtn) {
|
|
1891
|
+
saveBtn.dataset.saving = 'false';
|
|
1892
|
+
saveBtn.disabled = false;
|
|
1893
|
+
}
|
|
1847
1894
|
return;
|
|
1848
1895
|
}
|
|
1849
1896
|
|
|
@@ -1858,7 +1905,7 @@ class PRManager {
|
|
|
1858
1905
|
const commentRow = document.querySelector(`[data-comment-id="${commentId}"]`);
|
|
1859
1906
|
const commentDiv = commentRow.querySelector('.user-comment');
|
|
1860
1907
|
let bodyDiv = commentDiv.querySelector('.user-comment-body');
|
|
1861
|
-
const
|
|
1908
|
+
const editFormEl = commentDiv.querySelector('.user-comment-edit-form');
|
|
1862
1909
|
|
|
1863
1910
|
if (!bodyDiv) {
|
|
1864
1911
|
bodyDiv = document.createElement('div');
|
|
@@ -1870,7 +1917,7 @@ class PRManager {
|
|
|
1870
1917
|
bodyDiv.dataset.originalMarkdown = editedText;
|
|
1871
1918
|
bodyDiv.style.display = '';
|
|
1872
1919
|
|
|
1873
|
-
if (
|
|
1920
|
+
if (editFormEl) editFormEl.remove();
|
|
1874
1921
|
commentDiv.classList.remove('editing-mode');
|
|
1875
1922
|
|
|
1876
1923
|
const timestamp = commentDiv.querySelector('.user-comment-timestamp');
|
|
@@ -1884,6 +1931,11 @@ class PRManager {
|
|
|
1884
1931
|
} catch (error) {
|
|
1885
1932
|
console.error('Error saving comment:', error);
|
|
1886
1933
|
alert('Failed to save comment');
|
|
1934
|
+
// Re-enable save button on failure so the user can retry
|
|
1935
|
+
if (saveBtn) {
|
|
1936
|
+
saveBtn.dataset.saving = 'false';
|
|
1937
|
+
saveBtn.disabled = false;
|
|
1938
|
+
}
|
|
1887
1939
|
}
|
|
1888
1940
|
}
|
|
1889
1941
|
|
|
@@ -3346,8 +3398,16 @@ class PRManager {
|
|
|
3346
3398
|
// Set button to analyzing state
|
|
3347
3399
|
this.setButtonAnalyzing(data.analysisId);
|
|
3348
3400
|
|
|
3349
|
-
// Show
|
|
3350
|
-
if (window.
|
|
3401
|
+
// Show the appropriate progress modal
|
|
3402
|
+
if (data.status?.isCouncil && window.councilProgressModal && data.status?.councilConfig) {
|
|
3403
|
+
window.councilProgressModal.setPRMode();
|
|
3404
|
+
window.councilProgressModal.show(
|
|
3405
|
+
data.analysisId,
|
|
3406
|
+
data.status.councilConfig,
|
|
3407
|
+
null,
|
|
3408
|
+
{ configType: data.status.configType || 'advanced' }
|
|
3409
|
+
);
|
|
3410
|
+
} else if (window.progressModal) {
|
|
3351
3411
|
window.progressModal.show(data.analysisId);
|
|
3352
3412
|
} else {
|
|
3353
3413
|
console.warn('Progress modal not yet initialized');
|
|
@@ -3363,7 +3423,15 @@ class PRManager {
|
|
|
3363
3423
|
* Reopen progress modal when button is clicked during analysis
|
|
3364
3424
|
*/
|
|
3365
3425
|
reopenProgressModal() {
|
|
3366
|
-
if (this.currentAnalysisId
|
|
3426
|
+
if (!this.currentAnalysisId) return;
|
|
3427
|
+
|
|
3428
|
+
// If the council modal was used for this analysis, reopen it
|
|
3429
|
+
if (window.councilProgressModal && window.councilProgressModal.currentAnalysisId === this.currentAnalysisId) {
|
|
3430
|
+
window.councilProgressModal.reopenFromBackground();
|
|
3431
|
+
return;
|
|
3432
|
+
}
|
|
3433
|
+
|
|
3434
|
+
if (window.progressModal) {
|
|
3367
3435
|
window.progressModal.show(this.currentAnalysisId);
|
|
3368
3436
|
}
|
|
3369
3437
|
}
|
|
@@ -3393,23 +3461,26 @@ class PRManager {
|
|
|
3393
3461
|
}
|
|
3394
3462
|
|
|
3395
3463
|
/**
|
|
3396
|
-
* Fetch last
|
|
3397
|
-
* @returns {Promise<string>} Last
|
|
3464
|
+
* Fetch last review settings (custom instructions and council ID) from review record
|
|
3465
|
+
* @returns {Promise<{custom_instructions: string, last_council_id: string|null}>} Last review settings
|
|
3398
3466
|
*/
|
|
3399
|
-
async
|
|
3400
|
-
if (!this.currentPR) return '';
|
|
3467
|
+
async fetchLastReviewSettings() {
|
|
3468
|
+
if (!this.currentPR) return { custom_instructions: '', last_council_id: null };
|
|
3401
3469
|
|
|
3402
3470
|
const { owner, repo, number } = this.currentPR;
|
|
3403
3471
|
try {
|
|
3404
3472
|
const response = await fetch(`/api/pr/${owner}/${repo}/${number}/review-settings`);
|
|
3405
3473
|
if (!response.ok) {
|
|
3406
|
-
return '';
|
|
3474
|
+
return { custom_instructions: '', last_council_id: null };
|
|
3407
3475
|
}
|
|
3408
3476
|
const data = await response.json();
|
|
3409
|
-
return
|
|
3477
|
+
return {
|
|
3478
|
+
custom_instructions: data.custom_instructions || '',
|
|
3479
|
+
last_council_id: data.last_council_id || null
|
|
3480
|
+
};
|
|
3410
3481
|
} catch (error) {
|
|
3411
3482
|
console.warn('Error fetching last custom instructions:', error);
|
|
3412
|
-
return '';
|
|
3483
|
+
return { custom_instructions: '', last_council_id: null };
|
|
3413
3484
|
}
|
|
3414
3485
|
}
|
|
3415
3486
|
|
|
@@ -3438,118 +3509,105 @@ class PRManager {
|
|
|
3438
3509
|
}
|
|
3439
3510
|
|
|
3440
3511
|
try {
|
|
3441
|
-
// Run stale check and settings fetch in parallel to minimize dialog delay.
|
|
3442
|
-
// Use AbortController so the fetch is truly cancelled on timeout,
|
|
3443
|
-
// freeing the HTTP connection for subsequent requests.
|
|
3444
|
-
const STALE_TIMEOUT = 2000;
|
|
3445
|
-
const staleAbort = new AbortController();
|
|
3446
|
-
const staleTimer = setTimeout(() => {
|
|
3447
|
-
console.debug(`[Analyze] stale-check timed out after ${STALE_TIMEOUT}ms, aborting`);
|
|
3448
|
-
staleAbort.abort();
|
|
3449
|
-
}, STALE_TIMEOUT);
|
|
3450
|
-
const staleCheckPromise = fetch(`/api/pr/${owner}/${repo}/${number}/check-stale`, { signal: staleAbort.signal })
|
|
3451
|
-
.then(r => {
|
|
3452
|
-
clearTimeout(staleTimer);
|
|
3453
|
-
if (!r.ok) {
|
|
3454
|
-
console.warn(`Stale check failed with status ${r.status}`);
|
|
3455
|
-
return { _fetchError: true, status: r.status };
|
|
3456
|
-
}
|
|
3457
|
-
return r.json();
|
|
3458
|
-
})
|
|
3459
|
-
.catch(err => {
|
|
3460
|
-
clearTimeout(staleTimer);
|
|
3461
|
-
if (err.name === 'AbortError') {
|
|
3462
|
-
console.debug('[Analyze] stale-check aborted (timeout)');
|
|
3463
|
-
} else {
|
|
3464
|
-
console.warn('[Analyze] stale-check fetch error:', err);
|
|
3465
|
-
}
|
|
3466
|
-
return null;
|
|
3467
|
-
});
|
|
3468
|
-
|
|
3469
3512
|
// Show analysis config modal
|
|
3470
3513
|
if (!this.analysisConfigModal) {
|
|
3471
|
-
clearTimeout(staleTimer);
|
|
3472
3514
|
console.warn('AnalysisConfigModal not initialized, proceeding without config');
|
|
3473
3515
|
await this.startAnalysis(owner, repo, number, btn, {});
|
|
3474
3516
|
return;
|
|
3475
3517
|
}
|
|
3476
3518
|
|
|
3477
|
-
//
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
|
|
3481
|
-
|
|
3519
|
+
// Run stale check and settings fetch in parallel to minimize dialog delay
|
|
3520
|
+
// Use AbortController so the fetch is truly cancelled on timeout,
|
|
3521
|
+
// freeing the HTTP connection for subsequent requests.
|
|
3522
|
+
const _tParallel0 = performance.now();
|
|
3523
|
+
const staleAbort = new AbortController();
|
|
3524
|
+
const staleTimer = setTimeout(() => {
|
|
3525
|
+
console.debug(`[Analyze] stale-check timed out after ${STALE_TIMEOUT}ms, aborting`);
|
|
3526
|
+
staleAbort.abort();
|
|
3527
|
+
}, STALE_TIMEOUT);
|
|
3528
|
+
const staleCheckWithTimeout = fetch(`/api/pr/${owner}/${repo}/${number}/check-stale`, { signal: staleAbort.signal })
|
|
3529
|
+
.then(r => r.ok ? r.json() : null)
|
|
3530
|
+
.then(result => { clearTimeout(staleTimer); return result; })
|
|
3531
|
+
.catch(() => { clearTimeout(staleTimer); return null; });
|
|
3532
|
+
|
|
3533
|
+
const [staleResult, repoSettings, reviewSettings] = await Promise.all([
|
|
3534
|
+
staleCheckWithTimeout,
|
|
3535
|
+
this.fetchRepoSettings(),
|
|
3536
|
+
this.fetchLastReviewSettings()
|
|
3482
3537
|
]);
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3538
|
+
console.debug(`[Analyze] parallel-fetch (stale+settings): ${Math.round(performance.now() - _tParallel0)}ms`);
|
|
3539
|
+
|
|
3540
|
+
// Handle staleness result — check for expected properties to distinguish
|
|
3541
|
+
// a valid response from a failed/timed-out fetch (which resolves to null)
|
|
3542
|
+
if (staleResult && 'isStale' in staleResult) {
|
|
3543
|
+
// Handle PR state - show info for closed/merged PRs
|
|
3544
|
+
if (staleResult.prState && (staleResult.prState !== 'open' || staleResult.merged)) {
|
|
3545
|
+
const stateLabel = staleResult.merged ? 'merged' : 'closed';
|
|
3488
3546
|
if (window.toast) {
|
|
3489
|
-
window.toast.showWarning(
|
|
3547
|
+
window.toast.showWarning(`This PR is ${stateLabel}. Analysis will proceed on the existing data.`);
|
|
3490
3548
|
}
|
|
3491
|
-
}
|
|
3549
|
+
}
|
|
3550
|
+
|
|
3551
|
+
if (staleResult.isStale === null) {
|
|
3492
3552
|
if (window.toast) {
|
|
3493
|
-
window.toast.showWarning(
|
|
3494
|
-
}
|
|
3495
|
-
} else {
|
|
3496
|
-
// Handle PR state - show info for closed/merged PRs but still allow analysis
|
|
3497
|
-
if (staleResult.prState && (staleResult.prState !== 'open' || staleResult.merged)) {
|
|
3498
|
-
const stateLabel = staleResult.merged ? 'merged' : 'closed';
|
|
3499
|
-
if (window.toast) {
|
|
3500
|
-
window.toast.showWarning(`This PR is ${stateLabel}. Analysis will proceed on the existing data.`);
|
|
3501
|
-
}
|
|
3553
|
+
window.toast.showWarning('Could not verify PR is current. Proceeding with analysis.');
|
|
3502
3554
|
}
|
|
3555
|
+
} else if (staleResult.isStale === true) {
|
|
3556
|
+
if (window.confirmDialog) {
|
|
3557
|
+
const choice = await window.confirmDialog.show({
|
|
3558
|
+
title: 'PR Has New Commits',
|
|
3559
|
+
message: 'This pull request has new commits since you last loaded it. What would you like to do?',
|
|
3560
|
+
confirmText: 'Refresh & Analyze',
|
|
3561
|
+
confirmClass: 'btn-primary',
|
|
3562
|
+
secondaryText: 'Analyze Anyway',
|
|
3563
|
+
secondaryClass: 'btn-warning'
|
|
3564
|
+
});
|
|
3503
3565
|
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
} else if (staleResult.isStale === true) {
|
|
3509
|
-
if (!window.confirmDialog) {
|
|
3510
|
-
console.warn('ConfirmDialog not available for stale PR check');
|
|
3511
|
-
} else {
|
|
3512
|
-
const choice = await window.confirmDialog.show({
|
|
3513
|
-
title: 'PR Has New Commits',
|
|
3514
|
-
message: 'This pull request has new commits since you last loaded it. What would you like to do?',
|
|
3515
|
-
confirmText: 'Refresh & Analyze',
|
|
3516
|
-
confirmClass: 'btn-primary',
|
|
3517
|
-
secondaryText: 'Analyze Anyway',
|
|
3518
|
-
secondaryClass: 'btn-warning'
|
|
3519
|
-
});
|
|
3520
|
-
|
|
3521
|
-
if (choice === 'confirm') {
|
|
3522
|
-
await this.refreshPR();
|
|
3523
|
-
} else if (choice === 'secondary') {
|
|
3524
|
-
// User chose to analyze anyway
|
|
3525
|
-
} else {
|
|
3526
|
-
return;
|
|
3527
|
-
}
|
|
3566
|
+
if (choice === 'confirm') {
|
|
3567
|
+
await this.refreshPR();
|
|
3568
|
+
} else if (choice !== 'secondary') {
|
|
3569
|
+
return;
|
|
3528
3570
|
}
|
|
3529
3571
|
}
|
|
3530
3572
|
}
|
|
3531
|
-
}
|
|
3532
|
-
|
|
3573
|
+
} else if (!staleResult) {
|
|
3574
|
+
// Network error, HTTP error, or timeout — fail open with warning
|
|
3533
3575
|
if (window.toast) {
|
|
3534
3576
|
window.toast.showWarning('Could not verify PR is current. Proceeding with analysis.');
|
|
3535
3577
|
}
|
|
3536
3578
|
}
|
|
3537
3579
|
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
const
|
|
3542
|
-
const
|
|
3543
|
-
|
|
3544
|
-
|
|
3580
|
+
const lastCouncilId = reviewSettings.last_council_id;
|
|
3581
|
+
|
|
3582
|
+
// Determine the model and provider to use (priority: repo default > defaults)
|
|
3583
|
+
const currentModel = repoSettings?.default_model || 'opus';
|
|
3584
|
+
const currentProvider = repoSettings?.default_provider || 'claude';
|
|
3585
|
+
|
|
3586
|
+
// Determine default tab (priority: localStorage > repo settings > 'single')
|
|
3587
|
+
const tabStorageKey = PRManager.getRepoStorageKey('pair-review-tab', owner, repo);
|
|
3588
|
+
const rememberedTab = localStorage.getItem(tabStorageKey);
|
|
3589
|
+
const defaultTab = rememberedTab || repoSettings?.default_tab || 'single';
|
|
3590
|
+
|
|
3591
|
+
// Restore custom instructions (priority: database > localStorage)
|
|
3592
|
+
const instructionsStorageKey = PRManager.getRepoStorageKey('pair-review-instructions', owner, repo);
|
|
3593
|
+
const lastInstructions = reviewSettings.custom_instructions
|
|
3594
|
+
?? localStorage.getItem(instructionsStorageKey)
|
|
3595
|
+
?? '';
|
|
3596
|
+
|
|
3597
|
+
// Save tab selection to localStorage when user switches tabs
|
|
3598
|
+
this.analysisConfigModal.onTabChange = (tabId) => {
|
|
3599
|
+
localStorage.setItem(tabStorageKey, tabId);
|
|
3600
|
+
};
|
|
3545
3601
|
|
|
3546
3602
|
// Show the config modal
|
|
3547
3603
|
const config = await this.analysisConfigModal.show({
|
|
3548
3604
|
currentModel,
|
|
3549
3605
|
currentProvider,
|
|
3606
|
+
defaultTab,
|
|
3550
3607
|
repoInstructions: repoSettings?.default_instructions || '',
|
|
3551
3608
|
lastInstructions: lastInstructions,
|
|
3552
|
-
|
|
3609
|
+
lastCouncilId,
|
|
3610
|
+
defaultCouncilId: repoSettings?.default_council_id || null
|
|
3553
3611
|
});
|
|
3554
3612
|
|
|
3555
3613
|
// If user cancelled, do nothing
|
|
@@ -3557,13 +3615,12 @@ class PRManager {
|
|
|
3557
3615
|
return;
|
|
3558
3616
|
}
|
|
3559
3617
|
|
|
3560
|
-
//
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
localStorage.setItem(
|
|
3618
|
+
// Persist custom instructions to localStorage for immediate recall on next dialog open
|
|
3619
|
+
const submittedInstructions = config.customInstructions || '';
|
|
3620
|
+
if (submittedInstructions) {
|
|
3621
|
+
localStorage.setItem(instructionsStorageKey, submittedInstructions);
|
|
3564
3622
|
} else {
|
|
3565
|
-
localStorage.removeItem(
|
|
3566
|
-
localStorage.removeItem(providerStorageKey);
|
|
3623
|
+
localStorage.removeItem(instructionsStorageKey);
|
|
3567
3624
|
}
|
|
3568
3625
|
|
|
3569
3626
|
// Start the analysis with the selected config
|
|
@@ -3610,23 +3667,43 @@ class PRManager {
|
|
|
3610
3667
|
// Always do manual DOM cleanup as backup
|
|
3611
3668
|
document.querySelectorAll('.ai-suggestion-row').forEach(row => row.remove());
|
|
3612
3669
|
|
|
3613
|
-
//
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
3619
|
-
|
|
3670
|
+
// Determine endpoint and body based on whether this is a council analysis
|
|
3671
|
+
let analyzeUrl, analyzeBody;
|
|
3672
|
+
if (config.isCouncil) {
|
|
3673
|
+
analyzeUrl = `/api/analyze/council/${owner}/${repo}/${number}`;
|
|
3674
|
+
analyzeBody = {
|
|
3675
|
+
councilId: config.councilId || undefined,
|
|
3676
|
+
councilConfig: config.councilConfig || undefined,
|
|
3677
|
+
configType: config.configType || 'advanced',
|
|
3678
|
+
customInstructions: config.customInstructions || null
|
|
3679
|
+
};
|
|
3680
|
+
} else {
|
|
3681
|
+
analyzeUrl = `/api/analyze/${owner}/${repo}/${number}`;
|
|
3682
|
+
analyzeBody = {
|
|
3620
3683
|
provider: config.provider || 'claude',
|
|
3621
3684
|
model: config.model || 'opus',
|
|
3622
3685
|
tier: config.tier || 'balanced',
|
|
3623
3686
|
customInstructions: config.customInstructions || null,
|
|
3687
|
+
enabledLevels: config.enabledLevels || [1, 2, 3],
|
|
3624
3688
|
skipLevel3: config.skipLevel3 || false
|
|
3625
|
-
}
|
|
3689
|
+
};
|
|
3690
|
+
}
|
|
3691
|
+
|
|
3692
|
+
// Start AI analysis
|
|
3693
|
+
const response = await fetch(analyzeUrl, {
|
|
3694
|
+
method: 'POST',
|
|
3695
|
+
headers: {
|
|
3696
|
+
'Content-Type': 'application/json'
|
|
3697
|
+
},
|
|
3698
|
+
body: JSON.stringify(analyzeBody)
|
|
3626
3699
|
});
|
|
3627
3700
|
|
|
3628
3701
|
if (!response.ok) {
|
|
3629
|
-
const error = await response.json();
|
|
3702
|
+
const error = await response.json().catch(() => ({}));
|
|
3703
|
+
if (response.status === 404) {
|
|
3704
|
+
this.showWorktreeNotFoundError(owner, repo, number);
|
|
3705
|
+
return;
|
|
3706
|
+
}
|
|
3630
3707
|
throw new Error(error.error || 'Failed to start AI analysis');
|
|
3631
3708
|
}
|
|
3632
3709
|
|
|
@@ -3640,7 +3717,20 @@ class PRManager {
|
|
|
3640
3717
|
// Set analyzing state and show progress modal
|
|
3641
3718
|
this.setButtonAnalyzing(result.analysisId);
|
|
3642
3719
|
|
|
3643
|
-
|
|
3720
|
+
// Always use the unified progress modal
|
|
3721
|
+
if (window.councilProgressModal) {
|
|
3722
|
+
window.councilProgressModal.setPRMode();
|
|
3723
|
+
window.councilProgressModal.show(
|
|
3724
|
+
result.analysisId,
|
|
3725
|
+
config.isCouncil ? config.councilConfig : null,
|
|
3726
|
+
config.isCouncil ? config.councilName : null,
|
|
3727
|
+
{
|
|
3728
|
+
configType: config.isCouncil ? (config.configType || 'advanced') : 'single',
|
|
3729
|
+
enabledLevels: config.enabledLevels || [1, 2, 3]
|
|
3730
|
+
}
|
|
3731
|
+
);
|
|
3732
|
+
} else if (window.progressModal) {
|
|
3733
|
+
// Fallback to old progress modal if unified modal not available
|
|
3644
3734
|
window.progressModal.show(result.analysisId);
|
|
3645
3735
|
}
|
|
3646
3736
|
|
|
@@ -3651,6 +3741,34 @@ class PRManager {
|
|
|
3651
3741
|
}
|
|
3652
3742
|
}
|
|
3653
3743
|
|
|
3744
|
+
/**
|
|
3745
|
+
* Show an error when the worktree is not found during analysis.
|
|
3746
|
+
* Displays a helpful message with a reload link. If the user arrived
|
|
3747
|
+
* via auto-analyze (?analyze=true), the reload link preserves that
|
|
3748
|
+
* parameter so analysis re-triggers after setup.
|
|
3749
|
+
* @param {string} owner - Repository owner
|
|
3750
|
+
* @param {string} repo - Repository name
|
|
3751
|
+
* @param {number} number - PR number
|
|
3752
|
+
*/
|
|
3753
|
+
showWorktreeNotFoundError(owner, repo, number) {
|
|
3754
|
+
let setupUrl = `/pr/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${encodeURIComponent(number)}`;
|
|
3755
|
+
if (this._autoAnalyzeRequested) {
|
|
3756
|
+
setupUrl += '?analyze=true';
|
|
3757
|
+
}
|
|
3758
|
+
const container = document.getElementById('pr-container');
|
|
3759
|
+
if (container) {
|
|
3760
|
+
container.innerHTML = `
|
|
3761
|
+
<div class="error-container">
|
|
3762
|
+
<div class="error-icon">Warning</div>
|
|
3763
|
+
<div class="error-message">Worktree not found. Please reload the PR to set up the worktree before running analysis.</div>
|
|
3764
|
+
<a class="btn btn-primary" href="${this.escapeHtml(setupUrl)}">Reload PR</a>
|
|
3765
|
+
</div>
|
|
3766
|
+
`;
|
|
3767
|
+
container.style.display = 'block';
|
|
3768
|
+
}
|
|
3769
|
+
this.resetButton();
|
|
3770
|
+
}
|
|
3771
|
+
|
|
3654
3772
|
/**
|
|
3655
3773
|
* Refresh the PR data
|
|
3656
3774
|
*/
|