@in-the-loop-labs/pair-review 3.6.0 → 3.7.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 +4 -0
- package/package.json +20 -15
- 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/analysis-config.css +1807 -0
- package/public/css/pr.css +17 -1737
- package/public/index.html +11 -0
- package/public/js/components/AIPanel.js +89 -44
- package/public/js/components/AdvancedConfigTab.js +56 -4
- package/public/js/components/AnalysisConfigModal.js +41 -25
- package/public/js/components/ChatPanel.js +11 -1
- package/public/js/components/ReviewModal.js +135 -13
- package/public/js/components/SuggestionNavigator.js +55 -10
- package/public/js/components/VoiceCentricConfigTab.js +36 -0
- package/public/js/index.js +175 -16
- package/public/js/local.js +58 -8
- package/public/js/modules/suggestion-manager.js +25 -1
- package/public/js/modules/tour-renderer.js +45 -5
- package/public/js/pr.js +703 -171
- package/public/js/repo-links.js +328 -0
- package/public/js/utils/provider-model.js +88 -0
- package/public/js/utils/scroll-into-view.js +164 -0
- package/public/js/utils/storage-keys.js +50 -0
- package/public/local.html +10 -0
- package/public/pr.html +10 -0
- package/public/repo-settings.html +1 -0
- package/public/setup.html +2 -0
- package/src/ai/analyzer.js +125 -18
- package/src/ai/claude-provider.js +31 -3
- package/src/config.js +664 -10
- package/src/external/github-adapter.js +114 -25
- package/src/git/base-branch.js +11 -4
- package/src/github/client.js +482 -588
- package/src/github/errors.js +55 -0
- package/src/github/impl/graphql/pending-review-comments.js +230 -0
- package/src/github/impl/graphql/pending-review.js +153 -0
- package/src/github/impl/graphql/review-lifecycle.js +161 -0
- package/src/github/impl/graphql/stack-walker.js +210 -0
- package/src/github/impl/host/pending-review-comments.js +338 -0
- package/src/github/impl/rest/pending-review.js +251 -0
- package/src/github/impl/rest/review-lifecycle.js +226 -0
- package/src/github/impl/rest/stack-walker.js +309 -0
- package/src/github/operations/pending-review-comments.js +79 -0
- package/src/github/operations/pending-review.js +89 -0
- package/src/github/operations/review-lifecycle.js +126 -0
- package/src/github/operations/stack-walker.js +87 -0
- package/src/github/parser.js +230 -4
- package/src/github/stack-walker.js +14 -189
- package/src/links/repo-links.js +230 -0
- package/src/local-review.js +13 -4
- package/src/main.js +136 -32
- package/src/routes/analyses.js +30 -7
- package/src/routes/bulk-analysis-configs.js +295 -0
- package/src/routes/config.js +102 -2
- package/src/routes/external-comments.js +20 -10
- package/src/routes/github-collections.js +3 -1
- package/src/routes/local.js +101 -11
- package/src/routes/mcp.js +47 -4
- package/src/routes/pr.js +298 -68
- package/src/routes/setup.js +8 -3
- package/src/routes/stack-analysis.js +33 -9
- package/src/routes/worktrees.js +3 -2
- package/src/server.js +2 -0
- package/src/setup/pr-setup.js +37 -11
- package/src/setup/stack-setup.js +13 -3
- package/src/single-port.js +6 -3
|
@@ -64,9 +64,9 @@ class ReviewModal {
|
|
|
64
64
|
</div>
|
|
65
65
|
<div class="pending-draft-notice-content">
|
|
66
66
|
<span class="pending-draft-notice-text">
|
|
67
|
-
You have a pending draft review on GitHub with <strong id="pending-draft-count">0</strong> comments.
|
|
67
|
+
You have a pending draft review on <span class="rm-host-name">GitHub</span> with <strong id="pending-draft-count">0</strong> comments.
|
|
68
68
|
Submitting here will add to or complete this review.
|
|
69
|
-
<a href="#" id="pending-draft-link" target="_blank" rel="noopener noreferrer">Manage on GitHub</a>.
|
|
69
|
+
<a href="#" id="pending-draft-link" target="_blank" rel="noopener noreferrer">Manage on <span class="rm-host-name">GitHub</span></a>.
|
|
70
70
|
</span>
|
|
71
71
|
</div>
|
|
72
72
|
</div>
|
|
@@ -120,7 +120,7 @@ class ReviewModal {
|
|
|
120
120
|
<input type="radio" name="review-event" value="DRAFT">
|
|
121
121
|
<div class="review-type-content">
|
|
122
122
|
<span class="review-type-label">Save as Draft</span>
|
|
123
|
-
<span class="review-type-desc">Save your review as a draft on GitHub to finish later.</span>
|
|
123
|
+
<span class="review-type-desc">Save your review as a draft on <span class="rm-host-name">GitHub</span> to finish later.</span>
|
|
124
124
|
</div>
|
|
125
125
|
</label>
|
|
126
126
|
</div>
|
|
@@ -284,6 +284,11 @@ class ReviewModal {
|
|
|
284
284
|
// Update AI summary link visibility
|
|
285
285
|
this.updateAISummaryLink();
|
|
286
286
|
|
|
287
|
+
// Apply the configured remote-host display name + icon (resolves
|
|
288
|
+
// asynchronously after the modal HTML was built).
|
|
289
|
+
this.applyHostName();
|
|
290
|
+
this.applySubmitButtonIcon();
|
|
291
|
+
|
|
287
292
|
// Update pending draft notice
|
|
288
293
|
this.updatePendingDraftNotice();
|
|
289
294
|
}
|
|
@@ -311,11 +316,17 @@ class ReviewModal {
|
|
|
311
316
|
countElement.textContent = String(pendingDraft.comments_count || 0);
|
|
312
317
|
}
|
|
313
318
|
|
|
314
|
-
// Update the link
|
|
319
|
+
// Update the link. Prefer the URL built from the repo's configured
|
|
320
|
+
// url_template (host-correct) over the server-reported github_url,
|
|
321
|
+
// which some alt-hosts return as a wrong-host github.com/issues URL.
|
|
315
322
|
const linkElement = notice.querySelector('#pending-draft-link');
|
|
316
323
|
if (linkElement) {
|
|
317
|
-
|
|
318
|
-
|
|
324
|
+
const templatedUrl = (typeof window !== 'undefined' && window.RepoLinks
|
|
325
|
+
&& typeof window.RepoLinks.externalUrl === 'function')
|
|
326
|
+
? window.RepoLinks.externalUrl() : null;
|
|
327
|
+
const manageUrl = templatedUrl || pendingDraft.github_url;
|
|
328
|
+
if (manageUrl) {
|
|
329
|
+
linkElement.href = manageUrl;
|
|
319
330
|
linkElement.style.display = 'inline';
|
|
320
331
|
} else {
|
|
321
332
|
linkElement.style.display = 'none';
|
|
@@ -452,6 +463,8 @@ class ReviewModal {
|
|
|
452
463
|
submitBtn.disabled = false;
|
|
453
464
|
cancelBtn.style.display = 'inline-block';
|
|
454
465
|
closeBtn.style.display = 'inline-block';
|
|
466
|
+
// innerHTML reset drops any host icon — re-apply it.
|
|
467
|
+
this.applySubmitButtonIcon();
|
|
455
468
|
}
|
|
456
469
|
}
|
|
457
470
|
|
|
@@ -531,7 +544,7 @@ class ReviewModal {
|
|
|
531
544
|
const reviewUrl = result.reviewUrl || result.github_url;
|
|
532
545
|
if (isDraft) {
|
|
533
546
|
window.toast.showSuccess(
|
|
534
|
-
|
|
547
|
+
`Draft review submitted to ${ReviewModal.escapeHtml(ReviewModal.hostName())} successfully!`,
|
|
535
548
|
{
|
|
536
549
|
duration: 5000
|
|
537
550
|
}
|
|
@@ -541,7 +554,7 @@ class ReviewModal {
|
|
|
541
554
|
'Review submitted successfully!',
|
|
542
555
|
{
|
|
543
556
|
link: reviewUrl,
|
|
544
|
-
linkText:
|
|
557
|
+
linkText: `View on ${ReviewModal.escapeHtml(ReviewModal.hostName())}`,
|
|
545
558
|
duration: 5000
|
|
546
559
|
}
|
|
547
560
|
);
|
|
@@ -586,11 +599,16 @@ class ReviewModal {
|
|
|
586
599
|
}
|
|
587
600
|
|
|
588
601
|
if (isDraft) {
|
|
589
|
-
// After 2 seconds, open
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
602
|
+
// After 2 seconds, open the PR page for drafts. Use the PR's canonical
|
|
603
|
+
// html_url (correct host + `/pull/`) rather than the review's html_url,
|
|
604
|
+
// which some alt-hosts return as a github.com `/issues/<n>` URL. Never
|
|
605
|
+
// assume github.com — see resolveDraftPrUrl.
|
|
606
|
+
const prUrl = ReviewModal.resolveDraftPrUrl(pr, result);
|
|
607
|
+
if (prUrl) {
|
|
608
|
+
setTimeout(() => {
|
|
609
|
+
window.open(prUrl, '_blank');
|
|
610
|
+
}, 2000);
|
|
611
|
+
}
|
|
594
612
|
}
|
|
595
613
|
|
|
596
614
|
} catch (error) {
|
|
@@ -676,6 +694,110 @@ class ReviewModal {
|
|
|
676
694
|
localStorage.setItem(ASSISTED_BY_STORAGE_KEY, String(checkbox.checked));
|
|
677
695
|
}
|
|
678
696
|
|
|
697
|
+
/**
|
|
698
|
+
* Display name of the remote code host, for user-facing text in place of
|
|
699
|
+
* the literal "GitHub". Reads the configured `links.external.name` via
|
|
700
|
+
* `window.RepoLinks.hostName()`, falling back to "GitHub".
|
|
701
|
+
*
|
|
702
|
+
* @returns {string}
|
|
703
|
+
*/
|
|
704
|
+
static hostName() {
|
|
705
|
+
if (typeof window !== 'undefined' && window.RepoLinks
|
|
706
|
+
&& typeof window.RepoLinks.hostName === 'function') {
|
|
707
|
+
return window.RepoLinks.hostName();
|
|
708
|
+
}
|
|
709
|
+
return 'GitHub';
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Escape a string for safe interpolation into HTML. Used for the host
|
|
714
|
+
* name (user-supplied config) before it goes into the success toast,
|
|
715
|
+
* which renders its message/linkText via innerHTML.
|
|
716
|
+
*
|
|
717
|
+
* @param {string} text
|
|
718
|
+
* @returns {string}
|
|
719
|
+
*/
|
|
720
|
+
static escapeHtml(text) {
|
|
721
|
+
return String(text == null ? '' : text)
|
|
722
|
+
.replace(/&/g, '&')
|
|
723
|
+
.replace(/</g, '<')
|
|
724
|
+
.replace(/>/g, '>')
|
|
725
|
+
.replace(/"/g, '"')
|
|
726
|
+
.replace(/'/g, ''');
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Resolve the URL to open in a new tab after a draft submit.
|
|
731
|
+
*
|
|
732
|
+
* Precedence:
|
|
733
|
+
* 1. The URL built from the repo's configured `links.external.url_template`
|
|
734
|
+
* (`window.RepoLinks.externalUrl()`) — authoritative and host-correct.
|
|
735
|
+
* 2. The PR's canonical `html_url` (the host's own PR page).
|
|
736
|
+
* 3. The server-reported `github_url` as a last resort.
|
|
737
|
+
*
|
|
738
|
+
* Some alt-hosts return the pending-review `html_url` as a
|
|
739
|
+
* `github.com/.../issues/<n>` URL, which lands on the wrong host and page.
|
|
740
|
+
* We must never assume github.com, so there is no hardcoded fallback host:
|
|
741
|
+
* if none of the above yields a URL we open nothing.
|
|
742
|
+
*
|
|
743
|
+
* @param {{html_url?: string}|null|undefined} pr - current PR (from prManager)
|
|
744
|
+
* @param {{github_url?: string}|null|undefined} result - submit-review response
|
|
745
|
+
* @returns {string|null} URL to open, or null if none is available
|
|
746
|
+
*/
|
|
747
|
+
static resolveDraftPrUrl(pr, result) {
|
|
748
|
+
if (typeof window !== 'undefined' && window.RepoLinks
|
|
749
|
+
&& typeof window.RepoLinks.externalUrl === 'function') {
|
|
750
|
+
const templated = window.RepoLinks.externalUrl();
|
|
751
|
+
if (templated) return templated;
|
|
752
|
+
}
|
|
753
|
+
if (pr && pr.html_url) return pr.html_url;
|
|
754
|
+
if (result && result.github_url) return result.github_url;
|
|
755
|
+
return null;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Update host-name-dependent static text in the modal (the pending-draft
|
|
760
|
+
* notice and the "Save as Draft" description) to the configured host name.
|
|
761
|
+
* Called from `show()` because the name resolves asynchronously after the
|
|
762
|
+
* modal HTML is built. No-op when the modal isn't present.
|
|
763
|
+
*/
|
|
764
|
+
applyHostName() {
|
|
765
|
+
if (!this.modal) return;
|
|
766
|
+
const name = ReviewModal.hostName();
|
|
767
|
+
const spans = this.modal.querySelectorAll('.rm-host-name');
|
|
768
|
+
spans.forEach((el) => { el.textContent = name; });
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Prepend the configured external-host icon to the submit button, when an
|
|
773
|
+
* icon is configured for the repo. The icon is parsed via
|
|
774
|
+
* `window.RepoLinks.parseSvgIcon` (DOMParser + attribute stripping) and
|
|
775
|
+
* inserted as a DOM node — never via innerHTML. Idempotent: any previously
|
|
776
|
+
* inserted icon is removed first. No-op for plain github.com repos.
|
|
777
|
+
*/
|
|
778
|
+
applySubmitButtonIcon() {
|
|
779
|
+
const submitBtn = this.modal?.querySelector('#submit-review-btn-modal');
|
|
780
|
+
if (!submitBtn) return;
|
|
781
|
+
|
|
782
|
+
const existing = submitBtn.querySelector?.('.submit-host-icon');
|
|
783
|
+
if (existing) existing.remove();
|
|
784
|
+
|
|
785
|
+
if (typeof window === 'undefined' || !window.RepoLinks
|
|
786
|
+
|| typeof window.RepoLinks.externalIcon !== 'function'
|
|
787
|
+
|| typeof window.RepoLinks.parseSvgIcon !== 'function') {
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
const iconStr = window.RepoLinks.externalIcon();
|
|
791
|
+
if (!iconStr) return;
|
|
792
|
+
const svg = window.RepoLinks.parseSvgIcon(iconStr);
|
|
793
|
+
if (!svg) return;
|
|
794
|
+
|
|
795
|
+
svg.classList.add('submit-host-icon');
|
|
796
|
+
if (!svg.getAttribute('width')) svg.setAttribute('width', '16');
|
|
797
|
+
if (!svg.getAttribute('height')) svg.setAttribute('height', '16');
|
|
798
|
+
submitBtn.insertBefore(svg, submitBtn.firstChild);
|
|
799
|
+
}
|
|
800
|
+
|
|
679
801
|
}
|
|
680
802
|
|
|
681
803
|
// Initialize when DOM is ready if not already initialized
|
|
@@ -10,7 +10,10 @@ class SuggestionNavigator {
|
|
|
10
10
|
this.isCollapsed = this.loadCollapsedState();
|
|
11
11
|
this.element = null;
|
|
12
12
|
this.collapseToggle = null;
|
|
13
|
-
|
|
13
|
+
// Monotonic token so a fast Next/Prev that supersedes an in-flight
|
|
14
|
+
// goToSuggestion can tell the older call to bail after its await.
|
|
15
|
+
this._navGen = 0;
|
|
16
|
+
|
|
14
17
|
this.init();
|
|
15
18
|
this.bindEvents();
|
|
16
19
|
}
|
|
@@ -236,7 +239,7 @@ class SuggestionNavigator {
|
|
|
236
239
|
/**
|
|
237
240
|
* Navigate to specific suggestion by index
|
|
238
241
|
*/
|
|
239
|
-
goToSuggestion(index) {
|
|
242
|
+
async goToSuggestion(index) {
|
|
240
243
|
if (index < 0 || index >= this.suggestions.length) {
|
|
241
244
|
return;
|
|
242
245
|
}
|
|
@@ -244,10 +247,42 @@ class SuggestionNavigator {
|
|
|
244
247
|
this.currentSuggestionIndex = index;
|
|
245
248
|
this.updateCounter();
|
|
246
249
|
this.updateNavigationButtons();
|
|
250
|
+
// The suggestion's row only exists once its file body has rendered
|
|
251
|
+
// (lazy bodies start empty), so render it before the highlight/scroll
|
|
252
|
+
// lookups below — otherwise both silently miss on the first attempt.
|
|
253
|
+
// A collapsed file is expanded first so the row is actually visible.
|
|
254
|
+
const myGen = ++this._navGen;
|
|
255
|
+
await this.ensureSuggestionVisible(this.suggestions[index]);
|
|
256
|
+
// A newer goToSuggestion ran while we awaited and moved
|
|
257
|
+
// this.currentSuggestionIndex — let it own the highlight/scroll.
|
|
258
|
+
if (myGen !== this._navGen) return;
|
|
247
259
|
this.highlightCurrentSuggestion();
|
|
248
260
|
this.scrollToSuggestion();
|
|
249
261
|
}
|
|
250
262
|
|
|
263
|
+
/**
|
|
264
|
+
* Make sure a suggestion's file is expanded and its lazy diff body is
|
|
265
|
+
* rendered so the suggestion row exists in the DOM. Best effort: any
|
|
266
|
+
* failure falls through to the old lookup-miss behavior.
|
|
267
|
+
* @param {Object} suggestion
|
|
268
|
+
*/
|
|
269
|
+
async ensureSuggestionVisible(suggestion) {
|
|
270
|
+
const file = suggestion?.file;
|
|
271
|
+
const pm = window.prManager;
|
|
272
|
+
if (!file || !pm) return;
|
|
273
|
+
try {
|
|
274
|
+
const wrapper = pm.findFileElement?.(file);
|
|
275
|
+
if (wrapper?.classList.contains('collapsed') && pm.toggleFileCollapse) {
|
|
276
|
+
// Renders the lazy body and removes `collapsed`.
|
|
277
|
+
await pm.toggleFileCollapse(wrapper.dataset.fileName || file);
|
|
278
|
+
} else if (pm.ensureFileBodyRendered) {
|
|
279
|
+
await pm.ensureFileBodyRendered(file);
|
|
280
|
+
}
|
|
281
|
+
} catch (err) {
|
|
282
|
+
console.warn('[SuggestionNavigator] could not prepare suggestion file', file, err);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
251
286
|
/**
|
|
252
287
|
* Check if a suggestion should be skipped during navigation
|
|
253
288
|
*/
|
|
@@ -370,18 +405,22 @@ class SuggestionNavigator {
|
|
|
370
405
|
|
|
371
406
|
if (suggestionEl) {
|
|
372
407
|
const minimizer = window.prManager?.commentMinimizer;
|
|
408
|
+
let scrollTarget = suggestionEl;
|
|
373
409
|
if (minimizer?.active) {
|
|
374
410
|
// Expand file-level comments so the target becomes visible
|
|
375
411
|
minimizer.expandForElement(suggestionEl);
|
|
376
412
|
// Comments are minimized — scroll to the parent diff line instead
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
413
|
+
scrollTarget = minimizer.findDiffRowFor(suggestionEl) || suggestionEl;
|
|
414
|
+
}
|
|
415
|
+
// Land the target at the top of the diff panel (scroll-margin-top in
|
|
416
|
+
// pr.css offsets it below the sticky toolbar + file header).
|
|
417
|
+
const options = { behavior: 'smooth', block: 'start', inline: 'nearest' };
|
|
418
|
+
// Stable variant re-corrects after lazy file bodies render
|
|
419
|
+
// mid-scroll and shift the layout. Fire-and-forget.
|
|
420
|
+
if (window.ScrollUtils?.scrollIntoViewStable) {
|
|
421
|
+
window.ScrollUtils.scrollIntoViewStable(scrollTarget, options);
|
|
383
422
|
} else {
|
|
384
|
-
|
|
423
|
+
scrollTarget.scrollIntoView(options);
|
|
385
424
|
}
|
|
386
425
|
}
|
|
387
426
|
}
|
|
@@ -474,4 +513,10 @@ class SuggestionNavigator {
|
|
|
474
513
|
}
|
|
475
514
|
|
|
476
515
|
// Export for use
|
|
477
|
-
window
|
|
516
|
+
if (typeof window !== 'undefined') {
|
|
517
|
+
window.SuggestionNavigator = SuggestionNavigator;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
521
|
+
module.exports = SuggestionNavigator;
|
|
522
|
+
}
|
|
@@ -235,6 +235,37 @@ class VoiceCentricConfigTab {
|
|
|
235
235
|
*/
|
|
236
236
|
setDefaultCouncilId(councilId) {
|
|
237
237
|
this._pendingDefaultCouncilId = councilId;
|
|
238
|
+
// On a cached reopen the councils are already loaded, so loadCouncils() —
|
|
239
|
+
// and the _renderCouncilSelector() call that applies the pending default —
|
|
240
|
+
// will not run again (the modal instance is reused; see AnalysisConfigModal
|
|
241
|
+
// caching on window.analysisConfigModal). Apply it now so the saved/default
|
|
242
|
+
// council is restored instead of being silently dropped onto a blank
|
|
243
|
+
// "+ New Council" selection.
|
|
244
|
+
if (this._councilsLoaded && this._injected) {
|
|
245
|
+
this._renderCouncilSelector();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Reset selection and editor state for a fresh modal open.
|
|
251
|
+
*
|
|
252
|
+
* The AnalysisConfigModal (and therefore this tab) is reused across runs — and
|
|
253
|
+
* in the index/bulk flow, across different repositories. Without this reset a
|
|
254
|
+
* council selected in a previous run (or its pending default / dirty edits)
|
|
255
|
+
* would carry over and could be displayed or submitted for the next batch.
|
|
256
|
+
*/
|
|
257
|
+
reset() {
|
|
258
|
+
this.selectedCouncilId = null;
|
|
259
|
+
this._pendingDefaultCouncilId = null;
|
|
260
|
+
this._isDirty = false;
|
|
261
|
+
if (!this._injected) return;
|
|
262
|
+
const selector = this.modal.querySelector('#vc-council-selector');
|
|
263
|
+
if (selector) {
|
|
264
|
+
selector.value = '';
|
|
265
|
+
selector.classList.add('new-council-selected');
|
|
266
|
+
}
|
|
267
|
+
this._applyConfigToUI(this._defaultConfig());
|
|
268
|
+
this._markClean();
|
|
238
269
|
}
|
|
239
270
|
|
|
240
271
|
/**
|
|
@@ -1494,3 +1525,8 @@ class VoiceCentricConfigTab {
|
|
|
1494
1525
|
if (typeof window !== 'undefined') {
|
|
1495
1526
|
window.VoiceCentricConfigTab = VoiceCentricConfigTab;
|
|
1496
1527
|
}
|
|
1528
|
+
|
|
1529
|
+
// Export for unit testing (Node/CommonJS environment)
|
|
1530
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
1531
|
+
module.exports = { VoiceCentricConfigTab };
|
|
1532
|
+
}
|
package/public/js/index.js
CHANGED
|
@@ -90,6 +90,8 @@
|
|
|
90
90
|
|
|
91
91
|
/** localStorage key for persisting the active tab */
|
|
92
92
|
const TAB_STORAGE_KEY = 'pair-review-active-tab';
|
|
93
|
+
const BULK_ANALYSIS_TAB_STORAGE_KEY = 'pair-review-bulk-analysis-tab';
|
|
94
|
+
const BULK_ANALYSIS_INSTRUCTIONS_STORAGE_KEY = 'pair-review-bulk-analysis-instructions';
|
|
93
95
|
|
|
94
96
|
/**
|
|
95
97
|
* Format a relative time string from a date
|
|
@@ -133,6 +135,10 @@
|
|
|
133
135
|
return div.innerHTML;
|
|
134
136
|
}
|
|
135
137
|
|
|
138
|
+
// encodeBase64Utf8 / getRepoStorageKey live in public/js/utils/storage-keys.js
|
|
139
|
+
// (window.encodeBase64Utf8 / window.getRepoStorageKey), shared with pr.js so the
|
|
140
|
+
// per-repo keys this page writes stay byte-identical to those the PR page reads.
|
|
141
|
+
|
|
136
142
|
const LOCAL_REVIEW_PATH_URL_ERROR = 'Local reviews require a filesystem path, not a URL. Pass GitHub or Graphite URLs as PR review inputs instead.';
|
|
137
143
|
|
|
138
144
|
function isUrlLikeLocalReviewPath(value) {
|
|
@@ -1377,6 +1383,9 @@
|
|
|
1377
1383
|
window.__pairReview.chatProvider = config.chat_provider || 'pi';
|
|
1378
1384
|
const chatProviders = config.chat_providers || [];
|
|
1379
1385
|
window.__pairReview.chatProviders = chatProviders;
|
|
1386
|
+
window.__pairReview.defaultProvider = config.default_provider || 'claude';
|
|
1387
|
+
window.__pairReview.defaultModel = config.default_model || 'opus';
|
|
1388
|
+
window.__pairReview.hasGithubToken = Boolean(config.has_github_token);
|
|
1380
1389
|
window.__pairReview.enableGraphite = config.enable_graphite === true;
|
|
1381
1390
|
window.__pairReview.chatSpinner = config.chat_spinner || 'dots';
|
|
1382
1391
|
window.__pairReview.chatEnterToSend = config.chat_enter_to_send !== false;
|
|
@@ -1404,6 +1413,7 @@
|
|
|
1404
1413
|
|
|
1405
1414
|
/** Currently active SelectionMode instance (only one tab at a time) */
|
|
1406
1415
|
var activeSelection = null;
|
|
1416
|
+
var bulkAnalysisConfigModal = null;
|
|
1407
1417
|
|
|
1408
1418
|
/**
|
|
1409
1419
|
* SelectionMode manages checkbox-based selection for a single tab's table.
|
|
@@ -1877,6 +1887,40 @@
|
|
|
1877
1887
|
});
|
|
1878
1888
|
}
|
|
1879
1889
|
|
|
1890
|
+
/**
|
|
1891
|
+
* Read selected collection rows as PR descriptors.
|
|
1892
|
+
* @param {Set} selectedIds - PR URLs (data-pr-url values)
|
|
1893
|
+
* @param {string} tbodyId - tbody element ID
|
|
1894
|
+
* @returns {Array<{owner: string, repo: string, number: string, prUrl: string}>}
|
|
1895
|
+
*/
|
|
1896
|
+
function getSelectedCollectionRows(selectedIds, tbodyId) {
|
|
1897
|
+
var tbody = document.getElementById(tbodyId);
|
|
1898
|
+
if (!tbody) return [];
|
|
1899
|
+
|
|
1900
|
+
var rows = [];
|
|
1901
|
+
var trs = tbody.querySelectorAll('tr[data-pr-url]');
|
|
1902
|
+
for (var i = 0; i < trs.length; i++) {
|
|
1903
|
+
var tr = trs[i];
|
|
1904
|
+
var prUrl = tr.dataset.prUrl;
|
|
1905
|
+
if (!selectedIds.has(prUrl)) continue;
|
|
1906
|
+
if (tr.dataset.owner && tr.dataset.repo && tr.dataset.number) {
|
|
1907
|
+
rows.push({
|
|
1908
|
+
owner: tr.dataset.owner,
|
|
1909
|
+
repo: tr.dataset.repo,
|
|
1910
|
+
number: tr.dataset.number,
|
|
1911
|
+
prUrl: prUrl
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
return rows;
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
function buildReviewUrlsFromRows(rows, query) {
|
|
1919
|
+
return rows.map(function (row) {
|
|
1920
|
+
return '/pr/' + encodeURIComponent(row.owner) + '/' + encodeURIComponent(row.repo) + '/' + row.number + (query || '');
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1880
1924
|
/**
|
|
1881
1925
|
* Build pair-review URLs from selected collection rows.
|
|
1882
1926
|
* @param {Set} selectedIds - PR URLs (data-pr-url values)
|
|
@@ -1885,20 +1929,116 @@
|
|
|
1885
1929
|
* @returns {string[]} array of pair-review URLs
|
|
1886
1930
|
*/
|
|
1887
1931
|
function buildReviewUrls(selectedIds, tbodyId, query) {
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
urls.push('/pr/' + encodeURIComponent(owner) + '/' + encodeURIComponent(repo) + '/' + number + (query || ''));
|
|
1932
|
+
return buildReviewUrlsFromRows(getSelectedCollectionRows(selectedIds, tbodyId), query);
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
function getCommonRepository(rows) {
|
|
1936
|
+
if (rows.length === 0) return null;
|
|
1937
|
+
var first = rows[0];
|
|
1938
|
+
var firstKey = (first.owner + '/' + first.repo).toLowerCase();
|
|
1939
|
+
for (var i = 1; i < rows.length; i++) {
|
|
1940
|
+
if ((rows[i].owner + '/' + rows[i].repo).toLowerCase() !== firstKey) {
|
|
1941
|
+
return null;
|
|
1899
1942
|
}
|
|
1943
|
+
}
|
|
1944
|
+
return { owner: first.owner, repo: first.repo };
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
async function fetchBulkRepoSettings(commonRepo) {
|
|
1948
|
+
if (!commonRepo) return null;
|
|
1949
|
+
try {
|
|
1950
|
+
var response = await fetch('/api/repos/' + encodeURIComponent(commonRepo.owner) + '/' + encodeURIComponent(commonRepo.repo) + '/settings');
|
|
1951
|
+
if (!response.ok) return null;
|
|
1952
|
+
return await response.json();
|
|
1953
|
+
} catch (error) {
|
|
1954
|
+
console.warn('Failed to fetch repo settings for bulk analysis:', error);
|
|
1955
|
+
return null;
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
function getBulkAnalysisStorageKeys(commonRepo) {
|
|
1960
|
+
if (!commonRepo) {
|
|
1961
|
+
return {
|
|
1962
|
+
tab: BULK_ANALYSIS_TAB_STORAGE_KEY,
|
|
1963
|
+
instructions: BULK_ANALYSIS_INSTRUCTIONS_STORAGE_KEY
|
|
1964
|
+
};
|
|
1965
|
+
}
|
|
1966
|
+
return {
|
|
1967
|
+
tab: window.getRepoStorageKey('pair-review-tab', commonRepo.owner, commonRepo.repo),
|
|
1968
|
+
instructions: window.getRepoStorageKey('pair-review-instructions', commonRepo.owner, commonRepo.repo)
|
|
1969
|
+
};
|
|
1970
|
+
}
|
|
1971
|
+
|
|
1972
|
+
// Cache the /api/providers metadata so the bulk modal can resolve a coherent
|
|
1973
|
+
// provider/model pair before showing (mirrors PRManager._getProvidersInfo).
|
|
1974
|
+
function getBulkProvidersInfo() {
|
|
1975
|
+
window.__pairReview = window.__pairReview || {};
|
|
1976
|
+
if (!window.__pairReview._providersInfoPromise) {
|
|
1977
|
+
window.__pairReview._providersInfoPromise = fetch('/api/providers')
|
|
1978
|
+
.then(function (r) { return r.ok ? r.json() : {}; })
|
|
1979
|
+
.then(function (d) { return Array.isArray(d.providers) ? d.providers : []; })
|
|
1980
|
+
.catch(function () { return []; });
|
|
1981
|
+
}
|
|
1982
|
+
return window.__pairReview._providersInfoPromise;
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
async function showBulkAnalysisConfig(rows) {
|
|
1986
|
+
if (!window.AnalysisConfigModal) return null;
|
|
1987
|
+
|
|
1988
|
+
if (!bulkAnalysisConfigModal) {
|
|
1989
|
+
bulkAnalysisConfigModal = new window.AnalysisConfigModal();
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
var commonRepo = getCommonRepository(rows);
|
|
1993
|
+
var storageKeys = getBulkAnalysisStorageKeys(commonRepo);
|
|
1994
|
+
var repoSettings = await fetchBulkRepoSettings(commonRepo);
|
|
1995
|
+
var rememberedTab = localStorage.getItem(storageKeys.tab);
|
|
1996
|
+
var lastInstructions = localStorage.getItem(storageKeys.instructions) || '';
|
|
1997
|
+
|
|
1998
|
+
bulkAnalysisConfigModal.onTabChange = function (tabId) {
|
|
1999
|
+
localStorage.setItem(storageKeys.tab, tabId);
|
|
2000
|
+
};
|
|
2001
|
+
|
|
2002
|
+
var providersInfo = await getBulkProvidersInfo();
|
|
2003
|
+
var resolvedPair = window.resolveProviderModelPair([
|
|
2004
|
+
{ provider: repoSettings?.default_provider, model: repoSettings?.default_model },
|
|
2005
|
+
{ provider: window.__pairReview?.defaultProvider, model: window.__pairReview?.defaultModel }
|
|
2006
|
+
], providersInfo);
|
|
2007
|
+
|
|
2008
|
+
var config = await bulkAnalysisConfigModal.show({
|
|
2009
|
+
currentModel: resolvedPair.model,
|
|
2010
|
+
currentProvider: resolvedPair.provider,
|
|
2011
|
+
defaultTab: rememberedTab || repoSettings?.default_tab || 'single',
|
|
2012
|
+
repoInstructions: commonRepo ? (repoSettings?.default_instructions || '') : '',
|
|
2013
|
+
lastInstructions: lastInstructions,
|
|
2014
|
+
defaultCouncilId: commonRepo ? (repoSettings?.default_council_id || null) : null,
|
|
2015
|
+
hasPr: true,
|
|
2016
|
+
hasGithubToken: window.__pairReview?.hasGithubToken !== false
|
|
2017
|
+
});
|
|
2018
|
+
|
|
2019
|
+
if (!config) return null;
|
|
2020
|
+
|
|
2021
|
+
var submittedInstructions = config.customInstructions || '';
|
|
2022
|
+
if (submittedInstructions) {
|
|
2023
|
+
localStorage.setItem(storageKeys.instructions, submittedInstructions);
|
|
2024
|
+
} else {
|
|
2025
|
+
localStorage.removeItem(storageKeys.instructions);
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
return config;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
async function storeBulkAnalysisConfig(config) {
|
|
2032
|
+
var response = await fetch('/api/bulk-analysis-configs', {
|
|
2033
|
+
method: 'POST',
|
|
2034
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2035
|
+
body: JSON.stringify({ analysisConfig: config })
|
|
1900
2036
|
});
|
|
1901
|
-
return
|
|
2037
|
+
var data = await response.json().catch(function () { return {}; });
|
|
2038
|
+
if (!response.ok || !data.id) {
|
|
2039
|
+
throw new Error(data.error || 'Failed to save analysis settings');
|
|
2040
|
+
}
|
|
2041
|
+
return data.id;
|
|
1902
2042
|
}
|
|
1903
2043
|
|
|
1904
2044
|
/**
|
|
@@ -1924,10 +2064,29 @@
|
|
|
1924
2064
|
bulkOpenUrls(urls);
|
|
1925
2065
|
}
|
|
1926
2066
|
|
|
1927
|
-
function handleBulkAnalyze(selectedIds, selectionInstance) {
|
|
1928
|
-
var
|
|
1929
|
-
|
|
1930
|
-
|
|
2067
|
+
async function handleBulkAnalyze(selectedIds, selectionInstance) {
|
|
2068
|
+
var rows = getSelectedCollectionRows(selectedIds, selectionInstance.config.tbodyId);
|
|
2069
|
+
if (rows.length === 0) return;
|
|
2070
|
+
|
|
2071
|
+
if (!window.AnalysisConfigModal) {
|
|
2072
|
+
selectionInstance.exit();
|
|
2073
|
+
bulkOpenUrls(buildReviewUrlsFromRows(rows, '?analyze=true'));
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
try {
|
|
2078
|
+
var config = await showBulkAnalysisConfig(rows);
|
|
2079
|
+
if (!config) return;
|
|
2080
|
+
|
|
2081
|
+
var configId = await storeBulkAnalysisConfig(config);
|
|
2082
|
+
var query = '?analyze=true&analysisConfigId=' + encodeURIComponent(configId);
|
|
2083
|
+
var urls = buildReviewUrlsFromRows(rows, query);
|
|
2084
|
+
selectionInstance.exit();
|
|
2085
|
+
bulkOpenUrls(urls);
|
|
2086
|
+
} catch (error) {
|
|
2087
|
+
console.error('Bulk analyze error:', error);
|
|
2088
|
+
if (window.toast) window.toast.error('Failed to start bulk analysis: ' + error.message);
|
|
2089
|
+
}
|
|
1931
2090
|
}
|
|
1932
2091
|
|
|
1933
2092
|
// ─── Event Delegation ───────────────────────────────────────────────────────
|