@in-the-loop-labs/pair-review 3.1.2 → 3.1.4
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 +86 -2
- package/public/js/components/AIPanel.js +7 -4
- package/public/js/components/ChatPanel.js +34 -4
- package/public/js/components/CouncilProgressModal.js +3 -0
- package/public/js/components/DiffOptionsDropdown.js +93 -19
- package/public/js/components/SuggestionNavigator.js +2 -0
- package/public/js/modules/comment-manager.js +7 -0
- package/public/js/modules/comment-minimizer.js +151 -4
- package/public/js/modules/file-comment-manager.js +66 -2
- package/public/js/modules/suggestion-manager.js +2 -1
- package/public/js/pr.js +13 -0
- package/src/ai/analyzer.js +21 -3
- package/src/ai/claude-provider.js +1 -11
- package/src/ai/codex-provider.js +18 -16
- package/src/ai/copilot-provider.js +21 -21
- package/src/ai/gemini-provider.js +10 -0
- package/src/ai/pi-provider.js +22 -25
- package/src/ai/provider.js +26 -3
- package/src/chat/pi-bridge.js +8 -0
- package/src/chat/prompt-builder.js +2 -3
- package/src/chat/session-manager.js +1 -0
- package/src/config.js +1 -0
- package/src/database.js +31 -1
- package/src/local-review.js +21 -30
- package/src/local-scope.js +31 -23
- package/src/routes/executable-analysis.js +11 -9
- package/src/routes/local.js +52 -50
- package/src/routes/setup.js +4 -3
- package/src/setup/local-setup.js +8 -6
package/src/routes/local.js
CHANGED
|
@@ -26,7 +26,7 @@ const { buildReviewStartedPayload, buildReviewLoadedPayload, buildAnalysisStarte
|
|
|
26
26
|
const { mergeInstructions } = require('../utils/instructions');
|
|
27
27
|
const { getGitHubToken } = require('../config');
|
|
28
28
|
const { generateScopedDiff, computeScopedDigest, getBranchCommitCount, getFirstCommitSubject, detectAndBuildBranchInfo, findMergeBase, getCurrentBranch, getRepositoryName } = require('../local-review');
|
|
29
|
-
const { STOPS, isValidScope,
|
|
29
|
+
const { STOPS, isValidScope, normalizeScope, reviewScope, includesBranch, DEFAULT_SCOPE } = require('../local-scope');
|
|
30
30
|
const { getGeneratedFilePatterns } = require('../git/gitattributes');
|
|
31
31
|
const { getShaAbbrevLength } = require('../git/sha-abbrev');
|
|
32
32
|
const { validateCouncilConfig, normalizeCouncilConfig } = require('./councils');
|
|
@@ -77,9 +77,10 @@ function deleteLocalReviewDiff(reviewId) {
|
|
|
77
77
|
* Returns true if the guard fired (response already sent), false otherwise.
|
|
78
78
|
*/
|
|
79
79
|
async function rejectIfEmptyScope(res, review, localPath) {
|
|
80
|
+
const { start: scopeStart, end: scopeEnd } = reviewScope(review);
|
|
80
81
|
const scopeContext = {
|
|
81
|
-
scopeStart
|
|
82
|
-
scopeEnd
|
|
82
|
+
scopeStart,
|
|
83
|
+
scopeEnd,
|
|
83
84
|
baseBranch: review.local_base_branch || null,
|
|
84
85
|
};
|
|
85
86
|
const changedFiles = await getChangedFiles(localPath, scopeContext);
|
|
@@ -461,8 +462,7 @@ router.post('/api/local/start', async (req, res) => {
|
|
|
461
462
|
const config = req.app.get('config') || {};
|
|
462
463
|
// Generate diff using default scope
|
|
463
464
|
logger.log('API', `Starting local review for ${repoPath}`, 'cyan');
|
|
464
|
-
const scopeStart = existing
|
|
465
|
-
const scopeEnd = existing?.local_scope_end || DEFAULT_SCOPE.end;
|
|
465
|
+
const { start: scopeStart, end: scopeEnd } = existing ? reviewScope(existing) : DEFAULT_SCOPE;
|
|
466
466
|
|
|
467
467
|
// Fire review hook (non-blocking, after scope is resolved)
|
|
468
468
|
const hookEvent = existing ? 'review.loaded' : 'review.started';
|
|
@@ -580,9 +580,11 @@ router.get('/api/local/:reviewId', async (req, res) => {
|
|
|
580
580
|
}
|
|
581
581
|
}
|
|
582
582
|
|
|
583
|
-
// Build scope info for the response
|
|
584
|
-
|
|
585
|
-
|
|
583
|
+
// Build scope info for the response.
|
|
584
|
+
// normalizeScope clamps any legacy invalid scopes (e.g. branch-only,
|
|
585
|
+
// staged-only) to always include 'unstaged', since AI models read files
|
|
586
|
+
// from the working tree and the diff must match what they see.
|
|
587
|
+
const { start: scopeStart, end: scopeEnd } = reviewScope(review);
|
|
586
588
|
const baseBranch = review.local_base_branch || null;
|
|
587
589
|
|
|
588
590
|
// When scope does NOT include branch, check for branch detection info
|
|
@@ -693,8 +695,7 @@ router.get('/api/local/:reviewId', async (req, res) => {
|
|
|
693
695
|
const hookConfig = req.app.get('config') || {};
|
|
694
696
|
if (hasHooks('review.loaded', hookConfig)) {
|
|
695
697
|
getCachedUser(hookConfig).then(user => {
|
|
696
|
-
const hookScopeStart = review
|
|
697
|
-
const hookScopeEnd = review.local_scope_end || DEFAULT_SCOPE.end;
|
|
698
|
+
const { start: hookScopeStart, end: hookScopeEnd } = reviewScope(review);
|
|
698
699
|
const si = STOPS.indexOf(hookScopeStart);
|
|
699
700
|
const ei = STOPS.indexOf(hookScopeEnd);
|
|
700
701
|
const scope = STOPS.slice(si, ei + 1);
|
|
@@ -787,8 +788,7 @@ router.get('/api/local/:reviewId/diff', async (req, res) => {
|
|
|
787
788
|
// When ?w=1 or ?base=<branch>, regenerate the diff (transient view, not cached)
|
|
788
789
|
const hideWhitespace = req.query.w === '1';
|
|
789
790
|
const baseBranchOverride = req.query.base;
|
|
790
|
-
const scopeStart = review
|
|
791
|
-
const scopeEnd = review.local_scope_end || DEFAULT_SCOPE.end;
|
|
791
|
+
const { start: scopeStart, end: scopeEnd } = reviewScope(review);
|
|
792
792
|
const baseBranch = baseBranchOverride || review.local_base_branch;
|
|
793
793
|
let diffData;
|
|
794
794
|
|
|
@@ -900,8 +900,7 @@ router.get('/api/local/:reviewId/check-stale', async (req, res) => {
|
|
|
900
900
|
});
|
|
901
901
|
}
|
|
902
902
|
|
|
903
|
-
const scopeStart = review
|
|
904
|
-
const scopeEnd = review.local_scope_end || DEFAULT_SCOPE.end;
|
|
903
|
+
const { start: scopeStart, end: scopeEnd } = reviewScope(review);
|
|
905
904
|
|
|
906
905
|
// Always check HEAD SHA for supplementary fields
|
|
907
906
|
let headShaChanged = false;
|
|
@@ -1042,19 +1041,22 @@ async function handleExecutableAnalysis(req, res, {
|
|
|
1042
1041
|
registerProcessForCancellation
|
|
1043
1042
|
}, {
|
|
1044
1043
|
logLabel: `Review #${reviewId}`,
|
|
1045
|
-
buildContext: (r, { selectedModel: model, requestInstructions: customInstructions }) =>
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1044
|
+
buildContext: (r, { selectedModel: model, requestInstructions: customInstructions }) => {
|
|
1045
|
+
const { start: scopeStart, end: scopeEnd } = reviewScope(r);
|
|
1046
|
+
return {
|
|
1047
|
+
title: null,
|
|
1048
|
+
description: null,
|
|
1049
|
+
cwd: localPath,
|
|
1050
|
+
model,
|
|
1051
|
+
baseSha: null,
|
|
1052
|
+
headSha: r.local_head_sha || null,
|
|
1053
|
+
baseBranch: r.local_base_branch || null,
|
|
1054
|
+
headBranch: r.local_head_branch || null,
|
|
1055
|
+
scopeStart,
|
|
1056
|
+
scopeEnd,
|
|
1057
|
+
customInstructions: customInstructions || null
|
|
1058
|
+
};
|
|
1059
|
+
},
|
|
1058
1060
|
buildHookPayload: () => ({
|
|
1059
1061
|
mode: review.review_type || 'local',
|
|
1060
1062
|
localContext: { path: localPath, branch: review.local_head_branch, headSha: review.local_head_sha }
|
|
@@ -1185,8 +1187,7 @@ router.post('/api/local/:reviewId/analyses', async (req, res) => {
|
|
|
1185
1187
|
}
|
|
1186
1188
|
|
|
1187
1189
|
// Extract scope early — needed for both analysis run creation and diff generation
|
|
1188
|
-
const scopeStart = review
|
|
1189
|
-
const scopeEnd = review.local_scope_end || DEFAULT_SCOPE.end;
|
|
1190
|
+
const { start: scopeStart, end: scopeEnd } = reviewScope(review);
|
|
1190
1191
|
|
|
1191
1192
|
// Create DB analysis_runs record immediately so it's queryable for polling
|
|
1192
1193
|
const analysisRunRepo = new AnalysisRunRepository(db);
|
|
@@ -1281,13 +1282,14 @@ router.post('/api/local/:reviewId/analyses', async (req, res) => {
|
|
|
1281
1282
|
reviewType: 'local'
|
|
1282
1283
|
};
|
|
1283
1284
|
|
|
1284
|
-
// Get changed files for local mode path validation
|
|
1285
|
-
//
|
|
1286
|
-
//
|
|
1287
|
-
const
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
:
|
|
1285
|
+
// Get changed files for local mode path validation.
|
|
1286
|
+
// Use the scope-aware helper so the file list matches the generated diff
|
|
1287
|
+
// (covers branch, staged, unstaged, and untracked stops as appropriate).
|
|
1288
|
+
const changedFiles = await getChangedFiles(localPath, {
|
|
1289
|
+
scopeStart,
|
|
1290
|
+
scopeEnd,
|
|
1291
|
+
baseBranch: review.local_base_branch || null,
|
|
1292
|
+
});
|
|
1291
1293
|
|
|
1292
1294
|
// Log analysis start
|
|
1293
1295
|
logger.section(`Local AI Analysis Request - Review #${reviewId}`);
|
|
@@ -1297,7 +1299,7 @@ router.post('/api/local/:reviewId/analyses', async (req, res) => {
|
|
|
1297
1299
|
logger.log('API', `Provider: ${selectedProvider}`, 'cyan');
|
|
1298
1300
|
logger.log('API', `Model: ${selectedModel}`, 'cyan');
|
|
1299
1301
|
logger.log('API', `Tier: ${tier}`, 'cyan');
|
|
1300
|
-
logger.log('API', `Changed files: ${changedFiles
|
|
1302
|
+
logger.log('API', `Changed files: ${changedFiles.length}`, 'cyan');
|
|
1301
1303
|
if (combinedInstructions) {
|
|
1302
1304
|
logger.log('API', `Custom instructions: ${combinedInstructions.length} chars`, 'cyan');
|
|
1303
1305
|
}
|
|
@@ -1508,8 +1510,7 @@ router.post('/api/local/:reviewId/refresh', async (req, res) => {
|
|
|
1508
1510
|
|
|
1509
1511
|
// Check if HEAD has changed
|
|
1510
1512
|
const { getHeadSha } = require('../local-review');
|
|
1511
|
-
const scopeStart = review
|
|
1512
|
-
const scopeEnd = review.local_scope_end || DEFAULT_SCOPE.end;
|
|
1513
|
+
const { start: scopeStart, end: scopeEnd } = reviewScope(review);
|
|
1513
1514
|
const hasBranch = includesBranch(scopeStart);
|
|
1514
1515
|
let currentHeadSha;
|
|
1515
1516
|
let headShaChanged = false;
|
|
@@ -1627,8 +1628,7 @@ router.post('/api/local/:reviewId/resolve-head-change', async (req, res) => {
|
|
|
1627
1628
|
return res.status(400).json({ error: 'Local review is missing path information' });
|
|
1628
1629
|
}
|
|
1629
1630
|
|
|
1630
|
-
const scopeStart = review
|
|
1631
|
-
const scopeEnd = review.local_scope_end || DEFAULT_SCOPE.end;
|
|
1631
|
+
const { start: scopeStart, end: scopeEnd } = reviewScope(review);
|
|
1632
1632
|
|
|
1633
1633
|
if (action === 'update') {
|
|
1634
1634
|
// Read live branch — may differ from stored value after a checkout.
|
|
@@ -1788,7 +1788,7 @@ router.post('/api/local/:reviewId/set-scope', async (req, res) => {
|
|
|
1788
1788
|
await reviewRepo.updateLocalHeadSha(reviewId, headSha);
|
|
1789
1789
|
|
|
1790
1790
|
// Auto-name review from first commit subject when branch is newly in scope
|
|
1791
|
-
const oldScopeStart = review
|
|
1791
|
+
const { start: oldScopeStart } = reviewScope(review);
|
|
1792
1792
|
if (!review.name && includesBranch(scopeStart) && !includesBranch(oldScopeStart) && baseBranch) {
|
|
1793
1793
|
const firstSubject = await getFirstCommitSubject(localPath, baseBranch);
|
|
1794
1794
|
if (firstSubject) {
|
|
@@ -2021,8 +2021,7 @@ router.post('/api/local/:reviewId/analyses/council', async (req, res) => {
|
|
|
2021
2021
|
// Guard: reject if scope resolves to zero changed files
|
|
2022
2022
|
if (await rejectIfEmptyScope(res, review, localPath)) return;
|
|
2023
2023
|
|
|
2024
|
-
const councilScopeStart = review
|
|
2025
|
-
const councilScopeEnd = review.local_scope_end || DEFAULT_SCOPE.end;
|
|
2024
|
+
const { start: councilScopeStart, end: councilScopeEnd } = reviewScope(review);
|
|
2026
2025
|
const councilHasBranch = includesBranch(councilScopeStart);
|
|
2027
2026
|
|
|
2028
2027
|
// Compute merge-base when branch is in scope
|
|
@@ -2038,8 +2037,8 @@ router.post('/api/local/:reviewId/analyses/council', async (req, res) => {
|
|
|
2038
2037
|
const prMetadata = {
|
|
2039
2038
|
reviewType: 'local',
|
|
2040
2039
|
repository: review.repository,
|
|
2041
|
-
title:
|
|
2042
|
-
description:
|
|
2040
|
+
title: null,
|
|
2041
|
+
description: null,
|
|
2043
2042
|
base_sha: analysisBaseSha,
|
|
2044
2043
|
head_sha: review.local_head_sha,
|
|
2045
2044
|
base_branch: review.local_base_branch || null,
|
|
@@ -2049,10 +2048,13 @@ router.post('/api/local/:reviewId/analyses/council', async (req, res) => {
|
|
|
2049
2048
|
};
|
|
2050
2049
|
|
|
2051
2050
|
const analyzer = new Analyzer(db, 'council', 'council');
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
:
|
|
2051
|
+
// Use the scope-aware helper so the file list matches the generated diff
|
|
2052
|
+
// (covers branch, staged, unstaged, and untracked stops as appropriate).
|
|
2053
|
+
const changedFiles = await getChangedFiles(localPath, {
|
|
2054
|
+
scopeStart: councilScopeStart,
|
|
2055
|
+
scopeEnd: councilScopeEnd,
|
|
2056
|
+
baseBranch: review.local_base_branch || null,
|
|
2057
|
+
});
|
|
2056
2058
|
|
|
2057
2059
|
// Generate and cache diff
|
|
2058
2060
|
try {
|
package/src/routes/setup.js
CHANGED
|
@@ -15,7 +15,7 @@ const crypto = require('crypto');
|
|
|
15
15
|
const { activeSetups, broadcastSetupProgress } = require('./shared');
|
|
16
16
|
const { setupPRReview } = require('../setup/pr-setup');
|
|
17
17
|
const { setupLocalReview } = require('../setup/local-setup');
|
|
18
|
-
const { getGitHubToken } = require('../config');
|
|
18
|
+
const { getGitHubToken, expandPath } = require('../config');
|
|
19
19
|
const { queryOne } = require('../database');
|
|
20
20
|
const { normalizeRepository } = require('../utils/paths');
|
|
21
21
|
const logger = require('../utils/logger');
|
|
@@ -143,12 +143,13 @@ router.post('/api/setup/pr/:owner/:repo/:number', async (req, res) => {
|
|
|
143
143
|
*/
|
|
144
144
|
router.post('/api/setup/local', async (req, res) => {
|
|
145
145
|
try {
|
|
146
|
-
const { path:
|
|
146
|
+
const { path: rawPath } = req.body;
|
|
147
147
|
|
|
148
|
-
if (!
|
|
148
|
+
if (!rawPath) {
|
|
149
149
|
return res.status(400).json({ error: 'Missing required field: path' });
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
const targetPath = expandPath(rawPath);
|
|
152
153
|
const db = req.app.get('db');
|
|
153
154
|
|
|
154
155
|
// Concurrency guard
|
package/src/setup/local-setup.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
const { findGitRoot, getHeadSha, getCurrentBranch, getRepositoryName,
|
|
2
|
+
const { findGitRoot, getHeadSha, getCurrentBranch, getRepositoryName, generateScopedDiff, generateLocalReviewId, computeScopedDigest, findMainGitRoot } = require('../local-review');
|
|
3
3
|
const { ReviewRepository, RepoSettingsRepository } = require('../database');
|
|
4
4
|
const { localReviewDiffs } = require('../routes/shared');
|
|
5
5
|
const { fireHooks, hasHooks } = require('../hooks/hook-runner');
|
|
6
6
|
const { buildReviewStartedPayload, buildReviewLoadedPayload, getCachedUser } = require('../hooks/payloads');
|
|
7
|
-
const { STOPS, DEFAULT_SCOPE } = require('../local-scope');
|
|
7
|
+
const { STOPS, DEFAULT_SCOPE, reviewScope } = require('../local-scope');
|
|
8
8
|
const logger = require('../utils/logger');
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const fs = require('fs').promises;
|
|
@@ -102,13 +102,16 @@ async function setupLocalReview({ db, targetPath, onProgress, config }) {
|
|
|
102
102
|
// ── Step: diff ──────────────────────────────────────────────────────
|
|
103
103
|
let diff, stats, digest;
|
|
104
104
|
try {
|
|
105
|
+
const { start: scopeStart, end: scopeEnd } = existingReview ? reviewScope(existingReview) : DEFAULT_SCOPE;
|
|
106
|
+
const baseBranch = existingReview?.local_base_branch || null;
|
|
107
|
+
|
|
105
108
|
progress({ step: 'diff', status: 'running', message: 'Generating diff for local changes...' });
|
|
106
109
|
|
|
107
|
-
const diffResult = await
|
|
110
|
+
const diffResult = await generateScopedDiff(repoPath, scopeStart, scopeEnd, baseBranch);
|
|
108
111
|
diff = diffResult.diff;
|
|
109
112
|
stats = diffResult.stats;
|
|
110
113
|
|
|
111
|
-
digest = await
|
|
114
|
+
digest = await computeScopedDigest(repoPath, scopeStart, scopeEnd);
|
|
112
115
|
|
|
113
116
|
progress({ step: 'diff', status: 'completed', message: `Diff ready: ${stats.unstagedChanges} unstaged, ${stats.untrackedFiles} untracked` });
|
|
114
117
|
} catch (err) {
|
|
@@ -152,8 +155,7 @@ async function setupLocalReview({ db, targetPath, onProgress, config }) {
|
|
|
152
155
|
if (config && hasHooks(hookEvent, config)) {
|
|
153
156
|
getCachedUser(config).then(user => {
|
|
154
157
|
const builder = existingReview ? buildReviewLoadedPayload : buildReviewStartedPayload;
|
|
155
|
-
const scopeStart = existingReview
|
|
156
|
-
const scopeEnd = existingReview?.local_scope_end || DEFAULT_SCOPE.end;
|
|
158
|
+
const { start: scopeStart, end: scopeEnd } = existingReview ? reviewScope(existingReview) : DEFAULT_SCOPE;
|
|
157
159
|
const si = STOPS.indexOf(scopeStart);
|
|
158
160
|
const ei = STOPS.indexOf(scopeEnd);
|
|
159
161
|
const scope = STOPS.slice(si, ei + 1);
|