@in-the-loop-labs/pair-review 3.4.0 → 3.5.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/README.md +24 -0
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/public/css/pr.css +762 -6
- package/public/js/components/AIPanel.js +486 -42
- package/public/js/components/ChatPanel.js +2002 -528
- package/public/js/index.js +43 -0
- package/public/js/modules/comment-minimizer.js +66 -20
- package/public/js/modules/external-comment-manager.js +870 -0
- package/public/js/pr.js +297 -20
- package/public/local.html +21 -5
- package/public/pr.html +31 -5
- package/src/chat/chat-providers.js +64 -12
- package/src/config.js +21 -17
- package/src/database.js +580 -2
- package/src/external/github-adapter.js +152 -0
- package/src/external/index.js +37 -0
- package/src/git/fetch-helpers.js +29 -0
- package/src/git/worktree-pool-lifecycle.js +16 -5
- package/src/git/worktree.js +9 -8
- package/src/github/client.js +77 -1
- package/src/local-review.js +3 -0
- package/src/main.js +6 -3
- package/src/routes/config.js +27 -0
- package/src/routes/external-comments.js +394 -0
- package/src/routes/local.js +7 -0
- package/src/routes/setup.js +7 -0
- package/src/routes/stack-analysis.js +1 -1
- package/src/server.js +9 -0
- package/src/setup/local-setup.js +5 -1
- package/src/setup/pr-setup.js +7 -4
- package/src/single-port.js +2 -0
- package/src/utils/local-path-input.js +44 -0
package/public/js/index.js
CHANGED
|
@@ -133,6 +133,20 @@
|
|
|
133
133
|
return div.innerHTML;
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
const LOCAL_REVIEW_PATH_URL_ERROR = 'Local reviews require a filesystem path, not a URL. Pass GitHub or Graphite URLs as PR review inputs instead.';
|
|
137
|
+
|
|
138
|
+
function isUrlLikeLocalReviewPath(value) {
|
|
139
|
+
if (!value || typeof value !== 'string') return false;
|
|
140
|
+
const trimmed = value.trim();
|
|
141
|
+
if (!trimmed) return false;
|
|
142
|
+
if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) return true;
|
|
143
|
+
if (/^(?:github\.com|app\.graphite\.(?:dev|com))\//i.test(trimmed)) return true;
|
|
144
|
+
// Keep this aligned with src/utils/local-path-input.js: only a leading
|
|
145
|
+
// user@host:path token is treated as an SSH-style remote.
|
|
146
|
+
if (/^[^@/\\\s]+@[^:/\\\s]+:[^\s]+$/.test(trimmed)) return true;
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
|
|
136
150
|
/**
|
|
137
151
|
* Set loading state for a tab's form
|
|
138
152
|
* @param {string} tab - 'pr' or 'local'
|
|
@@ -710,6 +724,12 @@
|
|
|
710
724
|
return;
|
|
711
725
|
}
|
|
712
726
|
|
|
727
|
+
if (isUrlLikeLocalReviewPath(pathValue)) {
|
|
728
|
+
showError('local', LOCAL_REVIEW_PATH_URL_ERROR);
|
|
729
|
+
input.focus();
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
|
|
713
733
|
// Navigate to the setup page which shows step-by-step progress
|
|
714
734
|
// The /local?path= route serves setup.html which handles the full setup flow
|
|
715
735
|
let href = '/local?path=' + encodeURIComponent(pathValue);
|
|
@@ -717,6 +737,21 @@
|
|
|
717
737
|
window.location.href = href;
|
|
718
738
|
}
|
|
719
739
|
|
|
740
|
+
function handleLocalPathInput(event) {
|
|
741
|
+
const input = event && event.target ? event.target : document.getElementById('local-path-input');
|
|
742
|
+
const errorEl = document.getElementById('start-review-error-local');
|
|
743
|
+
if (!input || !errorEl) return;
|
|
744
|
+
|
|
745
|
+
if (isUrlLikeLocalReviewPath(input.value)) {
|
|
746
|
+
showError('local', LOCAL_REVIEW_PATH_URL_ERROR);
|
|
747
|
+
return;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
if (errorEl.textContent === LOCAL_REVIEW_PATH_URL_ERROR) {
|
|
751
|
+
errorEl.classList.remove('visible', 'info');
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
720
755
|
// ─── Browse Directory ──────────────────────────────────────────────────────
|
|
721
756
|
|
|
722
757
|
/**
|
|
@@ -746,6 +781,9 @@
|
|
|
746
781
|
|
|
747
782
|
if (!data.cancelled && data.path) {
|
|
748
783
|
input.value = data.path;
|
|
784
|
+
// Setting .value in JavaScript does not fire an input event, so run the
|
|
785
|
+
// same handler used for typing to clear any stale URL-specific error.
|
|
786
|
+
handleLocalPathInput({ target: input });
|
|
749
787
|
input.focus();
|
|
750
788
|
}
|
|
751
789
|
|
|
@@ -1873,6 +1911,11 @@
|
|
|
1873
1911
|
localForm.addEventListener('submit', handleStartLocal);
|
|
1874
1912
|
}
|
|
1875
1913
|
|
|
1914
|
+
const localPathInput = document.getElementById('local-path-input');
|
|
1915
|
+
if (localPathInput) {
|
|
1916
|
+
localPathInput.addEventListener('input', handleLocalPathInput);
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1876
1919
|
// Set up browse button handler
|
|
1877
1920
|
const browseBtn = document.getElementById('browse-local-btn');
|
|
1878
1921
|
if (browseBtn) {
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* CommentMinimizer - Manages "minimize comments" mode for the diff view.
|
|
4
4
|
*
|
|
5
|
-
* When active, all inline comment rows (.user-comment-row
|
|
6
|
-
*
|
|
7
|
-
* are injected on the right edge of each diff line that has
|
|
8
|
-
* a person icon (user comments)
|
|
5
|
+
* When active, all inline comment rows (.user-comment-row, .ai-suggestion-row,
|
|
6
|
+
* and .external-comment-row) are hidden via CSS class. Small indicator
|
|
7
|
+
* buttons are injected on the right edge of each diff line that has
|
|
8
|
+
* comments, showing a person icon (user comments), sparkles icon (AI
|
|
9
|
+
* suggestions), or chat-bubble icon (external review comments).
|
|
9
10
|
*
|
|
10
11
|
* File-level comments (.file-comment-card inside .file-comments-zone) are also
|
|
11
12
|
* hidden, with an indicator button injected into the file header bar.
|
|
@@ -23,6 +24,9 @@ class CommentMinimizer {
|
|
|
23
24
|
/** AI comment icon SVG — speech bubble with sparkles (matches CommentManager.AI_ICON_SVG, different size) */
|
|
24
25
|
static AI_COMMENT_ICON = `<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor"><path d="M7.75 1a.75.75 0 0 1 0 1.5h-5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h2c.199 0 .39.079.53.22.141.14.22.331.22.53v2.19l2.72-2.72a.747.747 0 0 1 .53-.22h4.5a.25.25 0 0 0 .25-.25v-2a.75.75 0 0 1 1.5 0v2c0 .464-.184.909-.513 1.237A1.746 1.746 0 0 1 13.25 12H9.06l-2.573 2.573A1.457 1.457 0 0 1 4 13.543V12H2.75A1.75 1.75 0 0 1 1 10.25v-7.5C1 1.784 1.784 1 2.75 1h5Zm4.519-.837a.248.248 0 0 1 .466 0l.238.648a3.726 3.726 0 0 0 2.218 2.219l.649.238a.249.249 0 0 1 0 .467l-.649.238a3.725 3.725 0 0 0-2.218 2.218l-.238.649a.248.248 0 0 1-.466 0l-.239-.649a3.725 3.725 0 0 0-2.218-2.218l-.649-.238a.249.249 0 0 1 0-.467l.649-.238A3.726 3.726 0 0 0 12.03.811l.239-.648Z"/></svg>`;
|
|
25
26
|
|
|
27
|
+
/** External comment icon SVG — plain chat bubble (octicon-comment). Matches the chat-comment glyph used elsewhere for external rows. */
|
|
28
|
+
static EXTERNAL_ICON = `<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor"><path d="M1.75 1h8.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 10.25 10H7.061l-2.574 2.573A1.458 1.458 0 0 1 2 11.543V10h-.25A1.75 1.75 0 0 1 0 8.25v-5.5C0 1.784.784 1 1.75 1ZM1.5 2.75v5.5c0 .138.112.25.25.25h1a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h3.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25Z"/></svg>`;
|
|
29
|
+
|
|
26
30
|
constructor() {
|
|
27
31
|
this._active = false;
|
|
28
32
|
// Track which diff lines have been expanded by the user (Set of diff row elements)
|
|
@@ -70,17 +74,22 @@ class CommentMinimizer {
|
|
|
70
74
|
|
|
71
75
|
this._removeAllIndicators();
|
|
72
76
|
|
|
73
|
-
// Find all comment and
|
|
77
|
+
// Find all comment, suggestion, and external-comment rows currently in the DOM
|
|
74
78
|
const commentRows = document.querySelectorAll('.user-comment-row');
|
|
75
79
|
const suggestionRows = document.querySelectorAll('.ai-suggestion-row');
|
|
80
|
+
const externalRows = document.querySelectorAll('.external-comment-row');
|
|
76
81
|
|
|
77
|
-
// Build a map: diff row element →
|
|
82
|
+
// Build a map: diff row element → indicator info entry
|
|
78
83
|
const lineMap = new Map();
|
|
84
|
+
const newEntry = () => ({
|
|
85
|
+
hasUser: false, hasAI: false, hasAdopted: false, hasExternal: false,
|
|
86
|
+
userCount: 0, aiCount: 0, adoptedCount: 0, externalCount: 0
|
|
87
|
+
});
|
|
79
88
|
|
|
80
89
|
for (const row of commentRows) {
|
|
81
90
|
const diffRow = this._findDiffRowFor(row);
|
|
82
91
|
if (!diffRow) continue;
|
|
83
|
-
const entry = lineMap.get(diffRow) ||
|
|
92
|
+
const entry = lineMap.get(diffRow) || newEntry();
|
|
84
93
|
if (row.querySelector('.adopted-comment')) {
|
|
85
94
|
entry.hasAdopted = true;
|
|
86
95
|
entry.adoptedCount++;
|
|
@@ -94,7 +103,7 @@ class CommentMinimizer {
|
|
|
94
103
|
for (const row of suggestionRows) {
|
|
95
104
|
const diffRow = this._findDiffRowFor(row);
|
|
96
105
|
if (!diffRow) continue;
|
|
97
|
-
const entry = lineMap.get(diffRow) ||
|
|
106
|
+
const entry = lineMap.get(diffRow) || newEntry();
|
|
98
107
|
// Count non-adopted suggestion divs only — adopted ones are already
|
|
99
108
|
// represented by the adopted comment row (avoid double-counting)
|
|
100
109
|
const allSuggestions = row.querySelectorAll('.ai-suggestion');
|
|
@@ -111,6 +120,20 @@ class CommentMinimizer {
|
|
|
111
120
|
lineMap.set(diffRow, entry);
|
|
112
121
|
}
|
|
113
122
|
|
|
123
|
+
for (const row of externalRows) {
|
|
124
|
+
const diffRow = this._findDiffRowFor(row);
|
|
125
|
+
if (!diffRow) continue;
|
|
126
|
+
const entry = lineMap.get(diffRow) || newEntry();
|
|
127
|
+
// Count root + replies — each .external-comment is one bubble in the
|
|
128
|
+
// thread card and the reviewer should see the same total here that
|
|
129
|
+
// the thread itself shows.
|
|
130
|
+
const bubbles = row.querySelectorAll('.external-comment');
|
|
131
|
+
const bubbleCount = bubbles.length || 1;
|
|
132
|
+
entry.hasExternal = true;
|
|
133
|
+
entry.externalCount += bubbleCount;
|
|
134
|
+
lineMap.set(diffRow, entry);
|
|
135
|
+
}
|
|
136
|
+
|
|
114
137
|
// Inject line-level indicators
|
|
115
138
|
for (const [diffRow, info] of lineMap) {
|
|
116
139
|
this._injectIndicator(diffRow, info);
|
|
@@ -121,8 +144,9 @@ class CommentMinimizer {
|
|
|
121
144
|
}
|
|
122
145
|
|
|
123
146
|
/**
|
|
124
|
-
* Walk backward from a comment/suggestion row to find its parent
|
|
125
|
-
* Skips other comment
|
|
147
|
+
* Walk backward from a comment/suggestion/external row to find its parent
|
|
148
|
+
* diff line. Skips other comment/suggestion/external rows and
|
|
149
|
+
* context-expand rows.
|
|
126
150
|
* @param {HTMLElement} row
|
|
127
151
|
* @returns {HTMLElement|null}
|
|
128
152
|
*/
|
|
@@ -132,6 +156,7 @@ class CommentMinimizer {
|
|
|
132
156
|
if (
|
|
133
157
|
!prev.classList.contains('user-comment-row') &&
|
|
134
158
|
!prev.classList.contains('ai-suggestion-row') &&
|
|
159
|
+
!prev.classList.contains('external-comment-row') &&
|
|
135
160
|
!prev.classList.contains('comment-form-row') &&
|
|
136
161
|
!prev.classList.contains('context-expand-row')
|
|
137
162
|
) {
|
|
@@ -158,10 +183,11 @@ class CommentMinimizer {
|
|
|
158
183
|
btn.className = 'comment-indicator';
|
|
159
184
|
btn.type = 'button';
|
|
160
185
|
|
|
161
|
-
// Build icon content —
|
|
162
|
-
// person (purple)
|
|
186
|
+
// Build icon content — four types:
|
|
187
|
+
// person (purple) = user-originated comments
|
|
163
188
|
// ai-comment (purple) = adopted AI suggestions
|
|
164
|
-
// sparkles (amber)
|
|
189
|
+
// sparkles (amber) = AI suggestions
|
|
190
|
+
// chat bubble (blue) = external review comments (e.g. GitHub)
|
|
165
191
|
const icons = [];
|
|
166
192
|
if (info.hasUser) {
|
|
167
193
|
icons.push(`<span class="indicator-icon indicator-user" title="${info.userCount} comment${info.userCount !== 1 ? 's' : ''}">${CommentMinimizer.PERSON_ICON}</span>`);
|
|
@@ -172,8 +198,11 @@ class CommentMinimizer {
|
|
|
172
198
|
if (info.hasAI) {
|
|
173
199
|
icons.push(`<span class="indicator-icon indicator-ai" title="${info.aiCount} suggestion${info.aiCount !== 1 ? 's' : ''}">${CommentMinimizer.SPARKLES_ICON}</span>`);
|
|
174
200
|
}
|
|
201
|
+
if (info.hasExternal) {
|
|
202
|
+
icons.push(`<span class="indicator-icon indicator-external" title="${info.externalCount} external comment${info.externalCount !== 1 ? 's' : ''}">${CommentMinimizer.EXTERNAL_ICON}</span>`);
|
|
203
|
+
}
|
|
175
204
|
|
|
176
|
-
const total = info.userCount + info.adoptedCount + info.aiCount;
|
|
205
|
+
const total = info.userCount + info.adoptedCount + info.aiCount + info.externalCount;
|
|
177
206
|
const countBadge = total > 1 ? `<span class="indicator-count">${total}</span>` : '';
|
|
178
207
|
|
|
179
208
|
btn.innerHTML = icons.join('') + countBadge;
|
|
@@ -182,11 +211,19 @@ class CommentMinimizer {
|
|
|
182
211
|
if (info.userCount) totalLabel.push(`${info.userCount} comment${info.userCount !== 1 ? 's' : ''}`);
|
|
183
212
|
if (info.adoptedCount) totalLabel.push(`${info.adoptedCount} adopted comment${info.adoptedCount !== 1 ? 's' : ''}`);
|
|
184
213
|
if (info.aiCount) totalLabel.push(`${info.aiCount} suggestion${info.aiCount !== 1 ? 's' : ''}`);
|
|
214
|
+
if (info.externalCount) totalLabel.push(`${info.externalCount} external comment${info.externalCount !== 1 ? 's' : ''}`);
|
|
185
215
|
btn.title = totalLabel.join(', ');
|
|
186
216
|
|
|
187
|
-
// Check if this line was previously expanded
|
|
217
|
+
// Check if this line was previously expanded. Re-applying
|
|
218
|
+
// `.comment-expanded` to the (possibly rebuilt) comment rows is
|
|
219
|
+
// important: when an external-comment refresh tears down and rebuilds
|
|
220
|
+
// `.external-comment-row` elements, the new rows have no expanded
|
|
221
|
+
// class even though the indicator button is still marked expanded.
|
|
222
|
+
// Without this step the indicator says "open" but the rows stay
|
|
223
|
+
// hidden via the `.comments-minimized` display: none rule.
|
|
188
224
|
if (this._expandedLines.has(diffRow)) {
|
|
189
225
|
btn.classList.add('expanded');
|
|
226
|
+
this._getCommentRowsFor(diffRow).forEach(row => row.classList.add('comment-expanded'));
|
|
190
227
|
}
|
|
191
228
|
|
|
192
229
|
// Click handler: toggle this line's comments
|
|
@@ -234,7 +271,8 @@ class CommentMinimizer {
|
|
|
234
271
|
while (next) {
|
|
235
272
|
if (
|
|
236
273
|
next.classList.contains('user-comment-row') ||
|
|
237
|
-
next.classList.contains('ai-suggestion-row')
|
|
274
|
+
next.classList.contains('ai-suggestion-row') ||
|
|
275
|
+
next.classList.contains('external-comment-row')
|
|
238
276
|
) {
|
|
239
277
|
rows.push(next);
|
|
240
278
|
} else if (
|
|
@@ -261,8 +299,12 @@ class CommentMinimizer {
|
|
|
261
299
|
* @returns {HTMLElement|null} The parent diff row, or null
|
|
262
300
|
*/
|
|
263
301
|
findDiffRowFor(element) {
|
|
264
|
-
const commentRow = element.closest('.user-comment-row, .ai-suggestion-row') || element;
|
|
265
|
-
if (
|
|
302
|
+
const commentRow = element.closest('.user-comment-row, .ai-suggestion-row, .external-comment-row') || element;
|
|
303
|
+
if (
|
|
304
|
+
!commentRow.classList.contains('user-comment-row') &&
|
|
305
|
+
!commentRow.classList.contains('ai-suggestion-row') &&
|
|
306
|
+
!commentRow.classList.contains('external-comment-row')
|
|
307
|
+
) {
|
|
266
308
|
return null;
|
|
267
309
|
}
|
|
268
310
|
return this._findDiffRowFor(commentRow);
|
|
@@ -293,8 +335,12 @@ class CommentMinimizer {
|
|
|
293
335
|
}
|
|
294
336
|
|
|
295
337
|
// Line-level: find the containing comment/suggestion row
|
|
296
|
-
const commentRow = element.closest('.user-comment-row, .ai-suggestion-row') || element;
|
|
297
|
-
if (
|
|
338
|
+
const commentRow = element.closest('.user-comment-row, .ai-suggestion-row, .external-comment-row') || element;
|
|
339
|
+
if (
|
|
340
|
+
!commentRow.classList.contains('user-comment-row') &&
|
|
341
|
+
!commentRow.classList.contains('ai-suggestion-row') &&
|
|
342
|
+
!commentRow.classList.contains('external-comment-row')
|
|
343
|
+
) {
|
|
298
344
|
return;
|
|
299
345
|
}
|
|
300
346
|
|