@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.
Files changed (150) hide show
  1. package/.pi/extensions/task/index.ts +1 -1
  2. package/.pi/skills/review-roulette/SKILL.md +1 -1
  3. package/LICENSE +201 -674
  4. package/README.md +2 -2
  5. package/bin/pair-review.js +1 -1
  6. package/package.json +2 -2
  7. package/plugin/.claude-plugin/plugin.json +2 -2
  8. package/plugin-code-critic/.claude-plugin/plugin.json +2 -2
  9. package/plugin-code-critic/skills/analyze/scripts/git-diff-lines +1 -1
  10. package/public/css/ai-summary-modal.css +1 -1
  11. package/public/css/pr.css +194 -0
  12. package/public/index.html +168 -3
  13. package/public/js/components/AIPanel.js +17 -3
  14. package/public/js/components/AISummaryModal.js +1 -1
  15. package/public/js/components/AdvancedConfigTab.js +1 -1
  16. package/public/js/components/AnalysisConfigModal.js +1 -1
  17. package/public/js/components/ChatPanel.js +42 -7
  18. package/public/js/components/ConfirmDialog.js +22 -3
  19. package/public/js/components/CouncilProgressModal.js +14 -1
  20. package/public/js/components/DiffOptionsDropdown.js +411 -24
  21. package/public/js/components/EmojiPicker.js +1 -1
  22. package/public/js/components/KeyboardShortcuts.js +1 -1
  23. package/public/js/components/PanelGroup.js +1 -1
  24. package/public/js/components/PreviewModal.js +1 -1
  25. package/public/js/components/ReviewModal.js +1 -1
  26. package/public/js/components/SplitButton.js +1 -1
  27. package/public/js/components/StatusIndicator.js +1 -1
  28. package/public/js/components/SuggestionNavigator.js +13 -6
  29. package/public/js/components/TabTitle.js +96 -0
  30. package/public/js/components/TextInputDialog.js +1 -1
  31. package/public/js/components/TimeoutSelect.js +1 -1
  32. package/public/js/components/Toast.js +7 -1
  33. package/public/js/components/VoiceCentricConfigTab.js +1 -1
  34. package/public/js/index.js +649 -44
  35. package/public/js/local.js +570 -77
  36. package/public/js/modules/analysis-history.js +4 -3
  37. package/public/js/modules/comment-manager.js +6 -1
  38. package/public/js/modules/comment-minimizer.js +304 -0
  39. package/public/js/modules/diff-context.js +1 -1
  40. package/public/js/modules/diff-renderer.js +1 -1
  41. package/public/js/modules/file-comment-manager.js +1 -1
  42. package/public/js/modules/file-list-merger.js +1 -1
  43. package/public/js/modules/gap-coordinates.js +1 -1
  44. package/public/js/modules/hunk-parser.js +1 -1
  45. package/public/js/modules/line-tracker.js +1 -1
  46. package/public/js/modules/panel-resizer.js +1 -1
  47. package/public/js/modules/storage-cleanup.js +1 -1
  48. package/public/js/modules/suggestion-manager.js +1 -1
  49. package/public/js/pr.js +83 -7
  50. package/public/js/repo-settings.js +1 -1
  51. package/public/js/utils/category-emoji.js +1 -1
  52. package/public/js/utils/file-order.js +1 -1
  53. package/public/js/utils/markdown.js +1 -1
  54. package/public/js/utils/suggestion-ui.js +1 -1
  55. package/public/js/utils/tier-icons.js +1 -1
  56. package/public/js/utils/time.js +1 -1
  57. package/public/js/ws-client.js +1 -1
  58. package/public/local.html +14 -0
  59. package/public/pr.html +3 -0
  60. package/public/setup.html +1 -1
  61. package/src/ai/analyzer.js +18 -12
  62. package/src/ai/claude-cli.js +1 -1
  63. package/src/ai/claude-provider.js +1 -1
  64. package/src/ai/codex-provider.js +1 -1
  65. package/src/ai/copilot-provider.js +1 -1
  66. package/src/ai/cursor-agent-provider.js +1 -1
  67. package/src/ai/gemini-provider.js +1 -1
  68. package/src/ai/index.js +1 -1
  69. package/src/ai/opencode-provider.js +1 -1
  70. package/src/ai/pi-provider.js +1 -1
  71. package/src/ai/prompts/baseline/consolidation/balanced.js +1 -1
  72. package/src/ai/prompts/baseline/consolidation/fast.js +1 -1
  73. package/src/ai/prompts/baseline/consolidation/thorough.js +1 -1
  74. package/src/ai/prompts/baseline/level1/balanced.js +1 -1
  75. package/src/ai/prompts/baseline/level1/fast.js +1 -1
  76. package/src/ai/prompts/baseline/level1/thorough.js +1 -1
  77. package/src/ai/prompts/baseline/level2/balanced.js +1 -1
  78. package/src/ai/prompts/baseline/level2/fast.js +1 -1
  79. package/src/ai/prompts/baseline/level2/thorough.js +1 -1
  80. package/src/ai/prompts/baseline/level3/balanced.js +1 -1
  81. package/src/ai/prompts/baseline/level3/fast.js +1 -1
  82. package/src/ai/prompts/baseline/level3/thorough.js +1 -1
  83. package/src/ai/prompts/baseline/orchestration/balanced.js +1 -1
  84. package/src/ai/prompts/baseline/orchestration/fast.js +1 -1
  85. package/src/ai/prompts/baseline/orchestration/thorough.js +1 -1
  86. package/src/ai/prompts/config.js +1 -1
  87. package/src/ai/prompts/index.js +1 -1
  88. package/src/ai/prompts/line-number-guidance.js +1 -1
  89. package/src/ai/prompts/render-for-skill.js +1 -1
  90. package/src/ai/prompts/shared/diff-instructions.js +1 -1
  91. package/src/ai/prompts/shared/output-schema.js +1 -1
  92. package/src/ai/prompts/shared/valid-files.js +1 -1
  93. package/src/ai/prompts/sparse-checkout-guidance.js +1 -1
  94. package/src/ai/provider-availability.js +1 -1
  95. package/src/ai/provider.js +1 -1
  96. package/src/ai/stream-parser.js +1 -1
  97. package/src/chat/acp-bridge.js +1 -1
  98. package/src/chat/api-reference.js +1 -1
  99. package/src/chat/chat-providers.js +1 -1
  100. package/src/chat/claude-code-bridge.js +1 -1
  101. package/src/chat/codex-bridge.js +1 -1
  102. package/src/chat/pi-bridge.js +1 -1
  103. package/src/chat/prompt-builder.js +1 -1
  104. package/src/chat/session-manager.js +1 -1
  105. package/src/config.js +3 -1
  106. package/src/database.js +591 -40
  107. package/src/events/review-events.js +1 -1
  108. package/src/git/base-branch.js +173 -0
  109. package/src/git/gitattributes.js +1 -1
  110. package/src/git/sha-abbrev.js +35 -0
  111. package/src/git/worktree.js +1 -1
  112. package/src/github/client.js +33 -2
  113. package/src/github/parser.js +1 -1
  114. package/src/hooks/hook-runner.js +100 -0
  115. package/src/hooks/payloads.js +212 -0
  116. package/src/local-review.js +469 -130
  117. package/src/local-scope.js +58 -0
  118. package/src/main.js +56 -5
  119. package/src/mcp-stdio.js +1 -1
  120. package/src/protocol-handler.js +1 -1
  121. package/src/routes/analyses.js +74 -11
  122. package/src/routes/chat.js +34 -1
  123. package/src/routes/config.js +2 -1
  124. package/src/routes/context-files.js +1 -1
  125. package/src/routes/councils.js +1 -1
  126. package/src/routes/github-collections.js +1 -1
  127. package/src/routes/local.js +735 -69
  128. package/src/routes/mcp.js +21 -11
  129. package/src/routes/pr.js +91 -13
  130. package/src/routes/reviews.js +1 -1
  131. package/src/routes/setup.js +2 -1
  132. package/src/routes/shared.js +1 -1
  133. package/src/routes/worktrees.js +213 -149
  134. package/src/server.js +31 -1
  135. package/src/setup/local-setup.js +47 -6
  136. package/src/setup/pr-setup.js +29 -6
  137. package/src/utils/auto-context.js +1 -1
  138. package/src/utils/category-emoji.js +1 -1
  139. package/src/utils/comment-formatter.js +1 -1
  140. package/src/utils/diff-annotator.js +1 -1
  141. package/src/utils/diff-file-list.js +1 -1
  142. package/src/utils/instructions.js +1 -1
  143. package/src/utils/json-extractor.js +1 -1
  144. package/src/utils/line-validation.js +1 -1
  145. package/src/utils/logger.js +1 -1
  146. package/src/utils/paths.js +1 -1
  147. package/src/utils/safe-parse-json.js +1 -1
  148. package/src/utils/stats-calculator.js +1 -1
  149. package/src/ws/index.js +1 -1
  150. package/src/ws/server.js +1 -1
