@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.
Files changed (48) hide show
  1. package/package.json +1 -1
  2. package/plugin/.claude-plugin/plugin.json +1 -1
  3. package/plugin/skills/review-requests/SKILL.md +54 -0
  4. package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
  5. package/public/css/pr.css +1081 -54
  6. package/public/css/repo-settings.css +452 -140
  7. package/public/js/components/AdvancedConfigTab.js +1364 -0
  8. package/public/js/components/AnalysisConfigModal.js +488 -112
  9. package/public/js/components/CouncilProgressModal.js +1416 -0
  10. package/public/js/components/TextInputDialog.js +231 -0
  11. package/public/js/components/TimeoutSelect.js +367 -0
  12. package/public/js/components/VoiceCentricConfigTab.js +1334 -0
  13. package/public/js/local.js +162 -83
  14. package/public/js/modules/analysis-history.js +185 -11
  15. package/public/js/modules/comment-manager.js +13 -0
  16. package/public/js/modules/file-comment-manager.js +28 -0
  17. package/public/js/pr.js +233 -115
  18. package/public/js/repo-settings.js +575 -106
  19. package/public/local.html +11 -1
  20. package/public/pr.html +6 -1
  21. package/public/repo-settings.html +28 -21
  22. package/public/setup.html +8 -2
  23. package/src/ai/analyzer.js +1262 -111
  24. package/src/ai/claude-cli.js +2 -2
  25. package/src/ai/claude-provider.js +6 -6
  26. package/src/ai/codex-provider.js +6 -6
  27. package/src/ai/copilot-provider.js +3 -3
  28. package/src/ai/cursor-agent-provider.js +6 -6
  29. package/src/ai/gemini-provider.js +6 -6
  30. package/src/ai/opencode-provider.js +6 -6
  31. package/src/ai/pi-provider.js +6 -6
  32. package/src/ai/prompts/baseline/consolidation/balanced.js +208 -0
  33. package/src/ai/prompts/baseline/consolidation/fast.js +175 -0
  34. package/src/ai/prompts/baseline/consolidation/thorough.js +283 -0
  35. package/src/ai/prompts/config.js +1 -1
  36. package/src/ai/prompts/index.js +26 -2
  37. package/src/ai/provider.js +4 -2
  38. package/src/database.js +417 -14
  39. package/src/main.js +1 -1
  40. package/src/routes/analysis.js +495 -10
  41. package/src/routes/config.js +36 -15
  42. package/src/routes/councils.js +351 -0
  43. package/src/routes/local.js +33 -11
  44. package/src/routes/mcp.js +9 -2
  45. package/src/routes/setup.js +12 -2
  46. package/src/routes/shared.js +126 -13
  47. package/src/server.js +34 -4
  48. 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
- await this.loadPR(owner, repo, parseInt(number));
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, number] = parts;
296
- await this.loadPR(owner, repo, number);
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 editForm = commentDiv.querySelector('.user-comment-edit-form');
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 (editForm) editForm.remove();
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 progress dialog for the running analysis
3350
- if (window.progressModal) {
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 && window.progressModal) {
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 used custom instructions from review record
3397
- * @returns {Promise<string>} Last custom instructions or empty string
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 fetchLastCustomInstructions() {
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 data.custom_instructions || '';
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
- // Fetch stale check, repo settings, and last instructions in parallel
3478
- const [staleResult, repoSettings, lastInstructions] = await Promise.all([
3479
- staleCheckPromise,
3480
- this.fetchRepoSettings().catch(() => null),
3481
- this.fetchLastCustomInstructions().catch(() => '')
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
- // Process stale check result
3485
- try {
3486
- if (staleResult === null) {
3487
- // Timed out or network error — fail open
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('Could not verify PR is current. Proceeding with analysis.');
3547
+ window.toast.showWarning(`This PR is ${stateLabel}. Analysis will proceed on the existing data.`);
3490
3548
  }
3491
- } else if (staleResult._fetchError) {
3549
+ }
3550
+
3551
+ if (staleResult.isStale === null) {
3492
3552
  if (window.toast) {
3493
- window.toast.showWarning(`Could not verify PR is current (${staleResult.status}). Proceeding with analysis.`);
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
- if (staleResult.isStale === null) {
3505
- if (window.toast) {
3506
- window.toast.showWarning('Could not verify PR is current. Proceeding with analysis.');
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
- } catch (staleError) {
3532
- console.warn('Error processing PR staleness:', staleError);
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
- // Determine the model and provider to use (priority: remembered > repo default > defaults)
3539
- const modelStorageKey = PRManager.getRepoStorageKey('pair-review-model', owner, repo);
3540
- const providerStorageKey = PRManager.getRepoStorageKey('pair-review-provider', owner, repo);
3541
- const rememberedModel = localStorage.getItem(modelStorageKey);
3542
- const rememberedProvider = localStorage.getItem(providerStorageKey);
3543
- const currentModel = rememberedModel || repoSettings?.default_model || 'opus';
3544
- const currentProvider = rememberedProvider || repoSettings?.default_provider || 'claude';
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
- rememberModel: !!(rememberedModel || rememberedProvider)
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
- // Save remembered model and provider preferences if requested
3561
- if (config.rememberModel) {
3562
- localStorage.setItem(modelStorageKey, config.model);
3563
- localStorage.setItem(providerStorageKey, config.provider);
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(modelStorageKey);
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
- // Start AI analysis with model, tier, and instructions
3614
- const response = await fetch(`/api/analyze/${owner}/${repo}/${number}`, {
3615
- method: 'POST',
3616
- headers: {
3617
- 'Content-Type': 'application/json'
3618
- },
3619
- body: JSON.stringify({
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
- if (window.progressModal) {
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
  */