@in-the-loop-labs/pair-review 3.1.3 → 3.2.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 (39) hide show
  1. package/package.json +1 -1
  2. package/plugin/.claude-plugin/plugin.json +1 -1
  3. package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
  4. package/public/css/pr.css +980 -3
  5. package/public/js/components/AIPanel.js +7 -4
  6. package/public/js/components/ChatPanel.js +34 -4
  7. package/public/js/components/CouncilProgressModal.js +11 -0
  8. package/public/js/components/NotificationDropdown.js +257 -0
  9. package/public/js/components/StackAnalysisDialog.js +313 -0
  10. package/public/js/components/StackProgressModal.js +475 -0
  11. package/public/js/components/StatusIndicator.js +1 -0
  12. package/public/js/components/SuggestionNavigator.js +2 -0
  13. package/public/js/modules/comment-manager.js +7 -0
  14. package/public/js/modules/comment-minimizer.js +151 -4
  15. package/public/js/modules/file-comment-manager.js +66 -2
  16. package/public/js/modules/suggestion-manager.js +2 -1
  17. package/public/js/pr.js +433 -2
  18. package/public/js/utils/notification-sounds.js +62 -0
  19. package/public/local.html +10 -0
  20. package/public/pr.html +12 -0
  21. package/public/setup.html +4 -0
  22. package/src/ai/claude-provider.js +1 -11
  23. package/src/ai/codex-provider.js +18 -16
  24. package/src/ai/copilot-provider.js +21 -21
  25. package/src/ai/gemini-provider.js +10 -0
  26. package/src/ai/pi-provider.js +22 -25
  27. package/src/ai/provider.js +26 -3
  28. package/src/chat/pi-bridge.js +8 -0
  29. package/src/chat/session-manager.js +1 -0
  30. package/src/git/base-branch.js +1 -51
  31. package/src/git/worktree-lock.js +88 -0
  32. package/src/git/worktree.js +64 -0
  33. package/src/github/stack-walker.js +196 -0
  34. package/src/routes/local.js +12 -8
  35. package/src/routes/pr.js +139 -26
  36. package/src/routes/sound.js +49 -0
  37. package/src/routes/stack-analysis.js +886 -0
  38. package/src/server.js +4 -0
  39. package/src/setup/stack-setup.js +77 -0