@@ -1,60 +1,97 @@
1
- // SPDX-License-Identifier: GPL-3.0-or-later
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. Currently supports a single
7
- * option: "Hide whitespace changes".
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
- * { onToggleWhitespace: (hidden) => { … } }
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._btn.addEventListener('click', (e) => {
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 callback so the consumer can apply the persisted state
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
- // Label wrapping checkbox for a nice click target
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 = this._hideWhitespace;
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
- popover.appendChild(label);
259
+ label.appendChild(document.createTextNode(text));
260
+ return label;
261
+ }
117
262
 
118
- document.body.appendChild(popover);
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
- this._popoverEl = popover;
121
- this._checkbox = checkbox;
374
+ trackContainer.appendChild(stopsRow);
375
+ section.appendChild(trackContainer);
122
376
 
123
- // Respond to checkbox changes
124
- checkbox.addEventListener('change', () => {
125
- this._hideWhitespace = checkbox.checked;
126
- this._persist();
127
- this._syncButtonActive();
128
- this._onToggleWhitespace(this._hideWhitespace);
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: GPL-3.0-or-later
1
+ // Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
2
2
  /**
3
3
  * EmojiPicker - GitHub-style emoji autocomplete popup for textareas
4
4
  * Shows emoji suggestions when user types ":" and filters as they type.
@@ -1,4 +1,4 @@
1
- // SPDX-License-Identifier: GPL-3.0-or-later
1
+ // Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
2
2
  /**
3
3
  * Keyboard Shortcuts Manager
4
4
  * Provides chord detection (key sequences like 'c c' or 'c x') and a help overlay
@@ -1,4 +1,4 @@
1
- // SPDX-License-Identifier: GPL-3.0-or-later
1
+ // Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
2
2
  /**
3
3
  * PanelGroup - Manages the right panel group layout
4
4
  * Coordinates the AI Review panel and Chat panel within a shared flex container.
@@ -1,4 +1,4 @@
1
- // SPDX-License-Identifier: GPL-3.0-or-later
1
+ // Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
2
2
  /**
3
3
  * Preview Comments Modal Component
4
4
  * Shows a text preview of user comments for copying to AI agents
@@ -1,4 +1,4 @@
1
- // SPDX-License-Identifier: GPL-3.0-or-later
1
+ // Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
2
2
  /**
3
3
  * Review Submission Modal Component
4
4
  * Allows users to submit their review with comments to GitHub
@@ -1,4 +1,4 @@
1
- // SPDX-License-Identifier: GPL-3.0-or-later
1
+ // Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
2
2
  /**
3
3
  * Split Button Component
4
4
  * A button with a main action area and a dropdown menu for additional actions
@@ -1,4 +1,4 @@
1
- // SPDX-License-Identifier: GPL-3.0-or-later
1
+ // Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
2
2
  /**
3
3
  * Status Indicator Component
4
4
  * Shows AI analysis status in the toolbar when running in background
@@ -1,4 +1,4 @@
1
- // SPDX-License-Identifier: GPL-3.0-or-later
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
- suggestionEl.scrollIntoView({
373
- behavior: 'smooth',
374
- block: 'center',
375
- inline: 'nearest'
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
  }