@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.
@@ -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
- if (repositoryName && !repositoryName.includes('/') && review.local_path) {
327
+ let branchName = 'unknown';
328
+ if (review.local_path) {
68
329
  try {
69
- const { getRepositoryName } = require('../local-review');
70
- const freshRepoName = await getRepositoryName(review.local_path);
71
- if (freshRepoName && freshRepoName.includes('/')) {
72
- repositoryName = freshRepoName;
73
- // Just use the fresh name for this response - don't write to DB in GET
74
- logger.log('API', `Using fresh repository name from git remote: ${freshRepoName}`, 'cyan');
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 name: ${repoError.message}`);
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: process.env.PAIR_REVIEW_BRANCH || 'unknown',
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
- console.error('Error fetching local review:', error);
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
- const diffData = localReviewDiffs.get(reviewId) || { diff: '', stats: {} };
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
- console.error('Error fetching local diff:', error);
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
- const storedDiffData = localReviewDiffs.get(reviewId);
537
+ // Get stored diff data (in-memory first, then fall back to DB)
538
+ let storedDiffData = localReviewDiffs.get(reviewId);
205
539
  if (!storedDiffData) {
206
- return res.json({
207
- isStale: null,
208
- error: 'No stored diff data found'
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
- console.warn('Analysis already completed or removed:', analysisId);
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
- console.warn('Analysis status not found during error handling:', analysisId);
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
- console.error('Error starting local AI analysis:', error);
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
- console.error('Error fetching local review suggestions:', error);
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
- console.error('Error fetching local review user comments:', error);
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
- console.error('Error creating local review user comment:', error);
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
- console.error('Error creating file-level comment:', error);
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
- console.error('Error updating file-level comment:', error);
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
- console.error('Error deleting file-level comment:', error);
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
- console.error('Error updating AI suggestion status:', error);
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
- console.error('Error fetching local review user comment:', error);
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
- console.error('Error updating local review user comment:', error);
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
- console.error('Error deleting all local review user comments:', error);
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
- console.error('Error deleting local review user comment:', error);
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
- console.error('Error restoring local review user comment:', error);
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
- console.warn('Error fetching AI suggestion stats:', e);
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
- console.error('Error checking for AI suggestions:', error);
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
- console.error('Error refreshing local diff:', error);
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
- console.error('Error fetching local review settings:', error);
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
- console.error('Error saving local review settings:', error);
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
- console.error('Error fetching analysis runs:', error);
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
- console.error('Error fetching latest analysis run:', error);
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
  });