@in-the-loop-labs/pair-review 3.5.2 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/public/css/analysis-config.css +1807 -0
- package/public/css/pr.css +1029 -2169
- package/public/index.html +11 -0
- package/public/js/components/AIPanel.js +39 -23
- package/public/js/components/AdvancedConfigTab.js +56 -4
- package/public/js/components/AnalysisConfigModal.js +41 -25
- package/public/js/components/ChatPanel.js +163 -3
- package/public/js/components/KeyboardShortcuts.js +10 -26
- package/public/js/components/ReviewModal.js +135 -13
- package/public/js/components/TourBar.js +248 -0
- package/public/js/components/VoiceCentricConfigTab.js +36 -0
- package/public/js/index.js +175 -16
- package/public/js/local.js +64 -8
- package/public/js/modules/cancel-background-job.js +183 -0
- package/public/js/modules/hunk-summary-renderer.js +116 -0
- package/public/js/modules/storage-cleanup.js +16 -0
- package/public/js/modules/suggestion-manager.js +25 -1
- package/public/js/modules/tour-renderer.js +755 -0
- package/public/js/pr.js +1826 -56
- package/public/js/repo-links.js +328 -0
- package/public/js/utils/modal-detection.js +77 -0
- package/public/js/utils/provider-model.js +88 -0
- package/public/js/utils/storage-keys.js +50 -0
- package/public/local.html +24 -0
- package/public/pr.html +24 -0
- package/public/repo-settings.html +1 -0
- package/public/setup.html +2 -0
- package/src/ai/abort-signal-wiring.js +130 -0
- package/src/ai/analyzer.js +125 -18
- package/src/ai/background-queue.js +290 -0
- package/src/ai/claude-cli.js +1 -1
- package/src/ai/claude-provider.js +50 -7
- package/src/ai/codex-provider.js +28 -5
- package/src/ai/copilot-provider.js +22 -3
- package/src/ai/cursor-agent-provider.js +22 -6
- package/src/ai/executable-provider.js +4 -19
- package/src/ai/gemini-provider.js +22 -5
- package/src/ai/hunk-hashing.js +161 -0
- package/src/ai/index.js +2 -0
- package/src/ai/opencode-provider.js +21 -5
- package/src/ai/pi-provider.js +21 -5
- package/src/ai/prompts/hunk-summary.js +199 -0
- package/src/ai/prompts/tour.js +232 -0
- package/src/ai/provider.js +21 -1
- package/src/ai/summary-generator.js +469 -0
- package/src/ai/tour-generator.js +568 -0
- package/src/config.js +778 -10
- package/src/database.js +282 -1
- 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 +201 -172
- package/src/main.js +133 -30
- package/src/routes/analyses.js +30 -7
- package/src/routes/bulk-analysis-configs.js +295 -0
- package/src/routes/config.js +118 -3
- package/src/routes/context-files.js +2 -29
- package/src/routes/external-comments.js +20 -10
- package/src/routes/github-collections.js +3 -1
- package/src/routes/local.js +410 -13
- package/src/routes/mcp.js +47 -4
- package/src/routes/middleware/validate-review-id.js +53 -0
- package/src/routes/pr.js +556 -71
- package/src/routes/reviews.js +145 -29
- 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
- package/src/utils/diff-hunks.js +65 -0
- package/src/utils/json-extractor.js +5 -2
|
@@ -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
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* TourBar - Sticky top bar shown while a guided tour is active.
|
|
4
|
+
*
|
|
5
|
+
* Displays "Stop N of M" plus Prev/Next/Exit controls. On completion the
|
|
6
|
+
* Prev/Next/Exit chrome swaps for Restart/Close. The bar consumes callbacks
|
|
7
|
+
* from PRManager; it never reaches back into application state.
|
|
8
|
+
*
|
|
9
|
+
* Lifecycle:
|
|
10
|
+
* const bar = new TourBar({ onPrev, onNext, onExit, onRestart });
|
|
11
|
+
* bar.mount(parent); // prepends to parent (defaults to document.body)
|
|
12
|
+
* bar.setStops(stops); // initial render
|
|
13
|
+
* bar.setActiveIndex(0); // pre-tour state
|
|
14
|
+
* bar.setCompleted(true); // toggles to Restart / Close chrome
|
|
15
|
+
* bar.unmount();
|
|
16
|
+
*
|
|
17
|
+
* The bar uses `position: sticky` so it visually pins to the top of its
|
|
18
|
+
* scrolling parent. Pass the diff-view scroll container as `parent` so the
|
|
19
|
+
* bar spans the diff width (and not the file-tree sidebar).
|
|
20
|
+
*
|
|
21
|
+
* Testability: instantiable in jsdom; CommonJS export at the bottom.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// Octicon SVG path data. `milestone` brands the bar overall; `location`
|
|
25
|
+
// marks the per-stop navigation chrome (mirrors the inline stop marker).
|
|
26
|
+
// Module-scoped names are prefixed so they don't collide with the same
|
|
27
|
+
// constants in tour-renderer.js when both are loaded as plain <script> tags
|
|
28
|
+
// into the shared global scope.
|
|
29
|
+
const TOUR_BAR_MILESTONE_PATH = 'M7.75 0a.75.75 0 0 1 .75.75V3h3.634c.414 0 .814.144 1.13.406l2.501 2.071a1.75 1.75 0 0 1 0 2.696l-2.5 2.07a1.75 1.75 0 0 1-1.131.407H8.5v5.6a.75.75 0 0 1-1.5 0V10.65H3.75A1.75 1.75 0 0 1 2 8.9V4.75C2 3.784 2.784 3 3.75 3H7V.75A.75.75 0 0 1 7.75 0Zm-4 4.5a.25.25 0 0 0-.25.25V8.9c0 .138.112.25.25.25h8.384a.25.25 0 0 0 .16-.058l2.5-2.07a.25.25 0 0 0 0-.386l-2.5-2.07a.25.25 0 0 0-.16-.058H3.75Z';
|
|
30
|
+
const TOUR_BAR_LOCATION_PATH = 'm12.596 11.596-3.535 3.536a1.5 1.5 0 0 1-2.122 0l-3.535-3.536a6.5 6.5 0 1 1 9.192-9.193 6.5 6.5 0 0 1 0 9.193Zm-1.06-8.132v-.001a5 5 0 1 0-7.072 7.072L8 14.07l3.536-3.534a5 5 0 0 0 0-7.072ZM8 9a2 2 0 1 1-.001-3.999A2 2 0 0 1 8 9Z';
|
|
31
|
+
|
|
32
|
+
function tourBarSvgIcon(pathData) {
|
|
33
|
+
return `<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="${pathData}"/></svg>`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
class TourBar {
|
|
37
|
+
/**
|
|
38
|
+
* @param {Object} callbacks
|
|
39
|
+
* @param {Function} [callbacks.onPrev] - Called when the user clicks Prev.
|
|
40
|
+
* @param {Function} [callbacks.onNext] - Called when the user clicks Next.
|
|
41
|
+
* @param {Function} [callbacks.onExit] - Called when the user clicks Exit.
|
|
42
|
+
* @param {Function} [callbacks.onRestart] - Called when the user clicks Restart (completion state).
|
|
43
|
+
*/
|
|
44
|
+
constructor({ onPrev, onNext, onExit, onRestart } = {}) {
|
|
45
|
+
this._onPrev = onPrev || (() => {});
|
|
46
|
+
this._onNext = onNext || (() => {});
|
|
47
|
+
this._onExit = onExit || (() => {});
|
|
48
|
+
this._onRestart = onRestart || (() => {});
|
|
49
|
+
|
|
50
|
+
this._stops = [];
|
|
51
|
+
this._activeIndex = -1;
|
|
52
|
+
this._completed = false;
|
|
53
|
+
|
|
54
|
+
this._root = null;
|
|
55
|
+
this._progressEl = null;
|
|
56
|
+
this._navEl = null;
|
|
57
|
+
this._prevBtn = null;
|
|
58
|
+
this._nextBtn = null;
|
|
59
|
+
this._exitBtn = null;
|
|
60
|
+
this._restartBtn = null;
|
|
61
|
+
this._closeBtn = null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Prepend the bar to `parent` (defaults to `document.body`). Idempotent.
|
|
66
|
+
* The bar's CSS uses `position: sticky`, so it pins to the top of the
|
|
67
|
+
* nearest scrollable ancestor — pass that scroll container as `parent`
|
|
68
|
+
* to scope the bar to a region rather than the viewport.
|
|
69
|
+
* @param {HTMLElement} [parent=document.body]
|
|
70
|
+
* @returns {TourBar}
|
|
71
|
+
*/
|
|
72
|
+
mount(parent) {
|
|
73
|
+
if (this._root && this._root.isConnected) return this;
|
|
74
|
+
const target = parent || document.body;
|
|
75
|
+
|
|
76
|
+
const root = document.createElement('div');
|
|
77
|
+
root.className = 'tour-bar';
|
|
78
|
+
root.setAttribute('role', 'toolbar');
|
|
79
|
+
root.setAttribute('aria-label', 'Guided tour controls');
|
|
80
|
+
|
|
81
|
+
const brand = document.createElement('div');
|
|
82
|
+
brand.className = 'tour-bar__brand';
|
|
83
|
+
brand.innerHTML = `${tourBarSvgIcon(TOUR_BAR_MILESTONE_PATH)}<span>Tour</span>`;
|
|
84
|
+
|
|
85
|
+
const progress = document.createElement('div');
|
|
86
|
+
progress.className = 'tour-bar__progress';
|
|
87
|
+
progress.textContent = '';
|
|
88
|
+
|
|
89
|
+
const nav = document.createElement('div');
|
|
90
|
+
nav.className = 'tour-bar__nav';
|
|
91
|
+
|
|
92
|
+
// Prev / Next / Exit (default chrome)
|
|
93
|
+
const prevBtn = document.createElement('button');
|
|
94
|
+
prevBtn.type = 'button';
|
|
95
|
+
prevBtn.className = 'tour-bar__prev';
|
|
96
|
+
prevBtn.innerHTML = `${tourBarSvgIcon(TOUR_BAR_LOCATION_PATH)}<span>Prev</span>`;
|
|
97
|
+
prevBtn.addEventListener('click', () => this._onPrev());
|
|
98
|
+
|
|
99
|
+
const nextBtn = document.createElement('button');
|
|
100
|
+
nextBtn.type = 'button';
|
|
101
|
+
nextBtn.className = 'tour-bar__next';
|
|
102
|
+
nextBtn.innerHTML = `<span>Next</span>${tourBarSvgIcon(TOUR_BAR_LOCATION_PATH)}`;
|
|
103
|
+
nextBtn.addEventListener('click', () => this._onNext());
|
|
104
|
+
|
|
105
|
+
const exitBtn = document.createElement('button');
|
|
106
|
+
exitBtn.type = 'button';
|
|
107
|
+
exitBtn.className = 'tour-bar__exit';
|
|
108
|
+
exitBtn.textContent = 'Exit';
|
|
109
|
+
exitBtn.addEventListener('click', () => this._onExit());
|
|
110
|
+
|
|
111
|
+
// Completion chrome (created up front, toggled by setCompleted)
|
|
112
|
+
const restartBtn = document.createElement('button');
|
|
113
|
+
restartBtn.type = 'button';
|
|
114
|
+
restartBtn.className = 'tour-bar__restart';
|
|
115
|
+
restartBtn.textContent = 'Restart';
|
|
116
|
+
restartBtn.style.display = 'none';
|
|
117
|
+
restartBtn.addEventListener('click', () => this._onRestart());
|
|
118
|
+
|
|
119
|
+
const closeBtn = document.createElement('button');
|
|
120
|
+
closeBtn.type = 'button';
|
|
121
|
+
closeBtn.className = 'tour-bar__close';
|
|
122
|
+
closeBtn.textContent = 'Close';
|
|
123
|
+
closeBtn.style.display = 'none';
|
|
124
|
+
closeBtn.addEventListener('click', () => this._onExit());
|
|
125
|
+
|
|
126
|
+
nav.appendChild(prevBtn);
|
|
127
|
+
nav.appendChild(nextBtn);
|
|
128
|
+
nav.appendChild(exitBtn);
|
|
129
|
+
nav.appendChild(restartBtn);
|
|
130
|
+
nav.appendChild(closeBtn);
|
|
131
|
+
|
|
132
|
+
root.appendChild(brand);
|
|
133
|
+
root.appendChild(progress);
|
|
134
|
+
root.appendChild(nav);
|
|
135
|
+
|
|
136
|
+
// Prepend so the bar becomes the first child — it pins above the
|
|
137
|
+
// diff-toolbar (which sits at top:0 in the same scroll container).
|
|
138
|
+
target.prepend(root);
|
|
139
|
+
|
|
140
|
+
this._root = root;
|
|
141
|
+
this._progressEl = progress;
|
|
142
|
+
this._navEl = nav;
|
|
143
|
+
this._prevBtn = prevBtn;
|
|
144
|
+
this._nextBtn = nextBtn;
|
|
145
|
+
this._exitBtn = exitBtn;
|
|
146
|
+
this._restartBtn = restartBtn;
|
|
147
|
+
this._closeBtn = closeBtn;
|
|
148
|
+
|
|
149
|
+
// Initial paint reflects whatever state was set before mount.
|
|
150
|
+
this._render();
|
|
151
|
+
return this;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Remove the bar from the DOM. Safe to call repeatedly.
|
|
156
|
+
*/
|
|
157
|
+
unmount() {
|
|
158
|
+
if (this._root && this._root.isConnected) {
|
|
159
|
+
this._root.remove();
|
|
160
|
+
}
|
|
161
|
+
this._root = null;
|
|
162
|
+
this._progressEl = null;
|
|
163
|
+
this._navEl = null;
|
|
164
|
+
this._prevBtn = null;
|
|
165
|
+
this._nextBtn = null;
|
|
166
|
+
this._exitBtn = null;
|
|
167
|
+
this._restartBtn = null;
|
|
168
|
+
this._closeBtn = null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* @param {Array<Object>} stops - The full ordered list of stops.
|
|
173
|
+
*/
|
|
174
|
+
setStops(stops) {
|
|
175
|
+
this._stops = Array.isArray(stops) ? stops : [];
|
|
176
|
+
this._render();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @param {number} index - The currently-active stop (0-based) or -1 for none.
|
|
181
|
+
*/
|
|
182
|
+
setActiveIndex(index) {
|
|
183
|
+
this._activeIndex = typeof index === 'number' ? index : -1;
|
|
184
|
+
this._render();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* @param {boolean} isCompleted - Swap to Restart/Close chrome when true.
|
|
189
|
+
*/
|
|
190
|
+
setCompleted(isCompleted) {
|
|
191
|
+
this._completed = isCompleted === true;
|
|
192
|
+
this._render();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// --- private ------------------------------------------------------------
|
|
196
|
+
|
|
197
|
+
_render() {
|
|
198
|
+
if (!this._root) return;
|
|
199
|
+
const total = this._stops.length;
|
|
200
|
+
const visibleIndex = this._activeIndex + 1; // 1-based for display
|
|
201
|
+
|
|
202
|
+
if (this._progressEl) {
|
|
203
|
+
if (total === 0) {
|
|
204
|
+
this._progressEl.textContent = '';
|
|
205
|
+
} else if (this._completed) {
|
|
206
|
+
this._progressEl.textContent = `Tour complete (${total} stop${total === 1 ? '' : 's'})`;
|
|
207
|
+
} else if (this._activeIndex < 0) {
|
|
208
|
+
this._progressEl.textContent = `${total} stop${total === 1 ? '' : 's'}`;
|
|
209
|
+
} else {
|
|
210
|
+
this._progressEl.textContent = `Stop ${visibleIndex} of ${total}`;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (this._completed) {
|
|
215
|
+
if (this._prevBtn) this._prevBtn.style.display = 'none';
|
|
216
|
+
if (this._nextBtn) this._nextBtn.style.display = 'none';
|
|
217
|
+
if (this._exitBtn) this._exitBtn.style.display = 'none';
|
|
218
|
+
if (this._restartBtn) this._restartBtn.style.display = '';
|
|
219
|
+
if (this._closeBtn) this._closeBtn.style.display = '';
|
|
220
|
+
} else {
|
|
221
|
+
if (this._prevBtn) this._prevBtn.style.display = '';
|
|
222
|
+
if (this._nextBtn) this._nextBtn.style.display = '';
|
|
223
|
+
if (this._exitBtn) this._exitBtn.style.display = '';
|
|
224
|
+
if (this._restartBtn) this._restartBtn.style.display = 'none';
|
|
225
|
+
if (this._closeBtn) this._closeBtn.style.display = 'none';
|
|
226
|
+
|
|
227
|
+
// Disable Prev at first stop; disable Next at last (no auto-completion
|
|
228
|
+
// happens in the bar — the orchestrator advances past the end to set
|
|
229
|
+
// completed state).
|
|
230
|
+
if (this._prevBtn) {
|
|
231
|
+
this._prevBtn.disabled = this._activeIndex <= 0;
|
|
232
|
+
}
|
|
233
|
+
if (this._nextBtn) {
|
|
234
|
+
// Next remains enabled on the last stop so the user can advance
|
|
235
|
+
// into completion state.
|
|
236
|
+
this._nextBtn.disabled = total === 0;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (typeof window !== 'undefined') {
|
|
243
|
+
window.TourBar = TourBar;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
247
|
+
module.exports = { TourBar };
|
|
248
|
+
}
|
|
@@ -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
|
+
}
|