@in-the-loop-labs/pair-review 1.4.4 → 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
@@ -13,7 +13,6 @@ class AnalysisConfigModal {
13
13
  this.selectedProvider = 'claude';
14
14
  this.selectedModel = 'opus';
15
15
  this.selectedPresets = new Set();
16
- this.rememberModel = false;
17
16
  this.repoInstructions = '';
18
17
  this.lastInstructions = '';
19
18
  this.providersLoaded = false;
@@ -43,8 +42,26 @@ class AnalysisConfigModal {
43
42
  { id: 'bugs', label: 'Bug Detection', instruction: 'Focus on potential bugs, edge cases, and error handling.' }
44
43
  ];
45
44
 
45
+ // Council tabs (lazily initialized after createModal)
46
+ this.councilTab = null; // Voice-centric council tab
47
+ this.advancedTab = null; // Level-centric (advanced) council tab
48
+
49
+ // Active tab tracking: 'single' | 'council' | 'advanced'
50
+ this.activeTab = 'single';
51
+
52
+ // Enabled levels for single-model mode (replaces skipLevel3)
53
+ this.enabledLevels = [1, 2, 3];
54
+
46
55
  this.createModal();
47
56
  this.setupEventListeners();
57
+
58
+ // Initialize council tabs if available
59
+ if (typeof VoiceCentricConfigTab !== 'undefined') {
60
+ this.councilTab = new VoiceCentricConfigTab(this.modal);
61
+ }
62
+ if (typeof AdvancedConfigTab !== 'undefined') {
63
+ this.advancedTab = new AdvancedConfigTab(this.modal);
64
+ }
48
65
  }
49
66
 
