@in-the-loop-labs/pair-review 1.6.1 → 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 (68) 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 +1875 -144
  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 +28 -18
  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/components/StatusIndicator.js +2 -2
  17. package/public/js/components/Toast.js +22 -1
  18. package/public/js/components/VoiceCentricConfigTab.js +2 -2
  19. package/public/js/index.js +8 -0
  20. package/public/js/local.js +25 -682
  21. package/public/js/modules/analysis-history.js +19 -66
  22. package/public/js/modules/comment-manager.js +57 -19
  23. package/public/js/modules/diff-context.js +176 -0
  24. package/public/js/modules/diff-renderer.js +30 -0
  25. package/public/js/modules/file-comment-manager.js +126 -105
  26. package/public/js/modules/file-list-merger.js +64 -0
  27. package/public/js/modules/panel-resizer.js +25 -6
  28. package/public/js/modules/suggestion-manager.js +40 -125
  29. package/public/js/pr.js +974 -178
  30. package/public/js/repo-settings.js +36 -6
  31. package/public/js/utils/category-emoji.js +44 -0
  32. package/public/js/utils/time.js +32 -0
  33. package/public/local.html +107 -71
  34. package/public/pr.html +107 -71
  35. package/public/repo-settings.html +32 -0
  36. package/src/ai/analyzer.js +8 -4
  37. package/src/ai/claude-provider.js +22 -11
  38. package/src/ai/copilot-provider.js +39 -9
  39. package/src/ai/cursor-agent-provider.js +36 -7
  40. package/src/ai/gemini-provider.js +17 -4
  41. package/src/ai/prompts/config.js +7 -1
  42. package/src/ai/provider-availability.js +1 -1
  43. package/src/ai/provider.js +25 -37
  44. package/src/ai/stream-parser.js +1 -1
  45. package/src/chat/CONVENTIONS.md +18 -0
  46. package/src/chat/pi-bridge.js +491 -0
  47. package/src/chat/prompt-builder.js +262 -0
  48. package/src/chat/session-manager.js +619 -0
  49. package/src/config.js +14 -0
  50. package/src/database.js +322 -15
  51. package/src/main.js +4 -17
  52. package/src/routes/analyses.js +721 -0
  53. package/src/routes/chat.js +655 -0
  54. package/src/routes/config.js +29 -8
  55. package/src/routes/context-files.js +223 -0
  56. package/src/routes/local.js +225 -1133
  57. package/src/routes/mcp.js +39 -30
  58. package/src/routes/pr.js +410 -52
  59. package/src/routes/reviews.js +1035 -0
  60. package/src/routes/shared.js +5 -30
  61. package/src/server.js +34 -12
  62. package/src/sse/review-events.js +46 -0
  63. package/src/utils/auto-context.js +88 -0
  64. package/src/utils/category-emoji.js +33 -0
  65. package/src/utils/diff-file-list.js +57 -0
  66. package/public/js/components/ProgressModal.js +0 -705
  67. package/src/routes/analysis.js +0 -1600
  68. 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
  }
