@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.
- package/.pi/extensions/task/index.ts +1 -1
- package/.pi/skills/review-roulette/SKILL.md +1 -1
- package/LICENSE +201 -674
- package/README.md +2 -2
- package/bin/pair-review.js +1 -1
- package/package.json +2 -2
- package/plugin/.claude-plugin/plugin.json +2 -2
- package/plugin-code-critic/.claude-plugin/plugin.json +2 -2
- package/plugin-code-critic/skills/analyze/scripts/git-diff-lines +1 -1
- package/public/css/ai-summary-modal.css +1 -1
- package/public/css/pr.css +194 -0
- package/public/index.html +168 -3
- package/public/js/components/AIPanel.js +17 -3
- package/public/js/components/AISummaryModal.js +1 -1
- package/public/js/components/AdvancedConfigTab.js +1 -1
- package/public/js/components/AnalysisConfigModal.js +1 -1
- package/public/js/components/ChatPanel.js +42 -7
- package/public/js/components/ConfirmDialog.js +22 -3
- package/public/js/components/CouncilProgressModal.js +14 -1
- package/public/js/components/DiffOptionsDropdown.js +411 -24
- package/public/js/components/EmojiPicker.js +1 -1
- package/public/js/components/KeyboardShortcuts.js +1 -1
- package/public/js/components/PanelGroup.js +1 -1
- package/public/js/components/PreviewModal.js +1 -1
- package/public/js/components/ReviewModal.js +1 -1
- package/public/js/components/SplitButton.js +1 -1
- package/public/js/components/StatusIndicator.js +1 -1
- package/public/js/components/SuggestionNavigator.js +13 -6
- package/public/js/components/TabTitle.js +96 -0
- package/public/js/components/TextInputDialog.js +1 -1
- package/public/js/components/TimeoutSelect.js +1 -1
- package/public/js/components/Toast.js +7 -1
- package/public/js/components/VoiceCentricConfigTab.js +1 -1
- package/public/js/index.js +649 -44
- package/public/js/local.js +570 -77
- package/public/js/modules/analysis-history.js +4 -3
- package/public/js/modules/comment-manager.js +6 -1
- package/public/js/modules/comment-minimizer.js +304 -0
- package/public/js/modules/diff-context.js +1 -1
- package/public/js/modules/diff-renderer.js +1 -1
- package/public/js/modules/file-comment-manager.js +1 -1
- package/public/js/modules/file-list-merger.js +1 -1
- package/public/js/modules/gap-coordinates.js +1 -1
- package/public/js/modules/hunk-parser.js +1 -1
- package/public/js/modules/line-tracker.js +1 -1
- package/public/js/modules/panel-resizer.js +1 -1
- package/public/js/modules/storage-cleanup.js +1 -1
- package/public/js/modules/suggestion-manager.js +1 -1
- package/public/js/pr.js +83 -7
- package/public/js/repo-settings.js +1 -1
- package/public/js/utils/category-emoji.js +1 -1
- package/public/js/utils/file-order.js +1 -1
- package/public/js/utils/markdown.js +1 -1
- package/public/js/utils/suggestion-ui.js +1 -1
- package/public/js/utils/tier-icons.js +1 -1
- package/public/js/utils/time.js +1 -1
- package/public/js/ws-client.js +1 -1
- package/public/local.html +14 -0
- package/public/pr.html +3 -0
- package/public/setup.html +1 -1
- package/src/ai/analyzer.js +18 -12
- package/src/ai/claude-cli.js +1 -1
- package/src/ai/claude-provider.js +1 -1
- package/src/ai/codex-provider.js +1 -1
- package/src/ai/copilot-provider.js +1 -1
- package/src/ai/cursor-agent-provider.js +1 -1
- package/src/ai/gemini-provider.js +1 -1
- package/src/ai/index.js +1 -1
- package/src/ai/opencode-provider.js +1 -1
- package/src/ai/pi-provider.js +1 -1
- package/src/ai/prompts/baseline/consolidation/balanced.js +1 -1
- package/src/ai/prompts/baseline/consolidation/fast.js +1 -1
- package/src/ai/prompts/baseline/consolidation/thorough.js +1 -1
- package/src/ai/prompts/baseline/level1/balanced.js +1 -1
- package/src/ai/prompts/baseline/level1/fast.js +1 -1
- package/src/ai/prompts/baseline/level1/thorough.js +1 -1
- package/src/ai/prompts/baseline/level2/balanced.js +1 -1
- package/src/ai/prompts/baseline/level2/fast.js +1 -1
- package/src/ai/prompts/baseline/level2/thorough.js +1 -1
- package/src/ai/prompts/baseline/level3/balanced.js +1 -1
- package/src/ai/prompts/baseline/level3/fast.js +1 -1
- package/src/ai/prompts/baseline/level3/thorough.js +1 -1
- package/src/ai/prompts/baseline/orchestration/balanced.js +1 -1
- package/src/ai/prompts/baseline/orchestration/fast.js +1 -1
- package/src/ai/prompts/baseline/orchestration/thorough.js +1 -1
- package/src/ai/prompts/config.js +1 -1
- package/src/ai/prompts/index.js +1 -1
- package/src/ai/prompts/line-number-guidance.js +1 -1
- package/src/ai/prompts/render-for-skill.js +1 -1
- package/src/ai/prompts/shared/diff-instructions.js +1 -1
- package/src/ai/prompts/shared/output-schema.js +1 -1
- package/src/ai/prompts/shared/valid-files.js +1 -1
- package/src/ai/prompts/sparse-checkout-guidance.js +1 -1
- package/src/ai/provider-availability.js +1 -1
- package/src/ai/provider.js +1 -1
- package/src/ai/stream-parser.js +1 -1
- package/src/chat/acp-bridge.js +1 -1
- package/src/chat/api-reference.js +1 -1
- package/src/chat/chat-providers.js +1 -1
- package/src/chat/claude-code-bridge.js +1 -1
- package/src/chat/codex-bridge.js +1 -1
- package/src/chat/pi-bridge.js +1 -1
- package/src/chat/prompt-builder.js +1 -1
- package/src/chat/session-manager.js +1 -1
- package/src/config.js +3 -1
- package/src/database.js +591 -40
- package/src/events/review-events.js +1 -1
- package/src/git/base-branch.js +173 -0
- package/src/git/gitattributes.js +1 -1
- package/src/git/sha-abbrev.js +35 -0
- package/src/git/worktree.js +1 -1
- package/src/github/client.js +33 -2
- package/src/github/parser.js +1 -1
- package/src/hooks/hook-runner.js +100 -0
- package/src/hooks/payloads.js +212 -0
- package/src/local-review.js +469 -130
- package/src/local-scope.js +58 -0
- package/src/main.js +56 -5
- package/src/mcp-stdio.js +1 -1
- package/src/protocol-handler.js +1 -1
- package/src/routes/analyses.js +74 -11
- package/src/routes/chat.js +34 -1
- package/src/routes/config.js +2 -1
- package/src/routes/context-files.js +1 -1
- package/src/routes/councils.js +1 -1
- package/src/routes/github-collections.js +1 -1
- package/src/routes/local.js +735 -69
- package/src/routes/mcp.js +21 -11
- package/src/routes/pr.js +91 -13
- package/src/routes/reviews.js +1 -1
- package/src/routes/setup.js +2 -1
- package/src/routes/shared.js +1 -1
- package/src/routes/worktrees.js +213 -149
- package/src/server.js +31 -1
- package/src/setup/local-setup.js +47 -6
- package/src/setup/pr-setup.js +29 -6
- package/src/utils/auto-context.js +1 -1
- package/src/utils/category-emoji.js +1 -1
- package/src/utils/comment-formatter.js +1 -1
- package/src/utils/diff-annotator.js +1 -1
- package/src/utils/diff-file-list.js +1 -1
- package/src/utils/instructions.js +1 -1
- package/src/utils/json-extractor.js +1 -1
- package/src/utils/line-validation.js +1 -1
- package/src/utils/logger.js +1 -1
- package/src/utils/paths.js +1 -1
- package/src/utils/safe-parse-json.js +1 -1
- package/src/utils/stats-calculator.js +1 -1
- package/src/ws/index.js +1 -1
- package/src/ws/server.js +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// SPDX-License-Identifier:
|
|
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,
|
|
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:
|
|
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:
|
|
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:
|
|
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,
|
package/public/js/pr.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// SPDX-License-Identifier:
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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.
|