@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.
Files changed (62) 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 +1930 -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 +2952 -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 +57 -19
  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 +964 -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 +36 -7
  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 +262 -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 +223 -0
  51. package/src/routes/local.js +225 -1133
  52. package/src/routes/mcp.js +39 -30
  53. package/src/routes/pr.js +410 -52
  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-file-list.js +57 -0
  61. package/src/routes/analysis.js +0 -1600
  62. package/src/routes/comments.js +0 -534
@@ -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
- // Override loadUserComments
175
- // DESIGN DECISION: Dismissed comments are NEVER shown in the diff panel.
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/local/${reviewId}/has-ai-suggestions`;
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/local/${reviewId}/suggestions?levels=${filterLevel}`;
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/local/${reviewId}/analysis-status`);
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
- // Patch CommentManager.saveUserComment for local mode
472
- // This is the method that handles creating new comments via the form
473
- if (manager.commentManager) {
474
- const cm = manager.commentManager;
475
- const originalSaveUserComment = cm.saveUserComment.bind(cm);
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}/analyze/council`;
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}/analyze`;
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
- // Open persistent SSE connection to detect externally-imported results
1365
- this.startExternalResultsListener();
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 = [];