@in-the-loop-labs/pair-review 3.3.6 → 3.4.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/package.json +2 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/public/css/styles.css +14 -2
- package/public/index.html +1 -0
- package/public/js/components/AIPanel.js +3 -14
- package/public/js/components/UpdateBanner.js +143 -0
- package/public/js/modules/diff-renderer.js +103 -7
- package/public/js/modules/file-comment-manager.js +34 -13
- package/public/js/modules/suggestion-manager.js +22 -6
- package/public/js/pr.js +3 -3
- package/public/local.html +2 -0
- package/public/pr.html +2 -0
- package/public/setup.html +1 -0
- package/src/config.js +29 -6
- package/src/git/diff-flags.js +5 -1
- package/src/git/worktree.js +23 -4
- package/src/local-review.js +26 -10
- package/src/main.js +33 -20
- package/src/mcp-stdio.js +7 -0
- package/src/routes/config.js +55 -1
- package/src/routes/pr.js +26 -10
- package/src/server.js +51 -9
- package/src/single-port.js +191 -0
- package/src/utils/diff-file-list.js +111 -2
- package/src/utils/paths.js +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@in-the-loop-labs/pair-review",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"description": "Your AI-powered code review partner - Close the feedback loop with AI coding agents",
|
|
5
5
|
"main": "src/server.js",
|
|
6
6
|
"bin": {
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"glob": "^13.0.6",
|
|
69
69
|
"markdown-it": "^13.0.2",
|
|
70
70
|
"open": "^9.1.0",
|
|
71
|
+
"semver": "^7.7.4",
|
|
71
72
|
"simple-git": "^3.19.1",
|
|
72
73
|
"update-notifier": "^5.1.0",
|
|
73
74
|
"uuid": "^11.1.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pair-review",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"description": "pair-review app integration — Open PRs and local changes in the pair-review web UI, run server-side AI analysis, and address review feedback. Requires the pair-review MCP server.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "in-the-loop-labs",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-critic",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"description": "AI-powered code review analysis — Run three-level AI analysis and implement-review-fix loops directly in your coding agent. Works standalone, no server required.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "in-the-loop-labs",
|
package/public/css/styles.css
CHANGED
|
@@ -31,7 +31,13 @@
|
|
|
31
31
|
--color-warning-bg: #fffae6;
|
|
32
32
|
--color-warning-border: #ffc552;
|
|
33
33
|
--color-warning-text: #7d4e00;
|
|
34
|
-
|
|
34
|
+
|
|
35
|
+
--color-info-bg: #eff6ff;
|
|
36
|
+
--color-info-border: #bfdbfe;
|
|
37
|
+
--color-info-accent: #3b82f6;
|
|
38
|
+
--color-info-text: #1e3a8a;
|
|
39
|
+
--color-info-text-muted: #3b5998;
|
|
40
|
+
|
|
35
41
|
--color-selection: #fff5b1;
|
|
36
42
|
--color-selection-num: #ffeb3b;
|
|
37
43
|
|
|
@@ -72,7 +78,13 @@
|
|
|
72
78
|
--color-warning-bg: #3d2e00;
|
|
73
79
|
--color-warning-border: #9e6a03;
|
|
74
80
|
--color-warning-text: #e3b341;
|
|
75
|
-
|
|
81
|
+
|
|
82
|
+
--color-info-bg: #152033;
|
|
83
|
+
--color-info-border: #2d4a7a;
|
|
84
|
+
--color-info-accent: #4a90e2;
|
|
85
|
+
--color-info-text: #cfe0f5;
|
|
86
|
+
--color-info-text-muted: #8aa9cf;
|
|
87
|
+
|
|
76
88
|
--color-selection: #3d3300;
|
|
77
89
|
--color-selection-num: #4d4000;
|
|
78
90
|
|
package/public/index.html
CHANGED
|
@@ -965,20 +965,9 @@ class AIPanel {
|
|
|
965
965
|
expandFileIfCollapsed(file) {
|
|
966
966
|
if (!file) return false;
|
|
967
967
|
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
// Fallback: partial path match
|
|
972
|
-
if (!fileWrapper) {
|
|
973
|
-
const allWrappers = document.querySelectorAll('.d2h-file-wrapper');
|
|
974
|
-
for (const wrapper of allWrappers) {
|
|
975
|
-
const wrapperFile = wrapper.dataset.fileName;
|
|
976
|
-
if (wrapperFile && (wrapperFile.includes(file) || file.includes(wrapperFile))) {
|
|
977
|
-
fileWrapper = wrapper;
|
|
978
|
-
break;
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
}
|
|
968
|
+
const fileWrapper = window.prManager?.findFileElement
|
|
969
|
+
? window.prManager.findFileElement(file)
|
|
970
|
+
: window.DiffRenderer?.findFileElement?.(file);
|
|
982
971
|
|
|
983
972
|
if (!fileWrapper) return false;
|
|
984
973
|
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Copyright 2026 Tim Perkins (tjwp) | SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* UpdateBanner Component
|
|
4
|
+
* Shows a persistent, dismissible corner-card notification when a newer
|
|
5
|
+
* version of pair-review is available. Single delivery path: on construction,
|
|
6
|
+
* fetch /api/config and show the banner if a pending_update exists.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const DISMISS_KEY = 'update-banner-dismissed';
|
|
10
|
+
|
|
11
|
+
class UpdateBanner {
|
|
12
|
+
constructor() {
|
|
13
|
+
this._banner = null;
|
|
14
|
+
this._dismissBtn = null;
|
|
15
|
+
this._version = null;
|
|
16
|
+
|
|
17
|
+
// Single path: fetch current config at construction; show banner if a
|
|
18
|
+
// pending update exists. No event listener, no WebSocket coupling.
|
|
19
|
+
// `fetch()` returns a Promise; `.then()` chains async steps; the final
|
|
20
|
+
// `.catch()` swallows network errors because the banner is non-critical.
|
|
21
|
+
fetch('/api/config')
|
|
22
|
+
.then(r => (r.ok ? r.json() : null))
|
|
23
|
+
.then(config => {
|
|
24
|
+
if (config && config.pending_update) this.show(config.pending_update);
|
|
25
|
+
})
|
|
26
|
+
.catch(() => { /* non-critical */ });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Show the update banner for the given version.
|
|
31
|
+
* No-op if already showing or dismissed for this version.
|
|
32
|
+
* @param {string} version
|
|
33
|
+
*/
|
|
34
|
+
show(version) {
|
|
35
|
+
if (!version) return;
|
|
36
|
+
|
|
37
|
+
// Already dismissed for this version in this session
|
|
38
|
+
if (sessionStorage.getItem(DISMISS_KEY) === version) return;
|
|
39
|
+
|
|
40
|
+
// Already showing this version
|
|
41
|
+
if (this._banner && this._version === version) return;
|
|
42
|
+
|
|
43
|
+
// Remove any existing banner (e.g., for an older version)
|
|
44
|
+
this._remove();
|
|
45
|
+
|
|
46
|
+
this._version = version;
|
|
47
|
+
|
|
48
|
+
// Theme-aware colors come from CSS custom properties (set in styles.css
|
|
49
|
+
// under :root and [data-theme="dark"]). The inline `var(..., fallback)`
|
|
50
|
+
// form keeps the banner readable even if the stylesheet hasn't loaded
|
|
51
|
+
// yet. No MutationObserver needed — CSS handles the theme switch.
|
|
52
|
+
const banner = document.createElement('div');
|
|
53
|
+
banner.setAttribute('data-update-banner', '');
|
|
54
|
+
Object.assign(banner.style, {
|
|
55
|
+
position: 'fixed',
|
|
56
|
+
top: '16px',
|
|
57
|
+
left: '16px',
|
|
58
|
+
zIndex: '1000',
|
|
59
|
+
maxWidth: '360px',
|
|
60
|
+
background: 'var(--color-info-bg, #eff6ff)',
|
|
61
|
+
border: '1px solid var(--color-info-border, #bfdbfe)',
|
|
62
|
+
borderLeft: '4px solid var(--color-info-accent, #3b82f6)',
|
|
63
|
+
borderRadius: '8px',
|
|
64
|
+
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.15)',
|
|
65
|
+
padding: '12px 14px',
|
|
66
|
+
fontSize: '13px',
|
|
67
|
+
lineHeight: '1.4',
|
|
68
|
+
color: 'var(--color-info-text, #1e3a8a)',
|
|
69
|
+
display: 'flex',
|
|
70
|
+
alignItems: 'flex-start',
|
|
71
|
+
gap: '10px'
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Two-line layout: headline + restart instruction on its own line.
|
|
75
|
+
const text = document.createElement('div');
|
|
76
|
+
text.style.flex = '1';
|
|
77
|
+
|
|
78
|
+
const headline = document.createElement('div');
|
|
79
|
+
headline.textContent = `pair-review v${version} is available.`;
|
|
80
|
+
|
|
81
|
+
const instruction = document.createElement('div');
|
|
82
|
+
instruction.textContent = 'Restart the server to update.';
|
|
83
|
+
instruction.style.marginTop = '2px';
|
|
84
|
+
|
|
85
|
+
text.appendChild(headline);
|
|
86
|
+
text.appendChild(instruction);
|
|
87
|
+
|
|
88
|
+
const dismissBtn = document.createElement('button');
|
|
89
|
+
dismissBtn.textContent = '\u00d7';
|
|
90
|
+
dismissBtn.setAttribute('aria-label', 'Dismiss');
|
|
91
|
+
Object.assign(dismissBtn.style, {
|
|
92
|
+
background: 'none',
|
|
93
|
+
border: 'none',
|
|
94
|
+
color: 'var(--color-info-text-muted, #3b5998)',
|
|
95
|
+
cursor: 'pointer',
|
|
96
|
+
fontSize: '18px',
|
|
97
|
+
padding: '0',
|
|
98
|
+
lineHeight: '1',
|
|
99
|
+
flexShrink: '0',
|
|
100
|
+
opacity: '0.8'
|
|
101
|
+
});
|
|
102
|
+
dismissBtn.addEventListener('click', () => this.dismiss());
|
|
103
|
+
|
|
104
|
+
banner.appendChild(text);
|
|
105
|
+
banner.appendChild(dismissBtn);
|
|
106
|
+
document.body.appendChild(banner);
|
|
107
|
+
this._banner = banner;
|
|
108
|
+
this._dismissBtn = dismissBtn;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Dismiss the banner and remember the choice for this session. */
|
|
112
|
+
dismiss() {
|
|
113
|
+
if (this._version) {
|
|
114
|
+
sessionStorage.setItem(DISMISS_KEY, this._version);
|
|
115
|
+
}
|
|
116
|
+
this._remove();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** @private */
|
|
120
|
+
_remove() {
|
|
121
|
+
if (this._banner && this._banner.parentNode) {
|
|
122
|
+
this._banner.parentNode.removeChild(this._banner);
|
|
123
|
+
}
|
|
124
|
+
this._banner = null;
|
|
125
|
+
this._dismissBtn = null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Singleton init (browser only)
|
|
130
|
+
if (typeof window !== 'undefined' && !window.updateBanner) {
|
|
131
|
+
if (document.readyState === 'loading') {
|
|
132
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
133
|
+
window.updateBanner = new UpdateBanner();
|
|
134
|
+
});
|
|
135
|
+
} else {
|
|
136
|
+
window.updateBanner = new UpdateBanner();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// CommonJS export for unit tests
|
|
141
|
+
if (typeof module !== 'undefined' && module.exports) {
|
|
142
|
+
module.exports = { UpdateBanner };
|
|
143
|
+
}
|
|
@@ -64,6 +64,9 @@ class DiffRenderer {
|
|
|
64
64
|
// Ruby
|
|
65
65
|
'rb': 'ruby',
|
|
66
66
|
'erb': 'erb',
|
|
67
|
+
// Elixir
|
|
68
|
+
'ex': 'elixir',
|
|
69
|
+
'exs': 'elixir',
|
|
67
70
|
// PHP
|
|
68
71
|
'php': 'php',
|
|
69
72
|
// Java/Kotlin/Scala
|
|
@@ -194,6 +197,88 @@ class DiffRenderer {
|
|
|
194
197
|
return div.innerHTML;
|
|
195
198
|
}
|
|
196
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Normalize a file path for DOM matching.
|
|
202
|
+
*
|
|
203
|
+
* Frontend mirror of `normalizePath()` + `resolveRenamedFile()` from
|
|
204
|
+
* `src/utils/paths.js`. Browser code cannot `require()` the backend module,
|
|
205
|
+
* so the logic is duplicated here. Keep this function in sync with those two
|
|
206
|
+
* when modifying normalization rules, otherwise the frontend and backend
|
|
207
|
+
* will disagree on which paths are equivalent.
|
|
208
|
+
*
|
|
209
|
+
* @param {string} filePath - File path to normalize
|
|
210
|
+
* @returns {string} Normalized file path
|
|
211
|
+
*/
|
|
212
|
+
static normalizeFilePath(filePath) {
|
|
213
|
+
if (typeof filePath !== 'string') return '';
|
|
214
|
+
|
|
215
|
+
let normalized = filePath.trim();
|
|
216
|
+
if (!normalized) return '';
|
|
217
|
+
|
|
218
|
+
// Resolve git rename syntax to the new path.
|
|
219
|
+
normalized = normalized.replace(/\{[^}]*\s*=>\s*([^}]*)\}/, '$1');
|
|
220
|
+
normalized = normalized.replace(/\/+/g, '/');
|
|
221
|
+
|
|
222
|
+
// Strip leading './' and '/' segments until the string stops changing.
|
|
223
|
+
// This mirrors normalizePath in src/utils/paths.js for interleaved cases
|
|
224
|
+
// like '/./src/foo.js'.
|
|
225
|
+
let prevLength;
|
|
226
|
+
do {
|
|
227
|
+
prevLength = normalized.length;
|
|
228
|
+
|
|
229
|
+
while (normalized.startsWith('./')) {
|
|
230
|
+
normalized = normalized.slice(2);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
while (normalized.startsWith('/')) {
|
|
234
|
+
normalized = normalized.slice(1);
|
|
235
|
+
}
|
|
236
|
+
} while (normalized.length !== prevLength);
|
|
237
|
+
|
|
238
|
+
return normalized;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Check whether two file paths should be treated as the same DOM target.
|
|
243
|
+
* @param {string} left - First file path
|
|
244
|
+
* @param {string} right - Second file path
|
|
245
|
+
* @returns {boolean} True when the paths refer to the same file
|
|
246
|
+
*/
|
|
247
|
+
static pathsMatch(left, right) {
|
|
248
|
+
const normalizedLeft = DiffRenderer.normalizeFilePath(left);
|
|
249
|
+
const normalizedRight = DiffRenderer.normalizeFilePath(right);
|
|
250
|
+
|
|
251
|
+
if (!normalizedLeft || !normalizedRight) return false;
|
|
252
|
+
|
|
253
|
+
return normalizedLeft === normalizedRight ||
|
|
254
|
+
normalizedLeft.endsWith(`/${normalizedRight}`) ||
|
|
255
|
+
normalizedRight.endsWith(`/${normalizedLeft}`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Try a direct selector lookup for a file path.
|
|
260
|
+
* Falls back silently when CSS.escape is unavailable or the selector is invalid.
|
|
261
|
+
* @param {string} attribute - data attribute to match (without brackets)
|
|
262
|
+
* @param {string} filePath - File path to search for
|
|
263
|
+
* @returns {Element|null} Matching element if found
|
|
264
|
+
*/
|
|
265
|
+
static queryFileElement(attribute, filePath) {
|
|
266
|
+
if (!filePath) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const css = globalThis.CSS;
|
|
271
|
+
const escapedValue = css && typeof css.escape === 'function'
|
|
272
|
+
? css.escape(filePath)
|
|
273
|
+
: filePath;
|
|
274
|
+
|
|
275
|
+
try {
|
|
276
|
+
return document.querySelector(`[${attribute}="${escapedValue}"]`);
|
|
277
|
+
} catch {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
197
282
|
/**
|
|
198
283
|
* Render a single diff line
|
|
199
284
|
* @param {HTMLElement|DocumentFragment} container - Container to append to
|
|
@@ -696,18 +781,29 @@ class DiffRenderer {
|
|
|
696
781
|
* @returns {Element|null} The file wrapper element or null if not found
|
|
697
782
|
*/
|
|
698
783
|
static findFileElement(filePath) {
|
|
699
|
-
|
|
700
|
-
let fileElement = document.querySelector(`[data-file-name="${filePath}"]`);
|
|
701
|
-
if (fileElement) return fileElement;
|
|
784
|
+
const normalizedPath = DiffRenderer.normalizeFilePath(filePath);
|
|
702
785
|
|
|
703
|
-
|
|
704
|
-
|
|
786
|
+
// Try direct selector lookups first when we can safely escape the value.
|
|
787
|
+
const selectorCandidates = [filePath];
|
|
788
|
+
if (normalizedPath && normalizedPath !== filePath) {
|
|
789
|
+
selectorCandidates.push(normalizedPath);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
for (const candidate of selectorCandidates) {
|
|
793
|
+
let fileElement = DiffRenderer.queryFileElement('data-file-name', candidate);
|
|
794
|
+
if (fileElement) return fileElement;
|
|
795
|
+
|
|
796
|
+
fileElement = DiffRenderer.queryFileElement('data-file-path', candidate);
|
|
797
|
+
if (fileElement) return fileElement;
|
|
798
|
+
}
|
|
705
799
|
|
|
706
|
-
//
|
|
800
|
+
// Fall back to normalized iteration so special characters, rename syntax,
|
|
801
|
+
// and formatted variants still resolve to the rendered wrapper.
|
|
707
802
|
const allFileWrappers = document.querySelectorAll('.d2h-file-wrapper');
|
|
708
803
|
for (const wrapper of allFileWrappers) {
|
|
709
804
|
const fileName = wrapper.dataset.fileName;
|
|
710
|
-
|
|
805
|
+
const filePathAttr = wrapper.dataset.filePath;
|
|
806
|
+
if (DiffRenderer.pathsMatch(fileName, filePath) || DiffRenderer.pathsMatch(filePathAttr, filePath)) {
|
|
711
807
|
return wrapper;
|
|
712
808
|
}
|
|
713
809
|
}
|
|
@@ -1168,17 +1168,22 @@ class FileCommentManager {
|
|
|
1168
1168
|
* @param {Array} suggestions - Array of file-level AI suggestions
|
|
1169
1169
|
*/
|
|
1170
1170
|
loadFileComments(comments, suggestions) {
|
|
1171
|
-
// Group by file
|
|
1172
|
-
|
|
1173
|
-
const
|
|
1171
|
+
// Group by rendered file comments zone so path variants still attach to the
|
|
1172
|
+
// correct file on initial load.
|
|
1173
|
+
const commentsByZone = new Map();
|
|
1174
|
+
const suggestionsByZone = new Map();
|
|
1174
1175
|
|
|
1175
1176
|
if (comments) {
|
|
1176
1177
|
for (const comment of comments) {
|
|
1177
1178
|
if (comment.is_file_level === 1) {
|
|
1178
|
-
|
|
1179
|
-
|
|
1179
|
+
const zone = this.findZoneForFile(comment.file);
|
|
1180
|
+
if (!zone) {
|
|
1181
|
+
continue;
|
|
1180
1182
|
}
|
|
1181
|
-
|
|
1183
|
+
if (!commentsByZone.has(zone)) {
|
|
1184
|
+
commentsByZone.set(zone, []);
|
|
1185
|
+
}
|
|
1186
|
+
commentsByZone.get(zone).push(comment);
|
|
1182
1187
|
}
|
|
1183
1188
|
}
|
|
1184
1189
|
}
|
|
@@ -1186,10 +1191,14 @@ class FileCommentManager {
|
|
|
1186
1191
|
if (suggestions) {
|
|
1187
1192
|
for (const suggestion of suggestions) {
|
|
1188
1193
|
if (suggestion.is_file_level === 1) {
|
|
1189
|
-
|
|
1190
|
-
|
|
1194
|
+
const zone = this.findZoneForFile(suggestion.file);
|
|
1195
|
+
if (!zone) {
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
if (!suggestionsByZone.has(zone)) {
|
|
1199
|
+
suggestionsByZone.set(zone, []);
|
|
1191
1200
|
}
|
|
1192
|
-
|
|
1201
|
+
suggestionsByZone.get(zone).push(suggestion);
|
|
1193
1202
|
}
|
|
1194
1203
|
}
|
|
1195
1204
|
}
|
|
@@ -1197,7 +1206,6 @@ class FileCommentManager {
|
|
|
1197
1206
|
// Find all file comment zones and populate them
|
|
1198
1207
|
const zones = document.querySelectorAll('.file-comments-zone');
|
|
1199
1208
|
for (const zone of zones) {
|
|
1200
|
-
const fileName = zone.dataset.fileName;
|
|
1201
1209
|
const container = zone.querySelector('.file-comments-container');
|
|
1202
1210
|
|
|
1203
1211
|
// Selectively clear existing cards based on what we're about to reload
|
|
@@ -1221,8 +1229,8 @@ class FileCommentManager {
|
|
|
1221
1229
|
}
|
|
1222
1230
|
}
|
|
1223
1231
|
|
|
1224
|
-
const fileComments =
|
|
1225
|
-
const fileSuggestions =
|
|
1232
|
+
const fileComments = commentsByZone.get(zone) || [];
|
|
1233
|
+
const fileSuggestions = suggestionsByZone.get(zone) || [];
|
|
1226
1234
|
|
|
1227
1235
|
// Display AI suggestions first
|
|
1228
1236
|
for (const suggestion of fileSuggestions) {
|
|
@@ -1245,7 +1253,20 @@ class FileCommentManager {
|
|
|
1245
1253
|
* @returns {HTMLElement|null} The zone element or null
|
|
1246
1254
|
*/
|
|
1247
1255
|
findZoneForFile(fileName) {
|
|
1248
|
-
|
|
1256
|
+
const fileWrapper = window.DiffRenderer?.findFileElement
|
|
1257
|
+
? window.DiffRenderer.findFileElement(fileName)
|
|
1258
|
+
: null;
|
|
1259
|
+
|
|
1260
|
+
if (fileWrapper) {
|
|
1261
|
+
return fileWrapper.querySelector('.file-comments-zone');
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
try {
|
|
1265
|
+
const escaped = globalThis.CSS?.escape ? CSS.escape(fileName) : fileName;
|
|
1266
|
+
return document.querySelector(`.file-comments-zone[data-file-name="${escaped}"]`);
|
|
1267
|
+
} catch {
|
|
1268
|
+
return null;
|
|
1269
|
+
}
|
|
1249
1270
|
}
|
|
1250
1271
|
|
|
1251
1272
|
/**
|
|
@@ -160,6 +160,26 @@ class SuggestionManager {
|
|
|
160
160
|
return window.CategoryEmoji?.getEmoji(category) || '\u{1F4AC}';
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Resolve a diff file wrapper for the provided path.
|
|
165
|
+
* @param {string} file - File path
|
|
166
|
+
* @returns {Element|null} Matching file wrapper
|
|
167
|
+
*/
|
|
168
|
+
findFileElement(file) {
|
|
169
|
+
if (this.prManager?.findFileElement) {
|
|
170
|
+
return this.prManager.findFileElement(file);
|
|
171
|
+
}
|
|
172
|
+
if (window.DiffRenderer?.findFileElement) {
|
|
173
|
+
return window.DiffRenderer.findFileElement(file);
|
|
174
|
+
}
|
|
175
|
+
try {
|
|
176
|
+
const escaped = globalThis.CSS?.escape ? CSS.escape(file) : file;
|
|
177
|
+
return document.querySelector(`[data-file-name="${escaped}"]`);
|
|
178
|
+
} catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
163
183
|
/**
|
|
164
184
|
* Find suggestions that target lines currently hidden in gaps
|
|
165
185
|
* @param {Array} suggestions - Array of suggestions
|
|
@@ -176,9 +196,7 @@ class SuggestionManager {
|
|
|
176
196
|
const side = suggestion.side || 'RIGHT';
|
|
177
197
|
|
|
178
198
|
// Find the file wrapper
|
|
179
|
-
const fileElement =
|
|
180
|
-
window.DiffRenderer.findFileElement(file) :
|
|
181
|
-
document.querySelector(`[data-file-name="${file}"]`);
|
|
199
|
+
const fileElement = this.findFileElement(file);
|
|
182
200
|
|
|
183
201
|
if (!fileElement) {
|
|
184
202
|
// File not in diff at all, not a hidden line issue
|
|
@@ -329,9 +347,7 @@ class SuggestionManager {
|
|
|
329
347
|
const line = parseInt(lineStr);
|
|
330
348
|
|
|
331
349
|
// Use helper method for file lookup
|
|
332
|
-
const fileElement =
|
|
333
|
-
window.DiffRenderer.findFileElement(file) :
|
|
334
|
-
document.querySelector(`[data-file-name="${file}"]`);
|
|
350
|
+
const fileElement = this.findFileElement(file);
|
|
335
351
|
|
|
336
352
|
if (!fileElement) {
|
|
337
353
|
// This can happen when AI suggests a file path that doesn't exist in the diff
|
package/public/js/pr.js
CHANGED
|
@@ -2032,7 +2032,7 @@ class PRManager {
|
|
|
2032
2032
|
* @param {string} filePath - Path of the file
|
|
2033
2033
|
*/
|
|
2034
2034
|
toggleFileCollapse(filePath) {
|
|
2035
|
-
const wrapper =
|
|
2035
|
+
const wrapper = this.findFileElement(filePath);
|
|
2036
2036
|
if (!wrapper) return;
|
|
2037
2037
|
|
|
2038
2038
|
const isCollapsed = wrapper.classList.contains('collapsed');
|
|
@@ -2058,7 +2058,7 @@ class PRManager {
|
|
|
2058
2058
|
* @param {boolean} isViewed - Whether the file is now viewed
|
|
2059
2059
|
*/
|
|
2060
2060
|
toggleFileViewed(filePath, isViewed) {
|
|
2061
|
-
const wrapper =
|
|
2061
|
+
const wrapper = this.findFileElement(filePath);
|
|
2062
2062
|
|
|
2063
2063
|
if (isViewed) {
|
|
2064
2064
|
this.viewedFiles.add(filePath);
|
|
@@ -4359,7 +4359,7 @@ class PRManager {
|
|
|
4359
4359
|
}
|
|
4360
4360
|
|
|
4361
4361
|
scrollToFile(filePath) {
|
|
4362
|
-
const fileWrapper =
|
|
4362
|
+
const fileWrapper = this.findFileElement(filePath);
|
|
4363
4363
|
if (fileWrapper) {
|
|
4364
4364
|
fileWrapper.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
4365
4365
|
}
|
package/public/local.html
CHANGED
|
@@ -562,6 +562,7 @@
|
|
|
562
562
|
|
|
563
563
|
<!-- Highlight.js for syntax highlighting -->
|
|
564
564
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
565
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/elixir.min.js"></script>
|
|
565
566
|
|
|
566
567
|
<!-- Markdown-it Library for rendering markdown in comments -->
|
|
567
568
|
<script src="https://cdn.jsdelivr.net/npm/markdown-it@13.0.2/dist/markdown-it.min.js"></script>
|
|
@@ -591,6 +592,7 @@
|
|
|
591
592
|
<!-- Components -->
|
|
592
593
|
<script src="/js/components/TabTitle.js"></script>
|
|
593
594
|
<script src="/js/components/Toast.js"></script>
|
|
595
|
+
<script src="/js/components/UpdateBanner.js"></script>
|
|
594
596
|
<script src="/js/components/ConfirmDialog.js"></script>
|
|
595
597
|
<script src="/js/components/TextInputDialog.js"></script>
|
|
596
598
|
<script src="/js/components/AnalysisConfigModal.js"></script>
|
package/public/pr.html
CHANGED
|
@@ -358,6 +358,7 @@
|
|
|
358
358
|
|
|
359
359
|
<!-- Highlight.js for syntax highlighting -->
|
|
360
360
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
|
361
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/elixir.min.js"></script>
|
|
361
362
|
|
|
362
363
|
<!-- Markdown-it Library for rendering markdown in comments -->
|
|
363
364
|
<script src="https://cdn.jsdelivr.net/npm/markdown-it@13.0.2/dist/markdown-it.min.js"></script>
|
|
@@ -387,6 +388,7 @@
|
|
|
387
388
|
<!-- Components -->
|
|
388
389
|
<script src="/js/components/TabTitle.js"></script>
|
|
389
390
|
<script src="/js/components/Toast.js"></script>
|
|
391
|
+
<script src="/js/components/UpdateBanner.js"></script>
|
|
390
392
|
<script src="/js/components/ConfirmDialog.js"></script>
|
|
391
393
|
<script src="/js/components/TextInputDialog.js"></script>
|
|
392
394
|
<script src="/js/components/AnalysisConfigModal.js"></script>
|