@in-the-loop-labs/pair-review 3.0.6 → 3.1.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 (79) hide show
  1. package/package.json +2 -1
  2. package/plugin/.claude-plugin/plugin.json +1 -1
  3. package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
  4. package/plugin-code-critic/skills/analyze/references/level1-balanced.md +8 -0
  5. package/plugin-code-critic/skills/analyze/references/level1-fast.md +7 -0
  6. package/plugin-code-critic/skills/analyze/references/level1-thorough.md +8 -0
  7. package/plugin-code-critic/skills/analyze/references/level2-balanced.md +9 -0
  8. package/plugin-code-critic/skills/analyze/references/level2-fast.md +8 -0
  9. package/plugin-code-critic/skills/analyze/references/level2-thorough.md +9 -0
  10. package/plugin-code-critic/skills/analyze/references/level3-balanced.md +9 -0
  11. package/plugin-code-critic/skills/analyze/references/level3-fast.md +8 -0
  12. package/plugin-code-critic/skills/analyze/references/level3-thorough.md +9 -0
  13. package/plugin-code-critic/skills/analyze/references/orchestration-balanced.md +9 -0
  14. package/plugin-code-critic/skills/analyze/references/orchestration-fast.md +5 -0
  15. package/plugin-code-critic/skills/analyze/references/orchestration-thorough.md +9 -0
  16. package/public/css/analysis-config.css +83 -0
  17. package/public/css/pr.css +191 -4
  18. package/public/index.html +20 -0
  19. package/public/js/components/AIPanel.js +1 -1
  20. package/public/js/components/AdvancedConfigTab.js +83 -8
  21. package/public/js/components/AnalysisConfigModal.js +155 -5
  22. package/public/js/components/ChatPanel.js +22 -5
  23. package/public/js/components/CouncilProgressModal.js +239 -22
  24. package/public/js/components/TimeoutSelect.js +2 -0
  25. package/public/js/components/VoiceCentricConfigTab.js +179 -12
  26. package/public/js/index.js +119 -1
  27. package/public/js/local.js +141 -47
  28. package/public/js/modules/suggestion-manager.js +2 -1
  29. package/public/js/pr.js +71 -12
  30. package/public/js/repo-settings.js +2 -2
  31. package/public/local.html +32 -11
  32. package/public/pr.html +2 -0
  33. package/src/ai/analyzer.js +371 -111
  34. package/src/ai/claude-provider.js +2 -0
  35. package/src/ai/codex-provider.js +1 -1
  36. package/src/ai/copilot-provider.js +2 -0
  37. package/src/ai/executable-provider.js +534 -0
  38. package/src/ai/gemini-provider.js +2 -0
  39. package/src/ai/index.js +9 -1
  40. package/src/ai/pi-provider.js +10 -8
  41. package/src/ai/prompts/baseline/consolidation/balanced.js +54 -2
  42. package/src/ai/prompts/baseline/consolidation/fast.js +31 -1
  43. package/src/ai/prompts/baseline/consolidation/thorough.js +46 -3
  44. package/src/ai/prompts/baseline/level1/balanced.js +12 -0
  45. package/src/ai/prompts/baseline/level1/fast.js +11 -0
  46. package/src/ai/prompts/baseline/level1/thorough.js +12 -0
  47. package/src/ai/prompts/baseline/level2/balanced.js +13 -0
  48. package/src/ai/prompts/baseline/level2/fast.js +12 -0
  49. package/src/ai/prompts/baseline/level2/thorough.js +13 -0
  50. package/src/ai/prompts/baseline/level3/balanced.js +13 -0
  51. package/src/ai/prompts/baseline/level3/fast.js +12 -0
  52. package/src/ai/prompts/baseline/level3/thorough.js +13 -0
  53. package/src/ai/prompts/baseline/orchestration/balanced.js +15 -0
  54. package/src/ai/prompts/baseline/orchestration/fast.js +11 -0
  55. package/src/ai/prompts/baseline/orchestration/thorough.js +15 -0
  56. package/src/ai/prompts/render-for-skill.js +3 -0
  57. package/src/ai/prompts/shared/output-schema.js +8 -0
  58. package/src/ai/provider.js +89 -4
  59. package/src/chat/prompt-builder.js +17 -1
  60. package/src/chat/session-manager.js +32 -28
  61. package/src/config.js +15 -2
  62. package/src/database.js +59 -15
  63. package/src/git/base-branch.js +113 -29
  64. package/src/local-review.js +15 -9
  65. package/src/main.js +3 -2
  66. package/src/routes/analyses.js +34 -8
  67. package/src/routes/chat.js +15 -8
  68. package/src/routes/config.js +3 -120
  69. package/src/routes/councils.js +15 -6
  70. package/src/routes/executable-analysis.js +494 -0
  71. package/src/routes/local.js +152 -15
  72. package/src/routes/mcp.js +9 -4
  73. package/src/routes/pr.js +166 -29
  74. package/src/routes/reviews.js +31 -5
  75. package/src/routes/shared.js +72 -5
  76. package/src/routes/worktrees.js +4 -2
  77. package/src/utils/comment-formatter.js +28 -11
  78. package/src/utils/instructions.js +22 -8
  79. package/src/utils/logger.js +20 -10
