@in-the-loop-labs/pair-review 3.9.1 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -17
- package/package.json +6 -9
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +3 -4
- package/plugin-pair-loop/.claude-plugin/plugin.json +19 -0
- package/plugin-pair-loop/skills/loop/SKILL.md +233 -0
- package/public/js/components/AdvancedConfigTab.js +1 -1
- package/public/js/components/AnalysisConfigModal.js +2 -2
- package/public/js/index.js +87 -10
- package/public/js/local.js +20 -10
- package/public/js/pr.js +67 -39
- package/public/js/repo-links.js +11 -3
- package/public/js/utils/analyze-params.js +69 -0
- package/public/js/utils/provider-model.js +67 -2
- package/public/js/vendor/pierre-diffs-worker.js +16158 -0
- package/public/js/vendor/pierre-diffs.js +1880 -0
- package/public/local.html +1 -0
- package/public/pr.html +1 -0
- package/public/setup.html +35 -16
- package/src/ai/analyzer.js +4 -4
- package/src/ai/antigravity-provider.js +594 -0
- package/src/ai/index.js +1 -1
- package/src/ai/opencode-provider.js +1 -1
- package/src/ai/provider.js +5 -5
- package/src/ai/stream-parser.js +1 -52
- package/src/chat/acp-bridge.js +1 -1
- package/src/chat/chat-providers.js +1 -9
- package/src/config.js +153 -31
- package/src/database.js +128 -4
- package/src/external/github-adapter.js +18 -3
- package/src/github/client.js +37 -0
- package/src/github/parser.js +41 -7
- package/src/interactive-analysis-config.js +2 -2
- package/src/links/repo-links.js +66 -28
- package/src/local-review.js +134 -5
- package/src/local-scope.js +38 -0
- package/src/main.js +203 -37
- package/src/routes/config.js +49 -14
- package/src/routes/external-comments.js +13 -1
- package/src/routes/github-collections.js +175 -13
- package/src/routes/local.js +11 -20
- package/src/routes/pr.js +63 -36
- package/src/routes/setup.js +63 -8
- package/src/routes/shared.js +85 -0
- package/src/routes/stack-analysis.js +39 -3
- package/src/server.js +74 -3
- package/src/setup/local-setup.js +23 -7
- package/src/setup/pr-setup.js +237 -39
- package/src/setup/stack-setup.js +7 -2
- package/src/single-port.js +73 -18
- package/src/utils/host-resolution.js +157 -0
- package/plugin-code-critic/skills/loop/SKILL.md +0 -373
- package/src/ai/gemini-provider.js +0 -752
package/public/js/index.js
CHANGED
|
@@ -445,8 +445,14 @@
|
|
|
445
445
|
? ''
|
|
446
446
|
: '<td class="col-author">' + authorDisplay + '</td>';
|
|
447
447
|
|
|
448
|
+
// `data-host` carries the alt-host api_host string for PRs that live on an
|
|
449
|
+
// alternate host; github.com rows have no host and render an empty
|
|
450
|
+
// attribute. The click handler threads it into the setup call so a dual
|
|
451
|
+
// repo's alt-host PR opens against the right system without re-probing.
|
|
452
|
+
var hostAttr = pr.host ? ' data-host="' + escapeHtml(pr.host) + '"' : '';
|
|
453
|
+
|
|
448
454
|
return '' +
|
|
449
|
-
'<tr class="collection-pr-row" data-pr-url="' + escapeHtml(prUrl) + '" data-owner="' + escapeHtml(pr.owner) + '" data-repo="' + escapeHtml(pr.repo) + '" data-number="' + pr.number + '">' +
|
|
455
|
+
'<tr class="collection-pr-row" data-pr-url="' + escapeHtml(prUrl) + '" data-owner="' + escapeHtml(pr.owner) + '" data-repo="' + escapeHtml(pr.repo) + '" data-number="' + pr.number + '"' + hostAttr + '>' +
|
|
450
456
|
'<td class="col-repo">' + escapeHtml(repoFull) + '</td>' +
|
|
451
457
|
'<td class="col-pr"><span class="collection-pr-number">#' + pr.number + '</span></td>' +
|
|
452
458
|
'<td class="col-title" title="' + escapeHtml(pr.title || '') + '">' + escapeHtml(pr.title || '') + '</td>' +
|
|
@@ -1293,7 +1299,14 @@
|
|
|
1293
1299
|
return {
|
|
1294
1300
|
owner: data.owner,
|
|
1295
1301
|
repo: data.repo,
|
|
1296
|
-
prNumber: data.prNumber
|
|
1302
|
+
prNumber: data.prNumber,
|
|
1303
|
+
// `host` tells setup which system the PR lives on: an api_host URL
|
|
1304
|
+
// string = alt host, null = github.com, undefined = unknown.
|
|
1305
|
+
host: data.host,
|
|
1306
|
+
bindingRepository: data.bindingRepository,
|
|
1307
|
+
// Whether the repo is dual (github + alt-host). Only dual repos probe,
|
|
1308
|
+
// so only they need an explicit github pick when the URL is a github URL.
|
|
1309
|
+
isDualHost: data.isDualHost
|
|
1297
1310
|
};
|
|
1298
1311
|
}
|
|
1299
1312
|
|
|
@@ -1343,7 +1356,22 @@
|
|
|
1343
1356
|
// Navigate to the PR route which serves setup.html (with step-by-step progress)
|
|
1344
1357
|
// for new PRs, or pr.html directly for PRs already in the database
|
|
1345
1358
|
let href = '/pr/' + encodeURIComponent(parsed.owner) + '/' + encodeURIComponent(parsed.repo) + '/' + encodeURIComponent(parsed.prNumber);
|
|
1346
|
-
|
|
1359
|
+
var params = new URLSearchParams();
|
|
1360
|
+
if (analyze) params.set('analyze', 'true');
|
|
1361
|
+
// Forward the parsed host so setup binds directly instead of probing:
|
|
1362
|
+
// - alt-host URL string → the api_host (setup binds that alt host)
|
|
1363
|
+
// - github URL (host === null) on a DUAL repo → the "github" sentinel, so
|
|
1364
|
+
// setup binds github.com instead of probing alt-first (which would fail
|
|
1365
|
+
// loudly if the alt host is down for a PR we KNOW is on github)
|
|
1366
|
+
// - anything else (plain/exclusive repo, unknown host) → omit; no probe
|
|
1367
|
+
// happens for those and omitting avoids the exclusive-null throw.
|
|
1368
|
+
if (typeof parsed.host === 'string' && parsed.host) {
|
|
1369
|
+
params.set('host', parsed.host);
|
|
1370
|
+
} else if (parsed.host === null && parsed.isDualHost) {
|
|
1371
|
+
params.set('host', 'github');
|
|
1372
|
+
}
|
|
1373
|
+
var qs = params.toString();
|
|
1374
|
+
if (qs) href += '?' + qs;
|
|
1347
1375
|
window.location.href = href;
|
|
1348
1376
|
}
|
|
1349
1377
|
|
|
@@ -1385,6 +1413,11 @@
|
|
|
1385
1413
|
window.__pairReview.chatProviders = chatProviders;
|
|
1386
1414
|
window.__pairReview.defaultProvider = config.default_provider || 'claude';
|
|
1387
1415
|
window.__pairReview.defaultModel = config.default_model || 'opus';
|
|
1416
|
+
// CLI/env override (PAIR_REVIEW_PROVIDER / PAIR_REVIEW_MODEL) surfaced
|
|
1417
|
+
// by /api/config. The bulk-analysis seed prepends these ahead of repo
|
|
1418
|
+
// settings so a `--provider` override outranks a repo's saved default.
|
|
1419
|
+
window.__pairReview.providerOverride = config.provider_override || null;
|
|
1420
|
+
window.__pairReview.modelOverride = config.model_override || null;
|
|
1388
1421
|
window.__pairReview.hasGithubToken = Boolean(config.has_github_token);
|
|
1389
1422
|
window.__pairReview.enableGraphite = config.enable_graphite === true;
|
|
1390
1423
|
window.__pairReview.chatSpinner = config.chat_spinner || 'dots';
|
|
@@ -1891,7 +1924,7 @@
|
|
|
1891
1924
|
* Read selected collection rows as PR descriptors.
|
|
1892
1925
|
* @param {Set} selectedIds - PR URLs (data-pr-url values)
|
|
1893
1926
|
* @param {string} tbodyId - tbody element ID
|
|
1894
|
-
* @returns {Array<{owner: string, repo: string, number: string, prUrl: string}>}
|
|
1927
|
+
* @returns {Array<{owner: string, repo: string, number: string, prUrl: string, host: string|undefined}>}
|
|
1895
1928
|
*/
|
|
1896
1929
|
function getSelectedCollectionRows(selectedIds, tbodyId) {
|
|
1897
1930
|
var tbody = document.getElementById(tbodyId);
|
|
@@ -1908,7 +1941,11 @@
|
|
|
1908
1941
|
owner: tr.dataset.owner,
|
|
1909
1942
|
repo: tr.dataset.repo,
|
|
1910
1943
|
number: tr.dataset.number,
|
|
1911
|
-
prUrl: prUrl
|
|
1944
|
+
prUrl: prUrl,
|
|
1945
|
+
// Alt-host rows carry a host; github.com rows leave it undefined. The
|
|
1946
|
+
// single-row click path threads this the same way (see the collection
|
|
1947
|
+
// row click handler) so bulk-open binds to the right system too.
|
|
1948
|
+
host: tr.dataset.host
|
|
1912
1949
|
});
|
|
1913
1950
|
}
|
|
1914
1951
|
}
|
|
@@ -1917,7 +1954,14 @@
|
|
|
1917
1954
|
|
|
1918
1955
|
function buildReviewUrlsFromRows(rows, query) {
|
|
1919
1956
|
return rows.map(function (row) {
|
|
1920
|
-
|
|
1957
|
+
// Preserve any existing params (analyze / analysisConfigId) and append the
|
|
1958
|
+
// alt host so setup opens the PR against the system it lives on instead of
|
|
1959
|
+
// re-probing. github.com rows (no host) keep the URL unchanged.
|
|
1960
|
+
var qs = query || '';
|
|
1961
|
+
if (row.host) {
|
|
1962
|
+
qs += (qs ? '&' : '?') + 'host=' + encodeURIComponent(row.host);
|
|
1963
|
+
}
|
|
1964
|
+
return '/pr/' + encodeURIComponent(row.owner) + '/' + encodeURIComponent(row.repo) + '/' + row.number + qs;
|
|
1921
1965
|
});
|
|
1922
1966
|
}
|
|
1923
1967
|
|
|
@@ -2000,10 +2044,18 @@
|
|
|
2000
2044
|
};
|
|
2001
2045
|
|
|
2002
2046
|
var providersInfo = await getBulkProvidersInfo();
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2047
|
+
// Shape __pairReview into the appConfig fields buildProviderModelScopes reads
|
|
2048
|
+
// so a CLI/env `--provider` override is prepended ahead of repo settings.
|
|
2049
|
+
var bulkAppConfig = {
|
|
2050
|
+
default_provider: window.__pairReview?.defaultProvider,
|
|
2051
|
+
default_model: window.__pairReview?.defaultModel,
|
|
2052
|
+
provider_override: window.__pairReview?.providerOverride || null,
|
|
2053
|
+
model_override: window.__pairReview?.modelOverride || null
|
|
2054
|
+
};
|
|
2055
|
+
var resolvedPair = window.resolveProviderModelPair(
|
|
2056
|
+
window.buildProviderModelScopes(repoSettings, bulkAppConfig),
|
|
2057
|
+
providersInfo
|
|
2058
|
+
);
|
|
2007
2059
|
|
|
2008
2060
|
var config = await bulkAnalysisConfigModal.show({
|
|
2009
2061
|
currentModel: resolvedPair.model,
|
|
@@ -2163,6 +2215,21 @@
|
|
|
2163
2215
|
return;
|
|
2164
2216
|
}
|
|
2165
2217
|
|
|
2218
|
+
// Alt-host PR: we already know owner/repo/number and the host it lives
|
|
2219
|
+
// on, so navigate straight to the PR route with the host as a query
|
|
2220
|
+
// param (setup.html forwards it into the setup POST body). This bypasses
|
|
2221
|
+
// the URL-parse round trip, which can't reliably recover the host from a
|
|
2222
|
+
// pasted alt-host html_url. github.com rows (no host) keep the existing
|
|
2223
|
+
// parse-and-submit flow below, byte-identical.
|
|
2224
|
+
var rowHost = collectionRow.dataset.host;
|
|
2225
|
+
if (rowHost) {
|
|
2226
|
+
var o = encodeURIComponent(collectionRow.dataset.owner);
|
|
2227
|
+
var r = encodeURIComponent(collectionRow.dataset.repo);
|
|
2228
|
+
var n = encodeURIComponent(collectionRow.dataset.number);
|
|
2229
|
+
window.location.href = '/pr/' + o + '/' + r + '/' + n + '?host=' + encodeURIComponent(rowHost);
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2166
2233
|
var prUrl = collectionRow.dataset.prUrl;
|
|
2167
2234
|
if (prUrl) {
|
|
2168
2235
|
// Switch to PR tab to show loading state (do NOT persist to
|
|
@@ -2542,4 +2609,14 @@
|
|
|
2542
2609
|
window.__pairReview = window.__pairReview || {};
|
|
2543
2610
|
window.__pairReview.refreshAnalysisSpinners = fetchAndApplyActiveAnalyses;
|
|
2544
2611
|
|
|
2612
|
+
// Test-only exports. In the browser `module` is undefined, so this is a
|
|
2613
|
+
// no-op; under Vitest (CommonJS) it exposes the internal bulk-open helpers
|
|
2614
|
+
// for direct unit testing rather than duplicating their logic in a test.
|
|
2615
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
2616
|
+
module.exports = {
|
|
2617
|
+
buildReviewUrlsFromRows: buildReviewUrlsFromRows,
|
|
2618
|
+
getSelectedCollectionRows: getSelectedCollectionRows
|
|
2619
|
+
};
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2545
2622
|
})();
|
package/public/js/local.js
CHANGED
|
@@ -70,9 +70,17 @@ class LocalManager {
|
|
|
70
70
|
await this.loadLocalReview();
|
|
71
71
|
|
|
72
72
|
// Auto-trigger analysis if ?analyze=true is present
|
|
73
|
-
const
|
|
73
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
74
|
+
const autoAnalyze = searchParams.get('analyze');
|
|
74
75
|
if (autoAnalyze === 'true' && !window.prManager.isAnalyzing) {
|
|
75
76
|
const manager = window.prManager;
|
|
77
|
+
// Provider/model override carried on the URL by single-port delegation
|
|
78
|
+
// (the delegated-to server is a different process whose env never saw the
|
|
79
|
+
// CLI flag). Prepended ahead of repo settings in _buildDefaultAnalysisConfig.
|
|
80
|
+
const urlOverride = {
|
|
81
|
+
provider: searchParams.get('provider'),
|
|
82
|
+
model: searchParams.get('model')
|
|
83
|
+
};
|
|
76
84
|
try {
|
|
77
85
|
// Prefer a stashed bulk-analysis config (threaded via analysisConfigId).
|
|
78
86
|
// The CLI uses it to carry --instructions plus the resolved
|
|
@@ -97,15 +105,15 @@ class LocalManager {
|
|
|
97
105
|
manager.fetchLastReviewSettings().catch(() => ({ custom_instructions: '', last_council_id: null })),
|
|
98
106
|
manager._getAppConfig()
|
|
99
107
|
]);
|
|
100
|
-
config = await manager._buildDefaultAnalysisConfig(repoSettings, reviewSettings, appConfig);
|
|
108
|
+
config = await manager._buildDefaultAnalysisConfig(repoSettings, reviewSettings, appConfig, null, urlOverride);
|
|
101
109
|
}
|
|
102
110
|
|
|
103
111
|
await this.startLocalAnalysis(null, config);
|
|
104
112
|
} finally {
|
|
105
113
|
const cleanUrl = new URL(window.location);
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
114
|
+
// Strip the whole auto-analyze intent bundle (analyze/analysisConfigId/
|
|
115
|
+
// council/provider/model) so a manual refresh does not replay the intent.
|
|
116
|
+
window.stripAnalyzeParams(cleanUrl);
|
|
109
117
|
history.replaceState(null, '', cleanUrl);
|
|
110
118
|
}
|
|
111
119
|
}
|
|
@@ -346,13 +354,15 @@ class LocalManager {
|
|
|
346
354
|
const lastCouncilId = reviewSettings.last_council_id;
|
|
347
355
|
|
|
348
356
|
// Resolve provider and model as a MATCHED pair so the council/advanced tabs
|
|
349
|
-
// are never seeded with a cross-provider model (e.g.
|
|
357
|
+
// are never seeded with a cross-provider model (e.g. antigravity + opus), which
|
|
350
358
|
// would blank the model <select> and be rejected by the backend.
|
|
359
|
+
// buildProviderModelScopes prepends any CLI/env override ahead of repo
|
|
360
|
+
// settings so `--provider` seeds the modal as the default selection.
|
|
351
361
|
const providersInfo = await manager._getProvidersInfo();
|
|
352
|
-
const { provider: currentProvider, model: currentModel } = window.resolveProviderModelPair(
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
362
|
+
const { provider: currentProvider, model: currentModel } = window.resolveProviderModelPair(
|
|
363
|
+
window.buildProviderModelScopes(repoSettings, appConfig),
|
|
364
|
+
providersInfo
|
|
365
|
+
);
|
|
356
366
|
|
|
357
367
|
// Determine default tab (priority: localStorage > repo settings > 'single')
|
|
358
368
|
const tabStorageKey = `pair-review-tab:local-${reviewId}`;
|
package/public/js/pr.js
CHANGED
|
@@ -634,7 +634,7 @@ class PRManager {
|
|
|
634
634
|
* @param {Object} reviewSettings - Review settings from fetchLastReviewSettings()
|
|
635
635
|
* @returns {Promise<Object>} Config object suitable for startAnalysis / startLocalAnalysis
|
|
636
636
|
*/
|
|
637
|
-
async _buildDefaultAnalysisConfig(repoSettings, reviewSettings, appConfig = {}, providersInfo = null) {
|
|
637
|
+
async _buildDefaultAnalysisConfig(repoSettings, reviewSettings, appConfig = {}, providersInfo = null, urlOverride = null) {
|
|
638
638
|
const defaultTab = repoSettings?.default_tab || 'single';
|
|
639
639
|
const councilId = repoSettings?.default_council_id || reviewSettings?.last_council_id || null;
|
|
640
640
|
|
|
@@ -642,6 +642,8 @@ class PRManager {
|
|
|
642
642
|
// highest priority for council selection. When present we force the council
|
|
643
643
|
// branch regardless of default_tab/settings, and derive configType from the
|
|
644
644
|
// council's own type ('council' or 'advanced') rather than the repo default.
|
|
645
|
+
// An explicit council outranks a provider/model override here, matching the
|
|
646
|
+
// backend precedence in resolveReviewConfig where --council beats --provider.
|
|
645
647
|
const urlSearch = (typeof window !== 'undefined' && window.location && window.location.search) || '';
|
|
646
648
|
const urlCouncilId = new URLSearchParams(urlSearch).get('council');
|
|
647
649
|
if (urlCouncilId) {
|
|
@@ -676,7 +678,14 @@ class PRManager {
|
|
|
676
678
|
}
|
|
677
679
|
}
|
|
678
680
|
|
|
679
|
-
|
|
681
|
+
// A CLI/env or delegation-URL provider/model override names a single
|
|
682
|
+
// provider, which is incompatible with a multi-voice council. When an
|
|
683
|
+
// override is active we bypass the repo's *default* council and force the
|
|
684
|
+
// single-provider path so `--provider` is always honored. (An explicit
|
|
685
|
+
// `?council=` above still wins, matching the backend precedence.)
|
|
686
|
+
const overrideActive = window.hasProviderModelOverride(appConfig, urlOverride);
|
|
687
|
+
|
|
688
|
+
if (!overrideActive && (defaultTab === 'council' || defaultTab === 'advanced') && councilId) {
|
|
680
689
|
// Fetch the full council config so the progress modal can render correctly
|
|
681
690
|
let councilConfig = null;
|
|
682
691
|
let councilName = null;
|
|
@@ -704,12 +713,14 @@ class PRManager {
|
|
|
704
713
|
// Resolve provider and model as a MATCHED pair. Resolving each half
|
|
705
714
|
// independently (repo || app || hardcoded) can mix a provider from one
|
|
706
715
|
// scope with a model from another, yielding an invalid pair (e.g.
|
|
707
|
-
//
|
|
716
|
+
// antigravity/opus) that startAnalysis would forward to the backend as-is.
|
|
717
|
+
// buildProviderModelScopes prepends any CLI/env or delegation-URL override
|
|
718
|
+
// ahead of repo settings so `--provider` outranks a repo's saved default.
|
|
708
719
|
const providers = providersInfo || await this._getProvidersInfo();
|
|
709
|
-
const { provider, model } = window.resolveProviderModelPair(
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
720
|
+
const { provider, model } = window.resolveProviderModelPair(
|
|
721
|
+
window.buildProviderModelScopes(repoSettings, appConfig, urlOverride),
|
|
722
|
+
providers
|
|
723
|
+
);
|
|
713
724
|
|
|
714
725
|
return {
|
|
715
726
|
provider,
|
|
@@ -750,10 +761,18 @@ class PRManager {
|
|
|
750
761
|
* @param {number} prNumber - PR number
|
|
751
762
|
*/
|
|
752
763
|
async _maybeAutoAnalyze(owner, repo, prNumber) {
|
|
753
|
-
const
|
|
764
|
+
const searchParams = new URLSearchParams(window.location.search);
|
|
765
|
+
const autoAnalyze = searchParams.get('analyze');
|
|
754
766
|
if (autoAnalyze === 'true' && !this.isAnalyzing) {
|
|
755
767
|
this._autoAnalyzeRequested = true;
|
|
756
768
|
let shouldCleanUrl = true;
|
|
769
|
+
// Provider/model override carried on the URL by single-port delegation
|
|
770
|
+
// (the delegated-to server is a different process whose env never saw the
|
|
771
|
+
// CLI flag). Prepended ahead of repo settings in _buildDefaultAnalysisConfig.
|
|
772
|
+
const urlOverride = {
|
|
773
|
+
provider: searchParams.get('provider'),
|
|
774
|
+
model: searchParams.get('model')
|
|
775
|
+
};
|
|
757
776
|
try {
|
|
758
777
|
// Skip refresh if we just loaded fresh data (loadPR sets _justLoaded = true).
|
|
759
778
|
// Otherwise, refresh to ensure we have the latest PR data in case the worktree
|
|
@@ -791,7 +810,7 @@ class PRManager {
|
|
|
791
810
|
this.fetchLastReviewSettings().catch(() => ({ custom_instructions: '', last_council_id: null })),
|
|
792
811
|
this._getAppConfig()
|
|
793
812
|
]);
|
|
794
|
-
config = await this._buildDefaultAnalysisConfig(repoSettings, reviewSettings, appConfig);
|
|
813
|
+
config = await this._buildDefaultAnalysisConfig(repoSettings, reviewSettings, appConfig, null, urlOverride);
|
|
795
814
|
}
|
|
796
815
|
|
|
797
816
|
await this.startAnalysis(owner, repo, prNumber, null, config);
|
|
@@ -799,9 +818,9 @@ class PRManager {
|
|
|
799
818
|
this._autoAnalyzeRequested = false;
|
|
800
819
|
if (shouldCleanUrl) {
|
|
801
820
|
const cleanUrl = new URL(window.location);
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
821
|
+
// Strip the whole auto-analyze intent bundle (analyze/analysisConfigId/
|
|
822
|
+
// council/provider/model) so a manual refresh does not replay the intent.
|
|
823
|
+
window.stripAnalyzeParams(cleanUrl);
|
|
805
824
|
history.replaceState(null, '', cleanUrl);
|
|
806
825
|
}
|
|
807
826
|
}
|
|
@@ -3053,13 +3072,15 @@ class PRManager {
|
|
|
3053
3072
|
]);
|
|
3054
3073
|
|
|
3055
3074
|
// Resolve provider and model as a MATCHED pair so the council/advanced tabs
|
|
3056
|
-
// are never seeded with a cross-provider model (e.g.
|
|
3075
|
+
// are never seeded with a cross-provider model (e.g. antigravity + opus), which
|
|
3057
3076
|
// would blank the model <select> and be rejected by the backend.
|
|
3077
|
+
// buildProviderModelScopes prepends any CLI/env override ahead of repo
|
|
3078
|
+
// settings so `--provider` seeds the modal as the default selection.
|
|
3058
3079
|
const providersInfo = await this._getProvidersInfo();
|
|
3059
|
-
const { provider: currentProvider, model: currentModel } = window.resolveProviderModelPair(
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3080
|
+
const { provider: currentProvider, model: currentModel } = window.resolveProviderModelPair(
|
|
3081
|
+
window.buildProviderModelScopes(repoSettings, appConfig),
|
|
3082
|
+
providersInfo
|
|
3083
|
+
);
|
|
3063
3084
|
const tabStorageKey = PRManager.getRepoStorageKey('pair-review-tab', owner, repo);
|
|
3064
3085
|
const rememberedTab = localStorage.getItem(tabStorageKey);
|
|
3065
3086
|
const defaultTab = rememberedTab || repoSettings?.default_tab || 'single';
|
|
@@ -6846,13 +6867,15 @@ class PRManager {
|
|
|
6846
6867
|
const lastCouncilId = reviewSettings.last_council_id;
|
|
6847
6868
|
|
|
6848
6869
|
// Resolve provider and model as a MATCHED pair so the council/advanced tabs
|
|
6849
|
-
// are never seeded with a cross-provider model (e.g.
|
|
6870
|
+
// are never seeded with a cross-provider model (e.g. antigravity + opus), which
|
|
6850
6871
|
// would blank the model <select> and be rejected by the backend.
|
|
6872
|
+
// buildProviderModelScopes prepends any CLI/env override ahead of repo
|
|
6873
|
+
// settings so `--provider` seeds the modal as the default selection.
|
|
6851
6874
|
const providersInfo = await this._getProvidersInfo();
|
|
6852
|
-
const { provider: currentProvider, model: currentModel } = window.resolveProviderModelPair(
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6875
|
+
const { provider: currentProvider, model: currentModel } = window.resolveProviderModelPair(
|
|
6876
|
+
window.buildProviderModelScopes(repoSettings, appConfig),
|
|
6877
|
+
providersInfo
|
|
6878
|
+
);
|
|
6856
6879
|
|
|
6857
6880
|
// Determine default tab (priority: localStorage > repo settings > 'single')
|
|
6858
6881
|
const tabStorageKey = PRManager.getRepoStorageKey('pair-review-tab', owner, repo);
|
|
@@ -7021,29 +7044,34 @@ class PRManager {
|
|
|
7021
7044
|
|
|
7022
7045
|
/**
|
|
7023
7046
|
* Build the worktree-not-found recovery URL. When the user arrived via
|
|
7024
|
-
* auto-analyze (?analyze=true), the reload link preserves the
|
|
7025
|
-
*
|
|
7026
|
-
*
|
|
7027
|
-
* `
|
|
7028
|
-
*
|
|
7029
|
-
* the repo/default analysis
|
|
7047
|
+
* auto-analyze (?analyze=true), the reload link preserves the whole
|
|
7048
|
+
* auto-analyze intent bundle (analyze/analysisConfigId/council/provider/model)
|
|
7049
|
+
* so analysis re-triggers with the same selection after worktree setup.
|
|
7050
|
+
* Carrying is delegated to the shared `carryAnalyzeParams` relay so this
|
|
7051
|
+
* hop stays in lockstep with the other three (see analyze-params.js);
|
|
7052
|
+
* dropping `council` would silently fall back to the repo/default analysis
|
|
7053
|
+
* config, and dropping `provider`/`model` would lose a `--provider` override.
|
|
7030
7054
|
* @param {string} owner - Repository owner
|
|
7031
7055
|
* @param {string} repo - Repository name
|
|
7032
7056
|
* @param {number} number - PR number
|
|
7033
7057
|
* @returns {string} The recovery URL (unescaped)
|
|
7034
7058
|
*/
|
|
7035
7059
|
_buildWorktreeRecoveryUrl(owner, repo, number) {
|
|
7036
|
-
|
|
7060
|
+
// A fixed base is fine — only pathname + search are used for the link, so
|
|
7061
|
+
// the origin is irrelevant (and this avoids depending on window.location).
|
|
7062
|
+
const setupUrl = new URL(
|
|
7063
|
+
`/pr/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${encodeURIComponent(number)}`,
|
|
7064
|
+
'http://localhost'
|
|
7065
|
+
);
|
|
7037
7066
|
if (this._autoAnalyzeRequested) {
|
|
7038
|
-
|
|
7039
|
-
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
setupUrl += `?${params.toString()}`;
|
|
7067
|
+
// The current search still carries the bundle here — it isn't stripped
|
|
7068
|
+
// until _maybeAutoAnalyze's finally block, which runs after this handler.
|
|
7069
|
+
// Force analyze=true so the retry still auto-triggers even if the source
|
|
7070
|
+
// somehow lost it, then carry the rest of the bundle via the shared relay.
|
|
7071
|
+
setupUrl.searchParams.set('analyze', 'true');
|
|
7072
|
+
window.carryAnalyzeParams(window.location.search, setupUrl);
|
|
7045
7073
|
}
|
|
7046
|
-
return setupUrl;
|
|
7074
|
+
return setupUrl.pathname + setupUrl.search;
|
|
7047
7075
|
}
|
|
7048
7076
|
|
|
7049
7077
|
/**
|
|
@@ -7055,14 +7083,14 @@ class PRManager {
|
|
|
7055
7083
|
* @param {number} number - PR number
|
|
7056
7084
|
*/
|
|
7057
7085
|
showWorktreeNotFoundError(owner, repo, number) {
|
|
7058
|
-
const
|
|
7086
|
+
const href = this._buildWorktreeRecoveryUrl(owner, repo, number);
|
|
7059
7087
|
const container = document.getElementById('pr-container');
|
|
7060
7088
|
if (container) {
|
|
7061
7089
|
container.innerHTML = `
|
|
7062
7090
|
<div class="error-container">
|
|
7063
7091
|
<div class="error-icon">Warning</div>
|
|
7064
7092
|
<div class="error-message">Worktree not found. Please reload the PR to set up the worktree before running analysis.</div>
|
|
7065
|
-
<a class="btn btn-primary" href="${this.escapeHtml(
|
|
7093
|
+
<a class="btn btn-primary" href="${this.escapeHtml(href)}">Reload PR</a>
|
|
7066
7094
|
</div>
|
|
7067
7095
|
`;
|
|
7068
7096
|
container.style.display = 'block';
|
package/public/js/repo-links.js
CHANGED
|
@@ -246,9 +246,17 @@
|
|
|
246
246
|
_currentLinks = null;
|
|
247
247
|
_currentContext = context || null;
|
|
248
248
|
try {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
)
|
|
249
|
+
// Pass the PR number (when known) so the server can resolve the link set
|
|
250
|
+
// against this PR's host for a dual-host repo (github links for a
|
|
251
|
+
// github-hosted PR, the external link for an alt-hosted one). Local mode
|
|
252
|
+
// has no PR number, so the query is omitted and the server returns the
|
|
253
|
+
// repo-level default.
|
|
254
|
+
let url = '/api/repos/' + encodeURIComponent(owner) + '/' + encodeURIComponent(repo) + '/links';
|
|
255
|
+
const number = context && context.number;
|
|
256
|
+
if (number !== undefined && number !== null && number !== '') {
|
|
257
|
+
url += '?number=' + encodeURIComponent(String(number));
|
|
258
|
+
}
|
|
259
|
+
const response = await fetch(url);
|
|
252
260
|
if (!response.ok) {
|
|
253
261
|
console.warn('[repo-links] Failed to fetch repo links:', response.status);
|
|
254
262
|
return;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Shared "auto-analyze intent" URL-param relay.
|
|
4
|
+
*
|
|
5
|
+
* The params `analyze`, `analysisConfigId`, `council`, `provider`, and `model`
|
|
6
|
+
* form a single bundle that must travel TOGETHER through every browser hop
|
|
7
|
+
* between the single-port delegation URL and the review page that consumes them:
|
|
8
|
+
*
|
|
9
|
+
* delegation URL → setup.html redirect(s) → review page auto-analyze
|
|
10
|
+
* review page → "Reload PR" retry → setup.html → review page auto-analyze
|
|
11
|
+
*
|
|
12
|
+
* Each hop used to cherry-pick fields by name, so any hop that forgot one
|
|
13
|
+
* silently dropped it — which is exactly how a delegated `--provider` override
|
|
14
|
+
* got lost on its way to the running server (and how a `--council` selection
|
|
15
|
+
* would drop on the "Reload PR" retry). Centralizing the bundle here means
|
|
16
|
+
* adding a new intent param later is a one-line edit in ONE place instead of a
|
|
17
|
+
* fresh bug at every call site.
|
|
18
|
+
*
|
|
19
|
+
* Note: setup-internal params (e.g. `path`, consumed by the local-mode setup
|
|
20
|
+
* POST) are deliberately NOT in the bundle, so they never leak onto the review
|
|
21
|
+
* page URL.
|
|
22
|
+
*/
|
|
23
|
+
const ANALYZE_PARAM_KEYS = ['analyze', 'analysisConfigId', 'council', 'provider', 'model'];
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Copy the auto-analyze intent bundle from a source query onto a target URL.
|
|
27
|
+
* Only non-empty values present in the source are copied; other params already
|
|
28
|
+
* on `toUrl` are left untouched.
|
|
29
|
+
*
|
|
30
|
+
* @param {URLSearchParams|string} fromSearch - source query (e.g. window.location.search)
|
|
31
|
+
* @param {URL} toUrl - destination URL object, mutated in place
|
|
32
|
+
* @returns {URL} the same `toUrl`, for chaining
|
|
33
|
+
*/
|
|
34
|
+
function carryAnalyzeParams(fromSearch, toUrl) {
|
|
35
|
+
if (!toUrl) return toUrl;
|
|
36
|
+
const src = typeof fromSearch === 'string' ? new URLSearchParams(fromSearch) : fromSearch;
|
|
37
|
+
if (!src || typeof src.get !== 'function') return toUrl;
|
|
38
|
+
for (const key of ANALYZE_PARAM_KEYS) {
|
|
39
|
+
const value = src.get(key);
|
|
40
|
+
if (value !== null && value !== '') {
|
|
41
|
+
toUrl.searchParams.set(key, value);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return toUrl;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Delete the auto-analyze intent bundle from a URL. Used by the consumer after
|
|
49
|
+
* it acts on the intent, so a manual page refresh does not replay it.
|
|
50
|
+
*
|
|
51
|
+
* @param {URL} url - URL object, mutated in place
|
|
52
|
+
* @returns {URL} the same `url`, for chaining
|
|
53
|
+
*/
|
|
54
|
+
function stripAnalyzeParams(url) {
|
|
55
|
+
if (!url) return url;
|
|
56
|
+
for (const key of ANALYZE_PARAM_KEYS) {
|
|
57
|
+
url.searchParams.delete(key);
|
|
58
|
+
}
|
|
59
|
+
return url;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (typeof window !== 'undefined') {
|
|
63
|
+
window.carryAnalyzeParams = carryAnalyzeParams;
|
|
64
|
+
window.stripAnalyzeParams = stripAnalyzeParams;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
68
|
+
module.exports = { carryAnalyzeParams, stripAnalyzeParams, ANALYZE_PARAM_KEYS };
|
|
69
|
+
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* settings, then app config) and historically each half was resolved
|
|
7
7
|
* independently with `repo || app || hardcoded`. When a scope overrides only
|
|
8
8
|
* ONE half, the halves can come from different scopes and produce an invalid
|
|
9
|
-
* pair such as `
|
|
9
|
+
* pair such as `antigravity` + `opus` (an Anthropic model). The modal then shows no
|
|
10
10
|
* selected model, and the non-modal auto-analyze path posts the broken pair
|
|
11
11
|
* straight to the backend.
|
|
12
12
|
*
|
|
@@ -86,10 +86,75 @@ function resolveProviderModelPair(scopes, providersInfo) {
|
|
|
86
86
|
return { provider: 'claude', model: providerDefaultModel(claude) };
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Build the ordered scope list for resolveProviderModelPair, injecting the
|
|
91
|
+
* CLI/env override AHEAD of saved repo settings.
|
|
92
|
+
*
|
|
93
|
+
* The override must outrank repo settings to honor the documented
|
|
94
|
+
* `CLI/env > repo settings` contract. Folding the override into appConfig's
|
|
95
|
+
* default_provider/default_model alone is NOT enough: every seed site passes
|
|
96
|
+
* repoSettings first, so an env-aware appConfig would still lose to a repo's
|
|
97
|
+
* saved default. Prepending the override scope is the only correct fix.
|
|
98
|
+
*
|
|
99
|
+
* Two override channels, highest precedence first:
|
|
100
|
+
* 1. `extraOverride` — a per-invocation override carried on the auto-analyze
|
|
101
|
+
* URL (single-port delegation: the flag can only reach the already-running
|
|
102
|
+
* server through the URL).
|
|
103
|
+
* 2. `appConfig.provider_override` / `appConfig.model_override` — the
|
|
104
|
+
* PAIR_REVIEW_PROVIDER / PAIR_REVIEW_MODEL env override surfaced by
|
|
105
|
+
* /api/config, for the process that actually received the CLI flag.
|
|
106
|
+
*
|
|
107
|
+
* A provider-only override (`{ provider: 'codex', model: null }`) resolves via
|
|
108
|
+
* resolveProviderModelPair to codex + codex's own default model, matching CLI
|
|
109
|
+
* semantics where `--provider codex` alone pins the provider.
|
|
110
|
+
*
|
|
111
|
+
* @param {Object} repoSettings - saved repo settings row (may be null)
|
|
112
|
+
* @param {Object} [appConfig] - /api/config response (or __pairReview-shaped equivalent)
|
|
113
|
+
* @param {{provider?: string, model?: string}} [extraOverride] - per-invocation override (URL params)
|
|
114
|
+
* @returns {Array<{provider?: string, model?: string}>} ordered scopes for resolveProviderModelPair
|
|
115
|
+
*/
|
|
116
|
+
function buildProviderModelScopes(repoSettings, appConfig = {}, extraOverride = null) {
|
|
117
|
+
const cfg = appConfig || {};
|
|
118
|
+
const scopes = [];
|
|
119
|
+
// 1. Per-invocation override (delegation URL params) — highest precedence.
|
|
120
|
+
if (extraOverride && (extraOverride.provider || extraOverride.model)) {
|
|
121
|
+
scopes.push({ provider: extraOverride.provider || null, model: extraOverride.model || null });
|
|
122
|
+
}
|
|
123
|
+
// 2. CLI/env override surfaced by /api/config (non-delegated invocations).
|
|
124
|
+
if (cfg.provider_override || cfg.model_override) {
|
|
125
|
+
scopes.push({ provider: cfg.provider_override || null, model: cfg.model_override || null });
|
|
126
|
+
}
|
|
127
|
+
// 3. Saved repo settings.
|
|
128
|
+
scopes.push({ provider: repoSettings?.default_provider, model: repoSettings?.default_model });
|
|
129
|
+
// 4. App/config defaults.
|
|
130
|
+
scopes.push({ provider: cfg.default_provider, model: cfg.default_model });
|
|
131
|
+
return scopes;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Whether a CLI/env provider-or-model override is active (from either channel).
|
|
136
|
+
* Used to decide whether auto-analyze should bypass a council default and force
|
|
137
|
+
* the single-provider path (a single-provider override is incompatible with a
|
|
138
|
+
* multi-voice council).
|
|
139
|
+
*
|
|
140
|
+
* @param {Object} [appConfig] - /api/config response (or __pairReview-shaped equivalent)
|
|
141
|
+
* @param {{provider?: string, model?: string}} [extraOverride] - per-invocation override (URL params)
|
|
142
|
+
* @returns {boolean}
|
|
143
|
+
*/
|
|
144
|
+
function hasProviderModelOverride(appConfig = {}, extraOverride = null) {
|
|
145
|
+
const cfg = appConfig || {};
|
|
146
|
+
return !!(
|
|
147
|
+
(extraOverride && (extraOverride.provider || extraOverride.model)) ||
|
|
148
|
+
cfg.provider_override || cfg.model_override
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
89
152
|
if (typeof window !== 'undefined') {
|
|
90
153
|
window.resolveProviderModelPair = resolveProviderModelPair;
|
|
154
|
+
window.buildProviderModelScopes = buildProviderModelScopes;
|
|
155
|
+
window.hasProviderModelOverride = hasProviderModelOverride;
|
|
91
156
|
}
|
|
92
157
|
|
|
93
158
|
if (typeof module !== 'undefined' && module.exports) {
|
|
94
|
-
module.exports = { resolveProviderModelPair };
|
|
159
|
+
module.exports = { resolveProviderModelPair, buildProviderModelScopes, hasProviderModelOverride };
|
|
95
160
|
}
|