@in-the-loop-labs/pair-review 3.0.6 → 3.1.1
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 +103 -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 +87 -9
- package/public/js/components/AnalysisConfigModal.js +206 -5
- package/public/js/components/ChatPanel.js +22 -5
- package/public/js/components/CouncilProgressModal.js +241 -23
- package/public/js/components/TimeoutSelect.js +2 -0
- package/public/js/components/VoiceCentricConfigTab.js +183 -13
- package/public/js/index.js +119 -1
- package/public/js/local.js +166 -51
- 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 +538 -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 +91 -4
- package/src/chat/prompt-builder.js +39 -4
- 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/github/parser.js +1 -1
- package/src/local-review.js +15 -9
- package/src/local-scope.js +83 -0
- 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
|
@@ -525,10 +525,13 @@ class VoiceCentricConfigTab {
|
|
|
525
525
|
}
|
|
526
526
|
});
|
|
527
527
|
|
|
528
|
-
// Provider change -> update model dropdowns
|
|
528
|
+
// Provider change -> update model dropdowns + executable state + timeout default
|
|
529
529
|
panel.addEventListener('change', (e) => {
|
|
530
530
|
if (e.target.classList.contains('voice-provider')) {
|
|
531
531
|
this._updateModelDropdown(e.target);
|
|
532
|
+
this._updateExecutableState(e.target);
|
|
533
|
+
this._updateLevelToggleState();
|
|
534
|
+
this._applyProviderDefaultTimeout(e.target);
|
|
532
535
|
}
|
|
533
536
|
// Model change -> update tier to match model's recommended tier
|
|
534
537
|
if (e.target.classList.contains('voice-model')) {
|
|
@@ -597,13 +600,16 @@ class VoiceCentricConfigTab {
|
|
|
597
600
|
TimeoutSelect.mount(mount, { className: 'vc-timeout', title: 'Per-reviewer timeout' });
|
|
598
601
|
}
|
|
599
602
|
|
|
600
|
-
// Populate provider dropdown
|
|
603
|
+
// Populate provider dropdown and update executable state
|
|
601
604
|
const newProviderSelect = list.querySelector(`.voice-provider[data-index="${index}"]`);
|
|
602
605
|
if (newProviderSelect) {
|
|
603
606
|
this._populateProviderDropdown(newProviderSelect);
|
|
607
|
+
this._applyProviderDefaultTimeout(newProviderSelect);
|
|
608
|
+
this._updateExecutableState(newProviderSelect);
|
|
604
609
|
}
|
|
605
610
|
|
|
606
611
|
this._updateRemoveButtonVisibility();
|
|
612
|
+
this._updateLevelToggleState();
|
|
607
613
|
this._markDirty();
|
|
608
614
|
}
|
|
609
615
|
|
|
@@ -619,6 +625,7 @@ class VoiceCentricConfigTab {
|
|
|
619
625
|
|
|
620
626
|
this._reindexReviewers();
|
|
621
627
|
this._updateRemoveButtonVisibility();
|
|
628
|
+
this._updateLevelToggleState();
|
|
622
629
|
this._markDirty();
|
|
623
630
|
}
|
|
624
631
|
|
|
@@ -669,6 +676,16 @@ class VoiceCentricConfigTab {
|
|
|
669
676
|
iconBtn.classList.toggle('has-instructions', hasContent);
|
|
670
677
|
}
|
|
671
678
|
|
|
679
|
+
/**
|
|
680
|
+
* Get the default timeout for a provider, falling back to the static DEFAULT_TIMEOUT.
|
|
681
|
+
* @param {string} providerId - Provider ID (e.g., 'pi', 'claude')
|
|
682
|
+
* @returns {number} Default timeout in ms
|
|
683
|
+
*/
|
|
684
|
+
_getProviderDefaultTimeout(providerId) {
|
|
685
|
+
const provider = this.providers[providerId];
|
|
686
|
+
return provider?.defaultTimeout ?? VoiceCentricConfigTab.DEFAULT_TIMEOUT;
|
|
687
|
+
}
|
|
688
|
+
|
|
672
689
|
/**
|
|
673
690
|
* Update the clock/timeout icon styling to indicate non-default timeout.
|
|
674
691
|
* @param {Element} panel - The council panel element
|
|
@@ -680,7 +697,9 @@ class VoiceCentricConfigTab {
|
|
|
680
697
|
const iconBtn = wrapper?.querySelector(`.toggle-timeout-icon[data-index="${index}"]`);
|
|
681
698
|
if (!iconBtn) return;
|
|
682
699
|
|
|
683
|
-
const
|
|
700
|
+
const providerId = wrapper?.querySelector('.voice-provider')?.value;
|
|
701
|
+
const defaultTimeout = this._getProviderDefaultTimeout(providerId);
|
|
702
|
+
const isNonDefault = parseInt(value, 10) !== defaultTimeout;
|
|
684
703
|
iconBtn.classList.toggle('has-custom-timeout', isNonDefault);
|
|
685
704
|
}
|
|
686
705
|
|
|
@@ -688,10 +707,56 @@ class VoiceCentricConfigTab {
|
|
|
688
707
|
const iconBtn = panel.querySelector('#vc-orchestration-timeout-toggle');
|
|
689
708
|
if (!iconBtn) return;
|
|
690
709
|
|
|
691
|
-
const
|
|
710
|
+
const orchRow = panel.querySelector('#vc-orchestration-voice');
|
|
711
|
+
const providerId = orchRow?.querySelector('.voice-provider')?.value;
|
|
712
|
+
const defaultTimeout = this._getProviderDefaultTimeout(providerId);
|
|
713
|
+
const isNonDefault = parseInt(value, 10) !== defaultTimeout;
|
|
692
714
|
iconBtn.classList.toggle('has-custom-timeout', isNonDefault);
|
|
693
715
|
}
|
|
694
716
|
|
|
717
|
+
/**
|
|
718
|
+
* When a voice's provider changes, update its timeout to the new provider's default,
|
|
719
|
+
* preserving explicit user overrides via Math.max when the user had customized the value.
|
|
720
|
+
* @param {HTMLSelectElement} providerSelect - The provider dropdown that changed
|
|
721
|
+
*/
|
|
722
|
+
_applyProviderDefaultTimeout(providerSelect) {
|
|
723
|
+
const panel = this.modal.querySelector('#tab-panel-council');
|
|
724
|
+
if (!panel) return;
|
|
725
|
+
|
|
726
|
+
const providerId = providerSelect.value;
|
|
727
|
+
const newDefault = this._getProviderDefaultTimeout(providerId);
|
|
728
|
+
const oldProviderId = providerSelect.dataset.previousProvider;
|
|
729
|
+
const oldDefault = oldProviderId ? this._getProviderDefaultTimeout(oldProviderId) : null;
|
|
730
|
+
|
|
731
|
+
// Determine which timeout element to update
|
|
732
|
+
const isOrchestration = providerSelect.dataset.target === 'orchestration';
|
|
733
|
+
if (isOrchestration) {
|
|
734
|
+
const timeoutEl = panel.querySelector('#vc-orchestration-timeout');
|
|
735
|
+
if (timeoutEl) {
|
|
736
|
+
const currentValue = parseInt(timeoutEl.value, 10);
|
|
737
|
+
const resolvedTimeout = (oldDefault !== null && currentValue !== oldDefault)
|
|
738
|
+
? Math.max(currentValue, newDefault)
|
|
739
|
+
: newDefault;
|
|
740
|
+
timeoutEl.value = String(resolvedTimeout);
|
|
741
|
+
this._updateOrchestrationTimeoutIcon(panel, String(resolvedTimeout));
|
|
742
|
+
}
|
|
743
|
+
} else {
|
|
744
|
+
const idx = providerSelect.dataset.index;
|
|
745
|
+
const wrapper = providerSelect.closest('.vc-reviewer');
|
|
746
|
+
const timeoutEl = wrapper?.querySelector('.vc-timeout');
|
|
747
|
+
if (timeoutEl) {
|
|
748
|
+
const currentValue = parseInt(timeoutEl.value, 10);
|
|
749
|
+
const resolvedTimeout = (oldDefault !== null && currentValue !== oldDefault)
|
|
750
|
+
? Math.max(currentValue, newDefault)
|
|
751
|
+
: newDefault;
|
|
752
|
+
timeoutEl.value = String(resolvedTimeout);
|
|
753
|
+
this._updateTimeoutIcon(panel, idx, String(resolvedTimeout));
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
providerSelect.dataset.previousProvider = providerId;
|
|
758
|
+
}
|
|
759
|
+
|
|
695
760
|
// --- Dropdown / model management ---
|
|
696
761
|
|
|
697
762
|
_updateAllVoiceDropdowns() {
|
|
@@ -705,10 +770,13 @@ class VoiceCentricConfigTab {
|
|
|
705
770
|
|
|
706
771
|
_populateProviderDropdown(select) {
|
|
707
772
|
const currentValue = select.value;
|
|
773
|
+
const isConsolidation = select.dataset.target === 'orchestration';
|
|
708
774
|
select.innerHTML = '';
|
|
709
775
|
const providerIds = Object.keys(this.providers).filter(id => {
|
|
710
776
|
const p = this.providers[id];
|
|
711
|
-
|
|
777
|
+
if (p.availability && !p.availability.available) return false;
|
|
778
|
+
if (isConsolidation && p.capabilities?.consolidation === false) return false;
|
|
779
|
+
return true;
|
|
712
780
|
}).sort((a, b) => (this.providers[a].name || a).localeCompare(this.providers[b].name || b));
|
|
713
781
|
|
|
714
782
|
for (const id of providerIds) {
|
|
@@ -779,6 +847,87 @@ class VoiceCentricConfigTab {
|
|
|
779
847
|
}
|
|
780
848
|
}
|
|
781
849
|
|
|
850
|
+
/**
|
|
851
|
+
* Update UI state for a reviewer row based on whether its provider is executable.
|
|
852
|
+
* Hides the tier dropdown and shows a note for executable providers.
|
|
853
|
+
* @param {HTMLSelectElement} providerSelect - The provider dropdown that changed
|
|
854
|
+
*/
|
|
855
|
+
_updateExecutableState(providerSelect) {
|
|
856
|
+
const providerId = providerSelect.value;
|
|
857
|
+
const provider = this.providers[providerId];
|
|
858
|
+
const isExecutable = provider?.isExecutable || false;
|
|
859
|
+
const noCustomInstructions = provider?.capabilities?.custom_instructions === false;
|
|
860
|
+
const container = providerSelect.closest('.voice-row');
|
|
861
|
+
if (!container) return;
|
|
862
|
+
|
|
863
|
+
const tierSelect = container.querySelector('.voice-tier');
|
|
864
|
+
if (tierSelect) {
|
|
865
|
+
tierSelect.style.display = isExecutable ? 'none' : '';
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// Hide per-reviewer instructions toggle and area when provider doesn't support them
|
|
869
|
+
const idx = providerSelect.dataset?.index;
|
|
870
|
+
const instrToggle = container.querySelector(`.toggle-instructions-icon[data-index="${idx}"]`);
|
|
871
|
+
if (instrToggle) {
|
|
872
|
+
instrToggle.style.display = noCustomInstructions ? 'none' : '';
|
|
873
|
+
}
|
|
874
|
+
const instrArea = container.querySelector(`.voice-instructions-area[data-index="${idx}"]`);
|
|
875
|
+
if (instrArea && noCustomInstructions) {
|
|
876
|
+
instrArea.style.display = 'none';
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Add or remove the executable note
|
|
880
|
+
let note = container.querySelector('.executable-note');
|
|
881
|
+
if (isExecutable && !note) {
|
|
882
|
+
note = document.createElement('span');
|
|
883
|
+
note.className = 'executable-note';
|
|
884
|
+
note.textContent = 'External tool';
|
|
885
|
+
note.title = 'External tool \u2014 runs its own analysis pipeline';
|
|
886
|
+
container.appendChild(note);
|
|
887
|
+
} else if (!isExecutable && note) {
|
|
888
|
+
note.remove();
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Update level toggle state based on whether all voices are executable.
|
|
894
|
+
* If all voices are executable, disable level checkboxes and show a note.
|
|
895
|
+
* If any native voice is present, re-enable.
|
|
896
|
+
*/
|
|
897
|
+
_updateLevelToggleState() {
|
|
898
|
+
const panel = this.modal.querySelector('#tab-panel-council');
|
|
899
|
+
if (!panel) return;
|
|
900
|
+
|
|
901
|
+
const reviewers = panel.querySelectorAll('.vc-reviewer');
|
|
902
|
+
let allExecutable = reviewers.length > 0;
|
|
903
|
+
reviewers.forEach(wrapper => {
|
|
904
|
+
const providerSelect = wrapper.querySelector('.voice-provider');
|
|
905
|
+
const providerId = providerSelect?.value;
|
|
906
|
+
const provider = this.providers[providerId];
|
|
907
|
+
if (!provider?.isExecutable) {
|
|
908
|
+
allExecutable = false;
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
|
|
912
|
+
const checkboxes = panel.querySelectorAll('.vc-level-checkbox');
|
|
913
|
+
checkboxes.forEach(cb => {
|
|
914
|
+
cb.disabled = allExecutable;
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
// Add or remove the all-executable note
|
|
918
|
+
const togglesContainer = panel.querySelector('.vc-level-toggles');
|
|
919
|
+
if (!togglesContainer) return;
|
|
920
|
+
let note = togglesContainer.querySelector('.vc-levels-disabled-note');
|
|
921
|
+
if (allExecutable && !note) {
|
|
922
|
+
note = document.createElement('p');
|
|
923
|
+
note.className = 'vc-levels-disabled-note section-hint-text';
|
|
924
|
+
note.textContent = 'Level selection does not apply when all reviewers are external tools';
|
|
925
|
+
togglesContainer.appendChild(note);
|
|
926
|
+
} else if (!allExecutable && note) {
|
|
927
|
+
note.remove();
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
782
931
|
/**
|
|
783
932
|
* Mount all TimeoutSelect instances on the panel.
|
|
784
933
|
* Called after HTML is injected into the DOM.
|
|
@@ -808,10 +957,11 @@ class VoiceCentricConfigTab {
|
|
|
808
957
|
// --- Config read/write ---
|
|
809
958
|
|
|
810
959
|
_defaultConfig() {
|
|
960
|
+
const defaultTimeout = this._getProviderDefaultTimeout(this._defaultProvider);
|
|
811
961
|
return {
|
|
812
|
-
voices: [{ provider: this._defaultProvider, model: this._defaultModel, tier: 'balanced', timeout:
|
|
962
|
+
voices: [{ provider: this._defaultProvider, model: this._defaultModel, tier: 'balanced', timeout: defaultTimeout }],
|
|
813
963
|
enabledLevels: [1, 2, 3],
|
|
814
|
-
orchestration: { provider: this._defaultProvider, model: this._defaultModel, tier: 'balanced', timeout:
|
|
964
|
+
orchestration: { provider: this._defaultProvider, model: this._defaultModel, tier: 'balanced', timeout: defaultTimeout }
|
|
815
965
|
};
|
|
816
966
|
}
|
|
817
967
|
|
|
@@ -836,7 +986,9 @@ class VoiceCentricConfigTab {
|
|
|
836
986
|
const timeout = timeoutSelect ? parseInt(timeoutSelect.value, 10) : VoiceCentricConfigTab.DEFAULT_TIMEOUT;
|
|
837
987
|
const idx = wrapper.dataset.index;
|
|
838
988
|
const instrInput = wrapper.querySelector(`.voice-instructions-input[data-index="${idx}"]`);
|
|
839
|
-
const
|
|
989
|
+
const providerInfo = this.providers[provider];
|
|
990
|
+
const supportsCustomInstructions = providerInfo?.capabilities?.custom_instructions !== false;
|
|
991
|
+
const customInstructions = supportsCustomInstructions ? (instrInput?.value?.trim() || undefined) : undefined;
|
|
840
992
|
|
|
841
993
|
if (provider && model) {
|
|
842
994
|
const voice = { provider, model, tier, timeout };
|
|
@@ -939,7 +1091,7 @@ class VoiceCentricConfigTab {
|
|
|
939
1091
|
list.innerHTML = '';
|
|
940
1092
|
const voices = vcConfig.voices || [];
|
|
941
1093
|
if (voices.length === 0) {
|
|
942
|
-
voices.push({ provider: this._defaultProvider, model: this._defaultModel, tier: 'balanced', timeout:
|
|
1094
|
+
voices.push({ provider: this._defaultProvider, model: this._defaultModel, tier: 'balanced', timeout: this._getProviderDefaultTimeout(this._defaultProvider) });
|
|
943
1095
|
}
|
|
944
1096
|
voices.forEach((voice, i) => {
|
|
945
1097
|
const wrapper = document.createElement('div');
|
|
@@ -954,7 +1106,9 @@ class VoiceCentricConfigTab {
|
|
|
954
1106
|
if (providerSelect) {
|
|
955
1107
|
this._populateProviderDropdown(providerSelect);
|
|
956
1108
|
providerSelect.value = voice.provider;
|
|
1109
|
+
providerSelect.dataset.previousProvider = voice.provider;
|
|
957
1110
|
this._updateModelDropdown(providerSelect);
|
|
1111
|
+
this._updateExecutableState(providerSelect);
|
|
958
1112
|
const modelSelect = row.querySelector('.voice-model');
|
|
959
1113
|
if (modelSelect) modelSelect.value = voice.model;
|
|
960
1114
|
const tierSelect = row.querySelector('.voice-tier');
|
|
@@ -965,13 +1119,17 @@ class VoiceCentricConfigTab {
|
|
|
965
1119
|
TimeoutSelect.mount(mount, { className: 'vc-timeout', title: 'Per-reviewer timeout' });
|
|
966
1120
|
}
|
|
967
1121
|
const timeoutEl = row.querySelector('.vc-timeout');
|
|
1122
|
+
const providerDefaultTimeout = this._getProviderDefaultTimeout(voice.provider);
|
|
968
1123
|
if (timeoutEl && voice.timeout) {
|
|
969
1124
|
timeoutEl.value = String(voice.timeout);
|
|
970
|
-
// Show the dropdown if non-default
|
|
971
|
-
if (voice.timeout !==
|
|
1125
|
+
// Show the dropdown if non-default for this provider
|
|
1126
|
+
if (voice.timeout !== providerDefaultTimeout) {
|
|
972
1127
|
timeoutEl.style.display = '';
|
|
973
1128
|
}
|
|
974
1129
|
this._updateTimeoutIcon(panel, String(i), String(voice.timeout));
|
|
1130
|
+
} else if (timeoutEl) {
|
|
1131
|
+
// No saved timeout — apply the provider's default
|
|
1132
|
+
timeoutEl.value = String(providerDefaultTimeout);
|
|
975
1133
|
}
|
|
976
1134
|
}
|
|
977
1135
|
|
|
@@ -985,6 +1143,7 @@ class VoiceCentricConfigTab {
|
|
|
985
1143
|
});
|
|
986
1144
|
|
|
987
1145
|
this._updateRemoveButtonVisibility();
|
|
1146
|
+
this._updateLevelToggleState();
|
|
988
1147
|
}
|
|
989
1148
|
|
|
990
1149
|
// Apply level toggles
|
|
@@ -1002,6 +1161,7 @@ class VoiceCentricConfigTab {
|
|
|
1002
1161
|
if (providerSelect) {
|
|
1003
1162
|
this._populateProviderDropdown(providerSelect);
|
|
1004
1163
|
providerSelect.value = vcConfig.orchestration.provider;
|
|
1164
|
+
providerSelect.dataset.previousProvider = vcConfig.orchestration.provider;
|
|
1005
1165
|
this._updateModelDropdown(providerSelect);
|
|
1006
1166
|
const modelSelect = orchRow.querySelector('.voice-model');
|
|
1007
1167
|
if (modelSelect) modelSelect.value = vcConfig.orchestration.model;
|
|
@@ -1012,13 +1172,18 @@ class VoiceCentricConfigTab {
|
|
|
1012
1172
|
|
|
1013
1173
|
// Restore orchestration timeout
|
|
1014
1174
|
const orchTimeoutSelect = panel.querySelector('#vc-orchestration-timeout');
|
|
1175
|
+
const orchProviderDefaultTimeout = this._getProviderDefaultTimeout(vcConfig.orchestration.provider);
|
|
1015
1176
|
if (orchTimeoutSelect && vcConfig.orchestration.timeout) {
|
|
1016
1177
|
orchTimeoutSelect.value = String(vcConfig.orchestration.timeout);
|
|
1017
|
-
// Show the dropdown if non-default
|
|
1018
|
-
if (vcConfig.orchestration.timeout !==
|
|
1178
|
+
// Show the dropdown if non-default for this provider
|
|
1179
|
+
if (vcConfig.orchestration.timeout !== orchProviderDefaultTimeout) {
|
|
1019
1180
|
orchTimeoutSelect.style.display = '';
|
|
1020
1181
|
}
|
|
1021
1182
|
this._updateOrchestrationTimeoutIcon(panel, String(vcConfig.orchestration.timeout));
|
|
1183
|
+
} else if (orchTimeoutSelect) {
|
|
1184
|
+
// No saved timeout — apply the provider's default
|
|
1185
|
+
orchTimeoutSelect.value = String(orchProviderDefaultTimeout);
|
|
1186
|
+
this._updateOrchestrationTimeoutIcon(panel, String(orchProviderDefaultTimeout));
|
|
1022
1187
|
}
|
|
1023
1188
|
|
|
1024
1189
|
// Restore orchestration custom instructions
|
|
@@ -1245,6 +1410,11 @@ class VoiceCentricConfigTab {
|
|
|
1245
1410
|
}
|
|
1246
1411
|
this._markClean();
|
|
1247
1412
|
await this.loadCouncils();
|
|
1413
|
+
const selector = this.modal.querySelector('#vc-council-selector');
|
|
1414
|
+
if (selector) {
|
|
1415
|
+
selector.value = this.selectedCouncilId;
|
|
1416
|
+
selector.classList.remove('new-council-selected');
|
|
1417
|
+
}
|
|
1248
1418
|
}
|
|
1249
1419
|
|
|
1250
1420
|
async _postCouncil(name, config) {
|
package/public/js/index.js
CHANGED
|
@@ -553,6 +553,9 @@
|
|
|
553
553
|
container.classList.remove('recent-reviews-loading');
|
|
554
554
|
localReviewsPagination.loaded = true;
|
|
555
555
|
|
|
556
|
+
// Apply analysis-in-progress spinners to any rows with active analyses
|
|
557
|
+
fetchAndApplyActiveAnalyses();
|
|
558
|
+
|
|
556
559
|
} catch (error) {
|
|
557
560
|
console.error('Error loading local reviews:', error);
|
|
558
561
|
container.innerHTML =
|
|
@@ -593,6 +596,9 @@
|
|
|
593
596
|
tbody.insertAdjacentHTML('beforeend', data.sessions.map(renderLocalReviewRow).join(''));
|
|
594
597
|
if (localSelection.active) localSelection.onRowsAdded();
|
|
595
598
|
|
|
599
|
+
// Apply analysis spinners to newly added rows
|
|
600
|
+
fetchAndApplyActiveAnalyses();
|
|
601
|
+
|
|
596
602
|
localReviewsPagination.lastTimestamp = data.sessions[data.sessions.length - 1].updated_at;
|
|
597
603
|
localReviewsPagination.hasMore = !!data.hasMore;
|
|
598
604
|
|
|
@@ -764,8 +770,10 @@
|
|
|
764
770
|
? '<a href="https://github.com/' + encodeURIComponent(review.author) + '" target="_blank" rel="noopener">' + escapeHtml(review.author) + '</a>'
|
|
765
771
|
: '';
|
|
766
772
|
|
|
773
|
+
var reviewIdAttr = review.review_id ? ' data-analysis-review-id="' + review.review_id + '"' : '';
|
|
774
|
+
|
|
767
775
|
return '' +
|
|
768
|
-
'<tr data-review-id="' + review.id + '">' +
|
|
776
|
+
'<tr data-review-id="' + review.id + '"' + reviewIdAttr + '>' +
|
|
769
777
|
'<td class="col-repo">' + escapeHtml(review.repository) + '</td>' +
|
|
770
778
|
'<td class="col-pr"><a href="' + link + '">#' + review.pr_number + '</a></td>' +
|
|
771
779
|
'<td class="col-title" title="' + escapeHtml(review.pr_title) + '">' + escapeHtml(review.pr_title) + '</td>' +
|
|
@@ -927,6 +935,9 @@
|
|
|
927
935
|
container.innerHTML = html;
|
|
928
936
|
container.classList.remove('recent-reviews-loading');
|
|
929
937
|
|
|
938
|
+
// Apply analysis-in-progress spinners to any rows with active analyses
|
|
939
|
+
fetchAndApplyActiveAnalyses();
|
|
940
|
+
|
|
930
941
|
} catch (error) {
|
|
931
942
|
console.error('Error loading recent reviews:', error);
|
|
932
943
|
container.innerHTML =
|
|
@@ -992,6 +1003,9 @@
|
|
|
992
1003
|
tbody.insertAdjacentHTML('beforeend', data.reviews.map(renderRecentReviewRow).join(''));
|
|
993
1004
|
if (prSelection.active) prSelection.onRowsAdded();
|
|
994
1005
|
|
|
1006
|
+
// Apply analysis spinners to newly added rows
|
|
1007
|
+
fetchAndApplyActiveAnalyses();
|
|
1008
|
+
|
|
995
1009
|
// Update pagination state - advance the cursor
|
|
996
1010
|
recentReviewsPagination.lastTimestamp = data.reviews[data.reviews.length - 1].last_accessed_at;
|
|
997
1011
|
recentReviewsPagination.hasMore = !!data.hasMore;
|
|
@@ -1138,6 +1152,7 @@
|
|
|
1138
1152
|
window.__pairReview.chatProviders = chatProviders;
|
|
1139
1153
|
window.__pairReview.enableGraphite = config.enable_graphite === true;
|
|
1140
1154
|
window.__pairReview.chatSpinner = config.chat_spinner || 'dots';
|
|
1155
|
+
window.__pairReview.chatEnterToSend = config.chat_enter_to_send !== false;
|
|
1141
1156
|
|
|
1142
1157
|
// Set chat feature state based on config and provider availability
|
|
1143
1158
|
let chatState = 'disabled';
|
|
@@ -1924,4 +1939,107 @@
|
|
|
1924
1939
|
}
|
|
1925
1940
|
});
|
|
1926
1941
|
|
|
1942
|
+
// ─── Analysis-in-progress Spinners ──────────────────────────────────────────
|
|
1943
|
+
|
|
1944
|
+
/** Set of reviewIds (integers) that currently have a spinner on the page */
|
|
1945
|
+
var activeSpinnerReviewIds = new Set();
|
|
1946
|
+
|
|
1947
|
+
/**
|
|
1948
|
+
* Find a table row matching the given reviewId.
|
|
1949
|
+
* Checks both PR rows (data-analysis-review-id) and local rows (data-session-id).
|
|
1950
|
+
* @param {number} reviewId - The reviews.id to find
|
|
1951
|
+
* @returns {HTMLElement|null}
|
|
1952
|
+
*/
|
|
1953
|
+
function findRowByReviewId(reviewId) {
|
|
1954
|
+
return document.querySelector(
|
|
1955
|
+
'tr[data-analysis-review-id="' + reviewId + '"], tr[data-session-id="' + reviewId + '"]'
|
|
1956
|
+
);
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
/**
|
|
1960
|
+
* Add a spinner to the title/name cell of a row.
|
|
1961
|
+
* @param {HTMLElement} row - The <tr> element
|
|
1962
|
+
* @param {number} reviewId - The reviewId to track
|
|
1963
|
+
*/
|
|
1964
|
+
function addSpinnerToRow(row, reviewId) {
|
|
1965
|
+
if (row.querySelector('.index-analysis-spinner')) return;
|
|
1966
|
+
|
|
1967
|
+
var spinner = document.createElement('span');
|
|
1968
|
+
spinner.className = 'index-analysis-spinner';
|
|
1969
|
+
spinner.title = 'Analysis in progress';
|
|
1970
|
+
|
|
1971
|
+
// PR rows: prepend in .col-title; local rows: prepend in .col-local-name
|
|
1972
|
+
var cell = row.querySelector('.col-title') || row.querySelector('.col-local-name');
|
|
1973
|
+
if (cell) {
|
|
1974
|
+
cell.insertBefore(spinner, cell.firstChild);
|
|
1975
|
+
}
|
|
1976
|
+
activeSpinnerReviewIds.add(reviewId);
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
/**
|
|
1980
|
+
* Remove a spinner from a row.
|
|
1981
|
+
* @param {HTMLElement} row - The <tr> element
|
|
1982
|
+
* @param {number} reviewId - The reviewId to untrack
|
|
1983
|
+
*/
|
|
1984
|
+
function removeSpinnerFromRow(row, reviewId) {
|
|
1985
|
+
var spinner = row.querySelector('.index-analysis-spinner');
|
|
1986
|
+
if (spinner) spinner.remove();
|
|
1987
|
+
activeSpinnerReviewIds.delete(reviewId);
|
|
1988
|
+
}
|
|
1989
|
+
|
|
1990
|
+
/**
|
|
1991
|
+
* Fetch active analyses and apply/remove spinners to matching rows.
|
|
1992
|
+
*/
|
|
1993
|
+
function fetchAndApplyActiveAnalyses() {
|
|
1994
|
+
fetch('/api/analyses/active')
|
|
1995
|
+
.then(function (res) { return res.json(); })
|
|
1996
|
+
.then(function (data) {
|
|
1997
|
+
if (!data.active) return;
|
|
1998
|
+
|
|
1999
|
+
var activeReviewIds = new Set();
|
|
2000
|
+
data.active.forEach(function (entry) {
|
|
2001
|
+
var rid = entry.reviewId;
|
|
2002
|
+
if (rid == null) return;
|
|
2003
|
+
activeReviewIds.add(rid);
|
|
2004
|
+
var row = findRowByReviewId(rid);
|
|
2005
|
+
if (row) addSpinnerToRow(row, rid);
|
|
2006
|
+
});
|
|
2007
|
+
|
|
2008
|
+
// Remove spinners for analyses that are no longer active
|
|
2009
|
+
activeSpinnerReviewIds.forEach(function (rid) {
|
|
2010
|
+
if (!activeReviewIds.has(rid)) {
|
|
2011
|
+
var row = findRowByReviewId(rid);
|
|
2012
|
+
if (row) removeSpinnerFromRow(row, rid);
|
|
2013
|
+
else activeSpinnerReviewIds.delete(rid);
|
|
2014
|
+
}
|
|
2015
|
+
});
|
|
2016
|
+
})
|
|
2017
|
+
.catch(function () { /* ignore — spinners are best-effort */ });
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
// Subscribe to real-time analysis events via WebSocket
|
|
2021
|
+
if (window.wsClient) {
|
|
2022
|
+
window.wsClient.connect();
|
|
2023
|
+
window.wsClient.subscribe('index:analyses', function (msg) {
|
|
2024
|
+
var reviewId = msg.reviewId;
|
|
2025
|
+
if (reviewId == null) return;
|
|
2026
|
+
|
|
2027
|
+
if (msg.type === 'analysis_started') {
|
|
2028
|
+
var row = findRowByReviewId(reviewId);
|
|
2029
|
+
if (row) addSpinnerToRow(row, reviewId);
|
|
2030
|
+
} else if (msg.type === 'analysis_ended') {
|
|
2031
|
+
var endRow = findRowByReviewId(reviewId);
|
|
2032
|
+
if (endRow) removeSpinnerFromRow(endRow, reviewId);
|
|
2033
|
+
else activeSpinnerReviewIds.delete(reviewId);
|
|
2034
|
+
}
|
|
2035
|
+
});
|
|
2036
|
+
|
|
2037
|
+
// On reconnect, re-fetch active analyses to catch anything missed
|
|
2038
|
+
window.addEventListener('wsReconnected', fetchAndApplyActiveAnalyses);
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
// Expose for use after data loads (loadRecentReviews / loadLocalReviews)
|
|
2042
|
+
window.__pairReview = window.__pairReview || {};
|
|
2043
|
+
window.__pairReview.refreshAnalysisSpinners = fetchAndApplyActiveAnalyses;
|
|
2044
|
+
|
|
1927
2045
|
})();
|