@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.
Files changed (63) hide show
  1. package/README.md +77 -4
  2. package/package.json +1 -1
  3. package/plugin/.claude-plugin/plugin.json +1 -1
  4. package/plugin/skills/review-requests/SKILL.md +4 -1
  5. package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
  6. package/plugin-code-critic/skills/analyze/SKILL.md +4 -3
  7. package/public/css/pr.css +1962 -114
  8. package/public/js/CONVENTIONS.md +16 -0
  9. package/public/js/components/AIPanel.js +66 -0
  10. package/public/js/components/AnalysisConfigModal.js +2 -2
  11. package/public/js/components/ChatPanel.js +2955 -0
  12. package/public/js/components/CouncilProgressModal.js +12 -16
  13. package/public/js/components/KeyboardShortcuts.js +3 -0
  14. package/public/js/components/PanelGroup.js +723 -0
  15. package/public/js/components/PreviewModal.js +3 -8
  16. package/public/js/index.js +8 -0
  17. package/public/js/local.js +17 -615
  18. package/public/js/modules/analysis-history.js +19 -68
  19. package/public/js/modules/comment-manager.js +103 -20
  20. package/public/js/modules/diff-context.js +176 -0
  21. package/public/js/modules/diff-renderer.js +30 -0
  22. package/public/js/modules/file-comment-manager.js +126 -105
  23. package/public/js/modules/file-list-merger.js +64 -0
  24. package/public/js/modules/panel-resizer.js +25 -6
  25. package/public/js/modules/suggestion-manager.js +40 -125
  26. package/public/js/pr.js +1009 -159
  27. package/public/js/repo-settings.js +36 -6
  28. package/public/js/utils/category-emoji.js +44 -0
  29. package/public/js/utils/time.js +32 -0
  30. package/public/local.html +107 -70
  31. package/public/pr.html +107 -70
  32. package/public/repo-settings.html +32 -0
  33. package/src/ai/analyzer.js +5 -1
  34. package/src/ai/copilot-provider.js +39 -9
  35. package/src/ai/cursor-agent-provider.js +45 -11
  36. package/src/ai/gemini-provider.js +17 -4
  37. package/src/ai/prompts/config.js +7 -1
  38. package/src/ai/provider-availability.js +1 -1
  39. package/src/ai/provider.js +25 -37
  40. package/src/chat/CONVENTIONS.md +18 -0
  41. package/src/chat/pi-bridge.js +491 -0
  42. package/src/chat/prompt-builder.js +272 -0
  43. package/src/chat/session-manager.js +619 -0
  44. package/src/config.js +14 -0
  45. package/src/database.js +322 -15
  46. package/src/main.js +4 -17
  47. package/src/routes/analyses.js +721 -0
  48. package/src/routes/chat.js +655 -0
  49. package/src/routes/config.js +29 -8
  50. package/src/routes/context-files.js +274 -0
  51. package/src/routes/local.js +225 -1133
  52. package/src/routes/mcp.js +39 -30
  53. package/src/routes/pr.js +424 -58
  54. package/src/routes/reviews.js +1035 -0
  55. package/src/routes/shared.js +4 -29
  56. package/src/server.js +34 -12
  57. package/src/sse/review-events.js +46 -0
  58. package/src/utils/auto-context.js +88 -0
  59. package/src/utils/category-emoji.js +33 -0
  60. package/src/utils/diff-annotator.js +75 -1
  61. package/src/utils/diff-file-list.js +57 -0
  62. package/src/routes/analysis.js +0 -1600
  63. package/src/routes/comments.js +0 -534
