@in-the-loop-labs/pair-review 2.6.2 → 2.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/bin/git-diff-lines +1 -1
- package/package.json +1 -1
- 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 +1 -1
- package/public/css/pr.css +201 -0
- package/public/index.html +168 -3
- package/public/js/components/AIPanel.js +16 -2
- package/public/js/components/ChatPanel.js +41 -6
- package/public/js/components/ConfirmDialog.js +21 -2
- package/public/js/components/CouncilProgressModal.js +13 -0
- package/public/js/components/DiffOptionsDropdown.js +410 -23
- package/public/js/components/SuggestionNavigator.js +12 -5
- package/public/js/components/TabTitle.js +96 -0
- package/public/js/components/Toast.js +6 -0
- package/public/js/index.js +648 -43
- package/public/js/local.js +569 -76
- package/public/js/modules/analysis-history.js +3 -2
- package/public/js/modules/comment-manager.js +5 -0
- package/public/js/modules/comment-minimizer.js +304 -0
- package/public/js/pr.js +82 -6
- package/public/local.html +14 -0
- package/public/pr.html +3 -0
- package/src/ai/analyzer.js +22 -16
- package/src/ai/cursor-agent-provider.js +21 -12
- package/src/chat/prompt-builder.js +3 -3
- package/src/config.js +2 -0
- package/src/database.js +590 -39
- package/src/git/base-branch.js +173 -0
- package/src/git/sha-abbrev.js +35 -0
- package/src/git/worktree.js +3 -2
- package/src/github/client.js +32 -1
- package/src/hooks/hook-runner.js +100 -0
- package/src/hooks/payloads.js +212 -0
- package/src/local-review.js +468 -129
- package/src/local-scope.js +58 -0
- package/src/main.js +57 -6
- package/src/routes/analyses.js +73 -10
- package/src/routes/chat.js +33 -0
- package/src/routes/config.js +1 -0
- package/src/routes/github-collections.js +2 -2
- package/src/routes/local.js +734 -68
- package/src/routes/mcp.js +20 -10
- package/src/routes/pr.js +92 -14
- package/src/routes/setup.js +1 -0
- package/src/routes/worktrees.js +212 -148
- package/src/server.js +30 -0
- package/src/setup/local-setup.js +46 -5
- package/src/setup/pr-setup.js +28 -5
- package/src/utils/diff-file-list.js +1 -1
|
@@ -133,10 +133,20 @@ class ConfirmDialog {
|
|
|
133
133
|
messageElement.textContent = options.message || 'Are you sure?';
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
// Helper: set button label + optional description subtitle
|
|
137
|
+
const setBtnContent = (btn, label, description) => {
|
|
138
|
+
if (!btn) return;
|
|
139
|
+
if (description) {
|
|
140
|
+
btn.innerHTML = `<span class="btn-label">${label}</span><span class="btn-desc">${description}</span>`;
|
|
141
|
+
} else {
|
|
142
|
+
btn.textContent = label;
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
136
146
|
// Set confirm button text and style
|
|
137
147
|
const confirmBtn = this.modal.querySelector('#confirm-dialog-btn');
|
|
138
148
|
if (confirmBtn) {
|
|
139
|
-
confirmBtn
|
|
149
|
+
setBtnContent(confirmBtn, options.confirmText || 'Confirm', options.confirmDesc);
|
|
140
150
|
// Remove previous style classes and add new one
|
|
141
151
|
confirmBtn.classList.remove('btn-primary', 'btn-secondary', 'btn-danger', 'btn-warning');
|
|
142
152
|
const confirmClass = options.confirmClass || 'btn-danger';
|
|
@@ -145,19 +155,28 @@ class ConfirmDialog {
|
|
|
145
155
|
|
|
146
156
|
// Set secondary button (optional 3rd button)
|
|
147
157
|
const secondaryBtn = this.modal.querySelector('#confirm-dialog-secondary-btn');
|
|
158
|
+
const container = this.modal.querySelector('.confirm-dialog-container');
|
|
148
159
|
if (secondaryBtn) {
|
|
149
160
|
if (options.secondaryText) {
|
|
150
|
-
secondaryBtn.
|
|
161
|
+
setBtnContent(secondaryBtn, options.secondaryText, options.secondaryDesc);
|
|
151
162
|
secondaryBtn.style.display = '';
|
|
152
163
|
// Remove previous style classes and add new one
|
|
153
164
|
secondaryBtn.classList.remove('btn-primary', 'btn-secondary', 'btn-danger', 'btn-warning');
|
|
154
165
|
const secondaryClass = options.secondaryClass || 'btn-secondary';
|
|
155
166
|
secondaryBtn.classList.add(secondaryClass);
|
|
167
|
+
if (container) container.classList.add('has-secondary');
|
|
156
168
|
} else {
|
|
157
169
|
secondaryBtn.style.display = 'none';
|
|
170
|
+
if (container) container.classList.remove('has-secondary');
|
|
158
171
|
}
|
|
159
172
|
}
|
|
160
173
|
|
|
174
|
+
// Set cancel button text (optional)
|
|
175
|
+
const cancelBtn = this.modal.querySelector('.modal-footer [data-action="cancel"]');
|
|
176
|
+
if (cancelBtn) {
|
|
177
|
+
setBtnContent(cancelBtn, options.cancelText || 'Cancel', options.cancelDesc);
|
|
178
|
+
}
|
|
179
|
+
|
|
161
180
|
// Store callbacks with promise resolution
|
|
162
181
|
this.onConfirm = () => {
|
|
163
182
|
if (options.onConfirm) {
|
|
@@ -806,6 +806,11 @@ class CouncilProgressModal {
|
|
|
806
806
|
window.prManager.setButtonComplete();
|
|
807
807
|
}
|
|
808
808
|
|
|
809
|
+
// Flash tab title
|
|
810
|
+
if (window.tabTitle) {
|
|
811
|
+
window.tabTitle.flashComplete();
|
|
812
|
+
}
|
|
813
|
+
|
|
809
814
|
// Reload suggestions
|
|
810
815
|
const manager = window.prManager || window.localManager;
|
|
811
816
|
if (manager && typeof manager.loadAISuggestions === 'function') {
|
|
@@ -884,6 +889,11 @@ class CouncilProgressModal {
|
|
|
884
889
|
if (window.prManager) {
|
|
885
890
|
window.prManager.resetButton();
|
|
886
891
|
}
|
|
892
|
+
|
|
893
|
+
// Flash tab title
|
|
894
|
+
if (window.tabTitle) {
|
|
895
|
+
window.tabTitle.flashFailed();
|
|
896
|
+
}
|
|
887
897
|
}
|
|
888
898
|
|
|
889
899
|
_handleCancellation(_status) {
|
|
@@ -924,6 +934,9 @@ class CouncilProgressModal {
|
|
|
924
934
|
if (window.prManager) {
|
|
925
935
|
window.prManager.resetButton();
|
|
926
936
|
}
|
|
937
|
+
|
|
938
|
+
// No tab-title flash for cancellation — the user initiated it, so no notification needed.
|
|
939
|
+
|
|
927
940
|
if (window.aiPanel?.setAnalysisState) {
|
|
928
941
|
window.aiPanel.setAnalysisState('unknown');
|
|
929
942
|
}
|
|
@@ -3,58 +3,95 @@
|
|
|
3
3
|
* DiffOptionsDropdown - Gear-icon popover for diff display options.
|
|
4
4
|
*
|
|
5
5
|
* Anchors a small dropdown below the gear button (#diff-options-btn) with
|
|
6
|
-
* checkbox toggles that control diff rendering.
|
|
7
|
-
*
|
|
6
|
+
* checkbox toggles that control diff rendering. Supports:
|
|
7
|
+
* - "Hide whitespace changes"
|
|
8
|
+
* - "Minimize comments" (collapse inline comments to line indicators)
|
|
9
|
+
* - Scope range selector (local mode only)
|
|
8
10
|
*
|
|
9
11
|
* Follows the same popover pattern used by PanelGroup._showPopover() /
|
|
10
12
|
* _hidePopover() (fixed positioning via getBoundingClientRect, click-outside
|
|
11
13
|
* and Escape to dismiss, opacity+transform animation).
|
|
12
14
|
*
|
|
15
|
+
* IMPORTANT: This dropdown is constructed in BOTH pr.js and local.js.
|
|
16
|
+
* LocalManager (local.js) destroys the pr.js instance and recreates it
|
|
17
|
+
* with scope-selector support. Any new callback added here MUST be
|
|
18
|
+
* threaded through both construction sites or it will silently no-op
|
|
19
|
+
* in local mode.
|
|
20
|
+
*
|
|
13
21
|
* Usage:
|
|
14
22
|
* const dropdown = new DiffOptionsDropdown(
|
|
15
23
|
* document.getElementById('diff-options-btn'),
|
|
16
|
-
* {
|
|
24
|
+
* {
|
|
25
|
+
* onToggleWhitespace: (hidden) => { … },
|
|
26
|
+
* onToggleMinimize: (minimized) => { … },
|
|
27
|
+
* onScopeChange: (start, end) => { … },
|
|
28
|
+
* initialScope: { start: 'unstaged', end: 'untracked' },
|
|
29
|
+
* branchAvailable: false
|
|
30
|
+
* }
|
|
17
31
|
* );
|
|
18
32
|
*/
|
|
19
33
|
|
|
20
34
|
const STORAGE_KEY = 'pair-review-hide-whitespace';
|
|
35
|
+
const MINIMIZE_STORAGE_KEY = 'pair-review-minimize-comments';
|
|
21
36
|
|
|
22
37
|
class DiffOptionsDropdown {
|
|
23
38
|
/**
|
|
24
39
|
* @param {HTMLElement} buttonElement - The gear icon button already in the DOM
|
|
25
40
|
* @param {Object} callbacks
|
|
26
41
|
* @param {function(boolean):void} callbacks.onToggleWhitespace
|
|
42
|
+
* @param {function(boolean):void} [callbacks.onToggleMinimize]
|
|
43
|
+
* @param {function(string,string):void} [callbacks.onScopeChange]
|
|
44
|
+
* @param {{start:string,end:string}} [callbacks.initialScope]
|
|
45
|
+
* @param {boolean} [callbacks.branchAvailable]
|
|
27
46
|
*/
|
|
28
|
-
constructor(buttonElement, { onToggleWhitespace }) {
|
|
47
|
+
constructor(buttonElement, { onToggleWhitespace, onToggleMinimize, onScopeChange, initialScope, branchAvailable }) {
|
|
29
48
|
this._btn = buttonElement;
|
|
30
49
|
this._onToggleWhitespace = onToggleWhitespace;
|
|
50
|
+
this._onToggleMinimize = onToggleMinimize || (() => {});
|
|
51
|
+
this._onScopeChange = onScopeChange || null;
|
|
31
52
|
|
|
32
53
|
this._popoverEl = null;
|
|
33
54
|
this._checkbox = null;
|
|
55
|
+
this._minimizeCheckbox = null;
|
|
34
56
|
this._visible = false;
|
|
35
57
|
this._outsideClickHandler = null;
|
|
36
58
|
this._escapeHandler = null;
|
|
37
59
|
|
|
60
|
+
// Scope state
|
|
61
|
+
const LS = window.LocalScope;
|
|
62
|
+
this._branchAvailable = Boolean(branchAvailable);
|
|
63
|
+
this._scopeStart = (initialScope && initialScope.start) || (LS ? LS.DEFAULT_SCOPE.start : 'unstaged');
|
|
64
|
+
this._scopeEnd = (initialScope && initialScope.end) || (LS ? LS.DEFAULT_SCOPE.end : 'untracked');
|
|
65
|
+
this._scopeStops = [];
|
|
66
|
+
this._scopeTrackEl = null;
|
|
67
|
+
this._scopeDebounceTimer = null;
|
|
68
|
+
this._scopeStatusEl = null;
|
|
69
|
+
|
|
38
70
|
// Read persisted state
|
|
39
71
|
this._hideWhitespace = localStorage.getItem(STORAGE_KEY) === 'true';
|
|
72
|
+
this._minimizeComments = localStorage.getItem(MINIMIZE_STORAGE_KEY) === 'true';
|
|
40
73
|
|
|
41
74
|
this._renderPopover();
|
|
42
75
|
this._syncButtonActive();
|
|
43
76
|
|
|
44
77
|
// Toggle popover on button click
|
|
45
|
-
this.
|
|
78
|
+
this._btnClickHandler = (e) => {
|
|
46
79
|
e.stopPropagation();
|
|
47
80
|
if (this._visible) {
|
|
48
81
|
this._hide();
|
|
49
82
|
} else {
|
|
50
83
|
this._show();
|
|
51
84
|
}
|
|
52
|
-
}
|
|
85
|
+
};
|
|
86
|
+
this._btn.addEventListener('click', this._btnClickHandler);
|
|
53
87
|
|
|
54
|
-
// Fire initial
|
|
88
|
+
// Fire initial callbacks so the consumer can apply persisted state
|
|
55
89
|
if (this._hideWhitespace) {
|
|
56
90
|
this._onToggleWhitespace(true);
|
|
57
91
|
}
|
|
92
|
+
if (this._minimizeComments) {
|
|
93
|
+
this._onToggleMinimize(true);
|
|
94
|
+
}
|
|
58
95
|
}
|
|
59
96
|
|
|
60
97
|
// ---------------------------------------------------------------------------
|
|
@@ -77,6 +114,65 @@ class DiffOptionsDropdown {
|
|
|
77
114
|
this._onToggleWhitespace(bool);
|
|
78
115
|
}
|
|
79
116
|
|
|
117
|
+
/** @returns {boolean} Whether comments are currently minimized */
|
|
118
|
+
get minimizeComments() {
|
|
119
|
+
return this._minimizeComments;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Programmatically set the minimize toggle (updates UI + storage). */
|
|
123
|
+
set minimizeComments(value) {
|
|
124
|
+
const bool = Boolean(value);
|
|
125
|
+
if (bool === this._minimizeComments) return;
|
|
126
|
+
this._minimizeComments = bool;
|
|
127
|
+
if (this._minimizeCheckbox) this._minimizeCheckbox.checked = bool;
|
|
128
|
+
this._persist();
|
|
129
|
+
this._syncButtonActive();
|
|
130
|
+
this._onToggleMinimize(bool);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Update branch availability (e.g. after base branch is set). */
|
|
134
|
+
set branchAvailable(value) {
|
|
135
|
+
this._branchAvailable = Boolean(value);
|
|
136
|
+
this._updateScopeUI();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Get current scope as {start, end}. */
|
|
140
|
+
get scope() {
|
|
141
|
+
return { start: this._scopeStart, end: this._scopeEnd };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Programmatically set scope. */
|
|
145
|
+
set scope(val) {
|
|
146
|
+
if (!val) return;
|
|
147
|
+
const LS = window.LocalScope;
|
|
148
|
+
if (LS && !LS.isValidScope(val.start, val.end)) return;
|
|
149
|
+
this._scopeStart = val.start;
|
|
150
|
+
this._scopeEnd = val.end;
|
|
151
|
+
this._updateScopeUI();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Clear the scope status indicator (call after scope change completes). */
|
|
155
|
+
clearScopeStatus() {
|
|
156
|
+
if (this._scopeStatusEl) {
|
|
157
|
+
this._scopeStatusEl.style.display = 'none';
|
|
158
|
+
this._scopeStatusEl.textContent = '';
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Remove all DOM elements and event listeners. Safe to call multiple times. */
|
|
163
|
+
destroy() {
|
|
164
|
+
this._hide();
|
|
165
|
+
clearTimeout(this._scopeDebounceTimer);
|
|
166
|
+
if (this._popoverEl) {
|
|
167
|
+
this._popoverEl.remove();
|
|
168
|
+
this._popoverEl = null;
|
|
169
|
+
}
|
|
170
|
+
if (this._btn && this._btnClickHandler) {
|
|
171
|
+
this._btn.removeEventListener('click', this._btnClickHandler);
|
|
172
|
+
this._btnClickHandler = null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
80
176
|
// ---------------------------------------------------------------------------
|
|
81
177
|
// DOM construction
|
|
82
178
|
// ---------------------------------------------------------------------------
|
|
@@ -92,7 +188,57 @@ class DiffOptionsDropdown {
|
|
|
92
188
|
popover.style.zIndex = '1100';
|
|
93
189
|
popover.style.transition = 'opacity 0.15s ease, transform 0.15s ease';
|
|
94
190
|
|
|
95
|
-
//
|
|
191
|
+
// Scope selector first — only in local mode
|
|
192
|
+
if (window.PAIR_REVIEW_LOCAL_MODE && window.LocalScope) {
|
|
193
|
+
this._renderScopeSelector(popover);
|
|
194
|
+
|
|
195
|
+
// Divider between scope selector and whitespace checkbox
|
|
196
|
+
const divider = document.createElement('div');
|
|
197
|
+
divider.style.height = '1px';
|
|
198
|
+
divider.style.background = 'var(--color-border-primary, #d0d7de)';
|
|
199
|
+
divider.style.margin = '0 20px';
|
|
200
|
+
popover.appendChild(divider);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// --- Whitespace checkbox ---
|
|
204
|
+
const wsLabel = this._createCheckboxLabel('Hide whitespace changes', this._hideWhitespace);
|
|
205
|
+
const wsCheckbox = wsLabel.querySelector('input');
|
|
206
|
+
popover.appendChild(wsLabel);
|
|
207
|
+
|
|
208
|
+
// --- Minimize comments checkbox ---
|
|
209
|
+
const minLabel = this._createCheckboxLabel('Minimize comments', this._minimizeComments);
|
|
210
|
+
const minCheckbox = minLabel.querySelector('input');
|
|
211
|
+
popover.appendChild(minLabel);
|
|
212
|
+
|
|
213
|
+
document.body.appendChild(popover);
|
|
214
|
+
|
|
215
|
+
this._popoverEl = popover;
|
|
216
|
+
this._checkbox = wsCheckbox;
|
|
217
|
+
this._minimizeCheckbox = minCheckbox;
|
|
218
|
+
|
|
219
|
+
// Respond to checkbox changes
|
|
220
|
+
wsCheckbox.addEventListener('change', () => {
|
|
221
|
+
this._hideWhitespace = wsCheckbox.checked;
|
|
222
|
+
this._persist();
|
|
223
|
+
this._syncButtonActive();
|
|
224
|
+
this._onToggleWhitespace(this._hideWhitespace);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
minCheckbox.addEventListener('change', () => {
|
|
228
|
+
this._minimizeComments = minCheckbox.checked;
|
|
229
|
+
this._persist();
|
|
230
|
+
this._syncButtonActive();
|
|
231
|
+
this._onToggleMinimize(this._minimizeComments);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Create a label element wrapping a checkbox.
|
|
237
|
+
* @param {string} text - Label text
|
|
238
|
+
* @param {boolean} checked - Initial checked state
|
|
239
|
+
* @returns {HTMLLabelElement}
|
|
240
|
+
*/
|
|
241
|
+
_createCheckboxLabel(text, checked) {
|
|
96
242
|
const label = document.createElement('label');
|
|
97
243
|
label.style.display = 'flex';
|
|
98
244
|
label.style.alignItems = 'center';
|
|
@@ -105,28 +251,268 @@ class DiffOptionsDropdown {
|
|
|
105
251
|
|
|
106
252
|
const checkbox = document.createElement('input');
|
|
107
253
|
checkbox.type = 'checkbox';
|
|
108
|
-
checkbox.checked =
|
|
254
|
+
checkbox.checked = checked;
|
|
109
255
|
checkbox.style.margin = '0';
|
|
110
256
|
checkbox.style.cursor = 'pointer';
|
|
111
257
|
|
|
112
|
-
const text = document.createTextNode('Hide whitespace changes');
|
|
113
|
-
|
|
114
258
|
label.appendChild(checkbox);
|
|
115
|
-
label.appendChild(text);
|
|
116
|
-
|
|
259
|
+
label.appendChild(document.createTextNode(text));
|
|
260
|
+
return label;
|
|
261
|
+
}
|
|
117
262
|
|
|
118
|
-
|
|
263
|
+
_renderScopeSelector(popover) {
|
|
264
|
+
const LS = window.LocalScope;
|
|
265
|
+
|
|
266
|
+
// Section container — generous horizontal padding so dots/labels breathe
|
|
267
|
+
const section = document.createElement('div');
|
|
268
|
+
section.style.padding = '8px 20px 12px';
|
|
269
|
+
section.className = 'scope-selector-section';
|
|
270
|
+
|
|
271
|
+
// Title row with status indicator
|
|
272
|
+
const titleRow = document.createElement('div');
|
|
273
|
+
titleRow.style.display = 'flex';
|
|
274
|
+
titleRow.style.alignItems = 'center';
|
|
275
|
+
titleRow.style.justifyContent = 'space-between';
|
|
276
|
+
titleRow.style.marginBottom = '10px';
|
|
277
|
+
|
|
278
|
+
const title = document.createElement('div');
|
|
279
|
+
title.textContent = 'Diff scope';
|
|
280
|
+
title.style.fontSize = '0.8125rem';
|
|
281
|
+
title.style.fontWeight = '600';
|
|
282
|
+
title.style.color = 'var(--color-text-primary, #24292f)';
|
|
283
|
+
|
|
284
|
+
const statusEl = document.createElement('div');
|
|
285
|
+
statusEl.style.fontSize = '11px';
|
|
286
|
+
statusEl.style.color = 'var(--color-text-secondary, #656d76)';
|
|
287
|
+
statusEl.style.display = 'none';
|
|
288
|
+
this._scopeStatusEl = statusEl;
|
|
289
|
+
|
|
290
|
+
titleRow.appendChild(title);
|
|
291
|
+
titleRow.appendChild(statusEl);
|
|
292
|
+
section.appendChild(titleRow);
|
|
293
|
+
|
|
294
|
+
// Track container
|
|
295
|
+
const trackContainer = document.createElement('div');
|
|
296
|
+
trackContainer.style.position = 'relative';
|
|
297
|
+
trackContainer.style.padding = '0';
|
|
298
|
+
this._numStops = LS.STOPS.length;
|
|
299
|
+
|
|
300
|
+
// Track background line — spans between centers of first and last columns.
|
|
301
|
+
// With N equal flex columns each 1/N wide, first center is at 1/(2N) and
|
|
302
|
+
// last center is at 1 - 1/(2N), so the line inset is 100%/(2N) on each side.
|
|
303
|
+
const trackInset = `calc(100% / ${this._numStops * 2})`;
|
|
304
|
+
const trackLine = document.createElement('div');
|
|
305
|
+
trackLine.style.position = 'absolute';
|
|
306
|
+
trackLine.style.top = '6px';
|
|
307
|
+
trackLine.style.left = trackInset;
|
|
308
|
+
trackLine.style.right = trackInset;
|
|
309
|
+
trackLine.style.height = '2px';
|
|
310
|
+
trackLine.style.background = 'var(--color-border-primary, #d0d7de)';
|
|
311
|
+
trackLine.style.borderRadius = '1px';
|
|
312
|
+
trackContainer.appendChild(trackLine);
|
|
313
|
+
|
|
314
|
+
// Highlighted range bar — positioned relative to the track line span
|
|
315
|
+
const rangeBar = document.createElement('div');
|
|
316
|
+
rangeBar.style.position = 'absolute';
|
|
317
|
+
rangeBar.style.top = '6px';
|
|
318
|
+
rangeBar.style.height = '2px';
|
|
319
|
+
rangeBar.style.background = 'var(--ai-primary, #8b5cf6)';
|
|
320
|
+
rangeBar.style.borderRadius = '1px';
|
|
321
|
+
rangeBar.style.transition = 'left 0.15s ease, width 0.15s ease';
|
|
322
|
+
trackContainer.appendChild(rangeBar);
|
|
323
|
+
this._rangeBarEl = rangeBar;
|
|
324
|
+
|
|
325
|
+
// Stops row — equal-width columns so dots are evenly spaced
|
|
326
|
+
const stopsRow = document.createElement('div');
|
|
327
|
+
stopsRow.style.display = 'flex';
|
|
328
|
+
stopsRow.style.position = 'relative';
|
|
329
|
+
|
|
330
|
+
this._scopeStops = [];
|
|
331
|
+
|
|
332
|
+
LS.STOPS.forEach((stop, i) => {
|
|
333
|
+
const stopEl = document.createElement('div');
|
|
334
|
+
stopEl.style.display = 'flex';
|
|
335
|
+
stopEl.style.flexDirection = 'column';
|
|
336
|
+
stopEl.style.alignItems = 'center';
|
|
337
|
+
stopEl.style.cursor = 'pointer';
|
|
338
|
+
stopEl.style.userSelect = 'none';
|
|
339
|
+
stopEl.style.flex = '1';
|
|
340
|
+
stopEl.dataset.stop = stop;
|
|
341
|
+
|
|
342
|
+
const dot = document.createElement('div');
|
|
343
|
+
dot.style.width = '14px';
|
|
344
|
+
dot.style.height = '14px';
|
|
345
|
+
dot.style.borderRadius = '50%';
|
|
346
|
+
dot.style.border = '2px solid var(--color-border-primary, #d0d7de)';
|
|
347
|
+
dot.style.boxSizing = 'border-box';
|
|
348
|
+
dot.style.background = 'var(--color-bg-primary, #ffffff)';
|
|
349
|
+
dot.style.transition = 'background 0.15s ease, border-color 0.15s ease, box-shadow 0.15s ease';
|
|
350
|
+
dot.style.position = 'relative';
|
|
351
|
+
dot.style.zIndex = '1';
|
|
352
|
+
dot.style.marginBottom = '6px';
|
|
353
|
+
|
|
354
|
+
const labelEl = document.createElement('div');
|
|
355
|
+
labelEl.textContent = stop.charAt(0).toUpperCase() + stop.slice(1);
|
|
356
|
+
labelEl.style.fontSize = '11px';
|
|
357
|
+
labelEl.style.lineHeight = '1.2';
|
|
358
|
+
labelEl.style.color = 'var(--color-text-secondary, #656d76)';
|
|
359
|
+
labelEl.style.whiteSpace = 'nowrap';
|
|
360
|
+
labelEl.style.transition = 'color 0.15s ease';
|
|
361
|
+
|
|
362
|
+
stopEl.appendChild(dot);
|
|
363
|
+
stopEl.appendChild(labelEl);
|
|
364
|
+
|
|
365
|
+
stopEl.addEventListener('click', (e) => {
|
|
366
|
+
e.stopPropagation();
|
|
367
|
+
this._handleStopClick(stop, e);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
stopsRow.appendChild(stopEl);
|
|
371
|
+
this._scopeStops.push({ stop, dotEl: dot, labelEl, containerEl: stopEl });
|
|
372
|
+
});
|
|
119
373
|
|
|
120
|
-
|
|
121
|
-
|
|
374
|
+
trackContainer.appendChild(stopsRow);
|
|
375
|
+
section.appendChild(trackContainer);
|
|
122
376
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
377
|
+
this._scopeTrackEl = trackContainer;
|
|
378
|
+
popover.appendChild(section);
|
|
379
|
+
|
|
380
|
+
// Initial UI sync
|
|
381
|
+
this._updateScopeUI();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
_handleStopClick(clickedStop, event) {
|
|
385
|
+
const LS = window.LocalScope;
|
|
386
|
+
if (!LS) return;
|
|
387
|
+
|
|
388
|
+
// Branch disabled? Ignore.
|
|
389
|
+
if (clickedStop === 'branch' && !this._branchAvailable) return;
|
|
390
|
+
|
|
391
|
+
const stops = LS.STOPS;
|
|
392
|
+
const ci = stops.indexOf(clickedStop);
|
|
393
|
+
const si = stops.indexOf(this._scopeStart);
|
|
394
|
+
const ei = stops.indexOf(this._scopeEnd);
|
|
395
|
+
|
|
396
|
+
let newStart = this._scopeStart;
|
|
397
|
+
let newEnd = this._scopeEnd;
|
|
398
|
+
|
|
399
|
+
// Alt/Option-click: solo-select this single stop
|
|
400
|
+
if (event && event.altKey) {
|
|
401
|
+
newStart = clickedStop;
|
|
402
|
+
newEnd = clickedStop;
|
|
403
|
+
} else {
|
|
404
|
+
// Checkbox-like toggle with contiguity constraint
|
|
405
|
+
const included = ci >= si && ci <= ei;
|
|
406
|
+
|
|
407
|
+
if (included) {
|
|
408
|
+
// Toggling OFF — only allowed at boundaries, and range must have >1 stop
|
|
409
|
+
if (si === ei) return;
|
|
410
|
+
if (ci === si) {
|
|
411
|
+
newStart = stops[si + 1];
|
|
412
|
+
} else if (ci === ei) {
|
|
413
|
+
newEnd = stops[ei - 1];
|
|
414
|
+
} else {
|
|
415
|
+
return; // Interior — can't break contiguity
|
|
416
|
+
}
|
|
417
|
+
} else {
|
|
418
|
+
// Toggling ON — only allowed if adjacent to current range
|
|
419
|
+
if (ci === si - 1) {
|
|
420
|
+
newStart = clickedStop;
|
|
421
|
+
} else if (ci === ei + 1) {
|
|
422
|
+
newEnd = clickedStop;
|
|
423
|
+
} else {
|
|
424
|
+
return; // Not adjacent — can't break contiguity
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// If branch not available and start would be branch, clamp
|
|
430
|
+
if (!this._branchAvailable && newStart === 'branch') {
|
|
431
|
+
newStart = 'staged';
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (!LS.isValidScope(newStart, newEnd)) return;
|
|
435
|
+
if (newStart === this._scopeStart && newEnd === this._scopeEnd) return;
|
|
436
|
+
|
|
437
|
+
this._scopeStart = newStart;
|
|
438
|
+
this._scopeEnd = newEnd;
|
|
439
|
+
this._updateScopeUI();
|
|
440
|
+
|
|
441
|
+
// Show pending status and debounce the backend call
|
|
442
|
+
this._setScopeStatus('Updating\u2026');
|
|
443
|
+
|
|
444
|
+
clearTimeout(this._scopeDebounceTimer);
|
|
445
|
+
this._scopeDebounceTimer = setTimeout(() => {
|
|
446
|
+
this._setScopeStatus('Loading diff\u2026');
|
|
447
|
+
if (this._onScopeChange) {
|
|
448
|
+
this._onScopeChange(this._scopeStart, this._scopeEnd);
|
|
449
|
+
}
|
|
450
|
+
}, 600);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
_setScopeStatus(text) {
|
|
454
|
+
if (!this._scopeStatusEl) return;
|
|
455
|
+
this._scopeStatusEl.textContent = text;
|
|
456
|
+
this._scopeStatusEl.style.display = text ? 'block' : 'none';
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
_updateScopeUI() {
|
|
460
|
+
const LS = window.LocalScope;
|
|
461
|
+
if (!LS || !this._scopeStops.length) return;
|
|
462
|
+
|
|
463
|
+
const stops = LS.STOPS;
|
|
464
|
+
const si = stops.indexOf(this._scopeStart);
|
|
465
|
+
const ei = stops.indexOf(this._scopeEnd);
|
|
466
|
+
|
|
467
|
+
this._scopeStops.forEach(({ stop, dotEl, labelEl, containerEl }, i) => {
|
|
468
|
+
const included = LS.scopeIncludes(this._scopeStart, this._scopeEnd, stop);
|
|
469
|
+
const isBranch = stop === 'branch';
|
|
470
|
+
const disabled = isBranch && !this._branchAvailable;
|
|
471
|
+
|
|
472
|
+
// Determine if clicking this stop would do anything (for cursor hint)
|
|
473
|
+
const isBoundary = included && (i === si || i === ei) && si !== ei;
|
|
474
|
+
const isAdjacent = !included && (i === si - 1 || i === ei + 1);
|
|
475
|
+
const clickable = !disabled && (isBoundary || isAdjacent);
|
|
476
|
+
|
|
477
|
+
if (disabled) {
|
|
478
|
+
// Disabled state
|
|
479
|
+
dotEl.style.background = 'var(--color-bg-tertiary, #f6f8fa)';
|
|
480
|
+
dotEl.style.borderColor = 'var(--color-border-secondary, #e1e4e8)';
|
|
481
|
+
dotEl.style.boxShadow = 'none';
|
|
482
|
+
labelEl.style.color = 'var(--color-text-tertiary, #8b949e)';
|
|
483
|
+
containerEl.style.cursor = 'not-allowed';
|
|
484
|
+
containerEl.style.opacity = '0.5';
|
|
485
|
+
} else if (included) {
|
|
486
|
+
// Included (filled) state
|
|
487
|
+
dotEl.style.background = 'var(--ai-primary, #8b5cf6)';
|
|
488
|
+
dotEl.style.borderColor = 'var(--ai-primary, #8b5cf6)';
|
|
489
|
+
dotEl.style.boxShadow = '0 0 0 2px rgba(139, 92, 246, 0.2)';
|
|
490
|
+
labelEl.style.color = 'var(--color-text-primary, #24292f)';
|
|
491
|
+
labelEl.style.fontWeight = '600';
|
|
492
|
+
containerEl.style.cursor = clickable ? 'pointer' : 'default';
|
|
493
|
+
containerEl.style.opacity = '1';
|
|
494
|
+
} else {
|
|
495
|
+
// Excluded (empty) state
|
|
496
|
+
dotEl.style.background = 'var(--color-bg-primary, #ffffff)';
|
|
497
|
+
dotEl.style.borderColor = 'var(--color-border-primary, #d0d7de)';
|
|
498
|
+
dotEl.style.boxShadow = 'none';
|
|
499
|
+
labelEl.style.color = 'var(--color-text-secondary, #656d76)';
|
|
500
|
+
labelEl.style.fontWeight = 'normal';
|
|
501
|
+
containerEl.style.cursor = clickable ? 'pointer' : 'default';
|
|
502
|
+
containerEl.style.opacity = clickable ? '1' : '0.6';
|
|
503
|
+
}
|
|
129
504
|
});
|
|
505
|
+
|
|
506
|
+
// Update range bar position.
|
|
507
|
+
// With N equal flex columns, center of column i = (2*i + 1) / (2*N).
|
|
508
|
+
// Range bar spans from center of start column to center of end column.
|
|
509
|
+
if (this._rangeBarEl && this._scopeStops.length >= 2) {
|
|
510
|
+
const N = stops.length;
|
|
511
|
+
const startCenter = (2 * si + 1) / (2 * N);
|
|
512
|
+
const endCenter = (2 * ei + 1) / (2 * N);
|
|
513
|
+
this._rangeBarEl.style.left = `calc(100% * ${startCenter})`;
|
|
514
|
+
this._rangeBarEl.style.width = `calc(100% * ${endCenter - startCenter})`;
|
|
515
|
+
}
|
|
130
516
|
}
|
|
131
517
|
|
|
132
518
|
// ---------------------------------------------------------------------------
|
|
@@ -195,12 +581,13 @@ class DiffOptionsDropdown {
|
|
|
195
581
|
|
|
196
582
|
_persist() {
|
|
197
583
|
localStorage.setItem(STORAGE_KEY, String(this._hideWhitespace));
|
|
584
|
+
localStorage.setItem(MINIMIZE_STORAGE_KEY, String(this._minimizeComments));
|
|
198
585
|
}
|
|
199
586
|
|
|
200
587
|
/** Add/remove `.active` on the gear button as a visual cue that filtering is on. */
|
|
201
588
|
_syncButtonActive() {
|
|
202
589
|
if (!this._btn) return;
|
|
203
|
-
this._btn.classList.toggle('active', this._hideWhitespace);
|
|
590
|
+
this._btn.classList.toggle('active', this._hideWhitespace || this._minimizeComments);
|
|
204
591
|
}
|
|
205
592
|
}
|
|
206
593
|
|
|
@@ -369,11 +369,18 @@ class SuggestionNavigator {
|
|
|
369
369
|
const suggestionEl = document.querySelector(`[data-suggestion-id="${currentSuggestion.id}"]`);
|
|
370
370
|
|
|
371
371
|
if (suggestionEl) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
372
|
+
const minimizer = window.prManager?.commentMinimizer;
|
|
373
|
+
if (minimizer?.active) {
|
|
374
|
+
// Comments are minimized — scroll to the parent diff line instead
|
|
375
|
+
const diffRow = minimizer.findDiffRowFor(suggestionEl);
|
|
376
|
+
if (diffRow) {
|
|
377
|
+
diffRow.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
|
|
378
|
+
} else {
|
|
379
|
+
suggestionEl.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
|
|
380
|
+
}
|
|
381
|
+
} else {
|
|
382
|
+
suggestionEl.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
|
|
383
|
+
}
|
|
377
384
|
}
|
|
378
385
|
}
|
|
379
386
|
}
|