@in-the-loop-labs/pair-review 2.6.3 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pi/extensions/task/index.ts +1 -1
- package/.pi/skills/review-roulette/SKILL.md +1 -1
- package/LICENSE +201 -674
- package/README.md +2 -2
- package/bin/pair-review.js +1 -1
- package/package.json +2 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin-code-critic/.claude-plugin/plugin.json +2 -2
- package/plugin-code-critic/skills/analyze/scripts/git-diff-lines +1 -1
- package/public/css/ai-summary-modal.css +1 -1
- package/public/css/pr.css +194 -0
- package/public/index.html +168 -3
- package/public/js/components/AIPanel.js +17 -3
- package/public/js/components/AISummaryModal.js +1 -1
- package/public/js/components/AdvancedConfigTab.js +1 -1
- package/public/js/components/AnalysisConfigModal.js +1 -1
- package/public/js/components/ChatPanel.js +42 -7
- package/public/js/components/ConfirmDialog.js +22 -3
- package/public/js/components/CouncilProgressModal.js +14 -1
- package/public/js/components/DiffOptionsDropdown.js +411 -24
- package/public/js/components/EmojiPicker.js +1 -1
- package/public/js/components/KeyboardShortcuts.js +1 -1
- package/public/js/components/PanelGroup.js +1 -1
- package/public/js/components/PreviewModal.js +1 -1
- package/public/js/components/ReviewModal.js +1 -1
- package/public/js/components/SplitButton.js +1 -1
- package/public/js/components/StatusIndicator.js +1 -1
- package/public/js/components/SuggestionNavigator.js +13 -6
- package/public/js/components/TabTitle.js +96 -0
- package/public/js/components/TextInputDialog.js +1 -1
- package/public/js/components/TimeoutSelect.js +1 -1
- package/public/js/components/Toast.js +7 -1
- package/public/js/components/VoiceCentricConfigTab.js +1 -1
- package/public/js/index.js +649 -44
- package/public/js/local.js +570 -77
- package/public/js/modules/analysis-history.js +4 -3
- package/public/js/modules/comment-manager.js +6 -1
- package/public/js/modules/comment-minimizer.js +304 -0
- package/public/js/modules/diff-context.js +1 -1
- package/public/js/modules/diff-renderer.js +1 -1
- package/public/js/modules/file-comment-manager.js +1 -1
- package/public/js/modules/file-list-merger.js +1 -1
- package/public/js/modules/gap-coordinates.js +1 -1
- package/public/js/modules/hunk-parser.js +1 -1
- package/public/js/modules/line-tracker.js +1 -1
- package/public/js/modules/panel-resizer.js +1 -1
- package/public/js/modules/storage-cleanup.js +1 -1
- package/public/js/modules/suggestion-manager.js +1 -1
- package/public/js/pr.js +83 -7
- package/public/js/repo-settings.js +1 -1
- package/public/js/utils/category-emoji.js +1 -1
- package/public/js/utils/file-order.js +1 -1
- package/public/js/utils/markdown.js +1 -1
- package/public/js/utils/suggestion-ui.js +1 -1
- package/public/js/utils/tier-icons.js +1 -1
- package/public/js/utils/time.js +1 -1
- package/public/js/ws-client.js +1 -1
- package/public/local.html +14 -0
- package/public/pr.html +3 -0
- package/public/setup.html +1 -1
- package/src/ai/analyzer.js +18 -12
- package/src/ai/claude-cli.js +1 -1
- package/src/ai/claude-provider.js +1 -1
- package/src/ai/codex-provider.js +1 -1
- package/src/ai/copilot-provider.js +1 -1
- package/src/ai/cursor-agent-provider.js +1 -1
- package/src/ai/gemini-provider.js +1 -1
- package/src/ai/index.js +1 -1
- package/src/ai/opencode-provider.js +1 -1
- package/src/ai/pi-provider.js +1 -1
- package/src/ai/prompts/baseline/consolidation/balanced.js +1 -1
- package/src/ai/prompts/baseline/consolidation/fast.js +1 -1
- package/src/ai/prompts/baseline/consolidation/thorough.js +1 -1
- package/src/ai/prompts/baseline/level1/balanced.js +1 -1
- package/src/ai/prompts/baseline/level1/fast.js +1 -1
- package/src/ai/prompts/baseline/level1/thorough.js +1 -1
- package/src/ai/prompts/baseline/level2/balanced.js +1 -1
- package/src/ai/prompts/baseline/level2/fast.js +1 -1
- package/src/ai/prompts/baseline/level2/thorough.js +1 -1
- package/src/ai/prompts/baseline/level3/balanced.js +1 -1
- package/src/ai/prompts/baseline/level3/fast.js +1 -1
- package/src/ai/prompts/baseline/level3/thorough.js +1 -1
- package/src/ai/prompts/baseline/orchestration/balanced.js +1 -1
- package/src/ai/prompts/baseline/orchestration/fast.js +1 -1
- package/src/ai/prompts/baseline/orchestration/thorough.js +1 -1
- package/src/ai/prompts/config.js +1 -1
- package/src/ai/prompts/index.js +1 -1
- package/src/ai/prompts/line-number-guidance.js +1 -1
- package/src/ai/prompts/render-for-skill.js +1 -1
- package/src/ai/prompts/shared/diff-instructions.js +1 -1
- package/src/ai/prompts/shared/output-schema.js +1 -1
- package/src/ai/prompts/shared/valid-files.js +1 -1
- package/src/ai/prompts/sparse-checkout-guidance.js +1 -1
- package/src/ai/provider-availability.js +1 -1
- package/src/ai/provider.js +1 -1
- package/src/ai/stream-parser.js +1 -1
- package/src/chat/acp-bridge.js +1 -1
- package/src/chat/api-reference.js +1 -1
- package/src/chat/chat-providers.js +1 -1
- package/src/chat/claude-code-bridge.js +1 -1
- package/src/chat/codex-bridge.js +1 -1
- package/src/chat/pi-bridge.js +1 -1
- package/src/chat/prompt-builder.js +1 -1
- package/src/chat/session-manager.js +1 -1
- package/src/config.js +3 -1
- package/src/database.js +591 -40
- package/src/events/review-events.js +1 -1
- package/src/git/base-branch.js +173 -0
- package/src/git/gitattributes.js +1 -1
- package/src/git/sha-abbrev.js +35 -0
- package/src/git/worktree.js +1 -1
- package/src/github/client.js +33 -2
- package/src/github/parser.js +1 -1
- package/src/hooks/hook-runner.js +100 -0
- package/src/hooks/payloads.js +212 -0
- package/src/local-review.js +469 -130
- package/src/local-scope.js +58 -0
- package/src/main.js +56 -5
- package/src/mcp-stdio.js +1 -1
- package/src/protocol-handler.js +1 -1
- package/src/routes/analyses.js +74 -11
- package/src/routes/chat.js +34 -1
- package/src/routes/config.js +2 -1
- package/src/routes/context-files.js +1 -1
- package/src/routes/councils.js +1 -1
- package/src/routes/github-collections.js +1 -1
- package/src/routes/local.js +735 -69
- package/src/routes/mcp.js +21 -11
- package/src/routes/pr.js +91 -13
- package/src/routes/reviews.js +1 -1
- package/src/routes/setup.js +2 -1
- package/src/routes/shared.js +1 -1
- package/src/routes/worktrees.js +213 -149
- package/src/server.js +31 -1
- package/src/setup/local-setup.js +47 -6
- package/src/setup/pr-setup.js +29 -6
- package/src/utils/auto-context.js +1 -1
- package/src/utils/category-emoji.js +1 -1
- package/src/utils/comment-formatter.js +1 -1
- package/src/utils/diff-annotator.js +1 -1
- package/src/utils/diff-file-list.js +1 -1
- package/src/utils/instructions.js +1 -1
- package/src/utils/json-extractor.js +1 -1
- package/src/utils/line-validation.js +1 -1
- package/src/utils/logger.js +1 -1
- package/src/utils/paths.js +1 -1
- package/src/utils/safe-parse-json.js +1 -1
- package/src/utils/stats-calculator.js +1 -1
- package/src/ws/index.js +1 -1
- package/src/ws/server.js +1 -1
package/public/js/local.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// SPDX-License-Identifier:
|
|
1
|
+
// Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
/**
|
|
3
3
|
* Local Mode Manager
|
|
4
4
|
*
|
|
@@ -230,10 +230,6 @@ class LocalManager {
|
|
|
230
230
|
|
|
231
231
|
// Override triggerAIAnalysis for local mode
|
|
232
232
|
manager.triggerAIAnalysis = async function() {
|
|
233
|
-
// Timeout (ms) for stale check — git commands can hang on locked repos.
|
|
234
|
-
// Defined locally to avoid relying on cross-script const from pr.js.
|
|
235
|
-
const STALE_TIMEOUT = 2000;
|
|
236
|
-
|
|
237
233
|
if (manager.isAnalyzing) {
|
|
238
234
|
manager.reopenModal();
|
|
239
235
|
return;
|
|
@@ -257,19 +253,13 @@ class LocalManager {
|
|
|
257
253
|
return;
|
|
258
254
|
}
|
|
259
255
|
|
|
260
|
-
// Run stale check and settings fetch in parallel to minimize dialog delay
|
|
261
|
-
//
|
|
262
|
-
// freeing the HTTP connection for subsequent requests.
|
|
256
|
+
// Run stale check and settings fetch in parallel to minimize dialog delay.
|
|
257
|
+
// Reuse the on-load staleness promise if still available, otherwise fetch fresh.
|
|
263
258
|
const _tParallel0 = performance.now();
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}, STALE_TIMEOUT);
|
|
269
|
-
const staleCheckWithTimeout = fetch(`/api/local/${reviewId}/check-stale`, { signal: staleAbort.signal })
|
|
270
|
-
.then(r => r.ok ? r.json() : null)
|
|
271
|
-
.then(result => { clearTimeout(staleTimer); return result; })
|
|
272
|
-
.catch(() => { clearTimeout(staleTimer); return null; });
|
|
259
|
+
const staleCheckWithTimeout = manager._stalenessPromise
|
|
260
|
+
? manager._stalenessPromise
|
|
261
|
+
: self._fetchLocalStaleness();
|
|
262
|
+
manager._stalenessPromise = null; // consume it
|
|
273
263
|
const [staleResult, repoSettings, reviewSettings] = await Promise.all([
|
|
274
264
|
staleCheckWithTimeout,
|
|
275
265
|
manager.fetchRepoSettings().catch(() => null),
|
|
@@ -644,9 +634,11 @@ class LocalManager {
|
|
|
644
634
|
}
|
|
645
635
|
|
|
646
636
|
/**
|
|
647
|
-
* Refresh the diff from the working directory
|
|
637
|
+
* Refresh the diff from the working directory.
|
|
638
|
+
* @param {Object} [opts] - Options
|
|
639
|
+
* @param {boolean} [opts.silent] - When true, auto-update on HEAD change without dialog
|
|
648
640
|
*/
|
|
649
|
-
async refreshDiff() {
|
|
641
|
+
async refreshDiff(opts = {}) {
|
|
650
642
|
const manager = window.prManager;
|
|
651
643
|
const refreshBtn = document.getElementById('local-refresh-btn');
|
|
652
644
|
|
|
@@ -669,64 +661,25 @@ class LocalManager {
|
|
|
669
661
|
const result = await response.json();
|
|
670
662
|
console.log('Diff refreshed:', result.stats);
|
|
671
663
|
|
|
672
|
-
//
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
const
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
if (
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
cancelText: 'Stay on Current Session',
|
|
684
|
-
confirmClass: 'btn-primary'
|
|
685
|
-
});
|
|
686
|
-
|
|
687
|
-
if (dialogResult === 'confirm') {
|
|
688
|
-
// Redirect to the new session
|
|
689
|
-
window.location.href = `/local/${result.newSessionId}`;
|
|
664
|
+
// HEAD change handling — branch scope is auto-updated by the backend;
|
|
665
|
+
// non-branch scope requires user decision via resolve-head-change.
|
|
666
|
+
if (result.headShaChanged) {
|
|
667
|
+
const LS = window.LocalScope;
|
|
668
|
+
const hasBranch = LS ? LS.scopeIncludes(this.scopeStart, this.scopeEnd, 'branch') : false;
|
|
669
|
+
|
|
670
|
+
if (!hasBranch) {
|
|
671
|
+
// Non-branch scope: let the user (or silent mode) decide
|
|
672
|
+
const resolved = await this._resolveHeadChange(result, opts);
|
|
673
|
+
if (!resolved) {
|
|
674
|
+
// User cancelled — keep old diff, early return
|
|
690
675
|
return;
|
|
691
676
|
}
|
|
692
|
-
} else {
|
|
693
|
-
// Fallback if confirmDialog is not available
|
|
694
|
-
const switchSession = confirm(
|
|
695
|
-
`HEAD has changed (${originalSha} -> ${newSha}). ` +
|
|
696
|
-
`Your comments and AI suggestions are tied to the previous commit. ` +
|
|
697
|
-
`Switch to the new session?`
|
|
698
|
-
);
|
|
699
|
-
|
|
700
|
-
if (switchSession) {
|
|
701
|
-
window.location.href = `/local/${result.newSessionId}`;
|
|
702
|
-
return;
|
|
703
|
-
}
|
|
704
|
-
}
|
|
705
|
-
|
|
706
|
-
// User chose to stay, show info toast
|
|
707
|
-
if (window.toast) {
|
|
708
|
-
window.toast.showInfo('Staying on current session. Refresh again to see this option.');
|
|
709
677
|
}
|
|
678
|
+
// Branch scope: backend already updated SHA and persisted diff — fall through
|
|
710
679
|
}
|
|
711
680
|
|
|
712
681
|
// Reload the diff display
|
|
713
|
-
await this.
|
|
714
|
-
|
|
715
|
-
// Re-render comments and AI suggestions on the fresh DOM
|
|
716
|
-
// (renderDiff clears the diff container, so we must re-populate)
|
|
717
|
-
const includeDismissed = window.aiPanel?.showDismissedComments || false;
|
|
718
|
-
await manager.loadUserComments(includeDismissed);
|
|
719
|
-
// Note: Unlike loadLocalReview() which skips this when analysisHistoryManager exists
|
|
720
|
-
// (because the manager triggers loadAISuggestions via onSelectionChange on init),
|
|
721
|
-
// refresh must call unconditionally since the manager won't re-fire its callback.
|
|
722
|
-
await manager.loadAISuggestions(null, manager.selectedRunId);
|
|
723
|
-
|
|
724
|
-
// Show success toast
|
|
725
|
-
if (window.toast) {
|
|
726
|
-
window.toast.showSuccess('Diff refreshed successfully');
|
|
727
|
-
} else if (window.showToast) {
|
|
728
|
-
window.showToast('Diff refreshed successfully', 'success');
|
|
729
|
-
}
|
|
682
|
+
await this._applyRefreshedDiff(manager, result);
|
|
730
683
|
|
|
731
684
|
} catch (error) {
|
|
732
685
|
console.error('Error refreshing diff:', error);
|
|
@@ -746,6 +699,198 @@ class LocalManager {
|
|
|
746
699
|
}
|
|
747
700
|
}
|
|
748
701
|
|
|
702
|
+
/**
|
|
703
|
+
* Handle a non-branch-scope HEAD SHA change.
|
|
704
|
+
* Shows a 3-option dialog (or auto-updates in silent mode).
|
|
705
|
+
* @returns {boolean} true if the session was updated in-place (caller should apply diff),
|
|
706
|
+
* false if cancelled or redirecting away (caller should skip _applyRefreshedDiff)
|
|
707
|
+
*/
|
|
708
|
+
async _resolveHeadChange(result, opts) {
|
|
709
|
+
const abbrevLen = this.localData?.shaAbbrevLength || 7;
|
|
710
|
+
const originalSha = result.previousHeadSha ? result.previousHeadSha.substring(0, abbrevLen) : 'unknown';
|
|
711
|
+
const newSha = result.currentHeadSha ? result.currentHeadSha.substring(0, abbrevLen) : 'unknown';
|
|
712
|
+
|
|
713
|
+
let action = 'update'; // default for silent mode
|
|
714
|
+
|
|
715
|
+
if (!opts.silent && window.confirmDialog) {
|
|
716
|
+
const dialogResult = await window.confirmDialog.show({
|
|
717
|
+
title: 'New Commit Detected',
|
|
718
|
+
message: `HEAD has moved from ${originalSha} to ${newSha}. Your review is based on the old commit.`,
|
|
719
|
+
confirmText: 'Continue This Session',
|
|
720
|
+
confirmDesc: 'Keep comments and suggestions, refresh diff to new HEAD',
|
|
721
|
+
confirmClass: 'btn-primary',
|
|
722
|
+
secondaryText: 'Start New Session',
|
|
723
|
+
secondaryDesc: 'Begin a fresh review from the new commit',
|
|
724
|
+
cancelText: 'Ignore the Change',
|
|
725
|
+
cancelDesc: 'Continue reviewing using the previous diff'
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
if (dialogResult === 'confirm') {
|
|
729
|
+
action = 'update';
|
|
730
|
+
} else if (dialogResult === 'secondary') {
|
|
731
|
+
action = 'new-session';
|
|
732
|
+
} else {
|
|
733
|
+
// Cancel — keep old diff
|
|
734
|
+
if (window.toast) {
|
|
735
|
+
window.toast.showInfo('Staying on current session with previous diff.');
|
|
736
|
+
}
|
|
737
|
+
return false;
|
|
738
|
+
}
|
|
739
|
+
} else if (!opts.silent) {
|
|
740
|
+
// Fallback if confirmDialog is not available
|
|
741
|
+
const switchSession = confirm(
|
|
742
|
+
`HEAD has changed (${originalSha} \u2192 ${newSha}). ` +
|
|
743
|
+
`Update this session with the new diff?`
|
|
744
|
+
);
|
|
745
|
+
action = switchSession ? 'update' : 'cancel';
|
|
746
|
+
if (action === 'cancel') return false;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
// Call resolve-head-change endpoint
|
|
750
|
+
const resp = await fetch(`/api/local/${this.reviewId}/resolve-head-change`, {
|
|
751
|
+
method: 'POST',
|
|
752
|
+
headers: { 'Content-Type': 'application/json' },
|
|
753
|
+
body: JSON.stringify({ action, newHeadSha: result.currentHeadSha })
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
if (!resp.ok) {
|
|
757
|
+
const err = await resp.json();
|
|
758
|
+
throw new Error(err.error || 'Failed to resolve head change');
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const data = await resp.json();
|
|
762
|
+
|
|
763
|
+
if (data.action === 'redirect') {
|
|
764
|
+
// UNIQUE conflict — redirect to existing session
|
|
765
|
+
window.location.href = `/local/${data.sessionId}`;
|
|
766
|
+
return false; // navigating away — caller must not fire _applyRefreshedDiff
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (data.action === 'new-session') {
|
|
770
|
+
window.location.href = `/local/${data.newSessionId}`;
|
|
771
|
+
return false; // navigating away — caller must not fire _applyRefreshedDiff
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// action === 'updated' — session SHA + diff updated, continue to reload
|
|
775
|
+
return true;
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
/**
|
|
779
|
+
* Reload the diff display, re-anchor comments, notify chat, clear stale state.
|
|
780
|
+
* Shared by refreshDiff() for both normal refreshes and HEAD-change updates.
|
|
781
|
+
*/
|
|
782
|
+
async _applyRefreshedDiff(manager, result) {
|
|
783
|
+
// Notify chat agent about diff refresh
|
|
784
|
+
if (window.chatPanel) {
|
|
785
|
+
if (result.headShaChanged) {
|
|
786
|
+
const prev = result.previousHeadSha;
|
|
787
|
+
const abbrevLen = this.localData?.shaAbbrevLength || 7;
|
|
788
|
+
window.chatPanel.queueDiffStateNotification(
|
|
789
|
+
`HEAD SHA changed: ${prev ? prev.substring(0, abbrevLen) : 'unknown'} \u2192 ${result.currentHeadSha ? result.currentHeadSha.substring(0, abbrevLen) : 'unknown'}.`
|
|
790
|
+
);
|
|
791
|
+
}
|
|
792
|
+
window.chatPanel.queueDiffStateNotification(
|
|
793
|
+
'Local diff refreshed from working directory.'
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Reload the diff display
|
|
798
|
+
await this.loadLocalDiff();
|
|
799
|
+
|
|
800
|
+
// Re-render comments and AI suggestions on the fresh DOM
|
|
801
|
+
// (renderDiff clears the diff container, so we must re-populate)
|
|
802
|
+
const includeDismissed = window.aiPanel?.showDismissedComments || false;
|
|
803
|
+
await manager.loadUserComments(includeDismissed);
|
|
804
|
+
// Note: Unlike loadLocalReview() which skips this when analysisHistoryManager exists
|
|
805
|
+
// (because the manager triggers loadAISuggestions via onSelectionChange on init),
|
|
806
|
+
// refresh must call unconditionally since the manager won't re-fire its callback.
|
|
807
|
+
await manager.loadAISuggestions(null, manager.selectedRunId);
|
|
808
|
+
|
|
809
|
+
// Clear stale state after successful refresh
|
|
810
|
+
manager._hideStaleBadge();
|
|
811
|
+
manager._stalenessPromise = null;
|
|
812
|
+
|
|
813
|
+
// Show success toast
|
|
814
|
+
if (window.toast) {
|
|
815
|
+
window.toast.showSuccess('Diff refreshed successfully');
|
|
816
|
+
} else if (window.showToast) {
|
|
817
|
+
window.showToast('Diff refreshed successfully', 'success');
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Check staleness on page load and show badge or auto-refresh.
|
|
823
|
+
*
|
|
824
|
+
* Logic mirrors PRManager._checkStalenessOnLoad but uses the local
|
|
825
|
+
* GET endpoint and only supports the 'stale' badge type (no merged/closed).
|
|
826
|
+
* @returns {Promise<Object|null>} The staleness result, or null on failure.
|
|
827
|
+
*/
|
|
828
|
+
async _checkLocalStalenessOnLoad() {
|
|
829
|
+
try {
|
|
830
|
+
const result = await this._fetchLocalStaleness();
|
|
831
|
+
if (!result) return null;
|
|
832
|
+
|
|
833
|
+
// Notify chat of HEAD SHA change even when diff digest is unchanged
|
|
834
|
+
// (e.g. git commit --amend with identical content, or rebase)
|
|
835
|
+
const abbrevLen = this.localData?.shaAbbrevLength || 7;
|
|
836
|
+
if (result.headShaChanged && window.chatPanel) {
|
|
837
|
+
window.chatPanel.queueDiffStateNotification(
|
|
838
|
+
`HEAD SHA changed (${result.previousHeadSha ? result.previousHeadSha.substring(0, abbrevLen) : 'unknown'} → ${result.currentHeadSha ? result.currentHeadSha.substring(0, abbrevLen) : 'unknown'}). The branch may have been rebased.`
|
|
839
|
+
);
|
|
840
|
+
}
|
|
841
|
+
if (result.isStale !== true) return result;
|
|
842
|
+
|
|
843
|
+
// Stale — decide: silent refresh or show badge
|
|
844
|
+
const manager = window.prManager;
|
|
845
|
+
const hasData = await manager._hasActiveSessionData();
|
|
846
|
+
if (hasData) {
|
|
847
|
+
console.debug('[Local] working directory stale, session has data — showing badge');
|
|
848
|
+
manager._showStaleBadge('stale', 'Working directory has changed');
|
|
849
|
+
if (window.chatPanel) {
|
|
850
|
+
// Notify chat of HEAD SHA change only when we have session data to protect
|
|
851
|
+
// (the !hasData path calls refreshDiff() which queues its own notification)
|
|
852
|
+
if (result.headShaChanged) {
|
|
853
|
+
window.chatPanel.queueDiffStateNotification(
|
|
854
|
+
`HEAD SHA changed (${result.previousHeadSha ? result.previousHeadSha.substring(0, abbrevLen) : 'unknown'} → ${result.currentHeadSha ? result.currentHeadSha.substring(0, abbrevLen) : 'unknown'}). The branch may have been rebased.`
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
window.chatPanel.queueDiffStateNotification(
|
|
858
|
+
'Working directory has changed since the diff was captured.'
|
|
859
|
+
);
|
|
860
|
+
}
|
|
861
|
+
} else {
|
|
862
|
+
// No user work to protect — refresh silently (auto-update on HEAD change)
|
|
863
|
+
console.debug('[Local] working directory stale, no session data — auto-refreshing');
|
|
864
|
+
await this.refreshDiff({ silent: true });
|
|
865
|
+
}
|
|
866
|
+
return result;
|
|
867
|
+
} catch {
|
|
868
|
+
// Fail silently — staleness badge is best-effort
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Fetch staleness data from the local review endpoint with a timeout.
|
|
875
|
+
* Uses GET to check the local review staleness endpoint.
|
|
876
|
+
* @returns {Promise<Object|null>} The parsed staleness result, or null on failure/timeout.
|
|
877
|
+
*/
|
|
878
|
+
async _fetchLocalStaleness() {
|
|
879
|
+
try {
|
|
880
|
+
const staleAbort = new AbortController();
|
|
881
|
+
const staleTimer = setTimeout(() => staleAbort.abort(), STALE_TIMEOUT);
|
|
882
|
+
const response = await fetch(
|
|
883
|
+
`/api/local/${this.reviewId}/check-stale`,
|
|
884
|
+
{ signal: staleAbort.signal }
|
|
885
|
+
);
|
|
886
|
+
clearTimeout(staleTimer);
|
|
887
|
+
if (!response.ok) return null;
|
|
888
|
+
return await response.json();
|
|
889
|
+
} catch {
|
|
890
|
+
return null;
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
749
894
|
/**
|
|
750
895
|
* Load local review data
|
|
751
896
|
*/
|
|
@@ -770,20 +915,47 @@ class LocalManager {
|
|
|
770
915
|
const reviewData = await response.json();
|
|
771
916
|
this.localData = reviewData;
|
|
772
917
|
|
|
918
|
+
// Read scope from metadata (backend now returns these)
|
|
919
|
+
const LS = window.LocalScope;
|
|
920
|
+
const scopeStart = reviewData.scopeStart || (LS ? LS.DEFAULT_SCOPE.start : 'unstaged');
|
|
921
|
+
const scopeEnd = reviewData.scopeEnd || (LS ? LS.DEFAULT_SCOPE.end : 'untracked');
|
|
922
|
+
this.scopeStart = scopeStart;
|
|
923
|
+
this.scopeEnd = scopeEnd;
|
|
924
|
+
|
|
773
925
|
// Create a currentPR-like object for compatibility
|
|
926
|
+
const hasBranch = LS ? LS.scopeIncludes(scopeStart, scopeEnd, 'branch') : false;
|
|
774
927
|
manager.currentPR = {
|
|
775
928
|
id: reviewData.id,
|
|
776
929
|
owner: 'local',
|
|
777
930
|
repo: reviewData.repository,
|
|
778
931
|
number: reviewData.id,
|
|
779
|
-
title:
|
|
932
|
+
title: hasBranch
|
|
933
|
+
? `Branch Changes - ${reviewData.branch} vs ${reviewData.baseBranch}`
|
|
934
|
+
: `Local Changes - ${reviewData.branch}`,
|
|
780
935
|
head_branch: reviewData.branch,
|
|
781
|
-
base_branch: reviewData.branch,
|
|
936
|
+
base_branch: hasBranch ? reviewData.baseBranch : reviewData.branch,
|
|
782
937
|
head_sha: reviewData.localHeadSha,
|
|
938
|
+
shaAbbrevLength: reviewData.shaAbbrevLength || 7,
|
|
783
939
|
reviewType: 'local',
|
|
784
940
|
localPath: reviewData.localPath
|
|
785
941
|
};
|
|
786
942
|
|
|
943
|
+
// Re-initialize DiffOptionsDropdown with scope options
|
|
944
|
+
const branchAvailable = Boolean(reviewData.branchAvailable);
|
|
945
|
+
if (manager.diffOptionsDropdown) {
|
|
946
|
+
manager.diffOptionsDropdown.destroy();
|
|
947
|
+
}
|
|
948
|
+
const diffOptionsBtn = document.getElementById('diff-options-btn');
|
|
949
|
+
if (diffOptionsBtn && window.DiffOptionsDropdown) {
|
|
950
|
+
manager.diffOptionsDropdown = new window.DiffOptionsDropdown(diffOptionsBtn, {
|
|
951
|
+
onToggleWhitespace: (hide) => manager.handleWhitespaceToggle(hide),
|
|
952
|
+
onToggleMinimize: (minimized) => manager.handleMinimizeToggle(minimized),
|
|
953
|
+
onScopeChange: (start, end) => this._handleScopeChange(start, end),
|
|
954
|
+
initialScope: { start: scopeStart, end: scopeEnd },
|
|
955
|
+
branchAvailable
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
|
|
787
959
|
// Update header with local info
|
|
788
960
|
this.updateLocalHeader(reviewData);
|
|
789
961
|
|
|
@@ -813,6 +985,7 @@ class LocalManager {
|
|
|
813
985
|
manager.analysisHistoryManager = new window.AnalysisHistoryManager({
|
|
814
986
|
reviewId: this.reviewId,
|
|
815
987
|
mode: 'local',
|
|
988
|
+
shaAbbrevLength: reviewData.shaAbbrevLength || 7,
|
|
816
989
|
onSelectionChange: (runId, _run) => {
|
|
817
990
|
manager.selectedRunId = runId;
|
|
818
991
|
manager.loadAISuggestions(null, runId);
|
|
@@ -837,6 +1010,9 @@ class LocalManager {
|
|
|
837
1010
|
window.prManager._initReviewEventListeners();
|
|
838
1011
|
}
|
|
839
1012
|
|
|
1013
|
+
// Fire-and-forget staleness check — shows badge or auto-refreshes
|
|
1014
|
+
manager._stalenessPromise = this._checkLocalStalenessOnLoad();
|
|
1015
|
+
|
|
840
1016
|
} catch (error) {
|
|
841
1017
|
console.error('Error loading local review:', error);
|
|
842
1018
|
manager.showError(error.message);
|
|
@@ -951,6 +1127,36 @@ class LocalManager {
|
|
|
951
1127
|
branchText.textContent = reviewData.branch || 'unknown';
|
|
952
1128
|
}
|
|
953
1129
|
|
|
1130
|
+
// Set descriptive tab title
|
|
1131
|
+
if (window.tabTitle && reviewData.branch) {
|
|
1132
|
+
window.tabTitle.setBase(reviewData.branch);
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
// Show base branch badge when branch is in scope
|
|
1136
|
+
const LS = window.LocalScope;
|
|
1137
|
+
const scopeStart = this.scopeStart || (LS ? LS.DEFAULT_SCOPE.start : 'unstaged');
|
|
1138
|
+
const scopeEnd = this.scopeEnd || (LS ? LS.DEFAULT_SCOPE.end : 'untracked');
|
|
1139
|
+
const hasBranch = LS ? LS.scopeIncludes(scopeStart, scopeEnd, 'branch') : false;
|
|
1140
|
+
|
|
1141
|
+
const branchVs = document.getElementById('local-branch-vs');
|
|
1142
|
+
const baseBranchEl = document.getElementById('local-base-branch');
|
|
1143
|
+
const baseBranchText = document.getElementById('local-base-branch-text');
|
|
1144
|
+
if (hasBranch && reviewData.baseBranch) {
|
|
1145
|
+
if (branchVs) branchVs.style.display = '';
|
|
1146
|
+
if (baseBranchEl) baseBranchEl.style.display = '';
|
|
1147
|
+
if (baseBranchText) baseBranchText.textContent = reviewData.baseBranch;
|
|
1148
|
+
} else {
|
|
1149
|
+
if (branchVs) branchVs.style.display = 'none';
|
|
1150
|
+
if (baseBranchEl) baseBranchEl.style.display = 'none';
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
// Update refresh button tooltip based on scope
|
|
1154
|
+
const refreshBtn = document.getElementById('local-refresh-btn');
|
|
1155
|
+
if (refreshBtn) {
|
|
1156
|
+
const scopeLabel = LS ? LS.scopeLabel(scopeStart, scopeEnd) : 'directory';
|
|
1157
|
+
refreshBtn.title = `Refresh diff (${scopeLabel})`;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
954
1160
|
// Update branch name (toolbar) and wire up copy button
|
|
955
1161
|
const branchName = document.getElementById('pr-branch-name');
|
|
956
1162
|
if (branchName) {
|
|
@@ -977,7 +1183,8 @@ class LocalManager {
|
|
|
977
1183
|
// Update commit SHA and wire up copy button
|
|
978
1184
|
const commitSha = document.getElementById('pr-commit-sha');
|
|
979
1185
|
if (commitSha && reviewData.localHeadSha) {
|
|
980
|
-
|
|
1186
|
+
const abbrevLen = reviewData.shaAbbrevLength || 7;
|
|
1187
|
+
commitSha.textContent = reviewData.localHeadSha.substring(0, abbrevLen);
|
|
981
1188
|
commitSha.dataset.fullSha = reviewData.localHeadSha;
|
|
982
1189
|
}
|
|
983
1190
|
|
|
@@ -1100,10 +1307,26 @@ class LocalManager {
|
|
|
1100
1307
|
const generatedFiles = new Set(data.generated_files || []);
|
|
1101
1308
|
|
|
1102
1309
|
if (!diffContent) {
|
|
1103
|
-
// Clear the diff container
|
|
1104
1310
|
const diffContainer = document.getElementById('diff-container');
|
|
1105
1311
|
if (diffContainer) {
|
|
1106
|
-
|
|
1312
|
+
const reviewData = this.localData;
|
|
1313
|
+
const branchInfo = reviewData?.branchInfo;
|
|
1314
|
+
const LS = window.LocalScope;
|
|
1315
|
+
const hasBranch = LS ? LS.scopeIncludes(this.scopeStart, this.scopeEnd, 'branch') : false;
|
|
1316
|
+
|
|
1317
|
+
// Show scope-aware empty message
|
|
1318
|
+
if (!hasBranch && branchInfo) {
|
|
1319
|
+
const scopeLabel = LS ? LS.scopeLabel(this.scopeStart, this.scopeEnd) : 'current scope';
|
|
1320
|
+
diffContainer.innerHTML = `<div class="no-diff">No changes in ${scopeLabel} scope.</div>`;
|
|
1321
|
+
} else {
|
|
1322
|
+
const scopeLabel = LS ? LS.scopeLabel(this.scopeStart, this.scopeEnd) : 'current scope';
|
|
1323
|
+
diffContainer.innerHTML = `<div class="no-diff">No changes in ${scopeLabel} scope. Change <strong>Diff scope</strong> or make some changes and click <strong>Refresh</strong> to reload.</div>`;
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// If branch has commits ahead and branch is not in scope, offer to expand
|
|
1327
|
+
if (!hasBranch && branchInfo) {
|
|
1328
|
+
this.showBranchReviewDialog(branchInfo);
|
|
1329
|
+
}
|
|
1107
1330
|
}
|
|
1108
1331
|
|
|
1109
1332
|
// Clear the file navigation sidebar
|
|
@@ -1204,6 +1427,271 @@ class LocalManager {
|
|
|
1204
1427
|
}
|
|
1205
1428
|
}
|
|
1206
1429
|
|
|
1430
|
+
/**
|
|
1431
|
+
* Apply the result of a scope-change POST to local state, UI, and diff.
|
|
1432
|
+
* Shared by _handleScopeChange and showBranchReviewDialog.handleConfirm.
|
|
1433
|
+
* @param {string} scopeStart - New start stop
|
|
1434
|
+
* @param {string} scopeEnd - New end stop
|
|
1435
|
+
* @param {Object} result - Response body from POST set-scope
|
|
1436
|
+
*/
|
|
1437
|
+
async _applyScopeResult(scopeStart, scopeEnd, result) {
|
|
1438
|
+
const manager = window.prManager;
|
|
1439
|
+
const LS = window.LocalScope;
|
|
1440
|
+
|
|
1441
|
+
// Update local state
|
|
1442
|
+
this.scopeStart = scopeStart;
|
|
1443
|
+
this.scopeEnd = scopeEnd;
|
|
1444
|
+
|
|
1445
|
+
// Update localData
|
|
1446
|
+
if (this.localData) {
|
|
1447
|
+
this.localData.scopeStart = scopeStart;
|
|
1448
|
+
this.localData.scopeEnd = scopeEnd;
|
|
1449
|
+
if (result.baseBranch) {
|
|
1450
|
+
this.localData.baseBranch = result.baseBranch;
|
|
1451
|
+
}
|
|
1452
|
+
if (result.localMode) {
|
|
1453
|
+
this.localData.localMode = result.localMode;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// Update currentPR
|
|
1458
|
+
const hasBranch = LS ? LS.includesBranch(scopeStart) : false;
|
|
1459
|
+
if (manager?.currentPR) {
|
|
1460
|
+
manager.currentPR.base_branch = hasBranch
|
|
1461
|
+
? (result.baseBranch || this.localData?.baseBranch || manager.currentPR.head_branch)
|
|
1462
|
+
: manager.currentPR.head_branch;
|
|
1463
|
+
manager.currentPR.title = hasBranch
|
|
1464
|
+
? `Branch Changes - ${manager.currentPR.head_branch} vs ${manager.currentPR.base_branch}`
|
|
1465
|
+
: `Local Changes - ${manager.currentPR.head_branch}`;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// Update header and reload diff
|
|
1469
|
+
this.updateLocalHeader(this.localData);
|
|
1470
|
+
await this.loadLocalDiff();
|
|
1471
|
+
|
|
1472
|
+
// Re-anchor comments and suggestions
|
|
1473
|
+
const includeDismissed = window.aiPanel?.showDismissedComments || false;
|
|
1474
|
+
await manager.loadUserComments(includeDismissed);
|
|
1475
|
+
await manager.loadAISuggestions(null, manager.selectedRunId);
|
|
1476
|
+
|
|
1477
|
+
// Only update dropdown if user hasn't clicked again since this request started
|
|
1478
|
+
if (manager?.diffOptionsDropdown) {
|
|
1479
|
+
const current = manager.diffOptionsDropdown.scope;
|
|
1480
|
+
if (current.start === scopeStart && current.end === scopeEnd) {
|
|
1481
|
+
manager.diffOptionsDropdown.clearScopeStatus();
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
/**
|
|
1487
|
+
* Handle scope change from DiffOptionsDropdown.
|
|
1488
|
+
* POSTs new scope to backend, reloads diff on success.
|
|
1489
|
+
* @param {string} scopeStart - New start stop
|
|
1490
|
+
* @param {string} scopeEnd - New end stop
|
|
1491
|
+
*/
|
|
1492
|
+
async _handleScopeChange(scopeStart, scopeEnd) {
|
|
1493
|
+
const manager = window.prManager;
|
|
1494
|
+
const LS = window.LocalScope;
|
|
1495
|
+
const oldStart = this.scopeStart;
|
|
1496
|
+
const oldEnd = this.scopeEnd;
|
|
1497
|
+
|
|
1498
|
+
try {
|
|
1499
|
+
const resp = await fetch(`/api/local/${this.reviewId}/set-scope`, {
|
|
1500
|
+
method: 'POST',
|
|
1501
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1502
|
+
body: JSON.stringify({ scopeStart, scopeEnd })
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
if (!resp.ok) {
|
|
1506
|
+
const err = await resp.json();
|
|
1507
|
+
throw new Error(err.error || 'Failed to set scope');
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
const result = await resp.json();
|
|
1511
|
+
await this._applyScopeResult(scopeStart, scopeEnd, result);
|
|
1512
|
+
|
|
1513
|
+
// Notify chat agent about scope change
|
|
1514
|
+
if (window.chatPanel) {
|
|
1515
|
+
const label = LS ? LS.scopeLabel(scopeStart, scopeEnd) : `${scopeStart}\u2013${scopeEnd}`;
|
|
1516
|
+
window.chatPanel.queueDiffStateNotification(
|
|
1517
|
+
`Diff scope changed to ${label}. The set of reviewed files has changed.`
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
if (window.toast) {
|
|
1522
|
+
const label = LS ? LS.scopeLabel(scopeStart, scopeEnd) : `${scopeStart}\u2013${scopeEnd}`;
|
|
1523
|
+
window.toast.showSuccess(`Scope: ${label}`);
|
|
1524
|
+
}
|
|
1525
|
+
} catch (error) {
|
|
1526
|
+
console.error('Failed to change scope:', error);
|
|
1527
|
+
if (window.toast) {
|
|
1528
|
+
window.toast.showError('Failed to change scope: ' + error.message);
|
|
1529
|
+
}
|
|
1530
|
+
// Rollback dropdown only if user hasn't clicked again
|
|
1531
|
+
if (manager?.diffOptionsDropdown) {
|
|
1532
|
+
const current = manager.diffOptionsDropdown.scope;
|
|
1533
|
+
if (current.start === scopeStart && current.end === scopeEnd) {
|
|
1534
|
+
manager.diffOptionsDropdown.scope = { start: oldStart, end: oldEnd };
|
|
1535
|
+
manager.diffOptionsDropdown.clearScopeStatus();
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
/**
|
|
1542
|
+
* Show a dialog prompting the user to review branch changes.
|
|
1543
|
+
* Uses the same modal pattern as ConfirmDialog/TextInputDialog.
|
|
1544
|
+
* @param {Object} branchInfo - Branch info with commitCount and baseBranch
|
|
1545
|
+
*/
|
|
1546
|
+
showBranchReviewDialog(branchInfo) {
|
|
1547
|
+
// Remove any existing branch review dialog
|
|
1548
|
+
const existing = document.getElementById('branch-review-dialog');
|
|
1549
|
+
if (existing) existing.remove();
|
|
1550
|
+
|
|
1551
|
+
const commitLabel = branchInfo.commitCount === 1 ? 'commit' : 'commits';
|
|
1552
|
+
|
|
1553
|
+
const overlay = document.createElement('div');
|
|
1554
|
+
overlay.id = 'branch-review-dialog';
|
|
1555
|
+
overlay.className = 'modal-overlay';
|
|
1556
|
+
overlay.style.display = 'flex';
|
|
1557
|
+
|
|
1558
|
+
overlay.innerHTML = `
|
|
1559
|
+
<div class="modal-backdrop" data-action="cancel"></div>
|
|
1560
|
+
<div class="modal-container confirm-dialog-container" style="width: 440px; height: auto;">
|
|
1561
|
+
<div class="modal-header">
|
|
1562
|
+
<h3>Branch Has Changes</h3>
|
|
1563
|
+
<button class="modal-close-btn" data-action="cancel" title="Close">
|
|
1564
|
+
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
|
|
1565
|
+
<path d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"/>
|
|
1566
|
+
</svg>
|
|
1567
|
+
</button>
|
|
1568
|
+
</div>
|
|
1569
|
+
|
|
1570
|
+
<div class="modal-body" style="padding: 16px 20px;">
|
|
1571
|
+
<p style="margin: 0 0 12px 0; font-size: 14px;">
|
|
1572
|
+
No uncommitted changes. This branch has <strong>${branchInfo.commitCount}</strong> ${commitLabel} ahead of <code style="padding: 2px 6px; background: var(--color-bg-tertiary); border-radius: 4px; font-size: 12px;">${branchInfo.baseBranch}</code>.
|
|
1573
|
+
</p>
|
|
1574
|
+
<label style="font-size: 12px; color: var(--color-text-tertiary); cursor: pointer; display: inline-flex; align-items: center; gap: 6px;">
|
|
1575
|
+
<input type="checkbox" id="branch-review-dont-ask" style="cursor: pointer;">
|
|
1576
|
+
Don't ask again for this repository
|
|
1577
|
+
</label>
|
|
1578
|
+
</div>
|
|
1579
|
+
|
|
1580
|
+
<div class="modal-footer">
|
|
1581
|
+
<button class="btn btn-secondary" data-action="cancel">Cancel</button>
|
|
1582
|
+
<button class="btn btn-primary" id="branch-review-confirm-btn" data-action="confirm">
|
|
1583
|
+
Expand Scope to Branch
|
|
1584
|
+
</button>
|
|
1585
|
+
</div>
|
|
1586
|
+
</div>
|
|
1587
|
+
`;
|
|
1588
|
+
|
|
1589
|
+
document.body.appendChild(overlay);
|
|
1590
|
+
|
|
1591
|
+
const reviewId = this.reviewId;
|
|
1592
|
+
const self = this;
|
|
1593
|
+
|
|
1594
|
+
const closeDialog = () => {
|
|
1595
|
+
overlay.style.display = 'none';
|
|
1596
|
+
overlay.remove();
|
|
1597
|
+
document.removeEventListener('keydown', keyHandler);
|
|
1598
|
+
};
|
|
1599
|
+
|
|
1600
|
+
const handleConfirm = async () => {
|
|
1601
|
+
const confirmBtn = overlay.querySelector('#branch-review-confirm-btn');
|
|
1602
|
+
if (confirmBtn) {
|
|
1603
|
+
confirmBtn.disabled = true;
|
|
1604
|
+
confirmBtn.textContent = 'Expanding...';
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
// Save "don't ask" preference if checked
|
|
1608
|
+
const dontAsk = overlay.querySelector('#branch-review-dont-ask');
|
|
1609
|
+
if (dontAsk?.checked) {
|
|
1610
|
+
try {
|
|
1611
|
+
await fetch(`/api/local/${reviewId}/branch-review-preference`, {
|
|
1612
|
+
method: 'POST',
|
|
1613
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1614
|
+
body: JSON.stringify({ preference: -1 })
|
|
1615
|
+
});
|
|
1616
|
+
} catch { /* non-fatal */ }
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
try {
|
|
1620
|
+
const LS = window.LocalScope;
|
|
1621
|
+
const newEnd = self.scopeEnd || (LS ? LS.DEFAULT_SCOPE.end : 'untracked');
|
|
1622
|
+
const resp = await fetch(`/api/local/${reviewId}/set-scope`, {
|
|
1623
|
+
method: 'POST',
|
|
1624
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1625
|
+
body: JSON.stringify({
|
|
1626
|
+
scopeStart: 'branch',
|
|
1627
|
+
scopeEnd: newEnd,
|
|
1628
|
+
baseBranch: branchInfo.baseBranch
|
|
1629
|
+
})
|
|
1630
|
+
});
|
|
1631
|
+
if (!resp.ok) {
|
|
1632
|
+
const err = await resp.json();
|
|
1633
|
+
throw new Error(err.error || 'Failed to expand scope');
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
const result = await resp.json();
|
|
1637
|
+
|
|
1638
|
+
// Update the dropdown branchAvailable flag
|
|
1639
|
+
const manager = window.prManager;
|
|
1640
|
+
if (manager?.diffOptionsDropdown) {
|
|
1641
|
+
manager.diffOptionsDropdown.branchAvailable = true;
|
|
1642
|
+
manager.diffOptionsDropdown.scope = { start: 'branch', end: newEnd };
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
closeDialog();
|
|
1646
|
+
|
|
1647
|
+
await self._applyScopeResult('branch', newEnd, result);
|
|
1648
|
+
|
|
1649
|
+
if (window.chatPanel) {
|
|
1650
|
+
const label = LS ? LS.scopeLabel('branch', newEnd) : 'branch';
|
|
1651
|
+
window.chatPanel.queueDiffStateNotification(
|
|
1652
|
+
`Diff scope changed to ${label} via branch review. The set of reviewed files has changed.`
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
if (window.toast) {
|
|
1657
|
+
const label = LS ? LS.scopeLabel('branch', newEnd) : 'Branch';
|
|
1658
|
+
window.toast.showSuccess(`Scope expanded to ${label}`);
|
|
1659
|
+
}
|
|
1660
|
+
} catch (error) {
|
|
1661
|
+
if (confirmBtn) {
|
|
1662
|
+
confirmBtn.disabled = false;
|
|
1663
|
+
confirmBtn.textContent = 'Expand Scope to Branch';
|
|
1664
|
+
}
|
|
1665
|
+
console.error('Failed to expand scope to branch:', error);
|
|
1666
|
+
if (window.toast) {
|
|
1667
|
+
window.toast.showError('Failed to expand scope: ' + error.message);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
};
|
|
1671
|
+
|
|
1672
|
+
// Event delegation for clicks
|
|
1673
|
+
overlay.addEventListener('click', (e) => {
|
|
1674
|
+
const action = e.target.closest('[data-action]')?.dataset.action;
|
|
1675
|
+
if (action === 'confirm') {
|
|
1676
|
+
handleConfirm();
|
|
1677
|
+
} else if (action === 'cancel') {
|
|
1678
|
+
closeDialog();
|
|
1679
|
+
}
|
|
1680
|
+
});
|
|
1681
|
+
|
|
1682
|
+
// Keyboard handler
|
|
1683
|
+
const keyHandler = (e) => {
|
|
1684
|
+
if (e.key === 'Escape') {
|
|
1685
|
+
closeDialog();
|
|
1686
|
+
} else if (e.key === 'Enter') {
|
|
1687
|
+
e.preventDefault();
|
|
1688
|
+
const btn = overlay.querySelector('#branch-review-confirm-btn');
|
|
1689
|
+
if (!btn?.disabled) handleConfirm();
|
|
1690
|
+
}
|
|
1691
|
+
};
|
|
1692
|
+
document.addEventListener('keydown', keyHandler);
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1207
1695
|
/**
|
|
1208
1696
|
* Get the progress dots container element
|
|
1209
1697
|
* @returns {HTMLElement|null}
|
|
@@ -1255,6 +1743,11 @@ class LocalManager {
|
|
|
1255
1743
|
}
|
|
1256
1744
|
|
|
1257
1745
|
// Initialize LocalManager when in local mode
|
|
1258
|
-
if (window.PAIR_REVIEW_LOCAL_MODE) {
|
|
1746
|
+
if (typeof window !== 'undefined' && window.PAIR_REVIEW_LOCAL_MODE) {
|
|
1259
1747
|
window.localManager = new LocalManager();
|
|
1260
1748
|
}
|
|
1749
|
+
|
|
1750
|
+
// Export for testing (Node.js environment)
|
|
1751
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
1752
|
+
module.exports = { LocalManager };
|
|
1753
|
+
}
|