@in-the-loop-labs/pair-review 3.5.2 → 3.7.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 +4 -0
- 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/analysis-config.css +1807 -0
- package/public/css/pr.css +1029 -2169
- package/public/index.html +11 -0
- package/public/js/components/AIPanel.js +39 -23
- package/public/js/components/AdvancedConfigTab.js +56 -4
- package/public/js/components/AnalysisConfigModal.js +41 -25
- package/public/js/components/ChatPanel.js +163 -3
- package/public/js/components/KeyboardShortcuts.js +10 -26
- package/public/js/components/ReviewModal.js +135 -13
- package/public/js/components/TourBar.js +248 -0
- package/public/js/components/VoiceCentricConfigTab.js +36 -0
- package/public/js/index.js +175 -16
- package/public/js/local.js +64 -8
- package/public/js/modules/cancel-background-job.js +183 -0
- package/public/js/modules/hunk-summary-renderer.js +116 -0
- package/public/js/modules/storage-cleanup.js +16 -0
- package/public/js/modules/suggestion-manager.js +25 -1
- package/public/js/modules/tour-renderer.js +755 -0
- package/public/js/pr.js +1826 -56
- package/public/js/repo-links.js +328 -0
- package/public/js/utils/modal-detection.js +77 -0
- package/public/js/utils/provider-model.js +88 -0
- package/public/js/utils/storage-keys.js +50 -0
- package/public/local.html +24 -0
- package/public/pr.html +24 -0
- package/public/repo-settings.html +1 -0
- package/public/setup.html +2 -0
- package/src/ai/abort-signal-wiring.js +130 -0
- package/src/ai/analyzer.js +125 -18
- package/src/ai/background-queue.js +290 -0
- package/src/ai/claude-cli.js +1 -1
- package/src/ai/claude-provider.js +50 -7
- package/src/ai/codex-provider.js +28 -5
- package/src/ai/copilot-provider.js +22 -3
- package/src/ai/cursor-agent-provider.js +22 -6
- package/src/ai/executable-provider.js +4 -19
- package/src/ai/gemini-provider.js +22 -5
- package/src/ai/hunk-hashing.js +161 -0
- package/src/ai/index.js +2 -0
- package/src/ai/opencode-provider.js +21 -5
- package/src/ai/pi-provider.js +21 -5
- package/src/ai/prompts/hunk-summary.js +199 -0
- package/src/ai/prompts/tour.js +232 -0
- package/src/ai/provider.js +21 -1
- package/src/ai/summary-generator.js +469 -0
- package/src/ai/tour-generator.js +568 -0
- package/src/config.js +778 -10
- package/src/database.js +282 -1
- 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 +201 -172
- package/src/main.js +133 -30
- package/src/routes/analyses.js +30 -7
- package/src/routes/bulk-analysis-configs.js +295 -0
- package/src/routes/config.js +118 -3
- package/src/routes/context-files.js +2 -29
- package/src/routes/external-comments.js +20 -10
- package/src/routes/github-collections.js +3 -1
- package/src/routes/local.js +410 -13
- package/src/routes/mcp.js +47 -4
- package/src/routes/middleware/validate-review-id.js +53 -0
- package/src/routes/pr.js +556 -71
- package/src/routes/reviews.js +145 -29
- 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/src/utils/diff-hunks.js +65 -0
- package/src/utils/json-extractor.js +5 -2
package/src/routes/config.js
CHANGED
|
@@ -20,7 +20,19 @@ const {
|
|
|
20
20
|
isCheckInProgress
|
|
21
21
|
} = require('../ai');
|
|
22
22
|
const { normalizeRepository } = require('../utils/paths');
|
|
23
|
-
const {
|
|
23
|
+
const {
|
|
24
|
+
isRunningViaNpx,
|
|
25
|
+
getGitHubToken,
|
|
26
|
+
getDefaultProvider,
|
|
27
|
+
getDefaultModel,
|
|
28
|
+
resolveHostBinding,
|
|
29
|
+
resolveBindingRepositoryFromPR,
|
|
30
|
+
getSummaryEnabled,
|
|
31
|
+
getSummaryAutoGenerate,
|
|
32
|
+
getTourEnabled,
|
|
33
|
+
getTourAutoGenerate
|
|
34
|
+
} = require('../config');
|
|
35
|
+
const { resolveRepoLinks } = require('../links/repo-links');
|
|
24
36
|
const { version } = require('../../package.json');
|
|
25
37
|
const semver = require('semver');
|
|
26
38
|
const { getAllChatProviders, getAllCachedChatAvailability } = require('../chat/chat-providers');
|
|
@@ -63,10 +75,54 @@ router.get('/runtime-config.js', (req, res) => {
|
|
|
63
75
|
|
|
64
76
|
/**
|
|
65
77
|
* Get user configuration (for frontend use)
|
|
66
|
-
* Returns safe-to-expose configuration values
|
|
78
|
+
* Returns safe-to-expose configuration values.
|
|
79
|
+
*
|
|
80
|
+
* GitHub token presence is reported with two distinct fields:
|
|
81
|
+
* - has_global_github_token: always present. True iff `getGitHubToken(config)`
|
|
82
|
+
* resolves a token from the top-level config / env (no repo context).
|
|
83
|
+
* - has_github_token: present ONLY when both ?owner and ?repo query
|
|
84
|
+
* parameters are supplied. True iff a token can be resolved for that
|
|
85
|
+
* specific repository via `resolveHostBinding(repo, config)` — this
|
|
86
|
+
* covers repo-scoped `token`, `token_command`, alt-host bindings, AND
|
|
87
|
+
* falls through to the global lookup. Callers that know which repo
|
|
88
|
+
* they're rendering should pass these params so that repo-scoped
|
|
89
|
+
* authentication is reflected accurately (e.g. for deciding whether
|
|
90
|
+
* to enable GitHub-comment dedup). When the params are absent, the
|
|
91
|
+
* repo-aware field is omitted entirely — there is no safe default
|
|
92
|
+
* that doesn't risk silently meaning the wrong thing.
|
|
93
|
+
*/
|
|
94
|
+
/**
|
|
95
|
+
* Resolve a coherent default provider/model PAIR for the frontend.
|
|
96
|
+
*
|
|
97
|
+
* `default_provider` and `default_model` seed the bulk modal, auto-analyze, and
|
|
98
|
+
* the manual analyze dialog as a single selection, so they must belong together.
|
|
99
|
+
* An explicitly configured model wins (the user opted into it). When no model is
|
|
100
|
+
* configured, derive it from the selected provider's own default rather than the
|
|
101
|
+
* provider-agnostic global default — otherwise a provider-only override (e.g.
|
|
102
|
+
* `default_provider: 'gemini'`) would pair with an Anthropic model like 'opus'.
|
|
103
|
+
*
|
|
104
|
+
* @param {Object} config - Configuration object
|
|
105
|
+
* @returns {{ provider: string, model: string }}
|
|
67
106
|
*/
|
|
107
|
+
function resolveDefaultProviderModel(config) {
|
|
108
|
+
const provider = getDefaultProvider(config);
|
|
109
|
+
const providerInfo = getAllProvidersInfo().find(p => p.id === provider);
|
|
110
|
+
const explicitModel = config.default_model || config.model;
|
|
111
|
+
// Only honour an explicit model if it actually belongs to the selected provider.
|
|
112
|
+
// `DEFAULT_CONFIG.default_model` is always populated (e.g. 'opus'), so a
|
|
113
|
+
// provider-only override like `default_provider: 'gemini'` would otherwise inherit
|
|
114
|
+
// a foreign Anthropic model and return a mismatched pair. When the model does not
|
|
115
|
+
// belong to the provider, derive a coherent default from the provider itself.
|
|
116
|
+
const modelBelongs = explicitModel && providerInfo?.models?.some(m => m.id === explicitModel);
|
|
117
|
+
if (modelBelongs) {
|
|
118
|
+
return { provider, model: explicitModel };
|
|
119
|
+
}
|
|
120
|
+
return { provider, model: providerInfo?.defaultModel || getDefaultModel(config) };
|
|
121
|
+
}
|
|
122
|
+
|
|
68
123
|
router.get('/api/config', (req, res) => {
|
|
69
124
|
const config = req.app.get('config') || {};
|
|
125
|
+
const defaultPair = resolveDefaultProviderModel(config);
|
|
70
126
|
|
|
71
127
|
// Build chat_providers array with availability
|
|
72
128
|
const chatAvailability = getAllCachedChatAvailability();
|
|
@@ -74,13 +130,34 @@ router.get('/api/config', (req, res) => {
|
|
|
74
130
|
id: p.id, name: p.name, type: p.type, available: chatAvailability[p.id]?.available || false
|
|
75
131
|
}));
|
|
76
132
|
|
|
133
|
+
// Repo-aware token resolution (opt-in via ?owner & ?repo query params).
|
|
134
|
+
// Both must be non-empty strings; missing/partial params fall back to the
|
|
135
|
+
// global-only response shape.
|
|
136
|
+
const { owner, repo } = req.query;
|
|
137
|
+
const hasRepoContext = typeof owner === 'string' && owner.length > 0
|
|
138
|
+
&& typeof repo === 'string' && repo.length > 0;
|
|
139
|
+
|
|
140
|
+
const hasGlobalGithubToken = Boolean(getGitHubToken(config));
|
|
141
|
+
let hasRepoGithubToken = null;
|
|
142
|
+
if (hasRepoContext) {
|
|
143
|
+
const repository = `${owner}/${repo}`;
|
|
144
|
+
// resolveHostBinding already falls through to top-level config when
|
|
145
|
+
// no repo-scoped token is configured, so this is a true union of
|
|
146
|
+
// "repo-scoped binding works" OR "global token works".
|
|
147
|
+
hasRepoGithubToken = Boolean(resolveHostBinding(repository, config).token);
|
|
148
|
+
}
|
|
149
|
+
|
|
77
150
|
// Only return safe configuration values (not secrets like github_token)
|
|
78
151
|
res.json({
|
|
79
152
|
version,
|
|
80
153
|
theme: config.theme || 'light',
|
|
81
|
-
|
|
154
|
+
has_global_github_token: hasGlobalGithubToken,
|
|
155
|
+
// Repo-aware field — only included when owner+repo were supplied.
|
|
156
|
+
...(hasRepoContext ? { has_github_token: hasRepoGithubToken } : {}),
|
|
82
157
|
comment_button_action: config.comment_button_action || 'submit',
|
|
83
158
|
comment_format: config.comment_format || 'legacy',
|
|
159
|
+
default_provider: defaultPair.provider,
|
|
160
|
+
default_model: defaultPair.model,
|
|
84
161
|
// Include npx detection for frontend command examples
|
|
85
162
|
is_running_via_npx: isRunningViaNpx(),
|
|
86
163
|
enable_chat: config.enable_chat !== false,
|
|
@@ -93,6 +170,14 @@ router.get('/api/config', (req, res) => {
|
|
|
93
170
|
enable_graphite: config.enable_graphite === true,
|
|
94
171
|
external_comments: config.external_comments !== false,
|
|
95
172
|
chat_spinner: config.chat_spinner || 'dots',
|
|
173
|
+
summaries: {
|
|
174
|
+
enabled: getSummaryEnabled(config),
|
|
175
|
+
auto_generate: getSummaryAutoGenerate(config)
|
|
176
|
+
},
|
|
177
|
+
tours: {
|
|
178
|
+
enabled: getTourEnabled(config),
|
|
179
|
+
auto_generate: getTourAutoGenerate(config)
|
|
180
|
+
},
|
|
96
181
|
// Share configuration for external review viewers.
|
|
97
182
|
// - url: The base URL of the external share site
|
|
98
183
|
// - method: Plumbed through for future use (e.g., POST-based share flows).
|
|
@@ -200,6 +285,35 @@ router.get('/api/repos/:owner/:repo/settings', async (req, res) => {
|
|
|
200
285
|
}
|
|
201
286
|
});
|
|
202
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Get repository-specific header link configuration.
|
|
290
|
+
* Reads `config.repos["owner/repo"].links` and returns:
|
|
291
|
+
* - external: { label, url_template, icon } | null
|
|
292
|
+
* - github: boolean (false means hide the default GitHub link)
|
|
293
|
+
* - graphite: boolean (false means hide the Graphite link)
|
|
294
|
+
*
|
|
295
|
+
* The icon SVG is sanitised server-side (script tags, on* handlers, and
|
|
296
|
+
* `javascript:` URLs stripped). The url_template is NOT substituted here —
|
|
297
|
+
* the frontend has the live PR/branch context, so it performs the
|
|
298
|
+
* whitelisted substitution at render time.
|
|
299
|
+
*/
|
|
300
|
+
router.get('/api/repos/:owner/:repo/links', (req, res) => {
|
|
301
|
+
try {
|
|
302
|
+
const { owner, repo } = req.params;
|
|
303
|
+
const repository = normalizeRepository(owner, repo);
|
|
304
|
+
const config = req.app.get('config') || {};
|
|
305
|
+
// Resolve via bindingRepository so monorepo-style configs (one
|
|
306
|
+
// `repos[...]` entry serving many captured owner/repo via
|
|
307
|
+
// url_pattern) surface the right link config.
|
|
308
|
+
const bindingRepository = resolveBindingRepositoryFromPR(owner, repo, config);
|
|
309
|
+
const links = resolveRepoLinks(config, bindingRepository);
|
|
310
|
+
res.json({ repository, links });
|
|
311
|
+
} catch (error) {
|
|
312
|
+
logger.error('Error resolving repo links:', error);
|
|
313
|
+
res.status(500).json({ error: 'Failed to resolve repository links' });
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
203
317
|
/**
|
|
204
318
|
* Save repository-specific settings
|
|
205
319
|
* Saves default_instructions, default_provider, and/or default_model for the repository
|
|
@@ -410,3 +524,4 @@ function _resetPendingUpdate() {
|
|
|
410
524
|
|
|
411
525
|
module.exports = router;
|
|
412
526
|
module.exports._resetPendingUpdate = _resetPendingUpdate;
|
|
527
|
+
module.exports._resolveDefaultProviderModel = resolveDefaultProviderModel;
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const express = require('express');
|
|
14
|
-
const {
|
|
14
|
+
const { ContextFileRepository, WorktreeRepository } = require('../database');
|
|
15
15
|
const logger = require('../utils/logger');
|
|
16
16
|
const { broadcastReviewEvent } = require('../events/review-events');
|
|
17
17
|
const { getDiffFileList } = require('../utils/diff-file-list');
|
|
18
|
+
const validateReviewId = require('./middleware/validate-review-id');
|
|
18
19
|
|
|
19
20
|
const router = express.Router();
|
|
20
21
|
|
|
@@ -45,34 +46,6 @@ async function resolveRepoRoot(db, review) {
|
|
|
45
46
|
return null;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
/**
|
|
49
|
-
* Middleware: validate that :reviewId exists in the reviews table.
|
|
50
|
-
* Attaches the review record to req.review for downstream handlers.
|
|
51
|
-
*/
|
|
52
|
-
async function validateReviewId(req, res, next) {
|
|
53
|
-
try {
|
|
54
|
-
const reviewId = parseInt(req.params.reviewId, 10);
|
|
55
|
-
|
|
56
|
-
if (isNaN(reviewId) || reviewId <= 0) {
|
|
57
|
-
return res.status(400).json({ error: 'Invalid review ID' });
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const db = req.app.get('db');
|
|
61
|
-
const reviewRepo = new ReviewRepository(db);
|
|
62
|
-
const review = await reviewRepo.getReview(reviewId);
|
|
63
|
-
|
|
64
|
-
if (!review) {
|
|
65
|
-
return res.status(404).json({ error: `Review #${reviewId} not found` });
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
req.review = review;
|
|
69
|
-
req.reviewId = reviewId;
|
|
70
|
-
next();
|
|
71
|
-
} catch (error) {
|
|
72
|
-
next(error);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
49
|
/**
|
|
77
50
|
* POST /api/reviews/:reviewId/context-files
|
|
78
51
|
* Add a context file range for a review.
|
|
@@ -34,8 +34,9 @@ const router = express.Router();
|
|
|
34
34
|
* Default dependencies for the sync flow. Tests override these via the
|
|
35
35
|
* `externalCommentsDeps` Express app setting (or by passing `_deps` to
|
|
36
36
|
* `executeSync` directly). Credential resolution is delegated to the
|
|
37
|
-
* adapter via `adapter.resolveCredentials(config)` — keeps the
|
|
38
|
-
* source-agnostic
|
|
37
|
+
* adapter via `adapter.resolveCredentials(config, repository)` — keeps the
|
|
38
|
+
* route source-agnostic, lets each adapter name its own env var in errors,
|
|
39
|
+
* and threads the repo through so per-repo alt-host bindings apply.
|
|
39
40
|
*/
|
|
40
41
|
const defaults = {
|
|
41
42
|
getAdapter
|
|
@@ -115,7 +116,7 @@ function isPRMode(review) {
|
|
|
115
116
|
* @param {Object} params.config - Server config (for token lookup)
|
|
116
117
|
* @param {Object} params.review - Validated review row
|
|
117
118
|
* @param {string} params.source - Adapter source name (e.g. 'github')
|
|
118
|
-
* @param {Object} [params._deps] - Test overrides for { GitHubClient, getGitHubToken, getAdapter }
|
|
119
|
+
* @param {Object} [params._deps] - Test overrides for { GitHubClient, getGitHubToken, getAdapter, resolveHostBinding, resolveBindingRepositoryFromPR }
|
|
119
120
|
* @returns {Promise<{count: number, lostAnchors: number, syncedAt: string}>}
|
|
120
121
|
*/
|
|
121
122
|
async function executeSync({ db, config, review, source, _deps }) {
|
|
@@ -124,12 +125,9 @@ async function executeSync({ db, config, review, source, _deps }) {
|
|
|
124
125
|
// Look up adapter — throws on unknown sources, caught by the route.
|
|
125
126
|
const adapter = deps.getAdapter(source);
|
|
126
127
|
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
// missing — the route's catch maps it to a 401 response.
|
|
131
|
-
const { client } = adapter.resolveCredentials(config || {}, _deps);
|
|
132
|
-
|
|
128
|
+
// Parse owner/repo BEFORE resolving credentials: the repository drives
|
|
129
|
+
// binding-aware credential resolution (per-repo api_host/token for
|
|
130
|
+
// alt-host repos), so it must be validated first.
|
|
133
131
|
const [owner, repo] = String(review.repository).split('/');
|
|
134
132
|
if (!owner || !repo) {
|
|
135
133
|
throw new BadRequestError(
|
|
@@ -137,6 +135,18 @@ async function executeSync({ db, config, review, source, _deps }) {
|
|
|
137
135
|
);
|
|
138
136
|
}
|
|
139
137
|
|
|
138
|
+
// Delegate credential resolution to the adapter so the route stays
|
|
139
|
+
// source-agnostic and each adapter can name its own env var in errors.
|
|
140
|
+
// Thread `review.repository` through so the adapter resolves the
|
|
141
|
+
// repo-scoped host binding (alt-host api_host + repo token) instead of
|
|
142
|
+
// always targeting api.github.com with the top-level github.com token.
|
|
143
|
+
// The adapter throws (e.g. GitHubApiError 401) when credentials are
|
|
144
|
+
// missing — the route's catch maps it to a 401 response.
|
|
145
|
+
// `isAltHost` reflects whether the resolved binding targets an alternate
|
|
146
|
+
// Git host. Alt-hosts don't implement GitHub's deprecated `position`
|
|
147
|
+
// field, so it drives line-based anchoring in `mapComment` below.
|
|
148
|
+
const { client, isAltHost } = adapter.resolveCredentials(config || {}, review.repository, _deps);
|
|
149
|
+
|
|
140
150
|
const apiRows = await adapter.fetchComments({
|
|
141
151
|
client,
|
|
142
152
|
owner,
|
|
@@ -156,7 +166,7 @@ async function executeSync({ db, config, review, source, _deps }) {
|
|
|
156
166
|
for (const apiRow of apiRows || []) {
|
|
157
167
|
let mapped;
|
|
158
168
|
try {
|
|
159
|
-
mapped = adapter.mapComment(apiRow);
|
|
169
|
+
mapped = adapter.mapComment(apiRow, { isAltHost });
|
|
160
170
|
} catch (mapError) {
|
|
161
171
|
// A malformed row from the source shouldn't kill the whole sync — log
|
|
162
172
|
// it and keep going. The adapter only throws for genuinely malformed
|
|
@@ -161,7 +161,9 @@ function registerCollection(def) {
|
|
|
161
161
|
}
|
|
162
162
|
|
|
163
163
|
const config = req.app.get('config');
|
|
164
|
-
|
|
164
|
+
// Cross-repo search on github.com — no per-repo binding applies here.
|
|
165
|
+
// Explicit `undefined` repository selects the no-repo (top-level) path.
|
|
166
|
+
const githubToken = getGitHubToken(config, undefined);
|
|
165
167
|
if (!githubToken) {
|
|
166
168
|
return res.status(401).json({ success: false, error: 'GitHub token not configured' });
|
|
167
169
|
}
|