@in-the-loop-labs/pair-review 3.0.6 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/skills/analyze/references/level1-balanced.md +8 -0
- package/plugin-code-critic/skills/analyze/references/level1-fast.md +7 -0
- package/plugin-code-critic/skills/analyze/references/level1-thorough.md +8 -0
- package/plugin-code-critic/skills/analyze/references/level2-balanced.md +9 -0
- package/plugin-code-critic/skills/analyze/references/level2-fast.md +8 -0
- package/plugin-code-critic/skills/analyze/references/level2-thorough.md +9 -0
- package/plugin-code-critic/skills/analyze/references/level3-balanced.md +9 -0
- package/plugin-code-critic/skills/analyze/references/level3-fast.md +8 -0
- package/plugin-code-critic/skills/analyze/references/level3-thorough.md +9 -0
- package/plugin-code-critic/skills/analyze/references/orchestration-balanced.md +9 -0
- package/plugin-code-critic/skills/analyze/references/orchestration-fast.md +5 -0
- package/plugin-code-critic/skills/analyze/references/orchestration-thorough.md +9 -0
- package/public/css/analysis-config.css +83 -0
- package/public/css/pr.css +191 -4
- package/public/index.html +20 -0
- package/public/js/components/AIPanel.js +1 -1
- package/public/js/components/AdvancedConfigTab.js +83 -8
- package/public/js/components/AnalysisConfigModal.js +155 -5
- package/public/js/components/ChatPanel.js +22 -5
- package/public/js/components/CouncilProgressModal.js +239 -22
- package/public/js/components/TimeoutSelect.js +2 -0
- package/public/js/components/VoiceCentricConfigTab.js +179 -12
- package/public/js/index.js +119 -1
- package/public/js/local.js +141 -47
- package/public/js/modules/suggestion-manager.js +2 -1
- package/public/js/pr.js +71 -12
- package/public/js/repo-settings.js +2 -2
- package/public/local.html +32 -11
- package/public/pr.html +2 -0
- package/src/ai/analyzer.js +371 -111
- package/src/ai/claude-provider.js +2 -0
- package/src/ai/codex-provider.js +1 -1
- package/src/ai/copilot-provider.js +2 -0
- package/src/ai/executable-provider.js +534 -0
- package/src/ai/gemini-provider.js +2 -0
- package/src/ai/index.js +9 -1
- package/src/ai/pi-provider.js +10 -8
- package/src/ai/prompts/baseline/consolidation/balanced.js +54 -2
- package/src/ai/prompts/baseline/consolidation/fast.js +31 -1
- package/src/ai/prompts/baseline/consolidation/thorough.js +46 -3
- package/src/ai/prompts/baseline/level1/balanced.js +12 -0
- package/src/ai/prompts/baseline/level1/fast.js +11 -0
- package/src/ai/prompts/baseline/level1/thorough.js +12 -0
- package/src/ai/prompts/baseline/level2/balanced.js +13 -0
- package/src/ai/prompts/baseline/level2/fast.js +12 -0
- package/src/ai/prompts/baseline/level2/thorough.js +13 -0
- package/src/ai/prompts/baseline/level3/balanced.js +13 -0
- package/src/ai/prompts/baseline/level3/fast.js +12 -0
- package/src/ai/prompts/baseline/level3/thorough.js +13 -0
- package/src/ai/prompts/baseline/orchestration/balanced.js +15 -0
- package/src/ai/prompts/baseline/orchestration/fast.js +11 -0
- package/src/ai/prompts/baseline/orchestration/thorough.js +15 -0
- package/src/ai/prompts/render-for-skill.js +3 -0
- package/src/ai/prompts/shared/output-schema.js +8 -0
- package/src/ai/provider.js +89 -4
- package/src/chat/prompt-builder.js +17 -1
- package/src/chat/session-manager.js +32 -28
- package/src/config.js +15 -2
- package/src/database.js +59 -15
- package/src/git/base-branch.js +113 -29
- package/src/local-review.js +15 -9
- package/src/main.js +3 -2
- package/src/routes/analyses.js +34 -8
- package/src/routes/chat.js +15 -8
- package/src/routes/config.js +3 -120
- package/src/routes/councils.js +15 -6
- package/src/routes/executable-analysis.js +494 -0
- package/src/routes/local.js +152 -15
- package/src/routes/mcp.js +9 -4
- package/src/routes/pr.js +166 -29
- package/src/routes/reviews.js +31 -5
- package/src/routes/shared.js +72 -5
- package/src/routes/worktrees.js +4 -2
- package/src/utils/comment-formatter.js +28 -11
- package/src/utils/instructions.js +22 -8
- package/src/utils/logger.js +20 -10
|
@@ -275,6 +275,28 @@ class AnalysisConfigModal {
|
|
|
275
275
|
</div>
|
|
276
276
|
</section>
|
|
277
277
|
|
|
278
|
+
<!-- Exclude Previous Findings -->
|
|
279
|
+
<details class="config-section exclude-previous-section">
|
|
280
|
+
<summary class="section-title">
|
|
281
|
+
Exclude Previous Findings
|
|
282
|
+
<span class="section-hint">(optional)</span>
|
|
283
|
+
</summary>
|
|
284
|
+
<div class="exclude-previous-options">
|
|
285
|
+
<label class="remember-toggle">
|
|
286
|
+
<input type="checkbox" id="exclude-github-comments" />
|
|
287
|
+
<span class="toggle-switch"></span>
|
|
288
|
+
<span class="toggle-label">GitHub PR review comments</span>
|
|
289
|
+
</label>
|
|
290
|
+
<p class="option-hint">Skip issues already noted in inline PR review comments</p>
|
|
291
|
+
<label class="remember-toggle">
|
|
292
|
+
<input type="checkbox" id="exclude-pr-feedback" />
|
|
293
|
+
<span class="toggle-switch"></span>
|
|
294
|
+
<span class="toggle-label">Existing pair-review feedback</span>
|
|
295
|
+
</label>
|
|
296
|
+
<p class="option-hint">Skip issues from previous AI suggestions and reviewer comments</p>
|
|
297
|
+
</div>
|
|
298
|
+
</details>
|
|
299
|
+
|
|
278
300
|
<!-- Focus Presets - Hidden for now, may reintroduce later -->
|
|
279
301
|
<section class="config-section" style="display: none;">
|
|
280
302
|
<h4 class="section-title">
|
|
@@ -499,7 +521,7 @@ class AnalysisConfigModal {
|
|
|
499
521
|
container.innerHTML = this.models.map(model => `
|
|
500
522
|
<button class="model-card ${model.id === this.selectedModel ? 'selected' : ''}" data-model="${model.id}" data-tier="${model.tier}">
|
|
501
523
|
<div class="model-badge ${model.badgeClass || ''}">${model.badge || ''}</div>
|
|
502
|
-
<div class="model-icon">${this.getModelIcon(model.tier)}</div>
|
|
524
|
+
<div class="model-icon">${model.icon || this.getModelIcon(model.tier)}</div>
|
|
503
525
|
<div class="model-info">
|
|
504
526
|
<span class="model-name">${model.name}</span>
|
|
505
527
|
<span class="model-tagline">${model.tagline || ''}</span>
|
|
@@ -549,6 +571,47 @@ class AnalysisConfigModal {
|
|
|
549
571
|
// Re-render model cards (handles its own event listeners)
|
|
550
572
|
this.renderModelCards();
|
|
551
573
|
|
|
574
|
+
// Toggle level toggles visibility based on provider's capabilities
|
|
575
|
+
const provider = this.providers[providerId];
|
|
576
|
+
const supportsLevels = provider.capabilities?.review_levels !== false;
|
|
577
|
+
const supportsCustomInstructions = provider.capabilities?.custom_instructions !== false;
|
|
578
|
+
const levelToggles = this.modal.querySelector('.single-level-toggles');
|
|
579
|
+
const skipLevel3Info = this.modal.querySelector('#skip-level3-info');
|
|
580
|
+
const existingLevelsNote = this.modal.querySelector('.executable-provider-levels-note');
|
|
581
|
+
const customInstructionsSection = this.modal.querySelector('#custom-instructions')?.closest('.config-section');
|
|
582
|
+
|
|
583
|
+
if (!supportsLevels) {
|
|
584
|
+
if (levelToggles) levelToggles.style.display = 'none';
|
|
585
|
+
if (skipLevel3Info) skipLevel3Info.style.display = 'none';
|
|
586
|
+
if (!existingLevelsNote) {
|
|
587
|
+
const note = document.createElement('div');
|
|
588
|
+
note.className = 'executable-provider-note executable-provider-levels-note';
|
|
589
|
+
note.textContent = 'This provider runs its own analysis pipeline';
|
|
590
|
+
levelToggles?.parentNode?.appendChild(note);
|
|
591
|
+
}
|
|
592
|
+
} else {
|
|
593
|
+
if (levelToggles) levelToggles.style.display = '';
|
|
594
|
+
if (skipLevel3Info) skipLevel3Info.style.display = '';
|
|
595
|
+
if (existingLevelsNote) existingLevelsNote.remove();
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Toggle custom instructions visibility based on provider's capabilities
|
|
599
|
+
const instructionsContainer = customInstructionsSection?.querySelector('.instructions-container');
|
|
600
|
+
const existingInstructionsNote = customInstructionsSection?.querySelector('.executable-provider-instructions-note');
|
|
601
|
+
|
|
602
|
+
if (!supportsCustomInstructions) {
|
|
603
|
+
if (instructionsContainer) instructionsContainer.style.display = 'none';
|
|
604
|
+
if (!existingInstructionsNote && customInstructionsSection) {
|
|
605
|
+
const note = document.createElement('div');
|
|
606
|
+
note.className = 'executable-provider-note executable-provider-instructions-note';
|
|
607
|
+
note.textContent = 'This provider runs its own analysis pipeline and does not accept custom instructions.';
|
|
608
|
+
customInstructionsSection.appendChild(note);
|
|
609
|
+
}
|
|
610
|
+
} else {
|
|
611
|
+
if (instructionsContainer) instructionsContainer.style.display = '';
|
|
612
|
+
if (existingInstructionsNote) existingInstructionsNote.remove();
|
|
613
|
+
}
|
|
614
|
+
|
|
552
615
|
// Update selection state for the selected model
|
|
553
616
|
if (this.selectedModel) {
|
|
554
617
|
this.selectModel(this.selectedModel);
|
|
@@ -711,7 +774,8 @@ class AnalysisConfigModal {
|
|
|
711
774
|
councilName: selectedCouncil?.name || null,
|
|
712
775
|
councilConfig: councilConfig,
|
|
713
776
|
customInstructions: this.modal.querySelector('#vc-custom-instructions')?.value?.trim() || '',
|
|
714
|
-
repoInstructions: this.repoInstructions
|
|
777
|
+
repoInstructions: this.repoInstructions,
|
|
778
|
+
excludePrevious: this._getAndSaveExcludePrevious()
|
|
715
779
|
};
|
|
716
780
|
|
|
717
781
|
if (this.onSubmit) this.onSubmit(config);
|
|
@@ -736,7 +800,8 @@ class AnalysisConfigModal {
|
|
|
736
800
|
councilName: selectedCouncil?.name || null,
|
|
737
801
|
councilConfig: councilConfig,
|
|
738
802
|
customInstructions: this.modal.querySelector('#council-custom-instructions')?.value?.trim() || '',
|
|
739
|
-
repoInstructions: this.repoInstructions
|
|
803
|
+
repoInstructions: this.repoInstructions,
|
|
804
|
+
excludePrevious: this._getAndSaveExcludePrevious()
|
|
740
805
|
};
|
|
741
806
|
|
|
742
807
|
if (this.onSubmit) this.onSubmit(config);
|
|
@@ -748,16 +813,21 @@ class AnalysisConfigModal {
|
|
|
748
813
|
const selectedModelCard = this.modal.querySelector('.model-card.selected');
|
|
749
814
|
const tier = selectedModelCard?.dataset?.tier || 'balanced';
|
|
750
815
|
|
|
816
|
+
const selectedProvider = this.providers[this.selectedProvider];
|
|
817
|
+
const noLevels = selectedProvider?.capabilities?.review_levels === false;
|
|
818
|
+
|
|
751
819
|
const config = {
|
|
752
820
|
provider: this.selectedProvider,
|
|
753
821
|
model: this.selectedModel,
|
|
754
822
|
tier: tier,
|
|
755
823
|
instructions: this.buildInstructions(),
|
|
756
|
-
customInstructions: this.modal.querySelector('#custom-instructions')?.value?.trim() || '',
|
|
824
|
+
customInstructions: selectedProvider?.capabilities?.custom_instructions === false ? '' : (this.modal.querySelector('#custom-instructions')?.value?.trim() || ''),
|
|
757
825
|
presets: Array.from(this.selectedPresets),
|
|
758
826
|
repoInstructions: this.repoInstructions,
|
|
759
827
|
enabledLevels: [...this.enabledLevels],
|
|
760
|
-
skipLevel3: !this.enabledLevels.includes(3)
|
|
828
|
+
skipLevel3: !this.enabledLevels.includes(3),
|
|
829
|
+
noLevels,
|
|
830
|
+
excludePrevious: this._getAndSaveExcludePrevious()
|
|
761
831
|
};
|
|
762
832
|
|
|
763
833
|
if (this.onSubmit) this.onSubmit(config);
|
|
@@ -895,6 +965,9 @@ class AnalysisConfigModal {
|
|
|
895
965
|
}
|
|
896
966
|
}
|
|
897
967
|
|
|
968
|
+
// Restore exclude-previous checkbox state from localStorage
|
|
969
|
+
this._restoreExcludePrevious(options);
|
|
970
|
+
|
|
898
971
|
// Remove loading state and reveal content
|
|
899
972
|
this._showLoading(false);
|
|
900
973
|
|
|
@@ -953,6 +1026,10 @@ class AnalysisConfigModal {
|
|
|
953
1026
|
<button class="analysis-tab" data-tab="advanced">Advanced</button>
|
|
954
1027
|
`;
|
|
955
1028
|
|
|
1029
|
+
// Hoist the exclude-previous section out of the single-model panel
|
|
1030
|
+
// so it remains visible across all tabs.
|
|
1031
|
+
const excludePreviousSection = singlePanel.querySelector('.exclude-previous-section');
|
|
1032
|
+
|
|
956
1033
|
// Assemble
|
|
957
1034
|
modalBody.innerHTML = '';
|
|
958
1035
|
modalBody.appendChild(tabBar);
|
|
@@ -960,6 +1037,12 @@ class AnalysisConfigModal {
|
|
|
960
1037
|
modalBody.appendChild(councilPanel);
|
|
961
1038
|
modalBody.appendChild(advancedPanel);
|
|
962
1039
|
|
|
1040
|
+
// Place the exclude-previous section after all tab panels,
|
|
1041
|
+
// just before the modal footer, so it's visible on every tab.
|
|
1042
|
+
if (excludePreviousSection) {
|
|
1043
|
+
modalBody.appendChild(excludePreviousSection);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
963
1046
|
// Tab click listeners
|
|
964
1047
|
tabBar.querySelectorAll('.analysis-tab').forEach(tab => {
|
|
965
1048
|
tab.addEventListener('click', () => {
|
|
@@ -1212,6 +1295,73 @@ class AnalysisConfigModal {
|
|
|
1212
1295
|
}, 200);
|
|
1213
1296
|
}
|
|
1214
1297
|
|
|
1298
|
+
/**
|
|
1299
|
+
* Restore exclude-previous checkbox state from localStorage.
|
|
1300
|
+
* Disables the GitHub checkbox when there is no PR or no GitHub token.
|
|
1301
|
+
* @param {Object} options - show() options
|
|
1302
|
+
* @private
|
|
1303
|
+
*/
|
|
1304
|
+
_restoreExcludePrevious(options) {
|
|
1305
|
+
const githubCb = this.modal.querySelector('#exclude-github-comments');
|
|
1306
|
+
const feedbackCb = this.modal.querySelector('#exclude-pr-feedback');
|
|
1307
|
+
if (!githubCb || !feedbackCb) return;
|
|
1308
|
+
|
|
1309
|
+
// Restore saved state
|
|
1310
|
+
let saved = { github: false, feedback: false };
|
|
1311
|
+
try {
|
|
1312
|
+
const raw = localStorage.getItem('pair-review-exclude-previous');
|
|
1313
|
+
if (raw) saved = JSON.parse(raw);
|
|
1314
|
+
} catch (_) { /* ignore parse errors */ }
|
|
1315
|
+
|
|
1316
|
+
githubCb.checked = !!saved.github;
|
|
1317
|
+
feedbackCb.checked = !!saved.feedback;
|
|
1318
|
+
|
|
1319
|
+
// Disable the GitHub checkbox when it cannot be used
|
|
1320
|
+
const canUseGithub = options.hasPr !== false && options.hasGithubToken !== false;
|
|
1321
|
+
githubCb.disabled = !canUseGithub;
|
|
1322
|
+
if (!canUseGithub) {
|
|
1323
|
+
const label = githubCb.closest('.remember-toggle');
|
|
1324
|
+
if (label) label.title = 'Not available — requires a PR with a GitHub token';
|
|
1325
|
+
} else {
|
|
1326
|
+
const label = githubCb.closest('.remember-toggle');
|
|
1327
|
+
if (label) label.title = '';
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
/**
|
|
1332
|
+
* Read current exclude-previous checkbox state, save to localStorage,
|
|
1333
|
+
* and return the config object.
|
|
1334
|
+
* @returns {{ github: boolean, feedback: boolean }}
|
|
1335
|
+
* @private
|
|
1336
|
+
*/
|
|
1337
|
+
_getAndSaveExcludePrevious() {
|
|
1338
|
+
const githubCb = this.modal.querySelector('#exclude-github-comments');
|
|
1339
|
+
const feedbackCb = this.modal.querySelector('#exclude-pr-feedback');
|
|
1340
|
+
|
|
1341
|
+
// When the GitHub checkbox is disabled, preserve the existing localStorage
|
|
1342
|
+
// value for `github` so the user's preference survives contexts where the
|
|
1343
|
+
// option is unavailable.
|
|
1344
|
+
let preservedGithub = false;
|
|
1345
|
+
if (githubCb?.disabled) {
|
|
1346
|
+
try {
|
|
1347
|
+
const raw = localStorage.getItem('pair-review-exclude-previous');
|
|
1348
|
+
if (raw) {
|
|
1349
|
+
const prev = JSON.parse(raw);
|
|
1350
|
+
preservedGithub = !!prev.github;
|
|
1351
|
+
}
|
|
1352
|
+
} catch (_) { /* ignore parse errors */ }
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
const state = {
|
|
1356
|
+
github: githubCb?.disabled ? preservedGithub : !!githubCb?.checked,
|
|
1357
|
+
feedback: !!feedbackCb?.checked
|
|
1358
|
+
};
|
|
1359
|
+
try {
|
|
1360
|
+
localStorage.setItem('pair-review-exclude-previous', JSON.stringify(state));
|
|
1361
|
+
} catch (_) { /* ignore storage errors */ }
|
|
1362
|
+
return state;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1215
1365
|
/**
|
|
1216
1366
|
* Cleanup event listeners and pending timeouts
|
|
1217
1367
|
*/
|
|
@@ -43,6 +43,7 @@ class ChatPanel {
|
|
|
43
43
|
this._sessionWarm = false; // true once the session has been used in this page load
|
|
44
44
|
this._activeProvider = window.__pairReview?.chatProvider || 'pi';
|
|
45
45
|
this._chatProviders = window.__pairReview?.chatProviders || [];
|
|
46
|
+
this._enterToSend = window.__pairReview?.chatEnterToSend ?? true;
|
|
46
47
|
|
|
47
48
|
this._render();
|
|
48
49
|
this._bindEvents();
|
|
@@ -141,7 +142,7 @@ class ChatPanel {
|
|
|
141
142
|
<div class="chat-panel__input-area">
|
|
142
143
|
<textarea class="chat-panel__input" placeholder="Ask about this review..." rows="1"></textarea>
|
|
143
144
|
<div class="chat-panel__input-footer">
|
|
144
|
-
<span class="chat-panel__input-hint">${typeof navigator !== 'undefined' && navigator.platform?.includes('Mac') ? '\u2318' : 'Ctrl'}+Enter to send</span>
|
|
145
|
+
<span class="chat-panel__input-hint" title="Configure with chat.enter_to_send">${this._enterToSend ? 'Enter to send, Shift+Enter for newline' : `${typeof navigator !== 'undefined' && navigator.platform?.includes('Mac') ? '\u2318' : 'Ctrl'}+Enter to send`}</span>
|
|
145
146
|
<div class="chat-panel__input-actions">
|
|
146
147
|
<button class="chat-panel__send-btn" title="Send" disabled>
|
|
147
148
|
<svg viewBox="0 0 16 16" fill="currentColor" width="14" height="14">
|
|
@@ -250,10 +251,26 @@ class ChatPanel {
|
|
|
250
251
|
|
|
251
252
|
// Keyboard shortcuts
|
|
252
253
|
this.inputEl.addEventListener('keydown', (e) => {
|
|
253
|
-
if (e.key === 'Enter'
|
|
254
|
-
e.
|
|
255
|
-
|
|
256
|
-
|
|
254
|
+
if (e.key === 'Enter') {
|
|
255
|
+
// Ignore Enter during IME composition (e.g. CJK input) so the
|
|
256
|
+
// composition-confirming keystroke is not swallowed.
|
|
257
|
+
if (e.isComposing) return;
|
|
258
|
+
|
|
259
|
+
if (this._enterToSend) {
|
|
260
|
+
// Enter sends, Shift+Enter inserts newline
|
|
261
|
+
if (e.shiftKey) return; // let browser insert newline
|
|
262
|
+
e.preventDefault();
|
|
263
|
+
if (this.inputEl.value.trim() && !this.isStreaming) {
|
|
264
|
+
this.sendMessage();
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
// Cmd+Enter / Ctrl+Enter sends, plain Enter inserts newline
|
|
268
|
+
if (e.metaKey || e.ctrlKey) {
|
|
269
|
+
e.preventDefault();
|
|
270
|
+
if (this.inputEl.value.trim() && !this.isStreaming) {
|
|
271
|
+
this.sendMessage();
|
|
272
|
+
}
|
|
273
|
+
}
|
|
257
274
|
}
|
|
258
275
|
}
|
|
259
276
|
});
|
|
@@ -23,6 +23,11 @@ class CouncilProgressModal {
|
|
|
23
23
|
// Track per-voice completion state
|
|
24
24
|
this._voiceStates = {};
|
|
25
25
|
|
|
26
|
+
// Track which voice keys are executable providers.
|
|
27
|
+
// Populated from progress events (levels.exec.voices) and persisted
|
|
28
|
+
// across show()/hide() cycles so reopened modals render correctly.
|
|
29
|
+
this._executableVoices = new Set();
|
|
30
|
+
|
|
26
31
|
this._createModal();
|
|
27
32
|
this._setupEventListeners();
|
|
28
33
|
}
|
|
@@ -54,9 +59,12 @@ class CouncilProgressModal {
|
|
|
54
59
|
// Detect rendering mode
|
|
55
60
|
const configType = options.configType || (councilConfig ? 'advanced' : 'single');
|
|
56
61
|
this._renderMode = configType;
|
|
62
|
+
this._noLevels = options.noLevels || false;
|
|
57
63
|
|
|
58
64
|
// Rebuild DOM based on mode
|
|
59
|
-
if (configType === 'single') {
|
|
65
|
+
if (configType === 'single' && this._noLevels) {
|
|
66
|
+
this._rebuildBodyNoLevels();
|
|
67
|
+
} else if (configType === 'single') {
|
|
60
68
|
const enabledLevels = options.enabledLevels || [1, 2, 3];
|
|
61
69
|
this._rebuildBodySingleModel(enabledLevels);
|
|
62
70
|
} else if (configType === 'council') {
|
|
@@ -239,11 +247,34 @@ class CouncilProgressModal {
|
|
|
239
247
|
* @param {Object} status
|
|
240
248
|
*/
|
|
241
249
|
updateProgress(status) {
|
|
250
|
+
// No-levels mode: update the single row based on overall status
|
|
251
|
+
if (this._noLevels) {
|
|
252
|
+
this._updateNoLevelsProgress(status);
|
|
253
|
+
// Terminal states
|
|
254
|
+
if (status.status === 'completed') {
|
|
255
|
+
this._handleCompletion(status);
|
|
256
|
+
} else if (status.status === 'failed') {
|
|
257
|
+
this._handleFailure(status);
|
|
258
|
+
} else if (status.status === 'cancelled') {
|
|
259
|
+
this._handleCancellation(status);
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
242
264
|
if (!status.levels || typeof status.levels !== 'object') {
|
|
243
265
|
console.warn('Council progress: invalid status structure', status);
|
|
244
266
|
return;
|
|
245
267
|
}
|
|
246
268
|
|
|
269
|
+
// Track executable voice keys from exec level data so that reopened
|
|
270
|
+
// modals can render executable reviewers correctly without relying on
|
|
271
|
+
// the config modal's provider cache (which may be empty on reopen).
|
|
272
|
+
if (status.levels.exec?.voices) {
|
|
273
|
+
for (const voiceKey of Object.keys(status.levels.exec.voices)) {
|
|
274
|
+
this._executableVoices.add(voiceKey);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
247
278
|
if (this._renderMode === 'single') {
|
|
248
279
|
// Single-model: update level headers directly
|
|
249
280
|
for (let level = 1; level <= 3; level++) {
|
|
@@ -292,6 +323,63 @@ class CouncilProgressModal {
|
|
|
292
323
|
}
|
|
293
324
|
}
|
|
294
325
|
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
// No-levels progress
|
|
328
|
+
// ---------------------------------------------------------------------------
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Update the single row for no-levels providers.
|
|
332
|
+
* Maps overall analysis status to the single "Running analysis..." row.
|
|
333
|
+
*/
|
|
334
|
+
_updateNoLevelsProgress(status) {
|
|
335
|
+
const header = this.modal.querySelector('.council-level-header[data-level="analysis"]');
|
|
336
|
+
if (!header) return;
|
|
337
|
+
|
|
338
|
+
const iconEl = header.querySelector('.council-level-icon');
|
|
339
|
+
const statusEl = header.querySelector('.council-level-status');
|
|
340
|
+
if (!iconEl || !statusEl) return;
|
|
341
|
+
|
|
342
|
+
// Derive state from overall status or from any level status
|
|
343
|
+
let state = 'pending';
|
|
344
|
+
if (status.status === 'running' || status.status === 'in_progress') {
|
|
345
|
+
state = 'running';
|
|
346
|
+
} else if (status.status === 'completed') {
|
|
347
|
+
state = 'completed';
|
|
348
|
+
} else if (status.status === 'failed') {
|
|
349
|
+
state = 'failed';
|
|
350
|
+
} else if (status.status === 'cancelled') {
|
|
351
|
+
state = 'cancelled';
|
|
352
|
+
} else if (status.levels) {
|
|
353
|
+
// Fall back to checking individual levels
|
|
354
|
+
const levelValues = Object.values(status.levels);
|
|
355
|
+
if (levelValues.some(l => l.status === 'running')) state = 'running';
|
|
356
|
+
else if (levelValues.some(l => l.status === 'completed')) state = 'running';
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
this._renderState(iconEl, statusEl, state, 'council-level');
|
|
360
|
+
|
|
361
|
+
// Show stream event text in the snippet element
|
|
362
|
+
const levelEl = header.closest('.council-level');
|
|
363
|
+
const snippetEl = levelEl?.querySelector('.council-level-snippet');
|
|
364
|
+
if (snippetEl) {
|
|
365
|
+
// Check for stream events in any level
|
|
366
|
+
let streamText = null;
|
|
367
|
+
if (status.levels) {
|
|
368
|
+
for (const lvl of Object.values(status.levels)) {
|
|
369
|
+
if (lvl.streamEvent?.text) {
|
|
370
|
+
streamText = lvl.streamEvent.text;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
if (state === 'running' && streamText) {
|
|
375
|
+
snippetEl.textContent = streamText;
|
|
376
|
+
snippetEl.style.display = 'block';
|
|
377
|
+
} else if (state !== 'running') {
|
|
378
|
+
snippetEl.style.display = 'none';
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
295
383
|
// ---------------------------------------------------------------------------
|
|
296
384
|
// Per-voice progress
|
|
297
385
|
// ---------------------------------------------------------------------------
|
|
@@ -518,6 +606,55 @@ class CouncilProgressModal {
|
|
|
518
606
|
}
|
|
519
607
|
}
|
|
520
608
|
|
|
609
|
+
// Handle executable voice updates (level 'exec'):
|
|
610
|
+
// Executable voices emit progress with level: 'exec' instead of numeric 1/2/3.
|
|
611
|
+
// The single "Running analysis..." row uses data-vc-level="exec".
|
|
612
|
+
const execLevel = status.levels?.exec;
|
|
613
|
+
if (execLevel) {
|
|
614
|
+
if (execLevel.voices) {
|
|
615
|
+
// Check if any exec voice was rendered with native L1/L2/L3 structure
|
|
616
|
+
// instead of the single exec row. This happens when the modal was opened
|
|
617
|
+
// before _executableVoices was populated. If so, re-render the full body.
|
|
618
|
+
let needsRerender = false;
|
|
619
|
+
for (const voiceId of Object.keys(execLevel.voices)) {
|
|
620
|
+
const execEl = this.modal.querySelector(`[data-vc-voice="${voiceId}"][data-vc-level="exec"]`);
|
|
621
|
+
if (!execEl) {
|
|
622
|
+
// Voice exists in exec progress but has no exec DOM element — was likely
|
|
623
|
+
// rendered with native structure. Re-render to fix.
|
|
624
|
+
needsRerender = true;
|
|
625
|
+
break;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (needsRerender && this.councilConfig) {
|
|
629
|
+
this._rebuildBodyVoiceCentric(this.councilConfig);
|
|
630
|
+
// Re-apply all progress to the fresh DOM
|
|
631
|
+
this._updateVoiceCentric(status);
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
for (const [voiceId, vStatus] of Object.entries(execLevel.voices)) {
|
|
636
|
+
this._setVoiceCentricLevelState(voiceId, 'exec', vStatus.status || 'running', vStatus);
|
|
637
|
+
}
|
|
638
|
+
} else if (execLevel.status && execLevel.status !== 'pending') {
|
|
639
|
+
// Aggregate terminal update without per-voice breakdown (completion/cancellation
|
|
640
|
+
// paths may collapse exec progress to just { status, progress }). Propagate the
|
|
641
|
+
// status to every rendered exec row, mirroring the pattern used for numeric levels
|
|
642
|
+
// with shared terminal state in _updateVoiceFromLevelStatus.
|
|
643
|
+
const terminalState = execLevel.status;
|
|
644
|
+
const execEls = this.modal.querySelectorAll('[data-vc-level="exec"]');
|
|
645
|
+
execEls.forEach(el => {
|
|
646
|
+
const voiceKey = el.dataset.vcVoice;
|
|
647
|
+
const stateKey = `${voiceKey}:exec`;
|
|
648
|
+
if (this._voiceStates[stateKey] !== 'completed' && this._voiceStates[stateKey] !== 'failed') {
|
|
649
|
+
this._setVoiceCentricLevelState(voiceKey, 'exec', terminalState, execLevel);
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
if (execLevel.streamEvent?.text && execLevel.voiceId) {
|
|
654
|
+
this._setVoiceCentricStreamText(execLevel.voiceId, 'exec', execLevel.streamEvent.text);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
521
658
|
// Handle per-voice orchestration updates (level 4):
|
|
522
659
|
// In voice-centric mode, each reviewer has a consolidation child at data-vc-level="4".
|
|
523
660
|
// The backend tracks per-voice orchestration state in levels[4].voices, including
|
|
@@ -1073,6 +1210,26 @@ class CouncilProgressModal {
|
|
|
1073
1210
|
}
|
|
1074
1211
|
}
|
|
1075
1212
|
|
|
1213
|
+
// Executable voices don't require enabled levels — add any not already captured
|
|
1214
|
+
// (handles all-executable councils where no levels are enabled).
|
|
1215
|
+
// Use _executableVoices (populated from progress events) as primary source,
|
|
1216
|
+
// falling back to the config modal's provider cache. This ensures executable
|
|
1217
|
+
// voices render correctly even when the modal is reopened for an already-running
|
|
1218
|
+
// analysis where the config modal cache may be empty.
|
|
1219
|
+
if (config.voices) {
|
|
1220
|
+
const providersInfo = window.analysisConfigModal?.providers || {};
|
|
1221
|
+
for (const voice of config.voices) {
|
|
1222
|
+
const isExec = providersInfo[voice.provider]?.isExecutable || false;
|
|
1223
|
+
if (isExec) {
|
|
1224
|
+
const sig = `${voice.provider}|${voice.model}|${voice.tier || 'balanced'}|${voice.customInstructions || ''}`;
|
|
1225
|
+
if (!seenSignatures.has(sig)) {
|
|
1226
|
+
seenSignatures.add(sig);
|
|
1227
|
+
uniqueVoices.push(voice);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1076
1233
|
// Build voiceMap: voiceKey -> { voice, levels } using deduplicated array indices
|
|
1077
1234
|
const voiceMap = new Map();
|
|
1078
1235
|
uniqueVoices.forEach((voice, idx) => {
|
|
@@ -1080,11 +1237,33 @@ class CouncilProgressModal {
|
|
|
1080
1237
|
voiceMap.set(voiceKey, { voice, levels: enabledLevelNums });
|
|
1081
1238
|
});
|
|
1082
1239
|
|
|
1240
|
+
// Also add any executable voices known from progress events but not yet
|
|
1241
|
+
// in the voiceMap (covers the case where config modal providers cache is
|
|
1242
|
+
// empty but we've already seen exec-level progress data).
|
|
1243
|
+
for (const execVoiceKey of this._executableVoices) {
|
|
1244
|
+
if (!voiceMap.has(execVoiceKey)) {
|
|
1245
|
+
// Find the matching voice in config.voices by key pattern
|
|
1246
|
+
const configVoices = config.voices || [];
|
|
1247
|
+
const matchingVoice = configVoices.find((v, idx) => {
|
|
1248
|
+
const candidateKey = `${v.provider}-${v.model}${idx > 0 ? `-${idx}` : ''}`;
|
|
1249
|
+
return candidateKey === execVoiceKey;
|
|
1250
|
+
});
|
|
1251
|
+
if (matchingVoice) {
|
|
1252
|
+
voiceMap.set(execVoiceKey, { voice: matchingVoice, levels: enabledLevelNums });
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1083
1257
|
let html = '<div class="council-progress-tree">';
|
|
1084
1258
|
|
|
1085
|
-
// Build a parent row for each unique voice
|
|
1259
|
+
// Build a parent row for each unique voice.
|
|
1260
|
+
// Determine executable status from: (1) _executableVoices set (populated from
|
|
1261
|
+
// progress events), or (2) config modal provider cache as fallback.
|
|
1262
|
+
const providersMap = window.analysisConfigModal?.providers || {};
|
|
1263
|
+
|
|
1086
1264
|
for (const [voiceKey, { voice, levels }] of voiceMap) {
|
|
1087
|
-
const
|
|
1265
|
+
const isExecutable = this._executableVoices.has(voiceKey) || (providersMap[voice.provider]?.isExecutable || false);
|
|
1266
|
+
const label = this._formatVoiceLabel(voice, { isExecutable });
|
|
1088
1267
|
|
|
1089
1268
|
html += `
|
|
1090
1269
|
<div class="council-level" data-voice-key="${voiceKey}">
|
|
@@ -1096,31 +1275,46 @@ class CouncilProgressModal {
|
|
|
1096
1275
|
<div class="council-level-children">
|
|
1097
1276
|
`;
|
|
1098
1277
|
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
const connectorClass = 'connector-mid';
|
|
1278
|
+
if (isExecutable) {
|
|
1279
|
+
// Executable voices: single "Running analysis..." row instead of L1/L2/L3
|
|
1102
1280
|
html += `
|
|
1103
|
-
<div class="council-voice
|
|
1104
|
-
<span class="council-voice-connector
|
|
1105
|
-
<span class="council-voice-icon
|
|
1106
|
-
<span class="council-voice-label">
|
|
1107
|
-
<span class="council-voice-status
|
|
1281
|
+
<div class="council-voice connector-last" data-vc-voice="${voiceKey}" data-vc-level="exec">
|
|
1282
|
+
<span class="council-voice-connector connector-last"></span>
|
|
1283
|
+
<span class="council-voice-icon pending">\u25CB</span>
|
|
1284
|
+
<span class="council-voice-label">Running analysis\u2026</span>
|
|
1285
|
+
<span class="council-voice-status pending">Pending</span>
|
|
1108
1286
|
<div class="council-voice-detail">
|
|
1109
1287
|
<div class="council-voice-snippet" style="display: none;"></div>
|
|
1110
1288
|
</div>
|
|
1111
1289
|
</div>
|
|
1112
1290
|
`;
|
|
1113
|
-
}
|
|
1291
|
+
} else {
|
|
1292
|
+
// Native voices: L1/L2/L3 children + orchestration
|
|
1293
|
+
levels.forEach((levelNum) => {
|
|
1294
|
+
const connectorClass = 'connector-mid';
|
|
1295
|
+
html += `
|
|
1296
|
+
<div class="council-voice ${connectorClass}" data-vc-voice="${voiceKey}" data-vc-level="${levelNum}">
|
|
1297
|
+
<span class="council-voice-connector ${connectorClass}"></span>
|
|
1298
|
+
<span class="council-voice-icon running"><span class="council-spinner"></span></span>
|
|
1299
|
+
<span class="council-voice-label">Level ${levelNum} \u2014 ${levelNames[levelNum]}</span>
|
|
1300
|
+
<span class="council-voice-status running">Running...</span>
|
|
1301
|
+
<div class="council-voice-detail">
|
|
1302
|
+
<div class="council-voice-snippet" style="display: none;"></div>
|
|
1303
|
+
</div>
|
|
1304
|
+
</div>
|
|
1305
|
+
`;
|
|
1306
|
+
});
|
|
1114
1307
|
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1308
|
+
// Orchestration child (always last)
|
|
1309
|
+
html += `
|
|
1310
|
+
<div class="council-voice connector-last" data-vc-voice="${voiceKey}" data-vc-level="4">
|
|
1311
|
+
<span class="council-voice-connector connector-last"></span>
|
|
1312
|
+
<span class="council-voice-icon pending">\u25CB</span>
|
|
1313
|
+
<span class="council-voice-label">Consolidation</span>
|
|
1314
|
+
<span class="council-voice-status pending">Pending</span>
|
|
1315
|
+
</div>
|
|
1316
|
+
`;
|
|
1317
|
+
}
|
|
1124
1318
|
|
|
1125
1319
|
html += `
|
|
1126
1320
|
</div>
|
|
@@ -1205,6 +1399,28 @@ class CouncilProgressModal {
|
|
|
1205
1399
|
body.innerHTML = html;
|
|
1206
1400
|
}
|
|
1207
1401
|
|
|
1402
|
+
/**
|
|
1403
|
+
* Rebuild the modal body for no-levels providers (e.g., executable providers).
|
|
1404
|
+
* Shows a single "Running analysis..." entry instead of the L1/L2/L3 breakdown.
|
|
1405
|
+
*/
|
|
1406
|
+
_rebuildBodyNoLevels() {
|
|
1407
|
+
const body = this.modal.querySelector('.council-progress-body');
|
|
1408
|
+
if (!body) return;
|
|
1409
|
+
|
|
1410
|
+
body.innerHTML = `
|
|
1411
|
+
<div class="council-progress-tree">
|
|
1412
|
+
<div class="council-level" data-level="analysis">
|
|
1413
|
+
<div class="council-level-header" data-level="analysis">
|
|
1414
|
+
<span class="council-level-icon pending">\u25CB</span>
|
|
1415
|
+
<span class="council-level-title">Running analysis\u2026</span>
|
|
1416
|
+
<span class="council-level-status pending">Pending</span>
|
|
1417
|
+
</div>
|
|
1418
|
+
<div class="council-level-snippet" style="display: none;"></div>
|
|
1419
|
+
</div>
|
|
1420
|
+
</div>
|
|
1421
|
+
`;
|
|
1422
|
+
}
|
|
1423
|
+
|
|
1208
1424
|
_resetFooter() {
|
|
1209
1425
|
const bgBtn = this.modal.querySelector('.council-bg-btn');
|
|
1210
1426
|
const cancelBtn = this.modal.querySelector('.council-cancel-btn');
|
|
@@ -1404,9 +1620,10 @@ class CouncilProgressModal {
|
|
|
1404
1620
|
* Example: { provider: 'claude', model: 'sonnet-4-5', tier: 'balanced' }
|
|
1405
1621
|
* -> "Claude sonnet-4-5 (Balanced)"
|
|
1406
1622
|
*/
|
|
1407
|
-
_formatVoiceLabel(voice) {
|
|
1623
|
+
_formatVoiceLabel(voice, { isExecutable = false } = {}) {
|
|
1408
1624
|
const provider = this._capitalize(voice.provider || 'unknown');
|
|
1409
1625
|
const model = voice.model || 'default';
|
|
1626
|
+
if (isExecutable) return `${provider} ${model}`;
|
|
1410
1627
|
const tier = this._capitalize(voice.tier || 'balanced');
|
|
1411
1628
|
return `${provider} ${model} (${tier})`;
|
|
1412
1629
|
}
|