@in-the-loop-labs/pair-review 3.5.2 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +15 -20
- 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/pr.css +603 -6
- package/public/js/components/ChatPanel.js +163 -3
- package/public/js/components/KeyboardShortcuts.js +10 -26
- package/public/js/components/TourBar.js +248 -0
- package/public/js/local.js +6 -0
- 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/tour-renderer.js +725 -0
- package/public/js/pr.js +1276 -2
- package/public/js/utils/modal-detection.js +77 -0
- package/public/local.html +17 -0
- package/public/pr.html +17 -0
- package/src/ai/abort-signal-wiring.js +130 -0
- 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 +114 -0
- package/src/database.js +282 -1
- package/src/local-review.js +189 -169
- package/src/routes/config.js +16 -1
- package/src/routes/context-files.js +2 -29
- package/src/routes/local.js +311 -4
- package/src/routes/middleware/validate-review-id.js +53 -0
- package/src/routes/pr.js +259 -4
- package/src/routes/reviews.js +145 -29
- package/src/utils/diff-hunks.js +65 -0
- package/src/utils/json-extractor.js +5 -2
package/src/local-review.js
CHANGED
|
@@ -15,6 +15,8 @@ const { STOPS, scopeIncludes, includesBranch, DEFAULT_SCOPE, scopeLabel, reviewS
|
|
|
15
15
|
const { initializeDatabase, ReviewRepository, RepoSettingsRepository } = require('./database');
|
|
16
16
|
const { startServer } = require('./server');
|
|
17
17
|
const { localReviewDiffs } = require('./routes/shared');
|
|
18
|
+
const summaryGenerator = require('./ai/summary-generator');
|
|
19
|
+
const tourGenerator = require('./ai/tour-generator');
|
|
18
20
|
const { getShaAbbrevLength } = require('./git/sha-abbrev');
|
|
19
21
|
const { GIT_DIFF_FLAGS, GIT_DIFF_FLAGS_ARRAY } = require('./git/diff-flags');
|
|
20
22
|
const open = (...args) => process.env.PAIR_REVIEW_NO_OPEN ? Promise.resolve() : import('open').then(({ default: open }) => open(...args));
|
|
@@ -690,6 +692,183 @@ async function generateLocalDiff(repoPath, options = {}) {
|
|
|
690
692
|
};
|
|
691
693
|
}
|
|
692
694
|
|
|
695
|
+
/**
|
|
696
|
+
* Set up a local review session: resolve git state, persist the diff,
|
|
697
|
+
* and enqueue the background summary job. Caller is responsible for
|
|
698
|
+
* starting the server and opening the browser.
|
|
699
|
+
*
|
|
700
|
+
* @param {Object} params
|
|
701
|
+
* @param {Object} params.db - Database instance
|
|
702
|
+
* @param {Object} params.config - Loaded config
|
|
703
|
+
* @param {string} params.repoPath - Path to the working tree (already validated)
|
|
704
|
+
* @param {Object} [params.flags] - CLI flags / options
|
|
705
|
+
* @returns {Promise<{sessionId: number, repoPath: string, branch: string, repository: string, headSha: string, diff: string, stats: Object, digest: string|null, branchInfo: Object|null, scopeStart: string, scopeEnd: string, baseBranch: string|null}>}
|
|
706
|
+
*/
|
|
707
|
+
async function setupLocalReviewSession({ db, config, repoPath, flags = {} }) {
|
|
708
|
+
const headSha = await module.exports.getHeadSha(repoPath);
|
|
709
|
+
console.log(`HEAD SHA: ${headSha}`);
|
|
710
|
+
|
|
711
|
+
const branch = await module.exports.getCurrentBranch(repoPath);
|
|
712
|
+
console.log(`Current branch: ${branch}`);
|
|
713
|
+
|
|
714
|
+
const reviewId = generateLocalReviewId(repoPath, headSha);
|
|
715
|
+
console.log(`Local review ID: ${reviewId}`);
|
|
716
|
+
|
|
717
|
+
const reviewRepo = new ReviewRepository(db);
|
|
718
|
+
const repository = await module.exports.getRepositoryName(repoPath);
|
|
719
|
+
console.log(`Repository: ${repository}`);
|
|
720
|
+
|
|
721
|
+
if (repository.includes('/')) {
|
|
722
|
+
try {
|
|
723
|
+
const mainRepoRoot = await module.exports.findMainGitRoot(repoPath);
|
|
724
|
+
const repoSettingsRepo = new RepoSettingsRepository(db);
|
|
725
|
+
await repoSettingsRepo.setLocalPath(repository, mainRepoRoot);
|
|
726
|
+
console.log(`Registered repository location: ${mainRepoRoot}`);
|
|
727
|
+
} catch (error) {
|
|
728
|
+
console.warn(`Could not register repository location: ${error.message}`);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
console.log('Checking for existing review session...');
|
|
733
|
+
let existingReview = await reviewRepo.getLocalReview(repoPath, headSha, branch);
|
|
734
|
+
|
|
735
|
+
if (!existingReview) {
|
|
736
|
+
const legacy = await reviewRepo.getLocalReviewByPathAndSha(repoPath, headSha);
|
|
737
|
+
if (legacy && legacy.local_head_branch === null) {
|
|
738
|
+
existingReview = legacy;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
if (!existingReview) {
|
|
743
|
+
const branchSession = await reviewRepo.getLocalBranchReview(repoPath, branch);
|
|
744
|
+
if (branchSession) {
|
|
745
|
+
existingReview = branchSession;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
let sessionId;
|
|
750
|
+
if (existingReview) {
|
|
751
|
+
sessionId = existingReview.id;
|
|
752
|
+
if (existingReview.local_head_sha !== headSha) {
|
|
753
|
+
await reviewRepo.updateLocalHeadSha(sessionId, headSha);
|
|
754
|
+
const abbrevLen = getShaAbbrevLength(repoPath);
|
|
755
|
+
console.log(`Updated HEAD SHA on session ${sessionId}: ${existingReview.local_head_sha.substring(0, abbrevLen)} -> ${headSha.substring(0, abbrevLen)}`);
|
|
756
|
+
}
|
|
757
|
+
if (existingReview.local_head_branch === null) {
|
|
758
|
+
await reviewRepo.updateReview(sessionId, { local_head_branch: branch });
|
|
759
|
+
console.log(`Backfilled branch on session ${sessionId}: ${branch}`);
|
|
760
|
+
}
|
|
761
|
+
console.log(`Resuming existing review session (ID: ${existingReview.id})`);
|
|
762
|
+
} else {
|
|
763
|
+
console.log('Creating new review session...');
|
|
764
|
+
sessionId = await reviewRepo.upsertLocalReview({
|
|
765
|
+
localPath: repoPath,
|
|
766
|
+
localHeadSha: headSha,
|
|
767
|
+
repository,
|
|
768
|
+
localHeadBranch: branch
|
|
769
|
+
});
|
|
770
|
+
console.log(`Created new review session (ID: ${sessionId})`);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const { start: scopeStart, end: scopeEnd } = existingReview ? reviewScope(existingReview) : DEFAULT_SCOPE;
|
|
774
|
+
|
|
775
|
+
const hookEvent = existingReview ? 'review.loaded' : 'review.started';
|
|
776
|
+
if (hasHooks(hookEvent, config)) {
|
|
777
|
+
getCachedUser(config).then(user => {
|
|
778
|
+
const builder = existingReview ? buildReviewLoadedPayload : buildReviewStartedPayload;
|
|
779
|
+
const si = STOPS.indexOf(scopeStart);
|
|
780
|
+
const ei = STOPS.indexOf(scopeEnd);
|
|
781
|
+
const scope = STOPS.slice(si, ei + 1);
|
|
782
|
+
const payload = builder({ reviewId: sessionId, mode: 'local', localContext: { path: repoPath, branch, headSha, scope }, user });
|
|
783
|
+
fireHooks(hookEvent, payload, config);
|
|
784
|
+
}).catch(err => { logger.warn(`Review hook failed: ${err.message}`); });
|
|
785
|
+
}
|
|
786
|
+
const baseBranch = existingReview?.local_base_branch || null;
|
|
787
|
+
|
|
788
|
+
console.log(`Generating diff for scope: ${scopeLabel(scopeStart, scopeEnd)}...`);
|
|
789
|
+
const { diff, stats } = await module.exports.generateScopedDiff(repoPath, scopeStart, scopeEnd, baseBranch);
|
|
790
|
+
|
|
791
|
+
let branchInfo = null;
|
|
792
|
+
if (!includesBranch(scopeStart)) {
|
|
793
|
+
const untrackedFiles = await getUntrackedFiles(repoPath);
|
|
794
|
+
branchInfo = await module.exports.detectAndBuildBranchInfo(repoPath, branch, {
|
|
795
|
+
repository,
|
|
796
|
+
diff,
|
|
797
|
+
untrackedFiles,
|
|
798
|
+
githubToken: getGitHubToken(config),
|
|
799
|
+
enableGraphite: config.enable_graphite === true
|
|
800
|
+
});
|
|
801
|
+
if (branchInfo) {
|
|
802
|
+
console.log(`\nNo uncommitted changes, but branch has ${branchInfo.commitCount} commit(s) ahead of ${branchInfo.baseBranch}.`);
|
|
803
|
+
console.log('The UI will offer to review branch changes.');
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (!diff && !branchInfo) {
|
|
808
|
+
console.log('\nNo changes detected in current scope. The UI will open anyway - you can change scope or make changes and refresh.');
|
|
809
|
+
} else if (diff) {
|
|
810
|
+
console.log(`Found ${stats.trackedChanges || 0} file(s) changed`);
|
|
811
|
+
if (stats.untrackedFiles > 0) {
|
|
812
|
+
console.log(` - ${stats.untrackedFiles} untracked file(s)`);
|
|
813
|
+
}
|
|
814
|
+
if (stats.stagedChanges > 0 && !scopeIncludes(scopeStart, scopeEnd, 'staged')) {
|
|
815
|
+
console.log(` - ${stats.stagedChanges} staged file(s) (outside current scope)`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
process.env.PAIR_REVIEW_LOCAL = 'true';
|
|
820
|
+
process.env.PAIR_REVIEW_LOCAL_PATH = repoPath;
|
|
821
|
+
process.env.PAIR_REVIEW_LOCAL_ID = String(sessionId);
|
|
822
|
+
process.env.PAIR_REVIEW_REPOSITORY = repository;
|
|
823
|
+
process.env.PAIR_REVIEW_BRANCH = branch;
|
|
824
|
+
process.env.PAIR_REVIEW_LOCAL_HEAD_SHA = headSha;
|
|
825
|
+
|
|
826
|
+
const digest = await module.exports.computeScopedDigest(repoPath, scopeStart, scopeEnd);
|
|
827
|
+
|
|
828
|
+
localReviewDiffs.set(sessionId, { diff, stats, digest, branchInfo });
|
|
829
|
+
|
|
830
|
+
try {
|
|
831
|
+
await reviewRepo.saveLocalDiff(sessionId, { diff, stats, digest });
|
|
832
|
+
} catch (persistError) {
|
|
833
|
+
logger.warn(`Could not persist diff to database: ${persistError.message}`);
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
summaryGenerator.kickOffSummaryJob({
|
|
837
|
+
db,
|
|
838
|
+
config,
|
|
839
|
+
reviewId: sessionId,
|
|
840
|
+
diffText: diff,
|
|
841
|
+
worktreePath: repoPath,
|
|
842
|
+
reviewContext: { prTitle: branch },
|
|
843
|
+
trigger: 'auto'
|
|
844
|
+
})?.catch((err) => logger.warn(`Hunk summary job failed for review ${sessionId}: ${err.message}`));
|
|
845
|
+
|
|
846
|
+
tourGenerator.kickOffTourJob({
|
|
847
|
+
db,
|
|
848
|
+
config,
|
|
849
|
+
reviewId: sessionId,
|
|
850
|
+
diffText: diff,
|
|
851
|
+
worktreePath: repoPath,
|
|
852
|
+
reviewContext: { prTitle: branch },
|
|
853
|
+
trigger: 'auto'
|
|
854
|
+
})?.catch((err) => logger.warn(`Tour job failed for review ${sessionId}: ${err.message}`));
|
|
855
|
+
|
|
856
|
+
return {
|
|
857
|
+
sessionId,
|
|
858
|
+
repoPath,
|
|
859
|
+
branch,
|
|
860
|
+
repository,
|
|
861
|
+
headSha,
|
|
862
|
+
diff,
|
|
863
|
+
stats,
|
|
864
|
+
digest,
|
|
865
|
+
branchInfo,
|
|
866
|
+
scopeStart,
|
|
867
|
+
scopeEnd,
|
|
868
|
+
baseBranch
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
|
|
693
872
|
/**
|
|
694
873
|
* Handle local review mode
|
|
695
874
|
* @param {string} targetPath - Target path to review (file or directory)
|
|
@@ -698,201 +877,43 @@ async function generateLocalDiff(repoPath, options = {}) {
|
|
|
698
877
|
*/
|
|
699
878
|
async function handleLocalReview(targetPath, flags = {}) {
|
|
700
879
|
let db = null;
|
|
880
|
+
let setupComplete = false;
|
|
701
881
|
|
|
702
882
|
try {
|
|
703
883
|
rejectUrlLikeLocalReviewPath(targetPath);
|
|
704
884
|
|
|
705
|
-
// Resolve target path
|
|
706
885
|
const resolvedPath = path.resolve(targetPath || process.cwd());
|
|
707
886
|
|
|
708
|
-
// Validate path exists
|
|
709
887
|
try {
|
|
710
888
|
await fs.access(resolvedPath);
|
|
711
889
|
} catch {
|
|
712
890
|
throw new Error(`Path does not exist: ${resolvedPath}`);
|
|
713
891
|
}
|
|
714
892
|
|
|
715
|
-
// Find git repository root
|
|
716
893
|
console.log(`Finding git repository root from ${resolvedPath}...`);
|
|
717
|
-
const repoPath = await findGitRoot(resolvedPath);
|
|
894
|
+
const repoPath = await module.exports.findGitRoot(resolvedPath);
|
|
718
895
|
console.log(`Found git repository at: ${repoPath}`);
|
|
719
896
|
|
|
720
|
-
// Get HEAD SHA for session identity
|
|
721
|
-
const headSha = await getHeadSha(repoPath);
|
|
722
|
-
console.log(`HEAD SHA: ${headSha}`);
|
|
723
|
-
|
|
724
|
-
// Get current branch
|
|
725
|
-
const branch = await getCurrentBranch(repoPath);
|
|
726
|
-
console.log(`Current branch: ${branch}`);
|
|
727
|
-
|
|
728
|
-
// Generate local review ID
|
|
729
|
-
const reviewId = generateLocalReviewId(repoPath, headSha);
|
|
730
|
-
console.log(`Local review ID: ${reviewId}`);
|
|
731
|
-
|
|
732
|
-
// Load configuration
|
|
733
897
|
const { config, isFirstRun } = await loadConfig();
|
|
734
898
|
|
|
735
|
-
// Show welcome message on first run
|
|
736
899
|
if (isFirstRun) {
|
|
737
900
|
showWelcomeMessage();
|
|
738
901
|
}
|
|
739
902
|
|
|
740
|
-
// Initialize database
|
|
741
903
|
console.log('Initializing database...');
|
|
742
904
|
db = await initializeDatabase(resolveDbName(config));
|
|
743
905
|
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
const repository = await getRepositoryName(repoPath);
|
|
747
|
-
console.log(`Repository: ${repository}`);
|
|
748
|
-
|
|
749
|
-
// If this is a GitHub repository (has owner/repo format), register the local path
|
|
750
|
-
// This enables future web UI sessions to find this repository without cloning
|
|
751
|
-
// Use findMainGitRoot to resolve worktrees to their parent repo
|
|
752
|
-
if (repository.includes('/')) {
|
|
753
|
-
try {
|
|
754
|
-
const mainRepoRoot = await findMainGitRoot(repoPath);
|
|
755
|
-
const repoSettingsRepo = new RepoSettingsRepository(db);
|
|
756
|
-
await repoSettingsRepo.setLocalPath(repository, mainRepoRoot);
|
|
757
|
-
console.log(`Registered repository location: ${mainRepoRoot}`);
|
|
758
|
-
} catch (error) {
|
|
759
|
-
// Non-fatal: registration failure shouldn't block the review
|
|
760
|
-
console.warn(`Could not register repository location: ${error.message}`);
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
console.log('Checking for existing review session...');
|
|
765
|
-
let existingReview = await reviewRepo.getLocalReview(repoPath, headSha, branch);
|
|
766
|
-
|
|
767
|
-
if (!existingReview) {
|
|
768
|
-
// Adopt legacy sessions that predate branch tracking (local_head_branch is NULL)
|
|
769
|
-
const legacy = await reviewRepo.getLocalReviewByPathAndSha(repoPath, headSha);
|
|
770
|
-
if (legacy && legacy.local_head_branch === null) {
|
|
771
|
-
existingReview = legacy;
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
if (!existingReview) {
|
|
776
|
-
// Check for existing branch-scope session on this path
|
|
777
|
-
// (branch scope sessions persist across HEAD changes)
|
|
778
|
-
const branchSession = await reviewRepo.getLocalBranchReview(repoPath, branch);
|
|
779
|
-
if (branchSession) {
|
|
780
|
-
existingReview = branchSession;
|
|
781
|
-
}
|
|
782
|
-
}
|
|
783
|
-
|
|
784
|
-
let sessionId;
|
|
785
|
-
if (existingReview) {
|
|
786
|
-
sessionId = existingReview.id;
|
|
787
|
-
// Update HEAD SHA if it changed (branch mode: new commits on same branch)
|
|
788
|
-
if (existingReview.local_head_sha !== headSha) {
|
|
789
|
-
await reviewRepo.updateLocalHeadSha(sessionId, headSha);
|
|
790
|
-
const abbrevLen = getShaAbbrevLength(repoPath);
|
|
791
|
-
console.log(`Updated HEAD SHA on session ${sessionId}: ${existingReview.local_head_sha.substring(0, abbrevLen)} -> ${headSha.substring(0, abbrevLen)}`);
|
|
792
|
-
}
|
|
793
|
-
// Backfill branch on legacy sessions
|
|
794
|
-
if (existingReview.local_head_branch === null) {
|
|
795
|
-
await reviewRepo.updateReview(sessionId, { local_head_branch: branch });
|
|
796
|
-
console.log(`Backfilled branch on session ${sessionId}: ${branch}`);
|
|
797
|
-
}
|
|
798
|
-
console.log(`Resuming existing review session (ID: ${existingReview.id})`);
|
|
799
|
-
} else {
|
|
800
|
-
console.log('Creating new review session...');
|
|
801
|
-
sessionId = await reviewRepo.upsertLocalReview({
|
|
802
|
-
localPath: repoPath,
|
|
803
|
-
localHeadSha: headSha,
|
|
804
|
-
repository,
|
|
805
|
-
localHeadBranch: branch
|
|
806
|
-
});
|
|
807
|
-
console.log(`Created new review session (ID: ${sessionId})`);
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
// Read scope from session (or use defaults for new sessions)
|
|
811
|
-
// Use reviewScope() to normalize legacy scopes that may not include 'unstaged'
|
|
812
|
-
const { start: scopeStart, end: scopeEnd } = existingReview ? reviewScope(existingReview) : DEFAULT_SCOPE;
|
|
813
|
-
|
|
814
|
-
// Fire review hook (non-blocking)
|
|
815
|
-
const hookEvent = existingReview ? 'review.loaded' : 'review.started';
|
|
816
|
-
if (hasHooks(hookEvent, config)) {
|
|
817
|
-
getCachedUser(config).then(user => {
|
|
818
|
-
const builder = existingReview ? buildReviewLoadedPayload : buildReviewStartedPayload;
|
|
819
|
-
const si = STOPS.indexOf(scopeStart);
|
|
820
|
-
const ei = STOPS.indexOf(scopeEnd);
|
|
821
|
-
const scope = STOPS.slice(si, ei + 1);
|
|
822
|
-
const payload = builder({ reviewId: sessionId, mode: 'local', localContext: { path: repoPath, branch, headSha, scope }, user });
|
|
823
|
-
fireHooks(hookEvent, payload, config);
|
|
824
|
-
}).catch(err => { logger.warn(`Review hook failed: ${err.message}`); });
|
|
825
|
-
}
|
|
826
|
-
const baseBranch = existingReview?.local_base_branch || null;
|
|
827
|
-
|
|
828
|
-
// Generate diff using session's actual scope
|
|
829
|
-
console.log(`Generating diff for scope: ${scopeLabel(scopeStart, scopeEnd)}...`);
|
|
830
|
-
const { diff, stats } = await generateScopedDiff(repoPath, scopeStart, scopeEnd, baseBranch);
|
|
831
|
-
|
|
832
|
-
// Branch detection: when scope does NOT include branch and no uncommitted changes,
|
|
833
|
-
// check if branch has commits ahead (frontend uses this to suggest expanding scope)
|
|
834
|
-
let branchInfo = null;
|
|
835
|
-
if (!includesBranch(scopeStart)) {
|
|
836
|
-
const untrackedFiles = await getUntrackedFiles(repoPath);
|
|
837
|
-
branchInfo = await detectAndBuildBranchInfo(repoPath, branch, {
|
|
838
|
-
repository,
|
|
839
|
-
diff,
|
|
840
|
-
untrackedFiles,
|
|
841
|
-
githubToken: getGitHubToken(config),
|
|
842
|
-
enableGraphite: config.enable_graphite === true
|
|
843
|
-
});
|
|
844
|
-
if (branchInfo) {
|
|
845
|
-
console.log(`\nNo uncommitted changes, but branch has ${branchInfo.commitCount} commit(s) ahead of ${branchInfo.baseBranch}.`);
|
|
846
|
-
console.log('The UI will offer to review branch changes.');
|
|
847
|
-
}
|
|
848
|
-
}
|
|
849
|
-
|
|
850
|
-
if (!diff && !branchInfo) {
|
|
851
|
-
console.log('\nNo changes detected in current scope. The UI will open anyway - you can change scope or make changes and refresh.');
|
|
852
|
-
} else if (diff) {
|
|
853
|
-
console.log(`Found ${stats.trackedChanges || 0} file(s) changed`);
|
|
854
|
-
if (stats.untrackedFiles > 0) {
|
|
855
|
-
console.log(` - ${stats.untrackedFiles} untracked file(s)`);
|
|
856
|
-
}
|
|
857
|
-
if (stats.stagedChanges > 0 && !scopeIncludes(scopeStart, scopeEnd, 'staged')) {
|
|
858
|
-
console.log(` - ${stats.stagedChanges} staged file(s) (outside current scope)`);
|
|
859
|
-
}
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
// Set environment variables for local mode (metadata only, not large data)
|
|
863
|
-
process.env.PAIR_REVIEW_LOCAL = 'true';
|
|
864
|
-
process.env.PAIR_REVIEW_LOCAL_PATH = repoPath;
|
|
865
|
-
process.env.PAIR_REVIEW_LOCAL_ID = String(sessionId);
|
|
866
|
-
process.env.PAIR_REVIEW_REPOSITORY = repository;
|
|
867
|
-
process.env.PAIR_REVIEW_BRANCH = branch;
|
|
868
|
-
process.env.PAIR_REVIEW_LOCAL_HEAD_SHA = headSha;
|
|
869
|
-
|
|
870
|
-
// Compute baseline digest NOW for accurate staleness detection later
|
|
871
|
-
// This must be done at diff-capture time, not lazily at check time
|
|
872
|
-
const digest = await computeScopedDigest(repoPath, scopeStart, scopeEnd);
|
|
873
|
-
|
|
874
|
-
// Store diff data in module-level Map (avoids process.env size limits and security concerns)
|
|
875
|
-
localReviewDiffs.set(sessionId, { diff, stats, digest, branchInfo });
|
|
876
|
-
|
|
877
|
-
// Persist diff to database so past sessions remain viewable without the server running
|
|
878
|
-
try {
|
|
879
|
-
const reviewRepo = new ReviewRepository(db);
|
|
880
|
-
await reviewRepo.saveLocalDiff(sessionId, { diff, stats, digest });
|
|
881
|
-
} catch (persistError) {
|
|
882
|
-
logger.warn(`Could not persist diff to database: ${persistError.message}`);
|
|
883
|
-
}
|
|
906
|
+
const session = await setupLocalReviewSession({ db, config, repoPath, flags });
|
|
907
|
+
setupComplete = true;
|
|
884
908
|
|
|
885
|
-
// Set model override if provided
|
|
886
909
|
if (flags.model) {
|
|
887
910
|
process.env.PAIR_REVIEW_MODEL = flags.model;
|
|
888
911
|
}
|
|
889
912
|
|
|
890
|
-
// Start server
|
|
891
913
|
console.log('Starting server...');
|
|
892
914
|
const port = await startServer(db);
|
|
893
915
|
|
|
894
|
-
|
|
895
|
-
let url = `http://localhost:${port}/local/${sessionId}`;
|
|
916
|
+
let url = `http://localhost:${port}/local/${session.sessionId}`;
|
|
896
917
|
if (flags.ai) {
|
|
897
918
|
url += '?analyze=true';
|
|
898
919
|
}
|
|
@@ -900,17 +921,15 @@ async function handleLocalReview(targetPath, flags = {}) {
|
|
|
900
921
|
await open(url);
|
|
901
922
|
|
|
902
923
|
console.log(`\nLocal review session started.`);
|
|
903
|
-
console.log(`Repository: ${repoPath}`);
|
|
904
|
-
console.log(`Branch: ${branch}`);
|
|
905
|
-
console.log(`Session ID: ${sessionId}\n`);
|
|
924
|
+
console.log(`Repository: ${session.repoPath}`);
|
|
925
|
+
console.log(`Branch: ${session.branch}`);
|
|
926
|
+
console.log(`Session ID: ${session.sessionId}\n`);
|
|
906
927
|
|
|
907
928
|
} catch (error) {
|
|
908
|
-
|
|
909
|
-
if (db) {
|
|
929
|
+
if (db && !setupComplete) {
|
|
910
930
|
db.close();
|
|
911
931
|
}
|
|
912
932
|
|
|
913
|
-
// Provide cleaner error messages for common issues
|
|
914
933
|
if (error.message.includes('does not exist')) {
|
|
915
934
|
console.error(`\n[ERROR] ${error.message}\n`);
|
|
916
935
|
} else if (error.message.includes('Not a git repository')) {
|
|
@@ -1057,6 +1076,7 @@ async function detectAndBuildBranchInfo(repoPath, branch, options = {}) {
|
|
|
1057
1076
|
|
|
1058
1077
|
module.exports = {
|
|
1059
1078
|
handleLocalReview,
|
|
1079
|
+
setupLocalReviewSession,
|
|
1060
1080
|
findGitRoot,
|
|
1061
1081
|
findMainGitRoot,
|
|
1062
1082
|
getHeadSha,
|
package/src/routes/config.js
CHANGED
|
@@ -20,7 +20,14 @@ const {
|
|
|
20
20
|
isCheckInProgress
|
|
21
21
|
} = require('../ai');
|
|
22
22
|
const { normalizeRepository } = require('../utils/paths');
|
|
23
|
-
const {
|
|
23
|
+
const {
|
|
24
|
+
isRunningViaNpx,
|
|
25
|
+
getGitHubToken,
|
|
26
|
+
getSummaryEnabled,
|
|
27
|
+
getSummaryAutoGenerate,
|
|
28
|
+
getTourEnabled,
|
|
29
|
+
getTourAutoGenerate
|
|
30
|
+
} = require('../config');
|
|
24
31
|
const { version } = require('../../package.json');
|
|
25
32
|
const semver = require('semver');
|
|
26
33
|
const { getAllChatProviders, getAllCachedChatAvailability } = require('../chat/chat-providers');
|
|
@@ -93,6 +100,14 @@ router.get('/api/config', (req, res) => {
|
|
|
93
100
|
enable_graphite: config.enable_graphite === true,
|
|
94
101
|
external_comments: config.external_comments !== false,
|
|
95
102
|
chat_spinner: config.chat_spinner || 'dots',
|
|
103
|
+
summaries: {
|
|
104
|
+
enabled: getSummaryEnabled(config),
|
|
105
|
+
auto_generate: getSummaryAutoGenerate(config)
|
|
106
|
+
},
|
|
107
|
+
tours: {
|
|
108
|
+
enabled: getTourEnabled(config),
|
|
109
|
+
auto_generate: getTourAutoGenerate(config)
|
|
110
|
+
},
|
|
96
111
|
// Share configuration for external review viewers.
|
|
97
112
|
// - url: The base URL of the external share site
|
|
98
113
|
// - method: Plumbed through for future use (e.g., POST-based share flows).
|
|
@@ -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.
|