@in-the-loop-labs/pair-review 1.6.2 → 2.0.1
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 +77 -4
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/review-requests/SKILL.md +4 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/skills/analyze/SKILL.md +4 -3
- package/public/css/pr.css +1962 -114
- package/public/js/CONVENTIONS.md +16 -0
- package/public/js/components/AIPanel.js +66 -0
- package/public/js/components/AnalysisConfigModal.js +2 -2
- package/public/js/components/ChatPanel.js +2955 -0
- package/public/js/components/CouncilProgressModal.js +12 -16
- package/public/js/components/KeyboardShortcuts.js +3 -0
- package/public/js/components/PanelGroup.js +723 -0
- package/public/js/components/PreviewModal.js +3 -8
- package/public/js/index.js +8 -0
- package/public/js/local.js +17 -615
- package/public/js/modules/analysis-history.js +19 -68
- package/public/js/modules/comment-manager.js +103 -20
- package/public/js/modules/diff-context.js +176 -0
- package/public/js/modules/diff-renderer.js +30 -0
- package/public/js/modules/file-comment-manager.js +126 -105
- package/public/js/modules/file-list-merger.js +64 -0
- package/public/js/modules/panel-resizer.js +25 -6
- package/public/js/modules/suggestion-manager.js +40 -125
- package/public/js/pr.js +1009 -159
- package/public/js/repo-settings.js +36 -6
- package/public/js/utils/category-emoji.js +44 -0
- package/public/js/utils/time.js +32 -0
- package/public/local.html +107 -70
- package/public/pr.html +107 -70
- package/public/repo-settings.html +32 -0
- package/src/ai/analyzer.js +5 -1
- package/src/ai/copilot-provider.js +39 -9
- package/src/ai/cursor-agent-provider.js +45 -11
- package/src/ai/gemini-provider.js +17 -4
- package/src/ai/prompts/config.js +7 -1
- package/src/ai/provider-availability.js +1 -1
- package/src/ai/provider.js +25 -37
- package/src/chat/CONVENTIONS.md +18 -0
- package/src/chat/pi-bridge.js +491 -0
- package/src/chat/prompt-builder.js +272 -0
- package/src/chat/session-manager.js +619 -0
- package/src/config.js +14 -0
- package/src/database.js +322 -15
- package/src/main.js +4 -17
- package/src/routes/analyses.js +721 -0
- package/src/routes/chat.js +655 -0
- package/src/routes/config.js +29 -8
- package/src/routes/context-files.js +274 -0
- package/src/routes/local.js +225 -1133
- package/src/routes/mcp.js +39 -30
- package/src/routes/pr.js +424 -58
- package/src/routes/reviews.js +1035 -0
- package/src/routes/shared.js +4 -29
- package/src/server.js +34 -12
- package/src/sse/review-events.js +46 -0
- package/src/utils/auto-context.js +88 -0
- package/src/utils/category-emoji.js +33 -0
- package/src/utils/diff-annotator.js +75 -1
- package/src/utils/diff-file-list.js +57 -0
- package/src/routes/analysis.js +0 -1600
- package/src/routes/comments.js +0 -534
package/src/routes/local.js
CHANGED
|
@@ -15,21 +15,21 @@
|
|
|
15
15
|
const express = require('express');
|
|
16
16
|
const path = require('path');
|
|
17
17
|
const fs = require('fs').promises;
|
|
18
|
-
const {
|
|
18
|
+
const { queryOne, run, ReviewRepository, RepoSettingsRepository, AnalysisRunRepository, CouncilRepository } = require('../database');
|
|
19
19
|
const Analyzer = require('../ai/analyzer');
|
|
20
20
|
const { v4: uuidv4 } = require('uuid');
|
|
21
21
|
const logger = require('../utils/logger');
|
|
22
|
+
const { broadcastReviewEvent } = require('../sse/review-events');
|
|
22
23
|
const { mergeInstructions } = require('../utils/instructions');
|
|
23
|
-
const { calculateStats, getStatsQuery } = require('../utils/stats-calculator');
|
|
24
24
|
const { generateLocalDiff, computeLocalDiffDigest } = require('../local-review');
|
|
25
25
|
const { getGeneratedFilePatterns } = require('../git/gitattributes');
|
|
26
|
+
const { validateCouncilConfig, normalizeCouncilConfig } = require('./councils');
|
|
27
|
+
const { TIERS, TIER_ALIASES, VALID_TIERS, resolveTier } = require('../ai/prompts/config');
|
|
26
28
|
const {
|
|
27
29
|
activeAnalyses,
|
|
28
|
-
progressClients,
|
|
29
30
|
localReviewDiffs,
|
|
30
|
-
|
|
31
|
+
reviewToAnalysisId,
|
|
31
32
|
getModel,
|
|
32
|
-
getLocalReviewKey,
|
|
33
33
|
determineCompletionInfo,
|
|
34
34
|
broadcastProgress,
|
|
35
35
|
CancellationError,
|
|
@@ -39,6 +39,27 @@ const {
|
|
|
39
39
|
|
|
40
40
|
const router = express.Router();
|
|
41
41
|
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Helpers – type-safe wrappers around localReviewDiffs Map
|
|
44
|
+
// JavaScript Maps use strict equality for keys. reviewId values arrive from
|
|
45
|
+
// req.params as strings, but every other code path stores them as integers.
|
|
46
|
+
// These helpers coerce once so callers never hit a string/int mismatch.
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
function toIntKey(reviewId) {
|
|
49
|
+
const key = typeof reviewId === 'number' ? reviewId : parseInt(reviewId, 10);
|
|
50
|
+
if (isNaN(key)) throw new Error(`Invalid reviewId for diff cache: ${reviewId}`);
|
|
51
|
+
return key;
|
|
52
|
+
}
|
|
53
|
+
function getLocalReviewDiff(reviewId) {
|
|
54
|
+
return localReviewDiffs.get(toIntKey(reviewId));
|
|
55
|
+
}
|
|
56
|
+
function setLocalReviewDiff(reviewId, value) {
|
|
57
|
+
localReviewDiffs.set(toIntKey(reviewId), value);
|
|
58
|
+
}
|
|
59
|
+
function deleteLocalReviewDiff(reviewId) {
|
|
60
|
+
localReviewDiffs.delete(toIntKey(reviewId));
|
|
61
|
+
}
|
|
62
|
+
|
|
42
63
|
/**
|
|
43
64
|
* Open native OS directory picker dialog and return the selected path.
|
|
44
65
|
* Uses osascript on macOS, zenity/kdialog on Linux, PowerShell on Windows.
|
|
@@ -184,7 +205,7 @@ router.delete('/api/local/sessions/:reviewId', async (req, res) => {
|
|
|
184
205
|
}
|
|
185
206
|
|
|
186
207
|
// Clean up in-memory diff cache to avoid stale data
|
|
187
|
-
|
|
208
|
+
deleteLocalReviewDiff(reviewId);
|
|
188
209
|
|
|
189
210
|
logger.success(`Deleted local review session #${reviewId}`);
|
|
190
211
|
|
|
@@ -268,7 +289,7 @@ router.post('/api/local/start', async (req, res) => {
|
|
|
268
289
|
const digest = await computeLocalDiffDigest(repoPath);
|
|
269
290
|
|
|
270
291
|
// Persist to in-memory Map
|
|
271
|
-
|
|
292
|
+
setLocalReviewDiff(sessionId, { diff, stats, digest });
|
|
272
293
|
|
|
273
294
|
// Persist to database
|
|
274
295
|
await reviewRepo.saveLocalDiff(sessionId, { diff, stats, digest });
|
|
@@ -445,7 +466,7 @@ router.get('/api/local/:reviewId/diff', async (req, res) => {
|
|
|
445
466
|
}
|
|
446
467
|
|
|
447
468
|
// Get diff from module-level storage, falling back to database
|
|
448
|
-
let diffData =
|
|
469
|
+
let diffData = getLocalReviewDiff(reviewId);
|
|
449
470
|
|
|
450
471
|
if (!diffData) {
|
|
451
472
|
// Try loading from database
|
|
@@ -453,7 +474,7 @@ router.get('/api/local/:reviewId/diff', async (req, res) => {
|
|
|
453
474
|
if (persistedDiff) {
|
|
454
475
|
diffData = persistedDiff;
|
|
455
476
|
// Cache-warm the in-memory Map
|
|
456
|
-
|
|
477
|
+
setLocalReviewDiff(reviewId, diffData);
|
|
457
478
|
logger.log('API', `Loaded persisted diff from DB for review #${reviewId}`, 'cyan');
|
|
458
479
|
} else {
|
|
459
480
|
diffData = { diff: '', stats: {} };
|
|
@@ -536,13 +557,13 @@ router.get('/api/local/:reviewId/check-stale', async (req, res) => {
|
|
|
536
557
|
}
|
|
537
558
|
|
|
538
559
|
// Get stored diff data (in-memory first, then fall back to DB)
|
|
539
|
-
let storedDiffData =
|
|
560
|
+
let storedDiffData = getLocalReviewDiff(reviewId);
|
|
540
561
|
if (!storedDiffData) {
|
|
541
562
|
const persistedDiff = await reviewRepo.getLocalDiff(reviewId);
|
|
542
563
|
if (persistedDiff) {
|
|
543
564
|
storedDiffData = persistedDiff;
|
|
544
565
|
// Cache-warm the in-memory Map
|
|
545
|
-
|
|
566
|
+
setLocalReviewDiff(reviewId, storedDiffData);
|
|
546
567
|
logger.log('API', `Loaded persisted diff from DB for staleness check on review #${reviewId}`, 'cyan');
|
|
547
568
|
} else {
|
|
548
569
|
return res.json({
|
|
@@ -593,7 +614,7 @@ router.get('/api/local/:reviewId/check-stale', async (req, res) => {
|
|
|
593
614
|
/**
|
|
594
615
|
* Start Level 1 AI analysis for local review
|
|
595
616
|
*/
|
|
596
|
-
router.post('/api/local/:reviewId/
|
|
617
|
+
router.post('/api/local/:reviewId/analyses', async (req, res) => {
|
|
597
618
|
try {
|
|
598
619
|
const reviewId = parseInt(req.params.reviewId);
|
|
599
620
|
|
|
@@ -616,7 +637,6 @@ router.post('/api/local/:reviewId/analyze', async (req, res) => {
|
|
|
616
637
|
}
|
|
617
638
|
|
|
618
639
|
// Validate tier
|
|
619
|
-
const VALID_TIERS = ['fast', 'balanced', 'thorough', 'free', 'standard', 'premium'];
|
|
620
640
|
if (requestTier && !VALID_TIERS.includes(requestTier)) {
|
|
621
641
|
return res.status(400).json({
|
|
622
642
|
error: `Invalid tier: "${requestTier}". Valid tiers: ${VALID_TIERS.join(', ')}`
|
|
@@ -682,12 +702,14 @@ router.post('/api/local/:reviewId/analyze', async (req, res) => {
|
|
|
682
702
|
// Create DB analysis_runs record immediately so it's queryable for polling
|
|
683
703
|
const analysisRunRepo = new AnalysisRunRepository(db);
|
|
684
704
|
const levelsConfig = parseEnabledLevels(requestEnabledLevels, requestSkipLevel3);
|
|
705
|
+
const tier = requestTier ? resolveTier(requestTier) : 'balanced';
|
|
685
706
|
try {
|
|
686
707
|
await analysisRunRepo.create({
|
|
687
708
|
id: runId,
|
|
688
709
|
reviewId,
|
|
689
710
|
provider: selectedProvider,
|
|
690
711
|
model: selectedModel,
|
|
712
|
+
tier,
|
|
691
713
|
repoInstructions,
|
|
692
714
|
requestInstructions,
|
|
693
715
|
headSha: review.local_head_sha || null,
|
|
@@ -720,12 +742,12 @@ router.post('/api/local/:reviewId/analyze', async (req, res) => {
|
|
|
720
742
|
};
|
|
721
743
|
activeAnalyses.set(analysisId, initialStatus);
|
|
722
744
|
|
|
723
|
-
// Store
|
|
724
|
-
|
|
725
|
-
localReviewToAnalysisId.set(reviewKey, analysisId);
|
|
745
|
+
// Store review to analysis ID mapping (unified map)
|
|
746
|
+
reviewToAnalysisId.set(reviewId, analysisId);
|
|
726
747
|
|
|
727
748
|
// Broadcast initial status
|
|
728
749
|
broadcastProgress(analysisId, initialStatus);
|
|
750
|
+
broadcastReviewEvent(reviewId, { type: 'review:analysis_started', analysisId });
|
|
729
751
|
|
|
730
752
|
// Create analyzer instance with provider and model
|
|
731
753
|
const analyzer = new Analyzer(db, selectedModel, selectedProvider);
|
|
@@ -754,8 +776,6 @@ router.post('/api/local/:reviewId/analyze', async (req, res) => {
|
|
|
754
776
|
logger.log('API', `Analysis ID: ${analysisId}`, 'magenta');
|
|
755
777
|
logger.log('API', `Provider: ${selectedProvider}`, 'cyan');
|
|
756
778
|
logger.log('API', `Model: ${selectedModel}`, 'cyan');
|
|
757
|
-
// Determine tier: request body > default ('balanced')
|
|
758
|
-
const tier = requestTier || 'balanced';
|
|
759
779
|
logger.log('API', `Tier: ${tier}`, 'cyan');
|
|
760
780
|
logger.log('API', `Changed files: ${changedFiles.length}`, 'cyan');
|
|
761
781
|
if (combinedInstructions) {
|
|
@@ -828,6 +848,7 @@ router.post('/api/local/:reviewId/analyze', async (req, res) => {
|
|
|
828
848
|
|
|
829
849
|
// Broadcast completion status
|
|
830
850
|
broadcastProgress(analysisId, completedStatus);
|
|
851
|
+
broadcastReviewEvent(reviewId, { type: 'review:analysis_completed' });
|
|
831
852
|
})
|
|
832
853
|
.catch(error => {
|
|
833
854
|
const currentStatus = activeAnalyses.get(analysisId);
|
|
@@ -867,9 +888,8 @@ router.post('/api/local/:reviewId/analyze', async (req, res) => {
|
|
|
867
888
|
broadcastProgress(analysisId, failedStatus);
|
|
868
889
|
})
|
|
869
890
|
.finally(() => {
|
|
870
|
-
// Clean up
|
|
871
|
-
|
|
872
|
-
localReviewToAnalysisId.delete(reviewKey);
|
|
891
|
+
// Clean up review to analysis ID mapping (unified map)
|
|
892
|
+
reviewToAnalysisId.delete(reviewId);
|
|
873
893
|
});
|
|
874
894
|
|
|
875
895
|
// Return analysis ID immediately (runId added for unified ID)
|
|
@@ -888,10 +908,13 @@ router.post('/api/local/:reviewId/analyze', async (req, res) => {
|
|
|
888
908
|
}
|
|
889
909
|
});
|
|
890
910
|
|
|
911
|
+
|
|
891
912
|
/**
|
|
892
|
-
*
|
|
913
|
+
* Refresh the diff for a local review
|
|
914
|
+
* Regenerates the diff from the current state of the working directory
|
|
915
|
+
* Returns sessionChanged flag if HEAD has changed since the session was created
|
|
893
916
|
*/
|
|
894
|
-
router.
|
|
917
|
+
router.post('/api/local/:reviewId/refresh', async (req, res) => {
|
|
895
918
|
try {
|
|
896
919
|
const reviewId = parseInt(req.params.reviewId);
|
|
897
920
|
|
|
@@ -902,8 +925,6 @@ router.get('/api/local/:reviewId/suggestions', async (req, res) => {
|
|
|
902
925
|
}
|
|
903
926
|
|
|
904
927
|
const db = req.app.get('db');
|
|
905
|
-
|
|
906
|
-
// Verify review exists
|
|
907
928
|
const reviewRepo = new ReviewRepository(db);
|
|
908
929
|
const review = await reviewRepo.getLocalReviewById(reviewId);
|
|
909
930
|
|
|
@@ -913,166 +934,99 @@ router.get('/api/local/:reviewId/suggestions', async (req, res) => {
|
|
|
913
934
|
});
|
|
914
935
|
}
|
|
915
936
|
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
const levelsParam = req.query.levels || 'final';
|
|
919
|
-
const requestedLevels = levelsParam.split(',').map(l => l.trim());
|
|
920
|
-
|
|
921
|
-
// Parse optional runId query parameter to fetch suggestions from a specific analysis run
|
|
922
|
-
// If not provided, defaults to the latest run
|
|
923
|
-
const runIdParam = req.query.runId;
|
|
924
|
-
|
|
925
|
-
// Build level filter clause
|
|
926
|
-
const levelConditions = [];
|
|
927
|
-
requestedLevels.forEach(level => {
|
|
928
|
-
if (level === 'final') {
|
|
929
|
-
levelConditions.push('ai_level IS NULL');
|
|
930
|
-
} else if (['1', '2', '3'].includes(level)) {
|
|
931
|
-
levelConditions.push(`ai_level = ${parseInt(level)}`);
|
|
932
|
-
}
|
|
933
|
-
});
|
|
937
|
+
const localPath = review.local_path;
|
|
938
|
+
const originalHeadSha = review.local_head_sha;
|
|
934
939
|
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
// Build the run ID filter clause
|
|
941
|
-
// If a specific runId is provided, use it directly; otherwise use subquery for latest
|
|
942
|
-
let runIdFilter;
|
|
943
|
-
let queryParams;
|
|
944
|
-
if (runIdParam) {
|
|
945
|
-
runIdFilter = 'ai_run_id = ?';
|
|
946
|
-
queryParams = [reviewId, runIdParam];
|
|
947
|
-
} else {
|
|
948
|
-
// Get AI suggestions from the comments table
|
|
949
|
-
// For local reviews, review_id stores the review ID
|
|
950
|
-
// Only return suggestions from the latest analysis run (ai_run_id)
|
|
951
|
-
// This preserves history while showing only the most recent results
|
|
952
|
-
//
|
|
953
|
-
// Note: If no AI suggestions exist (subquery returns NULL), the ai_run_id = NULL
|
|
954
|
-
// comparison returns no rows. This is intentional - we only show suggestions
|
|
955
|
-
// when there's a matching analysis run.
|
|
956
|
-
//
|
|
957
|
-
// Note: reviewId is passed twice because SQLite requires separate parameters
|
|
958
|
-
// for the outer WHERE clause and the subquery. A CTE could consolidate this but
|
|
959
|
-
// adds complexity without meaningful benefit here.
|
|
960
|
-
runIdFilter = `ai_run_id = (
|
|
961
|
-
SELECT ai_run_id FROM comments
|
|
962
|
-
WHERE review_id = ? AND source = 'ai' AND ai_run_id IS NOT NULL
|
|
963
|
-
ORDER BY created_at DESC
|
|
964
|
-
LIMIT 1
|
|
965
|
-
)`;
|
|
966
|
-
queryParams = [reviewId, reviewId];
|
|
940
|
+
if (!localPath) {
|
|
941
|
+
return res.status(400).json({
|
|
942
|
+
error: 'Local review is missing path information'
|
|
943
|
+
});
|
|
967
944
|
}
|
|
968
945
|
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
id,
|
|
972
|
-
source,
|
|
973
|
-
author,
|
|
974
|
-
ai_run_id,
|
|
975
|
-
ai_level,
|
|
976
|
-
ai_confidence,
|
|
977
|
-
file,
|
|
978
|
-
line_start,
|
|
979
|
-
line_end,
|
|
980
|
-
side,
|
|
981
|
-
type,
|
|
982
|
-
title,
|
|
983
|
-
body,
|
|
984
|
-
reasoning,
|
|
985
|
-
status,
|
|
986
|
-
is_file_level,
|
|
987
|
-
created_at,
|
|
988
|
-
updated_at
|
|
989
|
-
FROM comments
|
|
990
|
-
WHERE review_id = ?
|
|
991
|
-
AND source = 'ai'
|
|
992
|
-
AND ${levelFilter}
|
|
993
|
-
AND status IN ('active', 'dismissed', 'adopted', 'draft', 'submitted')
|
|
994
|
-
AND (is_raw = 0 OR is_raw IS NULL)
|
|
995
|
-
AND ${runIdFilter}
|
|
996
|
-
ORDER BY
|
|
997
|
-
CASE
|
|
998
|
-
WHEN ai_level IS NULL THEN 0
|
|
999
|
-
WHEN ai_level = 1 THEN 1
|
|
1000
|
-
WHEN ai_level = 2 THEN 2
|
|
1001
|
-
WHEN ai_level = 3 THEN 3
|
|
1002
|
-
ELSE 4
|
|
1003
|
-
END,
|
|
1004
|
-
is_file_level DESC,
|
|
1005
|
-
file,
|
|
1006
|
-
line_start
|
|
1007
|
-
`, queryParams);
|
|
1008
|
-
|
|
1009
|
-
const suggestions = rows.map(row => ({
|
|
1010
|
-
...row,
|
|
1011
|
-
reasoning: row.reasoning ? JSON.parse(row.reasoning) : null
|
|
1012
|
-
}));
|
|
1013
|
-
|
|
1014
|
-
res.json({ suggestions });
|
|
946
|
+
logger.log('API', `Refreshing diff for local review #${reviewId}`, 'cyan');
|
|
947
|
+
logger.log('API', `Local path: ${localPath}`, 'magenta');
|
|
1015
948
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
}
|
|
1022
|
-
});
|
|
949
|
+
// Check if HEAD has changed
|
|
950
|
+
const { getHeadSha } = require('../local-review');
|
|
951
|
+
let currentHeadSha;
|
|
952
|
+
let sessionChanged = false;
|
|
953
|
+
let newSessionId = null;
|
|
1023
954
|
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
* Uses CommentRepository.getUserComments() for consistency with PR mode
|
|
1027
|
-
*/
|
|
1028
|
-
router.get('/api/local/:reviewId/user-comments', async (req, res) => {
|
|
1029
|
-
try {
|
|
1030
|
-
const reviewId = parseInt(req.params.reviewId);
|
|
955
|
+
try {
|
|
956
|
+
currentHeadSha = await getHeadSha(localPath);
|
|
1031
957
|
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
958
|
+
if (originalHeadSha && currentHeadSha !== originalHeadSha) {
|
|
959
|
+
sessionChanged = true;
|
|
960
|
+
logger.log('API', `HEAD changed: ${originalHeadSha.substring(0, 7)} -> ${currentHeadSha.substring(0, 7)}`, 'yellow');
|
|
961
|
+
|
|
962
|
+
// Check if a session already exists for the new HEAD
|
|
963
|
+
const existingSession = await reviewRepo.getLocalReview(localPath, currentHeadSha);
|
|
964
|
+
if (existingSession) {
|
|
965
|
+
newSessionId = existingSession.id;
|
|
966
|
+
logger.log('API', `Existing session found for new HEAD: ${newSessionId}`, 'cyan');
|
|
967
|
+
} else {
|
|
968
|
+
// Create a new session for the new HEAD
|
|
969
|
+
const { getRepositoryName } = require('../local-review');
|
|
970
|
+
const repository = await getRepositoryName(localPath);
|
|
971
|
+
newSessionId = await reviewRepo.upsertLocalReview({
|
|
972
|
+
localPath: localPath,
|
|
973
|
+
localHeadSha: currentHeadSha,
|
|
974
|
+
repository
|
|
975
|
+
});
|
|
976
|
+
logger.log('API', `Created new session for new HEAD: ${newSessionId}`, 'cyan');
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
} catch (headError) {
|
|
980
|
+
logger.warn(`Could not check HEAD SHA: ${headError.message}`);
|
|
1036
981
|
}
|
|
1037
982
|
|
|
1038
|
-
|
|
983
|
+
// Regenerate the diff from the working directory
|
|
984
|
+
const { diff, stats } = await generateLocalDiff(localPath);
|
|
1039
985
|
|
|
1040
|
-
//
|
|
1041
|
-
const
|
|
1042
|
-
const review = await reviewRepo.getLocalReviewById(reviewId);
|
|
986
|
+
// Compute fresh digest for the new diff
|
|
987
|
+
const digest = await computeLocalDiffDigest(localPath);
|
|
1043
988
|
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
989
|
+
// Update the stored diff data for the appropriate session
|
|
990
|
+
const targetSessionId = sessionChanged ? newSessionId : reviewId;
|
|
991
|
+
setLocalReviewDiff(targetSessionId, { diff, stats, digest });
|
|
992
|
+
|
|
993
|
+
// Persist diff to database for future session recovery
|
|
994
|
+
try {
|
|
995
|
+
await reviewRepo.saveLocalDiff(targetSessionId, { diff, stats, digest });
|
|
996
|
+
} catch (persistError) {
|
|
997
|
+
logger.warn(`Could not persist diff to database: ${persistError.message}`);
|
|
1049
998
|
}
|
|
1050
999
|
|
|
1051
|
-
|
|
1052
|
-
// This ensures both modes use the same query logic and include the same columns
|
|
1053
|
-
const commentRepo = new CommentRepository(db);
|
|
1054
|
-
const { includeDismissed } = req.query;
|
|
1055
|
-
const comments = await commentRepo.getUserComments(reviewId, {
|
|
1056
|
-
includeDismissed: includeDismissed === 'true'
|
|
1057
|
-
});
|
|
1000
|
+
logger.success(`Diff refreshed: ${stats.unstagedChanges} unstaged, ${stats.untrackedFiles} untracked${stats.stagedChanges > 0 ? ` (${stats.stagedChanges} staged excluded)` : ''}`);
|
|
1058
1001
|
|
|
1059
1002
|
res.json({
|
|
1060
1003
|
success: true,
|
|
1061
|
-
|
|
1004
|
+
message: 'Diff refreshed successfully',
|
|
1005
|
+
sessionChanged,
|
|
1006
|
+
newSessionId: sessionChanged ? newSessionId : null,
|
|
1007
|
+
newHeadSha: sessionChanged ? currentHeadSha : null,
|
|
1008
|
+
originalHeadSha: originalHeadSha,
|
|
1009
|
+
stats: {
|
|
1010
|
+
trackedChanges: stats.trackedChanges || 0,
|
|
1011
|
+
untrackedFiles: stats.untrackedFiles || 0,
|
|
1012
|
+
stagedChanges: stats.stagedChanges || 0,
|
|
1013
|
+
unstagedChanges: stats.unstagedChanges || 0
|
|
1014
|
+
}
|
|
1062
1015
|
});
|
|
1063
1016
|
|
|
1064
1017
|
} catch (error) {
|
|
1065
|
-
logger.error('Error
|
|
1018
|
+
logger.error('Error refreshing local diff:', error);
|
|
1066
1019
|
res.status(500).json({
|
|
1067
|
-
error: 'Failed to
|
|
1020
|
+
error: 'Failed to refresh diff: ' + error.message
|
|
1068
1021
|
});
|
|
1069
1022
|
}
|
|
1070
1023
|
});
|
|
1071
1024
|
|
|
1072
1025
|
/**
|
|
1073
|
-
*
|
|
1026
|
+
* Get review settings for a local review
|
|
1027
|
+
* Returns the custom_instructions from the review record
|
|
1074
1028
|
*/
|
|
1075
|
-
router.
|
|
1029
|
+
router.get('/api/local/:reviewId/review-settings', async (req, res) => {
|
|
1076
1030
|
try {
|
|
1077
1031
|
const reviewId = parseInt(req.params.reviewId);
|
|
1078
1032
|
|
|
@@ -1082,60 +1036,46 @@ router.post('/api/local/:reviewId/user-comments', async (req, res) => {
|
|
|
1082
1036
|
});
|
|
1083
1037
|
}
|
|
1084
1038
|
|
|
1085
|
-
const { file, line_start, line_end, diff_position, side, body, parent_id, type, title } = req.body;
|
|
1086
|
-
|
|
1087
|
-
if (!file || !line_start || !body) {
|
|
1088
|
-
return res.status(400).json({
|
|
1089
|
-
error: 'Missing required fields: file, line_start, body'
|
|
1090
|
-
});
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
1039
|
const db = req.app.get('db');
|
|
1094
|
-
|
|
1095
|
-
// Verify review exists
|
|
1096
1040
|
const reviewRepo = new ReviewRepository(db);
|
|
1097
1041
|
const review = await reviewRepo.getLocalReviewById(reviewId);
|
|
1098
1042
|
|
|
1099
1043
|
if (!review) {
|
|
1100
|
-
return res.
|
|
1101
|
-
|
|
1044
|
+
return res.json({
|
|
1045
|
+
custom_instructions: null,
|
|
1046
|
+
last_council_id: null
|
|
1102
1047
|
});
|
|
1103
1048
|
}
|
|
1104
1049
|
|
|
1105
|
-
//
|
|
1106
|
-
|
|
1107
|
-
const
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
parent_id,
|
|
1116
|
-
type,
|
|
1117
|
-
title
|
|
1118
|
-
});
|
|
1050
|
+
// Find the last council used for this review
|
|
1051
|
+
let last_council_id = null;
|
|
1052
|
+
const lastCouncilRun = await queryOne(db, `
|
|
1053
|
+
SELECT model FROM analysis_runs
|
|
1054
|
+
WHERE review_id = ? AND provider = 'council' AND model != 'inline-config'
|
|
1055
|
+
ORDER BY started_at DESC LIMIT 1
|
|
1056
|
+
`, [review.id]);
|
|
1057
|
+
if (lastCouncilRun) {
|
|
1058
|
+
last_council_id = lastCouncilRun.model;
|
|
1059
|
+
}
|
|
1119
1060
|
|
|
1120
1061
|
res.json({
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
message: 'Comment saved successfully'
|
|
1062
|
+
custom_instructions: review.custom_instructions || null,
|
|
1063
|
+
last_council_id
|
|
1124
1064
|
});
|
|
1125
1065
|
|
|
1126
1066
|
} catch (error) {
|
|
1127
|
-
logger.error('Error
|
|
1067
|
+
logger.error('Error fetching local review settings:', error);
|
|
1128
1068
|
res.status(500).json({
|
|
1129
|
-
error:
|
|
1069
|
+
error: 'Failed to fetch review settings'
|
|
1130
1070
|
});
|
|
1131
1071
|
}
|
|
1132
1072
|
});
|
|
1133
1073
|
|
|
1134
1074
|
/**
|
|
1135
|
-
*
|
|
1136
|
-
*
|
|
1075
|
+
* Save review settings for a local review
|
|
1076
|
+
* Saves the custom_instructions to the review record
|
|
1137
1077
|
*/
|
|
1138
|
-
router.post('/api/local/:reviewId/
|
|
1078
|
+
router.post('/api/local/:reviewId/review-settings', async (req, res) => {
|
|
1139
1079
|
try {
|
|
1140
1080
|
const reviewId = parseInt(req.params.reviewId);
|
|
1141
1081
|
|
|
@@ -1145,998 +1085,150 @@ router.post('/api/local/:reviewId/file-comment', async (req, res) => {
|
|
|
1145
1085
|
});
|
|
1146
1086
|
}
|
|
1147
1087
|
|
|
1148
|
-
const {
|
|
1149
|
-
|
|
1150
|
-
if (!file || !body) {
|
|
1151
|
-
return res.status(400).json({
|
|
1152
|
-
error: 'Missing required fields: file, body'
|
|
1153
|
-
});
|
|
1154
|
-
}
|
|
1155
|
-
|
|
1156
|
-
// Validate body is not just whitespace
|
|
1157
|
-
const trimmedBody = body.trim();
|
|
1158
|
-
if (trimmedBody.length === 0) {
|
|
1159
|
-
return res.status(400).json({
|
|
1160
|
-
error: 'Comment body cannot be empty or whitespace only'
|
|
1161
|
-
});
|
|
1162
|
-
}
|
|
1088
|
+
const { custom_instructions } = req.body;
|
|
1163
1089
|
|
|
1164
1090
|
const db = req.app.get('db');
|
|
1165
|
-
|
|
1166
|
-
// Verify review exists
|
|
1167
1091
|
const reviewRepo = new ReviewRepository(db);
|
|
1168
1092
|
const review = await reviewRepo.getLocalReviewById(reviewId);
|
|
1169
1093
|
|
|
1170
1094
|
if (!review) {
|
|
1171
1095
|
return res.status(404).json({
|
|
1172
|
-
error:
|
|
1096
|
+
error: `Local review #${reviewId} not found`
|
|
1173
1097
|
});
|
|
1174
1098
|
}
|
|
1175
1099
|
|
|
1176
|
-
//
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
review_id: reviewId,
|
|
1180
|
-
file,
|
|
1181
|
-
body: trimmedBody,
|
|
1182
|
-
type,
|
|
1183
|
-
title,
|
|
1184
|
-
parent_id
|
|
1100
|
+
// Update the review with custom instructions
|
|
1101
|
+
await reviewRepo.updateReview(reviewId, {
|
|
1102
|
+
customInstructions: custom_instructions || null
|
|
1185
1103
|
});
|
|
1186
1104
|
|
|
1187
1105
|
res.json({
|
|
1188
1106
|
success: true,
|
|
1189
|
-
|
|
1190
|
-
message: 'File-level comment saved successfully'
|
|
1107
|
+
custom_instructions: custom_instructions || null
|
|
1191
1108
|
});
|
|
1192
1109
|
|
|
1193
1110
|
} catch (error) {
|
|
1194
|
-
logger.error('Error
|
|
1111
|
+
logger.error('Error saving local review settings:', error);
|
|
1195
1112
|
res.status(500).json({
|
|
1196
|
-
error:
|
|
1113
|
+
error: 'Failed to save review settings'
|
|
1197
1114
|
});
|
|
1198
1115
|
}
|
|
1199
1116
|
});
|
|
1200
1117
|
|
|
1201
1118
|
/**
|
|
1202
|
-
*
|
|
1119
|
+
* Trigger council analysis for a local review
|
|
1203
1120
|
*/
|
|
1204
|
-
router.
|
|
1121
|
+
router.post('/api/local/:reviewId/analyses/council', async (req, res) => {
|
|
1205
1122
|
try {
|
|
1206
|
-
const reviewId = parseInt(req.params.reviewId);
|
|
1207
|
-
const
|
|
1123
|
+
const reviewId = parseInt(req.params.reviewId, 10);
|
|
1124
|
+
const { councilId, councilConfig: inlineConfig, customInstructions: rawInstructions, configType: requestConfigType } = req.body || {};
|
|
1208
1125
|
|
|
1209
1126
|
if (isNaN(reviewId) || reviewId <= 0) {
|
|
1210
|
-
return res.status(400).json({
|
|
1211
|
-
error: 'Invalid review ID'
|
|
1212
|
-
});
|
|
1213
|
-
}
|
|
1214
|
-
|
|
1215
|
-
if (isNaN(commentId) || commentId <= 0) {
|
|
1216
|
-
return res.status(400).json({
|
|
1217
|
-
error: 'Invalid comment ID'
|
|
1218
|
-
});
|
|
1127
|
+
return res.status(400).json({ error: 'Invalid review ID' });
|
|
1219
1128
|
}
|
|
1220
1129
|
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
if (!body || !body.trim()) {
|
|
1224
|
-
return res.status(400).json({
|
|
1225
|
-
error: 'Comment body cannot be empty'
|
|
1226
|
-
});
|
|
1130
|
+
if (!councilId && !inlineConfig) {
|
|
1131
|
+
return res.status(400).json({ error: 'Either councilId or councilConfig is required' });
|
|
1227
1132
|
}
|
|
1228
1133
|
|
|
1229
1134
|
const db = req.app.get('db');
|
|
1230
1135
|
|
|
1231
|
-
//
|
|
1232
|
-
const
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
if (!comment) {
|
|
1237
|
-
return res.status(404).json({
|
|
1238
|
-
error: 'File-level comment not found'
|
|
1239
|
-
});
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
// Update comment
|
|
1243
|
-
await run(db, `
|
|
1244
|
-
UPDATE comments
|
|
1245
|
-
SET body = ?, updated_at = CURRENT_TIMESTAMP
|
|
1246
|
-
WHERE id = ?
|
|
1247
|
-
`, [body.trim(), commentId]);
|
|
1248
|
-
|
|
1249
|
-
res.json({
|
|
1250
|
-
success: true,
|
|
1251
|
-
message: 'File-level comment updated successfully'
|
|
1252
|
-
});
|
|
1253
|
-
|
|
1254
|
-
} catch (error) {
|
|
1255
|
-
logger.error('Error updating file-level comment:', error);
|
|
1256
|
-
res.status(500).json({
|
|
1257
|
-
error: 'Failed to update comment'
|
|
1258
|
-
});
|
|
1259
|
-
}
|
|
1260
|
-
});
|
|
1261
|
-
|
|
1262
|
-
/**
|
|
1263
|
-
* Delete file-level comment from a local review
|
|
1264
|
-
*/
|
|
1265
|
-
router.delete('/api/local/:reviewId/file-comment/:commentId', async (req, res) => {
|
|
1266
|
-
try {
|
|
1267
|
-
const reviewId = parseInt(req.params.reviewId);
|
|
1268
|
-
const commentId = parseInt(req.params.commentId);
|
|
1269
|
-
|
|
1270
|
-
if (isNaN(reviewId) || reviewId <= 0) {
|
|
1271
|
-
return res.status(400).json({
|
|
1272
|
-
error: 'Invalid review ID'
|
|
1273
|
-
});
|
|
1136
|
+
// Get review record
|
|
1137
|
+
const review = await queryOne(db, 'SELECT * FROM reviews WHERE id = ? AND review_type = ?', [reviewId, 'local']);
|
|
1138
|
+
if (!review) {
|
|
1139
|
+
return res.status(404).json({ error: 'Local review not found' });
|
|
1274
1140
|
}
|
|
1275
1141
|
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1142
|
+
// Resolve council config and determine config type
|
|
1143
|
+
let councilConfig;
|
|
1144
|
+
let configType;
|
|
1145
|
+
if (councilId) {
|
|
1146
|
+
const councilRepo = new CouncilRepository(db);
|
|
1147
|
+
const council = await councilRepo.getById(councilId);
|
|
1148
|
+
if (!council) {
|
|
1149
|
+
return res.status(404).json({ error: 'Council not found' });
|
|
1150
|
+
}
|
|
1151
|
+
councilConfig = council.config;
|
|
1152
|
+
configType = requestConfigType || council.type || 'advanced';
|
|
1153
|
+
} else {
|
|
1154
|
+
councilConfig = inlineConfig;
|
|
1155
|
+
configType = requestConfigType || 'advanced';
|
|
1280
1156
|
}
|
|
1281
1157
|
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
// Verify the comment exists, belongs to this review, and is a file-level comment
|
|
1285
|
-
const comment = await queryOne(db, `
|
|
1286
|
-
SELECT * FROM comments WHERE id = ? AND review_id = ? AND source = 'user' AND is_file_level = 1
|
|
1287
|
-
`, [commentId, reviewId]);
|
|
1158
|
+
councilConfig = normalizeCouncilConfig(councilConfig, configType);
|
|
1288
1159
|
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
});
|
|
1160
|
+
const configError = validateCouncilConfig(councilConfig, configType);
|
|
1161
|
+
if (configError) {
|
|
1162
|
+
return res.status(400).json({ error: `Invalid council config: ${configError}` });
|
|
1293
1163
|
}
|
|
1294
1164
|
|
|
1295
|
-
|
|
1296
|
-
const commentRepo = new CommentRepository(db);
|
|
1297
|
-
const result = await commentRepo.deleteComment(commentId);
|
|
1298
|
-
|
|
1299
|
-
res.json({
|
|
1300
|
-
success: true,
|
|
1301
|
-
message: 'File-level comment deleted successfully',
|
|
1302
|
-
dismissedSuggestionId: result.dismissedSuggestionId
|
|
1303
|
-
});
|
|
1304
|
-
|
|
1305
|
-
} catch (error) {
|
|
1306
|
-
logger.error('Error deleting file-level comment:', error);
|
|
1307
|
-
res.status(500).json({
|
|
1308
|
-
error: 'Failed to delete comment'
|
|
1309
|
-
});
|
|
1310
|
-
}
|
|
1311
|
-
});
|
|
1312
|
-
|
|
1313
|
-
/**
|
|
1314
|
-
* Update AI suggestion status for a local review
|
|
1315
|
-
* Sets status to 'adopted', 'dismissed', or 'active' (restored)
|
|
1316
|
-
*/
|
|
1317
|
-
router.post('/api/local/:reviewId/ai-suggestion/:suggestionId/status', async (req, res) => {
|
|
1318
|
-
try {
|
|
1319
|
-
const reviewId = parseInt(req.params.reviewId);
|
|
1320
|
-
const suggestionId = parseInt(req.params.suggestionId);
|
|
1321
|
-
const { status } = req.body;
|
|
1165
|
+
const localPath = review.local_path;
|
|
1322
1166
|
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1167
|
+
const prMetadata = {
|
|
1168
|
+
reviewType: 'local',
|
|
1169
|
+
repository: review.repository,
|
|
1170
|
+
title: review.name || 'Local changes',
|
|
1171
|
+
description: '',
|
|
1172
|
+
head_sha: review.local_head_sha
|
|
1173
|
+
};
|
|
1328
1174
|
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
error: 'Invalid suggestion ID'
|
|
1332
|
-
});
|
|
1333
|
-
}
|
|
1175
|
+
const analyzer = new Analyzer(db, 'council', 'council');
|
|
1176
|
+
const changedFiles = await analyzer.getLocalChangedFiles(localPath);
|
|
1334
1177
|
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1178
|
+
// Generate and cache diff
|
|
1179
|
+
try {
|
|
1180
|
+
const diffResult = await generateLocalDiff(localPath);
|
|
1181
|
+
const digest = await computeLocalDiffDigest(localPath);
|
|
1182
|
+
setLocalReviewDiff(reviewId, { diff: diffResult.diff, stats: diffResult.stats, digest });
|
|
1183
|
+
} catch (diffError) {
|
|
1184
|
+
logger.warn(`Could not generate diff for local council review ${reviewId}: ${diffError.message}`);
|
|
1339
1185
|
}
|
|
1340
1186
|
|
|
1341
|
-
|
|
1342
|
-
const
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
const
|
|
1346
|
-
|
|
1347
|
-
if (!suggestion) {
|
|
1348
|
-
return res.status(404).json({
|
|
1349
|
-
error: 'AI suggestion not found'
|
|
1350
|
-
});
|
|
1351
|
-
}
|
|
1187
|
+
// Resolve instructions
|
|
1188
|
+
const repoSettingsRepo = new RepoSettingsRepository(db);
|
|
1189
|
+
const reviewRepo = new ReviewRepository(db);
|
|
1190
|
+
const repoSettings = await repoSettingsRepo.getRepoSettings(review.repository);
|
|
1191
|
+
const repoInstructions = repoSettings?.default_instructions || null;
|
|
1192
|
+
const requestInstructions = rawInstructions?.trim() || null;
|
|
1352
1193
|
|
|
1353
|
-
if (
|
|
1354
|
-
|
|
1355
|
-
|
|
1194
|
+
if (requestInstructions) {
|
|
1195
|
+
await reviewRepo.updateReview(reviewId, {
|
|
1196
|
+
customInstructions: requestInstructions
|
|
1356
1197
|
});
|
|
1357
1198
|
}
|
|
1358
1199
|
|
|
1359
|
-
//
|
|
1360
|
-
|
|
1200
|
+
// Import launchCouncilAnalysis from analyses.js
|
|
1201
|
+
const analysesRouter = require('./analyses');
|
|
1202
|
+
const { analysisId, runId } = await analysesRouter.launchCouncilAnalysis(
|
|
1203
|
+
db,
|
|
1204
|
+
{
|
|
1205
|
+
reviewId,
|
|
1206
|
+
worktreePath: localPath,
|
|
1207
|
+
prMetadata,
|
|
1208
|
+
changedFiles,
|
|
1209
|
+
repository: review.repository,
|
|
1210
|
+
headSha: review.local_head_sha,
|
|
1211
|
+
logLabel: `local review #${reviewId}`,
|
|
1212
|
+
initialStatusExtra: { reviewId, reviewType: 'local' },
|
|
1213
|
+
extraBroadcastKeys: [`review-${reviewId}`],
|
|
1214
|
+
runUpdateExtra: { filesAnalyzed: changedFiles.length }
|
|
1215
|
+
},
|
|
1216
|
+
councilConfig,
|
|
1217
|
+
councilId,
|
|
1218
|
+
{ repoInstructions, requestInstructions },
|
|
1219
|
+
configType
|
|
1220
|
+
);
|
|
1361
1221
|
|
|
1362
1222
|
res.json({
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
logger.error('Error updating AI suggestion status:', error);
|
|
1369
|
-
res.status(500).json({
|
|
1370
|
-
error: error.message || 'Failed to update suggestion status'
|
|
1223
|
+
analysisId,
|
|
1224
|
+
runId,
|
|
1225
|
+
status: 'started',
|
|
1226
|
+
message: 'Council analysis started in background',
|
|
1227
|
+
isCouncil: true
|
|
1371
1228
|
});
|
|
1372
|
-
}
|
|
1373
|
-
});
|
|
1374
|
-
|
|
1375
|
-
/**
|
|
1376
|
-
* Get a single user comment from a local review
|
|
1377
|
-
*/
|
|
1378
|
-
router.get('/api/local/:reviewId/user-comments/:commentId', async (req, res) => {
|
|
1379
|
-
try {
|
|
1380
|
-
const reviewId = parseInt(req.params.reviewId);
|
|
1381
|
-
const commentId = parseInt(req.params.commentId);
|
|
1382
|
-
|
|
1383
|
-
if (isNaN(reviewId) || reviewId <= 0) {
|
|
1384
|
-
return res.status(400).json({
|
|
1385
|
-
error: 'Invalid review ID'
|
|
1386
|
-
});
|
|
1387
|
-
}
|
|
1388
|
-
|
|
1389
|
-
if (isNaN(commentId) || commentId <= 0) {
|
|
1390
|
-
return res.status(400).json({
|
|
1391
|
-
error: 'Invalid comment ID'
|
|
1392
|
-
});
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
const db = req.app.get('db');
|
|
1396
|
-
|
|
1397
|
-
// Get the comment and verify it belongs to this review
|
|
1398
|
-
const comment = await queryOne(db, `
|
|
1399
|
-
SELECT * FROM comments WHERE id = ? AND review_id = ? AND source = 'user'
|
|
1400
|
-
`, [commentId, reviewId]);
|
|
1401
|
-
|
|
1402
|
-
if (!comment) {
|
|
1403
|
-
return res.status(404).json({
|
|
1404
|
-
error: 'User comment not found'
|
|
1405
|
-
});
|
|
1406
|
-
}
|
|
1407
|
-
|
|
1408
|
-
res.json({
|
|
1409
|
-
id: comment.id,
|
|
1410
|
-
file: comment.file,
|
|
1411
|
-
line_start: comment.line_start,
|
|
1412
|
-
line_end: comment.line_end,
|
|
1413
|
-
body: comment.body,
|
|
1414
|
-
type: comment.type,
|
|
1415
|
-
title: comment.title,
|
|
1416
|
-
status: comment.status,
|
|
1417
|
-
created_at: comment.created_at,
|
|
1418
|
-
updated_at: comment.updated_at
|
|
1419
|
-
});
|
|
1420
|
-
|
|
1421
|
-
} catch (error) {
|
|
1422
|
-
logger.error('Error fetching local review user comment:', error);
|
|
1423
|
-
res.status(500).json({
|
|
1424
|
-
error: 'Failed to fetch comment'
|
|
1425
|
-
});
|
|
1426
|
-
}
|
|
1427
|
-
});
|
|
1428
|
-
|
|
1429
|
-
/**
|
|
1430
|
-
* Update user comment in a local review
|
|
1431
|
-
*/
|
|
1432
|
-
router.put('/api/local/:reviewId/user-comments/:commentId', async (req, res) => {
|
|
1433
|
-
try {
|
|
1434
|
-
const reviewId = parseInt(req.params.reviewId);
|
|
1435
|
-
const commentId = parseInt(req.params.commentId);
|
|
1436
|
-
|
|
1437
|
-
if (isNaN(reviewId) || reviewId <= 0) {
|
|
1438
|
-
return res.status(400).json({
|
|
1439
|
-
error: 'Invalid review ID'
|
|
1440
|
-
});
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
if (isNaN(commentId) || commentId <= 0) {
|
|
1444
|
-
return res.status(400).json({
|
|
1445
|
-
error: 'Invalid comment ID'
|
|
1446
|
-
});
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
const { body } = req.body;
|
|
1450
|
-
|
|
1451
|
-
if (!body || !body.trim()) {
|
|
1452
|
-
return res.status(400).json({
|
|
1453
|
-
error: 'Comment body cannot be empty'
|
|
1454
|
-
});
|
|
1455
|
-
}
|
|
1456
|
-
|
|
1457
|
-
const db = req.app.get('db');
|
|
1458
|
-
|
|
1459
|
-
// Verify the comment exists and belongs to this review
|
|
1460
|
-
const comment = await queryOne(db, `
|
|
1461
|
-
SELECT * FROM comments WHERE id = ? AND review_id = ? AND source = 'user'
|
|
1462
|
-
`, [commentId, reviewId]);
|
|
1463
|
-
|
|
1464
|
-
if (!comment) {
|
|
1465
|
-
return res.status(404).json({
|
|
1466
|
-
error: 'User comment not found'
|
|
1467
|
-
});
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
// Update comment
|
|
1471
|
-
await run(db, `
|
|
1472
|
-
UPDATE comments
|
|
1473
|
-
SET body = ?, updated_at = CURRENT_TIMESTAMP
|
|
1474
|
-
WHERE id = ?
|
|
1475
|
-
`, [body.trim(), commentId]);
|
|
1476
|
-
|
|
1477
|
-
res.json({
|
|
1478
|
-
success: true,
|
|
1479
|
-
message: 'Comment updated successfully'
|
|
1480
|
-
});
|
|
1481
|
-
|
|
1482
|
-
} catch (error) {
|
|
1483
|
-
logger.error('Error updating local review user comment:', error);
|
|
1484
|
-
res.status(500).json({
|
|
1485
|
-
error: 'Failed to update comment'
|
|
1486
|
-
});
|
|
1487
|
-
}
|
|
1488
|
-
});
|
|
1489
|
-
|
|
1490
|
-
/**
|
|
1491
|
-
* Bulk delete all user comments for a local review
|
|
1492
|
-
* Also dismisses any AI suggestions that were parents of the deleted comments.
|
|
1493
|
-
*/
|
|
1494
|
-
router.delete('/api/local/:reviewId/user-comments', async (req, res) => {
|
|
1495
|
-
try {
|
|
1496
|
-
const reviewId = parseInt(req.params.reviewId);
|
|
1497
|
-
|
|
1498
|
-
if (isNaN(reviewId) || reviewId <= 0) {
|
|
1499
|
-
return res.status(400).json({
|
|
1500
|
-
error: 'Invalid review ID'
|
|
1501
|
-
});
|
|
1502
|
-
}
|
|
1503
|
-
|
|
1504
|
-
const db = req.app.get('db');
|
|
1505
|
-
|
|
1506
|
-
// Verify review exists
|
|
1507
|
-
const reviewRepo = new ReviewRepository(db);
|
|
1508
|
-
const review = await reviewRepo.getLocalReviewById(reviewId);
|
|
1509
|
-
|
|
1510
|
-
if (!review) {
|
|
1511
|
-
return res.status(404).json({
|
|
1512
|
-
error: `Local review #${reviewId} not found`
|
|
1513
|
-
});
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
// Begin transaction to ensure atomicity
|
|
1517
|
-
await run(db, 'BEGIN TRANSACTION');
|
|
1518
|
-
|
|
1519
|
-
try {
|
|
1520
|
-
// Bulk delete using repository (also dismisses parent AI suggestions)
|
|
1521
|
-
const commentRepo = new CommentRepository(db);
|
|
1522
|
-
const result = await commentRepo.bulkDeleteComments(reviewId);
|
|
1523
|
-
|
|
1524
|
-
// Commit transaction
|
|
1525
|
-
await run(db, 'COMMIT');
|
|
1526
|
-
|
|
1527
|
-
res.json({
|
|
1528
|
-
success: true,
|
|
1529
|
-
deletedCount: result.deletedCount,
|
|
1530
|
-
dismissedSuggestionIds: result.dismissedSuggestionIds,
|
|
1531
|
-
message: `Deleted ${result.deletedCount} user comment${result.deletedCount !== 1 ? 's' : ''}`
|
|
1532
|
-
});
|
|
1533
|
-
|
|
1534
|
-
} catch (transactionError) {
|
|
1535
|
-
// Rollback transaction on error
|
|
1536
|
-
await run(db, 'ROLLBACK');
|
|
1537
|
-
throw transactionError;
|
|
1538
|
-
}
|
|
1539
|
-
|
|
1540
|
-
} catch (error) {
|
|
1541
|
-
logger.error('Error deleting all local review user comments:', error);
|
|
1542
|
-
res.status(500).json({
|
|
1543
|
-
error: 'Failed to delete comments'
|
|
1544
|
-
});
|
|
1545
|
-
}
|
|
1546
|
-
});
|
|
1547
|
-
|
|
1548
|
-
/**
|
|
1549
|
-
* Delete user comment from a local review
|
|
1550
|
-
* If the comment was adopted from an AI suggestion, the parent suggestion
|
|
1551
|
-
* is automatically transitioned to 'dismissed' state.
|
|
1552
|
-
*/
|
|
1553
|
-
router.delete('/api/local/:reviewId/user-comments/:commentId', async (req, res) => {
|
|
1554
|
-
try {
|
|
1555
|
-
const reviewId = parseInt(req.params.reviewId);
|
|
1556
|
-
const commentId = parseInt(req.params.commentId);
|
|
1557
|
-
|
|
1558
|
-
if (isNaN(reviewId) || reviewId <= 0) {
|
|
1559
|
-
return res.status(400).json({
|
|
1560
|
-
error: 'Invalid review ID'
|
|
1561
|
-
});
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
if (isNaN(commentId) || commentId <= 0) {
|
|
1565
|
-
return res.status(400).json({
|
|
1566
|
-
error: 'Invalid comment ID'
|
|
1567
|
-
});
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
const db = req.app.get('db');
|
|
1571
|
-
|
|
1572
|
-
// Verify the comment exists and belongs to this review
|
|
1573
|
-
const comment = await queryOne(db, `
|
|
1574
|
-
SELECT * FROM comments WHERE id = ? AND review_id = ? AND source = 'user'
|
|
1575
|
-
`, [commentId, reviewId]);
|
|
1576
|
-
|
|
1577
|
-
if (!comment) {
|
|
1578
|
-
return res.status(404).json({
|
|
1579
|
-
error: 'User comment not found'
|
|
1580
|
-
});
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
// Use CommentRepository to delete (also dismisses parent AI suggestion if applicable)
|
|
1584
|
-
const commentRepo = new CommentRepository(db);
|
|
1585
|
-
const result = await commentRepo.deleteComment(commentId);
|
|
1586
|
-
|
|
1587
|
-
res.json({
|
|
1588
|
-
success: true,
|
|
1589
|
-
message: 'Comment deleted successfully',
|
|
1590
|
-
dismissedSuggestionId: result.dismissedSuggestionId
|
|
1591
|
-
});
|
|
1592
|
-
|
|
1593
|
-
} catch (error) {
|
|
1594
|
-
logger.error('Error deleting local review user comment:', error);
|
|
1595
|
-
res.status(500).json({
|
|
1596
|
-
error: 'Failed to delete comment'
|
|
1597
|
-
});
|
|
1598
|
-
}
|
|
1599
|
-
});
|
|
1600
|
-
|
|
1601
|
-
/**
|
|
1602
|
-
* Restore a dismissed user comment in a local review
|
|
1603
|
-
* Sets status from 'inactive' back to 'active'
|
|
1604
|
-
*/
|
|
1605
|
-
router.put('/api/local/:reviewId/user-comments/:commentId/restore', async (req, res) => {
|
|
1606
|
-
try {
|
|
1607
|
-
const reviewId = parseInt(req.params.reviewId);
|
|
1608
|
-
const commentId = parseInt(req.params.commentId);
|
|
1609
|
-
|
|
1610
|
-
if (isNaN(reviewId) || reviewId <= 0) {
|
|
1611
|
-
return res.status(400).json({
|
|
1612
|
-
error: 'Invalid review ID'
|
|
1613
|
-
});
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
if (isNaN(commentId) || commentId <= 0) {
|
|
1617
|
-
return res.status(400).json({
|
|
1618
|
-
error: 'Invalid comment ID'
|
|
1619
|
-
});
|
|
1620
|
-
}
|
|
1621
|
-
|
|
1622
|
-
const db = req.app.get('db');
|
|
1623
|
-
|
|
1624
|
-
// Verify the comment exists and belongs to this review
|
|
1625
|
-
const comment = await queryOne(db, `
|
|
1626
|
-
SELECT * FROM comments WHERE id = ? AND review_id = ? AND source = 'user'
|
|
1627
|
-
`, [commentId, reviewId]);
|
|
1628
|
-
|
|
1629
|
-
if (!comment) {
|
|
1630
|
-
return res.status(404).json({
|
|
1631
|
-
error: 'User comment not found'
|
|
1632
|
-
});
|
|
1633
|
-
}
|
|
1634
|
-
|
|
1635
|
-
if (comment.status !== 'inactive') {
|
|
1636
|
-
return res.status(400).json({
|
|
1637
|
-
error: 'Comment is not dismissed'
|
|
1638
|
-
});
|
|
1639
|
-
}
|
|
1640
|
-
|
|
1641
|
-
// Restore the comment using CommentRepository
|
|
1642
|
-
const commentRepo = new CommentRepository(db);
|
|
1643
|
-
await commentRepo.restoreComment(commentId);
|
|
1644
|
-
|
|
1645
|
-
// Get the restored comment to return
|
|
1646
|
-
const restoredComment = await commentRepo.getComment(commentId, 'user');
|
|
1647
|
-
|
|
1648
|
-
res.json({
|
|
1649
|
-
success: true,
|
|
1650
|
-
message: 'Comment restored successfully',
|
|
1651
|
-
comment: restoredComment
|
|
1652
|
-
});
|
|
1653
|
-
|
|
1654
|
-
} catch (error) {
|
|
1655
|
-
logger.error('Error restoring local review user comment:', error);
|
|
1656
|
-
res.status(500).json({
|
|
1657
|
-
error: error.message || 'Failed to restore comment'
|
|
1658
|
-
});
|
|
1659
|
-
}
|
|
1660
|
-
});
|
|
1661
|
-
|
|
1662
|
-
/**
|
|
1663
|
-
* Check if analysis is running for a local review
|
|
1664
|
-
*/
|
|
1665
|
-
router.get('/api/local/:reviewId/analysis-status', async (req, res) => {
|
|
1666
|
-
try {
|
|
1667
|
-
const reviewId = parseInt(req.params.reviewId);
|
|
1668
|
-
|
|
1669
|
-
if (isNaN(reviewId) || reviewId <= 0) {
|
|
1670
|
-
return res.status(400).json({
|
|
1671
|
-
error: 'Invalid review ID'
|
|
1672
|
-
});
|
|
1673
|
-
}
|
|
1674
|
-
|
|
1675
|
-
const reviewKey = getLocalReviewKey(reviewId);
|
|
1676
|
-
const analysisId = localReviewToAnalysisId.get(reviewKey);
|
|
1677
|
-
|
|
1678
|
-
if (analysisId) {
|
|
1679
|
-
const analysis = activeAnalyses.get(analysisId);
|
|
1680
|
-
|
|
1681
|
-
if (analysis) {
|
|
1682
|
-
return res.json({
|
|
1683
|
-
running: true,
|
|
1684
|
-
analysisId,
|
|
1685
|
-
status: analysis
|
|
1686
|
-
});
|
|
1687
|
-
}
|
|
1688
|
-
|
|
1689
|
-
// Clean up stale mapping
|
|
1690
|
-
localReviewToAnalysisId.delete(reviewKey);
|
|
1691
|
-
}
|
|
1692
|
-
|
|
1693
|
-
// Fall back to database — an analysis may have been started externally (e.g. via MCP)
|
|
1694
|
-
const db = req.app.get('db');
|
|
1695
|
-
const analysisRunRepo = new AnalysisRunRepository(db);
|
|
1696
|
-
const latestRun = await analysisRunRepo.getLatestByReviewId(reviewId);
|
|
1697
|
-
|
|
1698
|
-
if (latestRun && latestRun.status === 'running') {
|
|
1699
|
-
return res.json({
|
|
1700
|
-
running: true,
|
|
1701
|
-
analysisId: latestRun.id,
|
|
1702
|
-
status: {
|
|
1703
|
-
id: latestRun.id,
|
|
1704
|
-
reviewId,
|
|
1705
|
-
reviewType: 'local',
|
|
1706
|
-
status: 'running',
|
|
1707
|
-
startedAt: latestRun.started_at,
|
|
1708
|
-
progress: 'Analysis in progress...',
|
|
1709
|
-
levels: {
|
|
1710
|
-
1: { status: 'running', progress: 'Running...' },
|
|
1711
|
-
2: { status: 'running', progress: 'Running...' },
|
|
1712
|
-
3: { status: 'running', progress: 'Running...' },
|
|
1713
|
-
4: { status: 'pending', progress: 'Pending' }
|
|
1714
|
-
},
|
|
1715
|
-
filesAnalyzed: latestRun.files_analyzed || 0,
|
|
1716
|
-
filesRemaining: 0
|
|
1717
|
-
}
|
|
1718
|
-
});
|
|
1719
|
-
}
|
|
1720
|
-
|
|
1721
|
-
res.json({
|
|
1722
|
-
running: false,
|
|
1723
|
-
analysisId: null,
|
|
1724
|
-
status: null
|
|
1725
|
-
});
|
|
1726
|
-
|
|
1727
|
-
} catch (error) {
|
|
1728
|
-
logger.error('Error checking local review analysis status:', error);
|
|
1729
|
-
res.status(500).json({
|
|
1730
|
-
error: 'Failed to check analysis status'
|
|
1731
|
-
});
|
|
1732
|
-
}
|
|
1733
|
-
});
|
|
1734
|
-
|
|
1735
|
-
/**
|
|
1736
|
-
* Check if a local review has existing AI suggestions
|
|
1737
|
-
*/
|
|
1738
|
-
router.get('/api/local/:reviewId/has-ai-suggestions', async (req, res) => {
|
|
1739
|
-
try {
|
|
1740
|
-
const reviewId = parseInt(req.params.reviewId);
|
|
1741
|
-
const { runId } = req.query;
|
|
1742
|
-
|
|
1743
|
-
if (isNaN(reviewId) || reviewId <= 0) {
|
|
1744
|
-
return res.status(400).json({
|
|
1745
|
-
error: 'Invalid review ID'
|
|
1746
|
-
});
|
|
1747
|
-
}
|
|
1748
|
-
|
|
1749
|
-
const db = req.app.get('db');
|
|
1750
|
-
|
|
1751
|
-
// Verify review exists
|
|
1752
|
-
const reviewRepo = new ReviewRepository(db);
|
|
1753
|
-
const review = await reviewRepo.getLocalReviewById(reviewId);
|
|
1754
|
-
|
|
1755
|
-
if (!review) {
|
|
1756
|
-
return res.status(404).json({
|
|
1757
|
-
error: `Local review #${reviewId} not found`
|
|
1758
|
-
});
|
|
1759
|
-
}
|
|
1760
|
-
|
|
1761
|
-
// Check if any AI suggestions exist for this review
|
|
1762
|
-
// Exclude raw council voice suggestions (is_raw=1) — only count final/consolidated suggestions
|
|
1763
|
-
const result = await queryOne(db, `
|
|
1764
|
-
SELECT EXISTS(
|
|
1765
|
-
SELECT 1 FROM comments
|
|
1766
|
-
WHERE review_id = ? AND source = 'ai' AND (is_raw = 0 OR is_raw IS NULL)
|
|
1767
|
-
) as has_suggestions
|
|
1768
|
-
`, [reviewId]);
|
|
1769
|
-
|
|
1770
|
-
const hasSuggestions = result?.has_suggestions === 1;
|
|
1771
|
-
|
|
1772
|
-
// Check if any analysis has been run using analysis_runs table
|
|
1773
|
-
let analysisHasRun = hasSuggestions;
|
|
1774
|
-
const analysisRunRepo = new AnalysisRunRepository(db);
|
|
1775
|
-
let selectedRun = null;
|
|
1776
|
-
try {
|
|
1777
|
-
// If runId is provided, fetch that specific run; otherwise get the latest
|
|
1778
|
-
if (runId) {
|
|
1779
|
-
selectedRun = await analysisRunRepo.getById(runId);
|
|
1780
|
-
} else {
|
|
1781
|
-
selectedRun = await analysisRunRepo.getLatestByReviewId(reviewId);
|
|
1782
|
-
}
|
|
1783
|
-
analysisHasRun = !!(selectedRun || hasSuggestions);
|
|
1784
|
-
} catch (e) {
|
|
1785
|
-
// Log the error at debug level before falling back
|
|
1786
|
-
logger.debug('analysis_runs query failed in local mode, falling back to hasSuggestions:', e.message);
|
|
1787
|
-
// Fall back to using hasSuggestions if analysis_runs table doesn't exist
|
|
1788
|
-
analysisHasRun = hasSuggestions;
|
|
1789
|
-
}
|
|
1790
|
-
|
|
1791
|
-
// Get AI summary from the selected analysis run if available, otherwise fall back to review summary
|
|
1792
|
-
const summary = selectedRun?.summary || review?.summary || null;
|
|
1793
|
-
|
|
1794
|
-
// Get stats for AI suggestions (issues/suggestions/praise for final level only)
|
|
1795
|
-
// Filter by runId if provided, otherwise use the latest analysis run
|
|
1796
|
-
let stats = { issues: 0, suggestions: 0, praise: 0 };
|
|
1797
|
-
if (hasSuggestions) {
|
|
1798
|
-
try {
|
|
1799
|
-
const statsQuery = getStatsQuery(runId);
|
|
1800
|
-
const statsResult = await query(db, statsQuery.query, statsQuery.params(reviewId));
|
|
1801
|
-
stats = calculateStats(statsResult);
|
|
1802
|
-
} catch (e) {
|
|
1803
|
-
logger.warn('Error fetching AI suggestion stats:', e);
|
|
1804
|
-
}
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
res.json({
|
|
1808
|
-
hasSuggestions: hasSuggestions,
|
|
1809
|
-
analysisHasRun: analysisHasRun,
|
|
1810
|
-
summary: summary,
|
|
1811
|
-
stats: stats
|
|
1812
|
-
});
|
|
1813
|
-
|
|
1814
|
-
} catch (error) {
|
|
1815
|
-
logger.error('Error checking for AI suggestions:', error);
|
|
1816
|
-
res.status(500).json({
|
|
1817
|
-
error: 'Failed to check for AI suggestions'
|
|
1818
|
-
});
|
|
1819
|
-
}
|
|
1820
|
-
});
|
|
1821
|
-
|
|
1822
|
-
/**
|
|
1823
|
-
* Server-Sent Events endpoint for local review AI analysis progress
|
|
1824
|
-
*/
|
|
1825
|
-
router.get('/api/local/:reviewId/ai-suggestions/status', (req, res) => {
|
|
1826
|
-
const reviewId = parseInt(req.params.reviewId);
|
|
1827
|
-
|
|
1828
|
-
// Find the analysis ID for this local review
|
|
1829
|
-
const reviewKey = getLocalReviewKey(reviewId);
|
|
1830
|
-
const analysisId = localReviewToAnalysisId.get(reviewKey);
|
|
1831
|
-
|
|
1832
|
-
// Set up SSE headers
|
|
1833
|
-
res.writeHead(200, {
|
|
1834
|
-
'Content-Type': 'text/event-stream',
|
|
1835
|
-
'Cache-Control': 'no-cache',
|
|
1836
|
-
'Connection': 'keep-alive',
|
|
1837
|
-
'Access-Control-Allow-Origin': '*',
|
|
1838
|
-
'Access-Control-Allow-Headers': 'Cache-Control'
|
|
1839
|
-
});
|
|
1840
|
-
|
|
1841
|
-
// Send initial connection message
|
|
1842
|
-
res.write('data: {"type":"connected","message":"Connected to progress stream"}\n\n');
|
|
1843
|
-
|
|
1844
|
-
// If we have an analysis ID, use it; otherwise use a placeholder
|
|
1845
|
-
const trackingId = analysisId || `local-${reviewId}`;
|
|
1846
|
-
|
|
1847
|
-
// Store client for this analysis
|
|
1848
|
-
if (!progressClients.has(trackingId)) {
|
|
1849
|
-
progressClients.set(trackingId, new Set());
|
|
1850
|
-
}
|
|
1851
|
-
progressClients.get(trackingId).add(res);
|
|
1852
|
-
|
|
1853
|
-
// Send current status if analysis exists
|
|
1854
|
-
if (analysisId) {
|
|
1855
|
-
const currentStatus = activeAnalyses.get(analysisId);
|
|
1856
|
-
if (currentStatus) {
|
|
1857
|
-
res.write(`data: ${JSON.stringify({
|
|
1858
|
-
type: 'progress',
|
|
1859
|
-
...currentStatus
|
|
1860
|
-
})}\n\n`);
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
|
|
1864
|
-
// Handle client disconnect
|
|
1865
|
-
req.on('close', () => {
|
|
1866
|
-
const clients = progressClients.get(trackingId);
|
|
1867
|
-
if (clients) {
|
|
1868
|
-
clients.delete(res);
|
|
1869
|
-
if (clients.size === 0) {
|
|
1870
|
-
progressClients.delete(trackingId);
|
|
1871
|
-
}
|
|
1872
|
-
}
|
|
1873
|
-
});
|
|
1874
|
-
|
|
1875
|
-
req.on('error', () => {
|
|
1876
|
-
const clients = progressClients.get(trackingId);
|
|
1877
|
-
if (clients) {
|
|
1878
|
-
clients.delete(res);
|
|
1879
|
-
if (clients.size === 0) {
|
|
1880
|
-
progressClients.delete(trackingId);
|
|
1881
|
-
}
|
|
1882
|
-
}
|
|
1883
|
-
});
|
|
1884
|
-
});
|
|
1885
|
-
|
|
1886
|
-
/**
|
|
1887
|
-
* Refresh the diff for a local review
|
|
1888
|
-
* Regenerates the diff from the current state of the working directory
|
|
1889
|
-
* Returns sessionChanged flag if HEAD has changed since the session was created
|
|
1890
|
-
*/
|
|
1891
|
-
router.post('/api/local/:reviewId/refresh', async (req, res) => {
|
|
1892
|
-
try {
|
|
1893
|
-
const reviewId = parseInt(req.params.reviewId);
|
|
1894
|
-
|
|
1895
|
-
if (isNaN(reviewId) || reviewId <= 0) {
|
|
1896
|
-
return res.status(400).json({
|
|
1897
|
-
error: 'Invalid review ID'
|
|
1898
|
-
});
|
|
1899
|
-
}
|
|
1900
|
-
|
|
1901
|
-
const db = req.app.get('db');
|
|
1902
|
-
const reviewRepo = new ReviewRepository(db);
|
|
1903
|
-
const review = await reviewRepo.getLocalReviewById(reviewId);
|
|
1904
|
-
|
|
1905
|
-
if (!review) {
|
|
1906
|
-
return res.status(404).json({
|
|
1907
|
-
error: `Local review #${reviewId} not found`
|
|
1908
|
-
});
|
|
1909
|
-
}
|
|
1910
|
-
|
|
1911
|
-
const localPath = review.local_path;
|
|
1912
|
-
const originalHeadSha = review.local_head_sha;
|
|
1913
|
-
|
|
1914
|
-
if (!localPath) {
|
|
1915
|
-
return res.status(400).json({
|
|
1916
|
-
error: 'Local review is missing path information'
|
|
1917
|
-
});
|
|
1918
|
-
}
|
|
1919
|
-
|
|
1920
|
-
logger.log('API', `Refreshing diff for local review #${reviewId}`, 'cyan');
|
|
1921
|
-
logger.log('API', `Local path: ${localPath}`, 'magenta');
|
|
1922
|
-
|
|
1923
|
-
// Check if HEAD has changed
|
|
1924
|
-
const { getHeadSha } = require('../local-review');
|
|
1925
|
-
let currentHeadSha;
|
|
1926
|
-
let sessionChanged = false;
|
|
1927
|
-
let newSessionId = null;
|
|
1928
|
-
|
|
1929
|
-
try {
|
|
1930
|
-
currentHeadSha = await getHeadSha(localPath);
|
|
1931
|
-
|
|
1932
|
-
if (originalHeadSha && currentHeadSha !== originalHeadSha) {
|
|
1933
|
-
sessionChanged = true;
|
|
1934
|
-
logger.log('API', `HEAD changed: ${originalHeadSha.substring(0, 7)} -> ${currentHeadSha.substring(0, 7)}`, 'yellow');
|
|
1935
|
-
|
|
1936
|
-
// Check if a session already exists for the new HEAD
|
|
1937
|
-
const existingSession = await reviewRepo.getLocalReview(localPath, currentHeadSha);
|
|
1938
|
-
if (existingSession) {
|
|
1939
|
-
newSessionId = existingSession.id;
|
|
1940
|
-
logger.log('API', `Existing session found for new HEAD: ${newSessionId}`, 'cyan');
|
|
1941
|
-
} else {
|
|
1942
|
-
// Create a new session for the new HEAD
|
|
1943
|
-
const { getRepositoryName } = require('../local-review');
|
|
1944
|
-
const repository = await getRepositoryName(localPath);
|
|
1945
|
-
newSessionId = await reviewRepo.upsertLocalReview({
|
|
1946
|
-
localPath: localPath,
|
|
1947
|
-
localHeadSha: currentHeadSha,
|
|
1948
|
-
repository
|
|
1949
|
-
});
|
|
1950
|
-
logger.log('API', `Created new session for new HEAD: ${newSessionId}`, 'cyan');
|
|
1951
|
-
}
|
|
1952
|
-
}
|
|
1953
|
-
} catch (headError) {
|
|
1954
|
-
logger.warn(`Could not check HEAD SHA: ${headError.message}`);
|
|
1955
|
-
}
|
|
1956
|
-
|
|
1957
|
-
// Regenerate the diff from the working directory
|
|
1958
|
-
const { diff, stats } = await generateLocalDiff(localPath);
|
|
1959
|
-
|
|
1960
|
-
// Compute fresh digest for the new diff
|
|
1961
|
-
const digest = await computeLocalDiffDigest(localPath);
|
|
1962
|
-
|
|
1963
|
-
// Update the stored diff data for the appropriate session
|
|
1964
|
-
const targetSessionId = sessionChanged ? newSessionId : reviewId;
|
|
1965
|
-
localReviewDiffs.set(targetSessionId, { diff, stats, digest });
|
|
1966
|
-
|
|
1967
|
-
// Persist diff to database for future session recovery
|
|
1968
|
-
try {
|
|
1969
|
-
await reviewRepo.saveLocalDiff(targetSessionId, { diff, stats, digest });
|
|
1970
|
-
} catch (persistError) {
|
|
1971
|
-
logger.warn(`Could not persist diff to database: ${persistError.message}`);
|
|
1972
|
-
}
|
|
1973
|
-
|
|
1974
|
-
logger.success(`Diff refreshed: ${stats.unstagedChanges} unstaged, ${stats.untrackedFiles} untracked${stats.stagedChanges > 0 ? ` (${stats.stagedChanges} staged excluded)` : ''}`);
|
|
1975
|
-
|
|
1976
|
-
res.json({
|
|
1977
|
-
success: true,
|
|
1978
|
-
message: 'Diff refreshed successfully',
|
|
1979
|
-
sessionChanged,
|
|
1980
|
-
newSessionId: sessionChanged ? newSessionId : null,
|
|
1981
|
-
newHeadSha: sessionChanged ? currentHeadSha : null,
|
|
1982
|
-
originalHeadSha: originalHeadSha,
|
|
1983
|
-
stats: {
|
|
1984
|
-
trackedChanges: stats.trackedChanges || 0,
|
|
1985
|
-
untrackedFiles: stats.untrackedFiles || 0,
|
|
1986
|
-
stagedChanges: stats.stagedChanges || 0,
|
|
1987
|
-
unstagedChanges: stats.unstagedChanges || 0
|
|
1988
|
-
}
|
|
1989
|
-
});
|
|
1990
|
-
|
|
1991
|
-
} catch (error) {
|
|
1992
|
-
logger.error('Error refreshing local diff:', error);
|
|
1993
|
-
res.status(500).json({
|
|
1994
|
-
error: 'Failed to refresh diff: ' + error.message
|
|
1995
|
-
});
|
|
1996
|
-
}
|
|
1997
|
-
});
|
|
1998
|
-
|
|
1999
|
-
/**
|
|
2000
|
-
* Get review settings for a local review
|
|
2001
|
-
* Returns the custom_instructions from the review record
|
|
2002
|
-
*/
|
|
2003
|
-
router.get('/api/local/:reviewId/review-settings', async (req, res) => {
|
|
2004
|
-
try {
|
|
2005
|
-
const reviewId = parseInt(req.params.reviewId);
|
|
2006
|
-
|
|
2007
|
-
if (isNaN(reviewId) || reviewId <= 0) {
|
|
2008
|
-
return res.status(400).json({
|
|
2009
|
-
error: 'Invalid review ID'
|
|
2010
|
-
});
|
|
2011
|
-
}
|
|
2012
|
-
|
|
2013
|
-
const db = req.app.get('db');
|
|
2014
|
-
const reviewRepo = new ReviewRepository(db);
|
|
2015
|
-
const review = await reviewRepo.getLocalReviewById(reviewId);
|
|
2016
|
-
|
|
2017
|
-
if (!review) {
|
|
2018
|
-
return res.json({
|
|
2019
|
-
custom_instructions: null,
|
|
2020
|
-
last_council_id: null
|
|
2021
|
-
});
|
|
2022
|
-
}
|
|
2023
|
-
|
|
2024
|
-
// Find the last council used for this review
|
|
2025
|
-
let last_council_id = null;
|
|
2026
|
-
const lastCouncilRun = await queryOne(db, `
|
|
2027
|
-
SELECT model FROM analysis_runs
|
|
2028
|
-
WHERE review_id = ? AND provider = 'council' AND model != 'inline-config'
|
|
2029
|
-
ORDER BY started_at DESC LIMIT 1
|
|
2030
|
-
`, [review.id]);
|
|
2031
|
-
if (lastCouncilRun) {
|
|
2032
|
-
last_council_id = lastCouncilRun.model;
|
|
2033
|
-
}
|
|
2034
|
-
|
|
2035
|
-
res.json({
|
|
2036
|
-
custom_instructions: review.custom_instructions || null,
|
|
2037
|
-
last_council_id
|
|
2038
|
-
});
|
|
2039
|
-
|
|
2040
|
-
} catch (error) {
|
|
2041
|
-
logger.error('Error fetching local review settings:', error);
|
|
2042
|
-
res.status(500).json({
|
|
2043
|
-
error: 'Failed to fetch review settings'
|
|
2044
|
-
});
|
|
2045
|
-
}
|
|
2046
|
-
});
|
|
2047
|
-
|
|
2048
|
-
/**
|
|
2049
|
-
* Save review settings for a local review
|
|
2050
|
-
* Saves the custom_instructions to the review record
|
|
2051
|
-
*/
|
|
2052
|
-
router.post('/api/local/:reviewId/review-settings', async (req, res) => {
|
|
2053
|
-
try {
|
|
2054
|
-
const reviewId = parseInt(req.params.reviewId);
|
|
2055
|
-
|
|
2056
|
-
if (isNaN(reviewId) || reviewId <= 0) {
|
|
2057
|
-
return res.status(400).json({
|
|
2058
|
-
error: 'Invalid review ID'
|
|
2059
|
-
});
|
|
2060
|
-
}
|
|
2061
|
-
|
|
2062
|
-
const { custom_instructions } = req.body;
|
|
2063
|
-
|
|
2064
|
-
const db = req.app.get('db');
|
|
2065
|
-
const reviewRepo = new ReviewRepository(db);
|
|
2066
|
-
const review = await reviewRepo.getLocalReviewById(reviewId);
|
|
2067
|
-
|
|
2068
|
-
if (!review) {
|
|
2069
|
-
return res.status(404).json({
|
|
2070
|
-
error: `Local review #${reviewId} not found`
|
|
2071
|
-
});
|
|
2072
|
-
}
|
|
2073
|
-
|
|
2074
|
-
// Update the review with custom instructions
|
|
2075
|
-
await reviewRepo.updateReview(reviewId, {
|
|
2076
|
-
customInstructions: custom_instructions || null
|
|
2077
|
-
});
|
|
2078
|
-
|
|
2079
|
-
res.json({
|
|
2080
|
-
success: true,
|
|
2081
|
-
custom_instructions: custom_instructions || null
|
|
2082
|
-
});
|
|
2083
|
-
|
|
2084
|
-
} catch (error) {
|
|
2085
|
-
logger.error('Error saving local review settings:', error);
|
|
2086
|
-
res.status(500).json({
|
|
2087
|
-
error: 'Failed to save review settings'
|
|
2088
|
-
});
|
|
2089
|
-
}
|
|
2090
|
-
});
|
|
2091
|
-
|
|
2092
|
-
/**
|
|
2093
|
-
* Get all analysis runs for a local review
|
|
2094
|
-
*/
|
|
2095
|
-
router.get('/api/local/:reviewId/analysis-runs', async (req, res) => {
|
|
2096
|
-
try {
|
|
2097
|
-
const reviewId = parseInt(req.params.reviewId, 10);
|
|
2098
|
-
|
|
2099
|
-
if (isNaN(reviewId) || reviewId <= 0) {
|
|
2100
|
-
return res.status(400).json({ error: 'Invalid review ID' });
|
|
2101
|
-
}
|
|
2102
|
-
|
|
2103
|
-
const db = req.app.get('db');
|
|
2104
|
-
const analysisRunRepo = new AnalysisRunRepository(db);
|
|
2105
|
-
const runs = await analysisRunRepo.getByReviewId(reviewId);
|
|
2106
|
-
|
|
2107
|
-
res.json({ runs: runs.map(r => ({
|
|
2108
|
-
...r,
|
|
2109
|
-
levels_config: r.levels_config ? JSON.parse(r.levels_config) : null
|
|
2110
|
-
})) });
|
|
2111
|
-
} catch (error) {
|
|
2112
|
-
logger.error('Error fetching analysis runs:', error);
|
|
2113
|
-
res.status(500).json({ error: 'Failed to fetch analysis runs' });
|
|
2114
|
-
}
|
|
2115
|
-
});
|
|
2116
|
-
|
|
2117
|
-
/**
|
|
2118
|
-
* Get the most recent analysis run for a local review
|
|
2119
|
-
*/
|
|
2120
|
-
router.get('/api/local/:reviewId/analysis-runs/latest', async (req, res) => {
|
|
2121
|
-
try {
|
|
2122
|
-
const reviewId = parseInt(req.params.reviewId, 10);
|
|
2123
|
-
|
|
2124
|
-
if (isNaN(reviewId) || reviewId <= 0) {
|
|
2125
|
-
return res.status(400).json({ error: 'Invalid review ID' });
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
const db = req.app.get('db');
|
|
2129
|
-
const analysisRunRepo = new AnalysisRunRepository(db);
|
|
2130
|
-
const run = await analysisRunRepo.getLatestByReviewId(reviewId);
|
|
2131
|
-
|
|
2132
|
-
if (!run) {
|
|
2133
|
-
return res.status(404).json({ error: 'No analysis runs found' });
|
|
2134
|
-
}
|
|
2135
|
-
|
|
2136
|
-
res.json({ run });
|
|
2137
1229
|
} catch (error) {
|
|
2138
|
-
logger.error('Error
|
|
2139
|
-
res.status(500).json({ error: 'Failed to
|
|
1230
|
+
logger.error('Error starting local council analysis:', error);
|
|
1231
|
+
res.status(500).json({ error: 'Failed to start council analysis' });
|
|
2140
1232
|
}
|
|
2141
1233
|
});
|
|
2142
1234
|
|