@@ -73,7 +73,15 @@ class LocalManager {
73
73
  const autoAnalyze = new URLSearchParams(window.location.search).get('analyze');
74
74
  if (autoAnalyze === 'true' && !window.prManager.isAnalyzing) {
75
75
  try {
76
- await this.startLocalAnalysis(null, {});
76
+ // Fetch repo settings so we honour the repository's default provider/council
77
+ const manager = window.prManager;
78
+ const [repoSettings, reviewSettings] = await Promise.all([
79
+ manager.fetchRepoSettings().catch(() => null),
80
+ manager.fetchLastReviewSettings().catch(() => ({ custom_instructions: '', last_council_id: null }))
81
+ ]);
82
+ const config = await manager._buildDefaultAnalysisConfig(repoSettings, reviewSettings);
83
+
84
+ await this.startLocalAnalysis(null, config);
77
85
  } finally {
78
86
  const cleanUrl = new URL(window.location);
79
87
  cleanUrl.searchParams.delete('analyze');
@@ -260,10 +268,11 @@ class LocalManager {
260
268
  ? manager._stalenessPromise
261
269
  : self._fetchLocalStaleness();
262
270
  manager._stalenessPromise = null; // consume it
263
- const [staleResult, repoSettings, reviewSettings] = await Promise.all([
271
+ const [staleResult, repoSettings, reviewSettings, appConfig] = await Promise.all([
264
272
  staleCheckWithTimeout,
265
273
  manager.fetchRepoSettings().catch(() => null),
266
- manager.fetchLastReviewSettings().catch(() => ({ custom_instructions: '', last_council_id: null }))
274
+ manager.fetchLastReviewSettings().catch(() => ({ custom_instructions: '', last_council_id: null })),
275
+ fetch('/api/config').then(r => r.ok ? r.json() : {}).catch(() => ({}))
267
276
  ]);
268
277
  console.debug(`[Analyze] parallel-fetch (stale+settings): ${Math.round(performance.now() - _tParallel0)}ms`);
269
278
 
@@ -329,7 +338,9 @@ class LocalManager {
329
338
  repoInstructions: repoSettings?.default_instructions || '',
330
339
  lastInstructions: lastInstructions,
331
340
  lastCouncilId,
332
- defaultCouncilId: repoSettings?.default_council_id || null
341
+ defaultCouncilId: repoSettings?.default_council_id || null,
342
+ hasPr: false,
343
+ hasGithubToken: Boolean(appConfig.has_github_token)
333
344
  });
334
345
 
335
346
  if (!config) {
@@ -376,7 +387,8 @@ class LocalManager {
376
387
  null,
377
388
  {
378
389
  configType: data.status?.isCouncil ? (data.status.configType || 'advanced') : 'single',
379
- enabledLevels: data.status?.enabledLevels || [1, 2, 3]
390
+ enabledLevels: data.status?.enabledLevels || [1, 2, 3],
391
+ noLevels: data.status?.noLevels || false
380
392
  }
381
393
  );
382
394
  }
@@ -480,6 +492,60 @@ class LocalManager {
480
492
  });
481
493
  };
482
494
 
495
+ // Base branch override for stack-aware diff in local mode
496
+ manager.currentBaseOverride = null;
497
+
498
+ // Render the base branch selector dropdown for stacked branches.
499
+ // When a local review has stack_data with 3+ entries, the user can pick
500
+ // which ancestor branch to diff against.
501
+ // Render the base branch selector when a Graphite stack has multiple ancestors.
502
+ // When shown, the selector replaces the static base branch text in the toolbar.
503
+ manager.renderBaseBranchSelector = function(pr) {
504
+ const selectorWrap = document.getElementById('base-branch-selector-wrap');
505
+ const sel = document.getElementById('base-branch-select');
506
+ const staticBase = document.getElementById('toolbar-base-branch-static');
507
+ if (!selectorWrap || !sel) return;
508
+
509
+ // Hide selector if no stack data or fewer than 3 entries (need at least 2 ancestors to switch between)
510
+ if (!pr.stack_data || pr.stack_data.length < 3) {
511
+ selectorWrap.setAttribute('hidden', '');
512
+ if (staticBase) staticBase.removeAttribute('hidden');
513
+ return;
514
+ }
515
+
516
+ // Ancestors = all stack entries except the last (current branch)
517
+ const ancestors = pr.stack_data.slice(0, -1);
518
+
519
+ // Build options using createElement for XSS safety
520
+ sel.innerHTML = '';
521
+ for (const entry of ancestors) {
522
+ const option = document.createElement('option');
523
+ option.value = entry.branch;
524
+ option.textContent = entry.prNumber ? `${entry.branch} (#${entry.prNumber})` : entry.branch;
525
+ if (entry.branch === pr.base_branch) {
526
+ option.selected = true;
527
+ }
528
+ sel.appendChild(option);
529
+ }
530
+
531
+ // Show selector, hide static text
532
+ selectorWrap.removeAttribute('hidden');
533
+ if (staticBase) staticBase.setAttribute('hidden', '');
534
+
535
+ // Wire up change listener (idempotent via data-listener-added pattern)
536
+ if (!sel.hasAttribute('data-listener-added')) {
537
+ sel.setAttribute('data-listener-added', 'true');
538
+ sel.addEventListener('change', async () => {
539
+ manager.currentBaseOverride = sel.value;
540
+ // If selection matches the original base, clear the override
541
+ if (sel.value === manager.currentPR.base_branch) {
542
+ manager.currentBaseOverride = null;
543
+ }
544
+ await self.loadLocalDiff();
545
+ });
546
+ }
547
+ };
548
+
483
549
  console.log('PRManager patched for local mode');
484
550
  }
485
551
 
@@ -509,7 +575,8 @@ class LocalManager {
509
575
  councilId: config.councilId || undefined,
510
576
  councilConfig: config.councilConfig || undefined,
511
577
  configType: config.configType || 'advanced',
512
- customInstructions: config.customInstructions || null
578
+ customInstructions: config.customInstructions || null,
579
+ excludePrevious: config.excludePrevious || undefined
513
580
  };
514
581
  } else {
515
582
  analyzeUrl = `/api/local/${this.reviewId}/analyses`;
@@ -519,7 +586,8 @@ class LocalManager {
519
586
  tier: config.tier || 'balanced',
520
587
  customInstructions: config.customInstructions || null,
521
588
  enabledLevels: config.enabledLevels || [1, 2, 3],
522
- skipLevel3: config.skipLevel3 || false
589
+ skipLevel3: config.skipLevel3 || false,
590
+ excludePrevious: config.excludePrevious || undefined
523
591
  };
524
592
  }
525
593
 
@@ -556,7 +624,8 @@ class LocalManager {
556
624
  config.isCouncil ? config.councilName : null,
557
625
  {
558
626
  configType: config.isCouncil ? (config.configType || 'advanced') : 'single',
559
- enabledLevels: config.enabledLevels || [1, 2, 3]
627
+ enabledLevels: config.enabledLevels || [1, 2, 3],
628
+ noLevels: config.noLevels || false
560
629
  }
561
630
  );
562
631
  }
