@in-the-loop-labs/pair-review 2.3.2 → 2.4.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/.pi/skills/review-model-guidance/SKILL.md +1 -1
- package/.pi/skills/review-roulette/SKILL.md +1 -1
- package/README.md +15 -1
- package/package.json +2 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/review-requests/SKILL.md +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/public/css/pr.css +296 -15
- package/public/index.html +121 -57
- package/public/js/components/AIPanel.js +2 -1
- package/public/js/components/AdvancedConfigTab.js +2 -2
- package/public/js/components/AnalysisConfigModal.js +2 -2
- package/public/js/components/ChatPanel.js +187 -28
- package/public/js/components/CouncilProgressModal.js +4 -7
- package/public/js/components/SplitButton.js +66 -1
- package/public/js/components/VoiceCentricConfigTab.js +2 -2
- package/public/js/index.js +274 -21
- package/public/js/modules/comment-manager.js +16 -12
- package/public/js/modules/file-comment-manager.js +8 -6
- package/public/js/pr.js +194 -5
- package/public/local.html +8 -1
- package/public/pr.html +17 -2
- package/src/ai/codex-provider.js +14 -2
- package/src/ai/copilot-provider.js +1 -10
- package/src/ai/cursor-agent-provider.js +1 -10
- package/src/ai/gemini-provider.js +8 -17
- package/src/chat/acp-bridge.js +442 -0
- package/src/chat/api-reference.js +539 -0
- package/src/chat/chat-providers.js +290 -0
- package/src/chat/claude-code-bridge.js +499 -0
- package/src/chat/codex-bridge.js +601 -0
- package/src/chat/pi-bridge.js +56 -3
- package/src/chat/prompt-builder.js +12 -11
- package/src/chat/session-manager.js +110 -29
- package/src/config.js +4 -2
- package/src/database.js +50 -2
- package/src/github/client.js +43 -0
- package/src/routes/chat.js +60 -27
- package/src/routes/config.js +24 -1
- package/src/routes/github-collections.js +126 -0
- package/src/routes/mcp.js +2 -1
- package/src/routes/pr.js +166 -2
- package/src/routes/reviews.js +2 -1
- package/src/routes/shared.js +70 -49
- package/src/server.js +27 -1
- package/src/utils/safe-parse-json.js +19 -0
- package/.pi/skills/pair-review-api/SKILL.md +0 -448
package/public/index.html
CHANGED
|
@@ -542,55 +542,6 @@
|
|
|
542
542
|
to { transform: rotate(360deg); }
|
|
543
543
|
}
|
|
544
544
|
|
|
545
|
-
/* Usage Info (shown when no reviews) */
|
|
546
|
-
.usage-info {
|
|
547
|
-
background-color: var(--color-bg-primary);
|
|
548
|
-
border: 1px solid var(--color-border-primary);
|
|
549
|
-
border-radius: var(--radius-lg);
|
|
550
|
-
padding: 24px;
|
|
551
|
-
text-align: left;
|
|
552
|
-
max-width: 600px;
|
|
553
|
-
width: 100%;
|
|
554
|
-
margin-bottom: 32px;
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
.usage-info h3 {
|
|
558
|
-
font-size: 16px;
|
|
559
|
-
font-weight: 600;
|
|
560
|
-
margin-bottom: 16px;
|
|
561
|
-
color: var(--color-text-primary);
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
.usage-info p {
|
|
565
|
-
color: var(--color-text-secondary);
|
|
566
|
-
margin-bottom: 12px;
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
.usage-info code {
|
|
570
|
-
background-color: var(--color-bg-secondary);
|
|
571
|
-
padding: 2px 6px;
|
|
572
|
-
border-radius: var(--radius-sm);
|
|
573
|
-
font-family: var(--font-mono);
|
|
574
|
-
font-size: 13px;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
.usage-info pre {
|
|
578
|
-
background-color: var(--color-bg-tertiary);
|
|
579
|
-
color: var(--color-text-primary);
|
|
580
|
-
padding: 16px;
|
|
581
|
-
border-radius: var(--radius-md);
|
|
582
|
-
margin: 12px 0;
|
|
583
|
-
overflow-x: auto;
|
|
584
|
-
font-family: var(--font-mono);
|
|
585
|
-
font-size: 13px;
|
|
586
|
-
line-height: 1.5;
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
.usage-info pre code {
|
|
590
|
-
background: none;
|
|
591
|
-
padding: 0;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
545
|
/* Recent Reviews Section */
|
|
595
546
|
.recent-reviews-section {
|
|
596
547
|
width: 100%;
|
|
@@ -754,6 +705,28 @@
|
|
|
754
705
|
color: var(--color-text-primary);
|
|
755
706
|
}
|
|
756
707
|
|
|
708
|
+
.btn-github-link {
|
|
709
|
+
display: inline-flex;
|
|
710
|
+
align-items: center;
|
|
711
|
+
justify-content: center;
|
|
712
|
+
width: 32px;
|
|
713
|
+
height: 32px;
|
|
714
|
+
padding: 0;
|
|
715
|
+
background: transparent;
|
|
716
|
+
border: 1px solid transparent;
|
|
717
|
+
border-radius: var(--radius-md);
|
|
718
|
+
color: var(--color-text-tertiary);
|
|
719
|
+
cursor: pointer;
|
|
720
|
+
transition: all var(--transition-fast);
|
|
721
|
+
text-decoration: none;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
.btn-github-link:hover {
|
|
725
|
+
background-color: var(--color-bg-secondary);
|
|
726
|
+
border-color: var(--color-border-primary);
|
|
727
|
+
color: var(--color-text-primary);
|
|
728
|
+
}
|
|
729
|
+
|
|
757
730
|
.recent-reviews-empty {
|
|
758
731
|
text-align: center;
|
|
759
732
|
padding: 40px 20px;
|
|
@@ -870,6 +843,76 @@
|
|
|
870
843
|
display: block;
|
|
871
844
|
}
|
|
872
845
|
|
|
846
|
+
.tab-divider {
|
|
847
|
+
width: 1px;
|
|
848
|
+
height: 16px;
|
|
849
|
+
background: var(--color-border-secondary);
|
|
850
|
+
align-self: center;
|
|
851
|
+
margin: 0 4px;
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
.tab-pane-header {
|
|
855
|
+
display: flex;
|
|
856
|
+
justify-content: flex-end;
|
|
857
|
+
align-items: center;
|
|
858
|
+
gap: 8px;
|
|
859
|
+
margin-bottom: 12px;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
.fetched-at-label {
|
|
863
|
+
font-size: 12px;
|
|
864
|
+
color: var(--color-fg-muted);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
.btn-refresh {
|
|
868
|
+
display: inline-flex;
|
|
869
|
+
align-items: center;
|
|
870
|
+
justify-content: center;
|
|
871
|
+
width: 32px;
|
|
872
|
+
height: 32px;
|
|
873
|
+
padding: 0;
|
|
874
|
+
background: transparent;
|
|
875
|
+
border: 1px solid var(--color-border-primary);
|
|
876
|
+
border-radius: var(--radius-md);
|
|
877
|
+
color: var(--color-text-tertiary);
|
|
878
|
+
cursor: pointer;
|
|
879
|
+
transition: all var(--transition-fast);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
.btn-refresh:hover {
|
|
883
|
+
background: var(--color-bg-secondary);
|
|
884
|
+
color: var(--color-text-primary);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
.btn-refresh.refreshing {
|
|
888
|
+
pointer-events: none;
|
|
889
|
+
opacity: 0.6;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
.btn-refresh.refreshing svg {
|
|
893
|
+
animation: spin 0.8s linear infinite;
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
.collection-pr-row {
|
|
897
|
+
cursor: pointer;
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
.collection-pr-row:hover {
|
|
901
|
+
background: var(--color-bg-secondary);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
.collection-pr-number {
|
|
905
|
+
color: var(--ai-primary);
|
|
906
|
+
font-weight: 500;
|
|
907
|
+
font-family: var(--font-mono);
|
|
908
|
+
cursor: pointer;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
.collection-pr-number:hover {
|
|
912
|
+
color: var(--ai-secondary);
|
|
913
|
+
text-decoration: underline;
|
|
914
|
+
}
|
|
915
|
+
|
|
873
916
|
/* Local Reviews Table - tighter spacing */
|
|
874
917
|
.recent-reviews-table.local-table th,
|
|
875
918
|
.recent-reviews-table.local-table td {
|
|
@@ -1021,10 +1064,6 @@
|
|
|
1021
1064
|
width: 100%;
|
|
1022
1065
|
}
|
|
1023
1066
|
|
|
1024
|
-
.usage-info {
|
|
1025
|
-
padding: 20px;
|
|
1026
|
-
}
|
|
1027
|
-
|
|
1028
1067
|
.recent-reviews-table {
|
|
1029
1068
|
font-size: 13px;
|
|
1030
1069
|
}
|
|
@@ -1073,6 +1112,9 @@
|
|
|
1073
1112
|
<div class="section-header" id="recent-reviews-header">
|
|
1074
1113
|
<div class="tab-bar" id="unified-tab-bar">
|
|
1075
1114
|
<button class="tab-btn active" data-tab="pr-tab" type="button">Pull Requests</button>
|
|
1115
|
+
<button class="tab-btn" data-tab="review-requests-tab" type="button">My Review Requests</button>
|
|
1116
|
+
<button class="tab-btn" data-tab="my-prs-tab" type="button">My PRs</button>
|
|
1117
|
+
<span class="tab-divider"></span>
|
|
1076
1118
|
<button class="tab-btn" data-tab="local-tab" type="button">Local Reviews</button>
|
|
1077
1119
|
</div>
|
|
1078
1120
|
</div>
|
|
@@ -1104,6 +1146,32 @@
|
|
|
1104
1146
|
</div>
|
|
1105
1147
|
</div>
|
|
1106
1148
|
|
|
1149
|
+
<!-- My Review Requests Tab -->
|
|
1150
|
+
<div class="tab-pane" id="review-requests-tab">
|
|
1151
|
+
<div class="tab-pane-header">
|
|
1152
|
+
<span class="fetched-at-label" id="review-requests-fetched-at"></span>
|
|
1153
|
+
<button class="btn-refresh" id="refresh-review-requests" title="Refresh from GitHub">
|
|
1154
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M1.705 8.005a.75.75 0 0 1 .834.656 5.5 5.5 0 0 0 9.592 2.97l-1.204-1.204a.25.25 0 0 1 .177-.427h3.646a.25.25 0 0 1 .25.25v3.646a.25.25 0 0 1-.427.177l-1.38-1.38A7.002 7.002 0 0 1 1.05 8.84a.75.75 0 0 1 .656-.834ZM8 2.5a5.487 5.487 0 0 0-4.131 1.869l1.204 1.204A.25.25 0 0 1 4.896 6H1.25A.25.25 0 0 1 1 5.75V2.104a.25.25 0 0 1 .427-.177l1.38 1.38A7.002 7.002 0 0 1 14.95 7.16a.75.75 0 0 1-1.49.178A5.5 5.5 0 0 0 8 2.5Z"/></svg>
|
|
1155
|
+
</button>
|
|
1156
|
+
</div>
|
|
1157
|
+
<div id="review-requests-container" class="recent-reviews-loading">
|
|
1158
|
+
Loading...
|
|
1159
|
+
</div>
|
|
1160
|
+
</div>
|
|
1161
|
+
|
|
1162
|
+
<!-- My PRs Tab -->
|
|
1163
|
+
<div class="tab-pane" id="my-prs-tab">
|
|
1164
|
+
<div class="tab-pane-header">
|
|
1165
|
+
<span class="fetched-at-label" id="my-prs-fetched-at"></span>
|
|
1166
|
+
<button class="btn-refresh" id="refresh-my-prs" title="Refresh from GitHub">
|
|
1167
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M1.705 8.005a.75.75 0 0 1 .834.656 5.5 5.5 0 0 0 9.592 2.97l-1.204-1.204a.25.25 0 0 1 .177-.427h3.646a.25.25 0 0 1 .25.25v3.646a.25.25 0 0 1-.427.177l-1.38-1.38A7.002 7.002 0 0 1 1.05 8.84a.75.75 0 0 1 .656-.834ZM8 2.5a5.487 5.487 0 0 0-4.131 1.869l1.204 1.204A.25.25 0 0 1 4.896 6H1.25A.25.25 0 0 1 1 5.75V2.104a.25.25 0 0 1 .427-.177l1.38 1.38A7.002 7.002 0 0 1 14.95 7.16a.75.75 0 0 1-1.49.178A5.5 5.5 0 0 0 8 2.5Z"/></svg>
|
|
1168
|
+
</button>
|
|
1169
|
+
</div>
|
|
1170
|
+
<div id="my-prs-container" class="recent-reviews-loading">
|
|
1171
|
+
Loading...
|
|
1172
|
+
</div>
|
|
1173
|
+
</div>
|
|
1174
|
+
|
|
1107
1175
|
<!-- Local Reviews Tab: Input + Listing -->
|
|
1108
1176
|
<div class="tab-pane" id="local-tab">
|
|
1109
1177
|
<div class="start-review-section">
|
|
@@ -1134,10 +1202,6 @@
|
|
|
1134
1202
|
</div>
|
|
1135
1203
|
</div>
|
|
1136
1204
|
</div>
|
|
1137
|
-
|
|
1138
|
-
<!-- Usage Info (shown when no reviews, hidden initially during loading) -->
|
|
1139
|
-
<!-- Content is populated from help modal via JS to avoid duplication -->
|
|
1140
|
-
<div class="usage-info loading-hidden" id="usage-info"></div>
|
|
1141
1205
|
</div>
|
|
1142
1206
|
</main>
|
|
1143
1207
|
</div>
|
|
@@ -1432,7 +1432,8 @@ class AIPanel {
|
|
|
1432
1432
|
*/
|
|
1433
1433
|
clearAllFindings() {
|
|
1434
1434
|
this.findings = [];
|
|
1435
|
-
this.comments
|
|
1435
|
+
// NOTE: Do NOT clear this.comments here. User comments are independent
|
|
1436
|
+
// of AI analysis and must persist across analysis runs.
|
|
1436
1437
|
this.currentIndex = -1; // Reset navigation
|
|
1437
1438
|
this.updateSegmentCounts();
|
|
1438
1439
|
this.renderFindings();
|
|
@@ -708,7 +708,7 @@ class AdvancedConfigTab {
|
|
|
708
708
|
const providerIds = Object.keys(this.providers).filter(id => {
|
|
709
709
|
const p = this.providers[id];
|
|
710
710
|
return !p.availability || p.availability.available;
|
|
711
|
-
});
|
|
711
|
+
}).sort((a, b) => (this.providers[a].name || a).localeCompare(this.providers[b].name || b));
|
|
712
712
|
|
|
713
713
|
for (const id of providerIds) {
|
|
714
714
|
const opt = document.createElement('option');
|
|
@@ -737,7 +737,7 @@ class AdvancedConfigTab {
|
|
|
737
737
|
if (!modelSelect) return;
|
|
738
738
|
|
|
739
739
|
const currentModel = modelSelect.value;
|
|
740
|
-
const models = provider.models || [];
|
|
740
|
+
const models = [...(provider.models || [])].sort((a, b) => (a.name || a.id).localeCompare(b.name || b.id));
|
|
741
741
|
modelSelect.innerHTML = '';
|
|
742
742
|
for (const model of models) {
|
|
743
743
|
const opt = document.createElement('option');
|
|
@@ -452,12 +452,12 @@ class AnalysisConfigModal {
|
|
|
452
452
|
const container = this.modal.querySelector('#provider-toggle-container');
|
|
453
453
|
if (!container) return;
|
|
454
454
|
|
|
455
|
-
// Filter to only show available providers
|
|
455
|
+
// Filter to only show available providers, sorted alphabetically by name
|
|
456
456
|
const availableProviderIds = Object.keys(this.providers).filter(providerId => {
|
|
457
457
|
const provider = this.providers[providerId];
|
|
458
458
|
// Show provider if no availability info (check pending) or if explicitly available
|
|
459
459
|
return !provider.availability || provider.availability.available;
|
|
460
|
-
});
|
|
460
|
+
}).sort((a, b) => (this.providers[a].name || a).localeCompare(this.providers[b].name || b));
|
|
461
461
|
|
|
462
462
|
// If selected provider is no longer available, select first available
|
|
463
463
|
if (availableProviderIds.length > 0 && !availableProviderIds.includes(this.selectedProvider)) {
|
|
@@ -32,10 +32,13 @@ class ChatPanel {
|
|
|
32
32
|
this._analysisContextRemoved = false;
|
|
33
33
|
this._sessionAnalysisRunId = null; // tracks which AI run ID's context is loaded in the current session
|
|
34
34
|
this._openPromise = null; // concurrency guard for open()
|
|
35
|
+
this._activeProvider = window.__pairReview?.chatProvider || 'pi';
|
|
36
|
+
this._chatProviders = window.__pairReview?.chatProviders || [];
|
|
35
37
|
|
|
36
38
|
this._render();
|
|
37
39
|
this._bindEvents();
|
|
38
40
|
this._initContextTooltip();
|
|
41
|
+
this._updateTitle();
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
/**
|
|
@@ -48,20 +51,27 @@ class ChatPanel {
|
|
|
48
51
|
<div id="chat-panel" class="chat-panel chat-panel--closed">
|
|
49
52
|
<div class="chat-panel__resize-handle" title="Drag to resize"></div>
|
|
50
53
|
<div class="chat-panel__header">
|
|
51
|
-
<div class="chat-
|
|
52
|
-
<button class="chat-
|
|
54
|
+
<div class="chat-panel__provider-picker">
|
|
55
|
+
<button class="chat-panel__provider-picker-btn" title="Switch provider">
|
|
53
56
|
<svg viewBox="0 0 16 16" fill="currentColor" width="14" height="14">
|
|
54
57
|
<path d="M1.75 1h8.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 10.25 10H7.061l-2.574 2.573A1.458 1.458 0 0 1 2 11.543V10h-.25A1.75 1.75 0 0 1 0 8.25v-5.5C0 1.784.784 1 1.75 1ZM1.5 2.75v5.5c0 .138.112.25.25.25h1a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h3.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25Zm13 2a.25.25 0 0 0-.25-.25h-.5a.75.75 0 0 1 0-1.5h.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 14.25 12H14v1.543a1.458 1.458 0 0 1-2.487 1.03L9.22 12.28a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215l2.22 2.22v-2.19a.75.75 0 0 1 .75-.75h1a.25.25 0 0 0 .25-.25Z"/>
|
|
55
58
|
</svg>
|
|
56
59
|
<span class="chat-panel__title-text">Chat · Pi</span>
|
|
57
|
-
<
|
|
58
|
-
|
|
59
|
-
<path d="m.427 1.927 1.215 1.215a8.002 8.002 0 1 1-1.6 5.685.75.75 0 1 1 1.493-.154 6.5 6.5 0 1 0 1.18-4.458l1.358 1.358A.25.25 0 0 1 3.896 6H.25A.25.25 0 0 1 0 5.75V2.104a.25.25 0 0 1 .427-.177ZM7.75 4a.75.75 0 0 1 .75.75v2.992l2.028.812a.75.75 0 0 1-.557 1.392l-2.5-1A.751.751 0 0 1 7 8.25v-3.5A.75.75 0 0 1 7.75 4Z"/>
|
|
60
|
+
<svg class="chat-panel__provider-chevron" viewBox="0 0 16 16" fill="currentColor" width="10" height="10">
|
|
61
|
+
<path d="M4.427 7.427l3.396 3.396a.25.25 0 0 0 .354 0l3.396-3.396A.25.25 0 0 0 11.396 7H4.604a.25.25 0 0 0-.177.427Z"/>
|
|
60
62
|
</svg>
|
|
61
63
|
</button>
|
|
64
|
+
<div class="chat-panel__provider-dropdown" style="display: none;"></div>
|
|
65
|
+
</div>
|
|
66
|
+
<div class="chat-panel__session-picker">
|
|
62
67
|
<div class="chat-panel__session-dropdown" style="display: none;"></div>
|
|
63
68
|
</div>
|
|
64
69
|
<div class="chat-panel__actions">
|
|
70
|
+
<button class="chat-panel__history-btn" title="Session history">
|
|
71
|
+
<svg viewBox="0 0 16 16" fill="currentColor" width="14" height="14">
|
|
72
|
+
<path d="m.427 1.927 1.215 1.215a8.002 8.002 0 1 1-1.6 5.685.75.75 0 1 1 1.493-.154 6.5 6.5 0 1 0 1.18-4.458l1.358 1.358A.25.25 0 0 1 3.896 6H.25A.25.25 0 0 1 0 5.75V2.104a.25.25 0 0 1 .427-.177ZM7.75 4a.75.75 0 0 1 .75.75v2.992l2.028.812a.75.75 0 0 1-.557 1.392l-2.5-1A.751.751 0 0 1 7 8.25v-3.5A.75.75 0 0 1 7.75 4Z"/>
|
|
73
|
+
</svg>
|
|
74
|
+
</button>
|
|
65
75
|
<button class="chat-panel__new-btn" title="New conversation">
|
|
66
76
|
<svg viewBox="0 0 16 16" fill="currentColor" width="14" height="14">
|
|
67
77
|
<path d="M7.75 2a.75.75 0 0 1 .75.75V7h4.25a.75.75 0 0 1 0 1.5H8.5v4.25a.75.75 0 0 1-1.5 0V8.5H2.75a.75.75 0 0 1 0-1.5H7V2.75A.75.75 0 0 1 7.75 2Z"/>
|
|
@@ -152,9 +162,12 @@ class ChatPanel {
|
|
|
152
162
|
this.dismissCommentBtn = this.container.querySelector('.chat-panel__action-btn--dismiss-comment');
|
|
153
163
|
this.createCommentBtn = this.container.querySelector('.chat-panel__action-btn--create-comment');
|
|
154
164
|
this.actionBarDismissBtn = this.container.querySelector('.chat-panel__action-bar-dismiss');
|
|
165
|
+
this.providerPickerEl = this.container.querySelector('.chat-panel__provider-picker');
|
|
166
|
+
this.providerPickerBtn = this.container.querySelector('.chat-panel__provider-picker-btn');
|
|
167
|
+
this.providerDropdown = this.container.querySelector('.chat-panel__provider-dropdown');
|
|
155
168
|
this.sessionPickerEl = this.container.querySelector('.chat-panel__session-picker');
|
|
156
|
-
this.sessionPickerBtn = this.container.querySelector('.chat-panel__session-picker-btn');
|
|
157
169
|
this.sessionDropdown = this.container.querySelector('.chat-panel__session-dropdown');
|
|
170
|
+
this.historyBtn = this.container.querySelector('.chat-panel__history-btn');
|
|
158
171
|
this.titleTextEl = this.container.querySelector('.chat-panel__title-text');
|
|
159
172
|
this.newContentPill = this.container.querySelector('.chat-panel__new-content-pill');
|
|
160
173
|
}
|
|
@@ -171,8 +184,11 @@ class ChatPanel {
|
|
|
171
184
|
// New conversation button
|
|
172
185
|
this.newBtn.addEventListener('click', () => this._startNewConversation());
|
|
173
186
|
|
|
174
|
-
//
|
|
175
|
-
this.
|
|
187
|
+
// Provider picker button
|
|
188
|
+
this.providerPickerBtn.addEventListener('click', () => this._toggleProviderDropdown());
|
|
189
|
+
|
|
190
|
+
// Session history button
|
|
191
|
+
this.historyBtn.addEventListener('click', () => this._toggleSessionDropdown());
|
|
176
192
|
|
|
177
193
|
// Send button
|
|
178
194
|
this.sendBtn.addEventListener('click', () => this.sendMessage());
|
|
@@ -232,7 +248,9 @@ class ChatPanel {
|
|
|
232
248
|
// Escape: close dropdown if open, stop agent if streaming, blur textarea if focused, otherwise close panel
|
|
233
249
|
this._onKeydown = (e) => {
|
|
234
250
|
if (e.key === 'Escape' && this.isOpen) {
|
|
235
|
-
if (this.
|
|
251
|
+
if (this._isProviderDropdownOpen()) {
|
|
252
|
+
this._hideProviderDropdown();
|
|
253
|
+
} else if (this._isSessionDropdownOpen()) {
|
|
236
254
|
this._hideSessionDropdown();
|
|
237
255
|
} else if (this.isStreaming) {
|
|
238
256
|
this._stopAgent();
|
|
@@ -254,6 +272,13 @@ class ChatPanel {
|
|
|
254
272
|
}
|
|
255
273
|
});
|
|
256
274
|
|
|
275
|
+
// Re-read chat providers once the <head> config fetch resolves
|
|
276
|
+
this._onChatStateChanged = () => {
|
|
277
|
+
this._chatProviders = window.__pairReview?.chatProviders || [];
|
|
278
|
+
this._updateTitle();
|
|
279
|
+
};
|
|
280
|
+
window.addEventListener('chat-state-changed', this._onChatStateChanged);
|
|
281
|
+
|
|
257
282
|
this._bindResizeEvents();
|
|
258
283
|
}
|
|
259
284
|
|
|
@@ -356,19 +381,32 @@ class ChatPanel {
|
|
|
356
381
|
|
|
357
382
|
/**
|
|
358
383
|
* Update the chat panel title with provider and model info.
|
|
359
|
-
* @param {string} [
|
|
384
|
+
* @param {string} [providerId] - Provider ID (looked up in _chatProviders for display name)
|
|
360
385
|
* @param {string} [model] - Model ID or display name (e.g. 'default', 'multi-model')
|
|
361
386
|
*/
|
|
362
|
-
_updateTitle(
|
|
387
|
+
_updateTitle(providerId, model) {
|
|
363
388
|
if (!this.titleTextEl) return;
|
|
389
|
+
const providerName = this._getProviderDisplayName(providerId || this._activeProvider);
|
|
364
390
|
const modelDisplay = model
|
|
365
391
|
? model.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')
|
|
366
392
|
: null;
|
|
367
|
-
const parts = ['Chat',
|
|
393
|
+
const parts = ['Chat', providerName];
|
|
368
394
|
if (modelDisplay) parts.push(modelDisplay);
|
|
369
395
|
this.titleTextEl.textContent = parts.join(' \u00b7 ');
|
|
370
396
|
}
|
|
371
397
|
|
|
398
|
+
/**
|
|
399
|
+
* Get display name for a provider ID from the _chatProviders array.
|
|
400
|
+
* Falls back to capitalized provider ID if not found.
|
|
401
|
+
* @param {string} providerId
|
|
402
|
+
* @returns {string}
|
|
403
|
+
*/
|
|
404
|
+
_getProviderDisplayName(providerId) {
|
|
405
|
+
const entry = this._chatProviders.find(p => p.id === providerId);
|
|
406
|
+
if (entry) return entry.name;
|
|
407
|
+
return providerId.charAt(0).toUpperCase() + providerId.slice(1);
|
|
408
|
+
}
|
|
409
|
+
|
|
372
410
|
/**
|
|
373
411
|
* Open the chat panel
|
|
374
412
|
* @param {Object} options - Optional context
|
|
@@ -493,6 +531,7 @@ class ChatPanel {
|
|
|
493
531
|
* Close the chat panel
|
|
494
532
|
*/
|
|
495
533
|
close() {
|
|
534
|
+
this._hideProviderDropdown();
|
|
496
535
|
this._hideSessionDropdown();
|
|
497
536
|
// Reset UI streaming state (buttons) but keep isStreaming and _streamingContent
|
|
498
537
|
// intact so the background WebSocket handler can continue accumulating events.
|
|
@@ -533,6 +572,7 @@ class ChatPanel {
|
|
|
533
572
|
* Preserves any unsent pending context cards and re-adds them to the new conversation.
|
|
534
573
|
*/
|
|
535
574
|
async _startNewConversation() {
|
|
575
|
+
this._hideProviderDropdown();
|
|
536
576
|
this._hideSessionDropdown();
|
|
537
577
|
// 1. Snapshot pending context before clearing (these are unsent context cards)
|
|
538
578
|
const savedContext = this._pendingContext.slice();
|
|
@@ -630,8 +670,8 @@ class ChatPanel {
|
|
|
630
670
|
console.debug('[ChatPanel] Loaded MRU session:', mru.id, 'messages:', mru.message_count);
|
|
631
671
|
|
|
632
672
|
if (mru.provider) {
|
|
633
|
-
|
|
634
|
-
this.
|
|
673
|
+
this._updateTitle(mru.provider, mru.model);
|
|
674
|
+
this._activeProvider = mru.provider;
|
|
635
675
|
}
|
|
636
676
|
|
|
637
677
|
if (mru.message_count > 0) {
|
|
@@ -688,6 +728,108 @@ class ChatPanel {
|
|
|
688
728
|
}
|
|
689
729
|
}
|
|
690
730
|
|
|
731
|
+
// ── Provider picker dropdown ──────────────────────────────────────────
|
|
732
|
+
|
|
733
|
+
_isProviderDropdownOpen() {
|
|
734
|
+
return this.providerDropdown && this.providerDropdown.style.display !== 'none';
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
_toggleProviderDropdown() {
|
|
738
|
+
if (this._isProviderDropdownOpen()) {
|
|
739
|
+
this._hideProviderDropdown();
|
|
740
|
+
} else {
|
|
741
|
+
this._showProviderDropdown();
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
_showProviderDropdown() {
|
|
746
|
+
if (!this.providerDropdown) return;
|
|
747
|
+
// Close session dropdown if open
|
|
748
|
+
this._hideSessionDropdown();
|
|
749
|
+
|
|
750
|
+
this._renderProviderDropdown();
|
|
751
|
+
this.providerDropdown.style.display = '';
|
|
752
|
+
this.providerPickerBtn.classList.add('chat-panel__provider-picker-btn--open');
|
|
753
|
+
|
|
754
|
+
// Bind outside-click-to-close (one-shot)
|
|
755
|
+
this._providerOutsideClickHandler = (e) => {
|
|
756
|
+
if (!this.providerPickerEl.contains(e.target)) {
|
|
757
|
+
this._hideProviderDropdown();
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
setTimeout(() => {
|
|
761
|
+
document.addEventListener('click', this._providerOutsideClickHandler);
|
|
762
|
+
}, 0);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
_hideProviderDropdown() {
|
|
766
|
+
if (!this.providerDropdown) return;
|
|
767
|
+
this.providerDropdown.style.display = 'none';
|
|
768
|
+
this.providerPickerBtn.classList.remove('chat-panel__provider-picker-btn--open');
|
|
769
|
+
if (this._providerOutsideClickHandler) {
|
|
770
|
+
document.removeEventListener('click', this._providerOutsideClickHandler);
|
|
771
|
+
this._providerOutsideClickHandler = null;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
_renderProviderDropdown() {
|
|
776
|
+
if (!this.providerDropdown) return;
|
|
777
|
+
const providers = [...this._chatProviders].sort((a, b) =>
|
|
778
|
+
a.name.localeCompare(b.name, undefined, { sensitivity: 'base' })
|
|
779
|
+
);
|
|
780
|
+
|
|
781
|
+
if (providers.length === 0) {
|
|
782
|
+
this.providerDropdown.innerHTML = `
|
|
783
|
+
<div class="chat-panel__provider-empty">No providers configured</div>
|
|
784
|
+
`;
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const items = providers.map(p => {
|
|
789
|
+
const isActive = p.id === this._activeProvider;
|
|
790
|
+
const isUnavailable = !p.available;
|
|
791
|
+
const classes = ['chat-panel__provider-item'];
|
|
792
|
+
if (isActive) classes.push('chat-panel__provider-item--active');
|
|
793
|
+
if (isUnavailable) classes.push('chat-panel__provider-item--unavailable');
|
|
794
|
+
|
|
795
|
+
const checkmark = isActive
|
|
796
|
+
? `<svg class="chat-panel__provider-check" viewBox="0 0 16 16" fill="currentColor" width="12" height="12">
|
|
797
|
+
<path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z"/>
|
|
798
|
+
</svg>`
|
|
799
|
+
: '';
|
|
800
|
+
|
|
801
|
+
return `
|
|
802
|
+
<button class="${classes.join(' ')}"
|
|
803
|
+
data-provider-id="${this._escapeHtml(p.id)}"
|
|
804
|
+
${isUnavailable ? 'disabled' : ''}>
|
|
805
|
+
<span class="chat-panel__provider-name">${this._escapeHtml(p.name)}</span>
|
|
806
|
+
${checkmark}
|
|
807
|
+
</button>
|
|
808
|
+
`;
|
|
809
|
+
}).join('');
|
|
810
|
+
|
|
811
|
+
this.providerDropdown.innerHTML = items;
|
|
812
|
+
|
|
813
|
+
// Bind click handlers
|
|
814
|
+
this.providerDropdown.querySelectorAll('.chat-panel__provider-item:not([disabled])').forEach(btn => {
|
|
815
|
+
btn.addEventListener('click', () => {
|
|
816
|
+
this._selectProvider(btn.dataset.providerId);
|
|
817
|
+
this._hideProviderDropdown();
|
|
818
|
+
});
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* Select a provider. Updates active provider and title. Only affects new sessions.
|
|
824
|
+
* @param {string} id - Provider ID to activate
|
|
825
|
+
*/
|
|
826
|
+
_selectProvider(id) {
|
|
827
|
+
if (id === this._activeProvider) return;
|
|
828
|
+
this._activeProvider = id;
|
|
829
|
+
this._updateTitle();
|
|
830
|
+
this._startNewConversation();
|
|
831
|
+
}
|
|
832
|
+
|
|
691
833
|
// ── Session picker dropdown ────────────────────────────────────────────
|
|
692
834
|
|
|
693
835
|
_isSessionDropdownOpen() {
|
|
@@ -704,31 +846,43 @@ class ChatPanel {
|
|
|
704
846
|
|
|
705
847
|
async _showSessionDropdown() {
|
|
706
848
|
if (!this.sessionDropdown) return;
|
|
849
|
+
// Close provider dropdown if open
|
|
850
|
+
this._hideProviderDropdown();
|
|
707
851
|
|
|
708
852
|
const sessions = await this._fetchSessions();
|
|
709
853
|
this._renderSessionDropdown(sessions);
|
|
710
854
|
this.sessionDropdown.style.display = '';
|
|
711
|
-
this.
|
|
855
|
+
this.historyBtn.classList.add('chat-panel__history-btn--open');
|
|
856
|
+
|
|
857
|
+
// Position the fixed dropdown relative to the history button
|
|
858
|
+
this._positionSessionDropdown();
|
|
712
859
|
|
|
713
860
|
// Bind outside-click-to-close (one-shot)
|
|
714
|
-
this.
|
|
715
|
-
if (!this.sessionPickerEl.contains(e.target)) {
|
|
861
|
+
this._sessionOutsideClickHandler = (e) => {
|
|
862
|
+
if (!this.sessionPickerEl.contains(e.target) && !this.historyBtn.contains(e.target)) {
|
|
716
863
|
this._hideSessionDropdown();
|
|
717
864
|
}
|
|
718
865
|
};
|
|
719
866
|
// Use setTimeout so the current click event doesn't immediately trigger close
|
|
720
867
|
setTimeout(() => {
|
|
721
|
-
document.addEventListener('click', this.
|
|
868
|
+
document.addEventListener('click', this._sessionOutsideClickHandler);
|
|
722
869
|
}, 0);
|
|
723
870
|
}
|
|
724
871
|
|
|
872
|
+
_positionSessionDropdown() {
|
|
873
|
+
if (!this.sessionDropdown || !this.historyBtn) return;
|
|
874
|
+
const rect = this.historyBtn.getBoundingClientRect();
|
|
875
|
+
this.sessionDropdown.style.top = `${rect.bottom + 4}px`;
|
|
876
|
+
this.sessionDropdown.style.right = `${window.innerWidth - rect.right}px`;
|
|
877
|
+
}
|
|
878
|
+
|
|
725
879
|
_hideSessionDropdown() {
|
|
726
880
|
if (!this.sessionDropdown) return;
|
|
727
881
|
this.sessionDropdown.style.display = 'none';
|
|
728
|
-
this.
|
|
729
|
-
if (this.
|
|
730
|
-
document.removeEventListener('click', this.
|
|
731
|
-
this.
|
|
882
|
+
this.historyBtn.classList.remove('chat-panel__history-btn--open');
|
|
883
|
+
if (this._sessionOutsideClickHandler) {
|
|
884
|
+
document.removeEventListener('click', this._sessionOutsideClickHandler);
|
|
885
|
+
this._sessionOutsideClickHandler = null;
|
|
732
886
|
}
|
|
733
887
|
}
|
|
734
888
|
|
|
@@ -748,12 +902,15 @@ class ChatPanel {
|
|
|
748
902
|
? this._truncate(s.first_message, 60)
|
|
749
903
|
: 'New conversation';
|
|
750
904
|
const timeAgo = this._formatRelativeTime(s.updated_at);
|
|
905
|
+
const providerLabel = s.provider
|
|
906
|
+
? `<span class="chat-panel__session-provider">${this._escapeHtml(this._getProviderDisplayName(s.provider))}</span>`
|
|
907
|
+
: '';
|
|
751
908
|
|
|
752
909
|
return `
|
|
753
910
|
<button class="chat-panel__session-item${isActive ? ' chat-panel__session-item--active' : ''}"
|
|
754
911
|
data-session-id="${s.id}">
|
|
755
912
|
<span class="chat-panel__session-preview">${this._escapeHtml(preview)}</span>
|
|
756
|
-
<span class="chat-panel__session-meta">${this._escapeHtml(timeAgo)}</span>
|
|
913
|
+
<span class="chat-panel__session-meta">${providerLabel}${this._escapeHtml(timeAgo)}</span>
|
|
757
914
|
</button>
|
|
758
915
|
`;
|
|
759
916
|
}).join('');
|
|
@@ -805,8 +962,8 @@ class ChatPanel {
|
|
|
805
962
|
|
|
806
963
|
// 4. Update title
|
|
807
964
|
if (sessionData.provider) {
|
|
808
|
-
|
|
809
|
-
this.
|
|
965
|
+
this._updateTitle(sessionData.provider, sessionData.model);
|
|
966
|
+
this._activeProvider = sessionData.provider;
|
|
810
967
|
} else {
|
|
811
968
|
this._updateTitle();
|
|
812
969
|
}
|
|
@@ -926,7 +1083,7 @@ class ChatPanel {
|
|
|
926
1083
|
|
|
927
1084
|
try {
|
|
928
1085
|
const body = {
|
|
929
|
-
provider:
|
|
1086
|
+
provider: this._activeProvider,
|
|
930
1087
|
reviewId: this.reviewId
|
|
931
1088
|
};
|
|
932
1089
|
if (contextCommentId) {
|
|
@@ -2286,10 +2443,11 @@ class ChatPanel {
|
|
|
2286
2443
|
* @param {Object} [toolInput] - Tool input/arguments (optional)
|
|
2287
2444
|
*/
|
|
2288
2445
|
_showToolUse(toolName, status, toolInput) {
|
|
2446
|
+
if (!toolName) return;
|
|
2289
2447
|
const streamingMsg = document.getElementById('chat-streaming-msg');
|
|
2290
2448
|
if (!streamingMsg) return;
|
|
2291
2449
|
|
|
2292
|
-
const isTask = toolName.toLowerCase() === 'task';
|
|
2450
|
+
const isTask = toolName.toLowerCase() === 'task' || toolName.toLowerCase() === 'agent';
|
|
2293
2451
|
|
|
2294
2452
|
if (status === 'start') {
|
|
2295
2453
|
this._hideThinkingIndicator();
|
|
@@ -2326,7 +2484,7 @@ class ChatPanel {
|
|
|
2326
2484
|
} else {
|
|
2327
2485
|
if (isTask) {
|
|
2328
2486
|
// Remove spinner from completed Task badge
|
|
2329
|
-
const badges = streamingMsg.querySelectorAll('.chat-panel__tool-badge[data-tool="Task"]:not(.chat-panel__tool-badge--transient)');
|
|
2487
|
+
const badges = streamingMsg.querySelectorAll('.chat-panel__tool-badge[data-tool="Task"]:not(.chat-panel__tool-badge--transient), .chat-panel__tool-badge[data-tool="Agent"]:not(.chat-panel__tool-badge--transient)');
|
|
2330
2488
|
badges.forEach(b => {
|
|
2331
2489
|
const spinner = b.querySelector('.chat-panel__tool-spinner');
|
|
2332
2490
|
if (spinner) spinner.remove();
|
|
@@ -2991,6 +3149,7 @@ class ChatPanel {
|
|
|
2991
3149
|
*/
|
|
2992
3150
|
destroy() {
|
|
2993
3151
|
document.removeEventListener('keydown', this._onKeydown);
|
|
3152
|
+
window.removeEventListener('chat-state-changed', this._onChatStateChanged);
|
|
2994
3153
|
this._closeSubscriptions();
|
|
2995
3154
|
this.messages = [];
|
|
2996
3155
|
|