@in-the-loop-labs/pair-review 3.1.3 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/public/css/pr.css +980 -3
- package/public/js/components/AIPanel.js +7 -4
- package/public/js/components/ChatPanel.js +34 -4
- package/public/js/components/CouncilProgressModal.js +11 -0
- package/public/js/components/NotificationDropdown.js +257 -0
- package/public/js/components/StackAnalysisDialog.js +313 -0
- package/public/js/components/StackProgressModal.js +475 -0
- package/public/js/components/StatusIndicator.js +1 -0
- package/public/js/components/SuggestionNavigator.js +2 -0
- package/public/js/modules/comment-manager.js +7 -0
- package/public/js/modules/comment-minimizer.js +151 -4
- package/public/js/modules/file-comment-manager.js +66 -2
- package/public/js/modules/suggestion-manager.js +2 -1
- package/public/js/pr.js +433 -2
- package/public/js/utils/notification-sounds.js +62 -0
- package/public/local.html +10 -0
- package/public/pr.html +12 -0
- package/public/setup.html +4 -0
- package/src/ai/claude-provider.js +1 -11
- package/src/ai/codex-provider.js +18 -16
- package/src/ai/copilot-provider.js +21 -21
- package/src/ai/gemini-provider.js +10 -0
- package/src/ai/pi-provider.js +22 -25
- package/src/ai/provider.js +26 -3
- package/src/chat/pi-bridge.js +8 -0
- package/src/chat/session-manager.js +1 -0
- package/src/git/base-branch.js +1 -51
- package/src/git/worktree-lock.js +88 -0
- package/src/git/worktree.js +64 -0
- package/src/github/stack-walker.js +196 -0
- package/src/routes/local.js +12 -8
- package/src/routes/pr.js +139 -26
- package/src/routes/sound.js +49 -0
- package/src/routes/stack-analysis.js +886 -0
- package/src/server.js +4 -0
- package/src/setup/stack-setup.js +77 -0
package/public/js/pr.js
CHANGED
|
@@ -178,6 +178,15 @@ class PRManager {
|
|
|
178
178
|
set: (v) => { this.lineTracker.potentialDragStart = v; }
|
|
179
179
|
});
|
|
180
180
|
|
|
181
|
+
// Stack analysis components
|
|
182
|
+
this.stackAnalysisDialog = window.StackAnalysisDialog ? new window.StackAnalysisDialog() : null;
|
|
183
|
+
this.stackProgressModal = window.StackProgressModal ? new window.StackProgressModal() : null;
|
|
184
|
+
// Track open state of split button and stack nav dropdowns
|
|
185
|
+
this._analyzeDropdownOpen = false;
|
|
186
|
+
this._stackNavOpen = false;
|
|
187
|
+
this._closeAnalyzeDropdown = null;
|
|
188
|
+
this._closeStackNav = null;
|
|
189
|
+
|
|
181
190
|
// Initialize event handlers and UI
|
|
182
191
|
this.setupEventHandlers();
|
|
183
192
|
this.initTheme();
|
|
@@ -198,6 +207,13 @@ class PRManager {
|
|
|
198
207
|
});
|
|
199
208
|
}
|
|
200
209
|
|
|
210
|
+
// Initialize notification sounds dropdown (bell icon)
|
|
211
|
+
const notifBtn = document.getElementById('notification-toggle');
|
|
212
|
+
if (notifBtn && window.NotificationDropdown) {
|
|
213
|
+
const notifEvents = window.PAIR_REVIEW_LOCAL_MODE ? ['analysis'] : ['analysis', 'setup'];
|
|
214
|
+
this.notificationDropdown = new window.NotificationDropdown(notifBtn, { events: notifEvents });
|
|
215
|
+
}
|
|
216
|
+
|
|
201
217
|
// In local mode, LocalManager handles init instead
|
|
202
218
|
if (!window.PAIR_REVIEW_LOCAL_MODE) {
|
|
203
219
|
this.init();
|
|
@@ -885,11 +901,12 @@ class PRManager {
|
|
|
885
901
|
if (breadcrumbRepo) breadcrumbRepo.textContent = pr.repo;
|
|
886
902
|
if (breadcrumbPr) breadcrumbPr.textContent = `#${pr.number}`;
|
|
887
903
|
|
|
888
|
-
// Update title
|
|
904
|
+
// Update title — wrap in stack nav dropdown when stack data is available
|
|
889
905
|
const titleElement = document.getElementById('pr-title-text');
|
|
890
906
|
if (titleElement) {
|
|
891
907
|
titleElement.textContent = pr.title;
|
|
892
908
|
}
|
|
909
|
+
this._renderStackNavDropdown(pr);
|
|
893
910
|
|
|
894
911
|
// Show/hide PR description info button
|
|
895
912
|
const descToggle = document.getElementById('pr-description-toggle');
|
|
@@ -1012,6 +1029,395 @@ class PRManager {
|
|
|
1012
1029
|
|
|
1013
1030
|
// Update pending draft indicator in toolbar
|
|
1014
1031
|
this.updatePendingDraftIndicator(pr.pendingDraft);
|
|
1032
|
+
|
|
1033
|
+
// Render analyze split button when stack data is available
|
|
1034
|
+
this._renderAnalyzeSplitButton(pr);
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
/**
|
|
1038
|
+
* Render the analyze split button when stack data is available.
|
|
1039
|
+
* Wraps the existing #analyze-btn with a dropdown toggle for "Analyze Stack".
|
|
1040
|
+
* @param {Object} pr - PR data with optional stack_data
|
|
1041
|
+
*/
|
|
1042
|
+
_renderAnalyzeSplitButton(pr) {
|
|
1043
|
+
const analyzeBtn = document.getElementById('analyze-btn');
|
|
1044
|
+
if (!analyzeBtn) return;
|
|
1045
|
+
|
|
1046
|
+
// Remove existing split container if present (re-render safe)
|
|
1047
|
+
const existingContainer = document.getElementById('analyze-split-container');
|
|
1048
|
+
if (existingContainer) {
|
|
1049
|
+
// Move analyze button back out of the container before removing
|
|
1050
|
+
existingContainer.parentNode.insertBefore(analyzeBtn, existingContainer);
|
|
1051
|
+
existingContainer.remove();
|
|
1052
|
+
}
|
|
1053
|
+
// Clean up previous outside-click handler
|
|
1054
|
+
if (this._closeAnalyzeDropdown) {
|
|
1055
|
+
document.removeEventListener('click', this._closeAnalyzeDropdown);
|
|
1056
|
+
this._closeAnalyzeDropdown = null;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// Determine stack PRs (non-trunk entries with PR numbers)
|
|
1060
|
+
const stackPRs = this._getStackPRs(pr);
|
|
1061
|
+
if (stackPRs.length < 2) return; // No meaningful stack
|
|
1062
|
+
|
|
1063
|
+
// Create split button container
|
|
1064
|
+
const container = document.createElement('div');
|
|
1065
|
+
container.className = 'analyze-split-container';
|
|
1066
|
+
container.id = 'analyze-split-container';
|
|
1067
|
+
|
|
1068
|
+
// Insert container where analyze button is, then move button inside
|
|
1069
|
+
analyzeBtn.parentNode.insertBefore(container, analyzeBtn);
|
|
1070
|
+
container.appendChild(analyzeBtn);
|
|
1071
|
+
|
|
1072
|
+
// Dropdown toggle (chevron)
|
|
1073
|
+
const toggle = document.createElement('button');
|
|
1074
|
+
toggle.className = 'analyze-dropdown-toggle';
|
|
1075
|
+
toggle.id = 'analyze-stack-toggle';
|
|
1076
|
+
toggle.type = 'button';
|
|
1077
|
+
toggle.setAttribute('aria-label', 'Stack analysis options');
|
|
1078
|
+
toggle.setAttribute('aria-haspopup', 'true');
|
|
1079
|
+
toggle.setAttribute('aria-expanded', 'false');
|
|
1080
|
+
toggle.innerHTML = `
|
|
1081
|
+
<svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
|
|
1082
|
+
<path d="M4.427 7.427l3.396 3.396a.25.25 0 00.354 0l3.396-3.396A.25.25 0 0011.396 7H4.604a.25.25 0 00-.177.427z"/>
|
|
1083
|
+
</svg>
|
|
1084
|
+
`;
|
|
1085
|
+
container.appendChild(toggle);
|
|
1086
|
+
|
|
1087
|
+
// Dropdown menu
|
|
1088
|
+
const menu = document.createElement('div');
|
|
1089
|
+
menu.className = 'analyze-dropdown-menu';
|
|
1090
|
+
menu.id = 'analyze-dropdown-menu';
|
|
1091
|
+
const itemBtn = document.createElement('button');
|
|
1092
|
+
itemBtn.className = 'analyze-dropdown-item';
|
|
1093
|
+
itemBtn.id = 'analyze-stack-btn';
|
|
1094
|
+
itemBtn.type = 'button';
|
|
1095
|
+
itemBtn.textContent = `Analyze Stack (${stackPRs.length} PRs)`;
|
|
1096
|
+
menu.appendChild(itemBtn);
|
|
1097
|
+
container.appendChild(menu);
|
|
1098
|
+
|
|
1099
|
+
// Event: toggle dropdown
|
|
1100
|
+
toggle.addEventListener('click', (e) => {
|
|
1101
|
+
e.stopPropagation();
|
|
1102
|
+
const isOpen = container.classList.toggle('open');
|
|
1103
|
+
toggle.setAttribute('aria-expanded', String(isOpen));
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
// Event: click stack analysis item
|
|
1107
|
+
itemBtn.addEventListener('click', (e) => {
|
|
1108
|
+
e.stopPropagation();
|
|
1109
|
+
container.classList.remove('open');
|
|
1110
|
+
toggle.setAttribute('aria-expanded', 'false');
|
|
1111
|
+
this.triggerStackAnalysis();
|
|
1112
|
+
});
|
|
1113
|
+
|
|
1114
|
+
// Close dropdown on outside click
|
|
1115
|
+
this._closeAnalyzeDropdown = (e) => {
|
|
1116
|
+
if (!container.contains(e.target)) {
|
|
1117
|
+
container.classList.remove('open');
|
|
1118
|
+
toggle.setAttribute('aria-expanded', 'false');
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
document.addEventListener('click', this._closeAnalyzeDropdown);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* Render the stack navigation dropdown around the PR title.
|
|
1126
|
+
* Replaces static title with a clickable dropdown when stack data has multiple PRs.
|
|
1127
|
+
* @param {Object} pr - PR data with optional stack_data
|
|
1128
|
+
*/
|
|
1129
|
+
_renderStackNavDropdown(pr) {
|
|
1130
|
+
const titleWrapper = document.querySelector('.pr-title-wrapper');
|
|
1131
|
+
if (!titleWrapper) return;
|
|
1132
|
+
|
|
1133
|
+
// Remove existing stack nav if present (re-render safe)
|
|
1134
|
+
const existingNav = titleWrapper.querySelector('.stack-nav-dropdown');
|
|
1135
|
+
if (existingNav) {
|
|
1136
|
+
// Restore the title element outside the nav wrapper
|
|
1137
|
+
const titleEl = existingNav.querySelector('#pr-title-text');
|
|
1138
|
+
if (titleEl) {
|
|
1139
|
+
titleWrapper.insertBefore(titleEl, existingNav);
|
|
1140
|
+
}
|
|
1141
|
+
existingNav.remove();
|
|
1142
|
+
}
|
|
1143
|
+
// Clean up previous outside-click handler
|
|
1144
|
+
if (this._closeStackNav) {
|
|
1145
|
+
document.removeEventListener('click', this._closeStackNav);
|
|
1146
|
+
this._closeStackNav = null;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
const stackPRs = this._getStackPRs(pr);
|
|
1150
|
+
if (stackPRs.length < 2) return; // No meaningful stack
|
|
1151
|
+
|
|
1152
|
+
const titleElement = document.getElementById('pr-title-text');
|
|
1153
|
+
if (!titleElement) return;
|
|
1154
|
+
|
|
1155
|
+
// Create dropdown wrapper
|
|
1156
|
+
const dropdown = document.createElement('div');
|
|
1157
|
+
dropdown.className = 'stack-nav-dropdown';
|
|
1158
|
+
|
|
1159
|
+
// Create trigger button wrapping the title
|
|
1160
|
+
const trigger = document.createElement('button');
|
|
1161
|
+
trigger.className = 'stack-nav-trigger';
|
|
1162
|
+
trigger.type = 'button';
|
|
1163
|
+
trigger.setAttribute('aria-haspopup', 'true');
|
|
1164
|
+
trigger.setAttribute('aria-expanded', 'false');
|
|
1165
|
+
|
|
1166
|
+
// Move title into trigger
|
|
1167
|
+
titleWrapper.insertBefore(dropdown, titleElement);
|
|
1168
|
+
dropdown.appendChild(trigger);
|
|
1169
|
+
trigger.appendChild(titleElement);
|
|
1170
|
+
|
|
1171
|
+
// Add chevron
|
|
1172
|
+
const chevron = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
1173
|
+
chevron.classList.add('stack-nav-chevron');
|
|
1174
|
+
chevron.setAttribute('viewBox', '0 0 16 16');
|
|
1175
|
+
chevron.setAttribute('width', '14');
|
|
1176
|
+
chevron.setAttribute('height', '14');
|
|
1177
|
+
chevron.innerHTML = '<path d="M4.427 7.427l3.396 3.396a.25.25 0 00.354 0l3.396-3.396A.25.25 0 0011.396 7H4.604a.25.25 0 00-.177.427z"/>';
|
|
1178
|
+
trigger.appendChild(chevron);
|
|
1179
|
+
|
|
1180
|
+
// Create menu
|
|
1181
|
+
const menu = document.createElement('div');
|
|
1182
|
+
menu.className = 'stack-nav-menu';
|
|
1183
|
+
dropdown.appendChild(menu);
|
|
1184
|
+
|
|
1185
|
+
// Populate menu items (reversed: stack base at bottom)
|
|
1186
|
+
const displayPRs = [...stackPRs].reverse();
|
|
1187
|
+
for (const stackPR of displayPRs) {
|
|
1188
|
+
const isCurrent = stackPR.prNumber === pr.number;
|
|
1189
|
+
const item = document.createElement('div');
|
|
1190
|
+
item.className = 'stack-nav-item';
|
|
1191
|
+
if (isCurrent) {
|
|
1192
|
+
item.classList.add('current');
|
|
1193
|
+
}
|
|
1194
|
+
item.dataset.pr = stackPR.prNumber;
|
|
1195
|
+
|
|
1196
|
+
// Text content column
|
|
1197
|
+
const textCol = document.createElement('div');
|
|
1198
|
+
textCol.className = 'stack-nav-text';
|
|
1199
|
+
|
|
1200
|
+
// Primary row: PR number + title inline
|
|
1201
|
+
const primaryRow = document.createElement('div');
|
|
1202
|
+
primaryRow.className = 'stack-nav-primary';
|
|
1203
|
+
|
|
1204
|
+
const numberSpan = document.createElement('span');
|
|
1205
|
+
numberSpan.className = 'stack-nav-number';
|
|
1206
|
+
numberSpan.textContent = `#${stackPR.prNumber}`;
|
|
1207
|
+
primaryRow.appendChild(numberSpan);
|
|
1208
|
+
|
|
1209
|
+
if (stackPR.title) {
|
|
1210
|
+
const titleSpan = document.createElement('span');
|
|
1211
|
+
titleSpan.className = 'stack-nav-title';
|
|
1212
|
+
titleSpan.textContent = stackPR.title;
|
|
1213
|
+
primaryRow.appendChild(titleSpan);
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
textCol.appendChild(primaryRow);
|
|
1217
|
+
|
|
1218
|
+
// Secondary row: branch name
|
|
1219
|
+
const branchRow = document.createElement('div');
|
|
1220
|
+
branchRow.className = 'stack-nav-branch';
|
|
1221
|
+
// SVG branch icon
|
|
1222
|
+
branchRow.innerHTML = '<svg class="stack-nav-branch-icon" width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path fill-rule="evenodd" d="M11.75 2.5a.75.75 0 100 1.5.75.75 0 000-1.5zm-2.25.75a2.25 2.25 0 113 2.122V6A2.5 2.5 0 0110 8.5H6a1 1 0 00-1 1v1.128a2.251 2.251 0 11-1.5 0V5.372a2.25 2.25 0 111.5 0v1.836A2.492 2.492 0 016 7h4a1 1 0 001-1v-.628A2.25 2.25 0 019.5 3.25zM4.25 12a.75.75 0 100 1.5.75.75 0 000-1.5zM3.5 3.25a.75.75 0 111.5 0 .75.75 0 01-1.5 0z"></path></svg>';
|
|
1223
|
+
const branchName = document.createElement('span');
|
|
1224
|
+
branchName.textContent = stackPR.branch || '';
|
|
1225
|
+
branchRow.appendChild(branchName);
|
|
1226
|
+
textCol.appendChild(branchRow);
|
|
1227
|
+
|
|
1228
|
+
item.appendChild(textCol);
|
|
1229
|
+
|
|
1230
|
+
// Navigate on click
|
|
1231
|
+
item.addEventListener('click', () => {
|
|
1232
|
+
if (stackPR.prNumber !== pr.number) {
|
|
1233
|
+
window.location.href = `/pr/${encodeURIComponent(pr.owner)}/${encodeURIComponent(pr.repo)}/${stackPR.prNumber}`;
|
|
1234
|
+
}
|
|
1235
|
+
dropdown.classList.remove('open');
|
|
1236
|
+
trigger.setAttribute('aria-expanded', 'false');
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
menu.appendChild(item);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// Toggle dropdown
|
|
1243
|
+
trigger.addEventListener('click', (e) => {
|
|
1244
|
+
e.stopPropagation();
|
|
1245
|
+
const isOpen = dropdown.classList.toggle('open');
|
|
1246
|
+
trigger.setAttribute('aria-expanded', String(isOpen));
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
// Close on outside click
|
|
1250
|
+
this._closeStackNav = (e) => {
|
|
1251
|
+
if (!dropdown.contains(e.target)) {
|
|
1252
|
+
dropdown.classList.remove('open');
|
|
1253
|
+
trigger.setAttribute('aria-expanded', 'false');
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
document.addEventListener('click', this._closeStackNav);
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
/**
|
|
1260
|
+
* Extract non-trunk stack PRs from PR data.
|
|
1261
|
+
* @param {Object} pr - PR data with optional stack_data
|
|
1262
|
+
* @returns {Array<Object>} Stack PR entries with prNumber, title, branch, hasAnalysis
|
|
1263
|
+
*/
|
|
1264
|
+
_getStackPRs(pr) {
|
|
1265
|
+
if (!pr.stack_data || !Array.isArray(pr.stack_data)) return [];
|
|
1266
|
+
return pr.stack_data.filter(entry => !entry.isTrunk && entry.prNumber);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* Trigger stack analysis flow:
|
|
1271
|
+
* 1. Open StackAnalysisDialog to select PRs
|
|
1272
|
+
* 2. Open AnalysisConfigModal for analysis config
|
|
1273
|
+
* 3. Call startStackAnalysis()
|
|
1274
|
+
*/
|
|
1275
|
+
async triggerStackAnalysis() {
|
|
1276
|
+
// If a stack analysis is active (running but hidden), reopen its progress modal
|
|
1277
|
+
if (this.stackProgressModal?.isActive) {
|
|
1278
|
+
this.stackProgressModal.reopenFromBackground();
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
|
|
1282
|
+
if (!this.currentPR) {
|
|
1283
|
+
this.showError('No PR loaded');
|
|
1284
|
+
return;
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
const { owner, repo, number } = this.currentPR;
|
|
1288
|
+
|
|
1289
|
+
try {
|
|
1290
|
+
// Open stack selection dialog
|
|
1291
|
+
if (!this.stackAnalysisDialog) {
|
|
1292
|
+
console.warn('StackAnalysisDialog not initialized');
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
const dialogResult = await this.stackAnalysisDialog.open(owner, repo, number);
|
|
1297
|
+
if (!dialogResult) return; // User cancelled
|
|
1298
|
+
const { selectedPRNumbers, prList } = dialogResult;
|
|
1299
|
+
if (!selectedPRNumbers || selectedPRNumbers.length === 0) return;
|
|
1300
|
+
|
|
1301
|
+
// Open analysis config modal
|
|
1302
|
+
if (!this.analysisConfigModal) {
|
|
1303
|
+
console.warn('AnalysisConfigModal not initialized, proceeding with defaults');
|
|
1304
|
+
await this.startStackAnalysis(owner, repo, number, selectedPRNumbers, {}, prList);
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
// Fetch settings in parallel
|
|
1309
|
+
const [repoSettings, reviewSettings] = await Promise.all([
|
|
1310
|
+
this.fetchRepoSettings().catch(() => null),
|
|
1311
|
+
this.fetchLastReviewSettings().catch(() => ({ custom_instructions: '', last_council_id: null }))
|
|
1312
|
+
]);
|
|
1313
|
+
|
|
1314
|
+
const currentModel = repoSettings?.default_model || 'opus';
|
|
1315
|
+
const currentProvider = repoSettings?.default_provider || 'claude';
|
|
1316
|
+
const tabStorageKey = PRManager.getRepoStorageKey('pair-review-tab', owner, repo);
|
|
1317
|
+
const rememberedTab = localStorage.getItem(tabStorageKey);
|
|
1318
|
+
const defaultTab = rememberedTab || repoSettings?.default_tab || 'single';
|
|
1319
|
+
const instructionsStorageKey = PRManager.getRepoStorageKey('pair-review-instructions', owner, repo);
|
|
1320
|
+
const lastInstructions = reviewSettings.custom_instructions
|
|
1321
|
+
?? localStorage.getItem(instructionsStorageKey)
|
|
1322
|
+
?? '';
|
|
1323
|
+
const lastCouncilId = reviewSettings.last_council_id;
|
|
1324
|
+
|
|
1325
|
+
this.analysisConfigModal.onTabChange = (tabId) => {
|
|
1326
|
+
localStorage.setItem(tabStorageKey, tabId);
|
|
1327
|
+
};
|
|
1328
|
+
|
|
1329
|
+
const config = await this.analysisConfigModal.show({
|
|
1330
|
+
currentModel,
|
|
1331
|
+
currentProvider,
|
|
1332
|
+
defaultTab,
|
|
1333
|
+
repoInstructions: repoSettings?.default_instructions || '',
|
|
1334
|
+
lastInstructions,
|
|
1335
|
+
lastCouncilId,
|
|
1336
|
+
defaultCouncilId: repoSettings?.default_council_id || null
|
|
1337
|
+
});
|
|
1338
|
+
|
|
1339
|
+
if (!config) return; // User cancelled
|
|
1340
|
+
|
|
1341
|
+
// Persist custom instructions
|
|
1342
|
+
const submittedInstructions = config.customInstructions || '';
|
|
1343
|
+
if (submittedInstructions) {
|
|
1344
|
+
localStorage.setItem(instructionsStorageKey, submittedInstructions);
|
|
1345
|
+
} else {
|
|
1346
|
+
localStorage.removeItem(instructionsStorageKey);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
await this.startStackAnalysis(owner, repo, number, selectedPRNumbers, config, prList);
|
|
1350
|
+
|
|
1351
|
+
} catch (error) {
|
|
1352
|
+
console.error('Error triggering stack analysis:', error);
|
|
1353
|
+
this.showError(`Failed to start stack analysis: ${error.message}`);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/**
|
|
1358
|
+
* Start stack analysis by posting to the backend and opening the progress modal.
|
|
1359
|
+
* @param {string} owner - Repository owner
|
|
1360
|
+
* @param {string} repo - Repository name
|
|
1361
|
+
* @param {number} number - Current PR number
|
|
1362
|
+
* @param {Array<number>} selectedPRNumbers - PRs to analyze
|
|
1363
|
+
* @param {Object} analysisConfig - Analysis configuration from the config modal
|
|
1364
|
+
* @param {Array<Object>} [prList] - PR metadata with titles from the selection dialog
|
|
1365
|
+
*/
|
|
1366
|
+
async startStackAnalysis(owner, repo, number, selectedPRNumbers, analysisConfig, prList) {
|
|
1367
|
+
try {
|
|
1368
|
+
const response = await fetch(`/api/pr/${owner}/${repo}/${number}/analyses/stack`, {
|
|
1369
|
+
method: 'POST',
|
|
1370
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1371
|
+
body: JSON.stringify({
|
|
1372
|
+
prNumbers: selectedPRNumbers,
|
|
1373
|
+
analysisConfig
|
|
1374
|
+
})
|
|
1375
|
+
});
|
|
1376
|
+
|
|
1377
|
+
if (!response.ok) {
|
|
1378
|
+
const error = await response.json().catch(() => ({}));
|
|
1379
|
+
throw new Error(error.error || 'Failed to start stack analysis');
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
const result = await response.json();
|
|
1383
|
+
|
|
1384
|
+
// Merge titles from dialog into backend response
|
|
1385
|
+
const prAnalysesWithTitles = (result.prAnalyses || []).map(pr => {
|
|
1386
|
+
const info = (prList || []).find(p => p.prNumber === pr.prNumber);
|
|
1387
|
+
return { ...pr, title: info?.title || pr.title };
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
// Set button to analyzing state so clicking it reopens the modal
|
|
1391
|
+
this.setButtonAnalyzing(result.stackAnalysisId);
|
|
1392
|
+
|
|
1393
|
+
// Update dropdown item to show "Analyzing Stack..."
|
|
1394
|
+
const stackBtn = document.getElementById('analyze-stack-btn');
|
|
1395
|
+
if (stackBtn) {
|
|
1396
|
+
stackBtn.textContent = 'Analyzing Stack...';
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
// Open stack progress modal
|
|
1400
|
+
if (this.stackProgressModal) {
|
|
1401
|
+
this.stackProgressModal.open(result.stackAnalysisId, prAnalysesWithTitles, {
|
|
1402
|
+
owner, repo,
|
|
1403
|
+
onComplete: () => {
|
|
1404
|
+
this.resetButton();
|
|
1405
|
+
// Reset dropdown item text
|
|
1406
|
+
const btn = document.getElementById('analyze-stack-btn');
|
|
1407
|
+
if (btn) {
|
|
1408
|
+
const stackPRs = this._getStackPRs(this.currentPR);
|
|
1409
|
+
btn.textContent = `Analyze Stack (${stackPRs.length} PRs)`;
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
} catch (error) {
|
|
1416
|
+
console.error('Error starting stack analysis:', error);
|
|
1417
|
+
if (window.toast) {
|
|
1418
|
+
window.toast.showError(`Stack analysis failed: ${error.message}`);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1015
1421
|
}
|
|
1016
1422
|
|
|
1017
1423
|
/**
|
|
@@ -2542,6 +2948,11 @@ class PRManager {
|
|
|
2542
2948
|
if (window.toast) {
|
|
2543
2949
|
window.toast.showSuccess('Comment dismissed');
|
|
2544
2950
|
}
|
|
2951
|
+
|
|
2952
|
+
if (apiResult.dismissedSuggestionId) {
|
|
2953
|
+
window.chatPanel?.queueUserActionHint(`[User Action: dismissed suggestion ${apiResult.dismissedSuggestionId}]`);
|
|
2954
|
+
}
|
|
2955
|
+
window.chatPanel?.queueUserActionHint(`[User Action: dismissed comment ${commentId}]`);
|
|
2545
2956
|
} catch (error) {
|
|
2546
2957
|
console.error('Error deleting comment:', error);
|
|
2547
2958
|
if (window.toast) {
|
|
@@ -2570,6 +2981,8 @@ class PRManager {
|
|
|
2570
2981
|
if (window.toast) {
|
|
2571
2982
|
window.toast.showSuccess('Comment restored');
|
|
2572
2983
|
}
|
|
2984
|
+
|
|
2985
|
+
window.chatPanel?.queueUserActionHint(`[User Action: restored comment ${commentId}]`);
|
|
2573
2986
|
} catch (error) {
|
|
2574
2987
|
console.error('Error restoring comment:', error);
|
|
2575
2988
|
if (window.toast) {
|
|
@@ -3050,6 +3463,7 @@ class PRManager {
|
|
|
3050
3463
|
});
|
|
3051
3464
|
this.displayUserComment(newComment, suggestionRow);
|
|
3052
3465
|
this._notifyAdoption(suggestionId, newComment);
|
|
3466
|
+
window.chatPanel?.queueUserActionHint(`[User Action: adopted suggestion ${suggestionId}]`);
|
|
3053
3467
|
} catch (error) {
|
|
3054
3468
|
console.error('Error saving edited suggestion:', error);
|
|
3055
3469
|
alert(`Failed to save suggestion: ${error.message}`);
|
|
@@ -3095,6 +3509,7 @@ class PRManager {
|
|
|
3095
3509
|
|
|
3096
3510
|
this.displayUserComment(result.newComment, result.suggestionRow);
|
|
3097
3511
|
this._notifyAdoption(suggestionId, result.newComment);
|
|
3512
|
+
window.chatPanel?.queueUserActionHint(`[User Action: adopted suggestion ${suggestionId}]`);
|
|
3098
3513
|
} catch (error) {
|
|
3099
3514
|
console.error('Error adopting suggestion:', error);
|
|
3100
3515
|
alert(`Failed to adopt suggestion: ${error.message}`);
|
|
@@ -3158,6 +3573,8 @@ class PRManager {
|
|
|
3158
3573
|
if (this.commentMinimizer) {
|
|
3159
3574
|
this.commentMinimizer.refreshIndicators();
|
|
3160
3575
|
}
|
|
3576
|
+
|
|
3577
|
+
window.chatPanel?.queueUserActionHint(`[User Action: dismissed suggestion ${suggestionId}]`);
|
|
3161
3578
|
} catch (error) {
|
|
3162
3579
|
console.error('Error dismissing suggestion:', error);
|
|
3163
3580
|
alert('Failed to dismiss suggestion');
|
|
@@ -3213,6 +3630,8 @@ class PRManager {
|
|
|
3213
3630
|
if (this.commentMinimizer) {
|
|
3214
3631
|
this.commentMinimizer.refreshIndicators();
|
|
3215
3632
|
}
|
|
3633
|
+
|
|
3634
|
+
window.chatPanel?.queueUserActionHint(`[User Action: restored suggestion ${suggestionId}]`);
|
|
3216
3635
|
} catch (error) {
|
|
3217
3636
|
console.error('Error restoring suggestion:', error);
|
|
3218
3637
|
alert('Failed to restore suggestion');
|
|
@@ -3960,6 +4379,10 @@ class PRManager {
|
|
|
3960
4379
|
btn.classList.add('btn-analyzing');
|
|
3961
4380
|
btn.disabled = false; // Keep clickable to reopen modal
|
|
3962
4381
|
|
|
4382
|
+
// Also highlight the split dropdown toggle if present
|
|
4383
|
+
const toggle = document.getElementById('analyze-stack-toggle');
|
|
4384
|
+
if (toggle) toggle.classList.add('btn-analyzing');
|
|
4385
|
+
|
|
3963
4386
|
const btnText = btn.querySelector('.btn-text');
|
|
3964
4387
|
if (btnText) {
|
|
3965
4388
|
btnText.textContent = 'Analyzing...';
|
|
@@ -3981,6 +4404,10 @@ class PRManager {
|
|
|
3981
4404
|
btn.classList.remove('btn-analyzing');
|
|
3982
4405
|
btn.classList.add('btn-complete');
|
|
3983
4406
|
|
|
4407
|
+
// Also clear the split dropdown toggle
|
|
4408
|
+
const toggleComplete = document.getElementById('analyze-stack-toggle');
|
|
4409
|
+
if (toggleComplete) toggleComplete.classList.remove('btn-analyzing');
|
|
4410
|
+
|
|
3984
4411
|
const btnText = btn.querySelector('.btn-text');
|
|
3985
4412
|
if (btnText) {
|
|
3986
4413
|
btnText.textContent = 'Complete';
|
|
@@ -4009,6 +4436,10 @@ class PRManager {
|
|
|
4009
4436
|
btn.classList.remove('btn-analyzing', 'btn-complete');
|
|
4010
4437
|
btn.disabled = false;
|
|
4011
4438
|
|
|
4439
|
+
// Also clear the split dropdown toggle
|
|
4440
|
+
const toggleReset = document.getElementById('analyze-stack-toggle');
|
|
4441
|
+
if (toggleReset) toggleReset.classList.remove('btn-analyzing');
|
|
4442
|
+
|
|
4012
4443
|
const btnText = btn.querySelector('.btn-text');
|
|
4013
4444
|
if (btnText) {
|
|
4014
4445
|
btnText.textContent = 'Analyze';
|
|
@@ -4181,7 +4612,7 @@ class PRManager {
|
|
|
4181
4612
|
reopenModal() {
|
|
4182
4613
|
if (!this.currentAnalysisId) return;
|
|
4183
4614
|
|
|
4184
|
-
// Reopen the progress modal
|
|
4615
|
+
// Reopen the per-PR progress modal (council/single analysis)
|
|
4185
4616
|
if (window.councilProgressModal && window.councilProgressModal.currentAnalysisId === this.currentAnalysisId) {
|
|
4186
4617
|
window.councilProgressModal.reopenFromBackground();
|
|
4187
4618
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Notification sound preferences backed by localStorage.
|
|
5
|
+
* Sound playback is delegated to the server (POST /api/play-sound) so that it
|
|
6
|
+
* works reliably even when the browser was opened programmatically without a
|
|
7
|
+
* user gesture (which would block Web Audio API).
|
|
8
|
+
*/
|
|
9
|
+
class NotificationSounds {
|
|
10
|
+
/**
|
|
11
|
+
* Returns the localStorage key for a given event type.
|
|
12
|
+
* @param {string} eventType - 'analysis' or 'setup'
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
_storageKey(eventType) {
|
|
16
|
+
return 'pair-review-notify-' + eventType;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check whether notifications are enabled for the given event type.
|
|
21
|
+
* Returns false if the key is missing (default off).
|
|
22
|
+
* @param {string} eventType - 'analysis' or 'setup'
|
|
23
|
+
* @returns {boolean}
|
|
24
|
+
*/
|
|
25
|
+
isEnabled(eventType) {
|
|
26
|
+
const val = localStorage.getItem(this._storageKey(eventType));
|
|
27
|
+
return val === 'true';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Set whether notifications are enabled for the given event type.
|
|
32
|
+
* @param {string} eventType - 'analysis' or 'setup'
|
|
33
|
+
* @param {boolean} enabled
|
|
34
|
+
*/
|
|
35
|
+
setEnabled(eventType, enabled) {
|
|
36
|
+
localStorage.setItem(this._storageKey(eventType), enabled ? 'true' : 'false');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Play a chime if notifications are enabled for the given event type.
|
|
41
|
+
* @param {string} eventType - 'analysis' or 'setup'
|
|
42
|
+
*/
|
|
43
|
+
playIfEnabled(eventType) {
|
|
44
|
+
if (this.isEnabled(eventType)) {
|
|
45
|
+
this.playChime();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Ask the server to play a system notification sound.
|
|
51
|
+
* Fire-and-forget — errors are silently ignored.
|
|
52
|
+
*/
|
|
53
|
+
playChime() {
|
|
54
|
+
fetch('/api/play-sound', { method: 'POST' }).catch(() => {});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
window.notificationSounds = new NotificationSounds();
|
|
59
|
+
|
|
60
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
61
|
+
module.exports = { NotificationSounds };
|
|
62
|
+
}
|
package/public/local.html
CHANGED
|
@@ -341,6 +341,14 @@
|
|
|
341
341
|
<path d="M9.598 1.591a.749.749 0 0 1 .785-.175 7.001 7.001 0 1 1-8.967 8.967.75.75 0 0 1 .961-.96 5.5 5.5 0 0 0 7.046-7.046.75.75 0 0 1 .175-.786Z"/>
|
|
342
342
|
</svg>
|
|
343
343
|
</button>
|
|
344
|
+
<button class="btn btn-icon" id="notification-toggle" title="Notification sounds">
|
|
345
|
+
<svg class="bell-icon-on" viewBox="0 0 16 16" fill="currentColor" width="16" height="16">
|
|
346
|
+
<path d="M8 16a2 2 0 0 0 1.985-1.75c.017-.137-.097-.25-.235-.25h-3.5c-.138 0-.252.113-.235.25A2 2 0 0 0 8 16ZM3 5a5 5 0 0 1 10 0v2.947c0 .05.015.098.042.139l1.703 2.555A1.519 1.519 0 0 1 13.482 13H2.518a1.516 1.516 0 0 1-1.263-2.36l1.703-2.554A.255.255 0 0 0 3 7.947Zm5-3.5A3.5 3.5 0 0 0 4.5 5v2.947c0 .346-.102.683-.294.97l-1.703 2.556a.017.017 0 0 0-.003.01l.001.006c0 .002.002.004.004.006l.006.004.007.001h10.964l.007-.001.006-.004.004-.006.001-.007a.017.017 0 0 0-.003-.01l-1.703-2.554a1.745 1.745 0 0 1-.294-.97V5A3.5 3.5 0 0 0 8 1.5Z"/>
|
|
347
|
+
</svg>
|
|
348
|
+
<svg class="bell-icon-off" viewBox="0 0 16 16" fill="currentColor" width="16" height="16" style="display: none;">
|
|
349
|
+
<path d="m4.182 4.31.016.011 10.104 7.316.013.01 1.375.996a.75.75 0 1 1-.88 1.214L13.626 13H2.518a1.516 1.516 0 0 1-1.263-2.36l1.703-2.554A.255.255 0 0 0 3 7.947V5.305L.31 3.357a.75.75 0 1 1 .88-1.214Zm7.373 7.19L4.5 6.391v1.556c0 .346-.102.683-.294.97l-1.703 2.556a.017.017 0 0 0-.003.01c0 .005.002.009.005.012l.006.004.007.001ZM8 1.5c-.997 0-1.895.416-2.534 1.086A.75.75 0 1 1 4.38 1.55 5 5 0 0 1 13 5v2.373a.75.75 0 0 1-1.5 0V5A3.5 3.5 0 0 0 8 1.5ZM8 16a2 2 0 0 1-1.985-1.75c-.017-.137.097-.25.235-.25h3.5c.138 0 .252.113.235.25A2 2 0 0 1 8 16Z"/>
|
|
350
|
+
</svg>
|
|
351
|
+
</button>
|
|
344
352
|
<a class="btn btn-icon settings-link" id="settings-link" href="#" title="Repository settings" style="display: none;">
|
|
345
353
|
<svg viewBox="0 0 16 16" fill="currentColor" width="16" height="16">
|
|
346
354
|
<path d="M8 0a8.2 8.2 0 0 1 .701.031C9.444.095 9.99.645 10.16 1.29l.288 1.107c.018.066.079.158.212.224.231.114.454.243.668.386.123.082.233.09.299.071l1.103-.303c.644-.176 1.392.021 1.82.63.27.385.506.792.704 1.218.315.675.111 1.422-.364 1.891l-.814.806c-.049.048-.098.147-.088.294.016.257.016.515 0 .772-.01.147.038.246.088.294l.814.806c.475.469.679 1.216.364 1.891a7.977 7.977 0 0 1-.704 1.217c-.428.61-1.176.807-1.82.63l-1.102-.302c-.067-.019-.177-.011-.3.071a5.909 5.909 0 0 1-.668.386c-.133.066-.194.158-.211.224l-.29 1.106c-.168.646-.715 1.196-1.458 1.26a8.006 8.006 0 0 1-1.402 0c-.743-.064-1.289-.614-1.458-1.26l-.289-1.106c-.018-.066-.079-.158-.212-.224a5.738 5.738 0 0 1-.668-.386c-.123-.082-.233-.09-.299-.071l-1.103.303c-.644.176-1.392-.021-1.82-.63a8.12 8.12 0 0 1-.704-1.218c-.315-.675-.111-1.422.363-1.891l.815-.806c.05-.048.098-.147.088-.294a6.214 6.214 0 0 1 0-.772c.01-.147-.038-.246-.088-.294l-.815-.806C.635 6.045.431 5.298.746 4.623a7.92 7.92 0 0 1 .704-1.217c.428-.61 1.176-.807 1.82-.63l1.102.302c.067.019.177.011.3-.071.214-.143.437-.272.668-.386.133-.066.194-.158.211-.224l.29-1.106C6.009.645 6.556.095 7.299.03 7.53.01 7.764 0 8 0Zm-.571 1.525c-.036.003-.108.036-.137.146l-.289 1.105c-.147.561-.549.967-.998 1.189-.173.086-.34.183-.5.29-.417.278-.97.423-1.529.27l-1.103-.303c-.109-.03-.175.016-.195.045-.22.312-.412.644-.573.99-.014.031-.021.11.059.19l.815.806c.411.406.562.957.53 1.456a4.709 4.709 0 0 0 0 .582c.032.499-.119 1.05-.53 1.456l-.815.806c-.081.08-.073.159-.059.19.162.346.353.677.573.989.02.03.085.076.195.046l1.102-.303c.56-.153 1.113-.008 1.53.27.161.107.328.204.501.29.447.222.85.629.997 1.189l.289 1.105c.029.109.101.143.137.146a6.6 6.6 0 0 0 1.142 0c.036-.003.108-.036.137-.146l.289-1.105c.147-.561.549-.967.998-1.189.173-.086.34-.183.5-.29.417-.278.97-.423 1.529-.27l1.103.303c.109.029.175-.016.195-.045.22-.313.411-.644.573-.99.014-.031.021-.11-.059-.19l-.815-.806c-.411-.406-.562-.957-.53-1.456a4.709 4.709 0 0 0 0-.582c-.032-.499.119-1.05.53-1.456l.815-.806c.081-.08.073-.159.059-.19a6.464 6.464 0 0 0-.573-.989c-.02-.03-.085-.076-.195-.046l-1.102.303c-.56.153-1.113.008-1.53-.27a4.44 4.44 0 0 0-.501-.29c-.447-.222-.85-.629-.997-1.189l-.289-1.105c-.029-.11-.101-.143-.137-.146a6.6 6.6 0 0 0-1.142 0ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM9.5 8a1.5 1.5 0 1 0-3.001.001A1.5 1.5 0 0 0 9.5 8Z"/>
|
|
@@ -590,6 +598,8 @@
|
|
|
590
598
|
<script src="/js/components/VoiceCentricConfigTab.js"></script>
|
|
591
599
|
<script src="/js/components/AdvancedConfigTab.js"></script>
|
|
592
600
|
<script src="/js/components/CouncilProgressModal.js"></script>
|
|
601
|
+
<script src="/js/utils/notification-sounds.js"></script>
|
|
602
|
+
<script src="/js/components/NotificationDropdown.js"></script>
|
|
593
603
|
<script src="/js/components/StatusIndicator.js"></script>
|
|
594
604
|
<script src="/js/components/SuggestionNavigator.js"></script>
|
|
595
605
|
<script src="/js/components/ReviewModal.js"></script>
|
package/public/pr.html
CHANGED
|
@@ -134,6 +134,14 @@
|
|
|
134
134
|
<path d="M9.598 1.591a.749.749 0 0 1 .785-.175 7.001 7.001 0 1 1-8.967 8.967.75.75 0 0 1 .961-.96 5.5 5.5 0 0 0 7.046-7.046.75.75 0 0 1 .175-.786Z"/>
|
|
135
135
|
</svg>
|
|
136
136
|
</button>
|
|
137
|
+
<button class="btn btn-icon" id="notification-toggle" title="Notification sounds">
|
|
138
|
+
<svg class="bell-icon-on" viewBox="0 0 16 16" fill="currentColor" width="16" height="16">
|
|
139
|
+
<path d="M8 16a2 2 0 0 0 1.985-1.75c.017-.137-.097-.25-.235-.25h-3.5c-.138 0-.252.113-.235.25A2 2 0 0 0 8 16ZM3 5a5 5 0 0 1 10 0v2.947c0 .05.015.098.042.139l1.703 2.555A1.519 1.519 0 0 1 13.482 13H2.518a1.516 1.516 0 0 1-1.263-2.36l1.703-2.554A.255.255 0 0 0 3 7.947Zm5-3.5A3.5 3.5 0 0 0 4.5 5v2.947c0 .346-.102.683-.294.97l-1.703 2.556a.017.017 0 0 0-.003.01l.001.006c0 .002.002.004.004.006l.006.004.007.001h10.964l.007-.001.006-.004.004-.006.001-.007a.017.017 0 0 0-.003-.01l-1.703-2.554a1.745 1.745 0 0 1-.294-.97V5A3.5 3.5 0 0 0 8 1.5Z"/>
|
|
140
|
+
</svg>
|
|
141
|
+
<svg class="bell-icon-off" viewBox="0 0 16 16" fill="currentColor" width="16" height="16" style="display: none;">
|
|
142
|
+
<path d="m4.182 4.31.016.011 10.104 7.316.013.01 1.375.996a.75.75 0 1 1-.88 1.214L13.626 13H2.518a1.516 1.516 0 0 1-1.263-2.36l1.703-2.554A.255.255 0 0 0 3 7.947V5.305L.31 3.357a.75.75 0 1 1 .88-1.214Zm7.373 7.19L4.5 6.391v1.556c0 .346-.102.683-.294.97l-1.703 2.556a.017.017 0 0 0-.003.01c0 .005.002.009.005.012l.006.004.007.001ZM8 1.5c-.997 0-1.895.416-2.534 1.086A.75.75 0 1 1 4.38 1.55 5 5 0 0 1 13 5v2.373a.75.75 0 0 1-1.5 0V5A3.5 3.5 0 0 0 8 1.5ZM8 16a2 2 0 0 1-1.985-1.75c-.017-.137.097-.25.235-.25h3.5c.138 0 .252.113.235.25A2 2 0 0 1 8 16Z"/>
|
|
143
|
+
</svg>
|
|
144
|
+
</button>
|
|
137
145
|
<a class="btn btn-icon settings-link" id="settings-link" href="#" title="Repository settings">
|
|
138
146
|
<svg viewBox="0 0 16 16" fill="currentColor" width="16" height="16">
|
|
139
147
|
<path d="M8 0a8.2 8.2 0 0 1 .701.031C9.444.095 9.99.645 10.16 1.29l.288 1.107c.018.066.079.158.212.224.231.114.454.243.668.386.123.082.233.09.299.071l1.103-.303c.644-.176 1.392.021 1.82.63.27.385.506.792.704 1.218.315.675.111 1.422-.364 1.891l-.814.806c-.049.048-.098.147-.088.294.016.257.016.515 0 .772-.01.147.038.246.088.294l.814.806c.475.469.679 1.216.364 1.891a7.977 7.977 0 0 1-.704 1.217c-.428.61-1.176.807-1.82.63l-1.102-.302c-.067-.019-.177-.011-.3.071a5.909 5.909 0 0 1-.668.386c-.133.066-.194.158-.211.224l-.29 1.106c-.168.646-.715 1.196-1.458 1.26a8.006 8.006 0 0 1-1.402 0c-.743-.064-1.289-.614-1.458-1.26l-.289-1.106c-.018-.066-.079-.158-.212-.224a5.738 5.738 0 0 1-.668-.386c-.123-.082-.233-.09-.299-.071l-1.103.303c-.644.176-1.392-.021-1.82-.63a8.12 8.12 0 0 1-.704-1.218c-.315-.675-.111-1.422.363-1.891l.815-.806c.05-.048.098-.147.088-.294a6.214 6.214 0 0 1 0-.772c.01-.147-.038-.246-.088-.294l-.815-.806C.635 6.045.431 5.298.746 4.623a7.92 7.92 0 0 1 .704-1.217c.428-.61 1.176-.807 1.82-.63l1.102.302c.067.019.177.011.3-.071.214-.143.437-.272.668-.386.133-.066.194-.158.211-.224l.29-1.106C6.009.645 6.556.095 7.299.03 7.53.01 7.764 0 8 0Zm-.571 1.525c-.036.003-.108.036-.137.146l-.289 1.105c-.147.561-.549.967-.998 1.189-.173.086-.34.183-.5.29-.417.278-.97.423-1.529.27l-1.103-.303c-.109-.03-.175.016-.195.045-.22.312-.412.644-.573.99-.014.031-.021.11.059.19l.815.806c.411.406.562.957.53 1.456a4.709 4.709 0 0 0 0 .582c.032.499-.119 1.05-.53 1.456l-.815.806c-.081.08-.073.159-.059.19.162.346.353.677.573.989.02.03.085.076.195.046l1.102-.303c.56-.153 1.113-.008 1.53.27.161.107.328.204.501.29.447.222.85.629.997 1.189l.289 1.105c.029.109.101.143.137.146a6.6 6.6 0 0 0 1.142 0c.036-.003.108-.036.137-.146l.289-1.105c.147-.561.549-.967.998-1.189.173-.086.34-.183.5-.29.417-.278.97-.423 1.529-.27l1.103.303c.109.029.175-.016.195-.045.22-.313.411-.644.573-.99.014-.031.021-.11-.059-.19l-.815-.806c-.411-.406-.562-.957-.53-1.456a4.709 4.709 0 0 0 0-.582c-.032-.499.119-1.05.53-1.456l.815-.806c.081-.08.073-.159.059-.19a6.464 6.464 0 0 0-.573-.989c-.02-.03-.085-.076-.195-.046l-1.102.303c-.56.153-1.113.008-1.53-.27a4.44 4.44 0 0 0-.501-.29c-.447-.222-.85-.629-.997-1.189l-.289-1.105c-.029-.11-.101-.143-.137-.146a6.6 6.6 0 0 0-1.142 0ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM9.5 8a1.5 1.5 0 1 0-3.001.001A1.5 1.5 0 0 0 9.5 8Z"/>
|
|
@@ -386,6 +394,8 @@
|
|
|
386
394
|
<script src="/js/components/VoiceCentricConfigTab.js"></script>
|
|
387
395
|
<script src="/js/components/AdvancedConfigTab.js"></script>
|
|
388
396
|
<script src="/js/components/CouncilProgressModal.js"></script>
|
|
397
|
+
<script src="/js/utils/notification-sounds.js"></script>
|
|
398
|
+
<script src="/js/components/NotificationDropdown.js"></script>
|
|
389
399
|
<script src="/js/components/StatusIndicator.js"></script>
|
|
390
400
|
<script src="/js/components/SuggestionNavigator.js"></script>
|
|
391
401
|
<script src="/js/components/ReviewModal.js"></script>
|
|
@@ -396,6 +406,8 @@
|
|
|
396
406
|
<script src="/js/components/EmojiPicker.js"></script>
|
|
397
407
|
<script src="/js/components/KeyboardShortcuts.js"></script>
|
|
398
408
|
<script src="/js/components/DiffOptionsDropdown.js"></script>
|
|
409
|
+
<script src="/js/components/StackAnalysisDialog.js"></script>
|
|
410
|
+
<script src="/js/components/StackProgressModal.js"></script>
|
|
399
411
|
|
|
400
412
|
<!-- PR Modules (must load before pr.js) -->
|
|
401
413
|
<script src="/js/modules/storage-cleanup.js"></script>
|