@@ -308,7 +235,7 @@ class LocalManager {
308
235
  const STALE_TIMEOUT = 2000;
309
236
 
310
237
  if (manager.isAnalyzing) {
311
- manager.reopenProgressModal();
238
+ manager.reopenModal();
312
239
  return;
313
240
  }
314
241
 
@@ -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();
@@ -450,18 +377,17 @@ class LocalManager {
450
377
  manager.setButtonAnalyzing(data.analysisId);
451
378
 
452
379
  // Show the appropriate progress modal
453
- if (data.status?.isCouncil && window.councilProgressModal && data.status?.councilConfig) {
380
+ if (window.councilProgressModal) {
454
381
  window.councilProgressModal.setLocalMode(reviewId);
455
382
  window.councilProgressModal.show(
456
383
  data.analysisId,
457
- data.status.councilConfig,
384
+ data.status?.isCouncil ? data.status.councilConfig : null,
458
385
  null,
459
- { configType: data.status.configType || 'advanced' }
386
+ {
387
+ configType: data.status?.isCouncil ? (data.status.configType || 'advanced') : 'single',
388
+ enabledLevels: data.status?.enabledLevels || [1, 2, 3]
389
+ }
460
390
  );
461
- } else if (window.progressModal) {
462
- // Update the SSE endpoint for progress modal
463
- self.patchProgressModalForLocal();
464
- window.progressModal.show(data.analysisId);
465
391
  }
466
392
  }
467
393
  } catch (error) {
@@ -469,473 +395,11 @@ class LocalManager {
469
395
  }
470
396
  };
471
397
 
472
- // Patch CommentManager.saveUserComment for local mode
473
- // This is the method that handles creating new comments via the form
474
- if (manager.commentManager) {
475
- const cm = manager.commentManager;
476
- const originalSaveUserComment = cm.saveUserComment.bind(cm);
477
-
478
- cm.saveUserComment = async function(textarea, formRow) {
479
- const fileName = textarea.dataset.file;
480
- const lineNumber = parseInt(textarea.dataset.line);
481
- const parsedEndLine = parseInt(textarea.dataset.lineEnd);
482
- const endLineNumber = !isNaN(parsedEndLine) ? parsedEndLine : lineNumber;
483
- const diffPosition = textarea.dataset.diffPosition ? parseInt(textarea.dataset.diffPosition) : null;
484
- const side = textarea.dataset.side || 'RIGHT';
485
- const content = textarea.value.trim();
486
-
487
- if (!content) {
488
- return;
489
- }
490
-
491
- // Prevent duplicate saves from rapid clicks or Cmd+Enter
492
- const saveBtn = formRow?.querySelector('.save-comment-btn');
493
- if (saveBtn?.dataset.saving === 'true') {
494
- return;
495
- }
496
- if (saveBtn) saveBtn.dataset.saving = 'true';
497
- if (saveBtn) saveBtn.disabled = true;
498
-
499
- try {
500
- const response = await fetch(`/api/local/${reviewId}/user-comments`, {
501
- method: 'POST',
502
- headers: {
503
- 'Content-Type': 'application/json'
504
- },
505
- body: JSON.stringify({
506
- file: fileName,
507
- line_start: lineNumber,
508
- line_end: endLineNumber,
509
- diff_position: diffPosition,
510
- side: side,
511
- body: content
512
- })
513
- });
514
-
515
- if (!response.ok) {
516
- const error = await response.json();
517
- throw new Error(error.error || 'Failed to save comment');
518
- }
519
-
520
- const result = await response.json();
521
-
522
- // Build comment object
523
- const commentData = {
524
- id: result.commentId,
525
- file: fileName,
526
- line_start: lineNumber,
527
- line_end: endLineNumber,
528
- diff_position: diffPosition,
529
- side: side, // Include side for suggestion code extraction
530
- body: content,
531
- created_at: new Date().toISOString()
532
- };
533
-
534
- // Create comment display row
535
- const targetRow = formRow.previousElementSibling;
536
- if (!targetRow) {
537
- console.error('Could not find target row for comment display');
538
- return;
539
- }
540
- cm.displayUserComment(commentData, targetRow);
541
-
542
- // Notify AI Panel about the new comment
543
- if (window.aiPanel?.addComment) {
544
- window.aiPanel.addComment(commentData);
545
- }
546
-
547
- // Hide form and clear selection
548
- cm.hideCommentForm();
549
- if (cm.prManager?.lineTracker) {
550
- cm.prManager.lineTracker.clearRangeSelection();
551
- }
552
-
553
- // Update comment count
554
- if (cm.prManager?.updateCommentCount) {
555
- cm.prManager.updateCommentCount();
556
- }
557
- } catch (error) {
558
- console.error('Error saving user comment:', error);
559
- alert('Failed to save comment: ' + error.message);
560
- // Re-enable save button on failure so the user can retry
561
- if (saveBtn) {
562
- saveBtn.dataset.saving = 'false';
563
- saveBtn.disabled = false;
564
- }
565
- }
566
- };
567
- }
568
-
569
- // Patch PRManager.deleteUserComment for local mode
570
- // DESIGN DECISION: Dismissed comments are NEVER shown in the diff panel.
571
- // They only appear in the AI/Review Panel when the "show dismissed" filter is ON.
572
- const originalDeleteUserComment = manager.deleteUserComment?.bind(manager);
573
- if (originalDeleteUserComment) {
574
- manager.deleteUserComment = async function(commentId) {
575
- try {
576
- const response = await fetch(`/api/local/${reviewId}/user-comments/${commentId}`, {
577
- method: 'DELETE'
578
- });
579
-
580
- if (!response.ok) {
581
- const error = await response.json();
582
- throw new Error(error.error || 'Failed to delete comment');
583
- }
584
-
585
- const apiResult = await response.json();
586
-
587
- // Check if dismissed comments filter is enabled for AI Panel updates
588
- const showDismissed = window.aiPanel?.showDismissedComments || false;
589
-
590
- // Always remove the comment from the diff view (design decision: dismissed comments never shown in diff)
591
- const commentRow = document.querySelector(`[data-comment-id="${commentId}"]`);
592
- if (commentRow) {
593
- commentRow.remove();
594
- manager.updateCommentCount();
595
- }
596
-
597
- // Also handle file-level comment cards
598
- const fileCommentCard = document.querySelector(`.file-comment-card[data-comment-id="${commentId}"]`);
599
- if (fileCommentCard) {
600
- const zone = fileCommentCard.closest('.file-comments-zone');
601
- fileCommentCard.remove();
602
- if (zone && manager.fileCommentManager) {
603
- manager.fileCommentManager.updateCommentCount(zone);
604
- }
605
- manager.updateCommentCount();
606
- }
607
-
608
- // Update AI Panel - transition to dismissed state or remove based on filter
609
- if (showDismissed && window.aiPanel?.updateComment) {
610
- // Update comment status to 'inactive' so it renders with dismissed styling in AI Panel
611
- window.aiPanel.updateComment(commentId, { status: 'inactive' });
612
- } else if (window.aiPanel?.removeComment) {
613
- window.aiPanel.removeComment(commentId);
614
- }
615
-
616
- // If a parent suggestion existed, the suggestion card is still collapsed/dismissed in the diff view.
617
- // Update AIPanel to show the suggestion as 'dismissed' (matching its visual state).
618
- // User can click "Show" to restore it to active state if they want to re-adopt.
619
- if (apiResult.dismissedSuggestionId && window.aiPanel?.updateFindingStatus) {
620
- window.aiPanel.updateFindingStatus(apiResult.dismissedSuggestionId, 'dismissed');
621
- }
622
-
623
- // Show success toast
624
- if (window.toast) {
625
- window.toast.showSuccess('Comment dismissed');
626
- }
627
- } catch (error) {
628
- console.error('Error deleting comment:', error);
629
- if (window.toast) {
630
- window.toast.showError('Failed to dismiss comment');
631
- }
632
- }
633
- };
634
- }
635
-
636
- // Patch PRManager.editUserComment for local mode
637
- // This method fetches comment data when editing
638
- const originalEditUserComment = manager.editUserComment?.bind(manager);
639
- if (originalEditUserComment) {
640
- manager.editUserComment = async function(commentId) {
641
- try {
642
- const commentRow = document.querySelector(`[data-comment-id="${commentId}"]`);
643
- if (!commentRow) return;
644
-
645
- const commentDiv = commentRow.querySelector('.user-comment');
646
- const bodyDiv = commentDiv.querySelector('.user-comment-body');
647
- let currentText = bodyDiv.dataset.originalMarkdown || '';
648
-
649
- // Fetch from local endpoint if needed
650
- if (!currentText) {
651
- const response = await fetch(`/api/local/${reviewId}/user-comments/${commentId}`);
652
- if (response.ok) {
653
- const data = await response.json();
654
- currentText = data.body || bodyDiv.textContent.trim();
655
- } else {
656
- currentText = bodyDiv.textContent.trim();
657
- }
658
- }
659
-
660
- if (commentDiv.classList.contains('editing-mode')) return;
661
-
662
- commentDiv.classList.add('editing-mode');
663
-
664
- const fileName = commentRow.dataset.file || '';
665
- const lineStart = commentRow.dataset.lineStart || '';
666
- const lineEnd = commentRow.dataset.lineEnd || lineStart;
667
- const side = commentRow.dataset.side || '';
668
-
669
- const editFormHTML = `
670
- <div class="user-comment-edit-form">
671
- <div class="comment-form-toolbar">
672
- <button type="button" class="btn btn-sm suggestion-btn" title="Insert a suggestion">
673
- ${CommentManager.SUGGESTION_ICON_SVG}
674
- </button>
675
- </div>
676
- <textarea
677
- id="edit-comment-${commentId}"
678
- class="comment-edit-textarea"
679
- placeholder="Enter your comment..."
680
- data-file="${fileName}"
681
- data-line="${lineStart}"
682
- data-line-end="${lineEnd}"
683
- data-side="${side || 'RIGHT'}"
684
- >${manager.escapeHtml(currentText)}</textarea>
685
- <div class="comment-edit-actions">
686
- <button class="btn btn-sm btn-primary save-edit-btn">Save</button>
687
- <button class="btn btn-sm btn-secondary cancel-edit-btn">Cancel</button>
688
- </div>
689
- </div>
690
- `;
691
-
692
- bodyDiv.style.display = 'none';
693
- bodyDiv.insertAdjacentHTML('afterend', editFormHTML);
694
-
695
- const editForm = commentDiv.querySelector('.user-comment-edit-form');
696
- const textarea = document.getElementById(`edit-comment-${commentId}`);
697
- const suggestionBtn = editForm.querySelector('.suggestion-btn');
698
- const saveBtn = editForm.querySelector('.save-edit-btn');
699
- const cancelBtn = editForm.querySelector('.cancel-edit-btn');
700
-
701
- if (textarea) {
702
- manager.autoResizeTextarea(textarea);
703
- textarea.focus();
704
- textarea.setSelectionRange(textarea.value.length, textarea.value.length);
705
- manager.updateSuggestionButtonState(textarea, suggestionBtn);
706
-
707
- suggestionBtn.addEventListener('click', () => {
708
- if (!suggestionBtn.disabled) {
709
- manager.insertSuggestionBlock(textarea, suggestionBtn);
710
- }
711
- });
712
-
713
- saveBtn.addEventListener('click', () => manager.saveEditedUserComment(commentId));
714
- cancelBtn.addEventListener('click', () => manager.cancelEditUserComment(commentId));
715
-
716
- textarea.addEventListener('input', () => {
717
- manager.autoResizeTextarea(textarea);
718
- manager.updateSuggestionButtonState(textarea, suggestionBtn);
719
- });
720
- }
721
- } catch (error) {
722
- console.error('Error editing comment:', error);
723
- alert('Failed to edit comment');
724
- }
725
- };
726
- }
727
-
728
- // Patch PRManager.saveEditedUserComment for local mode
729
- const originalSaveEditedUserComment = manager.saveEditedUserComment?.bind(manager);
730
- if (originalSaveEditedUserComment) {
731
- manager.saveEditedUserComment = async function(commentId) {
732
- // Prevent duplicate saves from rapid clicks or Cmd+Enter
733
- const editFormEl = document.querySelector(`#edit-comment-${commentId}`)?.closest('.user-comment-edit-form');
734
- const saveBtnEl = editFormEl?.querySelector('.save-edit-btn');
735
- if (saveBtnEl?.dataset.saving === 'true') {
736
- return;
737
- }
738
- if (saveBtnEl) saveBtnEl.dataset.saving = 'true';
739
- if (saveBtnEl) saveBtnEl.disabled = true;
740
-
741
- try {
742
- const textarea = document.getElementById(`edit-comment-${commentId}`);
743
- const editedText = textarea.value.trim();
744
-
745
- if (!editedText) {
746
- alert('Comment cannot be empty');
747
- textarea.focus();
748
- if (saveBtnEl) {
749
- saveBtnEl.dataset.saving = 'false';
750
- saveBtnEl.disabled = false;
751
- }
752
- return;
753
- }
754
-
755
- const response = await fetch(`/api/local/${reviewId}/user-comments/${commentId}`, {
756
- method: 'PUT',
757
- headers: { 'Content-Type': 'application/json' },
758
- body: JSON.stringify({ body: editedText })
759
- });
760
-
761
- if (!response.ok) throw new Error('Failed to update comment');
762
-
763
- const commentRow = document.querySelector(`[data-comment-id="${commentId}"]`);
764
- if (!commentRow) {
765
- console.error('Comment element not found');
766
- return;
767
- }
768
- const commentDiv = commentRow.querySelector('.user-comment');
769
- let bodyDiv = commentDiv.querySelector('.user-comment-body');
770
- const editForm = commentDiv.querySelector('.user-comment-edit-form');
771
-
772
- if (!bodyDiv) {
773
- bodyDiv = document.createElement('div');
774
- bodyDiv.className = 'user-comment-body';
775
- commentDiv.appendChild(bodyDiv);
776
- }
777
-
778
- bodyDiv.innerHTML = window.renderMarkdown ? window.renderMarkdown(editedText) : manager.escapeHtml(editedText);
779
- bodyDiv.dataset.originalMarkdown = editedText;
780
- bodyDiv.style.display = '';
781
-
782
- if (editForm) editForm.remove();
783
- commentDiv.classList.remove('editing-mode');
784
-
785
- const timestamp = commentDiv.querySelector('.user-comment-timestamp');
786
- if (timestamp) timestamp.textContent = new Date().toLocaleString();
787
-
788
- // Notify AI Panel about the updated comment body
789
- if (window.aiPanel?.updateComment) {
790
- window.aiPanel.updateComment(commentId, { body: editedText });
791
- }
792
-
793
- } catch (error) {
794
- console.error('Error saving comment:', error);
795
- alert('Failed to save comment');
796
- // Re-enable save button on failure so the user can retry
797
- if (saveBtnEl) {
798
- saveBtnEl.dataset.saving = 'false';
799
- saveBtnEl.disabled = false;
800
- }
801
- }
802
- };
803
- }
804
-
805
- // Patch PRManager.clearAllUserComments for local mode
806
- const originalClearAllUserComments = manager.clearAllUserComments?.bind(manager);
807
- if (originalClearAllUserComments) {
808
- manager.clearAllUserComments = async function() {
809
- // Count both line-level and file-level user comments
810
- const lineCommentRows = document.querySelectorAll('.user-comment-row');
811
- const fileCommentCards = document.querySelectorAll('.file-comment-card.user-comment');
812
- const totalComments = lineCommentRows.length + fileCommentCards.length;
813
-
814
- if (totalComments === 0) {
815
- if (window.toast?.showInfo) {
816
- window.toast.showInfo('No comments to clear');
817
- }
818
- return;
819
- }
820
-
821
- if (!window.confirmDialog) {
822
- alert('Confirmation dialog unavailable. Please refresh the page.');
823
- return;
824
- }
825
-
826
- const dialogResult = await window.confirmDialog.show({
827
- title: 'Clear All Comments?',
828
- message: `This will dismiss all ${totalComments} comment${totalComments !== 1 ? 's' : ''}. You can restore them later.`,
829
- confirmText: 'Clear All',
830
- confirmClass: 'btn-danger'
831
- });
832
-
833
- if (dialogResult !== 'confirm') return;
834
-
835
- try {
836
- const response = await fetch(`/api/local/${reviewId}/user-comments`, {
837
- method: 'DELETE'
838
- });
839
-
840
- if (!response.ok) throw new Error('Failed to delete comments');
841
-
842
- const result = await response.json();
843
- const deletedCount = result.deletedCount || totalComments;
844
-
845
- // Remove line-level comment rows from DOM
846
- lineCommentRows.forEach(row => row.remove());
847
-
848
- // Remove file-level comment cards from DOM
849
- fileCommentCards.forEach(card => {
850
- const zone = card.closest('.file-comments-zone');
851
- card.remove();
852
-
853
- // Update the file comment zone header button state
854
- if (zone && manager.fileCommentManager) {
855
- manager.fileCommentManager.updateCommentCount(zone);
856
- }
857
- });
858
-
859
- // Remove line-level and file-level comment elements from diff view
860
- // (They have been soft-deleted, so should not appear in the diff panel per design decision)
861
- // The comments array will be reloaded below with proper dismissed state.
862
-
863
- // Reload comments to update both internal state and AI Panel
864
- // This shows dismissed comments in AI Panel if filter is enabled, matching individual deletion behavior
865
- const includeDismissed = window.aiPanel?.showDismissedComments || false;
866
- await manager.loadUserComments(includeDismissed);
867
-
868
- // Update dismissed suggestions in the diff view UI
869
- // (AI Panel is already updated by loadUserComments via setComments)
870
- if (result.dismissedSuggestionIds && result.dismissedSuggestionIds.length > 0 && manager.updateDismissedSuggestionUI) {
871
- for (const suggestionId of result.dismissedSuggestionIds) {
872
- manager.updateDismissedSuggestionUI(suggestionId);
873
- }
874
- }
875
-
876
- // Show success toast notification
877
- if (window.toast) {
878
- window.toast.showSuccess(`Cleared ${deletedCount} comment${deletedCount !== 1 ? 's' : ''}`);
879
- }
880
- } catch (error) {
881
- console.error('Error clearing user comments:', error);
882
- if (window.toast) {
883
- window.toast.showError('Failed to clear comments');
884
- } else {
885
- alert('Failed to clear comments');
886
- }
887
- }
888
- };
889
- }
890
-
891
- // Patch SuggestionManager.createUserCommentFromSuggestion for local mode
892
- // This method is called when adopting AI suggestions
893
- if (manager.suggestionManager) {
894
- const sm = manager.suggestionManager;
895
-
896
- sm.createUserCommentFromSuggestion = async function(suggestionId, fileName, lineNumber, suggestionText, suggestionType, suggestionTitle, diffPosition, side) {
897
- // Format the comment text with emoji and category prefix
898
- const formattedText = sm.formatAdoptedComment(suggestionText, suggestionType);
899
-
900
- // Parse diff_position if it's a string (from dataset)
901
- const parsedDiffPosition = diffPosition ? parseInt(diffPosition) : null;
902
-
903
- const createResponse = await fetch(`/api/local/${reviewId}/user-comments`, {
904
- method: 'POST',
905
- headers: {
906
- 'Content-Type': 'application/json'
907
- },
908
- body: JSON.stringify({
909
- file: fileName,
910
- line_start: parseInt(lineNumber),
911
- line_end: parseInt(lineNumber),
912
- diff_position: parsedDiffPosition,
913
- side: side || 'RIGHT',
914
- body: formattedText,
915
- parent_id: suggestionId,
916
- type: suggestionType,
917
- title: suggestionTitle
918
- })
919
- });
920
-
921
- if (!createResponse.ok) {
922
- throw new Error('Failed to create user comment');
923
- }
924
-
925
- const result = await createResponse.json();
926
- return {
927
- id: result.commentId,
928
- file: fileName,
929
- line_start: parseInt(lineNumber),
930
- body: formattedText,
931
- type: suggestionType,
932
- title: suggestionTitle,
933
- parent_id: suggestionId,
934
- diff_position: parsedDiffPosition,
935
- created_at: new Date().toISOString()
936
- };
937
- };
938
- }
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.
939
403
 
940
404
  // Patch fetchRepoSettings to use the repository from local review data
941
405
  manager.fetchRepoSettings = async function() {
@@ -999,89 +463,9 @@ class LocalManager {
999
463
  }
1000
464
  };
1001
465
 
1002
- // Patch PRManager.restoreUserComment for local mode
1003
- // Uses the local API endpoint to restore dismissed comments
1004
- manager.restoreUserComment = async function(commentId) {
1005
- try {
1006
- const response = await fetch(`/api/local/${reviewId}/user-comments/${commentId}/restore`, {
1007
- method: 'PUT'
1008
- });
1009
- if (!response.ok) throw new Error('Failed to restore comment');
1010
-
1011
- // Reload comments to update both the diff view and AI panel
1012
- // Pass the current filter state from the AI panel
1013
- const includeDismissed = window.aiPanel?.showDismissedComments || false;
1014
- await manager.loadUserComments(includeDismissed);
1015
-
1016
- // Show success toast
1017
- if (window.toast) {
1018
- window.toast.showSuccess('Comment restored');
1019
- }
1020
- } catch (error) {
1021
- console.error('Error restoring comment:', error);
1022
- if (window.toast) {
1023
- window.toast.showError('Failed to restore comment');
1024
- } else {
1025
- alert('Failed to restore comment');
1026
- }
1027
- }
1028
- };
1029
-
1030
466
  console.log('PRManager patched for local mode');
1031
467
  }