@@ -0,0 +1,313 @@
1
+ // Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Stack Analysis Dialog Component
4
+ * Modal showing stack PRs with checkboxes for selection before analysis.
5
+ * Returns a Promise resolving to { selectedPRNumbers } or null if cancelled.
6
+ */
7
+ class StackAnalysisDialog {
8
+ constructor(container) {
9
+ this.container = container || document.body;
10
+ this.overlay = null;
11
+ this._resolve = null;
12
+ this._stackData = null;
13
+ this._currentPRNumber = null;
14
+
15
+ // Bind methods
16
+ this.handleKeydown = this.handleKeydown.bind(this);
17
+ }
18
+
19
+ /**
20
+ * Open the dialog, fetch stack info, and let the user select PRs.
21
+ * @param {string} owner - Repository owner
22
+ * @param {string} repo - Repository name
23
+ * @param {number} number - Current PR number
24
+ * @returns {Promise<{selectedPRNumbers: number[]}|null>} Selected PR numbers or null if cancelled
25
+ */
26
+ open(owner, repo, number) {
27
+ this._currentPRNumber = number;
28
+
29
+ return new Promise((resolve) => {
30
+ this._resolve = resolve;
31
+ this._createOverlay();
32
+ this._showLoading();
33
+ this._attachListeners();
34
+ this._fetchStackInfo(owner, repo, number);
35
+ });
36
+ }
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // DOM creation
40
+ // ---------------------------------------------------------------------------
41
+
42
+ _createOverlay() {
43
+ // Remove any existing overlay
44
+ this._removeOverlay();
45
+
46
+ const overlay = document.createElement('div');
47
+ overlay.className = 'stack-dialog-overlay';
48
+
49
+ overlay.innerHTML = `
50
+ <div class="stack-dialog-backdrop" data-action="cancel"></div>
51
+ <div class="stack-dialog">
52
+ <div class="stack-dialog-header">
53
+ <h3>Analyze Stack</h3>
54
+ <button class="modal-close-btn" data-action="cancel" title="Close">
55
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
56
+ <path d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"/>
57
+ </svg>
58
+ </button>
59
+ </div>
60
+ <div class="stack-dialog-body">
61
+ <!-- Populated dynamically -->
62
+ </div>
63
+ <div class="stack-dialog-footer">
64
+ <button class="btn btn-secondary" data-action="cancel">Cancel</button>
65
+ <button class="btn btn-primary stack-dialog-submit" data-action="submit" disabled>Configure &amp; Analyze</button>
66
+ </div>
67
+ </div>
68
+ `;
69
+
70
+ this.container.appendChild(overlay);
71
+ this.overlay = overlay;
72
+
73
+ // Event delegation for clicks
74
+ overlay.addEventListener('click', (e) => {
75
+ const action = e.target.closest('[data-action]')?.dataset.action;
76
+ if (action === 'cancel') {
77
+ this._cancel();
78
+ return;
79
+ }
80
+ if (action === 'submit') {
81
+ this._submit();
82
+ return;
83
+ }
84
+ if (action === 'select-all') {
85
+ this._setAllChecked(true);
86
+ return;
87
+ }
88
+ if (action === 'select-none') {
89
+ this._setAllChecked(false);
90
+ return;
91
+ }
92
+
93
+ // Checkbox change via clicking the row
94
+ const checkbox = e.target.closest('.stack-dialog-pr-checkbox');
95
+ if (checkbox) {
96
+ // Let the native checkbox toggle, then update button state
97
+ setTimeout(() => this._updateSubmitButton(), 0);
98
+ }
99
+ });
100
+
101
+ // Handle checkbox changes (covers keyboard toggle too)
102
+ overlay.addEventListener('change', () => {
103
+ this._updateSubmitButton();
104
+ });
105
+ }
106
+
107
+ _showLoading() {
108
+ const body = this.overlay?.querySelector('.stack-dialog-body');
109
+ if (!body) return;
110
+
111
+ body.innerHTML = `
112
+ <div class="stack-dialog-loading">
113
+ <span class="council-spinner"></span>
114
+ <span>Loading stack info...</span>
115
+ </div>
116
+ `;
117
+ }
118
+
119
+ _showError(message) {
120
+ const body = this.overlay?.querySelector('.stack-dialog-body');
121
+ if (!body) return;
122
+
123
+ body.innerHTML = `
124
+ <div class="stack-dialog-error">
125
+ <p>${this._escapeHtml(message)}</p>
126
+ </div>
127
+ `;
128
+ }
129
+
130
+ _renderPRList(stack) {
131
+ const body = this.overlay?.querySelector('.stack-dialog-body');
132
+ if (!body) return;
133
+
134
+ // Filter to non-trunk entries with PR numbers
135
+ const prs = stack.filter(entry => !entry.isTrunk && entry.prNumber);
136
+ if (prs.length === 0) {
137
+ this._showError('No PRs found in this stack.');
138
+ return;
139
+ }
140
+
141
+ this._stackData = prs;
142
+
143
+ // Display PRs in reverse order: tip of stack (top) first, base (bottom) last
144
+ const displayPRs = [...prs].reverse();
145
+
146
+ let html = `
147
+ <div class="stack-dialog-controls">
148
+ <button class="btn btn-sm btn-secondary" data-action="select-all">Select All</button>
149
+ <button class="btn btn-sm btn-secondary" data-action="select-none">Select None</button>
150
+ </div>
151
+ <div class="stack-dialog-pr-list">
152
+ `;
153
+
154
+ for (const pr of displayPRs) {
155
+ const isCurrent = pr.prNumber === this._currentPRNumber;
156
+ const currentClass = isCurrent ? ' stack-dialog-pr-current' : '';
157
+ const currentBadge = isCurrent ? ' <span class="stack-dialog-current-badge">\u2605</span>' : '';
158
+ const analysisBadge = pr.hasAnalysis
159
+ ? ' <span class="stack-dialog-analyzed-badge" title="Has existing analysis">\u2022 analyzed</span>'
160
+ : '';
161
+
162
+ html += `
163
+ <label class="stack-dialog-pr-item${currentClass}">
164
+ <input type="checkbox" class="stack-dialog-pr-checkbox" data-pr-number="${pr.prNumber}" checked />
165
+ <span class="stack-dialog-pr-number">#${pr.prNumber}</span>
166
+ <div class="stack-dialog-pr-info">
167
+ <span class="stack-dialog-pr-title-row">
168
+ ${currentBadge}
169
+ <span class="stack-dialog-pr-title">${this._escapeHtml(pr.title || pr.branch || '')}</span>
170
+ ${analysisBadge}
171
+ </span>
172
+ <span class="stack-dialog-pr-branch"><svg class="stack-dialog-branch-icon" width="12" height="12" viewBox="0 0 16 16" fill="currentColor"><path d="M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.493 2.493 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628A2.25 2.25 0 0 1 9.5 3.25Z"/></svg><span class="stack-dialog-branch-name">${this._escapeHtml(pr.branch || '')}</span></span>
173
+ </div>
174
+ </label>
175
+ `;
176
+ }
177
+
178
+ html += `
179
+ </div>
180
+ <div class="stack-dialog-note">
181
+ <span class="stack-dialog-note-info" title="Each PR gets its own worktree (created automatically if needed). All selected PRs are analyzed in parallel.">
182
+ <svg class="stack-dialog-info-icon" width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
183
+ <path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8Zm8-6.5a6.5 6.5 0 1 0 0 13 6.5 6.5 0 0 0 0-13ZM6.5 7.75A.75.75 0 0 1 7.25 7h1a.75.75 0 0 1 .75.75v2.75h.25a.75.75 0 0 1 0 1.5h-2a.75.75 0 0 1 0-1.5h.25v-2h-.25a.75.75 0 0 1-.75-.75ZM8 6a1 1 0 1 1 0-2 1 1 0 0 1 0 2Z"/>
184
+ </svg>
185
+ Each PR gets its own worktree (created automatically if needed). All selected PRs are analyzed in parallel.
186
+ </span>
187
+ </div>
188
+ `;
189
+
190
+ body.innerHTML = html;
191
+
192
+ // Enable submit button
193
+ this._updateSubmitButton();
194
+ }
195
+
196
+ // ---------------------------------------------------------------------------
197
+ // Fetch
198
+ // ---------------------------------------------------------------------------
199
+
200
+ async _fetchStackInfo(owner, repo, number) {
201
+ try {
202
+ const response = await fetch(`/api/pr/${owner}/${repo}/${number}/stack-info`);
203
+ if (!response.ok) {
204
+ const data = await response.json().catch(() => ({}));
205
+ throw new Error(data.error || `Failed to fetch stack info (${response.status})`);
206
+ }
207
+
208
+ const data = await response.json();
209
+ this._renderPRList(data.stack || []);
210
+ } catch (error) {
211
+ console.error('Error fetching stack info:', error);
212
+ this._showError(error.message || 'Failed to load stack information.');
213
+ }
214
+ }
215
+
216
+ // ---------------------------------------------------------------------------
217
+ // Actions
218
+ // ---------------------------------------------------------------------------
219
+
220
+ _getSelectedPRNumbers() {
221
+ if (!this.overlay) return [];
222
+ const checkboxes = this.overlay.querySelectorAll('.stack-dialog-pr-checkbox:checked');
223
+ return Array.from(checkboxes).map(cb => Number(cb.dataset.prNumber));
224
+ }
225
+
226
+ _setAllChecked(checked) {
227
+ if (!this.overlay) return;
228
+ const checkboxes = this.overlay.querySelectorAll('.stack-dialog-pr-checkbox');
229
+ checkboxes.forEach(cb => { cb.checked = checked; });
230
+ this._updateSubmitButton();
231
+ }
232
+
233
+ _updateSubmitButton() {
234
+ const submitBtn = this.overlay?.querySelector('.stack-dialog-submit');
235
+ if (!submitBtn) return;
236
+ const selected = this._getSelectedPRNumbers();
237
+ submitBtn.disabled = selected.length === 0;
238
+ }
239
+
240
+ _submit() {
241
+ const selectedPRNumbers = this._getSelectedPRNumbers();
242
+ if (selectedPRNumbers.length === 0) return;
243
+
244
+ // Build prList with titles for the progress modal
245
+ const selectedSet = new Set(selectedPRNumbers);
246
+ const prList = (this._stackData || [])
247
+ .filter(pr => selectedSet.has(pr.prNumber))
248
+ .map(pr => ({ prNumber: pr.prNumber, title: pr.title || pr.branch || '' }));
249
+
250
+ this._cleanup();
251
+ if (this._resolve) {
252
+ this._resolve({ selectedPRNumbers, prList });
253
+ this._resolve = null;
254
+ }
255
+ }
256
+
257
+ _cancel() {
258
+ this._cleanup();
259
+ if (this._resolve) {
260
+ this._resolve(null);
261
+ this._resolve = null;
262
+ }
263
+ }
264
+
265
+ // ---------------------------------------------------------------------------
266
+ // Listeners & cleanup
267
+ // ---------------------------------------------------------------------------
268
+
269
+ _attachListeners() {
270
+ document.addEventListener('keydown', this.handleKeydown);
271
+ }
272
+
273
+ handleKeydown(e) {
274
+ if (e.key === 'Escape') {
275
+ this._cancel();
276
+ }
277
+ }
278
+
279
+ _cleanup() {
280
+ document.removeEventListener('keydown', this.handleKeydown);
281
+ this._removeOverlay();
282
+ this._stackData = null;
283
+ this._currentPRNumber = null;
284
+ }
285
+
286
+ _removeOverlay() {
287
+ if (this.overlay) {
288
+ this.overlay.remove();
289
+ this.overlay = null;
290
+ }
291
+ }
292
+
293
+ // ---------------------------------------------------------------------------
294
+ // Utilities
295
+ // ---------------------------------------------------------------------------
296
+
297
+ _escapeHtml(str) {
298
+ if (!str) return '';
299
+ const div = document.createElement('div');
300
+ div.textContent = str;
301
+ return div.innerHTML;
302
+ }
303
+ }
304
+
305
+ // Export for use in other modules
306
+ if (typeof window !== 'undefined') {
307
+ window.StackAnalysisDialog = StackAnalysisDialog;
308
+ }
309
+
310
+ // Export for Node.js/test environments
311
+ if (typeof module !== 'undefined' && module.exports) {
312
+ module.exports = { StackAnalysisDialog };
313
+ }