@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
|
@@ -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() {
|
|
@@ -779,6 +844,87 @@ class VoiceCentricConfigTab {
|
|
|
779
844
|
}
|
|
780
845
|
}
|
|
781
846
|
|
|
847
|
+
/**
|
|
848
|
+
* Update UI state for a reviewer row based on whether its provider is executable.
|
|
849
|
+
* Hides the tier dropdown and shows a note for executable providers.
|
|
850
|
+
* @param {HTMLSelectElement} providerSelect - The provider dropdown that changed
|
|
851
|
+
*/
|
|
852
|
+
_updateExecutableState(providerSelect) {
|
|
853
|
+
const providerId = providerSelect.value;
|
|
854
|
+
const provider = this.providers[providerId];
|
|
855
|
+
const isExecutable = provider?.isExecutable || false;
|
|
856
|
+
const noCustomInstructions = provider?.capabilities?.custom_instructions === false;
|
|
857
|
+
const container = providerSelect.closest('.voice-row');
|
|
858
|
+
if (!container) return;
|
|
859
|
+
|
|
860
|
+
const tierSelect = container.querySelector('.voice-tier');
|
|
861
|
+
if (tierSelect) {
|
|
862
|
+
tierSelect.style.display = isExecutable ? 'none' : '';
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Hide per-reviewer instructions toggle and area when provider doesn't support them
|
|
866
|
+
const idx = providerSelect.dataset?.index;
|
|
867
|
+
const instrToggle = container.querySelector(`.toggle-instructions-icon[data-index="${idx}"]`);
|
|
868
|
+
if (instrToggle) {
|
|
869
|
+
instrToggle.style.display = noCustomInstructions ? 'none' : '';
|
|
870
|
+
}
|
|
871
|
+
const instrArea = container.querySelector(`.voice-instructions-area[data-index="${idx}"]`);
|
|
872
|
+
if (instrArea && noCustomInstructions) {
|
|
873
|
+
instrArea.style.display = 'none';
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Add or remove the executable note
|
|
877
|
+
let note = container.querySelector('.executable-note');
|
|
878
|
+
if (isExecutable && !note) {
|
|
879
|
+
note = document.createElement('span');
|
|
880
|
+
note.className = 'executable-note';
|
|
881
|
+
note.textContent = 'External tool';
|
|
882
|
+
note.title = 'External tool \u2014 runs its own analysis pipeline';
|
|
883
|
+
container.appendChild(note);
|
|
884
|
+
} else if (!isExecutable && note) {
|
|
885
|
+
note.remove();
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Update level toggle state based on whether all voices are executable.
|
|
891
|
+
* If all voices are executable, disable level checkboxes and show a note.
|
|
892
|
+
* If any native voice is present, re-enable.
|
|
893
|
+
*/
|
|
894
|
+
_updateLevelToggleState() {
|
|
895
|
+
const panel = this.modal.querySelector('#tab-panel-council');
|
|
896
|
+
if (!panel) return;
|
|
897
|
+
|
|
898
|
+
const reviewers = panel.querySelectorAll('.vc-reviewer');
|
|
899
|
+
let allExecutable = reviewers.length > 0;
|
|
900
|
+
reviewers.forEach(wrapper => {
|
|
901
|
+
const providerSelect = wrapper.querySelector('.voice-provider');
|
|
902
|
+
const providerId = providerSelect?.value;
|
|
903
|
+
const provider = this.providers[providerId];
|
|
904
|
+
if (!provider?.isExecutable) {
|
|
905
|
+
allExecutable = false;
|
|
906
|
+
}
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
const checkboxes = panel.querySelectorAll('.vc-level-checkbox');
|
|
910
|
+
checkboxes.forEach(cb => {
|
|
911
|
+
cb.disabled = allExecutable;
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
// Add or remove the all-executable note
|
|
915
|
+
const togglesContainer = panel.querySelector('.vc-level-toggles');
|
|
916
|
+
if (!togglesContainer) return;
|
|
917
|
+
let note = togglesContainer.querySelector('.vc-levels-disabled-note');
|
|
918
|
+
if (allExecutable && !note) {
|
|
919
|
+
note = document.createElement('p');
|
|
920
|
+
note.className = 'vc-levels-disabled-note section-hint-text';
|
|
921
|
+
note.textContent = 'Level selection does not apply when all reviewers are external tools';
|
|
922
|
+
togglesContainer.appendChild(note);
|
|
923
|
+
} else if (!allExecutable && note) {
|
|
924
|
+
note.remove();
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
782
928
|
/**
|
|
783
929
|
* Mount all TimeoutSelect instances on the panel.
|
|
784
930
|
* Called after HTML is injected into the DOM.
|
|
@@ -808,10 +954,11 @@ class VoiceCentricConfigTab {
|
|
|
808
954
|
// --- Config read/write ---
|
|
809
955
|
|
|
810
956
|
_defaultConfig() {
|
|
957
|
+
const defaultTimeout = this._getProviderDefaultTimeout(this._defaultProvider);
|
|
811
958
|
return {
|
|
812
|
-
voices: [{ provider: this._defaultProvider, model: this._defaultModel, tier: 'balanced', timeout:
|
|
959
|
+
voices: [{ provider: this._defaultProvider, model: this._defaultModel, tier: 'balanced', timeout: defaultTimeout }],
|
|
813
960
|
enabledLevels: [1, 2, 3],
|
|
814
|
-
orchestration: { provider: this._defaultProvider, model: this._defaultModel, tier: 'balanced', timeout:
|
|
961
|
+
orchestration: { provider: this._defaultProvider, model: this._defaultModel, tier: 'balanced', timeout: defaultTimeout }
|
|
815
962
|
};
|
|
816
963
|
}
|
|
817
964
|
|
|
@@ -836,7 +983,9 @@ class VoiceCentricConfigTab {
|
|
|
836
983
|
const timeout = timeoutSelect ? parseInt(timeoutSelect.value, 10) : VoiceCentricConfigTab.DEFAULT_TIMEOUT;
|
|
837
984
|
const idx = wrapper.dataset.index;
|
|
838
985
|
const instrInput = wrapper.querySelector(`.voice-instructions-input[data-index="${idx}"]`);
|
|
839
|
-
const
|
|
986
|
+
const providerInfo = this.providers[provider];
|
|
987
|
+
const supportsCustomInstructions = providerInfo?.capabilities?.custom_instructions !== false;
|
|
988
|
+
const customInstructions = supportsCustomInstructions ? (instrInput?.value?.trim() || undefined) : undefined;
|
|
840
989
|
|
|
841
990
|
if (provider && model) {
|
|
842
991
|
const voice = { provider, model, tier, timeout };
|
|
@@ -939,7 +1088,7 @@ class VoiceCentricConfigTab {
|
|
|
939
1088
|
list.innerHTML = '';
|
|
940
1089
|
const voices = vcConfig.voices || [];
|
|
941
1090
|
if (voices.length === 0) {
|
|
942
|
-
voices.push({ provider: this._defaultProvider, model: this._defaultModel, tier: 'balanced', timeout:
|
|
1091
|
+
voices.push({ provider: this._defaultProvider, model: this._defaultModel, tier: 'balanced', timeout: this._getProviderDefaultTimeout(this._defaultProvider) });
|
|
943
1092
|
}
|
|
944
1093
|
voices.forEach((voice, i) => {
|
|
945
1094
|
const wrapper = document.createElement('div');
|
|
@@ -954,7 +1103,9 @@ class VoiceCentricConfigTab {
|
|
|
954
1103
|
if (providerSelect) {
|
|
955
1104
|
this._populateProviderDropdown(providerSelect);
|
|
956
1105
|
providerSelect.value = voice.provider;
|
|
1106
|
+
providerSelect.dataset.previousProvider = voice.provider;
|
|
957
1107
|
this._updateModelDropdown(providerSelect);
|
|
1108
|
+
this._updateExecutableState(providerSelect);
|
|
958
1109
|
const modelSelect = row.querySelector('.voice-model');
|
|
959
1110
|
if (modelSelect) modelSelect.value = voice.model;
|
|
960
1111
|
const tierSelect = row.querySelector('.voice-tier');
|
|
@@ -965,13 +1116,17 @@ class VoiceCentricConfigTab {
|
|
|
965
1116
|
TimeoutSelect.mount(mount, { className: 'vc-timeout', title: 'Per-reviewer timeout' });
|
|
966
1117
|
}
|
|
967
1118
|
const timeoutEl = row.querySelector('.vc-timeout');
|
|
1119
|
+
const providerDefaultTimeout = this._getProviderDefaultTimeout(voice.provider);
|
|
968
1120
|
if (timeoutEl && voice.timeout) {
|
|
969
1121
|
timeoutEl.value = String(voice.timeout);
|
|
970
|
-
// Show the dropdown if non-default
|
|
971
|
-
if (voice.timeout !==
|
|
1122
|
+
// Show the dropdown if non-default for this provider
|
|
1123
|
+
if (voice.timeout !== providerDefaultTimeout) {
|
|
972
1124
|
timeoutEl.style.display = '';
|
|
973
1125
|
}
|
|
974
1126
|
this._updateTimeoutIcon(panel, String(i), String(voice.timeout));
|
|
1127
|
+
} else if (timeoutEl) {
|
|
1128
|
+
// No saved timeout — apply the provider's default
|
|
1129
|
+
timeoutEl.value = String(providerDefaultTimeout);
|
|
975
1130
|
}
|
|
976
1131
|
}
|
|
977
1132
|
|
|
@@ -985,6 +1140,7 @@ class VoiceCentricConfigTab {
|
|
|
985
1140
|
});
|
|
986
1141
|
|
|
987
1142
|
this._updateRemoveButtonVisibility();
|
|
1143
|
+
this._updateLevelToggleState();
|
|
988
1144
|
}
|
|
989
1145
|
|
|
990
1146
|
// Apply level toggles
|
|
@@ -1002,6 +1158,7 @@ class VoiceCentricConfigTab {
|
|
|
1002
1158
|
if (providerSelect) {
|
|
1003
1159
|
this._populateProviderDropdown(providerSelect);
|
|
1004
1160
|
providerSelect.value = vcConfig.orchestration.provider;
|
|
1161
|
+
providerSelect.dataset.previousProvider = vcConfig.orchestration.provider;
|
|
1005
1162
|
this._updateModelDropdown(providerSelect);
|
|
1006
1163
|
const modelSelect = orchRow.querySelector('.voice-model');
|
|
1007
1164
|
if (modelSelect) modelSelect.value = vcConfig.orchestration.model;
|
|
@@ -1012,13 +1169,18 @@ class VoiceCentricConfigTab {
|
|
|
1012
1169
|
|
|
1013
1170
|
// Restore orchestration timeout
|
|
1014
1171
|
const orchTimeoutSelect = panel.querySelector('#vc-orchestration-timeout');
|
|
1172
|
+
const orchProviderDefaultTimeout = this._getProviderDefaultTimeout(vcConfig.orchestration.provider);
|
|
1015
1173
|
if (orchTimeoutSelect && vcConfig.orchestration.timeout) {
|
|
1016
1174
|
orchTimeoutSelect.value = String(vcConfig.orchestration.timeout);
|
|
1017
|
-
// Show the dropdown if non-default
|
|
1018
|
-
if (vcConfig.orchestration.timeout !==
|
|
1175
|
+
// Show the dropdown if non-default for this provider
|
|
1176
|
+
if (vcConfig.orchestration.timeout !== orchProviderDefaultTimeout) {
|
|
1019
1177
|
orchTimeoutSelect.style.display = '';
|
|
1020
1178
|
}
|
|
1021
1179
|
this._updateOrchestrationTimeoutIcon(panel, String(vcConfig.orchestration.timeout));
|
|
1180
|
+
} else if (orchTimeoutSelect) {
|
|
1181
|
+
// No saved timeout — apply the provider's default
|
|
1182
|
+
orchTimeoutSelect.value = String(orchProviderDefaultTimeout);
|
|
1183
|
+
this._updateOrchestrationTimeoutIcon(panel, String(orchProviderDefaultTimeout));
|
|
1022
1184
|
}
|
|
1023
1185
|
|
|
1024
1186
|
// Restore orchestration custom instructions
|
|
@@ -1245,6 +1407,11 @@ class VoiceCentricConfigTab {
|
|
|
1245
1407
|
}
|
|
1246
1408
|
this._markClean();
|
|
1247
1409
|
await this.loadCouncils();
|
|
1410
|
+
const selector = this.modal.querySelector('#vc-council-selector');
|
|
1411
|
+
if (selector) {
|
|
1412
|
+
selector.value = this.selectedCouncilId;
|
|
1413
|
+
selector.classList.remove('new-council-selected');
|
|
1414
|
+
}
|
|
1248
1415
|
}
|
|
1249
1416
|
|
|
1250
1417
|
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
|
})();
|