@in-the-loop-labs/pair-review 1.4.3 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/review-requests/SKILL.md +54 -0
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/public/css/pr.css +1081 -54
- package/public/css/repo-settings.css +452 -140
- package/public/js/components/AdvancedConfigTab.js +1364 -0
- package/public/js/components/AnalysisConfigModal.js +488 -112
- package/public/js/components/CouncilProgressModal.js +1416 -0
- package/public/js/components/TextInputDialog.js +231 -0
- package/public/js/components/TimeoutSelect.js +367 -0
- package/public/js/components/VoiceCentricConfigTab.js +1334 -0
- package/public/js/local.js +162 -83
- package/public/js/modules/analysis-history.js +185 -11
- package/public/js/modules/comment-manager.js +13 -0
- package/public/js/modules/file-comment-manager.js +28 -0
- package/public/js/pr.js +233 -115
- package/public/js/repo-settings.js +575 -106
- package/public/local.html +11 -1
- package/public/pr.html +6 -1
- package/public/repo-settings.html +28 -21
- package/public/setup.html +8 -2
- package/src/ai/analyzer.js +1262 -111
- package/src/ai/claude-cli.js +2 -2
- package/src/ai/claude-provider.js +6 -6
- package/src/ai/codex-provider.js +6 -6
- package/src/ai/copilot-provider.js +3 -3
- package/src/ai/cursor-agent-provider.js +6 -6
- package/src/ai/gemini-provider.js +6 -6
- package/src/ai/opencode-provider.js +6 -6
- package/src/ai/pi-provider.js +6 -6
- package/src/ai/prompts/baseline/consolidation/balanced.js +208 -0
- package/src/ai/prompts/baseline/consolidation/fast.js +175 -0
- package/src/ai/prompts/baseline/consolidation/thorough.js +283 -0
- package/src/ai/prompts/config.js +1 -1
- package/src/ai/prompts/index.js +26 -2
- package/src/ai/provider.js +4 -2
- package/src/database.js +417 -14
- package/src/main.js +1 -1
- package/src/routes/analysis.js +495 -10
- package/src/routes/config.js +36 -15
- package/src/routes/councils.js +351 -0
- package/src/routes/local.js +33 -11
- package/src/routes/mcp.js +9 -2
- package/src/routes/setup.js +12 -2
- package/src/routes/shared.js +126 -13
- package/src/server.js +34 -4
- package/src/utils/stats-calculator.js +2 -0
|
@@ -13,6 +13,7 @@ class RepoSettingsPage {
|
|
|
13
13
|
this.hasUnsavedChanges = false;
|
|
14
14
|
this.providers = {};
|
|
15
15
|
this.selectedProvider = null;
|
|
16
|
+
this.councils = [];
|
|
16
17
|
|
|
17
18
|
this.init();
|
|
18
19
|
}
|
|
@@ -33,6 +34,9 @@ class RepoSettingsPage {
|
|
|
33
34
|
// Load providers first (needed to render model cards)
|
|
34
35
|
await this.loadProviders();
|
|
35
36
|
|
|
37
|
+
// Load councils (for default council dropdown)
|
|
38
|
+
await this.loadCouncils();
|
|
39
|
+
|
|
36
40
|
// Load settings
|
|
37
41
|
await this.loadSettings();
|
|
38
42
|
}
|
|
@@ -131,8 +135,60 @@ class RepoSettingsPage {
|
|
|
131
135
|
}
|
|
132
136
|
|
|
133
137
|
setupEventListeners() {
|
|
134
|
-
//
|
|
135
|
-
|
|
138
|
+
// Provider select
|
|
139
|
+
const providerSelect = document.getElementById('provider-select');
|
|
140
|
+
if (providerSelect) {
|
|
141
|
+
providerSelect.addEventListener('change', () => {
|
|
142
|
+
const newProviderId = providerSelect.value;
|
|
143
|
+
const previousProvider = this.selectedProvider;
|
|
144
|
+
this.selectedProvider = newProviderId;
|
|
145
|
+
this.currentSettings.default_provider = newProviderId;
|
|
146
|
+
|
|
147
|
+
// Re-render model select for the new provider
|
|
148
|
+
this.renderModelSelect();
|
|
149
|
+
|
|
150
|
+
// Try to map the model tier to the new provider
|
|
151
|
+
if (previousProvider && previousProvider !== newProviderId) {
|
|
152
|
+
const oldProvider = this.providers[previousProvider];
|
|
153
|
+
const newProvider = this.providers[newProviderId];
|
|
154
|
+
|
|
155
|
+
if (oldProvider && newProvider) {
|
|
156
|
+
const currentModel = oldProvider.models.find(m => m.id === this.currentSettings.default_model);
|
|
157
|
+
if (currentModel) {
|
|
158
|
+
const matchingModel = newProvider.models.find(m => m.tier === currentModel.tier);
|
|
159
|
+
const defaultModel = newProvider.models.find(m => m.default);
|
|
160
|
+
const fallbackModel = matchingModel || defaultModel || newProvider.models[0];
|
|
161
|
+
this.currentSettings.default_model = fallbackModel.id;
|
|
162
|
+
} else {
|
|
163
|
+
const defaultModel = newProvider.models.find(m => m.default) || newProvider.models[0];
|
|
164
|
+
this.currentSettings.default_model = defaultModel.id;
|
|
165
|
+
}
|
|
166
|
+
} else if (newProvider) {
|
|
167
|
+
const defaultModel = newProvider.models.find(m => m.default) || newProvider.models[0];
|
|
168
|
+
this.currentSettings.default_model = defaultModel.id;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Update model select and card to reflect the mapped model
|
|
172
|
+
const modelSelect = document.getElementById('model-select');
|
|
173
|
+
if (modelSelect) {
|
|
174
|
+
modelSelect.value = this.currentSettings.default_model;
|
|
175
|
+
}
|
|
176
|
+
this.renderModelCard();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
this.checkForChanges();
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Model select
|
|
184
|
+
const modelSelect = document.getElementById('model-select');
|
|
185
|
+
if (modelSelect) {
|
|
186
|
+
modelSelect.addEventListener('change', () => {
|
|
187
|
+
this.currentSettings.default_model = modelSelect.value;
|
|
188
|
+
this.renderModelCard();
|
|
189
|
+
this.checkForChanges();
|
|
190
|
+
});
|
|
191
|
+
}
|
|
136
192
|
|
|
137
193
|
// Instructions textarea
|
|
138
194
|
const textarea = document.getElementById('default-instructions');
|
|
@@ -144,6 +200,18 @@ class RepoSettingsPage {
|
|
|
144
200
|
});
|
|
145
201
|
}
|
|
146
202
|
|
|
203
|
+
// Analysis mode segmented control
|
|
204
|
+
const modeToggle = document.getElementById('analysis-mode-toggle');
|
|
205
|
+
if (modeToggle) {
|
|
206
|
+
modeToggle.addEventListener('click', (e) => {
|
|
207
|
+
const btn = e.target.closest('.mode-btn');
|
|
208
|
+
if (!btn) return;
|
|
209
|
+
this.setAnalysisMode(btn.dataset.mode);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Council custom dropdown listeners are attached in renderCouncilDropdown()
|
|
214
|
+
|
|
147
215
|
// Form submission
|
|
148
216
|
const form = document.getElementById('settings-form');
|
|
149
217
|
if (form) {
|
|
@@ -204,109 +272,340 @@ class RepoSettingsPage {
|
|
|
204
272
|
}
|
|
205
273
|
|
|
206
274
|
// Render provider buttons now that we have data
|
|
207
|
-
this.
|
|
275
|
+
this.renderProviderSelect();
|
|
208
276
|
|
|
209
277
|
} catch (error) {
|
|
210
278
|
console.error('Error loading providers:', error);
|
|
211
279
|
// No hardcoded fallback — rely on the /api/providers endpoint as the single source of truth.
|
|
212
280
|
// If the endpoint is unavailable, show an empty state rather than stale data.
|
|
213
281
|
this.providers = {};
|
|
214
|
-
this.
|
|
282
|
+
this.renderProviderSelect();
|
|
215
283
|
this.showToast('error', 'Failed to load AI providers. Please refresh the page.');
|
|
216
284
|
}
|
|
217
285
|
}
|
|
218
286
|
|
|
219
287
|
/**
|
|
220
|
-
*
|
|
288
|
+
* Load saved councils for the default council dropdown
|
|
221
289
|
*/
|
|
222
|
-
|
|
223
|
-
const container = document.getElementById('
|
|
290
|
+
async loadCouncils() {
|
|
291
|
+
const container = document.getElementById('default-council-dropdown');
|
|
224
292
|
if (!container) return;
|
|
225
293
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
}
|
|
294
|
+
try {
|
|
295
|
+
const response = await fetch('/api/councils');
|
|
296
|
+
if (!response.ok) throw new Error('Failed to fetch councils');
|
|
297
|
+
const data = await response.json();
|
|
298
|
+
this.councils = data.councils || [];
|
|
299
|
+
} catch (error) {
|
|
300
|
+
console.error('Error loading councils:', error);
|
|
301
|
+
this.councils = [];
|
|
302
|
+
}
|
|
303
|
+
}
|
|
235
304
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
305
|
+
/**
|
|
306
|
+
* Get the display label for a council type
|
|
307
|
+
* @param {string} type - Council type ('council' or 'advanced')
|
|
308
|
+
* @returns {{ label: string, cssClass: string }}
|
|
309
|
+
*/
|
|
310
|
+
getCouncilTypeBadge(type) {
|
|
311
|
+
if (type === 'advanced') {
|
|
312
|
+
return { label: 'Advanced', cssClass: 'badge-advanced' };
|
|
313
|
+
}
|
|
314
|
+
return { label: 'Standard', cssClass: 'badge-standard' };
|
|
240
315
|
}
|
|
241
316
|
|
|
242
317
|
/**
|
|
243
|
-
*
|
|
318
|
+
* Render the custom council dropdown
|
|
244
319
|
*/
|
|
245
|
-
|
|
246
|
-
|
|
320
|
+
renderCouncilDropdown() {
|
|
321
|
+
const container = document.getElementById('default-council-dropdown');
|
|
322
|
+
if (!container) return;
|
|
247
323
|
|
|
248
|
-
const
|
|
249
|
-
this.
|
|
324
|
+
const selectedId = this.currentSettings.default_council_id || '';
|
|
325
|
+
const selectedCouncil = this.councils.find(c => c.id === selectedId);
|
|
326
|
+
|
|
327
|
+
// Build trigger display
|
|
328
|
+
let triggerHTML;
|
|
329
|
+
if (selectedCouncil) {
|
|
330
|
+
const badge = this.getCouncilTypeBadge(selectedCouncil.type);
|
|
331
|
+
triggerHTML = `<span class="trigger-text">${this.escapeHtml(selectedCouncil.name)}</span>
|
|
332
|
+
<span class="council-type-badge ${badge.cssClass}">${badge.label}</span>`;
|
|
333
|
+
} else if (this.councils.length > 0) {
|
|
334
|
+
triggerHTML = '<span class="trigger-text placeholder">Select a council...</span>';
|
|
335
|
+
} else {
|
|
336
|
+
triggerHTML = '<span class="trigger-text placeholder">No councils yet — create one from the analysis config</span>';
|
|
337
|
+
}
|
|
250
338
|
|
|
251
|
-
|
|
252
|
-
|
|
339
|
+
// Build option list — sort alphabetically by name
|
|
340
|
+
const sortedCouncils = [...this.councils].sort((a, b) =>
|
|
341
|
+
(a.name || '').localeCompare(b.name || '', undefined, { sensitivity: 'base' })
|
|
342
|
+
);
|
|
343
|
+
let optionsHTML = '';
|
|
344
|
+
|
|
345
|
+
for (const council of sortedCouncils) {
|
|
346
|
+
const badge = this.getCouncilTypeBadge(council.type);
|
|
347
|
+
const isSelected = council.id === selectedId;
|
|
348
|
+
optionsHTML += `<div class="custom-dropdown-option${isSelected ? ' selected' : ''}" data-value="${this.escapeHtml(council.id)}" role="option" aria-selected="${isSelected}">
|
|
349
|
+
<span class="option-name">${this.escapeHtml(council.name)}</span>
|
|
350
|
+
<span class="council-type-badge ${badge.cssClass}">${badge.label}</span>
|
|
351
|
+
</div>`;
|
|
253
352
|
}
|
|
254
353
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
354
|
+
container.innerHTML = `
|
|
355
|
+
<button type="button" class="custom-dropdown-trigger" aria-haspopup="listbox" aria-expanded="false">
|
|
356
|
+
${triggerHTML}
|
|
357
|
+
</button>
|
|
358
|
+
<div class="custom-dropdown-list" role="listbox">
|
|
359
|
+
${optionsHTML}
|
|
360
|
+
</div>
|
|
361
|
+
`;
|
|
362
|
+
|
|
363
|
+
// Attach event listeners for the dropdown
|
|
364
|
+
this.setupCouncilDropdownListeners(container);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Set up event listeners for the custom council dropdown
|
|
369
|
+
* @param {HTMLElement} container - The dropdown container element
|
|
370
|
+
*/
|
|
371
|
+
setupCouncilDropdownListeners(container) {
|
|
372
|
+
const trigger = container.querySelector('.custom-dropdown-trigger');
|
|
373
|
+
const list = container.querySelector('.custom-dropdown-list');
|
|
374
|
+
if (!trigger || !list) return;
|
|
375
|
+
|
|
376
|
+
// Track focused option index for keyboard navigation
|
|
377
|
+
let focusedIndex = -1;
|
|
378
|
+
const getOptions = () => Array.from(list.querySelectorAll('.custom-dropdown-option'));
|
|
379
|
+
|
|
380
|
+
const updateFocus = (options, index) => {
|
|
381
|
+
options.forEach(opt => opt.classList.remove('focused'));
|
|
382
|
+
if (index >= 0 && index < options.length) {
|
|
383
|
+
options[index].classList.add('focused');
|
|
384
|
+
options[index].scrollIntoView({ block: 'nearest' });
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
// Toggle dropdown on trigger click
|
|
389
|
+
trigger.addEventListener('click', () => {
|
|
390
|
+
const isOpen = container.classList.contains('open');
|
|
391
|
+
if (isOpen) {
|
|
392
|
+
this.closeCouncilDropdown(container);
|
|
393
|
+
} else {
|
|
394
|
+
this.openCouncilDropdown(container);
|
|
395
|
+
focusedIndex = -1;
|
|
396
|
+
}
|
|
258
397
|
});
|
|
259
398
|
|
|
260
|
-
//
|
|
261
|
-
|
|
399
|
+
// Select option on click
|
|
400
|
+
list.addEventListener('click', (e) => {
|
|
401
|
+
const option = e.target.closest('.custom-dropdown-option');
|
|
402
|
+
if (!option) return;
|
|
403
|
+
this.selectCouncilOption(container, option.dataset.value);
|
|
404
|
+
});
|
|
262
405
|
|
|
263
|
-
//
|
|
264
|
-
|
|
265
|
-
const
|
|
266
|
-
const newProvider = this.providers[providerId];
|
|
406
|
+
// Keyboard navigation
|
|
407
|
+
trigger.addEventListener('keydown', (e) => {
|
|
408
|
+
const isOpen = container.classList.contains('open');
|
|
267
409
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (!newProvider.models || newProvider.models.length === 0) {
|
|
273
|
-
console.error(`Provider "${providerId}" has no models - this should not happen`);
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
const defaultModel = newProvider.models.find(m => m.default) || newProvider.models[0];
|
|
277
|
-
this.selectModel(defaultModel.id, markAsChanged);
|
|
278
|
-
this.checkForChanges();
|
|
410
|
+
if (e.key === 'Escape' && isOpen) {
|
|
411
|
+
e.preventDefault();
|
|
412
|
+
this.closeCouncilDropdown(container);
|
|
413
|
+
trigger.focus();
|
|
279
414
|
return;
|
|
280
415
|
}
|
|
281
416
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
417
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
418
|
+
e.preventDefault();
|
|
419
|
+
if (!isOpen) {
|
|
420
|
+
this.openCouncilDropdown(container);
|
|
421
|
+
focusedIndex = -1;
|
|
422
|
+
} else {
|
|
423
|
+
const options = getOptions();
|
|
424
|
+
if (focusedIndex >= 0 && focusedIndex < options.length) {
|
|
425
|
+
this.selectCouncilOption(container, options[focusedIndex].dataset.value);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
289
430
|
|
|
290
|
-
|
|
291
|
-
|
|
431
|
+
if ((e.key === 'ArrowDown' || e.key === 'ArrowUp') && isOpen) {
|
|
432
|
+
e.preventDefault();
|
|
433
|
+
const options = getOptions();
|
|
434
|
+
if (e.key === 'ArrowDown') {
|
|
435
|
+
focusedIndex = Math.min(focusedIndex + 1, options.length - 1);
|
|
436
|
+
} else {
|
|
437
|
+
focusedIndex = Math.max(focusedIndex - 1, 0);
|
|
292
438
|
}
|
|
439
|
+
updateFocus(options, focusedIndex);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
293
442
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
443
|
+
// Open on arrow down when closed
|
|
444
|
+
if (e.key === 'ArrowDown' && !isOpen) {
|
|
445
|
+
e.preventDefault();
|
|
446
|
+
this.openCouncilDropdown(container);
|
|
447
|
+
focusedIndex = 0;
|
|
448
|
+
updateFocus(getOptions(), focusedIndex);
|
|
299
449
|
}
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// Close on click outside (remove previous handler to avoid accumulation on re-render)
|
|
453
|
+
if (this._councilDropdownOutsideClickHandler) {
|
|
454
|
+
document.removeEventListener('click', this._councilDropdownOutsideClickHandler);
|
|
455
|
+
}
|
|
456
|
+
this._councilDropdownOutsideClickHandler = (e) => {
|
|
457
|
+
if (!container.contains(e.target) && container.classList.contains('open')) {
|
|
458
|
+
this.closeCouncilDropdown(container);
|
|
459
|
+
}
|
|
460
|
+
};
|
|
461
|
+
document.addEventListener('click', this._councilDropdownOutsideClickHandler);
|
|
462
|
+
}
|
|
300
463
|
|
|
464
|
+
/**
|
|
465
|
+
* Set the analysis mode (single model vs council) in the segmented control
|
|
466
|
+
* @param {string} mode - 'single' or 'council'
|
|
467
|
+
* @param {boolean} markChanged - Whether to trigger change detection (default true)
|
|
468
|
+
*/
|
|
469
|
+
setAnalysisMode(mode, markChanged = true) {
|
|
470
|
+
// Update button states
|
|
471
|
+
document.querySelectorAll('.mode-btn').forEach(btn => {
|
|
472
|
+
btn.classList.toggle('selected', btn.dataset.mode === mode);
|
|
473
|
+
});
|
|
474
|
+
// Show/hide panels
|
|
475
|
+
const singlePanel = document.getElementById('mode-panel-single');
|
|
476
|
+
const councilPanel = document.getElementById('mode-panel-council');
|
|
477
|
+
const cardPreview = document.getElementById('model-card-preview');
|
|
478
|
+
if (singlePanel) singlePanel.style.display = mode === 'single' ? '' : 'none';
|
|
479
|
+
if (councilPanel) councilPanel.style.display = mode === 'council' ? '' : 'none';
|
|
480
|
+
if (mode === 'single') {
|
|
481
|
+
if (cardPreview) cardPreview.style.display = '';
|
|
482
|
+
this.renderModelCard();
|
|
483
|
+
} else {
|
|
484
|
+
// Council mode: show council card if a council is selected
|
|
485
|
+
const councilId = this.currentSettings.default_council_id;
|
|
486
|
+
const council = councilId ? this.councils.find(c => c.id === councilId) : null;
|
|
487
|
+
if (council && cardPreview) {
|
|
488
|
+
cardPreview.style.display = '';
|
|
489
|
+
this.renderCouncilCard(council);
|
|
490
|
+
} else if (cardPreview) {
|
|
491
|
+
cardPreview.style.display = 'none';
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
// Map to default_tab: 'single' or 'council'
|
|
495
|
+
this.currentSettings.default_tab = mode === 'council' ? 'council' : 'single';
|
|
496
|
+
if (markChanged) {
|
|
301
497
|
this.checkForChanges();
|
|
302
498
|
}
|
|
303
499
|
}
|
|
304
500
|
|
|
305
501
|
/**
|
|
306
|
-
*
|
|
502
|
+
* Open the custom council dropdown
|
|
503
|
+
* @param {HTMLElement} container
|
|
504
|
+
*/
|
|
505
|
+
openCouncilDropdown(container) {
|
|
506
|
+
container.classList.add('open');
|
|
507
|
+
const trigger = container.querySelector('.custom-dropdown-trigger');
|
|
508
|
+
if (trigger) trigger.setAttribute('aria-expanded', 'true');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Close the custom council dropdown
|
|
513
|
+
* @param {HTMLElement} container
|
|
514
|
+
*/
|
|
515
|
+
closeCouncilDropdown(container) {
|
|
516
|
+
container.classList.remove('open');
|
|
517
|
+
const trigger = container.querySelector('.custom-dropdown-trigger');
|
|
518
|
+
if (trigger) trigger.setAttribute('aria-expanded', 'false');
|
|
519
|
+
// Clear focus highlights
|
|
520
|
+
container.querySelectorAll('.custom-dropdown-option.focused').forEach(
|
|
521
|
+
opt => opt.classList.remove('focused')
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Select a council option in the custom dropdown
|
|
527
|
+
* @param {HTMLElement} container
|
|
528
|
+
* @param {string} value - Council ID or empty string for "None"
|
|
529
|
+
*/
|
|
530
|
+
selectCouncilOption(container, value) {
|
|
531
|
+
this.currentSettings.default_council_id = value || null;
|
|
532
|
+
|
|
533
|
+
// Re-render the dropdown to update trigger display and selected state
|
|
534
|
+
this.renderCouncilDropdown();
|
|
535
|
+
this.closeCouncilDropdown(container);
|
|
536
|
+
|
|
537
|
+
// Render council card preview or hide it
|
|
538
|
+
const cardPreview = document.getElementById('model-card-preview');
|
|
539
|
+
const council = value ? this.councils.find(c => c.id === value) : null;
|
|
540
|
+
if (council && cardPreview) {
|
|
541
|
+
cardPreview.style.display = '';
|
|
542
|
+
this.renderCouncilCard(council);
|
|
543
|
+
} else if (cardPreview) {
|
|
544
|
+
cardPreview.style.display = 'none';
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
this.checkForChanges();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Escape HTML to prevent XSS in dynamic content
|
|
552
|
+
* @param {string} str
|
|
553
|
+
* @returns {string}
|
|
307
554
|
*/
|
|
308
|
-
|
|
309
|
-
|
|
555
|
+
escapeHtml(str) {
|
|
556
|
+
if (!str) return '';
|
|
557
|
+
const div = document.createElement('div');
|
|
558
|
+
div.textContent = str;
|
|
559
|
+
return div.innerHTML;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Render provider select dropdown
|
|
564
|
+
*/
|
|
565
|
+
renderProviderSelect() {
|
|
566
|
+
const select = document.getElementById('provider-select');
|
|
567
|
+
if (!select) return;
|
|
568
|
+
|
|
569
|
+
select.innerHTML = Object.entries(this.providers).map(([id, provider]) =>
|
|
570
|
+
`<option value="${id}" ${id === this.selectedProvider ? 'selected' : ''}>${provider.name}</option>`
|
|
571
|
+
).join('');
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Set the selected provider (used by updateUI for initial load)
|
|
576
|
+
*/
|
|
577
|
+
selectProvider(providerId) {
|
|
578
|
+
if (!this.providers[providerId]) return;
|
|
579
|
+
this.selectedProvider = providerId;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Render model select dropdown for the currently selected provider
|
|
584
|
+
*/
|
|
585
|
+
renderModelSelect() {
|
|
586
|
+
const select = document.getElementById('model-select');
|
|
587
|
+
if (!select) return;
|
|
588
|
+
|
|
589
|
+
const provider = this.providers[this.selectedProvider];
|
|
590
|
+
if (!provider) {
|
|
591
|
+
select.innerHTML = '';
|
|
592
|
+
this.renderModelCard();
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
select.innerHTML = provider.models.map(model =>
|
|
597
|
+
`<option value="${model.id}" ${model.id === this.currentSettings.default_model ? 'selected' : ''}>${model.name}</option>`
|
|
598
|
+
).join('');
|
|
599
|
+
|
|
600
|
+
this.renderModelCard();
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Render a model card preview for the currently selected provider + model.
|
|
605
|
+
* Reuses the model-card design from the analysis config modal (styled in pr.css).
|
|
606
|
+
*/
|
|
607
|
+
renderModelCard() {
|
|
608
|
+
const container = document.getElementById('model-card-preview');
|
|
310
609
|
if (!container) return;
|
|
311
610
|
|
|
312
611
|
const provider = this.providers[this.selectedProvider];
|
|
@@ -315,35 +614,185 @@ class RepoSettingsPage {
|
|
|
315
614
|
return;
|
|
316
615
|
}
|
|
317
616
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
617
|
+
// Find the selected model, fall back to default or first
|
|
618
|
+
const modelId = this.currentSettings.default_model;
|
|
619
|
+
const model = provider.models.find(m => m.id === modelId)
|
|
620
|
+
|| provider.models.find(m => m.default)
|
|
621
|
+
|| provider.models[0];
|
|
622
|
+
|
|
623
|
+
if (!model) {
|
|
624
|
+
container.innerHTML = '';
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const tierIcon = window.getTierIcon ? window.getTierIcon(model.tier) : '';
|
|
629
|
+
|
|
630
|
+
container.innerHTML = `
|
|
631
|
+
<div class="model-card selected settings-model-card-static" data-tier="${this.escapeHtml(model.tier || '')}">
|
|
632
|
+
<div class="model-badge ${this.escapeHtml(model.badgeClass || '')}">${this.escapeHtml(model.badge || '')}</div>
|
|
633
|
+
<div class="model-icon">${tierIcon}</div>
|
|
322
634
|
<div class="model-info">
|
|
323
|
-
<span class="model-name">${model.name}</span>
|
|
324
|
-
<span class="model-tagline">${model.tagline || ''}</span>
|
|
635
|
+
<span class="model-name">${this.escapeHtml(model.name)}</span>
|
|
636
|
+
<span class="model-tagline">${this.escapeHtml(model.tagline || '')}</span>
|
|
325
637
|
</div>
|
|
326
|
-
<p class="model-description">${model.description || ''}</p>
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
</svg>
|
|
331
|
-
</div>
|
|
332
|
-
</button>
|
|
333
|
-
`).join('');
|
|
638
|
+
<p class="model-description">${this.escapeHtml(model.description || '')}</p>
|
|
639
|
+
</div>
|
|
640
|
+
`;
|
|
641
|
+
}
|
|
334
642
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
643
|
+
/**
|
|
644
|
+
* Resolve provider/model IDs to display names using loaded provider data
|
|
645
|
+
* @param {string} providerId
|
|
646
|
+
* @param {string} modelId
|
|
647
|
+
* @returns {{ providerName: string, modelName: string }}
|
|
648
|
+
*/
|
|
649
|
+
resolveModelDisplay(providerId, modelId) {
|
|
650
|
+
const provider = this.providers[providerId];
|
|
651
|
+
if (!provider) {
|
|
652
|
+
return { providerName: providerId || 'Unknown', modelName: modelId || 'Unknown' };
|
|
653
|
+
}
|
|
654
|
+
const model = provider.models?.find(m => m.id === modelId);
|
|
655
|
+
return {
|
|
656
|
+
providerName: provider.name,
|
|
657
|
+
modelName: model ? model.name : (modelId || 'Unknown')
|
|
658
|
+
};
|
|
339
659
|
}
|
|
340
660
|
|
|
341
661
|
/**
|
|
342
|
-
*
|
|
343
|
-
*
|
|
662
|
+
* Render a council card preview into #model-card-preview
|
|
663
|
+
* @param {object} council - Council object with id, name, type, config
|
|
344
664
|
*/
|
|
345
|
-
|
|
346
|
-
|
|
665
|
+
renderCouncilCard(council) {
|
|
666
|
+
if (!council) return;
|
|
667
|
+
if (council.type === 'advanced') {
|
|
668
|
+
this.renderAdvancedCouncilCard(council);
|
|
669
|
+
} else {
|
|
670
|
+
this.renderVoiceCouncilCard(council);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Render a standard (voice) council card
|
|
676
|
+
* @param {object} council
|
|
677
|
+
*/
|
|
678
|
+
renderVoiceCouncilCard(council) {
|
|
679
|
+
const container = document.getElementById('model-card-preview');
|
|
680
|
+
if (!container) return;
|
|
681
|
+
|
|
682
|
+
const config = council.config || {};
|
|
683
|
+
const voices = config.voices || [];
|
|
684
|
+
const levels = config.levels || {};
|
|
685
|
+
|
|
686
|
+
// Build summary: "Levels 1, 2" for enabled levels
|
|
687
|
+
const enabledLevels = Object.entries(levels)
|
|
688
|
+
.filter(([, enabled]) => enabled)
|
|
689
|
+
.map(([level]) => level);
|
|
690
|
+
const summaryText = enabledLevels.length > 0
|
|
691
|
+
? `Levels ${enabledLevels.join(', ')}`
|
|
692
|
+
: 'No levels configured';
|
|
693
|
+
|
|
694
|
+
// Build reviewer list
|
|
695
|
+
const reviewerLines = voices.map(voice => {
|
|
696
|
+
const display = this.resolveModelDisplay(voice.provider, voice.model);
|
|
697
|
+
const tierLabel = voice.tier ? `<span class="council-card-tier">${this.escapeHtml(voice.tier)}</span>` : '';
|
|
698
|
+
return `<div class="council-card-reviewer">
|
|
699
|
+
<span class="council-card-reviewer-name">${this.escapeHtml(display.providerName)} / ${this.escapeHtml(display.modelName)}</span>
|
|
700
|
+
${tierLabel}
|
|
701
|
+
</div>`;
|
|
702
|
+
}).join('');
|
|
703
|
+
|
|
704
|
+
// Build consolidation section
|
|
705
|
+
let consolidationHTML = '';
|
|
706
|
+
if (config.consolidation && config.consolidation.provider) {
|
|
707
|
+
const consolDisplay = this.resolveModelDisplay(config.consolidation.provider, config.consolidation.model);
|
|
708
|
+
const consolTier = config.consolidation.tier ? `<span class="council-card-tier">${this.escapeHtml(config.consolidation.tier)}</span>` : '';
|
|
709
|
+
consolidationHTML = `
|
|
710
|
+
<div class="council-card-divider"></div>
|
|
711
|
+
<div class="council-card-consolidation">
|
|
712
|
+
<div class="council-card-consolidation-label">Consolidation</div>
|
|
713
|
+
<div class="council-card-reviewer">
|
|
714
|
+
<span class="council-card-reviewer-name">${this.escapeHtml(consolDisplay.providerName)} / ${this.escapeHtml(consolDisplay.modelName)}</span>
|
|
715
|
+
${consolTier}
|
|
716
|
+
</div>
|
|
717
|
+
</div>`;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
container.innerHTML = `
|
|
721
|
+
<div class="council-card">
|
|
722
|
+
<div class="council-card-name">${this.escapeHtml(council.name)}</div>
|
|
723
|
+
<div class="council-card-summary">${summaryText}</div>
|
|
724
|
+
<div class="council-card-reviewers">
|
|
725
|
+
${reviewerLines}
|
|
726
|
+
</div>
|
|
727
|
+
${consolidationHTML}
|
|
728
|
+
</div>
|
|
729
|
+
`;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Render an advanced council card with level-grouped reviewers
|
|
734
|
+
* @param {object} council
|
|
735
|
+
*/
|
|
736
|
+
renderAdvancedCouncilCard(council) {
|
|
737
|
+
const container = document.getElementById('model-card-preview');
|
|
738
|
+
if (!container) return;
|
|
739
|
+
|
|
740
|
+
const config = council.config || {};
|
|
741
|
+
const levels = config.levels || {};
|
|
742
|
+
|
|
743
|
+
const levelLabels = {
|
|
744
|
+
'1': 'Level 1 — Isolation',
|
|
745
|
+
'2': 'Level 2 — File Context',
|
|
746
|
+
'3': 'Level 3 — Codebase'
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
// Build level groups for enabled levels
|
|
750
|
+
let levelGroupsHTML = '';
|
|
751
|
+
for (const [levelNum, levelConfig] of Object.entries(levels)) {
|
|
752
|
+
if (!levelConfig || !levelConfig.enabled) continue;
|
|
753
|
+
const voices = levelConfig.voices || [];
|
|
754
|
+
const header = levelLabels[levelNum] || `Level ${levelNum}`;
|
|
755
|
+
const voiceLines = voices.map(voice => {
|
|
756
|
+
const display = this.resolveModelDisplay(voice.provider, voice.model);
|
|
757
|
+
const tierLabel = voice.tier ? `<span class="council-card-tier">${this.escapeHtml(voice.tier)}</span>` : '';
|
|
758
|
+
return `<div class="council-card-reviewer">
|
|
759
|
+
<span class="council-card-reviewer-name">${this.escapeHtml(display.providerName)} / ${this.escapeHtml(display.modelName)}</span>
|
|
760
|
+
${tierLabel}
|
|
761
|
+
</div>`;
|
|
762
|
+
}).join('');
|
|
763
|
+
levelGroupsHTML += `
|
|
764
|
+
<div class="council-card-level-header">${this.escapeHtml(header)}</div>
|
|
765
|
+
${voiceLines}`;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Build consolidation/orchestration section
|
|
769
|
+
let consolidationHTML = '';
|
|
770
|
+
if (config.consolidation && config.consolidation.provider) {
|
|
771
|
+
const consolDisplay = this.resolveModelDisplay(config.consolidation.provider, config.consolidation.model);
|
|
772
|
+
const consolTier = config.consolidation.tier ? `<span class="council-card-tier">${this.escapeHtml(config.consolidation.tier)}</span>` : '';
|
|
773
|
+
consolidationHTML = `
|
|
774
|
+
<div class="council-card-divider"></div>
|
|
775
|
+
<div class="council-card-consolidation">
|
|
776
|
+
<div class="council-card-consolidation-label">Orchestration</div>
|
|
777
|
+
<div class="council-card-reviewer">
|
|
778
|
+
<span class="council-card-reviewer-name">${this.escapeHtml(consolDisplay.providerName)} / ${this.escapeHtml(consolDisplay.modelName)}</span>
|
|
779
|
+
${consolTier}
|
|
780
|
+
</div>
|
|
781
|
+
</div>`;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
container.innerHTML = `
|
|
785
|
+
<div class="council-card">
|
|
786
|
+
<div class="council-card-name">
|
|
787
|
+
${this.escapeHtml(council.name)}
|
|
788
|
+
<span class="council-card-badge-advanced">Advanced</span>
|
|
789
|
+
</div>
|
|
790
|
+
<div class="council-card-reviewers">
|
|
791
|
+
${levelGroupsHTML}
|
|
792
|
+
</div>
|
|
793
|
+
${consolidationHTML}
|
|
794
|
+
</div>
|
|
795
|
+
`;
|
|
347
796
|
}
|
|
348
797
|
|
|
349
798
|
async loadSettings() {
|
|
@@ -362,6 +811,8 @@ class RepoSettingsPage {
|
|
|
362
811
|
this.originalSettings = {
|
|
363
812
|
default_provider: settings.default_provider || null,
|
|
364
813
|
default_model: settings.default_model || null,
|
|
814
|
+
default_tab: settings.default_tab || 'single',
|
|
815
|
+
default_council_id: settings.default_council_id || null,
|
|
365
816
|
default_instructions: settings.default_instructions || '',
|
|
366
817
|
local_path: settings.local_path || null
|
|
367
818
|
};
|
|
@@ -378,6 +829,8 @@ class RepoSettingsPage {
|
|
|
378
829
|
this.originalSettings = {
|
|
379
830
|
default_provider: null,
|
|
380
831
|
default_model: null,
|
|
832
|
+
default_tab: 'single',
|
|
833
|
+
default_council_id: null,
|
|
381
834
|
default_instructions: '',
|
|
382
835
|
local_path: null
|
|
383
836
|
};
|
|
@@ -393,19 +846,40 @@ class RepoSettingsPage {
|
|
|
393
846
|
|
|
394
847
|
if (!providerId || !this.providers[providerId]) {
|
|
395
848
|
// Provider doesn't exist, fall back to first available
|
|
396
|
-
// Update
|
|
397
|
-
// but don't mark as changed (no unsaved changes indicator)
|
|
849
|
+
// Update both settings so no false dirty state on load
|
|
398
850
|
providerId = availableProviders[0] || 'claude';
|
|
399
851
|
this.currentSettings.default_provider = providerId;
|
|
852
|
+
this.originalSettings.default_provider = providerId;
|
|
400
853
|
}
|
|
401
854
|
|
|
402
|
-
this.selectProvider(providerId
|
|
855
|
+
this.selectProvider(providerId);
|
|
856
|
+
this.renderProviderSelect();
|
|
403
857
|
|
|
404
|
-
//
|
|
405
|
-
|
|
406
|
-
|
|
858
|
+
// Validate saved model exists in current provider
|
|
859
|
+
const provider = this.providers[this.selectedProvider];
|
|
860
|
+
if (provider) {
|
|
861
|
+
const modelExists = provider.models.some(m => m.id === this.currentSettings.default_model);
|
|
862
|
+
if (!modelExists) {
|
|
863
|
+
const fallbackModel = provider.models.find(m => m.default) || provider.models[0];
|
|
864
|
+
if (fallbackModel) {
|
|
865
|
+
this.currentSettings.default_model = fallbackModel.id;
|
|
866
|
+
this.originalSettings.default_model = fallbackModel.id;
|
|
867
|
+
const modelSelect = document.getElementById('model-select');
|
|
868
|
+
if (modelSelect) modelSelect.value = fallbackModel.id;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
407
871
|
}
|
|
408
872
|
|
|
873
|
+
this.renderModelSelect();
|
|
874
|
+
|
|
875
|
+
// Update analysis mode segmented control (map 'advanced' to 'council')
|
|
876
|
+
const defaultTab = this.currentSettings.default_tab || 'single';
|
|
877
|
+
const mode = (defaultTab === 'council' || defaultTab === 'advanced') ? 'council' : 'single';
|
|
878
|
+
this.setAnalysisMode(mode, false);
|
|
879
|
+
|
|
880
|
+
// Update council custom dropdown
|
|
881
|
+
this.renderCouncilDropdown();
|
|
882
|
+
|
|
409
883
|
// Update instructions textarea
|
|
410
884
|
const textarea = document.getElementById('default-instructions');
|
|
411
885
|
if (textarea) {
|
|
@@ -442,19 +916,6 @@ class RepoSettingsPage {
|
|
|
442
916
|
}
|
|
443
917
|
}
|
|
444
918
|
|
|
445
|
-
selectModel(modelId, markAsChanged = true) {
|
|
446
|
-
// Update current settings
|
|
447
|
-
if (markAsChanged) {
|
|
448
|
-
this.currentSettings.default_model = modelId;
|
|
449
|
-
this.checkForChanges();
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
// Update UI
|
|
453
|
-
document.querySelectorAll('.model-card').forEach(card => {
|
|
454
|
-
card.classList.toggle('selected', card.dataset.model === modelId);
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
|
|
458
919
|
updateCharCount(count) {
|
|
459
920
|
const charCountEl = document.getElementById('char-count');
|
|
460
921
|
if (charCountEl) {
|
|
@@ -466,9 +927,11 @@ class RepoSettingsPage {
|
|
|
466
927
|
// Use nullish coalescing to normalize null/undefined for consistent comparison
|
|
467
928
|
const providerChanged = (this.currentSettings.default_provider ?? null) !== (this.originalSettings.default_provider ?? null);
|
|
468
929
|
const modelChanged = (this.currentSettings.default_model ?? null) !== (this.originalSettings.default_model ?? null);
|
|
930
|
+
const tabChanged = (this.currentSettings.default_tab ?? 'single') !== (this.originalSettings.default_tab ?? 'single');
|
|
931
|
+
const councilChanged = (this.currentSettings.default_council_id ?? null) !== (this.originalSettings.default_council_id ?? null);
|
|
469
932
|
const instructionsChanged = (this.currentSettings.default_instructions ?? '') !== (this.originalSettings.default_instructions ?? '');
|
|
470
933
|
|
|
471
|
-
this.hasUnsavedChanges = providerChanged || modelChanged || instructionsChanged;
|
|
934
|
+
this.hasUnsavedChanges = providerChanged || modelChanged || tabChanged || councilChanged || instructionsChanged;
|
|
472
935
|
|
|
473
936
|
// Show/hide action bar
|
|
474
937
|
const actionBar = document.getElementById('action-bar');
|
|
@@ -500,6 +963,8 @@ class RepoSettingsPage {
|
|
|
500
963
|
body: JSON.stringify({
|
|
501
964
|
default_provider: this.currentSettings.default_provider,
|
|
502
965
|
default_model: this.currentSettings.default_model,
|
|
966
|
+
default_tab: this.currentSettings.default_tab,
|
|
967
|
+
default_council_id: this.currentSettings.default_council_id,
|
|
503
968
|
default_instructions: this.currentSettings.default_instructions
|
|
504
969
|
})
|
|
505
970
|
});
|
|
@@ -571,6 +1036,8 @@ class RepoSettingsPage {
|
|
|
571
1036
|
body: JSON.stringify({
|
|
572
1037
|
default_provider: null,
|
|
573
1038
|
default_model: null,
|
|
1039
|
+
default_tab: null,
|
|
1040
|
+
default_council_id: null,
|
|
574
1041
|
default_instructions: '',
|
|
575
1042
|
local_path: null
|
|
576
1043
|
})
|
|
@@ -584,6 +1051,8 @@ class RepoSettingsPage {
|
|
|
584
1051
|
this.originalSettings = {
|
|
585
1052
|
default_provider: null,
|
|
586
1053
|
default_model: null,
|
|
1054
|
+
default_tab: 'single',
|
|
1055
|
+
default_council_id: null,
|
|
587
1056
|
default_instructions: '',
|
|
588
1057
|
local_path: null
|
|
589
1058
|
};
|