@in-the-loop-labs/pair-review 3.4.1 → 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.
@@ -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) and AI suggestion
6
- * rows (.ai-suggestion-row) are hidden via CSS class. Small indicator buttons
7
- * are injected on the right edge of each diff line that has comments, showing
8
- * a person icon (user comments) or sparkles icon (AI suggestions).
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 suggestion rows currently in the DOM
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 → { hasUser, hasAI, hasAdopted, userCount, aiCount, adoptedCount }
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) || { hasUser: false, hasAI: false, hasAdopted: false, userCount: 0, aiCount: 0, adoptedCount: 0 };
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) || { hasUser: false, hasAI: false, hasAdopted: false, userCount: 0, aiCount: 0, adoptedCount: 0 };
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 diff line.
125
- * Skips other comment rows, suggestion rows, and context-expand rows.
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 — three types:
162
- // person (purple) = user-originated comments
186
+ // Build icon content — four types:
187
+ // person (purple) = user-originated comments
163
188
  // ai-comment (purple) = adopted AI suggestions
164
- // sparkles (amber) = AI suggestions
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 (!commentRow.classList.contains('user-comment-row') && !commentRow.classList.contains('ai-suggestion-row')) {
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 (!commentRow.classList.contains('user-comment-row') && !commentRow.classList.contains('ai-suggestion-row')) {
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