@in-the-loop-labs/pair-review 3.6.0 → 3.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/package.json +20 -15
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/skills/analyze/scripts/git-diff-lines +0 -0
- package/public/css/analysis-config.css +1807 -0
- package/public/css/pr.css +17 -1737
- package/public/index.html +11 -0
- package/public/js/components/AIPanel.js +89 -44
- package/public/js/components/AdvancedConfigTab.js +56 -4
- package/public/js/components/AnalysisConfigModal.js +41 -25
- package/public/js/components/ChatPanel.js +11 -1
- package/public/js/components/ReviewModal.js +135 -13
- package/public/js/components/SuggestionNavigator.js +55 -10
- package/public/js/components/VoiceCentricConfigTab.js +36 -0
- package/public/js/index.js +175 -16
- package/public/js/local.js +58 -8
- package/public/js/modules/suggestion-manager.js +25 -1
- package/public/js/modules/tour-renderer.js +45 -5
- package/public/js/pr.js +703 -171
- package/public/js/repo-links.js +328 -0
- package/public/js/utils/provider-model.js +88 -0
- package/public/js/utils/scroll-into-view.js +164 -0
- package/public/js/utils/storage-keys.js +50 -0
- package/public/local.html +10 -0
- package/public/pr.html +10 -0
- package/public/repo-settings.html +1 -0
- package/public/setup.html +2 -0
- package/src/ai/analyzer.js +125 -18
- package/src/ai/claude-provider.js +31 -3
- package/src/config.js +664 -10
- package/src/external/github-adapter.js +114 -25
- package/src/git/base-branch.js +11 -4
- package/src/github/client.js +482 -588
- package/src/github/errors.js +55 -0
- package/src/github/impl/graphql/pending-review-comments.js +230 -0
- package/src/github/impl/graphql/pending-review.js +153 -0
- package/src/github/impl/graphql/review-lifecycle.js +161 -0
- package/src/github/impl/graphql/stack-walker.js +210 -0
- package/src/github/impl/host/pending-review-comments.js +338 -0
- package/src/github/impl/rest/pending-review.js +251 -0
- package/src/github/impl/rest/review-lifecycle.js +226 -0
- package/src/github/impl/rest/stack-walker.js +309 -0
- package/src/github/operations/pending-review-comments.js +79 -0
- package/src/github/operations/pending-review.js +89 -0
- package/src/github/operations/review-lifecycle.js +126 -0
- package/src/github/operations/stack-walker.js +87 -0
- package/src/github/parser.js +230 -4
- package/src/github/stack-walker.js +14 -189
- package/src/links/repo-links.js +230 -0
- package/src/local-review.js +13 -4
- package/src/main.js +136 -32
- package/src/routes/analyses.js +30 -7
- package/src/routes/bulk-analysis-configs.js +295 -0
- package/src/routes/config.js +102 -2
- package/src/routes/external-comments.js +20 -10
- package/src/routes/github-collections.js +3 -1
- package/src/routes/local.js +101 -11
- package/src/routes/mcp.js +47 -4
- package/src/routes/pr.js +298 -68
- package/src/routes/setup.js +8 -3
- package/src/routes/stack-analysis.js +33 -9
- package/src/routes/worktrees.js +3 -2
- package/src/server.js +2 -0
- package/src/setup/pr-setup.js +37 -11
- package/src/setup/stack-setup.js +13 -3
- package/src/single-port.js +6 -3
package/public/index.html
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
26
26
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
27
27
|
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,400&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
|
28
|
+
<link rel="stylesheet" href="/css/analysis-config.css">
|
|
28
29
|
|
|
29
30
|
<style>
|
|
30
31
|
:root {
|
|
@@ -1577,8 +1578,18 @@
|
|
|
1577
1578
|
</div>
|
|
1578
1579
|
|
|
1579
1580
|
<script src="/js/ws-client.js"></script>
|
|
1581
|
+
<script src="/js/utils/markdown.js"></script>
|
|
1582
|
+
<script src="/js/utils/tier-icons.js"></script>
|
|
1583
|
+
<script src="/js/utils/storage-keys.js"></script>
|
|
1584
|
+
<script src="/js/utils/provider-model.js"></script>
|
|
1580
1585
|
<script src="/js/components/Toast.js"></script>
|
|
1581
1586
|
<script src="/js/components/UpdateBanner.js"></script>
|
|
1587
|
+
<script src="/js/components/ConfirmDialog.js"></script>
|
|
1588
|
+
<script src="/js/components/TextInputDialog.js"></script>
|
|
1589
|
+
<script src="/js/components/TimeoutSelect.js"></script>
|
|
1590
|
+
<script src="/js/components/AnalysisConfigModal.js"></script>
|
|
1591
|
+
<script src="/js/components/VoiceCentricConfigTab.js"></script>
|
|
1592
|
+
<script src="/js/components/AdvancedConfigTab.js"></script>
|
|
1582
1593
|
<script src="/js/index.js"></script>
|
|
1583
1594
|
</body>
|
|
1584
1595
|
</html>
|
|
@@ -44,6 +44,10 @@ class AIPanel {
|
|
|
44
44
|
// Track selected item by stable identifier for restoration
|
|
45
45
|
this.selectedItemKey = null; // Format: "file:lineNumber:itemType:identity"
|
|
46
46
|
|
|
47
|
+
// Monotonic token so a fast move between items that supersedes an
|
|
48
|
+
// in-flight scrollTo* can tell the older call to bail after its await.
|
|
49
|
+
this._navGen = 0;
|
|
50
|
+
|
|
47
51
|
// Canonical file order for consistent sorting across components
|
|
48
52
|
this.fileOrder = new Map(); // Map of file path -> index
|
|
49
53
|
|
|
@@ -1130,9 +1134,19 @@ class AIPanel {
|
|
|
1130
1134
|
}
|
|
1131
1135
|
|
|
1132
1136
|
/**
|
|
1133
|
-
* Expand a file if it is collapsed
|
|
1137
|
+
* Expand a file if it is collapsed.
|
|
1138
|
+
*
|
|
1139
|
+
* Return contract (callers must handle both):
|
|
1140
|
+
* - `false` when nothing was expanded (no file, no wrapper, or already
|
|
1141
|
+
* expanded), OR a truthy non-thenable for the DOM-only fallback.
|
|
1142
|
+
* - a Promise when it routed through `prManager.toggleFileCollapse`,
|
|
1143
|
+
* which is async (it renders the lazy file body before revealing it).
|
|
1144
|
+
* Scroll callers await the Promise so the row lookup runs against a
|
|
1145
|
+
* rendered, visible body; the synchronous fast path is preserved when no
|
|
1146
|
+
* expansion is needed.
|
|
1147
|
+
*
|
|
1134
1148
|
* @param {string} file - The file path
|
|
1135
|
-
* @returns {boolean}
|
|
1149
|
+
* @returns {boolean|Promise<*>} See contract above.
|
|
1136
1150
|
*/
|
|
1137
1151
|
expandFileIfCollapsed(file) {
|
|
1138
1152
|
if (!file) return false;
|
|
@@ -1145,17 +1159,19 @@ class AIPanel {
|
|
|
1145
1159
|
|
|
1146
1160
|
// Check if collapsed
|
|
1147
1161
|
if (fileWrapper.classList.contains('collapsed')) {
|
|
1148
|
-
// Use prManager's toggle method if available (keeps state in sync)
|
|
1162
|
+
// Use prManager's toggle method if available (keeps state in sync).
|
|
1149
1163
|
const filePath = fileWrapper.dataset.fileName;
|
|
1150
1164
|
if (window.prManager?.toggleFileCollapse) {
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
//
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1165
|
+
// Async: renders the lazy body + removes `collapsed`. Hand the
|
|
1166
|
+
// Promise back so the caller can await render completion rather
|
|
1167
|
+
// than guessing with a fixed timeout.
|
|
1168
|
+
return window.prManager.toggleFileCollapse(filePath);
|
|
1169
|
+
}
|
|
1170
|
+
// Fallback: directly manipulate the DOM (no lazy render path).
|
|
1171
|
+
fileWrapper.classList.remove('collapsed');
|
|
1172
|
+
const header = fileWrapper.querySelector('.d2h-file-header');
|
|
1173
|
+
if (header && window.DiffRenderer) {
|
|
1174
|
+
window.DiffRenderer.updateFileHeaderState(header, true);
|
|
1159
1175
|
}
|
|
1160
1176
|
return true;
|
|
1161
1177
|
}
|
|
@@ -1166,11 +1182,19 @@ class AIPanel {
|
|
|
1166
1182
|
/**
|
|
1167
1183
|
* Scroll to an AI finding/suggestion in the diff view
|
|
1168
1184
|
*/
|
|
1169
|
-
scrollToFinding(findingId, file, line) {
|
|
1185
|
+
async scrollToFinding(findingId, file, line) {
|
|
1186
|
+
const myGen = ++this._navGen;
|
|
1170
1187
|
// Expand the file first if it's collapsed
|
|
1171
|
-
const
|
|
1188
|
+
const expansion = this.expandFileIfCollapsed(file);
|
|
1189
|
+
if (expansion && typeof expansion.then === 'function') await expansion;
|
|
1190
|
+
// Always render the target's lazy body — an expanded-but-offscreen
|
|
1191
|
+
// body has no suggestion rows until rendered, so the lookup below
|
|
1192
|
+
// would miss on the first attempt (expansion only covers the
|
|
1193
|
+
// collapsed case).
|
|
1194
|
+
if (file && window.prManager?.ensureFileBodyRendered) {
|
|
1195
|
+
try { await window.prManager.ensureFileBodyRendered(file); } catch { /* best effort */ }
|
|
1196
|
+
}
|
|
1172
1197
|
|
|
1173
|
-
// Small delay if we expanded to allow DOM to update
|
|
1174
1198
|
const doScroll = () => {
|
|
1175
1199
|
let targetSuggestion = null;
|
|
1176
1200
|
|
|
@@ -1205,34 +1229,54 @@ class AIPanel {
|
|
|
1205
1229
|
|
|
1206
1230
|
if (targetSuggestion) {
|
|
1207
1231
|
const minimizer = window.prManager?.commentMinimizer;
|
|
1232
|
+
let scrollTarget = targetSuggestion;
|
|
1208
1233
|
if (minimizer?.active) {
|
|
1209
1234
|
// Expand file-level comments so the target becomes visible
|
|
1210
1235
|
minimizer.expandForElement(targetSuggestion);
|
|
1211
1236
|
// Comments are minimized — scroll to the parent diff line instead
|
|
1212
|
-
|
|
1213
|
-
(diffRow || targetSuggestion).scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
1214
|
-
} else {
|
|
1215
|
-
targetSuggestion.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
1237
|
+
scrollTarget = minimizer.findDiffRowFor(targetSuggestion) || targetSuggestion;
|
|
1216
1238
|
}
|
|
1239
|
+
this._scrollDiffTarget(scrollTarget);
|
|
1217
1240
|
targetSuggestion.classList.add('current-suggestion');
|
|
1218
1241
|
setTimeout(() => targetSuggestion.classList.remove('current-suggestion'), 2000);
|
|
1219
1242
|
}
|
|
1220
1243
|
};
|
|
1221
1244
|
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1245
|
+
// A newer navigation took over while we awaited — let it win.
|
|
1246
|
+
if (myGen !== this._navGen) return;
|
|
1247
|
+
doScroll();
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
/**
|
|
1251
|
+
* Scroll a diff-panel element into view, preferring the stable helper
|
|
1252
|
+
* (re-corrects after lazy file bodies render mid-scroll and shift
|
|
1253
|
+
* layout). Fire-and-forget.
|
|
1254
|
+
* @param {Element} target
|
|
1255
|
+
*/
|
|
1256
|
+
_scrollDiffTarget(target) {
|
|
1257
|
+
// Land the target at the top of the diff panel (scroll-margin-top in
|
|
1258
|
+
// pr.css offsets it below the sticky toolbar + file header).
|
|
1259
|
+
const options = { behavior: 'smooth', block: 'start' };
|
|
1260
|
+
if (window.ScrollUtils?.scrollIntoViewStable) {
|
|
1261
|
+
window.ScrollUtils.scrollIntoViewStable(target, options);
|
|
1225
1262
|
} else {
|
|
1226
|
-
|
|
1263
|
+
target.scrollIntoView(options);
|
|
1227
1264
|
}
|
|
1228
1265
|
}
|
|
1229
1266
|
|
|
1230
1267
|
/**
|
|
1231
1268
|
* Scroll to a user comment in the diff view
|
|
1232
1269
|
*/
|
|
1233
|
-
scrollToComment(commentId, file, line) {
|
|
1270
|
+
async scrollToComment(commentId, file, line) {
|
|
1271
|
+
const myGen = ++this._navGen;
|
|
1234
1272
|
// Expand the file first if it's collapsed
|
|
1235
|
-
const
|
|
1273
|
+
const expansion = this.expandFileIfCollapsed(file);
|
|
1274
|
+
if (expansion && typeof expansion.then === 'function') await expansion;
|
|
1275
|
+
// Always render the target's lazy body — comment rows don't exist
|
|
1276
|
+
// inside an unrendered body, so the lookup below would miss.
|
|
1277
|
+
if (file && window.prManager?.ensureFileBodyRendered) {
|
|
1278
|
+
try { await window.prManager.ensureFileBodyRendered(file); } catch { /* best effort */ }
|
|
1279
|
+
}
|
|
1236
1280
|
|
|
1237
1281
|
const doScroll = () => {
|
|
1238
1282
|
let targetElement = null;
|
|
@@ -1275,13 +1319,13 @@ class AIPanel {
|
|
|
1275
1319
|
|
|
1276
1320
|
if (targetElement) {
|
|
1277
1321
|
const minimizer = window.prManager?.commentMinimizer;
|
|
1322
|
+
let scrollTarget = targetElement;
|
|
1278
1323
|
if (minimizer?.active) {
|
|
1279
1324
|
minimizer.expandForElement(targetElement);
|
|
1280
1325
|
const diffRow = isFileLevel ? null : minimizer.findDiffRowFor(targetElement);
|
|
1281
|
-
|
|
1282
|
-
} else {
|
|
1283
|
-
targetElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
1326
|
+
scrollTarget = diffRow || targetElement;
|
|
1284
1327
|
}
|
|
1328
|
+
this._scrollDiffTarget(scrollTarget);
|
|
1285
1329
|
// Add highlight effect
|
|
1286
1330
|
const commentDiv = isFileLevel ? targetElement : targetElement.querySelector('.user-comment');
|
|
1287
1331
|
if (commentDiv) {
|
|
@@ -1291,12 +1335,9 @@ class AIPanel {
|
|
|
1291
1335
|
}
|
|
1292
1336
|
};
|
|
1293
1337
|
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
} else {
|
|
1298
|
-
doScroll();
|
|
1299
|
-
}
|
|
1338
|
+
// A newer navigation took over while we awaited — let it win.
|
|
1339
|
+
if (myGen !== this._navGen) return;
|
|
1340
|
+
doScroll();
|
|
1300
1341
|
}
|
|
1301
1342
|
|
|
1302
1343
|
/**
|
|
@@ -1311,9 +1352,16 @@ class AIPanel {
|
|
|
1311
1352
|
* @param {string} file - File path for collapse-expand fallback
|
|
1312
1353
|
* @param {string|number} line - Anchor line; used for file/line fallback
|
|
1313
1354
|
*/
|
|
1314
|
-
scrollToExternalThread(threadId, source, file, line) {
|
|
1355
|
+
async scrollToExternalThread(threadId, source, file, line) {
|
|
1356
|
+
const myGen = ++this._navGen;
|
|
1315
1357
|
// Expand the file first if it's collapsed
|
|
1316
|
-
const
|
|
1358
|
+
const expansion = this.expandFileIfCollapsed(file);
|
|
1359
|
+
if (expansion && typeof expansion.then === 'function') await expansion;
|
|
1360
|
+
// Always render the target's lazy body — external thread rows don't
|
|
1361
|
+
// exist inside an unrendered body, so the lookup below would miss.
|
|
1362
|
+
if (file && window.prManager?.ensureFileBodyRendered) {
|
|
1363
|
+
try { await window.prManager.ensureFileBodyRendered(file); } catch { /* best effort */ }
|
|
1364
|
+
}
|
|
1317
1365
|
|
|
1318
1366
|
const doScroll = () => {
|
|
1319
1367
|
let target = null;
|
|
@@ -1354,13 +1402,12 @@ class AIPanel {
|
|
|
1354
1402
|
|
|
1355
1403
|
if (target) {
|
|
1356
1404
|
const minimizer = window.prManager?.commentMinimizer;
|
|
1405
|
+
let scrollTarget = target;
|
|
1357
1406
|
if (minimizer?.active) {
|
|
1358
1407
|
minimizer.expandForElement(target);
|
|
1359
|
-
|
|
1360
|
-
(diffRow || target).scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
1361
|
-
} else {
|
|
1362
|
-
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
1408
|
+
scrollTarget = minimizer.findDiffRowFor(target) || target;
|
|
1363
1409
|
}
|
|
1410
|
+
this._scrollDiffTarget(scrollTarget);
|
|
1364
1411
|
|
|
1365
1412
|
// Transient focus flash. The class is removed after 2s — if
|
|
1366
1413
|
// the row is rebuilt before then, the class is lost with it,
|
|
@@ -1370,11 +1417,9 @@ class AIPanel {
|
|
|
1370
1417
|
}
|
|
1371
1418
|
};
|
|
1372
1419
|
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
doScroll();
|
|
1377
|
-
}
|
|
1420
|
+
// A newer navigation took over while we awaited — let it win.
|
|
1421
|
+
if (myGen !== this._navGen) return;
|
|
1422
|
+
doScroll();
|
|
1378
1423
|
}
|
|
1379
1424
|
|
|
1380
1425
|
// ========================================
|
|
@@ -53,6 +53,12 @@ class AdvancedConfigTab {
|
|
|
53
53
|
this._injected = false;
|
|
54
54
|
this._councilsLoaded = false;
|
|
55
55
|
|
|
56
|
+
// Default orchestration provider/model for new councils, updated via
|
|
57
|
+
// setDefaultOrchestration(). Seeded here so _defaultConfig() is coherent
|
|
58
|
+
// even if reset() runs before setDefaultOrchestration().
|
|
59
|
+
this._defaultProvider = 'claude';
|
|
60
|
+
this._defaultModel = 'sonnet';
|
|
61
|
+
|
|
56
62
|
// Dirty state tracking
|
|
57
63
|
this._isDirty = false;
|
|
58
64
|
|
|
@@ -179,6 +185,37 @@ class AdvancedConfigTab {
|
|
|
179
185
|
*/
|
|
180
186
|
setDefaultCouncilId(councilId) {
|
|
181
187
|
this._pendingDefaultCouncilId = councilId;
|
|
188
|
+
// On a cached reopen the councils are already loaded, so loadCouncils() —
|
|
189
|
+
// and the _renderCouncilSelector() call that applies the pending default —
|
|
190
|
+
// will not run again (the modal instance is reused; see AnalysisConfigModal
|
|
191
|
+
// caching on window.analysisConfigModal). Apply it now so the saved/default
|
|
192
|
+
// council is restored instead of being silently dropped onto a blank
|
|
193
|
+
// "+ New Council" selection.
|
|
194
|
+
if (this._councilsLoaded && this._injected) {
|
|
195
|
+
this._renderCouncilSelector();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Reset selection and editor state for a fresh modal open.
|
|
201
|
+
*
|
|
202
|
+
* The AnalysisConfigModal (and therefore this tab) is reused across runs — and
|
|
203
|
+
* in the index/bulk flow, across different repositories. Without this reset a
|
|
204
|
+
* council selected in a previous run (or its pending default / dirty edits)
|
|
205
|
+
* would carry over and could be displayed or submitted for the next batch.
|
|
206
|
+
*/
|
|
207
|
+
reset() {
|
|
208
|
+
this.selectedCouncilId = null;
|
|
209
|
+
this._pendingDefaultCouncilId = null;
|
|
210
|
+
this._isDirty = false;
|
|
211
|
+
if (!this._injected) return;
|
|
212
|
+
const selector = this.modal.querySelector('#council-selector');
|
|
213
|
+
if (selector) {
|
|
214
|
+
selector.value = '';
|
|
215
|
+
selector.classList.add('new-council-selected');
|
|
216
|
+
}
|
|
217
|
+
this._applyConfigToUI(this._defaultConfig());
|
|
218
|
+
this._markClean();
|
|
182
219
|
}
|
|
183
220
|
|
|
184
221
|
/**
|
|
@@ -283,13 +320,23 @@ class AdvancedConfigTab {
|
|
|
283
320
|
}
|
|
284
321
|
|
|
285
322
|
_defaultConfig() {
|
|
323
|
+
const provider = this._defaultProvider || 'claude';
|
|
324
|
+
const model = this._defaultModel || 'sonnet';
|
|
325
|
+
const timeout = this._getProviderDefaultTimeout(provider);
|
|
326
|
+
// Seed one reviewer voice per enabled level. The server validator rejects an
|
|
327
|
+
// enabled level with an empty voices array (councils.js: "voices must be a
|
|
328
|
+
// non-empty array when enabled"), and _applyConfigToUI() wipes a level's row
|
|
329
|
+
// list when voices is empty — so an empty default would leave every level
|
|
330
|
+
// enabled with zero reviewer rows and fail at analysis kickoff. A fresh voice
|
|
331
|
+
// object per level avoids shared-reference aliasing.
|
|
332
|
+
const seedVoice = () => ({ provider, model, tier: 'balanced', timeout });
|
|
286
333
|
return {
|
|
287
334
|
levels: {
|
|
288
|
-
'1': { enabled: true, voices: [] },
|
|
289
|
-
'2': { enabled: true, voices: [] },
|
|
290
|
-
'3': { enabled: true, voices: [] }
|
|
335
|
+
'1': { enabled: true, voices: [seedVoice()] },
|
|
336
|
+
'2': { enabled: true, voices: [seedVoice()] },
|
|
337
|
+
'3': { enabled: true, voices: [seedVoice()] }
|
|
291
338
|
},
|
|
292
|
-
consolidation: { provider
|
|
339
|
+
consolidation: { provider, model, tier: 'balanced', timeout }
|
|
293
340
|
};
|
|
294
341
|
}
|
|
295
342
|
|
|
@@ -1432,3 +1479,8 @@ class AdvancedConfigTab {
|
|
|
1432
1479
|
if (typeof window !== 'undefined') {
|
|
1433
1480
|
window.AdvancedConfigTab = AdvancedConfigTab;
|
|
1434
1481
|
}
|
|
1482
|
+
|
|
1483
|
+
// Export for unit testing (Node/CommonJS environment)
|
|
1484
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
1485
|
+
module.exports = { AdvancedConfigTab };
|
|
1486
|
+
}
|
|
@@ -625,6 +625,15 @@ class AnalysisConfigModal {
|
|
|
625
625
|
* Select a model
|
|
626
626
|
*/
|
|
627
627
|
selectModel(modelId) {
|
|
628
|
+
// Guard: never store a model that doesn't belong to the current provider.
|
|
629
|
+
// Callers can pass a model resolved from a different scope (e.g. 'opus'
|
|
630
|
+
// while the provider is 'gemini'); fall back to the provider's default so
|
|
631
|
+
// the UI always shows a selected card and submissions carry a valid pair.
|
|
632
|
+
if (modelId && this.models.length && !this.models.some(m => m.id === modelId)) {
|
|
633
|
+
const fallback = this.models.find(m => m.default) || this.models[0];
|
|
634
|
+
modelId = fallback ? fallback.id : modelId;
|
|
635
|
+
}
|
|
636
|
+
|
|
628
637
|
this.selectedModel = modelId;
|
|
629
638
|
|
|
630
639
|
// Update UI
|
|
@@ -897,42 +906,41 @@ class AnalysisConfigModal {
|
|
|
897
906
|
// Build three-tab layout
|
|
898
907
|
this._injectTabLayout(options);
|
|
899
908
|
|
|
900
|
-
//
|
|
909
|
+
// Re-baseline council-tab state every open. The modal is reused across runs
|
|
910
|
+
// (and across repositories in the bulk flow), so always reset selection and
|
|
911
|
+
// pass normalized values — including empties — so a previous run's repo
|
|
912
|
+
// instructions, council selection, or dirty edits cannot bleed into the next.
|
|
913
|
+
const councilDefault = options.lastCouncilId || options.defaultCouncilId || null;
|
|
914
|
+
|
|
915
|
+
// Initialize voice-centric council tab.
|
|
916
|
+
//
|
|
917
|
+
// Order matters: setProviders() and setDefaultOrchestration() must run BEFORE
|
|
918
|
+
// reset(), because reset() repaints the UI from _defaultConfig(), which reads
|
|
919
|
+
// _defaultProvider/_defaultModel and the provider dropdown data. Calling reset()
|
|
920
|
+
// first would paint stale (previous-open) provider/model values on the reused
|
|
921
|
+
// modal. setDefaultCouncilId() runs last so any saved/default council is applied
|
|
922
|
+
// on top of the freshly reset defaults — including on cached reopens.
|
|
901
923
|
if (this.councilTab) {
|
|
902
924
|
const councilPanel = this.modal.querySelector('#tab-panel-council');
|
|
903
925
|
this.councilTab.inject(councilPanel);
|
|
904
926
|
this.councilTab.setProviders(this.providers);
|
|
905
|
-
|
|
906
|
-
if (options.repoInstructions) {
|
|
907
|
-
this.councilTab.setRepoInstructions(options.repoInstructions);
|
|
908
|
-
}
|
|
909
|
-
if (options.lastInstructions) {
|
|
910
|
-
this.councilTab.setLastInstructions(options.lastInstructions);
|
|
911
|
-
}
|
|
912
927
|
this.councilTab.setDefaultOrchestration(options.currentProvider, options.currentModel);
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
928
|
+
this.councilTab.reset();
|
|
929
|
+
this.councilTab.setRepoInstructions(options.repoInstructions || '');
|
|
930
|
+
this.councilTab.setLastInstructions(options.lastInstructions || '');
|
|
931
|
+
this.councilTab.setDefaultCouncilId(councilDefault);
|
|
917
932
|
}
|
|
918
933
|
|
|
919
|
-
// Initialize advanced (level-centric) tab
|
|
934
|
+
// Initialize advanced (level-centric) tab — same ordering rationale as above.
|
|
920
935
|
if (this.advancedTab) {
|
|
921
936
|
const advancedPanel = this.modal.querySelector('#tab-panel-advanced');
|
|
922
937
|
this.advancedTab.inject(advancedPanel);
|
|
923
938
|
this.advancedTab.setProviders(this.providers);
|
|
924
|
-
|
|
925
|
-
if (options.repoInstructions) {
|
|
926
|
-
this.advancedTab.setRepoInstructions(options.repoInstructions);
|
|
927
|
-
}
|
|
928
|
-
if (options.lastInstructions) {
|
|
929
|
-
this.advancedTab.setLastInstructions(options.lastInstructions);
|
|
930
|
-
}
|
|
931
939
|
this.advancedTab.setDefaultOrchestration(options.currentProvider, options.currentModel);
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
940
|
+
this.advancedTab.reset();
|
|
941
|
+
this.advancedTab.setRepoInstructions(options.repoInstructions || '');
|
|
942
|
+
this.advancedTab.setLastInstructions(options.lastInstructions || '');
|
|
943
|
+
this.advancedTab.setDefaultCouncilId(councilDefault);
|
|
936
944
|
}
|
|
937
945
|
|
|
938
946
|
// Set initial provider and model
|
|
@@ -942,7 +950,11 @@ class AnalysisConfigModal {
|
|
|
942
950
|
// Default to first available provider
|
|
943
951
|
this.selectProvider(Object.keys(this.providers)[0]);
|
|
944
952
|
}
|
|
945
|
-
|
|
953
|
+
// Only honour currentModel when it belongs to the selected provider.
|
|
954
|
+
// selectProvider() has already chosen a valid model for the provider (via
|
|
955
|
+
// tier matching); applying a foreign model here (e.g. 'opus' under 'gemini')
|
|
956
|
+
// would leave no model card selected and submit an invalid pair.
|
|
957
|
+
if (options.currentModel && this.models.some(m => m.id === options.currentModel)) {
|
|
946
958
|
this.selectModel(options.currentModel);
|
|
947
959
|
}
|
|
948
960
|
|
|
@@ -1434,3 +1446,7 @@ class AnalysisConfigModal {
|
|
|
1434
1446
|
if (typeof window !== 'undefined') {
|
|
1435
1447
|
window.AnalysisConfigModal = AnalysisConfigModal;
|
|
1436
1448
|
}
|
|
1449
|
+
|
|
1450
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
1451
|
+
module.exports = { AnalysisConfigModal };
|
|
1452
|
+
}
|
|
@@ -4613,7 +4613,17 @@ class ChatPanel {
|
|
|
4613
4613
|
const isVisible = rect.top >= 0 && rect.bottom <= window.innerHeight;
|
|
4614
4614
|
|
|
4615
4615
|
if (!isVisible) {
|
|
4616
|
-
|
|
4616
|
+
// Stable variant re-corrects after lazy file bodies render mid-scroll
|
|
4617
|
+
// and shift the layout (plain scrollIntoView lands off target the
|
|
4618
|
+
// first time on large diffs). Fire-and-forget.
|
|
4619
|
+
// Land the target at the top of the diff panel (scroll-margin-top in
|
|
4620
|
+
// pr.css offsets it below the sticky toolbar + file header).
|
|
4621
|
+
const scrollOptions = { behavior: 'smooth', block: 'start' };
|
|
4622
|
+
if (window.ScrollUtils?.scrollIntoViewStable) {
|
|
4623
|
+
window.ScrollUtils.scrollIntoViewStable(primaryRow, scrollOptions);
|
|
4624
|
+
} else {
|
|
4625
|
+
primaryRow.scrollIntoView(scrollOptions);
|
|
4626
|
+
}
|
|
4617
4627
|
}
|
|
4618
4628
|
|
|
4619
4629
|
// Apply the highlight to all target rows
|