@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.
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/review-requests/SKILL.md +54 -0
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/public/css/pr.css +1081 -54
- package/public/css/repo-settings.css +452 -140
- package/public/js/components/AdvancedConfigTab.js +1364 -0
- package/public/js/components/AnalysisConfigModal.js +488 -112
- package/public/js/components/CouncilProgressModal.js +1416 -0
- package/public/js/components/TextInputDialog.js +231 -0
- package/public/js/components/TimeoutSelect.js +367 -0
- package/public/js/components/VoiceCentricConfigTab.js +1334 -0
- package/public/js/local.js +162 -83
- package/public/js/modules/analysis-history.js +185 -11
- package/public/js/modules/comment-manager.js +13 -0
- package/public/js/modules/file-comment-manager.js +28 -0
- package/public/js/pr.js +233 -115
- package/public/js/repo-settings.js +575 -106
- package/public/local.html +11 -1
- package/public/pr.html +6 -1
- package/public/repo-settings.html +28 -21
- package/public/setup.html +8 -2
- package/src/ai/analyzer.js +1262 -111
- package/src/ai/claude-cli.js +2 -2
- package/src/ai/claude-provider.js +6 -6
- package/src/ai/codex-provider.js +6 -6
- package/src/ai/copilot-provider.js +3 -3
- package/src/ai/cursor-agent-provider.js +6 -6
- package/src/ai/gemini-provider.js +6 -6
- package/src/ai/opencode-provider.js +6 -6
- package/src/ai/pi-provider.js +6 -6
- package/src/ai/prompts/baseline/consolidation/balanced.js +208 -0
- package/src/ai/prompts/baseline/consolidation/fast.js +175 -0
- package/src/ai/prompts/baseline/consolidation/thorough.js +283 -0
- package/src/ai/prompts/config.js +1 -1
- package/src/ai/prompts/index.js +26 -2
- package/src/ai/provider.js +4 -2
- package/src/database.js +417 -14
- package/src/main.js +1 -1
- package/src/routes/analysis.js +495 -10
- package/src/routes/config.js +36 -15
- package/src/routes/councils.js +351 -0
- package/src/routes/local.js +33 -11
- package/src/routes/mcp.js +9 -2
- package/src/routes/setup.js +12 -2
- package/src/routes/shared.js +126 -13
- package/src/server.js +34 -4
- package/src/utils/stats-calculator.js +2 -0
|
@@ -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
|
-
<!--
|
|
248
|
+
<!-- Analysis Levels -->
|
|
229
249
|
<section class="config-section">
|
|
230
250
|
<h4 class="section-title">
|
|
231
|
-
Analysis
|
|
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
|
-
<
|
|
240
|
-
<
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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 — 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 — 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 — 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
|
-
//
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
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
|
|
527
|
-
const
|
|
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
|
-
//
|
|
532
|
-
if (
|
|
533
|
-
|
|
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
|
-
//
|
|
541
|
-
if (
|
|
542
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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
|
-
|
|
713
|
-
|
|
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.
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
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
|
-
|
|
724
|
-
|
|
884
|
+
textarea.value = '';
|
|
885
|
+
this.updateCharacterCount(0);
|
|
725
886
|
}
|
|
887
|
+
}
|
|
726
888
|
|
|
727
|
-
|
|
728
|
-
|
|
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
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
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
|
-
//
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
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
|
-
|
|
960
|
+
}
|
|
752
961
|
|
|
753
|
-
|
|
754
|
-
|
|
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
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
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
|
|
809
|
-
this.
|
|
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
|
-
|
|
817
|
-
|
|
818
|
-
|
|
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;
|