1032
468
 
1033
- /**
1034
- * Patch ProgressModal to use local SSE endpoint
1035
- */
1036
- patchProgressModalForLocal() {
1037
- const modal = window.progressModal;
1038
- if (!modal) return;
1039
-
1040
- const reviewId = this.reviewId;
1041
- const originalStartMonitoring = modal.startProgressMonitoring.bind(modal);
1042
-
1043
- modal.startProgressMonitoring = function() {
1044
- if (modal.eventSource) {
1045
- modal.eventSource.close();
1046
- }
1047
-
1048
- if (!modal.currentAnalysisId) return;
1049
-
1050
- // Use local SSE endpoint
1051
- modal.eventSource = new EventSource(`/api/local/${reviewId}/ai-suggestions/status`);
1052
-
1053
- modal.eventSource.onopen = () => {
1054
- console.log('Connected to local progress stream');
1055
- };
1056
-
1057
- modal.eventSource.onmessage = (event) => {
1058
- try {
1059
- const data = JSON.parse(event.data);
1060
-
1061
- if (data.type === 'connected') {
1062
- console.log('Local SSE connection established');
1063
- return;
1064
- }
1065
-
1066
- if (data.type === 'progress') {
1067
- modal.updateProgress(data);
1068
-
1069
- if (data.status === 'completed' || data.status === 'failed' || data.status === 'cancelled') {
1070
- modal.stopProgressMonitoring();
1071
- }
1072
- }
1073
- } catch (error) {
1074
- console.error('Error parsing SSE data:', error);
1075
- }
1076
- };
1077
-
1078
- modal.eventSource.onerror = (error) => {
1079
- console.error('SSE connection error:', error);
1080
- modal.fallbackToPolling();
1081
- };
1082
- };
1083
- }
1084
-
1085
469
  /**
1086
470
  * Start local AI analysis
1087
471
  */