@@ -647,7 +716,7 @@ class LocalManager {
647
716
  try {
648
717
  // Show loading state
649
718
  refreshBtn.disabled = true;
650
- refreshBtn.classList.add('btn-loading');
719
+ refreshBtn.classList.add('refreshing');
651
720
 
652
721
  const response = await fetch(`/api/local/${this.reviewId}/refresh`, {
653
722
  method: 'POST'
@@ -698,7 +767,7 @@ class LocalManager {
698
767
  // Reset button state
699
768
  if (refreshBtn) {
700
769
  refreshBtn.disabled = false;
701
- refreshBtn.classList.remove('btn-loading');
770
+ refreshBtn.classList.remove('refreshing');
702
771
  }
703
772
  }
704
773
  }
@@ -799,6 +868,13 @@ class LocalManager {
799
868
  );
800
869
  }
801
870
 
871
+ // Reset base branch override before reloading diff so the fetch uses the default base
872
+ manager.currentBaseOverride = null;
873
+ const baseSel = document.getElementById('base-branch-select');
874
+ if (baseSel && manager.currentPR?.base_branch) {
875
+ baseSel.value = manager.currentPR.base_branch;
876
+ }
877
+
802
878
  // Reload the diff display
803
879
  await this.loadLocalDiff();
804
880
 
@@ -947,7 +1023,8 @@ class LocalManager {
947
1023
  head_sha: reviewData.localHeadSha,
948
1024
  shaAbbrevLength: reviewData.shaAbbrevLength || 7,
949
1025
  reviewType: 'local',
950
- localPath: reviewData.localPath
1026
+ localPath: reviewData.localPath,
1027
+ stack_data: reviewData.stackData || null
951
1028
  };
952
1029
 
953
1030
  // Re-initialize DiffOptionsDropdown with scope options
@@ -1131,33 +1208,61 @@ class LocalManager {
1131
1208
  pathText.title = fullPath;
1132
1209
  }
1133
1210
 
1134
- // Update branch name (header badge)
1211
+ // Update branch name in header badge
1135
1212
  const branchText = document.getElementById('local-branch-text');
1136
1213
  if (branchText) {
1137
1214
  branchText.textContent = reviewData.branch || 'unknown';
1138
1215
  }
1139
1216
 
1217
+ // Wire up header branch copy button
1218
+ const branchCopy = document.getElementById('local-branch-copy');
1219
+ if (branchCopy && !branchCopy.hasAttribute('data-listener-added')) {
1220
+ branchCopy.setAttribute('data-listener-added', 'true');
1221
+ branchCopy.addEventListener('click', async (e) => {
1222
+ e.stopPropagation();
1223
+ const branch = branchText ? branchText.textContent : '';
1224
+ if (!branch || branch === '--' || branch === 'unknown') return;
1225
+ try {
1226
+ await navigator.clipboard.writeText(branch);
1227
+ branchCopy.classList.add('copied');
1228
+ setTimeout(() => branchCopy.classList.remove('copied'), 2000);
1229
+ } catch (err) {
1230
+ console.error('Failed to copy branch name:', err);
1231
+ }
1232
+ });
1233
+ }
1234
+
1140
1235
  // Set descriptive tab title
1141
1236
  if (window.tabTitle && reviewData.branch) {
1142
1237
  window.tabTitle.setBase(reviewData.branch);
1143
1238
  }
1144
1239
 
1145
- // Show base branch badge when branch is in scope
1240
+ // Show base branch in toolbar when branch is in scope
1146
1241
  const LS = window.LocalScope;
1147
1242
  const scopeStart = this.scopeStart || (LS ? LS.DEFAULT_SCOPE.start : 'unstaged');
1148
1243
  const scopeEnd = this.scopeEnd || (LS ? LS.DEFAULT_SCOPE.end : 'untracked');
1149
1244
  const hasBranch = LS ? LS.scopeIncludes(scopeStart, scopeEnd, 'branch') : false;
1150
1245
 
1246
+ // Toolbar base branch display (static text, selector is wired separately)
1247
+ const toolbarBaseWrap = document.getElementById('toolbar-base-branch-wrap');
1248
+ const toolbarBaseStatic = document.getElementById('toolbar-base-branch-static');
1249
+ const toolbarBaseText = document.getElementById('toolbar-base-branch-text');
1250
+ if (hasBranch && reviewData.baseBranch) {
1251
+ if (toolbarBaseText) toolbarBaseText.textContent = reviewData.baseBranch;
1252
+ if (toolbarBaseWrap) toolbarBaseWrap.removeAttribute('hidden');
1253
+ } else {
1254
+ if (toolbarBaseWrap) toolbarBaseWrap.setAttribute('hidden', '');
1255
+ }
1256
+
1257
+ // Hide header branch display — toolbar now shows branch info
1151
1258
  const branchVs = document.getElementById('local-branch-vs');
1152
1259
  const baseBranchEl = document.getElementById('local-base-branch');
1153
1260
  const baseBranchText = document.getElementById('local-base-branch-text');
1154
- if (hasBranch && reviewData.baseBranch) {
1155
- if (branchVs) branchVs.style.display = '';
1156
- if (baseBranchEl) baseBranchEl.style.display = '';
1157
- if (baseBranchText) baseBranchText.textContent = reviewData.baseBranch;
1158
- } else {
1159
- if (branchVs) branchVs.style.display = 'none';
1160
- if (baseBranchEl) baseBranchEl.style.display = 'none';
1261
+ if (branchVs) branchVs.style.display = 'none';
1262
+ if (baseBranchEl) baseBranchEl.style.display = 'none';
1263
+ // Keep baseBranchText updated for data purposes even though header is hidden
1264
+ if (baseBranchText && reviewData.baseBranch) {
1265
+ baseBranchText.textContent = reviewData.baseBranch;
1161
1266
  }
1162
1267
 
1163
1268
  // Update refresh button tooltip based on scope
@@ -1167,29 +1272,6 @@ class LocalManager {
1167
1272
  refreshBtn.title = `Refresh diff (${scopeLabel})`;
1168
1273
  }
1169
1274
 
1170
- // Update branch name (toolbar) and wire up copy button
1171
- const branchName = document.getElementById('pr-branch-name');
1172
- if (branchName) {
1173
- branchName.textContent = reviewData.branch || 'unknown';
1174
- }
1175
-
1176
- const branchCopy = document.getElementById('pr-branch-copy');
1177
- if (branchCopy && !branchCopy.hasAttribute('data-listener-added')) {
1178
- branchCopy.setAttribute('data-listener-added', 'true');
1179
- branchCopy.addEventListener('click', async (e) => {
1180
- e.stopPropagation();
1181
- const branch = branchName ? branchName.textContent : '';
1182
- if (!branch || branch === '--' || branch === 'unknown') return;
1183
- try {
1184
- await navigator.clipboard.writeText(branch);
1185
- branchCopy.classList.add('copied');
1186
- setTimeout(() => branchCopy.classList.remove('copied'), 2000);
1187
- } catch (err) {
1188
- console.error('Failed to copy branch name:', err);
1189
- }
1190
- });
1191
- }
1192
-
1193
1275
  // Update commit SHA and wire up copy button
1194
1276
  const commitSha = document.getElementById('pr-commit-sha');
1195
1277
  if (commitSha && reviewData.localHeadSha) {
@@ -1255,6 +1337,12 @@ class LocalManager {
1255
1337
  settingsLink.style.display = 'none';
1256
1338
  }
1257
1339
  }
1340
+
1341
+ // Render base branch selector for stacked branches
1342
+ const manager = window.prManager;
1343
+ if (manager?.renderBaseBranchSelector) {
1344
+ manager.renderBaseBranchSelector(manager.currentPR);
1345
+ }
1258
1346
  }
1259
1347
 
1260
1348
  /**
@@ -1301,10 +1389,11 @@ class LocalManager {
1301
1389
  const manager = window.prManager;
1302
1390
 
1303
1391
  try {
1304
- let diffUrl = `/api/local/${this.reviewId}/diff`;
1305
- if (manager.hideWhitespace) {
1306
- diffUrl += '?w=1';
1307
- }
1392
+ const params = new URLSearchParams();
1393
+ if (manager.hideWhitespace) params.set('w', '1');
1394
+ if (manager.currentBaseOverride) params.set('base', manager.currentBaseOverride);
1395
+ const queryString = params.toString();
1396
+ const diffUrl = `/api/local/${this.reviewId}/diff${queryString ? '?' + queryString : ''}`;
1308
1397
  const response = await fetch(diffUrl);
1309
1398
 
1310
1399
  if (!response.ok) {
@@ -1475,6 +1564,11 @@ class LocalManager {
1475
1564
  : `Local Changes - ${manager.currentPR.head_branch}`;
1476
1565
  }
1477
1566
 
1567
+ // Reset base branch override on scope change (base branch context may differ)
1568
+ if (manager) {
1569
+ manager.currentBaseOverride = null;
1570
+ }
1571
+
1478
1572
  // Update header and reload diff
1479
1573
  this.updateLocalHeader(this.localData);
1480
1574
  await this.loadLocalDiff();
@@ -482,6 +482,7 @@ class SuggestionManager {
482
482
  ${suggestion.type === 'praise'
483
483
  ? `<span class="praise-badge" title="Nice Work"><svg viewBox="0 0 16 16"><path d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25z"/></svg>Nice Work</span>`
484
484
  : `<span class="ai-suggestion-badge" data-type="${suggestion.type}" title="${this.getTypeDescription(suggestion.type)}"><svg viewBox="0 0 16 16" fill="currentColor" width="12" height="12"><path d="M9.6 2.279a.426.426 0 0 1 .8 0l.407 1.112a6.386 6.386 0 0 0 3.802 3.802l1.112.407a.426.426 0 0 1 0 .8l-1.112.407a6.386 6.386 0 0 0-3.802 3.802l-.407 1.112a.426.426 0 0 1-.8 0l-.407-1.112a6.386 6.386 0 0 0-3.802-3.802L4.279 8.4a.426.426 0 0 1 0-.8l1.112-.407a6.386 6.386 0 0 0 3.802-3.802L9.6 2.279Zm-4.267 8.837a.178.178 0 0 1 .334 0l.169.464a2.662 2.662 0 0 0 1.584 1.584l.464.169a.178.178 0 0 1 0 .334l-.464.169a2.662 2.662 0 0 0-1.584 1.584l-.169.464a.178.178 0 0 1-.334 0l-.169-.464a2.662 2.662 0 0 0-1.584-1.584l-.464-.169a.178.178 0 0 1 0-.334l.464-.169a2.662 2.662 0 0 0 1.584-1.584l.169-.464ZM2.8.14a.213.213 0 0 1 .4 0l.203.556a3.2 3.2 0 0 0 1.901 1.901l.556.203a.213.213 0 0 1 0 .4l-.556.203a3.2 3.2 0 0 0-1.901 1.901L3.2 5.86a.213.213 0 0 1-.4 0l-.203-.556A3.2 3.2 0 0 0 .696 3.403L.14 3.2a.213.213 0 0 1 0-.4l.556-.203A3.2 3.2 0 0 0 2.597.696L2.8.14Z"/></svg>AI Suggestion</span>`}
485
+ ${suggestion.severity ? `<span class="severity-badge severity-${suggestion.severity}">${escapeHtml(suggestion.severity.toUpperCase())}</span>` : ''}
485
486
  ${categoryLabel ? `<span class="ai-suggestion-category">${escapeHtml(categoryLabel)}</span>` : ''}
486
487
  <span class="ai-title">${escapeHtml(suggestion.title || '')}</span>
487
488
  </div>
@@ -497,7 +498,7 @@ class SuggestionManager {
497
498
  ${suggestion.type === 'praise'
498
499
  ? `<span class="praise-badge" title="Nice Work"><svg viewBox="0 0 16 16"><path d="M8 .25a.75.75 0 01.673.418l1.882 3.815 4.21.612a.75.75 0 01.416 1.279l-3.046 2.97.719 4.192a.75.75 0 01-1.088.791L8 12.347l-3.766 1.98a.75.75 0 01-1.088-.79l.72-4.194L.818 6.374a.75.75 0 01.416-1.28l4.21-.611L7.327.668A.75.75 0 018 .25z"/></svg>Nice Work</span>`
499
500
  : `<span class="ai-suggestion-badge collapsed" data-type="${suggestion.type}" title="${this.getTypeDescription(suggestion.type)}"><svg viewBox="0 0 16 16" fill="currentColor" width="10" height="10"><path d="M9.6 2.279a.426.426 0 0 1 .8 0l.407 1.112a6.386 6.386 0 0 0 3.802 3.802l1.112.407a.426.426 0 0 1 0 .8l-1.112.407a6.386 6.386 0 0 0-3.802 3.802l-.407 1.112a.426.426 0 0 1-.8 0l-.407-1.112a6.386 6.386 0 0 0-3.802-3.802L4.279 8.4a.426.426 0 0 1 0-.8l1.112-.407a6.386 6.386 0 0 0 3.802-3.802L9.6 2.279Zm-4.267 8.837a.178.178 0 0 1 .334 0l.169.464a2.662 2.662 0 0 0 1.584 1.584l.464.169a.178.178 0 0 1 0 .334l-.464.169a2.662 2.662 0 0 0-1.584 1.584l-.169.464a.178.178 0 0 1-.334 0l-.169-.464a2.662 2.662 0 0 0-1.584-1.584l-.464-.169a.178.178 0 0 1 0-.334l.464-.169a2.662 2.662 0 0 0 1.584-1.584l.169-.464ZM2.8.14a.213.213 0 0 1 .4 0l.203.556a3.2 3.2 0 0 0 1.901 1.901l.556.203a.213.213 0 0 1 0 .4l-.556.203a3.2 3.2 0 0 0-1.901 1.901L3.2 5.86a.213.213 0 0 1-.4 0l-.203-.556A3.2 3.2 0 0 0 .696 3.403L.14 3.2a.213.213 0 0 1 0-.4l.556-.203A3.2 3.2 0 0 0 2.597.696L2.8.14Z"/></svg>AI Suggestion</span>`}
500
- <span class="collapsed-text">${isAdopted ? 'Suggestion adopted' : 'Hidden AI suggestion'}</span>
501
+ ${suggestion.severity ? `<span class="severity-badge severity-${suggestion.severity}">${escapeHtml(suggestion.severity.toUpperCase())}</span>` : ''}
501
502
  <span class="collapsed-title">${escapeHtml(suggestion.title || '')}</span>
502
503
  <div class="ai-suggestion-header-right">
503
504
  ${suggestion.reasoning && suggestion.reasoning.length > 0 ? `
package/public/js/pr.js CHANGED
@@ -398,6 +398,51 @@ class PRManager {
398
398
  }
399
399
  }
400
400
 
401
+ /**
402
+ * Build analysis config from repo defaults (no modal interaction).
403
+ * Used by auto-analyze (--ai) to honour the repository's default provider/council.
404
+ * When the default is a council, fetches the council config from the server so the
405
+ * progress modal can render the voice/level layout.
406
+ * @param {Object|null} repoSettings - Repo settings from fetchRepoSettings()
407
+ * @param {Object} reviewSettings - Review settings from fetchLastReviewSettings()
408
+ * @returns {Promise<Object>} Config object suitable for startAnalysis / startLocalAnalysis
409
+ */
410
+ async _buildDefaultAnalysisConfig(repoSettings, reviewSettings) {
411
+ const defaultTab = repoSettings?.default_tab || 'single';
412
+ const councilId = repoSettings?.default_council_id || reviewSettings?.last_council_id || null;
413
+
414
+ if ((defaultTab === 'council' || defaultTab === 'advanced') && councilId) {
415
+ // Fetch the full council config so the progress modal can render correctly
416
+ let councilConfig = null;
417
+ let councilName = null;
418
+ try {
419
+ const resp = await fetch(`/api/councils/${councilId}`);
420
+ if (resp.ok) {
421
+ const data = await resp.json();
422
+ councilConfig = data.council?.config || null;
423
+ councilName = data.council?.name || null;
424
+ }
425
+ } catch (e) {
426
+ console.warn('Failed to fetch council config for auto-analyze:', e);
427
+ }
428
+
429
+ return {
430
+ isCouncil: true,
431
+ councilId,
432
+ councilConfig,
433
+ councilName,
434
+ configType: defaultTab,
435
+ customInstructions: null
436
+ };
437
+ }
438
+
439
+ return {
440
+ provider: repoSettings?.default_provider || 'claude',
441
+ model: repoSettings?.default_model || 'opus',
442
+ customInstructions: null
443
+ };
444
+ }
445
+
401
446
  /**
402
447
  * Auto-trigger analysis if ?analyze=true is present in the URL.
403
448
  * Skips refresh if data was just loaded fresh by loadPR (to avoid redundant fetches).
@@ -428,7 +473,14 @@ class PRManager {
428
473
  }
429
474
  }
430
475
 
431
- await this.startAnalysis(owner, repo, prNumber, null, {});
476
+ // Fetch repo settings so we honour the repository's default provider/council
477
+ const [repoSettings, reviewSettings] = await Promise.all([
478
+ this.fetchRepoSettings().catch(() => null),
479
+ this.fetchLastReviewSettings().catch(() => ({ custom_instructions: '', last_council_id: null }))
480
+ ]);
481
+ const config = await this._buildDefaultAnalysisConfig(repoSettings, reviewSettings);
482
+
483
+ await this.startAnalysis(owner, repo, prNumber, null, config);
432
484
  } finally {
433
485
  this._autoAnalyzeRequested = false;
434
486
  const cleanUrl = new URL(window.location);
@@ -667,10 +719,9 @@ class PRManager {
667
719
  */
668
720
  async loadAndDisplayFiles(owner, repo, number) {
669
721
  try {
670
- let diffUrl = `/api/pr/${owner}/${repo}/${number}/diff`;
671
- if (this.hideWhitespace) {
672
- diffUrl += '?w=1';
673
- }
722
+ const diffUrl = this.hideWhitespace
723
+ ? `/api/pr/${owner}/${repo}/${number}/diff?w=1`
724
+ : `/api/pr/${owner}/${repo}/${number}/diff`;
674
725
  const response = await fetch(diffUrl);
675
726
 
676
727
  if (response.ok) {
@@ -4112,7 +4163,8 @@ class PRManager {
4112
4163
  null,
4113
4164
  {
4114
4165
  configType: data.status?.isCouncil ? (data.status.configType || 'advanced') : 'single',
4115
- enabledLevels: data.status?.enabledLevels || [1, 2, 3]
4166
+ enabledLevels: data.status?.enabledLevels || [1, 2, 3],
4167
+ noLevels: data.status?.noLevels || false
4116
4168
  }
4117
4169
  );
4118
4170
  }
@@ -4223,10 +4275,11 @@ class PRManager {
4223
4275
  : this._fetchStaleness(owner, repo, number);
4224
4276
  this._stalenessPromise = null; // consume it
4225
4277
 
4226
- const [staleResult, repoSettings, reviewSettings] = await Promise.all([
4278
+ const [staleResult, repoSettings, reviewSettings, appConfig] = await Promise.all([
4227
4279
  staleCheckWithTimeout,
4228
4280
  this.fetchRepoSettings(),
4229
- this.fetchLastReviewSettings()
4281
+ this.fetchLastReviewSettings(),
4282
+ fetch('/api/config').then(r => r.ok ? r.json() : {}).catch(() => ({}))
4230
4283
  ]);
4231
4284
  console.debug(`[Analyze] parallel-fetch (stale+settings): ${Math.round(performance.now() - _tParallel0)}ms`);
4232
4285
 
@@ -4300,7 +4353,9 @@ class PRManager {
4300
4353
  repoInstructions: repoSettings?.default_instructions || '',
4301
4354
  lastInstructions: lastInstructions,
4302
4355
  lastCouncilId,
4303
- defaultCouncilId: repoSettings?.default_council_id || null
4356
+ defaultCouncilId: repoSettings?.default_council_id || null,
4357
+ hasPr: true,
4358
+ hasGithubToken: Boolean(appConfig.has_github_token)
4304
4359
  });
4305
4360
 
4306
4361
  // If user cancelled, do nothing
@@ -4368,7 +4423,8 @@ class PRManager {
4368
4423
  councilId: config.councilId || undefined,
4369
4424
  councilConfig: config.councilConfig || undefined,
4370
4425
  configType: config.configType || 'advanced',
4371
- customInstructions: config.customInstructions || null
4426
+ customInstructions: config.customInstructions || null,
4427
+ excludePrevious: config.excludePrevious || undefined
4372
4428
  };
4373
4429
  } else {
4374
4430
  analyzeUrl = `/api/pr/${owner}/${repo}/${number}/analyses`;
@@ -4378,7 +4434,8 @@ class PRManager {
4378
4434
  tier: config.tier || 'balanced',
4379
4435
  customInstructions: config.customInstructions || null,
4380
4436
  enabledLevels: config.enabledLevels || [1, 2, 3],
4381
- skipLevel3: config.skipLevel3 || false
4437
+ skipLevel3: config.skipLevel3 || false,
4438
+ excludePrevious: config.excludePrevious || undefined
4382
4439
  };
4383
4440
  }
4384
4441
 
@@ -4419,7 +4476,8 @@ class PRManager {
4419
4476
  config.isCouncil ? config.councilName : null,
4420
4477
  {
4421
4478
  configType: config.isCouncil ? (config.configType || 'advanced') : 'single',
4422
- enabledLevels: config.enabledLevels || [1, 2, 3]
4479
+ enabledLevels: config.enabledLevels || [1, 2, 3],
4480
+ noLevels: config.noLevels || false
4423
4481
  }
4424
4482
  );
4425
4483
  }
@@ -4669,6 +4727,7 @@ class PRManager {
4669
4727
 
4670
4728
  this._hideStaleBadge();
4671
4729
  this._stalenessPromise = null;
4730
+
4672
4731
  console.log('PR refreshed successfully');
4673
4732
  }
4674
4733
  } catch (error) {
@@ -635,12 +635,12 @@ class RepoSettingsPage {
635
635
  return;
636
636
  }
637
637
 
638
- const tierIcon = window.getTierIcon ? window.getTierIcon(model.tier) : '';
638
+ const modelIcon = model.icon || (window.getTierIcon ? window.getTierIcon(model.tier) : '');
639
639
 
640
640
  container.innerHTML = `
641
641
  <div class="model-card selected settings-model-card-static" data-tier="${this.escapeHtml(model.tier || '')}">
642
642
  <div class="model-badge ${this.escapeHtml(model.badgeClass || '')}">${this.escapeHtml(model.badge || '')}</div>
643
- <div class="model-icon">${tierIcon}</div>
643
+ <div class="model-icon">${modelIcon}</div>
644
644
  <div class="model-info">
645
645
  <span class="model-name">${this.escapeHtml(model.name)}</span>
646
646
  <span class="model-tagline">${this.escapeHtml(model.tagline || '')}</span>
package/public/local.html CHANGED
@@ -20,6 +20,7 @@
20
20
  window.__pairReview.chatProvider = config.chat_provider || 'pi';
21
21
  window.__pairReview.chatProviders = chatProviders;
22
22
  window.__pairReview.chatSpinner = config.chat_spinner || 'dots';
23
+ window.__pairReview.chatEnterToSend = config.chat_enter_to_send !== false;
23
24
  document.documentElement.setAttribute('data-chat', state);
24
25
  const shortcutsState = config.chat_enable_shortcuts === false ? 'disabled' : 'enabled';
25
26
  document.documentElement.setAttribute('data-chat-shortcuts', shortcutsState);
@@ -40,6 +41,7 @@
40
41
  <!-- PR Display Styles -->
41
42
  <link rel="stylesheet" href="/css/pr.css">
42
43
  <link rel="stylesheet" href="/css/ai-summary-modal.css">
44
+ <link rel="stylesheet" href="/css/analysis-config.css">
43
45
  <link rel="stylesheet" href="/css/styles.css">
44
46
 
45
47
  <!-- Highlight.js for syntax highlighting -->
@@ -128,6 +130,14 @@
128
130
  text-overflow: ellipsis;
129
131
  }
130
132
 
133
+ .local-branch-badge .toolbar-copy-btn {
134
+ opacity: 0;
135
+ }
136
+
137
+ .local-branch-badge:hover .toolbar-copy-btn {
138
+ opacity: 1;
139
+ }
140
+
131
141
  .local-branch-vs {
132
142
  flex-shrink: 0;
133
143
  }
@@ -288,6 +298,12 @@
288
298
  <path d="M11.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122V6A2.5 2.5 0 0110 8.5H6a1 1 0 00-1 1v1.128a2.251 2.251 0 11-1.5 0V5.372a2.25 2.25 0 111.5 0v1.836A2.492 2.492 0 016 7h4a1 1 0 001-1v-.628A2.25 2.25 0 019.5 3.25zM4.25 12a.75.75 0 100 1.5.75.75 0 000-1.5zM3.5 3.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0z"/>
289
299
  </svg>
290
300
  <span id="local-branch-text">--</span>
301
+ <button type="button" class="local-branch-copy toolbar-copy-btn" id="local-branch-copy" title="Copy branch name" aria-label="Copy branch name">
302
+ <svg viewBox="0 0 16 16" fill="currentColor" width="10" height="10">
303
+ <path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path>
304
+ <path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path>
305
+ </svg>
306
+ </button>
291
307
  </span>
292
308
  <span class="local-branch-vs" id="local-branch-vs" style="display: none; font-size: 11px; color: var(--color-text-tertiary); margin: 0 4px;">vs</span>
293
309
  <span class="local-branch-badge" id="local-base-branch" style="display: none;">
@@ -309,9 +325,13 @@
309
325
  <div class="header-right">
310
326
  <div class="header-icon-group">
311
327
  <button class="btn btn-icon" id="local-refresh-btn" title="Refresh diff from directory">
312
- <svg viewBox="0 0 16 16" fill="currentColor" width="16" height="16">
328
+ <svg class="refresh-icon" viewBox="0 0 16 16" fill="currentColor" width="16" height="16">
313
329
  <path d="M1.705 8.005a.75.75 0 0 1 .834.656 5.5 5.5 0 0 0 9.592 2.97l-1.204-1.204a.25.25 0 0 1 .177-.427h3.646a.25.25 0 0 1 .25.25v3.646a.25.25 0 0 1-.427.177l-1.38-1.38A7.002 7.002 0 0 1 1.05 8.84a.75.75 0 0 1 .656-.834ZM8 2.5a5.487 5.487 0 0 0-4.131 1.869l1.204 1.204A.25.25 0 0 1 4.896 6H1.25A.25.25 0 0 1 1 5.75V2.104a.25.25 0 0 1 .427-.177l1.38 1.38A7.002 7.002 0 0 1 14.95 7.16a.75.75 0 1 1-1.49.178A5.5 5.5 0 0 0 8 2.5Z"/>
314
330
  </svg>
331
+ <svg class="spinner-icon" viewBox="0 0 16 16" fill="currentColor" width="16" height="16">
332
+ <path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 8 0ZM1.5 8a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0Z" opacity="0.25"/>
333
+ <path d="M8 0a8 8 0 0 1 8 8h-2A6 6 0 0 0 8 2V0Z"/>
334
+ </svg>
315
335
  </button>
316
336
  <button class="btn btn-icon" id="theme-toggle" title="Toggle theme">
317
337
  <svg class="theme-icon-light" viewBox="0 0 16 16" fill="currentColor" width="16" height="16">
@@ -362,17 +382,18 @@
362
382
  </svg>
363
383
  </button>
364
384
  <div class="toolbar-meta" id="toolbar-meta">
365
- <span class="toolbar-branch" id="pr-branch">
366
- <svg viewBox="0 0 16 16" fill="currentColor" width="12" height="12">
367
- <path d="M11.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122V6A2.5 2.5 0 0110 8.5H6a1 1 0 00-1 1v1.128a2.251 2.251 0 11-1.5 0V5.372a2.25 2.25 0 111.5 0v1.836A2.492 2.492 0 016 7h4a1 1 0 001-1v-.628A2.25 2.25 0 019.5 3.25zM4.25 12a.75.75 0 100 1.5.75.75 0 000-1.5zM3.5 3.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0z"/>
368
- </svg>
369
- <span id="pr-branch-name">--</span>
370
- <button type="button" class="toolbar-branch-copy toolbar-copy-btn" id="pr-branch-copy" title="Copy branch name" aria-label="Copy branch name">
371
- <svg viewBox="0 0 16 16" fill="currentColor" width="10" height="10">
372
- <path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"></path>
373
- <path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"></path>
385
+ <span id="toolbar-base-branch-wrap" class="toolbar-base-branch-wrap" hidden>
386
+ <span class="base-branch-vs">base:</span>
387
+ <span id="toolbar-base-branch-static" class="toolbar-base-branch-static">
388
+ <svg viewBox="0 0 16 16" fill="currentColor" width="12" height="12">
389
+ <path d="M11.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122V6A2.5 2.5 0 0110 8.5H6a1 1 0 00-1 1v1.128a2.251 2.251 0 11-1.5 0V5.372a2.25 2.25 0 111.5 0v1.836A2.492 2.492 0 016 7h4a1 1 0 001-1v-.628A2.25 2.25 0 019.5 3.25zM4.25 12a.75.75 0 100 1.5.75.75 0 000-1.5zM3.5 3.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0z"/>
374
390
  </svg>
375
- </button>
391
+ <span id="toolbar-base-branch-text"></span>
392
+ </span>
393
+ <span id="base-branch-selector-wrap" class="base-branch-selector-wrap" hidden>
394
+ <label for="base-branch-select" class="sr-only">Compare against</label>
395
+ <select id="base-branch-select" class="base-branch-select" title="Change base branch for diff"></select>
396
+ </span>
376
397
  </span>
377
398
  <span class="toolbar-separator"></span>
378
399
  <span class="toolbar-stat toolbar-stat-additions" id="pr-additions">+0</span>
package/public/pr.html CHANGED
@@ -28,6 +28,7 @@
28
28
  window.__pairReview.share = config.share || null;
29
29
  window.__pairReview.enableGraphite = config.enable_graphite === true;
30
30
  window.__pairReview.chatSpinner = config.chat_spinner || 'dots';
31
+ window.__pairReview.chatEnterToSend = config.chat_enter_to_send !== false;
31
32
  document.documentElement.setAttribute('data-chat', state);
32
33
  const shortcutsState = config.chat_enable_shortcuts === false ? 'disabled' : 'enabled';
33
34
  document.documentElement.setAttribute('data-chat-shortcuts', shortcutsState);
@@ -48,6 +49,7 @@
48
49
  <!-- PR Display Styles -->
49
50
  <link rel="stylesheet" href="/css/pr.css">
50
51
  <link rel="stylesheet" href="/css/ai-summary-modal.css">
52
+ <link rel="stylesheet" href="/css/analysis-config.css">
51
53
  <link rel="stylesheet" href="/css/styles.css">
52
54
 
53
55
  <!-- Highlight.js for syntax highlighting -->