@in-the-loop-labs/pair-review 3.1.4 → 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 +894 -1
- package/public/js/components/CouncilProgressModal.js +8 -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/pr.js +420 -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/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
|
/**
|
|
@@ -3973,6 +4379,10 @@ class PRManager {
|
|
|
3973
4379
|
btn.classList.add('btn-analyzing');
|
|
3974
4380
|
btn.disabled = false; // Keep clickable to reopen modal
|
|
3975
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
|
+
|
|
3976
4386
|
const btnText = btn.querySelector('.btn-text');
|
|
3977
4387
|
if (btnText) {
|
|
3978
4388
|
btnText.textContent = 'Analyzing...';
|
|
@@ -3994,6 +4404,10 @@ class PRManager {
|
|
|
3994
4404
|
btn.classList.remove('btn-analyzing');
|
|
3995
4405
|
btn.classList.add('btn-complete');
|
|
3996
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
|
+
|
|
3997
4411
|
const btnText = btn.querySelector('.btn-text');
|
|
3998
4412
|
if (btnText) {
|
|
3999
4413
|
btnText.textContent = 'Complete';
|
|
@@ -4022,6 +4436,10 @@ class PRManager {
|
|
|
4022
4436
|
btn.classList.remove('btn-analyzing', 'btn-complete');
|
|
4023
4437
|
btn.disabled = false;
|
|
4024
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
|
+
|
|
4025
4443
|
const btnText = btn.querySelector('.btn-text');
|
|
4026
4444
|
if (btnText) {
|
|
4027
4445
|
btnText.textContent = 'Analyze';
|
|
@@ -4194,7 +4612,7 @@ class PRManager {
|
|
|
4194
4612
|
reopenModal() {
|
|
4195
4613
|
if (!this.currentAnalysisId) return;
|
|
4196
4614
|
|
|
4197
|
-
// Reopen the progress modal
|
|
4615
|
+
// Reopen the per-PR progress modal (council/single analysis)
|
|
4198
4616
|
if (window.councilProgressModal && window.councilProgressModal.currentAnalysisId === this.currentAnalysisId) {
|
|
4199
4617
|
window.councilProgressModal.reopenFromBackground();
|
|
4200
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>
|
package/public/setup.html
CHANGED
|
@@ -528,6 +528,7 @@
|
|
|
528
528
|
|
|
529
529
|
<!-- WebSocket client -->
|
|
530
530
|
<script src="/js/ws-client.js"></script>
|
|
531
|
+
<script src="/js/utils/notification-sounds.js"></script>
|
|
531
532
|
|
|
532
533
|
<script>
|
|
533
534
|
// Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
|
|
@@ -818,6 +819,9 @@
|
|
|
818
819
|
updateProgressBar();
|
|
819
820
|
showRedirect();
|
|
820
821
|
|
|
822
|
+
// Play notification sound for PR setup completion
|
|
823
|
+
if (window.notificationSounds && mode !== 'local') window.notificationSounds.playIfEnabled('setup');
|
|
824
|
+
|
|
821
825
|
// Small delay so the user sees the completed state
|
|
822
826
|
setTimeout(function() {
|
|
823
827
|
if (msg.reviewUrl) {
|