@in-the-loop-labs/pair-review 1.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/LICENSE +674 -0
- package/README.md +371 -0
- package/bin/git-diff-lines +146 -0
- package/bin/pair-review.js +49 -0
- package/package.json +71 -0
- package/public/css/ai-summary-modal.css +183 -0
- package/public/css/pr.css +8698 -0
- package/public/css/repo-settings.css +891 -0
- package/public/css/styles.css +479 -0
- package/public/favicon.png +0 -0
- package/public/index.html +1104 -0
- package/public/js/components/AIPanel.js +1639 -0
- package/public/js/components/AISummaryModal.js +278 -0
- package/public/js/components/AnalysisConfigModal.js +684 -0
- package/public/js/components/ConfirmDialog.js +227 -0
- package/public/js/components/PreviewModal.js +344 -0
- package/public/js/components/ProgressModal.js +678 -0
- package/public/js/components/ReviewModal.js +531 -0
- package/public/js/components/SplitButton.js +382 -0
- package/public/js/components/StatusIndicator.js +265 -0
- package/public/js/components/SuggestionNavigator.js +489 -0
- package/public/js/components/Toast.js +166 -0
- package/public/js/local.js +1580 -0
- package/public/js/modules/analysis-history.js +940 -0
- package/public/js/modules/comment-manager.js +643 -0
- package/public/js/modules/diff-renderer.js +585 -0
- package/public/js/modules/file-comment-manager.js +1242 -0
- package/public/js/modules/gap-coordinates.js +190 -0
- package/public/js/modules/hunk-parser.js +358 -0
- package/public/js/modules/line-tracker.js +386 -0
- package/public/js/modules/panel-resizer.js +228 -0
- package/public/js/modules/storage-cleanup.js +36 -0
- package/public/js/modules/suggestion-manager.js +692 -0
- package/public/js/pr.js +3503 -0
- package/public/js/repo-settings.js +691 -0
- package/public/js/utils/file-order.js +87 -0
- package/public/js/utils/markdown.js +97 -0
- package/public/js/utils/suggestion-ui.js +55 -0
- package/public/js/utils/tier-icons.js +25 -0
- package/public/local.html +460 -0
- package/public/pr.html +329 -0
- package/public/repo-settings.html +243 -0
- package/src/ai/analyzer.js +2592 -0
- package/src/ai/claude-cli.js +153 -0
- package/src/ai/claude-provider.js +261 -0
- package/src/ai/codex-provider.js +361 -0
- package/src/ai/copilot-provider.js +345 -0
- package/src/ai/gemini-provider.js +375 -0
- package/src/ai/index.js +47 -0
- package/src/ai/prompts/baseline/_meta.json +14 -0
- package/src/ai/prompts/baseline/level1/balanced.js +239 -0
- package/src/ai/prompts/baseline/level1/fast.js +194 -0
- package/src/ai/prompts/baseline/level1/thorough.js +319 -0
- package/src/ai/prompts/baseline/level2/balanced.js +248 -0
- package/src/ai/prompts/baseline/level2/fast.js +201 -0
- package/src/ai/prompts/baseline/level2/thorough.js +367 -0
- package/src/ai/prompts/baseline/level3/balanced.js +280 -0
- package/src/ai/prompts/baseline/level3/fast.js +220 -0
- package/src/ai/prompts/baseline/level3/thorough.js +459 -0
- package/src/ai/prompts/baseline/orchestration/balanced.js +259 -0
- package/src/ai/prompts/baseline/orchestration/fast.js +213 -0
- package/src/ai/prompts/baseline/orchestration/thorough.js +446 -0
- package/src/ai/prompts/config.js +52 -0
- package/src/ai/prompts/index.js +267 -0
- package/src/ai/prompts/shared/diff-instructions.js +50 -0
- package/src/ai/prompts/shared/output-schema.js +179 -0
- package/src/ai/prompts/shared/valid-files.js +37 -0
- package/src/ai/provider.js +260 -0
- package/src/config.js +139 -0
- package/src/database.js +2284 -0
- package/src/git/gitattributes.js +207 -0
- package/src/git/worktree.js +688 -0
- package/src/github/client.js +893 -0
- package/src/github/parser.js +247 -0
- package/src/local-review.js +691 -0
- package/src/main.js +987 -0
- package/src/routes/analysis.js +897 -0
- package/src/routes/comments.js +534 -0
- package/src/routes/config.js +250 -0
- package/src/routes/local.js +1728 -0
- package/src/routes/pr.js +1164 -0
- package/src/routes/shared.js +218 -0
- package/src/routes/worktrees.js +500 -0
- package/src/server.js +295 -0
- package/src/utils/diff-annotator.js +414 -0
- package/src/utils/instructions.js +33 -0
- package/src/utils/json-extractor.js +107 -0
- package/src/utils/line-validation.js +183 -0
- package/src/utils/logger.js +142 -0
- package/src/utils/paths.js +161 -0
- package/src/utils/stats-calculator.js +86 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
/**
|
|
3
|
+
* LineTracker - Line number mapping, range selection, and highlighting
|
|
4
|
+
* Handles line number extraction, range selection for multi-line comments,
|
|
5
|
+
* and line highlighting for the diff view.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
class LineTracker {
|
|
9
|
+
constructor() {
|
|
10
|
+
// Line range selection state
|
|
11
|
+
this.rangeSelectionStart = null;
|
|
12
|
+
this.rangeSelectionEnd = null;
|
|
13
|
+
this.isDraggingRange = false;
|
|
14
|
+
this.dragStartLine = null;
|
|
15
|
+
this.dragEndLine = null;
|
|
16
|
+
this.potentialDragStart = null;
|
|
17
|
+
// Global mouseup handler reference for cleanup
|
|
18
|
+
this.handleGlobalMouseUp = null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Get the line number from a diff row
|
|
23
|
+
* Handles both added/context lines (new line numbers) and deleted lines (old line numbers).
|
|
24
|
+
*
|
|
25
|
+
* When side is specified:
|
|
26
|
+
* - 'LEFT': Returns the OLD line number (for deleted lines or context lines in OLD coordinate system)
|
|
27
|
+
* - 'RIGHT': Returns the NEW line number (for added lines or context lines in NEW coordinate system)
|
|
28
|
+
*
|
|
29
|
+
* When side is not specified (default behavior):
|
|
30
|
+
* - Returns dataset.lineNumber which is the primary line number for the row's type
|
|
31
|
+
* - For deleted lines: returns oldNumber
|
|
32
|
+
* - For added/context lines: returns newNumber
|
|
33
|
+
*
|
|
34
|
+
* Priority order:
|
|
35
|
+
* 1. dataset.lineNumber (or oldLineNumber/newLineNumber based on side)
|
|
36
|
+
* 2. .line-num2: new line numbers for added/context lines
|
|
37
|
+
* 3. .line-num1: old line numbers for deleted lines
|
|
38
|
+
* 4. Nested selectors as fallback
|
|
39
|
+
* @param {Element} row - Table row element
|
|
40
|
+
* @param {string} [side] - Optional side ('LEFT' or 'RIGHT') to get specific coordinate system
|
|
41
|
+
* @returns {number|null} The line number or null if not found
|
|
42
|
+
*/
|
|
43
|
+
getLineNumber(row, side) {
|
|
44
|
+
// If a specific side is requested, use the appropriate line number
|
|
45
|
+
if (side === 'LEFT') {
|
|
46
|
+
// LEFT side: use old line number
|
|
47
|
+
// Check data-old-line-number first (available on deleted lines and context lines)
|
|
48
|
+
if (row.dataset?.oldLineNumber) {
|
|
49
|
+
const oldNum = parseInt(row.dataset.oldLineNumber);
|
|
50
|
+
if (!isNaN(oldNum)) return oldNum;
|
|
51
|
+
}
|
|
52
|
+
// Fallback to .line-num1 span
|
|
53
|
+
const lineNum1 = row.querySelector('.line-num1')?.textContent?.trim();
|
|
54
|
+
if (lineNum1) return parseInt(lineNum1);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (side === 'RIGHT') {
|
|
59
|
+
// RIGHT side: use new line number
|
|
60
|
+
// Check data-new-line-number first (available on added lines and context lines)
|
|
61
|
+
if (row.dataset?.newLineNumber) {
|
|
62
|
+
const newNum = parseInt(row.dataset.newLineNumber);
|
|
63
|
+
if (!isNaN(newNum)) return newNum;
|
|
64
|
+
}
|
|
65
|
+
// Check data-line-number as fallback (for added/context lines, this is the new number)
|
|
66
|
+
if (row.dataset?.lineNumber && row.dataset?.side === 'RIGHT') {
|
|
67
|
+
const datasetNum = parseInt(row.dataset.lineNumber);
|
|
68
|
+
if (!isNaN(datasetNum)) return datasetNum;
|
|
69
|
+
}
|
|
70
|
+
// Fallback to .line-num2 span
|
|
71
|
+
const lineNum2 = row.querySelector('.line-num2')?.textContent?.trim();
|
|
72
|
+
if (lineNum2) return parseInt(lineNum2);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Default behavior (no side specified): return the row's primary line number
|
|
77
|
+
// Primary: use dataset.lineNumber if available (set during renderDiffLine)
|
|
78
|
+
// This correctly handles both deleted lines (uses oldNumber) and added/context lines (uses newNumber)
|
|
79
|
+
if (row.dataset?.lineNumber) {
|
|
80
|
+
const datasetNum = parseInt(row.dataset.lineNumber);
|
|
81
|
+
if (!isNaN(datasetNum)) return datasetNum;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Fallback: check span elements
|
|
85
|
+
// For added/context lines, check .line-num2 (new line number)
|
|
86
|
+
let lineNum = row.querySelector('.line-num2')?.textContent?.trim();
|
|
87
|
+
if (lineNum) return parseInt(lineNum);
|
|
88
|
+
|
|
89
|
+
// For deleted lines, check .line-num1 (old line number)
|
|
90
|
+
lineNum = row.querySelector('.line-num1')?.textContent?.trim();
|
|
91
|
+
if (lineNum) return parseInt(lineNum);
|
|
92
|
+
|
|
93
|
+
// Alternative: .line-num-new
|
|
94
|
+
lineNum = row.querySelector('.line-num-new')?.textContent?.trim();
|
|
95
|
+
if (lineNum) return parseInt(lineNum);
|
|
96
|
+
|
|
97
|
+
// Nested: inside .d2h-code-linenumber container
|
|
98
|
+
const lineNumCell = row.querySelector('.d2h-code-linenumber');
|
|
99
|
+
if (lineNumCell) {
|
|
100
|
+
const lineNum2 = lineNumCell.querySelector('.line-num2');
|
|
101
|
+
if (lineNum2) {
|
|
102
|
+
lineNum = lineNum2.textContent?.trim();
|
|
103
|
+
if (lineNum) return parseInt(lineNum);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Build a set of visible line numbers for a file element
|
|
112
|
+
* This is more efficient than checking each line individually when processing multiple suggestions
|
|
113
|
+
* @param {Element} fileElement - The file wrapper element
|
|
114
|
+
* @returns {Set<number>} Set of visible line numbers
|
|
115
|
+
*/
|
|
116
|
+
buildVisibleLinesSet(fileElement) {
|
|
117
|
+
const visibleLines = new Set();
|
|
118
|
+
const lineRows = fileElement.querySelectorAll('tr');
|
|
119
|
+
|
|
120
|
+
for (const row of lineRows) {
|
|
121
|
+
const lineNum = this.getLineNumber(row);
|
|
122
|
+
if (lineNum !== null) {
|
|
123
|
+
visibleLines.add(lineNum);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return visibleLines;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Start line range selection
|
|
132
|
+
* @param {HTMLElement} row - The starting row
|
|
133
|
+
* @param {number} lineNumber - The line number
|
|
134
|
+
* @param {string} fileName - The file name
|
|
135
|
+
* @param {string} side - The side ('LEFT' or 'RIGHT')
|
|
136
|
+
*/
|
|
137
|
+
startRangeSelection(row, lineNumber, fileName, side = 'RIGHT') {
|
|
138
|
+
// Clear any existing selection
|
|
139
|
+
this.clearRangeSelection();
|
|
140
|
+
|
|
141
|
+
// Set start of range (including side for GitHub API)
|
|
142
|
+
this.rangeSelectionStart = {
|
|
143
|
+
row: row,
|
|
144
|
+
lineNumber: lineNumber,
|
|
145
|
+
fileName: fileName,
|
|
146
|
+
side: side
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Add visual indicator
|
|
150
|
+
row.classList.add('line-range-start');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Complete line range selection
|
|
155
|
+
* @param {HTMLElement} endRow - The ending row
|
|
156
|
+
* @param {number} endLineNumber - The ending line number
|
|
157
|
+
* @param {string} fileName - The file name
|
|
158
|
+
* @param {Function} showCommentFormCallback - Callback to show comment form
|
|
159
|
+
*/
|
|
160
|
+
completeRangeSelection(endRow, endLineNumber, fileName, showCommentFormCallback) {
|
|
161
|
+
if (!this.rangeSelectionStart) return;
|
|
162
|
+
|
|
163
|
+
// Ensure we're in the same file
|
|
164
|
+
if (this.rangeSelectionStart.fileName !== fileName) {
|
|
165
|
+
alert('Cannot select range across different files');
|
|
166
|
+
this.clearRangeSelection();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const startLine = this.rangeSelectionStart.lineNumber;
|
|
171
|
+
const endLine = endLineNumber;
|
|
172
|
+
|
|
173
|
+
// Ensure start is before end
|
|
174
|
+
const minLine = Math.min(startLine, endLine);
|
|
175
|
+
const maxLine = Math.max(startLine, endLine);
|
|
176
|
+
|
|
177
|
+
// Highlight all rows in range (pass side to avoid highlighting both deleted and added lines with same line number)
|
|
178
|
+
const side = this.rangeSelectionStart.side;
|
|
179
|
+
this.highlightLineRange(this.rangeSelectionStart.row, endRow, fileName, minLine, maxLine, side);
|
|
180
|
+
|
|
181
|
+
// Store end of range
|
|
182
|
+
this.rangeSelectionEnd = {
|
|
183
|
+
row: endRow,
|
|
184
|
+
lineNumber: endLineNumber,
|
|
185
|
+
fileName: fileName
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// Get diff position from the end row (GitHub uses position at end of range)
|
|
189
|
+
const diffPosition = endRow.dataset.diffPosition;
|
|
190
|
+
|
|
191
|
+
if (showCommentFormCallback) {
|
|
192
|
+
showCommentFormCallback(endRow, minLine, fileName, diffPosition, maxLine, side || 'RIGHT');
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Highlight all lines in a range
|
|
198
|
+
* @param {HTMLElement} startRow - The starting row element
|
|
199
|
+
* @param {HTMLElement} endRow - The ending row element
|
|
200
|
+
* @param {string} fileName - The file name
|
|
201
|
+
* @param {number} minLine - The minimum line number
|
|
202
|
+
* @param {number} maxLine - The maximum line number
|
|
203
|
+
* @param {string} side - The side of the diff ('LEFT' for deleted lines, 'RIGHT' for added/context)
|
|
204
|
+
*/
|
|
205
|
+
highlightLineRange(startRow, endRow, fileName, minLine, maxLine, side) {
|
|
206
|
+
// Find all rows in the file between minLine and maxLine
|
|
207
|
+
const fileWrapper = startRow.closest('.d2h-file-wrapper');
|
|
208
|
+
if (!fileWrapper) return;
|
|
209
|
+
|
|
210
|
+
const allRows = fileWrapper.querySelectorAll('tr[data-line-number]');
|
|
211
|
+
|
|
212
|
+
allRows.forEach(row => {
|
|
213
|
+
const lineNum = parseInt(row.dataset.lineNumber);
|
|
214
|
+
const rowSide = row.dataset.side || 'RIGHT';
|
|
215
|
+
// Match by line number range, file name, and side
|
|
216
|
+
// This prevents deleted lines (LEFT) from matching added/context lines (RIGHT) with same line number
|
|
217
|
+
if (lineNum >= minLine && lineNum <= maxLine &&
|
|
218
|
+
row.dataset.fileName === fileName &&
|
|
219
|
+
rowSide === side) {
|
|
220
|
+
row.classList.add('line-range-selected');
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Clear line range selection
|
|
227
|
+
*/
|
|
228
|
+
clearRangeSelection() {
|
|
229
|
+
// Remove all selection highlights
|
|
230
|
+
document.querySelectorAll('.line-range-start, .line-range-selected').forEach(row => {
|
|
231
|
+
row.classList.remove('line-range-start', 'line-range-selected');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// Clean up global listener if it exists
|
|
235
|
+
if (this.handleGlobalMouseUp) {
|
|
236
|
+
document.removeEventListener('mouseup', this.handleGlobalMouseUp);
|
|
237
|
+
this.handleGlobalMouseUp = null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Clear state
|
|
241
|
+
this.rangeSelectionStart = null;
|
|
242
|
+
this.rangeSelectionEnd = null;
|
|
243
|
+
this.isDraggingRange = false;
|
|
244
|
+
this.dragStartLine = null;
|
|
245
|
+
this.dragEndLine = null;
|
|
246
|
+
this.potentialDragStart = null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Start drag selection
|
|
251
|
+
* @param {HTMLElement} row - The starting row
|
|
252
|
+
* @param {number} lineNumber - The line number
|
|
253
|
+
* @param {string} fileName - The file name
|
|
254
|
+
* @param {string} side - The side ('LEFT' or 'RIGHT')
|
|
255
|
+
*/
|
|
256
|
+
startDragSelection(row, lineNumber, fileName, side = 'RIGHT') {
|
|
257
|
+
// Clear any existing selection and ensure cleanup
|
|
258
|
+
this.clearRangeSelection();
|
|
259
|
+
|
|
260
|
+
// Set dragging state
|
|
261
|
+
this.isDraggingRange = true;
|
|
262
|
+
this.dragStartLine = lineNumber;
|
|
263
|
+
this.dragEndLine = lineNumber;
|
|
264
|
+
|
|
265
|
+
// Set start of range, including side for GitHub API
|
|
266
|
+
this.rangeSelectionStart = {
|
|
267
|
+
row: row,
|
|
268
|
+
lineNumber: lineNumber,
|
|
269
|
+
fileName: fileName,
|
|
270
|
+
side: side
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// Add visual indicator
|
|
274
|
+
row.classList.add('line-range-selected');
|
|
275
|
+
|
|
276
|
+
// Add global mouse up handler to catch mouseup outside of line numbers
|
|
277
|
+
// Store as bound function for reliable cleanup
|
|
278
|
+
this.handleGlobalMouseUp = (e) => {
|
|
279
|
+
if (this.isDraggingRange) {
|
|
280
|
+
this.completeDragSelection(row, this.dragEndLine || lineNumber, fileName);
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
document.addEventListener('mouseup', this.handleGlobalMouseUp);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Update drag selection as mouse moves
|
|
288
|
+
* @param {HTMLElement} row - The current row
|
|
289
|
+
* @param {number} lineNumber - The current line number
|
|
290
|
+
* @param {string} fileName - The file name
|
|
291
|
+
*/
|
|
292
|
+
updateDragSelection(row, lineNumber, fileName) {
|
|
293
|
+
if (!this.isDraggingRange || !this.rangeSelectionStart) return;
|
|
294
|
+
|
|
295
|
+
// Ensure we're in the same file
|
|
296
|
+
if (this.rangeSelectionStart.fileName !== fileName) return;
|
|
297
|
+
|
|
298
|
+
// Update end line
|
|
299
|
+
this.dragEndLine = lineNumber;
|
|
300
|
+
|
|
301
|
+
// Update end of range
|
|
302
|
+
this.rangeSelectionEnd = {
|
|
303
|
+
row: row,
|
|
304
|
+
lineNumber: lineNumber,
|
|
305
|
+
fileName: fileName
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
// Clear existing highlights
|
|
309
|
+
document.querySelectorAll('.line-range-selected').forEach(r => {
|
|
310
|
+
r.classList.remove('line-range-selected');
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Highlight all rows in range (pass side to avoid highlighting both deleted and added lines with same line number)
|
|
314
|
+
const minLine = Math.min(this.dragStartLine, lineNumber);
|
|
315
|
+
const maxLine = Math.max(this.dragStartLine, lineNumber);
|
|
316
|
+
const side = this.rangeSelectionStart.side;
|
|
317
|
+
this.highlightLineRange(this.rangeSelectionStart.row, row, fileName, minLine, maxLine, side);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Complete drag selection
|
|
322
|
+
* @param {HTMLElement} row - The ending row
|
|
323
|
+
* @param {number} lineNumber - The ending line number
|
|
324
|
+
* @param {string} fileName - The file name
|
|
325
|
+
*/
|
|
326
|
+
completeDragSelection(row, lineNumber, fileName) {
|
|
327
|
+
if (!this.isDraggingRange) return;
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
// Update end of range
|
|
331
|
+
this.rangeSelectionEnd = {
|
|
332
|
+
row: row,
|
|
333
|
+
lineNumber: lineNumber,
|
|
334
|
+
fileName: fileName
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
// If we have a valid range (more than one line), keep selection
|
|
338
|
+
const minLine = Math.min(this.dragStartLine, this.dragEndLine);
|
|
339
|
+
const maxLine = Math.max(this.dragStartLine, this.dragEndLine);
|
|
340
|
+
|
|
341
|
+
if (minLine === maxLine) {
|
|
342
|
+
// Single line - clear selection
|
|
343
|
+
this.clearRangeSelection();
|
|
344
|
+
} else {
|
|
345
|
+
// Multi-line - keep selection for user to click + button
|
|
346
|
+
// The selection stays highlighted until they click a comment button or clear it
|
|
347
|
+
}
|
|
348
|
+
} finally {
|
|
349
|
+
// Always clean up the global listener and dragging state
|
|
350
|
+
if (this.handleGlobalMouseUp) {
|
|
351
|
+
document.removeEventListener('mouseup', this.handleGlobalMouseUp);
|
|
352
|
+
this.handleGlobalMouseUp = null;
|
|
353
|
+
}
|
|
354
|
+
this.isDraggingRange = false;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Check if there is an active range selection
|
|
360
|
+
* @returns {boolean} True if there is an active range selection
|
|
361
|
+
*/
|
|
362
|
+
hasActiveSelection() {
|
|
363
|
+
return this.rangeSelectionStart !== null && this.rangeSelectionEnd !== null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Get the current selection range
|
|
368
|
+
* @returns {{ start: number, end: number, fileName: string, side: string }|null}
|
|
369
|
+
*/
|
|
370
|
+
getSelectionRange() {
|
|
371
|
+
if (!this.hasActiveSelection()) return null;
|
|
372
|
+
|
|
373
|
+
const minLine = Math.min(this.rangeSelectionStart.lineNumber, this.rangeSelectionEnd.lineNumber);
|
|
374
|
+
const maxLine = Math.max(this.rangeSelectionStart.lineNumber, this.rangeSelectionEnd.lineNumber);
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
start: minLine,
|
|
378
|
+
end: maxLine,
|
|
379
|
+
fileName: this.rangeSelectionStart.fileName,
|
|
380
|
+
side: this.rangeSelectionStart.side
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Make LineTracker available globally
|
|
386
|
+
window.LineTracker = LineTracker;
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
/**
|
|
3
|
+
* Panel Resizer Module
|
|
4
|
+
* Handles drag-to-resize functionality for the sidebar and AI panel
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
window.PanelResizer = (function() {
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
// Configuration
|
|
11
|
+
const CONFIG = {
|
|
12
|
+
sidebar: {
|
|
13
|
+
min: 150,
|
|
14
|
+
max: 400,
|
|
15
|
+
default: 260,
|
|
16
|
+
storageKey: 'sidebar-width',
|
|
17
|
+
cssVar: '--sidebar-width'
|
|
18
|
+
},
|
|
19
|
+
'ai-panel': {
|
|
20
|
+
min: 200,
|
|
21
|
+
max: 600,
|
|
22
|
+
default: 320,
|
|
23
|
+
storageKey: 'ai-panel-width',
|
|
24
|
+
cssVar: '--ai-panel-width'
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// State
|
|
29
|
+
let isDragging = false;
|
|
30
|
+
let currentPanel = null;
|
|
31
|
+
let startX = 0;
|
|
32
|
+
let startWidth = 0;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Initialize the panel resizer
|
|
36
|
+
*/
|
|
37
|
+
function init() {
|
|
38
|
+
// Apply saved widths on load
|
|
39
|
+
applySavedWidths();
|
|
40
|
+
|
|
41
|
+
// Set up event listeners for resize handles
|
|
42
|
+
const handles = document.querySelectorAll('.resize-handle');
|
|
43
|
+
handles.forEach(handle => {
|
|
44
|
+
handle.addEventListener('mousedown', onMouseDown);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Global mouse events for drag
|
|
48
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
49
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Apply saved widths from localStorage
|
|
54
|
+
*/
|
|
55
|
+
function applySavedWidths() {
|
|
56
|
+
Object.keys(CONFIG).forEach(panelName => {
|
|
57
|
+
const config = CONFIG[panelName];
|
|
58
|
+
const savedWidth = localStorage.getItem(config.storageKey);
|
|
59
|
+
|
|
60
|
+
if (savedWidth) {
|
|
61
|
+
const width = parseInt(savedWidth, 10);
|
|
62
|
+
if (width >= config.min && width <= config.max) {
|
|
63
|
+
document.documentElement.style.setProperty(config.cssVar, `${width}px`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Get the current width of a panel
|
|
71
|
+
* @param {string} panelName - 'sidebar' or 'ai-panel'
|
|
72
|
+
* @returns {number} Current width in pixels
|
|
73
|
+
*/
|
|
74
|
+
function getPanelWidth(panelName) {
|
|
75
|
+
const config = CONFIG[panelName];
|
|
76
|
+
if (!config) return 0;
|
|
77
|
+
|
|
78
|
+
const cssValue = getComputedStyle(document.documentElement)
|
|
79
|
+
.getPropertyValue(config.cssVar)
|
|
80
|
+
.trim();
|
|
81
|
+
|
|
82
|
+
return parseInt(cssValue, 10) || config.default;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Set the width of a panel
|
|
87
|
+
* @param {string} panelName - 'sidebar' or 'ai-panel'
|
|
88
|
+
* @param {number} width - Width in pixels
|
|
89
|
+
* @param {boolean} save - Whether to save to localStorage
|
|
90
|
+
*/
|
|
91
|
+
function setPanelWidth(panelName, width, save = true) {
|
|
92
|
+
const config = CONFIG[panelName];
|
|
93
|
+
if (!config) return;
|
|
94
|
+
|
|
95
|
+
// Clamp to min/max
|
|
96
|
+
const clampedWidth = Math.max(config.min, Math.min(config.max, width));
|
|
97
|
+
|
|
98
|
+
// Apply to CSS
|
|
99
|
+
document.documentElement.style.setProperty(config.cssVar, `${clampedWidth}px`);
|
|
100
|
+
|
|
101
|
+
// Save to localStorage
|
|
102
|
+
if (save) {
|
|
103
|
+
localStorage.setItem(config.storageKey, clampedWidth.toString());
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Handle mousedown on resize handle
|
|
109
|
+
* @param {MouseEvent} e
|
|
110
|
+
*/
|
|
111
|
+
function onMouseDown(e) {
|
|
112
|
+
const handle = e.target.closest('.resize-handle');
|
|
113
|
+
if (!handle) return;
|
|
114
|
+
|
|
115
|
+
const panelName = handle.dataset.panel;
|
|
116
|
+
const panelEl = panelName === 'sidebar'
|
|
117
|
+
? document.getElementById('files-sidebar')
|
|
118
|
+
: document.getElementById('ai-panel');
|
|
119
|
+
|
|
120
|
+
// Don't allow resize if panel is collapsed
|
|
121
|
+
if (panelEl && panelEl.classList.contains('collapsed')) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
isDragging = true;
|
|
126
|
+
currentPanel = panelName;
|
|
127
|
+
startX = e.clientX;
|
|
128
|
+
startWidth = getPanelWidth(panelName);
|
|
129
|
+
|
|
130
|
+
// Add visual feedback
|
|
131
|
+
handle.classList.add('dragging');
|
|
132
|
+
document.body.classList.add('resizing');
|
|
133
|
+
|
|
134
|
+
e.preventDefault();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Handle mousemove during drag
|
|
139
|
+
* @param {MouseEvent} e
|
|
140
|
+
*/
|
|
141
|
+
function onMouseMove(e) {
|
|
142
|
+
if (!isDragging || !currentPanel) return;
|
|
143
|
+
|
|
144
|
+
const config = CONFIG[currentPanel];
|
|
145
|
+
if (!config) return;
|
|
146
|
+
|
|
147
|
+
// Calculate delta based on panel position
|
|
148
|
+
// For sidebar (left panel): moving right increases width
|
|
149
|
+
// For ai-panel (right panel): moving left increases width
|
|
150
|
+
let delta;
|
|
151
|
+
if (currentPanel === 'sidebar') {
|
|
152
|
+
delta = e.clientX - startX;
|
|
153
|
+
} else {
|
|
154
|
+
delta = startX - e.clientX;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const newWidth = startWidth + delta;
|
|
158
|
+
setPanelWidth(currentPanel, newWidth, false); // Don't save during drag
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Handle mouseup to end drag
|
|
163
|
+
* @param {MouseEvent} e
|
|
164
|
+
*/
|
|
165
|
+
function onMouseUp(e) {
|
|
166
|
+
if (!isDragging) return;
|
|
167
|
+
|
|
168
|
+
// Save final width
|
|
169
|
+
if (currentPanel) {
|
|
170
|
+
const finalWidth = getPanelWidth(currentPanel);
|
|
171
|
+
const config = CONFIG[currentPanel];
|
|
172
|
+
if (config) {
|
|
173
|
+
localStorage.setItem(config.storageKey, finalWidth.toString());
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Clean up
|
|
178
|
+
const handle = document.querySelector('.resize-handle.dragging');
|
|
179
|
+
if (handle) {
|
|
180
|
+
handle.classList.remove('dragging');
|
|
181
|
+
}
|
|
182
|
+
document.body.classList.remove('resizing');
|
|
183
|
+
|
|
184
|
+
isDragging = false;
|
|
185
|
+
currentPanel = null;
|
|
186
|
+
startX = 0;
|
|
187
|
+
startWidth = 0;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Get saved width for a panel (useful for AIPanel integration)
|
|
192
|
+
* @param {string} panelName - 'sidebar' or 'ai-panel'
|
|
193
|
+
* @returns {number|null} Saved width or null if not saved
|
|
194
|
+
*/
|
|
195
|
+
function getSavedWidth(panelName) {
|
|
196
|
+
const config = CONFIG[panelName];
|
|
197
|
+
if (!config) return null;
|
|
198
|
+
|
|
199
|
+
const saved = localStorage.getItem(config.storageKey);
|
|
200
|
+
if (saved) {
|
|
201
|
+
const width = parseInt(saved, 10);
|
|
202
|
+
if (width >= config.min && width <= config.max) {
|
|
203
|
+
return width;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get default width for a panel
|
|
211
|
+
* @param {string} panelName - 'sidebar' or 'ai-panel'
|
|
212
|
+
* @returns {number} Default width
|
|
213
|
+
*/
|
|
214
|
+
function getDefaultWidth(panelName) {
|
|
215
|
+
const config = CONFIG[panelName];
|
|
216
|
+
return config ? config.default : 0;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Public API
|
|
220
|
+
return {
|
|
221
|
+
init,
|
|
222
|
+
getPanelWidth,
|
|
223
|
+
setPanelWidth,
|
|
224
|
+
getSavedWidth,
|
|
225
|
+
getDefaultWidth,
|
|
226
|
+
applySavedWidths
|
|
227
|
+
};
|
|
228
|
+
})();
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
/**
|
|
3
|
+
* Storage Cleanup Module
|
|
4
|
+
* Cleans up legacy localStorage keys from older versions of the app.
|
|
5
|
+
* This prevents stale/orphaned data from causing confusion across sessions.
|
|
6
|
+
*
|
|
7
|
+
* Used by both PR mode (pr.js) and Local mode (local.js) on startup.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Remove legacy localStorage keys that are no longer used.
|
|
12
|
+
* This runs on every page load to clean up stale data.
|
|
13
|
+
*/
|
|
14
|
+
function cleanupLegacyLocalStorage() {
|
|
15
|
+
const legacyKeys = [
|
|
16
|
+
'pair-review-session-state', // Old session state format
|
|
17
|
+
'reviewPanelSegment', // Unscoped version (now uses reviewPanelSegment_${key})
|
|
18
|
+
'pair-review-preferences', // Old preferences format
|
|
19
|
+
'pairReviewSidebarCollapsed', // Duplicate of file-sidebar-collapsed
|
|
20
|
+
'pairReviewTheme', // Duplicate of theme
|
|
21
|
+
'settingsReferrer', // Unscoped version (now uses settingsReferrer:${repo})
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// Remove known legacy keys
|
|
25
|
+
legacyKeys.forEach(key => {
|
|
26
|
+
if (localStorage.getItem(key) !== null) {
|
|
27
|
+
localStorage.removeItem(key);
|
|
28
|
+
console.log(`[cleanup] Removed legacy localStorage key: ${key}`);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Export for use in other modules
|
|
34
|
+
if (typeof window !== 'undefined') {
|
|
35
|
+
window.cleanupLegacyLocalStorage = cleanupLegacyLocalStorage;
|
|
36
|
+
}
|