@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,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
  * Analysis History Manager
4
4
  *
@@ -20,11 +20,12 @@ class AnalysisHistoryManager {
20
20
  * @param {Function} options.onSelectionChange - Callback when a run is selected, receives (runId, run)
21
21
  * @param {string} options.containerPrefix - Prefix for DOM element IDs (default: 'analysis-context')
22
22
  */
23
- constructor({ reviewId, mode, onSelectionChange, containerPrefix = 'analysis-context' }) {
23
+ constructor({ reviewId, mode, onSelectionChange, containerPrefix = 'analysis-context', shaAbbrevLength = 7 }) {
24
24
  this.reviewId = reviewId;
25
25
  this.mode = mode;
26
26
  this.onSelectionChange = onSelectionChange;
27
27
  this.containerPrefix = containerPrefix;
28
+ this.shaAbbrevLength = shaAbbrevLength;
28
29
 
29
30
  // State
30
31
  this.runs = [];
@@ -421,7 +422,7 @@ class AnalysisHistoryManager {
421
422
 
422
423
  // Format HEAD SHA - show abbreviated version with full SHA in title
423
424
  const headSha = run.head_sha;
424
- const headShaDisplay = headSha ? headSha.substring(0, 7) : null;
425
+ const headShaDisplay = headSha ? headSha.substring(0, this.shaAbbrevLength) : null;
425
426
 
426
427
  // Format status
427
428
  const statusInfo = this.formatStatus(run.status);
@@ -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
  * CommentManager - Comment UI handling
4
4
  * Handles comment forms, editing, saving, deletion, and display.
@@ -500,6 +500,11 @@ class CommentManager {
500
500
  this.prManager.updateCommentCount();
501
501
  }
502
502
 
503
+ // Refresh minimize-mode indicators so the new comment is reflected
504
+ if (window.prManager?.commentMinimizer) {
505
+ window.prManager.commentMinimizer.refreshIndicators();
506
+ }
507
+
503
508
  } catch (error) {
504
509
  console.error('Error saving comment:', error);
505
510
  alert('Failed to save comment');
@@ -0,0 +1,304 @@
1
+ // Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * CommentMinimizer - Manages "minimize comments" mode for the diff view.
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).
9
+ *
10
+ * Clicking an indicator toggles visibility of that line's comments only.
11
+ */
12
+
13
+ class CommentMinimizer {
14
+ /** Person icon SVG (matches comment-manager.js octicon-person) */
15
+ static PERSON_ICON = `<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor"><path d="M10.561 8.073a6.005 6.005 0 0 1 3.432 5.142.75.75 0 1 1-1.498.07 4.5 4.5 0 0 0-8.99 0 .75.75 0 0 1-1.498-.07 6.004 6.004 0 0 1 3.431-5.142 3.999 3.999 0 1 1 5.123 0ZM10.5 5a2.5 2.5 0 1 0-5 0 2.5 2.5 0 0 0 5 0Z"/></svg>`;
16
+
17
+ /** Sparkles icon SVG (matches AI suggestion badge) */
18
+ static SPARKLES_ICON = `<svg viewBox="0 0 16 16" width="14" height="14" fill="currentColor"><path d="M9.6 2.279a.426.426 0 0 1 .8 0l.407 1.112a6.386 6.386 0 0 0 3.802 3.802l1.112.407a.426.426 0 0 1 0 .8l-1.112.407a6.386 6.386 0 0 0-3.802 3.802l-.407 1.112a.426.426 0 0 1-.8 0l-.407-1.112a6.386 6.386 0 0 0-3.802-3.802L4.279 8.4a.426.426 0 0 1 0-.8l1.112-.407a6.386 6.386 0 0 0 3.802-3.802L9.6 2.279Zm-4.267 8.837a.178.178 0 0 1 .334 0l.169.464a2.662 2.662 0 0 0 1.584 1.584l.464.169a.178.178 0 0 1 0 .334l-.464.169a2.662 2.662 0 0 0-1.584 1.584l-.169.464a.178.178 0 0 1-.334 0l-.169-.464a2.662 2.662 0 0 0-1.584-1.584l-.464-.169a.178.178 0 0 1 0-.334l.464-.169a2.662 2.662 0 0 0 1.584-1.584l.169-.464ZM2.8.14a.213.213 0 0 1 .4 0l.203.556a3.2 3.2 0 0 0 1.901 1.901l.556.203a.213.213 0 0 1 0 .4l-.556.203a3.2 3.2 0 0 0-1.901 1.901L3.2 5.86a.213.213 0 0 1-.4 0l-.203-.556A3.2 3.2 0 0 0 .696 3.403L.14 3.2a.213.213 0 0 1 0-.4l.556-.203A3.2 3.2 0 0 0 2.597.696L2.8.14Z"/></svg>`;
19
+
20
+ /** AI comment icon SVG — speech bubble with sparkles (for adopted AI suggestions) */
21
+ 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>`;
22
+
23
+ constructor() {
24
+ this._active = false;
25
+ // Track which diff lines have been expanded by the user (Set of diff row elements)
26
+ this._expandedLines = new Set();
27
+ }
28
+
29
+ /** @returns {boolean} Whether minimize mode is active */
30
+ get active() {
31
+ return this._active;
32
+ }
33
+
34
+ /**
35
+ * Enable or disable minimize mode.
36
+ * @param {boolean} minimized
37
+ */
38
+ setMinimized(minimized) {
39
+ this._active = minimized;
40
+ this._expandedLines.clear();
41
+
42
+ const diffContainer = document.getElementById('diff-container');
43
+ if (!diffContainer) return;
44
+
45
+ if (minimized) {
46
+ diffContainer.classList.add('comments-minimized');
47
+ this.refreshIndicators();
48
+ } else {
49
+ diffContainer.classList.remove('comments-minimized');
50
+ this._removeAllIndicators();
51
+ // Remove any per-line expansion overrides
52
+ document.querySelectorAll('.comment-expanded').forEach(el => el.classList.remove('comment-expanded'));
53
+ }
54
+ }
55
+
56
+ /**
57
+ * Rebuild all indicator buttons on diff lines.
58
+ * Call this after comments or suggestions are added/removed/re-rendered.
59
+ */
60
+ refreshIndicators() {
61
+ if (!this._active) return;
62
+
63
+ this._removeAllIndicators();
64
+
65
+ // Find all comment and suggestion rows currently in the DOM
66
+ const commentRows = document.querySelectorAll('.user-comment-row');
67
+ const suggestionRows = document.querySelectorAll('.ai-suggestion-row');
68
+
69
+ // Build a map: diff row element → { hasUser, hasAI, hasAdopted, userCount, aiCount, adoptedCount }
70
+ const lineMap = new Map();
71
+
72
+ for (const row of commentRows) {
73
+ const diffRow = this._findDiffRowFor(row);
74
+ if (!diffRow) continue;
75
+ const entry = lineMap.get(diffRow) || { hasUser: false, hasAI: false, hasAdopted: false, userCount: 0, aiCount: 0, adoptedCount: 0 };
76
+ if (row.querySelector('.adopted-comment')) {
77
+ entry.hasAdopted = true;
78
+ entry.adoptedCount++;
79
+ } else {
80
+ entry.hasUser = true;
81
+ entry.userCount++;
82
+ }
83
+ lineMap.set(diffRow, entry);
84
+ }
85
+
86
+ for (const row of suggestionRows) {
87
+ const diffRow = this._findDiffRowFor(row);
88
+ if (!diffRow) continue;
89
+ const entry = lineMap.get(diffRow) || { hasUser: false, hasAI: false, hasAdopted: false, userCount: 0, aiCount: 0, adoptedCount: 0 };
90
+ // Count non-adopted suggestion divs only — adopted ones are already
91
+ // represented by the adopted comment row (avoid double-counting)
92
+ const allSuggestions = row.querySelectorAll('.ai-suggestion');
93
+ let activeCount = 0;
94
+ for (const s of allSuggestions) {
95
+ if (!s.dataset?.hiddenForAdoption) {
96
+ activeCount++;
97
+ }
98
+ }
99
+ if (activeCount > 0) {
100
+ entry.hasAI = true;
101
+ entry.aiCount += activeCount;
102
+ }
103
+ lineMap.set(diffRow, entry);
104
+ }
105
+
106
+ // Inject indicators
107
+ for (const [diffRow, info] of lineMap) {
108
+ this._injectIndicator(diffRow, info);
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Walk backward from a comment/suggestion row to find its parent diff line.
114
+ * Skips other comment rows, suggestion rows, and context-expand rows.
115
+ * @param {HTMLElement} row
116
+ * @returns {HTMLElement|null}
117
+ */
118
+ _findDiffRowFor(row) {
119
+ let prev = row.previousElementSibling;
120
+ while (prev) {
121
+ if (
122
+ !prev.classList.contains('user-comment-row') &&
123
+ !prev.classList.contains('ai-suggestion-row') &&
124
+ !prev.classList.contains('comment-form-row') &&
125
+ !prev.classList.contains('context-expand-row')
126
+ ) {
127
+ return prev;
128
+ }
129
+ prev = prev.previousElementSibling;
130
+ }
131
+ return null;
132
+ }
133
+
134
+ /**
135
+ * Inject an indicator button into a diff line's code cell.
136
+ * @param {HTMLElement} diffRow - The diff table row
137
+ * @param {Object} info - { hasUser, hasAI, hasAdopted, userCount, aiCount, adoptedCount }
138
+ */
139
+ _injectIndicator(diffRow, info) {
140
+ const codeCell = diffRow.querySelector('.d2h-code-line-ctn');
141
+ if (!codeCell) return;
142
+
143
+ // Don't double-inject
144
+ if (codeCell.querySelector('.comment-indicator')) return;
145
+
146
+ const btn = document.createElement('button');
147
+ btn.className = 'comment-indicator';
148
+ btn.type = 'button';
149
+
150
+ // Build icon content — three types:
151
+ // person (purple) = user-originated comments
152
+ // ai-comment (purple) = adopted AI suggestions
153
+ // sparkles (amber) = AI suggestions
154
+ const icons = [];
155
+ if (info.hasUser) {
156
+ icons.push(`<span class="indicator-icon indicator-user" title="${info.userCount} comment${info.userCount !== 1 ? 's' : ''}">${CommentMinimizer.PERSON_ICON}</span>`);
157
+ }
158
+ if (info.hasAdopted) {
159
+ icons.push(`<span class="indicator-icon indicator-adopted" title="${info.adoptedCount} adopted comment${info.adoptedCount !== 1 ? 's' : ''}">${CommentMinimizer.AI_COMMENT_ICON}</span>`);
160
+ }
161
+ if (info.hasAI) {
162
+ icons.push(`<span class="indicator-icon indicator-ai" title="${info.aiCount} suggestion${info.aiCount !== 1 ? 's' : ''}">${CommentMinimizer.SPARKLES_ICON}</span>`);
163
+ }
164
+
165
+ const total = info.userCount + info.adoptedCount + info.aiCount;
166
+ const countBadge = total > 1 ? `<span class="indicator-count">${total}</span>` : '';
167
+
168
+ btn.innerHTML = icons.join('') + countBadge;
169
+
170
+ const totalLabel = [];
171
+ if (info.userCount) totalLabel.push(`${info.userCount} comment${info.userCount !== 1 ? 's' : ''}`);
172
+ if (info.adoptedCount) totalLabel.push(`${info.adoptedCount} adopted comment${info.adoptedCount !== 1 ? 's' : ''}`);
173
+ if (info.aiCount) totalLabel.push(`${info.aiCount} suggestion${info.aiCount !== 1 ? 's' : ''}`);
174
+ btn.title = totalLabel.join(', ');
175
+
176
+ // Check if this line was previously expanded
177
+ if (this._expandedLines.has(diffRow)) {
178
+ btn.classList.add('expanded');
179
+ }
180
+
181
+ // Click handler: toggle this line's comments
182
+ btn.addEventListener('click', (e) => {
183
+ e.stopPropagation();
184
+ e.preventDefault();
185
+ this._toggleLineComments(diffRow, btn);
186
+ });
187
+
188
+ // Make the code cell position:relative for absolute positioning of the indicator
189
+ codeCell.style.position = 'relative';
190
+ codeCell.appendChild(btn);
191
+ }
192
+
193
+ /**
194
+ * Toggle visibility of comment/suggestion rows for a specific diff line.
195
+ * @param {HTMLElement} diffRow
196
+ * @param {HTMLElement} btn - The indicator button
197
+ */
198
+ _toggleLineComments(diffRow, btn) {
199
+ const isExpanded = this._expandedLines.has(diffRow);
200
+
201
+ if (isExpanded) {
202
+ // Collapse: remove .comment-expanded from this line's rows
203
+ this._expandedLines.delete(diffRow);
204
+ btn.classList.remove('expanded');
205
+ this._getCommentRowsFor(diffRow).forEach(row => row.classList.remove('comment-expanded'));
206
+ } else {
207
+ // Expand: add .comment-expanded to this line's rows
208
+ this._expandedLines.add(diffRow);
209
+ btn.classList.add('expanded');
210
+ this._getCommentRowsFor(diffRow).forEach(row => row.classList.add('comment-expanded'));
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Get all comment/suggestion rows that belong to a diff line.
216
+ * Walks forward from the diff row, collecting adjacent comment/suggestion rows.
217
+ * @param {HTMLElement} diffRow
218
+ * @returns {HTMLElement[]}
219
+ */
220
+ _getCommentRowsFor(diffRow) {
221
+ const rows = [];
222
+ let next = diffRow.nextElementSibling;
223
+ while (next) {
224
+ if (
225
+ next.classList.contains('user-comment-row') ||
226
+ next.classList.contains('ai-suggestion-row')
227
+ ) {
228
+ rows.push(next);
229
+ } else if (
230
+ next.classList.contains('comment-form-row') ||
231
+ next.classList.contains('context-expand-row')
232
+ ) {
233
+ // Skip these but keep looking
234
+ next = next.nextElementSibling;
235
+ continue;
236
+ } else {
237
+ // Hit another diff line — stop
238
+ break;
239
+ }
240
+ next = next.nextElementSibling;
241
+ }
242
+ return rows;
243
+ }
244
+
245
+ /**
246
+ * Find the parent diff row for a given comment/suggestion element.
247
+ * Public wrapper around _findDiffRowFor that first locates the containing
248
+ * comment/suggestion row from any child element.
249
+ * @param {HTMLElement} element - Any element inside (or equal to) a comment/suggestion row
250
+ * @returns {HTMLElement|null} The parent diff row, or null
251
+ */
252
+ findDiffRowFor(element) {
253
+ const commentRow = element.closest('.user-comment-row, .ai-suggestion-row') || element;
254
+ if (!commentRow.classList.contains('user-comment-row') && !commentRow.classList.contains('ai-suggestion-row')) {
255
+ return null;
256
+ }
257
+ return this._findDiffRowFor(commentRow);
258
+ }
259
+
260
+ // TODO: expose via API route so chat can programmatically expand findings when discussing them
261
+ /**
262
+ * Expand comments for a given element so it becomes visible when minimized.
263
+ * Call this before scrolling to a comment/suggestion row that may be hidden.
264
+ * @param {HTMLElement} element - The target comment/suggestion element (or row)
265
+ */
266
+ expandForElement(element) {
267
+ if (!this._active) return;
268
+
269
+ // Find the containing comment/suggestion row
270
+ const commentRow = element.closest('.user-comment-row, .ai-suggestion-row') || element;
271
+ if (!commentRow.classList.contains('user-comment-row') && !commentRow.classList.contains('ai-suggestion-row')) {
272
+ return;
273
+ }
274
+
275
+ // Find the parent diff row for this comment row
276
+ const diffRow = this._findDiffRowFor(commentRow);
277
+ if (!diffRow) return;
278
+
279
+ // Already expanded — nothing to do
280
+ if (this._expandedLines.has(diffRow)) return;
281
+
282
+ // Expand all comment rows for this diff line
283
+ this._expandedLines.add(diffRow);
284
+ this._getCommentRowsFor(diffRow).forEach(row => row.classList.add('comment-expanded'));
285
+
286
+ // Update the indicator button's expanded state
287
+ const btn = diffRow.querySelector('.d2h-code-line-ctn .comment-indicator');
288
+ if (btn) {
289
+ btn.classList.add('expanded');
290
+ }
291
+ }
292
+
293
+ /** Remove all indicator buttons from the DOM. */
294
+ _removeAllIndicators() {
295
+ document.querySelectorAll('.comment-indicator').forEach(btn => btn.remove());
296
+ }
297
+ }
298
+
299
+ window.CommentMinimizer = CommentMinimizer;
300
+
301
+ // Export for testing
302
+ if (typeof module !== 'undefined' && module.exports) {
303
+ module.exports = { CommentMinimizer };
304
+ }
@@ -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
  * DiffContext - Extract diff hunks for chat context enrichment.
4
4
  * Provides utilities to pull relevant unified diff sections for
@@ -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
  * DiffRenderer - Diff parsing and line rendering
4
4
  * Handles rendering of diff content, syntax highlighting,
@@ -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
  * FileCommentManager - File-level comment UI handling
4
4
  * Handles file-level comments zone rendering, forms, and interactions.
@@ -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
  * File List Merger - Merges diff files with context files for sidebar display.
4
4
  *
@@ -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
  * Gap Coordinates - Coordinate system utilities for diff gap expansion
4
4
  *
@@ -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
  * HunkParser - Hunk header parsing and gap context expansion
4
4
  * Handles parsing of unified diff hunk headers and expansion of collapsed sections
@@ -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
  * LineTracker - Line number mapping, range selection, and highlighting
4
4
  * Handles line number extraction, range selection for multi-line comments,
@@ -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
  * Panel Resizer Module
4
4
  * Handles drag-to-resize functionality for the sidebar and AI panel
@@ -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
  * Storage Cleanup Module
4
4
  * Cleans up legacy localStorage keys from older versions of the app.
@@ -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
  * SuggestionManager - AI suggestion handling
4
4
  * Handles display, adopt, dismiss, and restore of AI suggestions.
package/public/js/pr.js CHANGED
@@ -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
  * Pull Request UI Management
4
4
  * Main orchestrator that coordinates the extracted modules:
@@ -136,6 +136,8 @@ class PRManager {
136
136
  this.hideWhitespace = false;
137
137
  // Diff options dropdown (gear icon popover)
138
138
  this.diffOptionsDropdown = null;
139
+ // Comment minimizer — manages minimize mode indicators
140
+ this.commentMinimizer = window.CommentMinimizer ? new window.CommentMinimizer() : null;
139
141
  // Cached staleness check promise — shared between on-load and triggerAIAnalysis
140
142
  this._stalenessPromise = null;
141
143
  // Unique client ID for self-echo suppression on WebSocket review events.
@@ -192,6 +194,7 @@ class PRManager {
192
194
  if (diffOptionsBtn && window.DiffOptionsDropdown) {
193
195
  this.diffOptionsDropdown = new window.DiffOptionsDropdown(diffOptionsBtn, {
194
196
  onToggleWhitespace: (hide) => this.handleWhitespaceToggle(hide),
197
+ onToggleMinimize: (minimized) => this.handleMinimizeToggle(minimized),
195
198
  });
196
199
  }
197
200
 
@@ -461,6 +464,11 @@ class PRManager {
461
464
  // Render PR header with metadata
462
465
  this.renderPRHeader(prData);
463
466
 
467
+ // Set descriptive tab title
468
+ if (window.tabTitle) {
469
+ window.tabTitle.setBase(`PR #${number}`);
470
+ }
471
+
464
472
  // Fetch diff and file list from diff endpoint
465
473
  await this.loadAndDisplayFiles(owner, repo, number);
466
474
 
@@ -491,6 +499,7 @@ class PRManager {
491
499
  this.analysisHistoryManager = new window.AnalysisHistoryManager({
492
500
  reviewId: this.currentPR.id,
493
501
  mode: 'pr',
502
+ shaAbbrevLength: this.currentPR.shaAbbrevLength || 7,
494
503
  onSelectionChange: (runId, _run) => {
495
504
  this.selectedRunId = runId;
496
505
  this.loadAISuggestions(null, runId);
@@ -757,6 +766,18 @@ class PRManager {
757
766
  });
758
767
  }
759
768
 
769
+ /**
770
+ * Handle the minimize comments toggle from DiffOptionsDropdown.
771
+ * Toggles minimize mode which hides inline comments/suggestions and
772
+ * shows compact indicators on diff lines instead.
773
+ * @param {boolean} minimized - Whether to minimize comments
774
+ */
775
+ handleMinimizeToggle(minimized) {
776
+ if (this.commentMinimizer) {
777
+ this.commentMinimizer.setMinimized(minimized);
778
+ }
779
+ }
780
+
760
781
  /**
761
782
  * Parse unified diff to extract per-file patches
762
783
  * @param {string} diff - Full unified diff
@@ -879,7 +900,8 @@ class PRManager {
879
900
  const commitSha = document.getElementById('pr-commit-sha');
880
901
  const commitCopy = document.getElementById('pr-commit-copy');
881
902
  if (commitSha && pr.head_sha) {
882
- commitSha.textContent = pr.head_sha.substring(0, 7);
903
+ const abbrevLen = pr.shaAbbrevLength || 7;
904
+ commitSha.textContent = pr.head_sha.substring(0, abbrevLen);
883
905
  // Store full SHA for copying (updates on refresh)
884
906
  commitSha.dataset.fullSha = pr.head_sha;
885
907
 
@@ -2453,6 +2475,11 @@ class PRManager {
2453
2475
  }
2454
2476
  }
2455
2477
 
2478
+ // Refresh minimize-mode indicators so deleted comments no longer show
2479
+ if (this.commentMinimizer) {
2480
+ this.commentMinimizer.refreshIndicators();
2481
+ }
2482
+
2456
2483
  // Show success toast
2457
2484
  if (window.toast) {
2458
2485
  window.toast.showSuccess('Comment dismissed');
@@ -2673,6 +2700,11 @@ class PRManager {
2673
2700
  }
2674
2701
 
2675
2702
  this.updateCommentCount();
2703
+
2704
+ // Refresh minimize-mode indicators (no-op when minimize mode is off)
2705
+ if (this.commentMinimizer) {
2706
+ this.commentMinimizer.refreshIndicators();
2707
+ }
2676
2708
  } catch (error) {
2677
2709
  console.error('Error loading user comments:', error);
2678
2710
  }
@@ -2753,7 +2785,11 @@ class PRManager {
2753
2785
  }
2754
2786
 
2755
2787
  async displayAISuggestions(suggestions) {
2756
- return this.suggestionManager.displayAISuggestions(suggestions);
2788
+ await this.suggestionManager.displayAISuggestions(suggestions);
2789
+ // Refresh minimize-mode indicators (no-op when minimize mode is off)
2790
+ if (this.commentMinimizer) {
2791
+ this.commentMinimizer.refreshIndicators();
2792
+ }
2757
2793
  }
2758
2794
 
2759
2795
  createSuggestionRow(suggestions) {
@@ -2867,6 +2903,11 @@ class PRManager {
2867
2903
  }
2868
2904
 
2869
2905
  this.updateCommentCount();
2906
+
2907
+ // Refresh minimize-mode indicators so the adopted comment is reflected
2908
+ if (this.commentMinimizer) {
2909
+ this.commentMinimizer.refreshIndicators();
2910
+ }
2870
2911
  }
2871
2912
 
2872
2913
  /**
@@ -2991,6 +3032,11 @@ class PRManager {
2991
3032
  if (window.aiPanel) {
2992
3033
  window.aiPanel.updateFindingStatus(suggestionId, 'dismissed');
2993
3034
  }
3035
+
3036
+ // Refresh minimize-mode indicators after suggestion state change
3037
+ if (this.commentMinimizer) {
3038
+ this.commentMinimizer.refreshIndicators();
3039
+ }
2994
3040
  } catch (error) {
2995
3041
  console.error('Error dismissing suggestion:', error);
2996
3042
  alert('Failed to dismiss suggestion');
@@ -3041,6 +3087,11 @@ class PRManager {
3041
3087
  if (window.aiPanel) {
3042
3088
  window.aiPanel.updateFindingStatus(suggestionId, 'active');
3043
3089
  }
3090
+
3091
+ // Refresh minimize-mode indicators after suggestion state change
3092
+ if (this.commentMinimizer) {
3093
+ this.commentMinimizer.refreshIndicators();
3094
+ }
3044
3095
  } catch (error) {
3045
3096
  console.error('Error restoring suggestion:', error);
3046
3097
  alert('Failed to restore suggestion');
@@ -4365,6 +4416,14 @@ class PRManager {
4365
4416
  const hasData = await this._hasActiveSessionData();
4366
4417
  if (hasData) {
4367
4418
  this._showStaleBadge('stale');
4419
+ if (window.chatPanel) {
4420
+ const abbrevLen = this.currentPR?.shaAbbrevLength || 7;
4421
+ const oldSha = result.localHeadSha ? result.localHeadSha.substring(0, abbrevLen) : 'unknown';
4422
+ const newSha = result.remoteHeadSha ? result.remoteHeadSha.substring(0, abbrevLen) : 'unknown';
4423
+ window.chatPanel.queueDiffStateNotification(
4424
+ `PR HEAD has changed (${oldSha} → ${newSha}). The diff has not been refreshed yet.`
4425
+ );
4426
+ }
4368
4427
  } else {
4369
4428
  // No user work to protect — refresh silently
4370
4429
  await this.refreshPR();
@@ -4427,8 +4486,9 @@ class PRManager {
4427
4486
  /**
4428
4487
  * Show the stale badge with an optional variant class.
4429
4488
  * @param {'stale'|'closed'|'merged'} type
4489
+ * @param {string} [title] - Optional custom tooltip text. Falls back to type-specific defaults.
4430
4490
  */
4431
- _showStaleBadge(type) {
4491
+ _showStaleBadge(type, title) {
4432
4492
  const badge = document.getElementById('stale-badge');
4433
4493
  if (!badge) return;
4434
4494
 
@@ -4439,14 +4499,14 @@ class PRManager {
4439
4499
  if (type === 'merged') {
4440
4500
  badge.classList.add('pr-merged');
4441
4501
  if (textEl) textEl.textContent = 'MERGED';
4442
- badge.title = 'This PR has been merged';
4502
+ badge.title = title || 'This PR has been merged';
4443
4503
  } else if (type === 'closed') {
4444
4504
  badge.classList.add('pr-closed');
4445
4505
  if (textEl) textEl.textContent = 'CLOSED';
4446
- badge.title = 'This PR has been closed';
4506
+ badge.title = title || 'This PR has been closed';
4447
4507
  } else {
4448
4508
  if (textEl) textEl.textContent = 'STALE';
4449
- badge.title = 'PR data is outdated';
4509
+ badge.title = title || 'PR data is outdated';
4450
4510
  }
4451
4511
  badge.style.display = '';
4452
4512
  }
@@ -4482,6 +4542,8 @@ class PRManager {
4482
4542
  diffContainer.innerHTML = '<div class="loading">Refreshing pull request...</div>';
4483
4543
  }
4484
4544
 
4545
+ const oldHeadSha = this.currentPR?.head_sha;
4546
+
4485
4547
  try {
4486
4548
  // Call refresh API endpoint to fetch fresh data from GitHub
4487
4549
  const response = await fetch(`/api/pr/${owner}/${repo}/${number}/refresh`, {
@@ -4499,6 +4561,15 @@ class PRManager {
4499
4561
  if (data.success && data.data) {
4500
4562
  this.currentPR = data.data;
4501
4563
 
4564
+ // Notify chat agent if HEAD SHA changed
4565
+ const newHeadSha = data.data?.head_sha;
4566
+ if (window.chatPanel && oldHeadSha && newHeadSha && oldHeadSha !== newHeadSha) {
4567
+ const abbrevLen = this.currentPR?.shaAbbrevLength || 7;
4568
+ window.chatPanel.queueDiffStateNotification(
4569
+ `PR refreshed. HEAD changed: ${oldHeadSha.substring(0, abbrevLen)} → ${newHeadSha.substring(0, abbrevLen)}.`
4570
+ );
4571
+ }
4572
+
4502
4573
  // Save scroll position and expanded state
4503
4574
  const scrollPosition = window.scrollY;
4504
4575
  const expandedFolders = new Set(this.expandedFolders);
@@ -5149,6 +5220,11 @@ if (typeof document !== 'undefined') {
5149
5220
  window.PanelResizer.init();
5150
5221
  }
5151
5222
 
5223
+ // Initialize tab title manager
5224
+ if (typeof TabTitle !== 'undefined') {
5225
+ window.tabTitle = new TabTitle();
5226
+ }
5227
+
5152
5228
  prManager = new PRManager();
5153
5229
  // CRITICAL FIX: Make prManager available globally for component access
5154
5230
  window.prManager = prManager;
@@ -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
  * Repository Settings Page JavaScript
4
4
  * Handles loading, saving, and managing repository AI settings
@@ -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
  * Canonical category-to-emoji mapping for AI suggestion types.
4
4
  * Used by SuggestionManager and FileCommentManager to format adopted comments.
@@ -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
  * File ordering utilities for consistent file display across components.
4
4
  *
@@ -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
  * Safe markdown renderer for comments
4
4
  * Uses markdown-it with security settings to prevent XSS