@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/local-review.js
CHANGED
|
@@ -4,7 +4,7 @@ const { promisify } = require('util');
|
|
|
4
4
|
const crypto = require('crypto');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const fs = require('fs').promises;
|
|
7
|
-
const { loadConfig, showWelcomeMessage, resolveDbName, getGitHubToken } = require('./config');
|
|
7
|
+
const { loadConfig, showWelcomeMessage, resolveDbName, getGitHubToken, resolveHostBinding } = require('./config');
|
|
8
8
|
const logger = require('./utils/logger');
|
|
9
9
|
const { rejectUrlLikeLocalReviewPath } = require('./utils/local-path-input');
|
|
10
10
|
const { fireHooks, hasHooks } = require('./hooks/hook-runner');
|
|
@@ -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,186 @@ 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
|
+
// Resolve binding so alt-host repos look up PRs on the right host.
|
|
795
|
+
const branchBinding = repository ? resolveHostBinding(repository, config) : null;
|
|
796
|
+
branchInfo = await module.exports.detectAndBuildBranchInfo(repoPath, branch, {
|
|
797
|
+
repository,
|
|
798
|
+
diff,
|
|
799
|
+
untrackedFiles,
|
|
800
|
+
githubToken: branchBinding?.token || getGitHubToken(config),
|
|
801
|
+
hostBinding: branchBinding,
|
|
802
|
+
enableGraphite: config.enable_graphite === true
|
|
803
|
+
});
|
|
804
|
+
if (branchInfo) {
|
|
805
|
+
console.log(`\nNo uncommitted changes, but branch has ${branchInfo.commitCount} commit(s) ahead of ${branchInfo.baseBranch}.`);
|
|
806
|
+
console.log('The UI will offer to review branch changes.');
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (!diff && !branchInfo) {
|
|
811
|
+
console.log('\nNo changes detected in current scope. The UI will open anyway - you can change scope or make changes and refresh.');
|
|
812
|
+
} else if (diff) {
|
|
813
|
+
console.log(`Found ${stats.trackedChanges || 0} file(s) changed`);
|
|
814
|
+
if (stats.untrackedFiles > 0) {
|
|
815
|
+
console.log(` - ${stats.untrackedFiles} untracked file(s)`);
|
|
816
|
+
}
|
|
817
|
+
if (stats.stagedChanges > 0 && !scopeIncludes(scopeStart, scopeEnd, 'staged')) {
|
|
818
|
+
console.log(` - ${stats.stagedChanges} staged file(s) (outside current scope)`);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
process.env.PAIR_REVIEW_LOCAL = 'true';
|
|
823
|
+
process.env.PAIR_REVIEW_LOCAL_PATH = repoPath;
|
|
824
|
+
process.env.PAIR_REVIEW_LOCAL_ID = String(sessionId);
|
|
825
|
+
process.env.PAIR_REVIEW_REPOSITORY = repository;
|
|
826
|
+
process.env.PAIR_REVIEW_BRANCH = branch;
|
|
827
|
+
process.env.PAIR_REVIEW_LOCAL_HEAD_SHA = headSha;
|
|
828
|
+
|
|
829
|
+
const digest = await module.exports.computeScopedDigest(repoPath, scopeStart, scopeEnd);
|
|
830
|
+
|
|
831
|
+
localReviewDiffs.set(sessionId, { diff, stats, digest, branchInfo });
|
|
832
|
+
|
|
833
|
+
try {
|
|
834
|
+
await reviewRepo.saveLocalDiff(sessionId, { diff, stats, digest });
|
|
835
|
+
} catch (persistError) {
|
|
836
|
+
logger.warn(`Could not persist diff to database: ${persistError.message}`);
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
summaryGenerator.kickOffSummaryJob({
|
|
840
|
+
db,
|
|
841
|
+
config,
|
|
842
|
+
reviewId: sessionId,
|
|
843
|
+
diffText: diff,
|
|
844
|
+
worktreePath: repoPath,
|
|
845
|
+
reviewContext: { prTitle: branch },
|
|
846
|
+
trigger: 'auto'
|
|
847
|
+
})?.catch((err) => logger.warn(`Hunk summary job failed for review ${sessionId}: ${err.message}`));
|
|
848
|
+
|
|
849
|
+
tourGenerator.kickOffTourJob({
|
|
850
|
+
db,
|
|
851
|
+
config,
|
|
852
|
+
reviewId: sessionId,
|
|
853
|
+
diffText: diff,
|
|
854
|
+
worktreePath: repoPath,
|
|
855
|
+
reviewContext: { prTitle: branch },
|
|
856
|
+
trigger: 'auto'
|
|
857
|
+
})?.catch((err) => logger.warn(`Tour job failed for review ${sessionId}: ${err.message}`));
|
|
858
|
+
|
|
859
|
+
return {
|
|
860
|
+
sessionId,
|
|
861
|
+
repoPath,
|
|
862
|
+
branch,
|
|
863
|
+
repository,
|
|
864
|
+
headSha,
|
|
865
|
+
diff,
|
|
866
|
+
stats,
|
|
867
|
+
digest,
|
|
868
|
+
branchInfo,
|
|
869
|
+
scopeStart,
|
|
870
|
+
scopeEnd,
|
|
871
|
+
baseBranch
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
|
|
693
875
|
/**
|
|
694
876
|
* Handle local review mode
|
|
695
877
|
* @param {string} targetPath - Target path to review (file or directory)
|
|
@@ -698,201 +880,43 @@ async function generateLocalDiff(repoPath, options = {}) {
|
|
|
698
880
|
*/
|
|
699
881
|
async function handleLocalReview(targetPath, flags = {}) {
|
|
700
882
|
let db = null;
|
|
883
|
+
let setupComplete = false;
|
|
701
884
|
|
|
702
885
|
try {
|
|
703
886
|
rejectUrlLikeLocalReviewPath(targetPath);
|
|
704
887
|
|
|
705
|
-
// Resolve target path
|
|
706
888
|
const resolvedPath = path.resolve(targetPath || process.cwd());
|
|
707
889
|
|
|
708
|
-
// Validate path exists
|
|
709
890
|
try {
|
|
710
891
|
await fs.access(resolvedPath);
|
|
711
892
|
} catch {
|
|
712
893
|
throw new Error(`Path does not exist: ${resolvedPath}`);
|
|
713
894
|
}
|
|
714
895
|
|
|
715
|
-
// Find git repository root
|
|
716
896
|
console.log(`Finding git repository root from ${resolvedPath}...`);
|
|
717
|
-
const repoPath = await findGitRoot(resolvedPath);
|
|
897
|
+
const repoPath = await module.exports.findGitRoot(resolvedPath);
|
|
718
898
|
console.log(`Found git repository at: ${repoPath}`);
|
|
719
899
|
|
|
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
900
|
const { config, isFirstRun } = await loadConfig();
|
|
734
901
|
|
|
735
|
-
// Show welcome message on first run
|
|
736
902
|
if (isFirstRun) {
|
|
737
903
|
showWelcomeMessage();
|
|
738
904
|
}
|
|
739
905
|
|
|
740
|
-
// Initialize database
|
|
741
906
|
console.log('Initializing database...');
|
|
742
907
|
db = await initializeDatabase(resolveDbName(config));
|
|
743
908
|
|
|
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
|
-
}
|
|
909
|
+
const session = await setupLocalReviewSession({ db, config, repoPath, flags });
|
|
910
|
+
setupComplete = true;
|
|
809
911
|
|
|
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
|
-
}
|
|
884
|
-
|
|
885
|
-
// Set model override if provided
|
|
886
912
|
if (flags.model) {
|
|
887
913
|
process.env.PAIR_REVIEW_MODEL = flags.model;
|
|
888
914
|
}
|
|
889
915
|
|
|
890
|
-
// Start server
|
|
891
916
|
console.log('Starting server...');
|
|
892
917
|
const port = await startServer(db);
|
|
893
918
|
|
|
894
|
-
|
|
895
|
-
let url = `http://localhost:${port}/local/${sessionId}`;
|
|
919
|
+
let url = `http://localhost:${port}/local/${session.sessionId}`;
|
|
896
920
|
if (flags.ai) {
|
|
897
921
|
url += '?analyze=true';
|
|
898
922
|
}
|
|
@@ -900,17 +924,15 @@ async function handleLocalReview(targetPath, flags = {}) {
|
|
|
900
924
|
await open(url);
|
|
901
925
|
|
|
902
926
|
console.log(`\nLocal review session started.`);
|
|
903
|
-
console.log(`Repository: ${repoPath}`);
|
|
904
|
-
console.log(`Branch: ${branch}`);
|
|
905
|
-
console.log(`Session ID: ${sessionId}\n`);
|
|
927
|
+
console.log(`Repository: ${session.repoPath}`);
|
|
928
|
+
console.log(`Branch: ${session.branch}`);
|
|
929
|
+
console.log(`Session ID: ${session.sessionId}\n`);
|
|
906
930
|
|
|
907
931
|
} catch (error) {
|
|
908
|
-
|
|
909
|
-
if (db) {
|
|
932
|
+
if (db && !setupComplete) {
|
|
910
933
|
db.close();
|
|
911
934
|
}
|
|
912
935
|
|
|
913
|
-
// Provide cleaner error messages for common issues
|
|
914
936
|
if (error.message.includes('does not exist')) {
|
|
915
937
|
console.error(`\n[ERROR] ${error.message}\n`);
|
|
916
938
|
} else if (error.message.includes('Not a git repository')) {
|
|
@@ -1019,11 +1041,12 @@ async function getFirstCommitSubject(repoPath, baseBranch) {
|
|
|
1019
1041
|
* @param {string} [options.diff] - The uncommitted diff content (empty = eligible)
|
|
1020
1042
|
* @param {Array} [options.untrackedFiles] - Untracked files array (empty = eligible)
|
|
1021
1043
|
* @param {string} [options.githubToken] - Resolved GitHub token for PR lookup
|
|
1044
|
+
* @param {Object} [options.hostBinding] - Resolved host binding (apiHost/token/features) for alt-host PR lookup
|
|
1022
1045
|
* @param {boolean} [options.enableGraphite] - When true, try Graphite CLI for parent branch
|
|
1023
1046
|
* @returns {Promise<{baseBranch: string, commitCount: number, source: string, prNumber?: number}|null>}
|
|
1024
1047
|
*/
|
|
1025
1048
|
async function detectAndBuildBranchInfo(repoPath, branch, options = {}) {
|
|
1026
|
-
const { repository, diff, untrackedFiles, githubToken, enableGraphite } = options;
|
|
1049
|
+
const { repository, diff, untrackedFiles, githubToken, hostBinding, enableGraphite } = options;
|
|
1027
1050
|
|
|
1028
1051
|
// Guard: detached HEAD, has uncommitted changes, or has untracked files
|
|
1029
1052
|
if (branch === 'HEAD') return null;
|
|
@@ -1032,7 +1055,12 @@ async function detectAndBuildBranchInfo(repoPath, branch, options = {}) {
|
|
|
1032
1055
|
|
|
1033
1056
|
try {
|
|
1034
1057
|
const { detectBaseBranch } = require('./git/base-branch');
|
|
1035
|
-
const depsOverride = githubToken
|
|
1058
|
+
const depsOverride = githubToken || hostBinding
|
|
1059
|
+
? {
|
|
1060
|
+
getGitHubToken: () => githubToken || '',
|
|
1061
|
+
getHostBinding: () => hostBinding || null
|
|
1062
|
+
}
|
|
1063
|
+
: undefined;
|
|
1036
1064
|
const detection = await detectBaseBranch(repoPath, branch, {
|
|
1037
1065
|
repository,
|
|
1038
1066
|
enableGraphite,
|
|
@@ -1057,6 +1085,7 @@ async function detectAndBuildBranchInfo(repoPath, branch, options = {}) {
|
|
|
1057
1085
|
|
|
1058
1086
|
module.exports = {
|
|
1059
1087
|
handleLocalReview,
|
|
1088
|
+
setupLocalReviewSession,
|
|
1060
1089
|
findGitRoot,
|
|
1061
1090
|
findMainGitRoot,
|
|
1062
1091
|
getHeadSha,
|