50
67
  /**
@@ -131,6 +148,14 @@ class AnalysisConfigModal {
131
148
  await this.loadProviders(true);
132
149
  this.renderProviderButtons();
133
150
 
151
+ // Propagate refreshed providers to council and advanced tabs
152
+ if (this.councilTab) {
153
+ this.councilTab.setProviders(this.providers);
154
+ }
155
+ if (this.advancedTab) {
156
+ this.advancedTab.setProviders(this.providers);
157
+ }
158
+
134
159
  if (this.availabilityCheckInProgress && attempts < maxAttempts) {
135
160
  const timeoutId = setTimeout(poll, pollInterval);
136
161
  this.pendingPollTimeouts.push(timeoutId);
@@ -218,17 +243,12 @@ class AnalysisConfigModal {
218
243
  <div class="model-cards" id="model-cards-container">
219
244
  <!-- Model cards rendered dynamically -->
220
245
  </div>
221
- <label class="remember-toggle">
222
- <input type="checkbox" id="remember-model" />
223
- <span class="toggle-switch"></span>
224
- <span class="toggle-label">Remember choices for this repository</span>
225
- </label>
226
246
  </section>
227
247
 
228
- <!-- Skip Level 3 Analysis -->
248
+ <!-- Analysis Levels -->
229
249
  <section class="config-section">
230
250
  <h4 class="section-title">
231
- Analysis Scope
251
+ Analysis Levels
232
252
  </h4>
233
253
  <div class="skip-level3-info" id="skip-level3-info" style="display: none;">
234
254
  <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
@@ -236,11 +256,23 @@ class AnalysisConfigModal {
236
256
  </svg>
237
257
  <span>Codebase-wide analysis is automatically skipped for fast-tier models.</span>
238
258
  </div>
239
- <label class="remember-toggle" id="skip-level3-toggle">
240
- <input type="checkbox" id="skip-level3" />
241
- <span class="toggle-switch"></span>
242
- <span class="toggle-label">Skip codebase-wide analysis (Level 3)</span>
243
- </label>
259
+ <div class="single-level-toggles">
260
+ <label class="remember-toggle">
261
+ <input type="checkbox" class="single-level-checkbox" data-level="1" checked />
262
+ <span class="toggle-switch"></span>
263
+ <span class="toggle-label">Level 1 &mdash; Changes in Isolation</span>
264
+ </label>
265
+ <label class="remember-toggle">
266
+ <input type="checkbox" class="single-level-checkbox" data-level="2" checked />
267
+ <span class="toggle-switch"></span>
268
+ <span class="toggle-label">Level 2 &mdash; File Context</span>
269
+ </label>
270
+ <label class="remember-toggle">
271
+ <input type="checkbox" class="single-level-checkbox" data-level="3" checked />
272
+ <span class="toggle-switch"></span>
273
+ <span class="toggle-label">Level 3 &mdash; Codebase Context</span>
274
+ </label>
275
+ </div>
244
276
  </section>
245
277
 
246
278
  <!-- Focus Presets - Hidden for now, may reintroduce later -->
@@ -308,6 +340,11 @@ class AnalysisConfigModal {
308
340
  </div>
309
341
 
310
342
  <div class="modal-footer analysis-config-footer">
343
+ <div class="council-footer-left" id="council-footer-left" style="display: none;">
344
+ <span class="council-dirty-hint" id="council-dirty-hint">Unsaved changes</span>
345
+ <button class="btn btn-sm btn-secondary" id="council-footer-save-btn"
346
+ title="Save configuration changes. Unsaved changes will be auto-saved as a new configuration when you analyze.">Save</button>
347
+ </div>
311
348
  <button class="btn btn-secondary" data-action="cancel">Cancel</button>
312
349
  <button class="btn btn-primary btn-analyze" data-action="submit" title="Start Analysis (Cmd/Ctrl+Enter)">
313
350
  <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
@@ -333,16 +370,11 @@ class AnalysisConfigModal {
333
370
  chip.addEventListener('click', () => this.togglePreset(chip.dataset.preset));
334
371
  });
335
372
 
336
- // Remember toggle
337
- const rememberCheckbox = this.modal.querySelector('#remember-model');
338
- rememberCheckbox?.addEventListener('change', (e) => {
339
- this.rememberModel = e.target.checked;
340
- });
341
-
342
- // Skip Level 3 toggle
343
- const skipLevel3Checkbox = this.modal.querySelector('#skip-level3');
344
- skipLevel3Checkbox?.addEventListener('change', (e) => {
345
- this.skipLevel3 = e.target.checked;
373
+ // Single-model level checkboxes
374
+ this.modal.querySelectorAll('.single-level-checkbox').forEach(cb => {
375
+ cb.addEventListener('change', () => {
376
+ this._updateEnabledLevels();
377
+ });
346
378
  });
347
379
 
348
380
  // Refresh providers button
@@ -356,13 +388,17 @@ class AnalysisConfigModal {
356
388
  });
357
389
 
358
390
  // Keyboard shortcut: Cmd+Enter / Ctrl+Enter to start analysis
359
- textarea?.addEventListener('keydown', (e) => {
391
+ // Listen on the entire modal so it works from both Single and Council tabs
392
+ this.modal.addEventListener('keydown', (e) => {
360
393
  if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
394
+ // Don't intercept Cmd+Enter inside comment form textareas
395
+ if (e.target.matches('.comment-textarea, .comment-edit-textarea')) return;
361
396
  e.preventDefault();
362
- // Only submit if not over character limit (button would be disabled)
363
397
  const submitBtn = this.modal.querySelector('[data-action="submit"]');
364
398
  if (submitBtn && !submitBtn.disabled) {
365
- this.handleSubmit();
399
+ this.handleSubmit().catch(err => {
400
+ console.error('Error in handleSubmit:', err);
401
+ });
366
402
  }
367
403
  }
368
404
  });
@@ -384,7 +420,9 @@ class AnalysisConfigModal {
384
420
  if (action === 'cancel') {
385
421
  this.hide();
386
422
  } else if (action === 'submit') {
387
- this.handleSubmit();
423
+ this.handleSubmit().catch(err => {
424
+ console.error('Error in handleSubmit:', err);
425
+ });
388
426
  }
389
427
  });
390
428
 
@@ -523,29 +561,28 @@ class AnalysisConfigModal {
523
561
  const selectedCard = this.modal.querySelector(`.model-card[data-model="${modelId}"]`);
524
562
  const tier = selectedCard?.dataset?.tier;
525
563
 
526
- // Update skip level 3 checkbox based on tier
527
- const skipLevel3Checkbox = this.modal.querySelector('#skip-level3');
564
+ // Update level 3 checkbox based on tier
565
+ const level3Checkbox = this.modal.querySelector('.single-level-checkbox[data-level="3"]');
528
566
  const skipLevel3Info = this.modal.querySelector('#skip-level3-info');
529
567
 
530
568
  if (tier === 'fast') {
531
- // Always check for fast tier models and show info banner
532
- if (skipLevel3Checkbox) {
533
- skipLevel3Checkbox.checked = true;
534
- this.skipLevel3 = true;
569
+ // Auto-uncheck L3 for fast tier models and show info banner
570
+ if (level3Checkbox) {
571
+ level3Checkbox.checked = false;
535
572
  }
536
573
  if (skipLevel3Info) {
537
574
  skipLevel3Info.style.display = 'flex';
538
575
  }
539
576
  } else {
540
- // Always uncheck for non-fast tiers and hide info banner
541
- if (skipLevel3Checkbox) {
542
- skipLevel3Checkbox.checked = false;
543
- this.skipLevel3 = false;
577
+ // Re-check L3 for non-fast tiers and hide info banner
578
+ if (level3Checkbox) {
579
+ level3Checkbox.checked = true;
544
580
  }
545
581
  if (skipLevel3Info) {
546
582
  skipLevel3Info.style.display = 'none';
547
583
  }
548
584
  }
585
+ this._updateEnabledLevels();
549
586
  }
550
587
 
551
588
  /**
@@ -647,8 +684,58 @@ class AnalysisConfigModal {
647
684
  /**
648
685
  * Handle form submission
649
686
  */