@@ -1,534 +0,0 @@
1
- // SPDX-License-Identifier: GPL-3.0-or-later
2
- /**
3
- * Comment CRUD Routes
4
- *
5
- * Handles all comment-related endpoints:
6
- * - AI suggestion status updates and editing
7
- * - User comment CRUD operations
8
- * - Bulk comment operations
9
- */
10
-
11
- const express = require('express');
12
- const { queryOne, run, CommentRepository, ReviewRepository } = require('../database');
13
- const { normalizeRepository } = require('../utils/paths');
14
-
15
- const router = express.Router();
16
-
17
- /**
18
- * Helper function to verify that a review_id exists.
19
- * Checks both reviews table (for PR and local mode) and pr_metadata table (legacy).
20
- * @param {Database} db - Database instance
21
- * @param {number} reviewId - The review_id to verify
22
- * @returns {Promise<boolean>} True if the ID exists in either table
23
- */
24
- async function verifyReviewIdExists(db, reviewId) {
25
- // First check reviews table (preferred - handles both PR and local mode)
26
- const review = await queryOne(db, `
27
- SELECT id FROM reviews WHERE id = ?
28
- `, [reviewId]);
29
-
30
- if (review) {
31
- return true;
32
- }
33
-
34
- // Fall back to checking pr_metadata for legacy compatibility
35
- // This handles cases where old data used prMetadata.id directly
36
- const prMetadata = await queryOne(db, `
37
- SELECT id FROM pr_metadata WHERE id = ?
38
- `, [reviewId]);
39
-
40
- return !!prMetadata;
41
- }
42
-
43
- /**
44
- * Edit AI suggestion and adopt as user comment
45
- */
46
- router.post('/api/ai-suggestion/:id/edit', async (req, res) => {
47
- try {
48
- const { id } = req.params;
49
- const { editedText, action } = req.body;
50
-
51
- if (action !== 'adopt_edited') {
52
- return res.status(400).json({
53
- error: 'Invalid action. Must be "adopt_edited"'
54
- });
55
- }
56
-
57
- if (!editedText || !editedText.trim()) {
58
- return res.status(400).json({
59
- error: 'Edited text cannot be empty'
60
- });
61
- }
62
-
63
- const db = req.app.get('db');
64
- const commentRepo = new CommentRepository(db);
65
-
66
- // Get the suggestion to validate PR exists
67
- const suggestion = await commentRepo.getComment(id, 'ai');
68
-
69
- if (!suggestion) {
70
- return res.status(404).json({
71
- error: 'AI suggestion not found'
72
- });
73
- }
74
-
75
- // Validate PR/review exists (checks both reviews and pr_metadata tables)
76
- const exists = await verifyReviewIdExists(db, suggestion.review_id);
77
-
78
- if (!exists) {
79
- return res.status(404).json({
80
- error: 'Associated pull request or review not found'
81
- });
82
- }
83
-
84
- // Adopt the suggestion with edited text using repository
85
- const userCommentId = await commentRepo.adoptSuggestion(id, editedText);
86
-
87
- // Update suggestion status to adopted and link to user comment
88
- await commentRepo.updateSuggestionStatus(id, 'adopted', userCommentId);
89
-
90
- res.json({
91
- success: true,
92
- userCommentId,
93
- message: 'Suggestion edited and adopted as user comment'
94
- });
95
-
96
- } catch (error) {
97
- console.error('Error editing suggestion:', error);
98
- res.status(500).json({
99
- error: error.message || 'Failed to edit suggestion'
100
- });
101
- }
102
- });
103
-
104
- /**
105
- * Update AI suggestion status
106
- * Sets status to 'adopted', 'dismissed', or 'active' (restored)
107
- * Note: This only updates the status flag. For 'adopted' status, the actual
108
- * user comment creation is handled separately via /api/user-comment endpoint.
109
- */
110
- router.post('/api/ai-suggestion/:id/status', async (req, res) => {
111
- try {
112
- const { id } = req.params;
113
- const { status } = req.body;
114
-
115
- if (!['adopted', 'dismissed', 'active'].includes(status)) {
116
- return res.status(400).json({
117
- error: 'Invalid status. Must be "adopted", "dismissed", or "active"'
118
- });
119
- }
120
-
121
- const db = req.app.get('db');
122
- const commentRepo = new CommentRepository(db);
123
-
124
- // Get the suggestion
125
- const suggestion = await commentRepo.getComment(id, 'ai');
126
-
127
- if (!suggestion) {
128
- return res.status(404).json({
129
- error: 'AI suggestion not found'
130
- });
131
- }
132
-
133
- // Update suggestion status using repository
134
- await commentRepo.updateSuggestionStatus(id, status);
135
-
136
- res.json({
137
- success: true,
138
- status
139
- });
140
-
141
- } catch (error) {
142
- console.error('Error updating suggestion status:', error);
143
- res.status(500).json({
144
- error: error.message || 'Failed to update suggestion status'
145
- });
146
- }
147
- });
148
-
149
- /**
150
- * Create file-level user comment
151
- * File-level comments are about an entire file, not tied to specific lines
152
- */
153
- router.post('/api/file-comment', async (req, res) => {
154
- try {
155
- const { review_id, file, body, commit_sha, parent_id, type, title } = req.body;
156
-
157
- if (!review_id || !file || !body) {
158
- return res.status(400).json({
159
- error: 'Missing required fields: review_id, file, body'
160
- });
161
- }
162
-
163
- // Validate body is not just whitespace
164
- const trimmedBody = body.trim();
165
- if (trimmedBody.length === 0) {
166
- return res.status(400).json({
167
- error: 'Comment body cannot be empty or whitespace only'
168
- });
169
- }
170
-
171
- const db = req.app.get('db');
172
-
173
- // Verify PR/review exists (checks both reviews and pr_metadata tables)
174
- const exists = await verifyReviewIdExists(db, review_id);
175
-
176
- if (!exists) {
177
- return res.status(404).json({
178
- error: 'Pull request or review not found'
179
- });
180
- }
181
-
182
- // Create file-level user comment using repository
183
- const commentRepo = new CommentRepository(db);
184
- const commentId = await commentRepo.createFileComment({
185
- review_id,
186
- file,
187
- body: trimmedBody,
188
- commit_sha,
189
- type,
190
- title,
191
- parent_id
192
- });
193
-
194
- res.json({
195
- success: true,
196
- commentId,
197
- message: 'File-level comment saved successfully'
198
- });
199
-
200
- } catch (error) {
201
- console.error('Error creating file-level comment:', error);
202
- res.status(500).json({
203
- error: error.message || 'Failed to create file-level comment'
204
- });
205
- }
206
- });
207
-
208
- /**
209
- * Create user comment
210
- */
211
- router.post('/api/user-comment', async (req, res) => {
212
- try {
213
- const { review_id, file, line_start, line_end, diff_position, side, commit_sha, body, parent_id, type, title } = req.body;
214
-
215
- if (!review_id || !file || !line_start || !body) {
216
- return res.status(400).json({
217
- error: 'Missing required fields: review_id, file, line_start, body'
218
- });
219
- }
220
-
221
- const db = req.app.get('db');
222
-
223
- // Verify PR/review exists (checks both reviews and pr_metadata tables)
224
- const exists = await verifyReviewIdExists(db, review_id);
225
-
226
- if (!exists) {
227
- return res.status(404).json({
228
- error: 'Pull request or review not found'
229
- });
230
- }
231
-
232
- // Create user comment using repository
233
- const commentRepo = new CommentRepository(db);
234
- const commentId = await commentRepo.createLineComment({
235
- review_id,
236
- file,
237
- line_start,
238
- line_end,
239
- diff_position,
240
- side,
241
- commit_sha,
242
- body,
243
- parent_id,
244
- type,
245
- title
246
- });
247
-
248
- res.json({
249
- success: true,
250
- commentId,
251
- message: 'Comment saved successfully'
252
- });
253
-
254
- } catch (error) {
255
- console.error('Error creating user comment:', error);
256
- res.status(500).json({
257
- error: error.message || 'Failed to create comment'
258
- });
259
- }
260
- });
261
-
262
- /**
263
- * Get user comments for a PR (by owner/repo/number format for consistency)
264
- * Query params:
265
- * - includeDismissed: if 'true', includes dismissed (inactive) comments
266
- */
267
- router.get('/api/pr/:owner/:repo/:number/user-comments', async (req, res) => {
268
- try {
269
- const { owner, repo, number } = req.params;
270
- const { includeDismissed } = req.query;
271
- const prNumber = parseInt(number);
272
-
273
- if (isNaN(prNumber) || prNumber <= 0) {
274
- return res.status(400).json({
275
- error: 'Invalid pull request number'
276
- });
277
- }
278
-
279
- const repository = normalizeRepository(owner, repo);
280
- const db = req.app.get('db');
281
-
282
- // Get or create a review record for this PR
283
- // Comments are associated with review.id to avoid ID collision with local mode
284
- const reviewRepo = new ReviewRepository(db);
285
- const review = await reviewRepo.getReviewByPR(prNumber, repository);
286
-
287
- if (!review) {
288
- return res.json({
289
- success: true,
290
- comments: []
291
- });
292
- }
293
-
294
- // Use CommentRepository to fetch comments with options
295
- const commentRepo = new CommentRepository(db);
296
- const comments = await commentRepo.getUserComments(review.id, {
297
- includeDismissed: includeDismissed === 'true'
298
- });
299
-
300
- res.json({
301
- success: true,
302
- comments: comments || []
303
- });
304
-
305
- } catch (error) {
306
- console.error('Error fetching user comments:', error);
307
- res.status(500).json({
308
- error: 'Failed to fetch user comments'
309
- });
310
- }
311
- });
312
-
313
- /**
314
- * Get single user comment
315
- */
316
- router.get('/api/user-comment/:id', async (req, res) => {
317
- try {
318
- const { id } = req.params;
319
- const db = req.app.get('db');
320
- const commentRepo = new CommentRepository(db);
321
-
322
- const comment = await commentRepo.getComment(id, 'user');
323
-
324
- if (!comment) {
325
- return res.status(404).json({
326
- error: 'User comment not found'
327
- });
328
- }
329
-
330
- res.json(comment);
331
-
332
- } catch (error) {
333
- console.error('Error fetching user comment:', error);
334
- res.status(500).json({
335
- error: error.message || 'Failed to fetch comment'
336
- });
337
- }
338
- });
339
-
340
- /**
341
- * Update user comment
342
- */
343
- router.put('/api/user-comment/:id', async (req, res) => {
344
- try {
345
- const { id } = req.params;
346
- const { body } = req.body;
347
-
348
- if (!body || !body.trim()) {
349
- return res.status(400).json({
350
- error: 'Comment body cannot be empty'
351
- });
352
- }
353
-
354
- const db = req.app.get('db');
355
- const commentRepo = new CommentRepository(db);
356
-
357
- // Update comment using repository
358
- await commentRepo.updateComment(id, body);
359
-
360
- res.json({
361
- success: true,
362
- message: 'Comment updated successfully'
363
- });
364
-
365
- } catch (error) {
366
- console.error('Error updating user comment:', error);
367
-
368
- // Return 404 if comment not found
369
- if (error.message && error.message.includes('not found')) {
370
- return res.status(404).json({
371
- error: error.message
372
- });
373
- }
374
-
375
- res.status(500).json({
376
- error: error.message || 'Failed to update comment'
377
- });
378
- }
379
- });
380
-
381
- /**
382
- * Delete user comment
383
- * If the comment was adopted from an AI suggestion, the parent suggestion
384
- * is automatically transitioned to 'dismissed' state.
385
- */
386
- router.delete('/api/user-comment/:id', async (req, res) => {
387
- try {
388
- const { id } = req.params;
389
-
390
- const db = req.app.get('db');
391
- const commentRepo = new CommentRepository(db);
392
-
393
- // Soft delete using repository (also dismisses parent AI suggestion if applicable)
394
- const result = await commentRepo.deleteComment(id);
395
-
396
- res.json({
397
- success: true,
398
- message: 'Comment deleted successfully',
399
- dismissedSuggestionId: result.dismissedSuggestionId
400
- });
401
-
402
- } catch (error) {
403
- console.error('Error deleting user comment:', error);
404
-
405
- // Return 404 if comment not found
406
- if (error.message && error.message.includes('not found')) {
407
- return res.status(404).json({
408
- error: error.message
409
- });
410
- }
411
-
412
- res.status(500).json({
413
- error: error.message || 'Failed to delete comment'
414
- });
415
- }
416
- });
417
-
418
- /**
419
- * Restore a dismissed user comment
420
- * Sets status from 'inactive' back to 'active'
421
- */
422
- router.put('/api/user-comment/:id/restore', async (req, res) => {
423
- try {
424
- const { id } = req.params;
425
- const commentId = parseInt(id, 10);
426
-
427
- if (isNaN(commentId)) {
428
- return res.status(400).json({ error: 'Invalid comment ID' });
429
- }
430
-
431
- const db = req.app.get('db');
432
- const commentRepo = new CommentRepository(db);
433
-
434
- // Restore the comment
435
- await commentRepo.restoreComment(commentId);
436
-
437
- // Get the restored comment to return
438
- const comment = await commentRepo.getComment(commentId, 'user');
439
-
440
- res.json({
441
- success: true,
442
- message: 'Comment restored successfully',
443
- comment
444
- });
445
-
446
- } catch (error) {
447
- console.error('Error restoring user comment:', error);
448
-
449
- // Return 404 if comment not found
450
- if (error.message && error.message.includes('not found')) {
451
- return res.status(404).json({
452
- error: error.message
453
- });
454
- }
455
-
456
- // Return 400 if comment is not dismissed
457
- if (error.message && error.message.includes('not dismissed')) {
458
- return res.status(400).json({
459
- error: error.message
460
- });
461
- }
462
-
463
- res.status(500).json({
464
- error: error.message || 'Failed to restore comment'
465
- });
466
- }
467
- });
468
-
469
- /**
470
- * Bulk delete all user comments for a PR
471
- * Also dismisses any AI suggestions that were parents of the deleted comments.
472
- */
473
- router.delete('/api/pr/:owner/:repo/:number/user-comments', async (req, res) => {
474
- try {
475
- const { owner, repo, number } = req.params;
476
- const prNumber = parseInt(number);
477
-
478
- if (isNaN(prNumber) || prNumber <= 0) {
479
- return res.status(400).json({
480
- error: 'Invalid pull request number'
481
- });
482
- }
483
-
484
- const db = req.app.get('db');
485
- const repository = normalizeRepository(owner, repo);
486
-
487
- // Get the review record to find associated comments
488
- // Comments are associated with review.id to avoid ID collision with local mode
489
- const reviewRepo = new ReviewRepository(db);
490
- const review = await reviewRepo.getReviewByPR(prNumber, repository);
491
-
492
- // If no review exists, there are no comments to delete - return success with 0 deletions
493
- if (!review) {
494
- return res.json({
495
- success: true,
496
- deletedCount: 0,
497
- dismissedSuggestionIds: [],
498
- message: 'No comments to delete'
499
- });
500
- }
501
-
502
- // Begin transaction to ensure atomicity
503
- await run(db, 'BEGIN TRANSACTION');
504
-
505
- try {
506
- // Bulk delete using repository (also dismisses parent AI suggestions)
507
- const commentRepo = new CommentRepository(db);
508
- const result = await commentRepo.bulkDeleteComments(review.id);
509
-
510
- // Commit transaction
511
- await run(db, 'COMMIT');
512
-
513
- res.json({
514
- success: true,
515
- deletedCount: result.deletedCount,
516
- dismissedSuggestionIds: result.dismissedSuggestionIds,
517
- message: `Deleted ${result.deletedCount} user comment${result.deletedCount !== 1 ? 's' : ''}`
518
- });
519
-
520
- } catch (transactionError) {
521
- // Rollback transaction on error
522
- await run(db, 'ROLLBACK');
523
- throw transactionError;
524
- }
525
-
526
- } catch (error) {
527
- console.error('Error deleting user comments:', error);
528
- res.status(500).json({
529
- error: error.message || 'Failed to delete comments'
530
- });
531
- }
532
- });
533
-
534
- module.exports = router;