@in-the-loop-labs/pair-review 3.4.1 → 3.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -1
- package/package.json +15 -20
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/skills/analyze/scripts/git-diff-lines +0 -0
- package/public/css/pr.css +762 -6
- package/public/js/components/AIPanel.js +486 -42
- package/public/js/components/ChatPanel.js +2002 -528
- package/public/js/modules/comment-minimizer.js +66 -20
- package/public/js/modules/external-comment-manager.js +870 -0
- package/public/js/pr.js +297 -20
- package/public/local.html +21 -5
- package/public/pr.html +31 -5
- package/src/ai/claude-provider.js +68 -56
- package/src/ai/codex-provider.js +64 -33
- package/src/chat/api-reference.js +1 -1
- package/src/chat/chat-providers.js +90 -12
- package/src/chat/codex-bridge.js +238 -29
- package/src/chat/session-manager.js +1 -0
- package/src/config.js +2 -1
- package/src/database.js +566 -2
- package/src/external/github-adapter.js +152 -0
- package/src/external/index.js +37 -0
- package/src/github/client.js +77 -1
- package/src/main.js +3 -2
- package/src/routes/config.js +27 -0
- package/src/routes/external-comments.js +394 -0
- package/src/server.js +9 -0
package/public/js/pr.js
CHANGED
|
@@ -305,6 +305,21 @@ class PRManager {
|
|
|
305
305
|
refreshBtn.addEventListener('click', () => this.refreshPR());
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
+
// Refresh external (GitHub) review comments. Handler lives on the
|
|
309
|
+
// instance so unit tests can call it directly (avoids duplicating the
|
|
310
|
+
// production click logic in tests, per CLAUDE.md).
|
|
311
|
+
// Skip wiring entirely when the external_comments feature toggle is
|
|
312
|
+
// off — the button is also CSS-hidden via .external-comments-only, so
|
|
313
|
+
// this is belt-and-suspenders.
|
|
314
|
+
if (this._externalCommentsEnabled()) {
|
|
315
|
+
const refreshExternalBtnPanel = document.getElementById('refresh-external-comments-btn-panel');
|
|
316
|
+
if (refreshExternalBtnPanel) {
|
|
317
|
+
refreshExternalBtnPanel.addEventListener('click', () => {
|
|
318
|
+
void this._handleExternalCommentsRefreshClick({ button: refreshExternalBtnPanel });
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
308
323
|
// PR description popover
|
|
309
324
|
this.setupPRDescriptionPopover();
|
|
310
325
|
|
|
@@ -612,6 +627,15 @@ class PRManager {
|
|
|
612
627
|
// Fire-and-forget staleness check — shows badge or auto-refreshes
|
|
613
628
|
this._stalenessPromise = this._checkStalenessOnLoad(owner, repo, number);
|
|
614
629
|
|
|
630
|
+
// Fire-and-forget external review-comment sync + render.
|
|
631
|
+
// Runs after the diff and user comments have been rendered so the
|
|
632
|
+
// external-comment manager has DOM anchors to attach blue rows to.
|
|
633
|
+
// Failures are swallowed inside _loadExternalComments and surface via
|
|
634
|
+
// console.warn; they must never block the main PR-load path.
|
|
635
|
+
void this._loadExternalComments().catch((err) => {
|
|
636
|
+
console.warn('[external-comments] _loadExternalComments threw', err);
|
|
637
|
+
});
|
|
638
|
+
|
|
615
639
|
} catch (error) {
|
|
616
640
|
console.error('Error loading PR:', error);
|
|
617
641
|
this.showError(error.message);
|
|
@@ -623,16 +647,260 @@ class PRManager {
|
|
|
623
647
|
}
|
|
624
648
|
|
|
625
649
|
/**
|
|
626
|
-
*
|
|
627
|
-
*
|
|
628
|
-
*
|
|
650
|
+
* Sync external review comments against the source system and render the
|
|
651
|
+
* results. Non-blocking from the user's perspective: a sync failure does
|
|
652
|
+
* not prevent rendering of any cached rows from a previous run.
|
|
653
|
+
*
|
|
654
|
+
* No-op in local mode (external sources require a real GitHub PR).
|
|
655
|
+
*/
|
|
656
|
+
/**
|
|
657
|
+
* Re-render every overlay (AI suggestions, user comments, external comments)
|
|
658
|
+
* after the underlying diff DOM has been rebuilt.
|
|
659
|
+
*
|
|
660
|
+
* Used by anything that destroys and re-mounts the diff (refreshPR, the
|
|
661
|
+
* whitespace toggle, pre-analysis refresh). Centralizing keeps the three
|
|
662
|
+
* renderers from drifting: each one has its own clear()/append cycle and
|
|
663
|
+
* forgetting any of them produces hard-to-spot bugs like "comments
|
|
664
|
+
* disappeared after refresh".
|
|
665
|
+
*
|
|
666
|
+
* @param {Object} [options]
|
|
667
|
+
* @param {string} [options.analysisRunId] - Optional run id to pin AI suggestions to.
|
|
668
|
+
* @param {boolean} [options.syncExternal=false] - When true, fire the
|
|
669
|
+
* external-comments sync POST before re-rendering. Used by `refreshPR`
|
|
670
|
+
* where the diff was just fetched fresh from GitHub and cached anchors
|
|
671
|
+
* may not match the new commit. GET-only callers (whitespace toggle,
|
|
672
|
+
* analysis rebuilds, post-analysis reload) pass the default and reuse
|
|
673
|
+
* the existing mirror.
|
|
674
|
+
*/
|
|
675
|
+
async _rerenderAllOverlays({ analysisRunId, syncExternal = false } = {}) {
|
|
676
|
+
const includeDismissed = window.aiPanel?.showDismissedComments || false;
|
|
677
|
+
await this.loadUserComments(includeDismissed);
|
|
678
|
+
await this.loadAISuggestions(null, analysisRunId);
|
|
679
|
+
// Skip external-comment re-rendering entirely when the feature is off.
|
|
680
|
+
// The manager is never populated with rows in that case, so there's
|
|
681
|
+
// nothing to re-anchor.
|
|
682
|
+
if (!this._externalCommentsEnabled()) return;
|
|
683
|
+
try {
|
|
684
|
+
if (typeof window !== 'undefined' && window.externalCommentManager) {
|
|
685
|
+
if (this.currentPR && this.currentPR.id) {
|
|
686
|
+
window.externalCommentManager.reviewId = this.currentPR.id;
|
|
687
|
+
}
|
|
688
|
+
if (syncExternal) {
|
|
689
|
+
// refreshPR path: full sync+load through the canonical entry
|
|
690
|
+
// point. `_loadExternalComments` already owns the toast + error
|
|
691
|
+
// wiring for the sync result.
|
|
692
|
+
await this._loadExternalComments();
|
|
693
|
+
} else {
|
|
694
|
+
// GET-only path: the local mirror is current; just re-anchor
|
|
695
|
+
// rows on the freshly-rebuilt diff DOM.
|
|
696
|
+
await window.externalCommentManager.loadAndRender();
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
} catch (err) {
|
|
700
|
+
console.warn('[external-comments] re-render after diff rebuild failed', err);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Click handler for the "refresh external comments" header button.
|
|
706
|
+
*
|
|
707
|
+
* Extracted from `setupEventListeners` so unit tests can exercise the
|
|
708
|
+
* production handler directly without re-implementing it (CLAUDE.md
|
|
709
|
+
* forbids duplicating production code in tests). The DOM button id is
|
|
710
|
+
* the canonical attach point — pass it in for tests that build their
|
|
711
|
+
* own button.
|
|
712
|
+
*
|
|
713
|
+
* @param {Object} [options]
|
|
714
|
+
* @param {HTMLElement} [options.button] - The button element to toggle. Defaults to `#refresh-external-comments-btn-panel`.
|
|
715
|
+
* @returns {Promise<void>}
|
|
716
|
+
*/
|
|
717
|
+
async _handleExternalCommentsRefreshClick({ button } = {}) {
|
|
718
|
+
// Defensive guard: even if a stale caller invokes this with the
|
|
719
|
+
// feature disabled, swallow it. UI wiring already skips this path.
|
|
720
|
+
if (!this._externalCommentsEnabled()) return;
|
|
721
|
+
const btn = button
|
|
722
|
+
|| (typeof document !== 'undefined' ? document.getElementById('refresh-external-comments-btn-panel') : null);
|
|
723
|
+
if (!btn || btn.disabled) return;
|
|
724
|
+
btn.disabled = true;
|
|
725
|
+
btn.classList.add('is-refreshing');
|
|
726
|
+
btn.setAttribute('aria-busy', 'true');
|
|
727
|
+
btn.removeAttribute('data-state');
|
|
728
|
+
try {
|
|
729
|
+
await this._loadExternalComments();
|
|
730
|
+
} finally {
|
|
731
|
+
btn.disabled = false;
|
|
732
|
+
btn.classList.remove('is-refreshing');
|
|
733
|
+
btn.removeAttribute('aria-busy');
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* Feature toggle for the external-comments (GitHub PR review-comment)
|
|
739
|
+
* subsystem. Reads `window.PAIR_REVIEW_RUNTIME_CONFIG` which is set
|
|
740
|
+
* synchronously by `/runtime-config.js` BEFORE this file loads, so the
|
|
741
|
+
* check is safe at any call site. Defaults to enabled when the flag is
|
|
742
|
+
* missing — preserves behavior for any environment that omits the
|
|
743
|
+
* runtime-config script (e.g., older test fixtures).
|
|
744
|
+
* @returns {boolean}
|
|
745
|
+
*/
|
|
746
|
+
_externalCommentsEnabled() {
|
|
747
|
+
if (typeof window === 'undefined') return true;
|
|
748
|
+
return window.PAIR_REVIEW_RUNTIME_CONFIG?.external_comments_enabled !== false;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
async _loadExternalComments() {
|
|
752
|
+
if (typeof window !== 'undefined' && window.PAIR_REVIEW_LOCAL_MODE) return;
|
|
753
|
+
if (!this._externalCommentsEnabled()) return;
|
|
754
|
+
if (!this.currentPR || !this.currentPR.id) return;
|
|
755
|
+
if (typeof window === 'undefined' || !window.externalCommentManager) return;
|
|
756
|
+
|
|
757
|
+
window.externalCommentManager.reviewId = this.currentPR.id;
|
|
758
|
+
|
|
759
|
+
// Route through the manager's `syncAndRender`. That method owns the
|
|
760
|
+
// in-flight guard for the FULL sync+load sequence, so a GET-only
|
|
761
|
+
// caller (analysis rebuild, whitespace toggle) that hits
|
|
762
|
+
// `loadAndRender` during this window joins the same promise instead
|
|
763
|
+
// of racing the POST with a stale GET. The manager surfaces sync
|
|
764
|
+
// result and sync error separately so this method can fire the
|
|
765
|
+
// status-aware toasts without intercepting render.
|
|
766
|
+
let result;
|
|
767
|
+
try {
|
|
768
|
+
result = await window.externalCommentManager.syncAndRender({
|
|
769
|
+
syncFn: () => this._syncExternalComments(),
|
|
770
|
+
});
|
|
771
|
+
} catch (err) {
|
|
772
|
+
console.warn('[external-comments] syncAndRender failed', err);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (result && result.syncError) {
|
|
777
|
+
// Sync failed but render proceeded against the previously-cached
|
|
778
|
+
// mirror. Toast + button-error cue so the reviewer knows the
|
|
779
|
+
// counts may lag upstream.
|
|
780
|
+
this._showExternalSyncErrorToast(result.syncError);
|
|
781
|
+
this._markExternalRefreshErrorState();
|
|
782
|
+
console.warn('[external-comments] sync failed; rendering cached data only', result.syncError);
|
|
783
|
+
} else if (result && result.syncResult && typeof result.syncResult.lostAnchors === 'number' && result.syncResult.lostAnchors > 0) {
|
|
784
|
+
// Surface lost-anchor counts so the reviewer knows why visible
|
|
785
|
+
// external-comment counts may lag what GitHub shows — these are
|
|
786
|
+
// comments whose anchors were destroyed upstream (force-push, file
|
|
787
|
+
// delete, etc.) and have no place to render in the current diff.
|
|
788
|
+
this._showExternalLostAnchorsToast(result.syncResult.lostAnchors);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Pick a toast message by HTTP status for a failed external-comment sync.
|
|
794
|
+
* Falls back to a generic message when status is missing or unknown.
|
|
795
|
+
* @private
|
|
796
|
+
*/
|
|
797
|
+
_showExternalSyncErrorToast(err) {
|
|
798
|
+
const status = err && typeof err.status === 'number' ? err.status : 0;
|
|
799
|
+
let message;
|
|
800
|
+
if (status === 401) {
|
|
801
|
+
message = 'GitHub token missing or invalid — external comments not refreshed.';
|
|
802
|
+
} else if (status === 403) {
|
|
803
|
+
message = 'GitHub denied the request (403) — external comments not refreshed.';
|
|
804
|
+
} else if (status === 429) {
|
|
805
|
+
message = 'GitHub rate limited — external comments not refreshed.';
|
|
806
|
+
} else {
|
|
807
|
+
message = 'Could not refresh GitHub review comments.';
|
|
808
|
+
}
|
|
809
|
+
try {
|
|
810
|
+
if (typeof window !== 'undefined') {
|
|
811
|
+
if (window.toast && typeof window.toast.showError === 'function') {
|
|
812
|
+
window.toast.showError(message);
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
if (typeof window.showToast === 'function') {
|
|
816
|
+
window.showToast(message, 'error');
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
} catch {
|
|
821
|
+
// toast helpers must never break the page; swallow.
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Show a warning toast describing how many external comments couldn't be
|
|
827
|
+
* anchored to the current diff (lost-anchor count from the sync result).
|
|
828
|
+
* Mirrors `_showExternalSyncErrorToast` so failures and partial successes
|
|
829
|
+
* have symmetrical UI treatment.
|
|
830
|
+
* @param {number} count - Number of lost-anchor comments
|
|
831
|
+
* @private
|
|
832
|
+
*/
|
|
833
|
+
_showExternalLostAnchorsToast(count) {
|
|
834
|
+
if (typeof window === 'undefined') return;
|
|
835
|
+
const noun = count === 1 ? 'comment' : 'comments';
|
|
836
|
+
const message = `${count} ${noun} lost their anchor due to upstream changes`;
|
|
837
|
+
try {
|
|
838
|
+
if (window.toast && typeof window.toast.showWarning === 'function') {
|
|
839
|
+
window.toast.showWarning(message);
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
if (window.toast && typeof window.toast.showInfo === 'function') {
|
|
843
|
+
window.toast.showInfo(message);
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
if (typeof window.showToast === 'function') {
|
|
847
|
+
window.showToast(message, 'warn');
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
} catch {
|
|
851
|
+
// toast helpers must never break the page; swallow.
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* Briefly mark the refresh button so the user notices the failure even if
|
|
857
|
+
* they dismissed the toast. Cleared after 4s; no state machine — best
|
|
858
|
+
* effort cue. No-op when the button isn't present.
|
|
859
|
+
* @private
|
|
860
|
+
*/
|
|
861
|
+
_markExternalRefreshErrorState() {
|
|
862
|
+
if (typeof document === 'undefined') return;
|
|
863
|
+
const btn = document.getElementById('refresh-external-comments-btn-panel');
|
|
864
|
+
if (!btn) return;
|
|
865
|
+
btn.setAttribute('data-state', 'error');
|
|
866
|
+
setTimeout(() => {
|
|
867
|
+
if (btn.getAttribute('data-state') === 'error') {
|
|
868
|
+
btn.removeAttribute('data-state');
|
|
869
|
+
}
|
|
870
|
+
}, 4000);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* POST to the sync endpoint for the GitHub source. Coalesced server-side
|
|
875
|
+
* for concurrent (review, source) calls; safe to invoke from page-load and
|
|
876
|
+
* the manual refresh button in parallel.
|
|
877
|
+
* @returns {Promise<{ count: number, lostAnchors: number, syncedAt: string }>}
|
|
878
|
+
*/
|
|
879
|
+
async _syncExternalComments() {
|
|
880
|
+
const source = 'github';
|
|
881
|
+
const res = await fetch(
|
|
882
|
+
`/api/reviews/${this.currentPR.id}/external-comments/sync?source=${source}`,
|
|
883
|
+
{ method: 'POST' }
|
|
884
|
+
);
|
|
885
|
+
if (!res.ok) {
|
|
886
|
+
const body = await res.json().catch(() => ({}));
|
|
887
|
+
const err = new Error(body.error || `Sync failed with status ${res.status}`);
|
|
888
|
+
err.status = res.status;
|
|
889
|
+
throw err;
|
|
890
|
+
}
|
|
891
|
+
return res.json();
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Reload AI suggestions, user comments, and external comments after an
|
|
896
|
+
* analysis completes. Shared by the foreground `analysis_completed`
|
|
897
|
+
* handler and the deferred `_dirtyAnalysis` branch in the
|
|
898
|
+
* visibilitychange listener. Routing through `_rerenderAllOverlays`
|
|
899
|
+
* keeps external-comment rows in sync with the other two overlays
|
|
900
|
+
* whenever post-analysis refresh fires.
|
|
629
901
|
*/
|
|
630
902
|
_reloadAfterAnalysis() {
|
|
631
|
-
|
|
632
|
-
return Promise.all([
|
|
633
|
-
this.loadAISuggestions(),
|
|
634
|
-
this.loadUserComments(includeDismissed)
|
|
635
|
-
]);
|
|
903
|
+
return this._rerenderAllOverlays();
|
|
636
904
|
}
|
|
637
905
|
|
|
638
906
|
/**
|
|
@@ -842,10 +1110,11 @@ class PRManager {
|
|
|
842
1110
|
// Re-fetch and re-render the diff
|
|
843
1111
|
await this.loadAndDisplayFiles(owner, repo, number);
|
|
844
1112
|
|
|
845
|
-
// Re-anchor
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
1113
|
+
// Re-anchor every overlay (user comments, AI suggestions, external
|
|
1114
|
+
// comments) via the shared helper so the three renderers can't drift.
|
|
1115
|
+
// The diff DOM was just rebuilt, so external-comment rows are gone
|
|
1116
|
+
// until loadAndRender re-inserts them.
|
|
1117
|
+
await this._rerenderAllOverlays({ analysisRunId: this.selectedRunId });
|
|
849
1118
|
|
|
850
1119
|
// Restore scroll position after the DOM settles
|
|
851
1120
|
requestAnimationFrame(() => {
|
|
@@ -5221,14 +5490,22 @@ class PRManager {
|
|
|
5221
5490
|
// Reload the files/diff with fresh data
|
|
5222
5491
|
await this.loadAndDisplayFiles(owner, repo, number);
|
|
5223
5492
|
|
|
5224
|
-
// Re-render
|
|
5225
|
-
// (renderDiff clears the diff container
|
|
5226
|
-
|
|
5227
|
-
|
|
5228
|
-
//
|
|
5229
|
-
// (
|
|
5230
|
-
//
|
|
5231
|
-
|
|
5493
|
+
// Re-render the three independent overlay layers on the fresh DOM
|
|
5494
|
+
// (renderDiff clears the diff container). Going through the shared
|
|
5495
|
+
// helper guarantees the three renderers can't drift again — adding
|
|
5496
|
+
// a fourth overlay only requires updating this one place.
|
|
5497
|
+
// `syncExternal: true` because refreshPR fetched a brand-new diff
|
|
5498
|
+
// (commit SHA may have changed). Cached external-comment anchors
|
|
5499
|
+
// need a sync POST to re-evaluate which ones are outdated against
|
|
5500
|
+
// the new HEAD — otherwise we'd render stale `is_outdated` flags.
|
|
5501
|
+
// Note: Unlike loadPR() which skips loadAISuggestions when
|
|
5502
|
+
// analysisHistoryManager exists (because the manager triggers it via
|
|
5503
|
+
// onSelectionChange on init), refresh must call unconditionally
|
|
5504
|
+
// since the manager won't re-fire its callback.
|
|
5505
|
+
await this._rerenderAllOverlays({
|
|
5506
|
+
analysisRunId: this.selectedRunId,
|
|
5507
|
+
syncExternal: true,
|
|
5508
|
+
});
|
|
5232
5509
|
|
|
5233
5510
|
// Restore expanded folders
|
|
5234
5511
|
this.expandedFolders = expandedFolders;
|
package/public/local.html
CHANGED
|
@@ -525,11 +525,20 @@
|
|
|
525
525
|
|
|
526
526
|
<!-- Segment Control -->
|
|
527
527
|
<div class="segment-control" id="segment-control">
|
|
528
|
-
<
|
|
529
|
-
<
|
|
530
|
-
|
|
531
|
-
|
|
528
|
+
<button type="button" class="segment-scroll segment-scroll--left" id="segment-scroll-left" hidden aria-label="Scroll segments left" tabindex="-1">
|
|
529
|
+
<svg viewBox="0 0 16 16" width="12" height="12" fill="currentColor" aria-hidden="true"><path d="M9.78 12.78a.75.75 0 0 1-1.06 0L4.47 8.53a.75.75 0 0 1 0-1.06l4.25-4.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042L6.06 8l3.72 3.72a.75.75 0 0 1 0 1.06Z"/></svg>
|
|
530
|
+
</button>
|
|
531
|
+
<div class="segment-control-scroll" id="segment-control-scroll">
|
|
532
|
+
<div class="segment-control-inner">
|
|
533
|
+
<button class="segment-btn active" data-segment="ai">AI <span class="segment-count">(0)</span></button>
|
|
534
|
+
<button class="segment-btn" data-segment="comments">User <span class="segment-count">(0)</span></button>
|
|
535
|
+
<button class="segment-btn" data-segment="external" hidden>External <span class="segment-count">(0)</span></button>
|
|
536
|
+
<button class="segment-btn" data-segment="all">All <span class="segment-count">(0)</span></button>
|
|
537
|
+
</div>
|
|
532
538
|
</div>
|
|
539
|
+
<button type="button" class="segment-scroll segment-scroll--right" id="segment-scroll-right" hidden aria-label="Scroll segments right" tabindex="-1">
|
|
540
|
+
<svg viewBox="0 0 16 16" width="12" height="12" fill="currentColor" aria-hidden="true"><path d="M6.22 3.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L9.94 8 6.22 4.28a.75.75 0 0 1 0-1.06Z"/></svg>
|
|
541
|
+
</button>
|
|
533
542
|
</div>
|
|
534
543
|
|
|
535
544
|
<!-- Level Filter -->
|
|
@@ -543,7 +552,9 @@
|
|
|
543
552
|
<!-- Findings Summary -->
|
|
544
553
|
<div class="findings-summary" id="findings-summary">
|
|
545
554
|
<div class="findings-header">
|
|
546
|
-
|
|
555
|
+
<!-- Nav controls (prev | n of N | next) are injected by AIPanel.updateFindingsHeader.
|
|
556
|
+
Local mode has no external comments, so the actions slot stays empty. -->
|
|
557
|
+
<div class="findings-nav" id="findings-nav"></div>
|
|
547
558
|
</div>
|
|
548
559
|
<div class="findings-list" id="findings-list">
|
|
549
560
|
<div class="findings-empty">
|
|
@@ -641,6 +652,11 @@
|
|
|
641
652
|
window.PAIR_REVIEW_LOCAL_MODE = true;
|
|
642
653
|
</script>
|
|
643
654
|
|
|
655
|
+
<!-- Runtime configuration: sets window.PAIR_REVIEW_RUNTIME_CONFIG and
|
|
656
|
+
the `external-comments-disabled` class on documentElement before
|
|
657
|
+
pr.js / AIPanel initialise. Must load BEFORE pr.js. -->
|
|
658
|
+
<script src="/runtime-config.js"></script>
|
|
659
|
+
|
|
644
660
|
<!-- PR JavaScript (shared with local mode) -->
|
|
645
661
|
<script src="/js/pr.js"></script>
|
|
646
662
|
|
package/public/pr.html
CHANGED
|
@@ -321,11 +321,20 @@
|
|
|
321
321
|
|
|
322
322
|
<!-- Segment Control -->
|
|
323
323
|
<div class="segment-control" id="segment-control">
|
|
324
|
-
<
|
|
325
|
-
<
|
|
326
|
-
|
|
327
|
-
|
|
324
|
+
<button type="button" class="segment-scroll segment-scroll--left" id="segment-scroll-left" hidden aria-label="Scroll segments left" tabindex="-1">
|
|
325
|
+
<svg viewBox="0 0 16 16" width="12" height="12" fill="currentColor" aria-hidden="true"><path d="M9.78 12.78a.75.75 0 0 1-1.06 0L4.47 8.53a.75.75 0 0 1 0-1.06l4.25-4.25a.751.751 0 0 1 1.042.018.751.751 0 0 1 .018 1.042L6.06 8l3.72 3.72a.75.75 0 0 1 0 1.06Z"/></svg>
|
|
326
|
+
</button>
|
|
327
|
+
<div class="segment-control-scroll" id="segment-control-scroll">
|
|
328
|
+
<div class="segment-control-inner">
|
|
329
|
+
<button class="segment-btn active" data-segment="ai">AI <span class="segment-count">(0)</span></button>
|
|
330
|
+
<button class="segment-btn" data-segment="comments">User <span class="segment-count">(0)</span></button>
|
|
331
|
+
<button class="segment-btn" data-segment="external">External <span class="segment-count">(0)</span></button>
|
|
332
|
+
<button class="segment-btn" data-segment="all">All <span class="segment-count">(0)</span></button>
|
|
333
|
+
</div>
|
|
328
334
|
</div>
|
|
335
|
+
<button type="button" class="segment-scroll segment-scroll--right" id="segment-scroll-right" hidden aria-label="Scroll segments right" tabindex="-1">
|
|
336
|
+
<svg viewBox="0 0 16 16" width="12" height="12" fill="currentColor" aria-hidden="true"><path d="M6.22 3.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L9.94 8 6.22 4.28a.75.75 0 0 1 0-1.06Z"/></svg>
|
|
337
|
+
</button>
|
|
329
338
|
</div>
|
|
330
339
|
|
|
331
340
|
<!-- Level Filter -->
|
|
@@ -339,7 +348,16 @@
|
|
|
339
348
|
<!-- Findings Summary -->
|
|
340
349
|
<div class="findings-summary" id="findings-summary">
|
|
341
350
|
<div class="findings-header">
|
|
342
|
-
|
|
351
|
+
<!-- Nav controls (prev | n of N | next) are injected by AIPanel.updateFindingsHeader.
|
|
352
|
+
Kept as a static slot so the sibling actions container survives re-render. -->
|
|
353
|
+
<div class="findings-nav" id="findings-nav"></div>
|
|
354
|
+
<div class="findings-header-actions">
|
|
355
|
+
<button class="findings-nav-btn btn-refresh-external-comments external-comments-only" id="refresh-external-comments-btn-panel" title="Refresh GitHub comments" aria-label="Refresh GitHub comments">
|
|
356
|
+
<svg viewBox="0 0 16 16" fill="currentColor" width="12" height="12" aria-hidden="true">
|
|
357
|
+
<path d="M1.705 8.005a.75.75 0 0 1 .834.656 5.5 5.5 0 0 0 9.592 2.97l-1.204-1.204a.25.25 0 0 1 .177-.427h3.646a.25.25 0 0 1 .25.25v3.646a.25.25 0 0 1-.427.177l-1.38-1.38A7.002 7.002 0 0 1 1.05 8.84a.75.75 0 0 1 .656-.834ZM8 2.5a5.487 5.487 0 0 0-4.131 1.869l1.204 1.204A.25.25 0 0 1 4.896 6H1.25A.25.25 0 0 1 1 5.75V2.104a.25.25 0 0 1 .427-.177l1.38 1.38A7.002 7.002 0 0 1 14.95 7.16a.75.75 0 0 1-1.49.178A5.5 5.5 0 0 0 8 2.5Z"/>
|
|
358
|
+
</svg>
|
|
359
|
+
</button>
|
|
360
|
+
</div>
|
|
343
361
|
</div>
|
|
344
362
|
<div class="findings-list" id="findings-list">
|
|
345
363
|
<div class="findings-empty">
|
|
@@ -429,9 +447,17 @@
|
|
|
429
447
|
<!-- Chat Panel Component -->
|
|
430
448
|
<script src="/js/components/ChatPanel.js"></script>
|
|
431
449
|
|
|
450
|
+
<!-- External review-comment renderer (must load after chat-panel + comment-manager) -->
|
|
451
|
+
<script src="/js/modules/external-comment-manager.js"></script>
|
|
452
|
+
|
|
432
453
|
<!-- Panel Group Component -->
|
|
433
454
|
<script src="/js/components/PanelGroup.js"></script>
|
|
434
455
|
|
|
456
|
+
<!-- Runtime configuration: sets window.PAIR_REVIEW_RUNTIME_CONFIG and
|
|
457
|
+
the `external-comments-disabled` class on documentElement before
|
|
458
|
+
pr.js / AIPanel initialise. Must load BEFORE pr.js. -->
|
|
459
|
+
<script src="/runtime-config.js"></script>
|
|
460
|
+
|
|
435
461
|
<!-- PR JavaScript -->
|
|
436
462
|
<script src="/js/pr.js"></script>
|
|
437
463
|
</body>
|
|
@@ -17,19 +17,32 @@ const { StreamParser, parseClaudeLine } = require('./stream-parser');
|
|
|
17
17
|
const BIN_DIR = path.join(__dirname, '..', '..', 'bin');
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
|
-
* Claude model definitions with tier mappings
|
|
20
|
+
* Claude model definitions with tier mappings.
|
|
21
|
+
*
|
|
22
|
+
* Effort is set via the CLAUDE_CODE_EFFORT_LEVEL env var (highest-precedence way
|
|
23
|
+
* to control reasoning effort; takes precedence over the --effort CLI flag and is
|
|
24
|
+
* not deprecated). Extended thinking is forced on globally via `--thinking enabled`
|
|
25
|
+
* in the constructor's base args; individual models can override this via extra_args
|
|
26
|
+
* (e.g., Haiku uses adaptive thinking for efficiency).
|
|
27
|
+
*
|
|
28
|
+
* Effort support by model (newest CLIs): Opus 4.8 / 4.7 support low|medium|high|
|
|
29
|
+
* xhigh|max; Opus 4.6 & Sonnet 4.6 support low|medium|high|max (no xhigh); Haiku
|
|
30
|
+
* has no effort levels.
|
|
21
31
|
*/
|
|
22
32
|
const CLAUDE_MODELS = [
|
|
33
|
+
// ── Thorough tier ───────────────────────────────────────────────────────
|
|
23
34
|
{
|
|
24
|
-
id: 'opus
|
|
35
|
+
id: 'opus',
|
|
36
|
+
aliases: ['opus-4.7-xhigh'],
|
|
25
37
|
cli_model: 'claude-opus-4-7',
|
|
26
38
|
env: { CLAUDE_CODE_EFFORT_LEVEL: 'xhigh' },
|
|
27
39
|
name: 'Opus 4.7 XHigh',
|
|
28
40
|
tier: 'thorough',
|
|
29
|
-
tagline: '
|
|
30
|
-
description: 'Opus 4.7
|
|
31
|
-
badge: '
|
|
32
|
-
badgeClass: 'badge-power'
|
|
41
|
+
tagline: 'Maximum Depth',
|
|
42
|
+
description: 'Opus 4.7 with extra-high effort — deepest analysis',
|
|
43
|
+
badge: 'Most Thorough',
|
|
44
|
+
badgeClass: 'badge-power',
|
|
45
|
+
default: true
|
|
33
46
|
},
|
|
34
47
|
{
|
|
35
48
|
id: 'opus-4.7-high',
|
|
@@ -37,33 +50,46 @@ const CLAUDE_MODELS = [
|
|
|
37
50
|
env: { CLAUDE_CODE_EFFORT_LEVEL: 'high' },
|
|
38
51
|
name: 'Opus 4.7 High',
|
|
39
52
|
tier: 'thorough',
|
|
40
|
-
tagline: '
|
|
41
|
-
description: 'Opus 4.7
|
|
53
|
+
tagline: 'High Effort',
|
|
54
|
+
description: 'Opus 4.7 with high effort — thorough, quicker than XHigh',
|
|
55
|
+
badge: 'Thorough',
|
|
56
|
+
badgeClass: 'badge-power'
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'opus-4.8-xhigh',
|
|
60
|
+
cli_model: 'claude-opus-4-8',
|
|
61
|
+
env: { CLAUDE_CODE_EFFORT_LEVEL: 'xhigh' },
|
|
62
|
+
name: 'Opus 4.8 XHigh',
|
|
63
|
+
tier: 'thorough',
|
|
64
|
+
tagline: 'Newest',
|
|
65
|
+
description: 'Opus 4.8 (newest) with extra-high effort',
|
|
42
66
|
badge: 'Latest',
|
|
43
67
|
badgeClass: 'badge-power'
|
|
44
68
|
},
|
|
45
69
|
{
|
|
46
|
-
id: 'opus',
|
|
47
|
-
|
|
48
|
-
cli_model: 'claude-opus-4-6',
|
|
70
|
+
id: 'opus-4.8-high',
|
|
71
|
+
cli_model: 'claude-opus-4-8',
|
|
49
72
|
env: { CLAUDE_CODE_EFFORT_LEVEL: 'high' },
|
|
50
|
-
name: 'Opus 4.
|
|
73
|
+
name: 'Opus 4.8 High',
|
|
51
74
|
tier: 'thorough',
|
|
52
|
-
tagline: '
|
|
53
|
-
description: 'Opus 4.
|
|
54
|
-
badge: '
|
|
55
|
-
badgeClass: 'badge-power'
|
|
56
|
-
default: true
|
|
75
|
+
tagline: 'Newest',
|
|
76
|
+
description: 'Opus 4.8 (newest) with high effort',
|
|
77
|
+
badge: 'Latest',
|
|
78
|
+
badgeClass: 'badge-power'
|
|
57
79
|
},
|
|
58
80
|
{
|
|
59
|
-
id: '
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
81
|
+
id: 'opus-4.6-high',
|
|
82
|
+
aliases: ['opus-4.6-low', 'opus-4.6-medium', 'opus-4.5'],
|
|
83
|
+
cli_model: 'claude-opus-4-6',
|
|
84
|
+
env: { CLAUDE_CODE_EFFORT_LEVEL: 'high' },
|
|
85
|
+
name: 'Opus 4.6 High',
|
|
86
|
+
tier: 'thorough',
|
|
87
|
+
tagline: 'Previous Gen',
|
|
88
|
+
description: 'Opus 4.6 with high effort',
|
|
89
|
+
badge: 'Previous Gen',
|
|
90
|
+
badgeClass: 'badge-power'
|
|
66
91
|
},
|
|
92
|
+
// ── Balanced tier ───────────────────────────────────────────────────────
|
|
67
93
|
{
|
|
68
94
|
id: 'sonnet-4.6',
|
|
69
95
|
cli_model: 'claude-sonnet-4-6',
|
|
@@ -74,28 +100,6 @@ const CLAUDE_MODELS = [
|
|
|
74
100
|
badge: 'Standard',
|
|
75
101
|
badgeClass: 'badge-recommended'
|
|
76
102
|
},
|
|
77
|
-
{
|
|
78
|
-
id: 'opus-4.6-low',
|
|
79
|
-
cli_model: 'claude-opus-4-6',
|
|
80
|
-
env: { CLAUDE_CODE_EFFORT_LEVEL: 'low' },
|
|
81
|
-
name: 'Opus 4.6 Low',
|
|
82
|
-
tier: 'balanced',
|
|
83
|
-
tagline: 'Fast Opus',
|
|
84
|
-
description: 'Opus 4.6 with low effort — quick and capable',
|
|
85
|
-
badge: 'Balanced',
|
|
86
|
-
badgeClass: 'badge-recommended'
|
|
87
|
-
},
|
|
88
|
-
{
|
|
89
|
-
id: 'opus-4.6-medium',
|
|
90
|
-
cli_model: 'claude-opus-4-6',
|
|
91
|
-
env: { CLAUDE_CODE_EFFORT_LEVEL: 'medium' },
|
|
92
|
-
name: 'Opus 4.6 Medium',
|
|
93
|
-
tier: 'balanced',
|
|
94
|
-
tagline: 'Balanced Opus',
|
|
95
|
-
description: 'Opus 4.6 with medium effort — balanced depth',
|
|
96
|
-
badge: 'Thorough',
|
|
97
|
-
badgeClass: 'badge-power'
|
|
98
|
-
},
|
|
99
103
|
{
|
|
100
104
|
id: 'opus-4.6-1m',
|
|
101
105
|
cli_model: 'claude-opus-4-6[1m]',
|
|
@@ -106,15 +110,17 @@ const CLAUDE_MODELS = [
|
|
|
106
110
|
badge: 'More Context',
|
|
107
111
|
badgeClass: 'badge-power'
|
|
108
112
|
},
|
|
113
|
+
// ── Fast tier ───────────────────────────────────────────────────────────
|
|
109
114
|
{
|
|
110
|
-
id: '
|
|
111
|
-
cli_model: 'claude-
|
|
112
|
-
name: '
|
|
113
|
-
tier: '
|
|
114
|
-
tagline: '
|
|
115
|
-
description: '
|
|
116
|
-
badge: '
|
|
117
|
-
badgeClass: 'badge-
|
|
115
|
+
id: 'haiku',
|
|
116
|
+
cli_model: 'claude-haiku-4-5-20251001',
|
|
117
|
+
name: 'Haiku 4.5',
|
|
118
|
+
tier: 'fast',
|
|
119
|
+
tagline: 'Lightning Fast',
|
|
120
|
+
description: 'Quick analysis for simple changes',
|
|
121
|
+
badge: 'Fastest',
|
|
122
|
+
badgeClass: 'badge-speed',
|
|
123
|
+
extra_args: ['--thinking', 'adaptive']
|
|
118
124
|
}
|
|
119
125
|
];
|
|
120
126
|
|
|
@@ -196,7 +202,12 @@ class ClaudeProvider extends AIProvider {
|
|
|
196
202
|
// user's configured environment. To disable skills, add --disable-slash-commands
|
|
197
203
|
// to extra_args in provider/model config.
|
|
198
204
|
const hooksArgs = ['--settings', '{"disableAllHooks":true}'];
|
|
199
|
-
|
|
205
|
+
// Force extended thinking on for every analysis call. The Claude CLI's
|
|
206
|
+
// `--thinking` flag accepts enabled|adaptive|disabled; we always want
|
|
207
|
+
// reasoning engaged for code review. User config extra_args appended later
|
|
208
|
+
// win over this (commander uses the last occurrence) if an override is set.
|
|
209
|
+
const thinkingArgs = ['--thinking', 'enabled'];
|
|
210
|
+
const baseArgs = ['-p', '--verbose', ...cliModelArgs, '--output-format', 'stream-json', ...thinkingArgs, ...hooksArgs, ...permissionArgs];
|
|
200
211
|
if (maxBudget) {
|
|
201
212
|
const budgetNum = parseFloat(maxBudget);
|
|
202
213
|
if (isNaN(budgetNum) || budgetNum <= 0) {
|
|
@@ -242,7 +253,8 @@ class ClaudeProvider extends AIProvider {
|
|
|
242
253
|
// - string: use this exact value for --model
|
|
243
254
|
// - null: explicitly suppress --model (for tools that want the model set via env instead)
|
|
244
255
|
const builtIn = CLAUDE_MODELS.find(m => m.id === modelId || (m.aliases && m.aliases.includes(modelId)));
|
|
245
|
-
const
|
|
256
|
+
const modelKeys = new Set([modelId, builtIn?.id, ...(builtIn?.aliases || [])].filter(Boolean));
|
|
257
|
+
const configModel = configOverrides.models?.find(m => modelKeys.has(m.id));
|
|
246
258
|
const resolvedCliModel = configModel?.cli_model !== undefined
|
|
247
259
|
? configModel.cli_model
|
|
248
260
|
: (builtIn?.cli_model !== undefined ? builtIn.cli_model : modelId);
|