@in-the-loop-labs/pair-review 1.3.2 → 1.4.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 +67 -38
- 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/index.html +270 -623
- package/public/js/index.js +1071 -0
- package/public/js/local.js +80 -0
- package/public/js/modules/analysis-history.js +5 -1
- package/public/local.html +45 -2
- package/src/ai/claude-provider.js +12 -7
- package/src/ai/codex-provider.js +9 -7
- package/src/ai/cursor-agent-provider.js +9 -6
- package/src/ai/gemini-provider.js +9 -7
- package/src/ai/index.js +1 -0
- package/src/ai/opencode-provider.js +9 -7
- package/src/ai/pi-provider.js +859 -0
- package/src/ai/provider.js +32 -8
- package/src/ai/stream-parser.js +171 -2
- package/src/config.js +1 -1
- package/src/database.js +170 -40
- package/src/local-review.js +9 -0
- package/src/routes/local.js +390 -41
- package/src/utils/json-extractor.js +129 -39
package/src/routes/local.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
const express = require('express');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const fs = require('fs').promises;
|
|
16
18
|
const { query, queryOne, run, ReviewRepository, RepoSettingsRepository, CommentRepository, AnalysisRunRepository } = require('../database');
|
|
17
19
|
const Analyzer = require('../ai/analyzer');
|
|
18
20
|
const { v4: uuidv4 } = require('uuid');
|
|
@@ -36,6 +38,264 @@ const {
|
|
|
36
38
|
|
|
37
39
|
const router = express.Router();
|
|
38
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Open native OS directory picker dialog and return the selected path.
|
|
43
|
+
* Uses osascript on macOS, zenity/kdialog on Linux, PowerShell on Windows.
|
|
44
|
+
* Must be registered BEFORE /:reviewId param routes.
|
|
45
|
+
*/
|
|
46
|
+
router.post('/api/local/browse', async (req, res) => {
|
|
47
|
+
try {
|
|
48
|
+
const { execFile } = require('child_process');
|
|
49
|
+
const { promisify } = require('util');
|
|
50
|
+
const execFileAsync = promisify(execFile);
|
|
51
|
+
|
|
52
|
+
let selectedPath = null;
|
|
53
|
+
const platform = process.platform;
|
|
54
|
+
|
|
55
|
+
if (platform === 'darwin') {
|
|
56
|
+
// macOS: use osascript to open native folder picker
|
|
57
|
+
const { stdout } = await execFileAsync('osascript', [
|
|
58
|
+
'-e', 'set selectedFolder to POSIX path of (choose folder with prompt "Select a directory to review")',
|
|
59
|
+
], { timeout: 120000 });
|
|
60
|
+
selectedPath = stdout.trim();
|
|
61
|
+
// osascript appends trailing slash; remove it for consistency
|
|
62
|
+
if (selectedPath.endsWith('/') && selectedPath.length > 1) {
|
|
63
|
+
selectedPath = selectedPath.slice(0, -1);
|
|
64
|
+
}
|
|
65
|
+
} else if (platform === 'win32') {
|
|
66
|
+
// Windows: use PowerShell folder browser dialog
|
|
67
|
+
const psScript = `
|
|
68
|
+
Add-Type -AssemblyName System.Windows.Forms
|
|
69
|
+
$dialog = New-Object System.Windows.Forms.FolderBrowserDialog
|
|
70
|
+
$dialog.Description = "Select a directory to review"
|
|
71
|
+
$dialog.ShowNewFolderButton = $false
|
|
72
|
+
if ($dialog.ShowDialog() -eq [System.Windows.Forms.DialogResult]::OK) {
|
|
73
|
+
Write-Output $dialog.SelectedPath
|
|
74
|
+
}
|
|
75
|
+
`;
|
|
76
|
+
const { stdout } = await execFileAsync('powershell', ['-NoProfile', '-Command', psScript], { timeout: 120000 });
|
|
77
|
+
selectedPath = stdout.trim();
|
|
78
|
+
} else {
|
|
79
|
+
// Linux: try zenity first, then kdialog
|
|
80
|
+
try {
|
|
81
|
+
const { stdout } = await execFileAsync('zenity', ['--file-selection', '--directory', '--title=Select a directory to review'], { timeout: 120000 });
|
|
82
|
+
selectedPath = stdout.trim();
|
|
83
|
+
} catch (zenityError) {
|
|
84
|
+
if (zenityError.code === 1) {
|
|
85
|
+
// Exit code 1 means user cancelled the dialog
|
|
86
|
+
return res.json({ success: true, path: null, cancelled: true });
|
|
87
|
+
}
|
|
88
|
+
// Only fall through to kdialog if zenity is not installed (code 127 or ENOENT)
|
|
89
|
+
if (zenityError.code !== 127 && zenityError.code !== 'ENOENT') {
|
|
90
|
+
return res.status(500).json({
|
|
91
|
+
error: 'Directory picker failed: ' + (zenityError.message || 'Unknown error')
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const { stdout } = await execFileAsync('kdialog', ['--getexistingdirectory', '.', '--title', 'Select a directory to review'], { timeout: 120000 });
|
|
96
|
+
selectedPath = stdout.trim();
|
|
97
|
+
} catch (kdialogError) {
|
|
98
|
+
if (kdialogError.code === 1) {
|
|
99
|
+
return res.json({ success: true, path: null, cancelled: true });
|
|
100
|
+
}
|
|
101
|
+
return res.status(501).json({
|
|
102
|
+
error: 'No supported file dialog found. Install zenity or kdialog, or enter the path manually.'
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!selectedPath) {
|
|
109
|
+
// User cancelled the dialog
|
|
110
|
+
return res.json({ success: true, path: null, cancelled: true });
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
res.json({ success: true, path: selectedPath, cancelled: false });
|
|
114
|
+
|
|
115
|
+
} catch (error) {
|
|
116
|
+
// User cancellation on macOS throws error code -128
|
|
117
|
+
if (error.code === 1 || (error.message && error.message.includes('-128'))) {
|
|
118
|
+
return res.json({ success: true, path: null, cancelled: true });
|
|
119
|
+
}
|
|
120
|
+
// Handle timeout (process killed)
|
|
121
|
+
if (error.killed) {
|
|
122
|
+
return res.status(504).json({
|
|
123
|
+
error: 'Directory picker timed out'
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
logger.error(`Error opening directory picker: ${error.message}`);
|
|
127
|
+
res.status(500).json({
|
|
128
|
+
error: 'Failed to open directory picker'
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* List local review sessions with pagination
|
|
135
|
+
* Must be registered BEFORE /:reviewId param routes
|
|
136
|
+
*/
|
|
137
|
+
router.get('/api/local/sessions', async (req, res) => {
|
|
138
|
+
try {
|
|
139
|
+
const limit = Math.min(Math.max(parseInt(req.query.limit) || 10, 1), 100);
|
|
140
|
+
const before = req.query.before || undefined;
|
|
141
|
+
|
|
142
|
+
const db = req.app.get('db');
|
|
143
|
+
const reviewRepo = new ReviewRepository(db);
|
|
144
|
+
const { sessions, hasMore } = await reviewRepo.listLocalSessions({ limit, before });
|
|
145
|
+
|
|
146
|
+
res.json({
|
|
147
|
+
success: true,
|
|
148
|
+
sessions,
|
|
149
|
+
hasMore
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
} catch (error) {
|
|
153
|
+
logger.error(`Error listing local sessions: ${error.message}`);
|
|
154
|
+
res.status(500).json({
|
|
155
|
+
error: 'Failed to list local sessions'
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Delete a local review session
|
|
162
|
+
* Must be registered BEFORE /:reviewId param routes
|
|
163
|
+
* Only deletes DB records — does NOT remove files on disk.
|
|
164
|
+
*/
|
|
165
|
+
router.delete('/api/local/sessions/:reviewId', async (req, res) => {
|
|
166
|
+
try {
|
|
167
|
+
const reviewId = parseInt(req.params.reviewId);
|
|
168
|
+
|
|
169
|
+
if (isNaN(reviewId) || reviewId <= 0) {
|
|
170
|
+
return res.status(400).json({
|
|
171
|
+
error: 'Invalid review ID'
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const db = req.app.get('db');
|
|
176
|
+
const reviewRepo = new ReviewRepository(db);
|
|
177
|
+
const deleted = await reviewRepo.deleteLocalSession(reviewId);
|
|
178
|
+
|
|
179
|
+
if (!deleted) {
|
|
180
|
+
return res.status(404).json({
|
|
181
|
+
error: `Local review #${reviewId} not found`
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Clean up in-memory diff cache to avoid stale data
|
|
186
|
+
localReviewDiffs.delete(reviewId);
|
|
187
|
+
|
|
188
|
+
logger.success(`Deleted local review session #${reviewId}`);
|
|
189
|
+
|
|
190
|
+
res.json({
|
|
191
|
+
success: true,
|
|
192
|
+
reviewId
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
} catch (error) {
|
|
196
|
+
logger.error(`Error deleting local session: ${error.message}`);
|
|
197
|
+
res.status(500).json({
|
|
198
|
+
error: 'Failed to delete local session'
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Start a new local review from the web UI
|
|
205
|
+
* Must be registered BEFORE /:reviewId param routes
|
|
206
|
+
*/
|
|
207
|
+
router.post('/api/local/start', async (req, res) => {
|
|
208
|
+
try {
|
|
209
|
+
const { path: inputPath } = req.body || {};
|
|
210
|
+
|
|
211
|
+
if (!inputPath || typeof inputPath !== 'string' || !inputPath.trim()) {
|
|
212
|
+
return res.status(400).json({
|
|
213
|
+
error: 'Missing required field: path'
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Required inline (not reusing top-level import) so that vi.spyOn()
|
|
218
|
+
// replacements on the module exports are visible at call time in integration tests.
|
|
219
|
+
const { findGitRoot, getHeadSha, getRepositoryName, getCurrentBranch } = require('../local-review');
|
|
220
|
+
|
|
221
|
+
// Resolve the path
|
|
222
|
+
const resolvedPath = path.resolve(inputPath.trim());
|
|
223
|
+
|
|
224
|
+
// Validate path exists
|
|
225
|
+
try {
|
|
226
|
+
const stat = await fs.stat(resolvedPath);
|
|
227
|
+
if (!stat.isDirectory()) {
|
|
228
|
+
return res.status(400).json({
|
|
229
|
+
error: 'Path is not a directory'
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
} catch (err) {
|
|
233
|
+
return res.status(400).json({
|
|
234
|
+
error: 'Path does not exist'
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Find git root
|
|
239
|
+
let repoPath;
|
|
240
|
+
try {
|
|
241
|
+
repoPath = await findGitRoot(resolvedPath);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
return res.status(400).json({
|
|
244
|
+
error: 'Not a git repository'
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Gather git info
|
|
249
|
+
const headSha = await getHeadSha(repoPath);
|
|
250
|
+
const repository = await getRepositoryName(repoPath);
|
|
251
|
+
const branch = await getCurrentBranch(repoPath);
|
|
252
|
+
|
|
253
|
+
// Create or resume session
|
|
254
|
+
const db = req.app.get('db');
|
|
255
|
+
const reviewRepo = new ReviewRepository(db);
|
|
256
|
+
const sessionId = await reviewRepo.upsertLocalReview({
|
|
257
|
+
localPath: repoPath,
|
|
258
|
+
localHeadSha: headSha,
|
|
259
|
+
repository
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Generate diff
|
|
263
|
+
logger.log('API', `Starting local review for ${repoPath}`, 'cyan');
|
|
264
|
+
const { diff, stats } = await generateLocalDiff(repoPath);
|
|
265
|
+
|
|
266
|
+
// Compute digest for staleness detection
|
|
267
|
+
const digest = await computeLocalDiffDigest(repoPath);
|
|
268
|
+
|
|
269
|
+
// Persist to in-memory Map
|
|
270
|
+
localReviewDiffs.set(sessionId, { diff, stats, digest });
|
|
271
|
+
|
|
272
|
+
// Persist to database
|
|
273
|
+
await reviewRepo.saveLocalDiff(sessionId, { diff, stats, digest });
|
|
274
|
+
|
|
275
|
+
logger.success(`Local review session #${sessionId} started for ${repository} (branch: ${branch})`);
|
|
276
|
+
|
|
277
|
+
res.json({
|
|
278
|
+
success: true,
|
|
279
|
+
reviewUrl: `/local/${sessionId}`,
|
|
280
|
+
sessionId,
|
|
281
|
+
repository,
|
|
282
|
+
branch,
|
|
283
|
+
stats: {
|
|
284
|
+
trackedChanges: stats.trackedChanges || 0,
|
|
285
|
+
untrackedFiles: stats.untrackedFiles || 0,
|
|
286
|
+
stagedChanges: stats.stagedChanges || 0,
|
|
287
|
+
unstagedChanges: stats.unstagedChanges || 0
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
} catch (error) {
|
|
292
|
+
logger.error(`Error starting local review: ${error.message}`);
|
|
293
|
+
res.status(500).json({
|
|
294
|
+
error: 'Failed to start local review'
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
39
299
|
/**
|
|
40
300
|
* Get local review metadata
|
|
41
301
|
*/
|
|
@@ -64,18 +324,33 @@ router.get('/api/local/:reviewId', async (req, res) => {
|
|
|
64
324
|
// Note: GET requests are read-only - no database writes here.
|
|
65
325
|
// Repository name updates happen during session creation or refresh.
|
|
66
326
|
let repositoryName = review.repository;
|
|
67
|
-
|
|
327
|
+
let branchName = 'unknown';
|
|
328
|
+
if (review.local_path) {
|
|
68
329
|
try {
|
|
69
|
-
const { getRepositoryName } = require('../local-review');
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
330
|
+
const { getRepositoryName, getCurrentBranch } = require('../local-review');
|
|
331
|
+
|
|
332
|
+
// Always fetch current branch from the working directory
|
|
333
|
+
branchName = await getCurrentBranch(review.local_path);
|
|
334
|
+
|
|
335
|
+
if (repositoryName && !repositoryName.includes('/')) {
|
|
336
|
+
const freshRepoName = await getRepositoryName(review.local_path);
|
|
337
|
+
if (freshRepoName && freshRepoName.includes('/')) {
|
|
338
|
+
repositoryName = freshRepoName;
|
|
339
|
+
// Just use the fresh name for this response - don't write to DB in GET
|
|
340
|
+
logger.log('API', `Using fresh repository name from git remote: ${freshRepoName}`, 'cyan');
|
|
341
|
+
}
|
|
75
342
|
}
|
|
76
343
|
} catch (repoError) {
|
|
77
344
|
// Keep the original name if we can't get a better one
|
|
78
|
-
logger.warn(`Could not refresh repository
|
|
345
|
+
logger.warn(`Could not refresh repository/branch info: ${repoError.message}`);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Fall back to env var if local_path is not available (e.g. CLI-started sessions)
|
|
350
|
+
if (branchName === 'unknown') {
|
|
351
|
+
branchName = process.env.PAIR_REVIEW_BRANCH || 'unknown';
|
|
352
|
+
if (branchName !== 'unknown') {
|
|
353
|
+
logger.log('API', `Using PAIR_REVIEW_BRANCH env var for branch: ${branchName}`, 'cyan');
|
|
79
354
|
}
|
|
80
355
|
}
|
|
81
356
|
|
|
@@ -84,21 +359,66 @@ router.get('/api/local/:reviewId', async (req, res) => {
|
|
|
84
359
|
localPath: review.local_path,
|
|
85
360
|
localHeadSha: review.local_head_sha,
|
|
86
361
|
repository: repositoryName,
|
|
87
|
-
branch:
|
|
362
|
+
branch: branchName,
|
|
88
363
|
reviewType: 'local',
|
|
89
364
|
status: review.status,
|
|
365
|
+
name: review.name || null,
|
|
90
366
|
createdAt: review.created_at,
|
|
91
367
|
updatedAt: review.updated_at
|
|
92
368
|
});
|
|
93
369
|
|
|
94
370
|
} catch (error) {
|
|
95
|
-
|
|
371
|
+
logger.error('Error fetching local review:', error.stack || error.message);
|
|
96
372
|
res.status(500).json({
|
|
97
373
|
error: 'Failed to fetch local review'
|
|
98
374
|
});
|
|
99
375
|
}
|
|
100
376
|
});
|
|
101
377
|
|
|
378
|
+
/**
|
|
379
|
+
* Update local review session name
|
|
380
|
+
*/
|
|
381
|
+
router.patch('/api/local/:reviewId/name', async (req, res) => {
|
|
382
|
+
try {
|
|
383
|
+
const reviewId = parseInt(req.params.reviewId);
|
|
384
|
+
|
|
385
|
+
if (isNaN(reviewId) || reviewId <= 0) {
|
|
386
|
+
return res.status(400).json({
|
|
387
|
+
error: 'Invalid review ID'
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const db = req.app.get('db');
|
|
392
|
+
const reviewRepo = new ReviewRepository(db);
|
|
393
|
+
const review = await reviewRepo.getLocalReviewById(reviewId);
|
|
394
|
+
|
|
395
|
+
if (!review) {
|
|
396
|
+
return res.status(404).json({
|
|
397
|
+
error: `Local review #${reviewId} not found`
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Allow null to clear the name, otherwise trim and cap at 200 chars
|
|
402
|
+
let { name } = req.body;
|
|
403
|
+
if (name !== null && name !== undefined) {
|
|
404
|
+
name = String(name).trim().slice(0, 200) || null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
await reviewRepo.updateReview(reviewId, { name });
|
|
408
|
+
|
|
409
|
+
res.json({
|
|
410
|
+
success: true,
|
|
411
|
+
name
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
} catch (error) {
|
|
415
|
+
logger.error(`Error updating local review name: ${error.message}`);
|
|
416
|
+
res.status(500).json({
|
|
417
|
+
error: 'Failed to update review name'
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
|
|
102
422
|
/**
|
|
103
423
|
* Get local diff
|
|
104
424
|
*/
|
|
@@ -123,8 +443,22 @@ router.get('/api/local/:reviewId/diff', async (req, res) => {
|
|
|
123
443
|
});
|
|
124
444
|
}
|
|
125
445
|
|
|
126
|
-
// Get diff from module-level storage
|
|
127
|
-
|
|
446
|
+
// Get diff from module-level storage, falling back to database
|
|
447
|
+
let diffData = localReviewDiffs.get(reviewId);
|
|
448
|
+
|
|
449
|
+
if (!diffData) {
|
|
450
|
+
// Try loading from database
|
|
451
|
+
const persistedDiff = await reviewRepo.getLocalDiff(reviewId);
|
|
452
|
+
if (persistedDiff) {
|
|
453
|
+
diffData = persistedDiff;
|
|
454
|
+
// Cache-warm the in-memory Map
|
|
455
|
+
localReviewDiffs.set(reviewId, diffData);
|
|
456
|
+
logger.log('API', `Loaded persisted diff from DB for review #${reviewId}`, 'cyan');
|
|
457
|
+
} else {
|
|
458
|
+
diffData = { diff: '', stats: {} };
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
128
462
|
const { diff: diffContent, stats } = diffData;
|
|
129
463
|
|
|
130
464
|
// Detect generated files via .gitattributes
|
|
@@ -160,7 +494,7 @@ router.get('/api/local/:reviewId/diff', async (req, res) => {
|
|
|
160
494
|
});
|
|
161
495
|
|
|
162
496
|
} catch (error) {
|
|
163
|
-
|
|
497
|
+
logger.error('Error fetching local diff:', error);
|
|
164
498
|
res.status(500).json({
|
|
165
499
|
error: 'Failed to fetch local diff'
|
|
166
500
|
});
|
|
@@ -200,13 +534,21 @@ router.get('/api/local/:reviewId/check-stale', async (req, res) => {
|
|
|
200
534
|
});
|
|
201
535
|
}
|
|
202
536
|
|
|
203
|
-
// Get stored diff data
|
|
204
|
-
|
|
537
|
+
// Get stored diff data (in-memory first, then fall back to DB)
|
|
538
|
+
let storedDiffData = localReviewDiffs.get(reviewId);
|
|
205
539
|
if (!storedDiffData) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
540
|
+
const persistedDiff = await reviewRepo.getLocalDiff(reviewId);
|
|
541
|
+
if (persistedDiff) {
|
|
542
|
+
storedDiffData = persistedDiff;
|
|
543
|
+
// Cache-warm the in-memory Map
|
|
544
|
+
localReviewDiffs.set(reviewId, storedDiffData);
|
|
545
|
+
logger.log('API', `Loaded persisted diff from DB for staleness check on review #${reviewId}`, 'cyan');
|
|
546
|
+
} else {
|
|
547
|
+
return res.json({
|
|
548
|
+
isStale: null,
|
|
549
|
+
error: 'No stored diff data found'
|
|
550
|
+
});
|
|
551
|
+
}
|
|
210
552
|
}
|
|
211
553
|
|
|
212
554
|
// Check if baseline digest exists (must be computed at diff-capture time)
|
|
@@ -441,7 +783,7 @@ router.post('/api/local/:reviewId/analyze', async (req, res) => {
|
|
|
441
783
|
|
|
442
784
|
const currentStatus = activeAnalyses.get(analysisId);
|
|
443
785
|
if (!currentStatus) {
|
|
444
|
-
|
|
786
|
+
logger.warn('Analysis already completed or removed:', analysisId);
|
|
445
787
|
return;
|
|
446
788
|
}
|
|
447
789
|
|
|
@@ -479,7 +821,7 @@ router.post('/api/local/:reviewId/analyze', async (req, res) => {
|
|
|
479
821
|
.catch(error => {
|
|
480
822
|
const currentStatus = activeAnalyses.get(analysisId);
|
|
481
823
|
if (!currentStatus) {
|
|
482
|
-
|
|
824
|
+
logger.warn('Analysis status not found during error handling:', analysisId);
|
|
483
825
|
return;
|
|
484
826
|
}
|
|
485
827
|
|
|
@@ -528,7 +870,7 @@ router.post('/api/local/:reviewId/analyze', async (req, res) => {
|
|
|
528
870
|
});
|
|
529
871
|
|
|
530
872
|
} catch (error) {
|
|
531
|
-
|
|
873
|
+
logger.error('Error starting local AI analysis:', error);
|
|
532
874
|
res.status(500).json({
|
|
533
875
|
error: 'Failed to start AI analysis'
|
|
534
876
|
});
|
|
@@ -654,7 +996,7 @@ router.get('/api/local/:reviewId/suggestions', async (req, res) => {
|
|
|
654
996
|
res.json({ suggestions });
|
|
655
997
|
|
|
656
998
|
} catch (error) {
|
|
657
|
-
|
|
999
|
+
logger.error('Error fetching local review suggestions:', error);
|
|
658
1000
|
res.status(500).json({
|
|
659
1001
|
error: 'Failed to fetch AI suggestions'
|
|
660
1002
|
});
|
|
@@ -702,7 +1044,7 @@ router.get('/api/local/:reviewId/user-comments', async (req, res) => {
|
|
|
702
1044
|
});
|
|
703
1045
|
|
|
704
1046
|
} catch (error) {
|
|
705
|
-
|
|
1047
|
+
logger.error('Error fetching local review user comments:', error);
|
|
706
1048
|
res.status(500).json({
|
|
707
1049
|
error: 'Failed to fetch user comments'
|
|
708
1050
|
});
|
|
@@ -764,7 +1106,7 @@ router.post('/api/local/:reviewId/user-comments', async (req, res) => {
|
|
|
764
1106
|
});
|
|
765
1107
|
|
|
766
1108
|
} catch (error) {
|
|
767
|
-
|
|
1109
|
+
logger.error('Error creating local review user comment:', error);
|
|
768
1110
|
res.status(500).json({
|
|
769
1111
|
error: error.message || 'Failed to create comment'
|
|
770
1112
|
});
|
|
@@ -831,7 +1173,7 @@ router.post('/api/local/:reviewId/file-comment', async (req, res) => {
|
|
|
831
1173
|
});
|
|
832
1174
|
|
|
833
1175
|
} catch (error) {
|
|
834
|
-
|
|
1176
|
+
logger.error('Error creating file-level comment:', error);
|
|
835
1177
|
res.status(500).json({
|
|
836
1178
|
error: error.message || 'Failed to create file-level comment'
|
|
837
1179
|
});
|
|
@@ -892,7 +1234,7 @@ router.put('/api/local/:reviewId/file-comment/:commentId', async (req, res) => {
|
|
|
892
1234
|
});
|
|
893
1235
|
|
|
894
1236
|
} catch (error) {
|
|
895
|
-
|
|
1237
|
+
logger.error('Error updating file-level comment:', error);
|
|
896
1238
|
res.status(500).json({
|
|
897
1239
|
error: 'Failed to update comment'
|
|
898
1240
|
});
|
|
@@ -943,7 +1285,7 @@ router.delete('/api/local/:reviewId/file-comment/:commentId', async (req, res) =
|
|
|
943
1285
|
});
|
|
944
1286
|
|
|
945
1287
|
} catch (error) {
|
|
946
|
-
|
|
1288
|
+
logger.error('Error deleting file-level comment:', error);
|
|
947
1289
|
res.status(500).json({
|
|
948
1290
|
error: 'Failed to delete comment'
|
|
949
1291
|
});
|
|
@@ -1005,7 +1347,7 @@ router.post('/api/local/:reviewId/ai-suggestion/:suggestionId/status', async (re
|
|
|
1005
1347
|
});
|
|
1006
1348
|
|
|
1007
1349
|
} catch (error) {
|
|
1008
|
-
|
|
1350
|
+
logger.error('Error updating AI suggestion status:', error);
|
|
1009
1351
|
res.status(500).json({
|
|
1010
1352
|
error: error.message || 'Failed to update suggestion status'
|
|
1011
1353
|
});
|
|
@@ -1059,7 +1401,7 @@ router.get('/api/local/:reviewId/user-comments/:commentId', async (req, res) =>
|
|
|
1059
1401
|
});
|
|
1060
1402
|
|
|
1061
1403
|
} catch (error) {
|
|
1062
|
-
|
|
1404
|
+
logger.error('Error fetching local review user comment:', error);
|
|
1063
1405
|
res.status(500).json({
|
|
1064
1406
|
error: 'Failed to fetch comment'
|
|
1065
1407
|
});
|
|
@@ -1120,7 +1462,7 @@ router.put('/api/local/:reviewId/user-comments/:commentId', async (req, res) =>
|
|
|
1120
1462
|
});
|
|
1121
1463
|
|
|
1122
1464
|
} catch (error) {
|
|
1123
|
-
|
|
1465
|
+
logger.error('Error updating local review user comment:', error);
|
|
1124
1466
|
res.status(500).json({
|
|
1125
1467
|
error: 'Failed to update comment'
|
|
1126
1468
|
});
|
|
@@ -1178,7 +1520,7 @@ router.delete('/api/local/:reviewId/user-comments', async (req, res) => {
|
|
|
1178
1520
|
}
|
|
1179
1521
|
|
|
1180
1522
|
} catch (error) {
|
|
1181
|
-
|
|
1523
|
+
logger.error('Error deleting all local review user comments:', error);
|
|
1182
1524
|
res.status(500).json({
|
|
1183
1525
|
error: 'Failed to delete comments'
|
|
1184
1526
|
});
|
|
@@ -1231,7 +1573,7 @@ router.delete('/api/local/:reviewId/user-comments/:commentId', async (req, res)
|
|
|
1231
1573
|
});
|
|
1232
1574
|
|
|
1233
1575
|
} catch (error) {
|
|
1234
|
-
|
|
1576
|
+
logger.error('Error deleting local review user comment:', error);
|
|
1235
1577
|
res.status(500).json({
|
|
1236
1578
|
error: 'Failed to delete comment'
|
|
1237
1579
|
});
|
|
@@ -1292,7 +1634,7 @@ router.put('/api/local/:reviewId/user-comments/:commentId/restore', async (req,
|
|
|
1292
1634
|
});
|
|
1293
1635
|
|
|
1294
1636
|
} catch (error) {
|
|
1295
|
-
|
|
1637
|
+
logger.error('Error restoring local review user comment:', error);
|
|
1296
1638
|
res.status(500).json({
|
|
1297
1639
|
error: error.message || 'Failed to restore comment'
|
|
1298
1640
|
});
|
|
@@ -1439,7 +1781,7 @@ router.get('/api/local/:reviewId/has-ai-suggestions', async (req, res) => {
|
|
|
1439
1781
|
const statsResult = await query(db, statsQuery.query, statsQuery.params(reviewId));
|
|
1440
1782
|
stats = calculateStats(statsResult);
|
|
1441
1783
|
} catch (e) {
|
|
1442
|
-
|
|
1784
|
+
logger.warn('Error fetching AI suggestion stats:', e);
|
|
1443
1785
|
}
|
|
1444
1786
|
}
|
|
1445
1787
|
|
|
@@ -1451,7 +1793,7 @@ router.get('/api/local/:reviewId/has-ai-suggestions', async (req, res) => {
|
|
|
1451
1793
|
});
|
|
1452
1794
|
|
|
1453
1795
|
} catch (error) {
|
|
1454
|
-
|
|
1796
|
+
logger.error('Error checking for AI suggestions:', error);
|
|
1455
1797
|
res.status(500).json({
|
|
1456
1798
|
error: 'Failed to check for AI suggestions'
|
|
1457
1799
|
});
|
|
@@ -1603,6 +1945,13 @@ router.post('/api/local/:reviewId/refresh', async (req, res) => {
|
|
|
1603
1945
|
const targetSessionId = sessionChanged ? newSessionId : reviewId;
|
|
1604
1946
|
localReviewDiffs.set(targetSessionId, { diff, stats, digest });
|
|
1605
1947
|
|
|
1948
|
+
// Persist diff to database for future session recovery
|
|
1949
|
+
try {
|
|
1950
|
+
await reviewRepo.saveLocalDiff(targetSessionId, { diff, stats, digest });
|
|
1951
|
+
} catch (persistError) {
|
|
1952
|
+
logger.warn(`Could not persist diff to database: ${persistError.message}`);
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1606
1955
|
logger.success(`Diff refreshed: ${stats.unstagedChanges} unstaged, ${stats.untrackedFiles} untracked${stats.stagedChanges > 0 ? ` (${stats.stagedChanges} staged excluded)` : ''}`);
|
|
1607
1956
|
|
|
1608
1957
|
res.json({
|
|
@@ -1621,7 +1970,7 @@ router.post('/api/local/:reviewId/refresh', async (req, res) => {
|
|
|
1621
1970
|
});
|
|
1622
1971
|
|
|
1623
1972
|
} catch (error) {
|
|
1624
|
-
|
|
1973
|
+
logger.error('Error refreshing local diff:', error);
|
|
1625
1974
|
res.status(500).json({
|
|
1626
1975
|
error: 'Failed to refresh diff: ' + error.message
|
|
1627
1976
|
});
|
|
@@ -1657,7 +2006,7 @@ router.get('/api/local/:reviewId/review-settings', async (req, res) => {
|
|
|
1657
2006
|
});
|
|
1658
2007
|
|
|
1659
2008
|
} catch (error) {
|
|
1660
|
-
|
|
2009
|
+
logger.error('Error fetching local review settings:', error);
|
|
1661
2010
|
res.status(500).json({
|
|
1662
2011
|
error: 'Failed to fetch review settings'
|
|
1663
2012
|
});
|
|
@@ -1701,7 +2050,7 @@ router.post('/api/local/:reviewId/review-settings', async (req, res) => {
|
|
|
1701
2050
|
});
|
|
1702
2051
|
|
|
1703
2052
|
} catch (error) {
|
|
1704
|
-
|
|
2053
|
+
logger.error('Error saving local review settings:', error);
|
|
1705
2054
|
res.status(500).json({
|
|
1706
2055
|
error: 'Failed to save review settings'
|
|
1707
2056
|
});
|
|
@@ -1725,7 +2074,7 @@ router.get('/api/local/:reviewId/analysis-runs', async (req, res) => {
|
|
|
1725
2074
|
|
|
1726
2075
|
res.json({ runs });
|
|
1727
2076
|
} catch (error) {
|
|
1728
|
-
|
|
2077
|
+
logger.error('Error fetching analysis runs:', error);
|
|
1729
2078
|
res.status(500).json({ error: 'Failed to fetch analysis runs' });
|
|
1730
2079
|
}
|
|
1731
2080
|
});
|
|
@@ -1751,7 +2100,7 @@ router.get('/api/local/:reviewId/analysis-runs/latest', async (req, res) => {
|
|
|
1751
2100
|
|
|
1752
2101
|
res.json({ run });
|
|
1753
2102
|
} catch (error) {
|
|
1754
|
-
|
|
2103
|
+
logger.error('Error fetching latest analysis run:', error);
|
|
1755
2104
|
res.status(500).json({ error: 'Failed to fetch latest analysis run' });
|
|
1756
2105
|
}
|
|
1757
2106
|
});
|