@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
|
@@ -1,60 +1,97 @@
|
|
|
1
|
-
// SPDX-License-Identifier:
|
|
1
|
+
// Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
/**
|
|
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
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// SPDX-License-Identifier:
|
|
1
|
+
// Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
/**
|
|
3
3
|
* AI Suggestion Navigation Sidebar Component
|
|
4
4
|
* Shows navigation controls and list of all AI suggestions
|
|
@@ -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
|
}
|