650
- handleSubmit() {
651
- // Extract tier from the selected model's data-tier attribute
687
+ async handleSubmit() {
688
+ if (this.activeTab === 'council') {
689
+ // Voice-centric council tab
690
+ if (!this.councilTab || !this.councilTab.validate()) return;
691
+
692
+ await this.councilTab.autoSaveIfDirty();
693
+
694
+ const councilConfig = this.councilTab.getCouncilConfig();
695
+ const councilId = this.councilTab.getSelectedCouncilId();
696
+ const selectedCouncil = this.councilTab.councils.find(c => c.id === councilId);
697
+
698
+ const config = {
699
+ isCouncil: true,
700
+ configType: 'council',
701
+ councilId: councilId,
702
+ councilName: selectedCouncil?.name || null,
703
+ councilConfig: councilConfig,
704
+ customInstructions: this.modal.querySelector('#vc-custom-instructions')?.value?.trim() || '',
705
+ repoInstructions: this.repoInstructions
706
+ };
707
+
708
+ if (this.onSubmit) this.onSubmit(config);
709
+ this.hide(true);
710
+ return;
711
+ }
712
+
713
+ if (this.activeTab === 'advanced') {
714
+ // Level-centric (advanced) council tab
715
+ if (!this.advancedTab || !this.advancedTab.validate()) return;
716
+
717
+ await this.advancedTab.autoSaveIfDirty();
718
+
719
+ const councilConfig = this.advancedTab.getCouncilConfig();
720
+ const councilId = this.advancedTab.getSelectedCouncilId();
721
+ const selectedCouncil = this.advancedTab.councils.find(c => c.id === councilId);
722
+
723
+ const config = {
724
+ isCouncil: true,
725
+ configType: 'advanced',
726
+ councilId: councilId,
727
+ councilName: selectedCouncil?.name || null,
728
+ councilConfig: councilConfig,
729
+ customInstructions: this.modal.querySelector('#council-custom-instructions')?.value?.trim() || '',
730
+ repoInstructions: this.repoInstructions
731
+ };
732
+
733
+ if (this.onSubmit) this.onSubmit(config);
734
+ this.hide(true);
735
+ return;
736
+ }
737
+
738
+ // Single model tab
652
739
  const selectedModelCard = this.modal.querySelector('.model-card.selected');
653
740
  const tier = selectedModelCard?.dataset?.tier || 'balanced';
654
741
 
@@ -659,16 +746,12 @@ class AnalysisConfigModal {
659
746
  instructions: this.buildInstructions(),
660
747
  customInstructions: this.modal.querySelector('#custom-instructions')?.value?.trim() || '',
661
748
  presets: Array.from(this.selectedPresets),
662
- rememberModel: this.rememberModel,
663
749
  repoInstructions: this.repoInstructions,
664
- skipLevel3: this.skipLevel3
750
+ enabledLevels: [...this.enabledLevels],
751
+ skipLevel3: !this.enabledLevels.includes(3)
665
752
  };
666
753
 
667
- if (this.onSubmit) {
668
- this.onSubmit(config);
669
- }
670
-
671
- // Hide with wasSubmitted=true to avoid calling onCancel
754
+ if (this.onSubmit) this.onSubmit(config);
672
755
  this.hide(true);
673
756
  }
674
757
 
@@ -679,20 +762,12 @@ class AnalysisConfigModal {
679
762
  * @param {string} options.currentModel - Currently selected model
680
763
  * @param {string} options.repoInstructions - Default instructions from repo settings
681
764
  * @param {string} options.lastInstructions - Last used custom instructions
682
- * @param {boolean} options.rememberModel - Whether model was remembered
683
765
  * @param {Function} options.onSubmit - Callback when analysis is started
684
766
  * @returns {Promise<Object|null>} Promise that resolves to config or null if cancelled
685
767
  */
686
768
  async show(options = {}) {
687
769
  if (!this.modal) return null;
688
770
 
689
- // Load providers from backend before showing modal
690
- await this.loadProviders();
691
-
692
- // Render provider buttons and model cards now that we have provider data
693
- this.renderProviderButtons();
694
- this.renderModelCards();
695
-
696
771
  return new Promise((resolve) => {
697
772
  // Store callbacks
698
773
  this.onSubmit = (config) => {
@@ -702,70 +777,348 @@ class AnalysisConfigModal {
702
777
  resolve(null);
703
778
  };
704
779
 
705
- // Set initial provider and model
706
- if (options.currentProvider && this.providers[options.currentProvider]) {
707
- this.selectProvider(options.currentProvider);
708
- } else if (Object.keys(this.providers).length > 0) {
709
- // Default to first available provider
710
- this.selectProvider(Object.keys(this.providers)[0]);
780
+ // Show modal immediately with loading state (providers may take a moment)
781
+ this._showLoading(true);
782
+ this.modal.style.display = 'flex';
783
+ requestAnimationFrame(() => {
784
+ this.modal.classList.add('visible');
785
+ });
786
+ this.isVisible = true;
787
+
788
+ // Add escape key listener when modal is shown
789
+ document.addEventListener('keydown', this.escapeHandler);
790
+
791
+ // Load providers and populate content in the background
792
+ this._initializeContent(options);
793
+ });
794
+ }
795
+
796
+ /**
797
+ * Initialize modal content after it's visible.
798
+ * Loads providers, renders UI, and configures options.
799
+ * @param {Object} options - Configuration options passed to show()
800
+ * @private
801
+ */
802
+ async _initializeContent(options) {
803
+ try {
804
+ await this.loadProviders();
805
+ } catch (error) {
806
+ console.error('Error loading providers:', error);
807
+ }
808
+
809
+ // Render provider buttons and model cards now that we have provider data
810
+ this.renderProviderButtons();
811
+ this.renderModelCards();
812
+
813
+ // Build three-tab layout
814
+ this._injectTabLayout(options);
815
+
816
+ // Initialize voice-centric council tab
817
+ if (this.councilTab) {
818
+ const councilPanel = this.modal.querySelector('#tab-panel-council');
819
+ this.councilTab.inject(councilPanel);
820
+ this.councilTab.setProviders(this.providers);
821
+
822
+ if (options.repoInstructions) {
823
+ this.councilTab.setRepoInstructions(options.repoInstructions);
824
+ }
825
+ if (options.lastInstructions) {
826
+ this.councilTab.setLastInstructions(options.lastInstructions);
711
827
  }
712
- if (options.currentModel) {
713
- this.selectModel(options.currentModel);
828
+ this.councilTab.setDefaultOrchestration(options.currentProvider, options.currentModel);
829
+ const councilDefault = options.lastCouncilId || options.defaultCouncilId || null;
830
+ if (councilDefault) {
831
+ this.councilTab.setDefaultCouncilId(councilDefault);
714
832
  }
833
+ }
834
+
835
+ // Initialize advanced (level-centric) tab
836
+ if (this.advancedTab) {
837
+ const advancedPanel = this.modal.querySelector('#tab-panel-advanced');
838
+ this.advancedTab.inject(advancedPanel);
839
+ this.advancedTab.setProviders(this.providers);
715
840
 
716
841
  if (options.repoInstructions) {
717
- this.repoInstructions = options.repoInstructions;
718
- const repoBanner = this.modal.querySelector('#repo-instructions-banner');
719
- if (repoBanner) repoBanner.style.display = 'flex';
720
- const repoText = this.modal.querySelector('#repo-instructions-text');
721
- if (repoText) repoText.textContent = options.repoInstructions;
842
+ this.advancedTab.setRepoInstructions(options.repoInstructions);
843
+ }
844
+ if (options.lastInstructions) {
845
+ this.advancedTab.setLastInstructions(options.lastInstructions);
846
+ }
847
+ this.advancedTab.setDefaultOrchestration(options.currentProvider, options.currentModel);
848
+ const councilDefault = options.lastCouncilId || options.defaultCouncilId || null;
849
+ if (councilDefault) {
850
+ this.advancedTab.setDefaultCouncilId(councilDefault);
851
+ }
852
+ }
853
+
854
+ // Set initial provider and model
855
+ if (options.currentProvider && this.providers[options.currentProvider]) {
856
+ this.selectProvider(options.currentProvider);
857
+ } else if (Object.keys(this.providers).length > 0) {
858
+ // Default to first available provider
859
+ this.selectProvider(Object.keys(this.providers)[0]);
860
+ }
861
+ if (options.currentModel) {
862
+ this.selectModel(options.currentModel);
863
+ }
864
+
865
+ if (options.repoInstructions) {
866
+ this.repoInstructions = options.repoInstructions;
867
+ const repoBanner = this.modal.querySelector('#repo-instructions-banner');
868
+ if (repoBanner) repoBanner.style.display = 'flex';
869
+ const repoText = this.modal.querySelector('#repo-instructions-text');
870
+ if (repoText) repoText.textContent = options.repoInstructions;
871
+ } else {
872
+ const repoBanner = this.modal.querySelector('#repo-instructions-banner');
873
+ if (repoBanner) repoBanner.style.display = 'none';
874
+ }
875
+
876
+ // Always get textarea reference and set its value
877
+ // This ensures any stale content from race conditions is cleared
878
+ const textarea = this.modal.querySelector('#custom-instructions');
879
+ if (textarea) {
880
+ if (options.lastInstructions) {
881
+ textarea.value = options.lastInstructions;
882
+ this.updateCharacterCount(options.lastInstructions.length);
722
883
  } else {
723
- const repoBanner = this.modal.querySelector('#repo-instructions-banner');
724
- if (repoBanner) repoBanner.style.display = 'none';
884
+ textarea.value = '';
885
+ this.updateCharacterCount(0);
725
886
  }
887
+ }
726
888
 
727
- // Always get textarea reference and set its value
728
- // This ensures any stale content from race conditions is cleared
889
+ // Remove loading state and reveal content
890
+ this._showLoading(false);
891
+
892
+ // Focus the textarea without scrolling the modal body
893
+ setTimeout(() => {
729
894
  const textarea = this.modal.querySelector('#custom-instructions');
895
+ const modalBody = this.modal.querySelector('.analysis-config-body');
730
896
  if (textarea) {
731
- if (options.lastInstructions) {
732
- textarea.value = options.lastInstructions;
733
- this.updateCharacterCount(options.lastInstructions.length);
734
- } else {
735
- textarea.value = '';
736
- this.updateCharacterCount(0);
897
+ textarea.focus({ preventScroll: true });
898
+ // Ensure modal body is scrolled to top
899
+ if (modalBody) {
900
+ modalBody.scrollTop = 0;
737
901
  }
738
902
  }
903
+ }, 50);
904
+ }
739
905
 
740
- if (options.rememberModel) {
741
- this.rememberModel = true;
742
- const rememberCheckbox = this.modal.querySelector('#remember-model');
743
- if (rememberCheckbox) rememberCheckbox.checked = true;
744
- }
906
+ /**
907
+ * Build the three-tab layout in the modal body.
908
+ * Wraps existing single-model content into the first tab panel,
909
+ * then creates council and advanced panels.
910
+ * @param {Object} options - show() options (for defaultTab)
911
+ * @private
912
+ */
913
+ _injectTabLayout(options) {
914
+ const modalBody = this.modal.querySelector('.analysis-config-body');
915
+ if (!modalBody) return;
916
+
917
+ // Only inject DOM once; on subsequent opens the tab bar already exists
918
+ if (!modalBody.querySelector('.analysis-tab-bar')) {
919
+ // Wrap existing content in a "Single Model" tab panel
920
+ const existingContent = Array.from(modalBody.children);
921
+ const singlePanel = document.createElement('div');
922
+ singlePanel.id = 'tab-panel-single';
923
+ singlePanel.className = 'tab-panel active';
924
+ existingContent.forEach(child => singlePanel.appendChild(child));
925
+
926
+ // Create council (voice-centric) tab panel
927
+ const councilPanel = document.createElement('div');
928
+ councilPanel.id = 'tab-panel-council';
929
+ councilPanel.className = 'tab-panel';
930
+ councilPanel.style.display = 'none';
931
+
932
+ // Create advanced (level-centric) tab panel
933
+ const advancedPanel = document.createElement('div');
934
+ advancedPanel.id = 'tab-panel-advanced';
935
+ advancedPanel.className = 'tab-panel';
936
+ advancedPanel.style.display = 'none';
937
+
938
+ // Create tab bar
939
+ const tabBar = document.createElement('div');
940
+ tabBar.className = 'analysis-tab-bar';
941
+ tabBar.innerHTML = `
942
+ <button class="analysis-tab active" data-tab="single">Single Model</button>
943
+ <button class="analysis-tab" data-tab="council">Council <span class="beta-badge">BETA</span></button>
944
+ <button class="analysis-tab" data-tab="advanced">Advanced <span class="beta-badge">BETA</span></button>
945
+ `;
745
946
 
746
- // Show modal with animation
747
- this.modal.style.display = 'flex';
748
- requestAnimationFrame(() => {
749
- this.modal.classList.add('visible');
947
+ // Assemble
948
+ modalBody.innerHTML = '';
949
+ modalBody.appendChild(tabBar);
950
+ modalBody.appendChild(singlePanel);
951
+ modalBody.appendChild(councilPanel);
952
+ modalBody.appendChild(advancedPanel);
953
+
954
+ // Tab click listeners
955
+ tabBar.querySelectorAll('.analysis-tab').forEach(tab => {
956
+ tab.addEventListener('click', () => {
957
+ this._switchTab(tab.dataset.tab);
958
+ });
750
959
  });
751
- this.isVisible = true;
960
+ }
752
961
 
753
- // Add escape key listener when modal is shown
754
- document.addEventListener('keydown', this.escapeHandler);
962
+ // Always apply the default tab hide() resets to 'single', so on re-open
963
+ // we must restore the remembered tab from localStorage / repo settings.
964
+ const defaultTab = options.defaultTab || 'single';
965
+ this._switchTab(defaultTab, true);
966
+ }
755
967
 
756
- // Focus the textarea without scrolling the modal body
757
- setTimeout(() => {
758
- const textarea = this.modal.querySelector('#custom-instructions');
759
- const modalBody = this.modal.querySelector('.analysis-config-body');
760
- if (textarea) {
761
- textarea.focus({ preventScroll: true });
762
- // Ensure modal body is scrolled to top
763
- if (modalBody) {
764
- modalBody.scrollTop = 0;
765
- }
766
- }
767
- }, 200);
968
+ /**
969
+ * Switch the active tab. Handles panel visibility, instruction sync,
970
+ * lazy council loading, and submit button text.
971
+ * @param {string} tabId - 'single', 'council', or 'advanced'
972
+ * @private
973
+ */
974
+ _switchTab(tabId, skipCallback = false) {
975
+ this.activeTab = tabId;
976
+
977
+ // Notify listeners of tab change (for localStorage persistence)
978
+ if (!skipCallback && this.onTabChange) {
979
+ this.onTabChange(tabId);
980
+ }
981
+
982
+ const tabBar = this.modal.querySelector('.analysis-tab-bar');
983
+ if (tabBar) {
984
+ tabBar.querySelectorAll('.analysis-tab').forEach(t => {
985
+ t.classList.toggle('active', t.dataset.tab === tabId);
986
+ });
987
+ }
988
+
989
+ const singlePanel = this.modal.querySelector('#tab-panel-single');
990
+ const councilPanel = this.modal.querySelector('#tab-panel-council');
991
+ const advancedPanel = this.modal.querySelector('#tab-panel-advanced');
992
+
993
+ // Hide all panels
994
+ if (singlePanel) singlePanel.style.display = 'none';
995
+ if (councilPanel) councilPanel.style.display = 'none';
996
+ if (advancedPanel) advancedPanel.style.display = 'none';
997
+
998
+ if (tabId === 'single') {
999
+ if (singlePanel) singlePanel.style.display = '';
1000
+ // Sync instructions from whichever council tab was last active
1001
+ const vcTextarea = this.modal.querySelector('#vc-custom-instructions');
1002
+ const advTextarea = this.modal.querySelector('#council-custom-instructions');
1003
+ const singleTextarea = this.modal.querySelector('#custom-instructions');
1004
+ const source = vcTextarea?.value || advTextarea?.value || '';
1005
+ if (singleTextarea && source) {
1006
+ singleTextarea.value = source;
1007
+ singleTextarea.dispatchEvent(new Event('input', { bubbles: true }));
1008
+ }
1009
+ } else if (tabId === 'council') {
1010
+ if (councilPanel) councilPanel.style.display = '';
1011
+ // Sync instructions to council tab
1012
+ const singleTextarea = this.modal.querySelector('#custom-instructions');
1013
+ const vcTextarea = this.modal.querySelector('#vc-custom-instructions');
1014
+ if (singleTextarea && vcTextarea) {
1015
+ vcTextarea.value = singleTextarea.value;
1016
+ }
1017
+ // Load councils on first switch
1018
+ if (this.councilTab && !this.councilTab._councilsLoaded) {
1019
+ this.councilTab.loadCouncils();
1020
+ }
1021
+ if (this.councilTab) {
1022
+ this.councilTab._updateAllVoiceDropdowns();
1023
+ }
1024
+ } else if (tabId === 'advanced') {
1025
+ if (advancedPanel) advancedPanel.style.display = '';
1026
+ // Sync instructions to advanced tab
1027
+ const singleTextarea = this.modal.querySelector('#custom-instructions');
1028
+ const advTextarea = this.modal.querySelector('#council-custom-instructions');
1029
+ if (singleTextarea && advTextarea) {
1030
+ advTextarea.value = singleTextarea.value;
1031
+ }
1032
+ // Load councils on first switch
1033
+ if (this.advancedTab && !this.advancedTab._councilsLoaded) {
1034
+ this.advancedTab.loadCouncils();
1035
+ }
1036
+ if (this.advancedTab) {
1037
+ this.advancedTab._updateAllVoiceDropdowns();
1038
+ }
1039
+ }
1040
+
1041
+ // Update submit button text
1042
+ const submitBtnSpan = this.modal.querySelector('[data-action="submit"] span');
1043
+ if (submitBtnSpan) {
1044
+ if (tabId === 'council' || tabId === 'advanced') {
1045
+ submitBtnSpan.textContent = 'Analyze with Council';
1046
+ } else {
1047
+ submitBtnSpan.textContent = 'Start Analysis';
1048
+ }
1049
+ }
1050
+
1051
+ // Show/hide council footer (dirty hint)
1052
+ const dirtyHintContainer = this.modal.querySelector('#council-footer-left');
1053
+ if (dirtyHintContainer) {
1054
+ if (tabId === 'council' && this.councilTab) {
1055
+ dirtyHintContainer.style.display = this.councilTab.isDirty ? '' : 'none';
1056
+ } else if (tabId === 'advanced' && this.advancedTab) {
1057
+ dirtyHintContainer.style.display = this.advancedTab._isDirty ? '' : 'none';
1058
+ } else {
1059
+ dirtyHintContainer.style.display = 'none';
1060
+ }
1061
+ }
1062
+ }
1063
+
1064
+ /**
1065
+ * Update the enabledLevels array from the single-model level checkboxes.
1066
+ * Enforces at least one level must be enabled.
1067
+ * @private
1068
+ */
1069
+ _updateEnabledLevels() {
1070
+ const checkboxes = this.modal.querySelectorAll('.single-level-checkbox');
1071
+ const levels = [];
1072
+ checkboxes.forEach(cb => {
1073
+ if (cb.checked) levels.push(parseInt(cb.dataset.level, 10));
768
1074
  });
1075
+
1076
+ // Enforce at least one level
1077
+ if (levels.length === 0) {
1078
+ // Re-check L1 as minimum
1079
+ const l1 = this.modal.querySelector('.single-level-checkbox[data-level="1"]');
1080
+ if (l1) l1.checked = true;
1081
+ levels.push(1);
1082
+ if (window.toast) {
1083
+ window.toast.showWarning('At least one analysis level must be enabled.');
1084
+ }
1085
+ }
1086
+
1087
+ this.enabledLevels = levels;
1088
+ // Backward-compat: keep skipLevel3 in sync
1089
+ this.skipLevel3 = !levels.includes(3);
1090
+ }
1091
+
1092
+ /**
1093
+ * Toggle loading state overlay on the modal body
1094
+ * @param {boolean} loading - Whether to show the loading state
1095
+ * @private
1096
+ */
1097
+ _showLoading(loading) {
1098
+ const body = this.modal.querySelector('.analysis-config-body');
1099
+ const footer = this.modal.querySelector('.analysis-config-footer');
1100
+ const submitBtn = this.modal.querySelector('[data-action="submit"]');
1101
+
1102
+ if (loading) {
1103
+ // Add loading overlay to body
1104
+ let overlay = this.modal.querySelector('.config-loading-overlay');
1105
+ if (!overlay) {
1106
+ overlay = document.createElement('div');
1107
+ overlay.className = 'config-loading-overlay';
1108
+ overlay.innerHTML = `
1109
+ <div class="config-loading-spinner"></div>
1110
+ <span>Loading providers…</span>
1111
+ `;
1112
+ body.style.position = 'relative';
1113
+ body.appendChild(overlay);
1114
+ }
1115
+ overlay.style.display = '';
1116
+ if (submitBtn) submitBtn.disabled = true;
1117
+ } else {
1118
+ const overlay = this.modal.querySelector('.config-loading-overlay');
1119
+ if (overlay) overlay.style.display = 'none';
1120
+ if (submitBtn) submitBtn.disabled = false;
1121
+ }
769
1122
  }
770
1123
 
771
1124
  /**
@@ -805,22 +1158,45 @@ class AnalysisConfigModal {
805
1158
  this.updateCharacterCount(0);
806
1159
  const repoExpanded = this.modal.querySelector('#repo-instructions-expanded');
807
1160
  if (repoExpanded) repoExpanded.style.display = 'none';
808
- // Reset rememberModel state to prevent stale values on next show
809
- this.rememberModel = false;
810
- const rememberCheckbox = this.modal.querySelector('#remember-model');
811
- if (rememberCheckbox) {
812
- rememberCheckbox.checked = false;
813
- }
814
- // Reset skipLevel3 state
1161
+ // Reset level checkboxes
1162
+ this.enabledLevels = [1, 2, 3];
815
1163
  this.skipLevel3 = false;
816
- const skipLevel3Checkbox = this.modal.querySelector('#skip-level3');
817
- if (skipLevel3Checkbox) {
818
- skipLevel3Checkbox.checked = false;
819
- }
1164
+ this.modal.querySelectorAll('.single-level-checkbox').forEach(cb => {
1165
+ cb.checked = true;
1166
+ });
820
1167
  const skipLevel3Info = this.modal.querySelector('#skip-level3-info');
821
1168
  if (skipLevel3Info) {
822
1169
  skipLevel3Info.style.display = 'none';
823
1170
  }
1171
+ // Reset to single-model tab for next open
1172
+ this.activeTab = 'single';
1173
+ if (this.councilTab) {
1174
+ this.councilTab._isDirty = false;
1175
+ }
1176
+ if (this.advancedTab) {
1177
+ this.advancedTab._isDirty = false;
1178
+ }
1179
+ const tabBar = this.modal.querySelector('.analysis-tab-bar');
1180
+ if (tabBar) {
1181
+ tabBar.querySelectorAll('.analysis-tab').forEach(t => {
1182
+ t.classList.toggle('active', t.dataset.tab === 'single');
1183
+ });
1184
+ }
1185
+ const singlePanel = this.modal.querySelector('#tab-panel-single');
1186
+ const councilPanel = this.modal.querySelector('#tab-panel-council');
1187
+ const advancedPanel = this.modal.querySelector('#tab-panel-advanced');
1188
+ if (singlePanel) singlePanel.style.display = '';
1189
+ if (councilPanel) councilPanel.style.display = 'none';
1190
+ if (advancedPanel) advancedPanel.style.display = 'none';
1191
+ // Reset dirty hint container
1192
+ const dirtyHintContainer = this.modal.querySelector('#council-footer-left');
1193
+ if (dirtyHintContainer) dirtyHintContainer.style.display = 'none';
1194
+ // Reset submit button text
1195
+ const submitBtnSpan = this.modal.querySelector('[data-action="submit"] span');
1196
+ if (submitBtnSpan) submitBtnSpan.textContent = 'Start Analysis';
1197
+ // Clear loading overlay if still present
1198
+ const loadingOverlay = this.modal.querySelector('.config-loading-overlay');
1199
+ if (loadingOverlay) loadingOverlay.style.display = 'none';
824
1200
  // Clear callbacks
825
1201
  this.onSubmit = null;
826
1202
  this.onCancel = null;