@@ -1103,7 +487,7 @@ class LocalManager {
1103
487
  // Determine endpoint and body based on whether this is a council analysis
1104
488
  let analyzeUrl, analyzeBody;
1105
489
  if (config.isCouncil) {
1106
- analyzeUrl = `/api/local/${this.reviewId}/analyze/council`;
490
+ analyzeUrl = `/api/local/${this.reviewId}/analyses/council`;
1107
491
  analyzeBody = {
1108
492
  councilId: config.councilId || undefined,
1109
493
  councilConfig: config.councilConfig || undefined,
@@ -1111,7 +495,7 @@ class LocalManager {
1111
495
  customInstructions: config.customInstructions || null
1112
496
  };
1113
497
  } else {
1114
- analyzeUrl = `/api/local/${this.reviewId}/analyze`;
498
+ analyzeUrl = `/api/local/${this.reviewId}/analyses`;
1115
499
  analyzeBody = {
1116
500
  provider: config.provider || 'claude',
1117
501
  model: config.model || 'opus',
@@ -1158,12 +542,6 @@ class LocalManager {
1158
542
  enabledLevels: config.enabledLevels || [1, 2, 3]
1159
543
  }
1160
544
  );
1161
- } else {
1162
- // Fallback to old progress modal if unified modal not available
1163
- this.patchProgressModalForLocal();
1164
- if (window.progressModal) {
1165
- window.progressModal.show(result.analysisId);
1166
- }
1167
545
  }
1168
546
 
1169
547
  } catch (error) {
@@ -1420,8 +798,10 @@ class LocalManager {
1420
798
  // Check for running analysis
1421
799
  await manager.checkRunningAnalysis();
1422
800
 
1423
- // Open persistent SSE connection to detect externally-imported results
1424
- this.startExternalResultsListener();
801
+ // Listen for review mutation events via multiplexed SSE
802
+ if (window.prManager?._initReviewEventListeners) {
803
+ window.prManager._initReviewEventListeners();
804
+ }
1425
805
 
1426
806
  } catch (error) {
1427
807
  console.error('Error loading local review:', error);
@@ -1431,44 +811,6 @@ class LocalManager {
1431
811
  }
1432
812
  }
1433
813
 
1434
- /**
1435
- * Listen for externally-imported analysis results via SSE.
1436
- * When the POST /api/analysis-results endpoint stores new suggestions,
1437
- * it broadcasts on the `local-${reviewId}` key. This listener picks
1438
- * that up and refreshes suggestions automatically.
1439
- */
1440
- startExternalResultsListener() {
1441
- if (this._externalResultsSource) return;
1442
- const reviewId = this.reviewId;
1443
-
1444
- this._externalResultsSource = new EventSource(
1445
- `/api/local/${reviewId}/ai-suggestions/status`
1446
- );
1447
-
1448
- this._externalResultsSource.onmessage = (event) => {
1449
- try {
1450
- const data = JSON.parse(event.data);
1451
- if (data.type === 'progress' && data.status === 'completed' && data.source === 'external') {
1452
- console.log('External analysis results detected, refreshing suggestions');
1453
- const manager = window.prManager;
1454
- if (manager?.analysisHistoryManager) {
1455
- manager.analysisHistoryManager.refresh({ switchToNew: true })
1456
- .then(() => manager.loadAISuggestions());
1457
- } else if (manager?.loadAISuggestions) {
1458
- manager.loadAISuggestions();
1459
- }
1460
- }
1461
- } catch (e) { /* ignore parse errors */ }
1462
- };
1463
-
1464
- // Clean up on page unload
1465
- window.addEventListener('beforeunload', () => {
1466
- if (this._externalResultsSource) {
1467
- this._externalResultsSource.close();
1468
- this._externalResultsSource = null;
1469
- }
1470
- });
1471
- }
1472
814
 
1473
815
  /**
1474
816
  * Initialize inline name editing for the review title in the header
@@ -1737,6 +1079,7 @@ class LocalManager {
1737
1079
 
1738
1080
  // Parse the unified diff to extract files
1739
1081
  const filePatchMap = manager.parseUnifiedDiff(diffContent);
1082
+ manager.filePatches = filePatchMap;
1740
1083
 
1741
1084
  // Build file list from diff
1742
1085
  const files = [];
@@ -1847,7 +1190,7 @@ class LocalManager {
1847
1190
 
1848
1191
  // Map levels to dot phases
1849
1192
  const phaseMap = {
1850
- 4: 'orchestration', // Orchestration/finalization is level 4 in ProgressModal
1193
+ 4: 'orchestration', // Orchestration/finalization is level 4 in progress modal
1851
1194
  1: 'level1',
1852
1195
  2: 'level2',
1853
1196
  3: 'level3'