@in-the-loop-labs/pair-review 1.6.2 → 2.0.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 +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 +1930 -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 +2952 -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 +57 -19
- 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 +964 -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 +36 -7
- 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 +262 -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 +223 -0
- package/src/routes/local.js +225 -1133
- package/src/routes/mcp.js +39 -30
- package/src/routes/pr.js +410 -52
- 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-file-list.js +57 -0
- package/src/routes/analysis.js +0 -1600
- package/src/routes/comments.js +0 -534
package/public/js/local.js
CHANGED
|
@@ -168,83 +168,10 @@ class LocalManager {
|
|
|
168
168
|
};
|
|
169
169
|
|
|
170
170
|
// Store original methods we need to patch
|
|
171
|
-
const originalLoadUserComments = manager.loadUserComments.bind(manager);
|
|
172
171
|
const originalLoadAISuggestions = manager.loadAISuggestions.bind(manager);
|
|
173
172
|
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
// They only appear in the AI/Review Panel when the "show dismissed" filter is ON.
|
|
177
|
-
// This provides cleaner UX - the diff view shows only active comments, while
|
|
178
|
-
// the AI Panel serves as the "inbox" where you can optionally see and restore dismissed items.
|
|
179
|
-
manager.loadUserComments = async function(includeDismissed = false) {
|
|
180
|
-
if (!manager.currentPR) return;
|
|
181
|
-
|
|
182
|
-
try {
|
|
183
|
-
const queryParam = includeDismissed ? '?includeDismissed=true' : '';
|
|
184
|
-
const response = await fetch(`/api/local/${reviewId}/user-comments${queryParam}`);
|
|
185
|
-
if (!response.ok) return;
|
|
186
|
-
|
|
187
|
-
const data = await response.json();
|
|
188
|
-
manager.userComments = data.comments || [];
|
|
189
|
-
|
|
190
|
-
// Separate file-level and line-level comments for diff view rendering
|
|
191
|
-
// Skip inactive (dismissed) comments - they should not appear in the diff view
|
|
192
|
-
const fileLevelComments = [];
|
|
193
|
-
const lineLevelComments = [];
|
|
194
|
-
|
|
195
|
-
manager.userComments.forEach(comment => {
|
|
196
|
-
// Skip inactive (dismissed) comments - they should not appear in the diff view
|
|
197
|
-
if (comment.status === 'inactive') {
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
if (comment.is_file_level === 1) {
|
|
201
|
-
fileLevelComments.push(comment);
|
|
202
|
-
} else {
|
|
203
|
-
lineLevelComments.push(comment);
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
// Clear existing comment rows before re-rendering
|
|
208
|
-
document.querySelectorAll('.user-comment-row').forEach(row => row.remove());
|
|
209
|
-
|
|
210
|
-
// Display line-level comments inline with diff (only active comments reach here)
|
|
211
|
-
lineLevelComments.forEach(comment => {
|
|
212
|
-
const fileElement = manager.findFileElement(comment.file);
|
|
213
|
-
if (!fileElement) return;
|
|
214
|
-
|
|
215
|
-
// Use the comment's side to determine which coordinate system to search in
|
|
216
|
-
// LEFT side = OLD coordinates (deleted lines or context lines in OLD coords)
|
|
217
|
-
// RIGHT side = NEW coordinates (added lines or context lines in NEW coords)
|
|
218
|
-
const side = comment.side || 'RIGHT';
|
|
219
|
-
|
|
220
|
-
const lineRows = fileElement.querySelectorAll('tr');
|
|
221
|
-
for (const row of lineRows) {
|
|
222
|
-
// Pass side to getLineNumber() to get the correct coordinate system
|
|
223
|
-
// This allows context lines (which have BOTH old and new line numbers) to be found
|
|
224
|
-
// when the comment was placed on a LEFT-side line (old coordinate)
|
|
225
|
-
const lineNum = manager.getLineNumber(row, side);
|
|
226
|
-
if (lineNum === comment.line_start) {
|
|
227
|
-
manager.displayUserComment(comment, row);
|
|
228
|
-
break;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
// Load file-level comments into their zones (only active comments reach here)
|
|
234
|
-
if (manager.fileCommentManager && fileLevelComments.length > 0) {
|
|
235
|
-
manager.fileCommentManager.loadFileComments(fileLevelComments, []);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Populate AI Panel with all comments (including dismissed if requested)
|
|
239
|
-
if (window.aiPanel?.setComments) {
|
|
240
|
-
window.aiPanel.setComments(manager.userComments);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
manager.updateCommentCount();
|
|
244
|
-
} catch (error) {
|
|
245
|
-
console.error('Error loading user comments:', error);
|
|
246
|
-
}
|
|
247
|
-
};
|
|
173
|
+
// Note: loadUserComments no longer needs patching because pr.js now uses the unified
|
|
174
|
+
// /api/reviews/:reviewId/comments endpoint which works for both PR and local mode.
|
|
248
175
|
|
|
249
176
|
// Override loadAISuggestions
|
|
250
177
|
manager.loadAISuggestions = async function(level = null, runId = null) {
|
|
@@ -257,7 +184,7 @@ class LocalManager {
|
|
|
257
184
|
|
|
258
185
|
// First, check if analysis has been run and get summary data for the selected run
|
|
259
186
|
try {
|
|
260
|
-
let checkUrl = `/api/
|
|
187
|
+
let checkUrl = `/api/reviews/${reviewId}/suggestions/check`;
|
|
261
188
|
if (filterRunId) {
|
|
262
189
|
checkUrl += `?runId=${filterRunId}`;
|
|
263
190
|
}
|
|
@@ -282,7 +209,7 @@ class LocalManager {
|
|
|
282
209
|
console.warn('Error checking analysis status:', checkError);
|
|
283
210
|
}
|
|
284
211
|
|
|
285
|
-
let url = `/api/
|
|
212
|
+
let url = `/api/reviews/${reviewId}/suggestions?levels=${filterLevel}`;
|
|
286
213
|
if (filterRunId) {
|
|
287
214
|
url += `&runId=${filterRunId}`;
|
|
288
215
|
}
|
|
@@ -440,7 +367,7 @@ class LocalManager {
|
|
|
440
367
|
// Override checkRunningAnalysis
|
|
441
368
|
manager.checkRunningAnalysis = async function() {
|
|
442
369
|
try {
|
|
443
|
-
const response = await fetch(`/api/
|
|
370
|
+
const response = await fetch(`/api/reviews/${reviewId}/analyses/status`);
|
|
444
371
|
if (!response.ok) return;
|
|
445
372
|
|
|
446
373
|
const data = await response.json();
|
|
@@ -468,473 +395,11 @@ class LocalManager {
|
|
|
468
395
|
}
|
|
469
396
|
};
|
|
470
397
|
|
|
471
|
-
//
|
|
472
|
-
//
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
cm.saveUserComment = async function(textarea, formRow) {
|
|
478
|
-
const fileName = textarea.dataset.file;
|
|
479
|
-
const lineNumber = parseInt(textarea.dataset.line);
|
|
480
|
-
const parsedEndLine = parseInt(textarea.dataset.lineEnd);
|
|
481
|
-
const endLineNumber = !isNaN(parsedEndLine) ? parsedEndLine : lineNumber;
|
|
482
|
-
const diffPosition = textarea.dataset.diffPosition ? parseInt(textarea.dataset.diffPosition) : null;
|
|
483
|
-
const side = textarea.dataset.side || 'RIGHT';
|
|
484
|
-
const content = textarea.value.trim();
|
|
485
|
-
|
|
486
|
-
if (!content) {
|
|
487
|
-
return;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// Prevent duplicate saves from rapid clicks or Cmd+Enter
|
|
491
|
-
const saveBtn = formRow?.querySelector('.save-comment-btn');
|
|
492
|
-
if (saveBtn?.dataset.saving === 'true') {
|
|
493
|
-
return;
|
|
494
|
-
}
|
|
495
|
-
if (saveBtn) saveBtn.dataset.saving = 'true';
|
|
496
|
-
if (saveBtn) saveBtn.disabled = true;
|
|
497
|
-
|
|
498
|
-
try {
|
|
499
|
-
const response = await fetch(`/api/local/${reviewId}/user-comments`, {
|
|
500
|
-
method: 'POST',
|
|
501
|
-
headers: {
|
|
502
|
-
'Content-Type': 'application/json'
|
|
503
|
-
},
|
|
504
|
-
body: JSON.stringify({
|
|
505
|
-
file: fileName,
|
|
506
|
-
line_start: lineNumber,
|
|
507
|
-
line_end: endLineNumber,
|
|
508
|
-
diff_position: diffPosition,
|
|
509
|
-
side: side,
|
|
510
|
-
body: content
|
|
511
|
-
})
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
if (!response.ok) {
|
|
515
|
-
const error = await response.json();
|
|
516
|
-
throw new Error(error.error || 'Failed to save comment');
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
const result = await response.json();
|
|
520
|
-
|
|
521
|
-
// Build comment object
|
|
522
|
-
const commentData = {
|
|
523
|
-
id: result.commentId,
|
|
524
|
-
file: fileName,
|
|
525
|
-
line_start: lineNumber,
|
|
526
|
-
line_end: endLineNumber,
|
|
527
|
-
diff_position: diffPosition,
|
|
528
|
-
side: side, // Include side for suggestion code extraction
|
|
529
|
-
body: content,
|
|
530
|
-
created_at: new Date().toISOString()
|
|
531
|
-
};
|
|
532
|
-
|
|
533
|
-
// Create comment display row
|
|
534
|
-
const targetRow = formRow.previousElementSibling;
|
|
535
|
-
if (!targetRow) {
|
|
536
|
-
console.error('Could not find target row for comment display');
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
|
-
cm.displayUserComment(commentData, targetRow);
|
|
540
|
-
|
|
541
|
-
// Notify AI Panel about the new comment
|
|
542
|
-
if (window.aiPanel?.addComment) {
|
|
543
|
-
window.aiPanel.addComment(commentData);
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
// Hide form and clear selection
|
|
547
|
-
cm.hideCommentForm();
|
|
548
|
-
if (cm.prManager?.lineTracker) {
|
|
549
|
-
cm.prManager.lineTracker.clearRangeSelection();
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// Update comment count
|
|
553
|
-
if (cm.prManager?.updateCommentCount) {
|
|
554
|
-
cm.prManager.updateCommentCount();
|
|
555
|
-
}
|
|
556
|
-
} catch (error) {
|
|
557
|
-
console.error('Error saving user comment:', error);
|
|
558
|
-
alert('Failed to save comment: ' + error.message);
|
|
559
|
-
// Re-enable save button on failure so the user can retry
|
|
560
|
-
if (saveBtn) {
|
|
561
|
-
saveBtn.dataset.saving = 'false';
|
|
562
|
-
saveBtn.disabled = false;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
};
|
|
566
|
-
}
|
|
567
|
-
|
|
568
|
-
// Patch PRManager.deleteUserComment for local mode
|
|
569
|
-
// DESIGN DECISION: Dismissed comments are NEVER shown in the diff panel.
|
|
570
|
-
// They only appear in the AI/Review Panel when the "show dismissed" filter is ON.
|
|
571
|
-
const originalDeleteUserComment = manager.deleteUserComment?.bind(manager);
|
|
572
|
-
if (originalDeleteUserComment) {
|
|
573
|
-
manager.deleteUserComment = async function(commentId) {
|
|
574
|
-
try {
|
|
575
|
-
const response = await fetch(`/api/local/${reviewId}/user-comments/${commentId}`, {
|
|
576
|
-
method: 'DELETE'
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
if (!response.ok) {
|
|
580
|
-
const error = await response.json();
|
|
581
|
-
throw new Error(error.error || 'Failed to delete comment');
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
const apiResult = await response.json();
|
|
585
|
-
|
|
586
|
-
// Check if dismissed comments filter is enabled for AI Panel updates
|
|
587
|
-
const showDismissed = window.aiPanel?.showDismissedComments || false;
|
|
588
|
-
|
|
589
|
-
// Always remove the comment from the diff view (design decision: dismissed comments never shown in diff)
|
|
590
|
-
const commentRow = document.querySelector(`[data-comment-id="${commentId}"]`);
|
|
591
|
-
if (commentRow) {
|
|
592
|
-
commentRow.remove();
|
|
593
|
-
manager.updateCommentCount();
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
// Also handle file-level comment cards
|
|
597
|
-
const fileCommentCard = document.querySelector(`.file-comment-card[data-comment-id="${commentId}"]`);
|
|
598
|
-
if (fileCommentCard) {
|
|
599
|
-
const zone = fileCommentCard.closest('.file-comments-zone');
|
|
600
|
-
fileCommentCard.remove();
|
|
601
|
-
if (zone && manager.fileCommentManager) {
|
|
602
|
-
manager.fileCommentManager.updateCommentCount(zone);
|
|
603
|
-
}
|
|
604
|
-
manager.updateCommentCount();
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// Update AI Panel - transition to dismissed state or remove based on filter
|
|
608
|
-
if (showDismissed && window.aiPanel?.updateComment) {
|
|
609
|
-
// Update comment status to 'inactive' so it renders with dismissed styling in AI Panel
|
|
610
|
-
window.aiPanel.updateComment(commentId, { status: 'inactive' });
|
|
611
|
-
} else if (window.aiPanel?.removeComment) {
|
|
612
|
-
window.aiPanel.removeComment(commentId);
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// If a parent suggestion existed, the suggestion card is still collapsed/dismissed in the diff view.
|
|
616
|
-
// Update AIPanel to show the suggestion as 'dismissed' (matching its visual state).
|
|
617
|
-
// User can click "Show" to restore it to active state if they want to re-adopt.
|
|
618
|
-
if (apiResult.dismissedSuggestionId && window.aiPanel?.updateFindingStatus) {
|
|
619
|
-
window.aiPanel.updateFindingStatus(apiResult.dismissedSuggestionId, 'dismissed');
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
// Show success toast
|
|
623
|
-
if (window.toast) {
|
|
624
|
-
window.toast.showSuccess('Comment dismissed');
|
|
625
|
-
}
|
|
626
|
-
} catch (error) {
|
|
627
|
-
console.error('Error deleting comment:', error);
|
|
628
|
-
if (window.toast) {
|
|
629
|
-
window.toast.showError('Failed to dismiss comment');
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
};
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
// Patch PRManager.editUserComment for local mode
|
|
636
|
-
// This method fetches comment data when editing
|
|
637
|
-
const originalEditUserComment = manager.editUserComment?.bind(manager);
|
|
638
|
-
if (originalEditUserComment) {
|
|
639
|
-
manager.editUserComment = async function(commentId) {
|
|
640
|
-
try {
|
|
641
|
-
const commentRow = document.querySelector(`[data-comment-id="${commentId}"]`);
|
|
642
|
-
if (!commentRow) return;
|
|
643
|
-
|
|
644
|
-
const commentDiv = commentRow.querySelector('.user-comment');
|
|
645
|
-
const bodyDiv = commentDiv.querySelector('.user-comment-body');
|
|
646
|
-
let currentText = bodyDiv.dataset.originalMarkdown || '';
|
|
647
|
-
|
|
648
|
-
// Fetch from local endpoint if needed
|
|
649
|
-
if (!currentText) {
|
|
650
|
-
const response = await fetch(`/api/local/${reviewId}/user-comments/${commentId}`);
|
|
651
|
-
if (response.ok) {
|
|
652
|
-
const data = await response.json();
|
|
653
|
-
currentText = data.body || bodyDiv.textContent.trim();
|
|
654
|
-
} else {
|
|
655
|
-
currentText = bodyDiv.textContent.trim();
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
|
|
659
|
-
if (commentDiv.classList.contains('editing-mode')) return;
|
|
660
|
-
|
|
661
|
-
commentDiv.classList.add('editing-mode');
|
|
662
|
-
|
|
663
|
-
const fileName = commentRow.dataset.file || '';
|
|
664
|
-
const lineStart = commentRow.dataset.lineStart || '';
|
|
665
|
-
const lineEnd = commentRow.dataset.lineEnd || lineStart;
|
|
666
|
-
const side = commentRow.dataset.side || '';
|
|
667
|
-
|
|
668
|
-
const editFormHTML = `
|
|
669
|
-
<div class="user-comment-edit-form">
|
|
670
|
-
<div class="comment-form-toolbar">
|
|
671
|
-
<button type="button" class="btn btn-sm suggestion-btn" title="Insert a suggestion">
|
|
672
|
-
${CommentManager.SUGGESTION_ICON_SVG}
|
|
673
|
-
</button>
|
|
674
|
-
</div>
|
|
675
|
-
<textarea
|
|
676
|
-
id="edit-comment-${commentId}"
|
|
677
|
-
class="comment-edit-textarea"
|
|
678
|
-
placeholder="Enter your comment..."
|
|
679
|
-
data-file="${fileName}"
|
|
680
|
-
data-line="${lineStart}"
|
|
681
|
-
data-line-end="${lineEnd}"
|
|
682
|
-
data-side="${side || 'RIGHT'}"
|
|
683
|
-
>${manager.escapeHtml(currentText)}</textarea>
|
|
684
|
-
<div class="comment-edit-actions">
|
|
685
|
-
<button class="btn btn-sm btn-primary save-edit-btn">Save</button>
|
|
686
|
-
<button class="btn btn-sm btn-secondary cancel-edit-btn">Cancel</button>
|
|
687
|
-
</div>
|
|
688
|
-
</div>
|
|
689
|
-
`;
|
|
690
|
-
|
|
691
|
-
bodyDiv.style.display = 'none';
|
|
692
|
-
bodyDiv.insertAdjacentHTML('afterend', editFormHTML);
|
|
693
|
-
|
|
694
|
-
const editForm = commentDiv.querySelector('.user-comment-edit-form');
|
|
695
|
-
const textarea = document.getElementById(`edit-comment-${commentId}`);
|
|
696
|
-
const suggestionBtn = editForm.querySelector('.suggestion-btn');
|
|
697
|
-
const saveBtn = editForm.querySelector('.save-edit-btn');
|
|
698
|
-
const cancelBtn = editForm.querySelector('.cancel-edit-btn');
|
|
699
|
-
|
|
700
|
-
if (textarea) {
|
|
701
|
-
manager.autoResizeTextarea(textarea);
|
|
702
|
-
textarea.focus();
|
|
703
|
-
textarea.setSelectionRange(textarea.value.length, textarea.value.length);
|
|
704
|
-
manager.updateSuggestionButtonState(textarea, suggestionBtn);
|
|
705
|
-
|
|
706
|
-
suggestionBtn.addEventListener('click', () => {
|
|
707
|
-
if (!suggestionBtn.disabled) {
|
|
708
|
-
manager.insertSuggestionBlock(textarea, suggestionBtn);
|
|
709
|
-
}
|
|
710
|
-
});
|
|
711
|
-
|
|
712
|
-
saveBtn.addEventListener('click', () => manager.saveEditedUserComment(commentId));
|
|
713
|
-
cancelBtn.addEventListener('click', () => manager.cancelEditUserComment(commentId));
|
|
714
|
-
|
|
715
|
-
textarea.addEventListener('input', () => {
|
|
716
|
-
manager.autoResizeTextarea(textarea);
|
|
717
|
-
manager.updateSuggestionButtonState(textarea, suggestionBtn);
|
|
718
|
-
});
|
|
719
|
-
}
|
|
720
|
-
} catch (error) {
|
|
721
|
-
console.error('Error editing comment:', error);
|
|
722
|
-
alert('Failed to edit comment');
|
|
723
|
-
}
|
|
724
|
-
};
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
// Patch PRManager.saveEditedUserComment for local mode
|
|
728
|
-
const originalSaveEditedUserComment = manager.saveEditedUserComment?.bind(manager);
|
|
729
|
-
if (originalSaveEditedUserComment) {
|
|
730
|
-
manager.saveEditedUserComment = async function(commentId) {
|
|
731
|
-
// Prevent duplicate saves from rapid clicks or Cmd+Enter
|
|
732
|
-
const editFormEl = document.querySelector(`#edit-comment-${commentId}`)?.closest('.user-comment-edit-form');
|
|
733
|
-
const saveBtnEl = editFormEl?.querySelector('.save-edit-btn');
|
|
734
|
-
if (saveBtnEl?.dataset.saving === 'true') {
|
|
735
|
-
return;
|
|
736
|
-
}
|
|
737
|
-
if (saveBtnEl) saveBtnEl.dataset.saving = 'true';
|
|
738
|
-
if (saveBtnEl) saveBtnEl.disabled = true;
|
|
739
|
-
|
|
740
|
-
try {
|
|
741
|
-
const textarea = document.getElementById(`edit-comment-${commentId}`);
|
|
742
|
-
const editedText = textarea.value.trim();
|
|
743
|
-
|
|
744
|
-
if (!editedText) {
|
|
745
|
-
alert('Comment cannot be empty');
|
|
746
|
-
textarea.focus();
|
|
747
|
-
if (saveBtnEl) {
|
|
748
|
-
saveBtnEl.dataset.saving = 'false';
|
|
749
|
-
saveBtnEl.disabled = false;
|
|
750
|
-
}
|
|
751
|
-
return;
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
const response = await fetch(`/api/local/${reviewId}/user-comments/${commentId}`, {
|
|
755
|
-
method: 'PUT',
|
|
756
|
-
headers: { 'Content-Type': 'application/json' },
|
|
757
|
-
body: JSON.stringify({ body: editedText })
|
|
758
|
-
});
|
|
759
|
-
|
|
760
|
-
if (!response.ok) throw new Error('Failed to update comment');
|
|
761
|
-
|
|
762
|
-
const commentRow = document.querySelector(`[data-comment-id="${commentId}"]`);
|
|
763
|
-
if (!commentRow) {
|
|
764
|
-
console.error('Comment element not found');
|
|
765
|
-
return;
|
|
766
|
-
}
|
|
767
|
-
const commentDiv = commentRow.querySelector('.user-comment');
|
|
768
|
-
let bodyDiv = commentDiv.querySelector('.user-comment-body');
|
|
769
|
-
const editForm = commentDiv.querySelector('.user-comment-edit-form');
|
|
770
|
-
|
|
771
|
-
if (!bodyDiv) {
|
|
772
|
-
bodyDiv = document.createElement('div');
|
|
773
|
-
bodyDiv.className = 'user-comment-body';
|
|
774
|
-
commentDiv.appendChild(bodyDiv);
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
bodyDiv.innerHTML = window.renderMarkdown ? window.renderMarkdown(editedText) : manager.escapeHtml(editedText);
|
|
778
|
-
bodyDiv.dataset.originalMarkdown = editedText;
|
|
779
|
-
bodyDiv.style.display = '';
|
|
780
|
-
|
|
781
|
-
if (editForm) editForm.remove();
|
|
782
|
-
commentDiv.classList.remove('editing-mode');
|
|
783
|
-
|
|
784
|
-
const timestamp = commentDiv.querySelector('.user-comment-timestamp');
|
|
785
|
-
if (timestamp) timestamp.textContent = new Date().toLocaleString();
|
|
786
|
-
|
|
787
|
-
// Notify AI Panel about the updated comment body
|
|
788
|
-
if (window.aiPanel?.updateComment) {
|
|
789
|
-
window.aiPanel.updateComment(commentId, { body: editedText });
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
} catch (error) {
|
|
793
|
-
console.error('Error saving comment:', error);
|
|
794
|
-
alert('Failed to save comment');
|
|
795
|
-
// Re-enable save button on failure so the user can retry
|
|
796
|
-
if (saveBtnEl) {
|
|
797
|
-
saveBtnEl.dataset.saving = 'false';
|
|
798
|
-
saveBtnEl.disabled = false;
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
};
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
// Patch PRManager.clearAllUserComments for local mode
|
|
805
|
-
const originalClearAllUserComments = manager.clearAllUserComments?.bind(manager);
|
|
806
|
-
if (originalClearAllUserComments) {
|
|
807
|
-
manager.clearAllUserComments = async function() {
|
|
808
|
-
// Count both line-level and file-level user comments
|
|
809
|
-
const lineCommentRows = document.querySelectorAll('.user-comment-row');
|
|
810
|
-
const fileCommentCards = document.querySelectorAll('.file-comment-card.user-comment');
|
|
811
|
-
const totalComments = lineCommentRows.length + fileCommentCards.length;
|
|
812
|
-
|
|
813
|
-
if (totalComments === 0) {
|
|
814
|
-
if (window.toast?.showInfo) {
|
|
815
|
-
window.toast.showInfo('No comments to clear');
|
|
816
|
-
}
|
|
817
|
-
return;
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
if (!window.confirmDialog) {
|
|
821
|
-
alert('Confirmation dialog unavailable. Please refresh the page.');
|
|
822
|
-
return;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
const dialogResult = await window.confirmDialog.show({
|
|
826
|
-
title: 'Clear All Comments?',
|
|
827
|
-
message: `This will dismiss all ${totalComments} comment${totalComments !== 1 ? 's' : ''}. You can restore them later.`,
|
|
828
|
-
confirmText: 'Clear All',
|
|
829
|
-
confirmClass: 'btn-danger'
|
|
830
|
-
});
|
|
831
|
-
|
|
832
|
-
if (dialogResult !== 'confirm') return;
|
|
833
|
-
|
|
834
|
-
try {
|
|
835
|
-
const response = await fetch(`/api/local/${reviewId}/user-comments`, {
|
|
836
|
-
method: 'DELETE'
|
|
837
|
-
});
|
|
838
|
-
|
|
839
|
-
if (!response.ok) throw new Error('Failed to delete comments');
|
|
840
|
-
|
|
841
|
-
const result = await response.json();
|
|
842
|
-
const deletedCount = result.deletedCount || totalComments;
|
|
843
|
-
|
|
844
|
-
// Remove line-level comment rows from DOM
|
|
845
|
-
lineCommentRows.forEach(row => row.remove());
|
|
846
|
-
|
|
847
|
-
// Remove file-level comment cards from DOM
|
|
848
|
-
fileCommentCards.forEach(card => {
|
|
849
|
-
const zone = card.closest('.file-comments-zone');
|
|
850
|
-
card.remove();
|
|
851
|
-
|
|
852
|
-
// Update the file comment zone header button state
|
|
853
|
-
if (zone && manager.fileCommentManager) {
|
|
854
|
-
manager.fileCommentManager.updateCommentCount(zone);
|
|
855
|
-
}
|
|
856
|
-
});
|
|
857
|
-
|
|
858
|
-
// Remove line-level and file-level comment elements from diff view
|
|
859
|
-
// (They have been soft-deleted, so should not appear in the diff panel per design decision)
|
|
860
|
-
// The comments array will be reloaded below with proper dismissed state.
|
|
861
|
-
|
|
862
|
-
// Reload comments to update both internal state and AI Panel
|
|
863
|
-
// This shows dismissed comments in AI Panel if filter is enabled, matching individual deletion behavior
|
|
864
|
-
const includeDismissed = window.aiPanel?.showDismissedComments || false;
|
|
865
|
-
await manager.loadUserComments(includeDismissed);
|
|
866
|
-
|
|
867
|
-
// Update dismissed suggestions in the diff view UI
|
|
868
|
-
// (AI Panel is already updated by loadUserComments via setComments)
|
|
869
|
-
if (result.dismissedSuggestionIds && result.dismissedSuggestionIds.length > 0 && manager.updateDismissedSuggestionUI) {
|
|
870
|
-
for (const suggestionId of result.dismissedSuggestionIds) {
|
|
871
|
-
manager.updateDismissedSuggestionUI(suggestionId);
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
// Show success toast notification
|
|
876
|
-
if (window.toast) {
|
|
877
|
-
window.toast.showSuccess(`Cleared ${deletedCount} comment${deletedCount !== 1 ? 's' : ''}`);
|
|
878
|
-
}
|
|
879
|
-
} catch (error) {
|
|
880
|
-
console.error('Error clearing user comments:', error);
|
|
881
|
-
if (window.toast) {
|
|
882
|
-
window.toast.showError('Failed to clear comments');
|
|
883
|
-
} else {
|
|
884
|
-
alert('Failed to clear comments');
|
|
885
|
-
}
|
|
886
|
-
}
|
|
887
|
-
};
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
// Patch SuggestionManager.createUserCommentFromSuggestion for local mode
|
|
891
|
-
// This method is called when adopting AI suggestions
|
|
892
|
-
if (manager.suggestionManager) {
|
|
893
|
-
const sm = manager.suggestionManager;
|
|
894
|
-
|
|
895
|
-
sm.createUserCommentFromSuggestion = async function(suggestionId, fileName, lineNumber, suggestionText, suggestionType, suggestionTitle, diffPosition, side) {
|
|
896
|
-
// Format the comment text with emoji and category prefix
|
|
897
|
-
const formattedText = sm.formatAdoptedComment(suggestionText, suggestionType);
|
|
898
|
-
|
|
899
|
-
// Parse diff_position if it's a string (from dataset)
|
|
900
|
-
const parsedDiffPosition = diffPosition ? parseInt(diffPosition) : null;
|
|
901
|
-
|
|
902
|
-
const createResponse = await fetch(`/api/local/${reviewId}/user-comments`, {
|
|
903
|
-
method: 'POST',
|
|
904
|
-
headers: {
|
|
905
|
-
'Content-Type': 'application/json'
|
|
906
|
-
},
|
|
907
|
-
body: JSON.stringify({
|
|
908
|
-
file: fileName,
|
|
909
|
-
line_start: parseInt(lineNumber),
|
|
910
|
-
line_end: parseInt(lineNumber),
|
|
911
|
-
diff_position: parsedDiffPosition,
|
|
912
|
-
side: side || 'RIGHT',
|
|
913
|
-
body: formattedText,
|
|
914
|
-
parent_id: suggestionId,
|
|
915
|
-
type: suggestionType,
|
|
916
|
-
title: suggestionTitle
|
|
917
|
-
})
|
|
918
|
-
});
|
|
919
|
-
|
|
920
|
-
if (!createResponse.ok) {
|
|
921
|
-
throw new Error('Failed to create user comment');
|
|
922
|
-
}
|
|
923
|
-
|
|
924
|
-
const result = await createResponse.json();
|
|
925
|
-
return {
|
|
926
|
-
id: result.commentId,
|
|
927
|
-
file: fileName,
|
|
928
|
-
line_start: parseInt(lineNumber),
|
|
929
|
-
body: formattedText,
|
|
930
|
-
type: suggestionType,
|
|
931
|
-
title: suggestionTitle,
|
|
932
|
-
parent_id: suggestionId,
|
|
933
|
-
diff_position: parsedDiffPosition,
|
|
934
|
-
created_at: new Date().toISOString()
|
|
935
|
-
};
|
|
936
|
-
};
|
|
937
|
-
}
|
|
398
|
+
// Note: Comment-related method overrides (saveUserComment, deleteUserComment,
|
|
399
|
+
// editUserComment, saveEditedUserComment, clearAllUserComments,
|
|
400
|
+
// createUserCommentFromSuggestion, restoreUserComment) have been removed because
|
|
401
|
+
// the base PRManager methods now use the unified /api/reviews/:reviewId/comments
|
|
402
|
+
// endpoints which work for both PR and local mode.
|
|
938
403
|
|
|
939
404
|
// Patch fetchRepoSettings to use the repository from local review data
|
|
940
405
|
manager.fetchRepoSettings = async function() {
|
|
@@ -998,34 +463,6 @@ class LocalManager {
|
|
|
998
463
|
}
|
|
999
464
|
};
|
|
1000
465
|
|
|
1001
|
-
// Patch PRManager.restoreUserComment for local mode
|
|
1002
|
-
// Uses the local API endpoint to restore dismissed comments
|
|
1003
|
-
manager.restoreUserComment = async function(commentId) {
|
|
1004
|
-
try {
|
|
1005
|
-
const response = await fetch(`/api/local/${reviewId}/user-comments/${commentId}/restore`, {
|
|
1006
|
-
method: 'PUT'
|
|
1007
|
-
});
|
|
1008
|
-
if (!response.ok) throw new Error('Failed to restore comment');
|
|
1009
|
-
|
|
1010
|
-
// Reload comments to update both the diff view and AI panel
|
|
1011
|
-
// Pass the current filter state from the AI panel
|
|
1012
|
-
const includeDismissed = window.aiPanel?.showDismissedComments || false;
|
|
1013
|
-
await manager.loadUserComments(includeDismissed);
|
|
1014
|
-
|
|
1015
|
-
// Show success toast
|
|
1016
|
-
if (window.toast) {
|
|
1017
|
-
window.toast.showSuccess('Comment restored');
|
|
1018
|
-
}
|
|
1019
|
-
} catch (error) {
|
|
1020
|
-
console.error('Error restoring comment:', error);
|
|
1021
|
-
if (window.toast) {
|
|
1022
|
-
window.toast.showError('Failed to restore comment');
|
|
1023
|
-
} else {
|
|
1024
|
-
alert('Failed to restore comment');
|
|
1025
|
-
}
|
|
1026
|
-
}
|
|
1027
|
-
};
|
|
1028
|
-
|
|
1029
466
|
console.log('PRManager patched for local mode');
|
|
1030
467
|
}
|
|
1031
468
|
|
|
@@ -1050,7 +487,7 @@ class LocalManager {
|
|
|
1050
487
|
// Determine endpoint and body based on whether this is a council analysis
|
|
1051
488
|
let analyzeUrl, analyzeBody;
|
|
1052
489
|
if (config.isCouncil) {
|
|
1053
|
-
analyzeUrl = `/api/local/${this.reviewId}/
|
|
490
|
+
analyzeUrl = `/api/local/${this.reviewId}/analyses/council`;
|
|
1054
491
|
analyzeBody = {
|
|
1055
492
|
councilId: config.councilId || undefined,
|
|
1056
493
|
councilConfig: config.councilConfig || undefined,
|
|
@@ -1058,7 +495,7 @@ class LocalManager {
|
|
|
1058
495
|
customInstructions: config.customInstructions || null
|
|
1059
496
|
};
|
|
1060
497
|
} else {
|
|
1061
|
-
analyzeUrl = `/api/local/${this.reviewId}/
|
|
498
|
+
analyzeUrl = `/api/local/${this.reviewId}/analyses`;
|
|
1062
499
|
analyzeBody = {
|
|
1063
500
|
provider: config.provider || 'claude',
|
|
1064
501
|
model: config.model || 'opus',
|
|
@@ -1361,8 +798,10 @@ class LocalManager {
|
|
|
1361
798
|
// Check for running analysis
|
|
1362
799
|
await manager.checkRunningAnalysis();
|
|
1363
800
|
|
|
1364
|
-
//
|
|
1365
|
-
|
|
801
|
+
// Listen for review mutation events via multiplexed SSE
|
|
802
|
+
if (window.prManager?._initReviewEventListeners) {
|
|
803
|
+
window.prManager._initReviewEventListeners();
|
|
804
|
+
}
|
|
1366
805
|
|
|
1367
806
|
} catch (error) {
|
|
1368
807
|
console.error('Error loading local review:', error);
|
|
@@ -1372,44 +811,6 @@ class LocalManager {
|
|
|
1372
811
|
}
|
|
1373
812
|
}
|
|
1374
813
|
|
|
1375
|
-
/**
|
|
1376
|
-
* Listen for externally-imported analysis results via SSE.
|
|
1377
|
-
* When the POST /api/analysis-results endpoint stores new suggestions,
|
|
1378
|
-
* it broadcasts on the `local-${reviewId}` key. This listener picks
|
|
1379
|
-
* that up and refreshes suggestions automatically.
|
|
1380
|
-
*/
|
|
1381
|
-
startExternalResultsListener() {
|
|
1382
|
-
if (this._externalResultsSource) return;
|
|
1383
|
-
const reviewId = this.reviewId;
|
|
1384
|
-
|
|
1385
|
-
this._externalResultsSource = new EventSource(
|
|
1386
|
-
`/api/local/${reviewId}/ai-suggestions/status`
|
|
1387
|
-
);
|
|
1388
|
-
|
|
1389
|
-
this._externalResultsSource.onmessage = (event) => {
|
|
1390
|
-
try {
|
|
1391
|
-
const data = JSON.parse(event.data);
|
|
1392
|
-
if (data.type === 'progress' && data.status === 'completed' && data.source === 'external') {
|
|
1393
|
-
console.log('External analysis results detected, refreshing suggestions');
|
|
1394
|
-
const manager = window.prManager;
|
|
1395
|
-
if (manager?.analysisHistoryManager) {
|
|
1396
|
-
manager.analysisHistoryManager.refresh({ switchToNew: true })
|
|
1397
|
-
.then(() => manager.loadAISuggestions());
|
|
1398
|
-
} else if (manager?.loadAISuggestions) {
|
|
1399
|
-
manager.loadAISuggestions();
|
|
1400
|
-
}
|
|
1401
|
-
}
|
|
1402
|
-
} catch (e) { /* ignore parse errors */ }
|
|
1403
|
-
};
|
|
1404
|
-
|
|
1405
|
-
// Clean up on page unload
|
|
1406
|
-
window.addEventListener('beforeunload', () => {
|
|
1407
|
-
if (this._externalResultsSource) {
|
|
1408
|
-
this._externalResultsSource.close();
|
|
1409
|
-
this._externalResultsSource = null;
|
|
1410
|
-
}
|
|
1411
|
-
});
|
|
1412
|
-
}
|
|
1413
814
|
|
|
1414
815
|
/**
|
|
1415
816
|
* Initialize inline name editing for the review title in the header
|
|
@@ -1678,6 +1079,7 @@ class LocalManager {
|
|
|
1678
1079
|
|
|
1679
1080
|
// Parse the unified diff to extract files
|
|
1680
1081
|
const filePatchMap = manager.parseUnifiedDiff(diffContent);
|
|
1082
|
+
manager.filePatches = filePatchMap;
|
|
1681
1083
|
|
|
1682
1084
|
// Build file list from diff
|
|
1683
1085
|
const files = [];
|