@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/comments.js
DELETED
|
@@ -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;
|