@in-the-loop-labs/pair-review 3.2.2 → 3.3.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/README.md +7 -6
- package/package.json +5 -4
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/skills/analyze/references/orchestration-balanced.md +9 -1
- package/plugin-code-critic/skills/analyze/references/orchestration-fast.md +8 -1
- package/plugin-code-critic/skills/analyze/references/orchestration-thorough.md +8 -7
- package/public/css/repo-settings.css +347 -0
- package/public/index.html +46 -9
- package/public/js/components/AIPanel.js +79 -37
- package/public/js/components/DiffOptionsDropdown.js +84 -1
- package/public/js/index.js +31 -6
- package/public/js/modules/analysis-history.js +11 -7
- package/public/js/pr.js +22 -0
- package/public/js/repo-settings.js +334 -6
- package/public/repo-settings.html +29 -0
- package/src/ai/analyzer.js +28 -19
- package/src/ai/claude-cli.js +2 -0
- package/src/ai/claude-provider.js +4 -1
- package/src/ai/prompts/baseline/consolidation/balanced.js +6 -4
- package/src/ai/prompts/baseline/consolidation/fast.js +6 -2
- package/src/ai/prompts/baseline/consolidation/thorough.js +7 -6
- package/src/ai/prompts/baseline/orchestration/balanced.js +13 -1
- package/src/ai/prompts/baseline/orchestration/fast.js +12 -1
- package/src/ai/prompts/baseline/orchestration/thorough.js +8 -7
- package/src/ai/provider.js +7 -6
- package/src/chat/session-manager.js +6 -3
- package/src/config.js +230 -38
- package/src/database.js +766 -38
- package/src/git/worktree-pool-lifecycle.js +674 -0
- package/src/git/worktree-pool-usage.js +216 -0
- package/src/git/worktree.js +46 -13
- package/src/main.js +185 -26
- package/src/routes/analyses.js +48 -26
- package/src/routes/chat.js +27 -3
- package/src/routes/config.js +17 -5
- package/src/routes/executable-analysis.js +38 -19
- package/src/routes/local.js +19 -6
- package/src/routes/mcp.js +13 -2
- package/src/routes/pr.js +72 -29
- package/src/routes/setup.js +41 -4
- package/src/routes/stack-analysis.js +29 -10
- package/src/routes/worktrees.js +294 -9
- package/src/server.js +20 -3
- package/src/setup/pr-setup.js +161 -27
- package/src/ws/server.js +51 -1
|
@@ -793,38 +793,7 @@ class AIPanel {
|
|
|
793
793
|
this.findingsList.querySelectorAll('.quick-action-chat').forEach(btn => {
|
|
794
794
|
btn.addEventListener('click', (e) => {
|
|
795
795
|
e.stopPropagation(); // Prevent triggering item click
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
const findingId = btn.dataset.findingId ? parseInt(btn.dataset.findingId, 10) : null;
|
|
799
|
-
const commentId = btn.dataset.commentId ? parseInt(btn.dataset.commentId, 10) : null;
|
|
800
|
-
const file = btn.dataset.findingFile || '';
|
|
801
|
-
const title = btn.dataset.findingTitle || '';
|
|
802
|
-
|
|
803
|
-
// Build context from the finding data
|
|
804
|
-
let suggestionContext = { title, file };
|
|
805
|
-
|
|
806
|
-
if (findingId && this.findings) {
|
|
807
|
-
const finding = this.findings.find(f => f.id === findingId);
|
|
808
|
-
if (finding) {
|
|
809
|
-
suggestionContext = {
|
|
810
|
-
suggestionId: findingId ? String(findingId) : null,
|
|
811
|
-
title: finding.title || title,
|
|
812
|
-
body: finding.formattedBody || finding.body || '',
|
|
813
|
-
type: finding.type || '',
|
|
814
|
-
file: finding.file || file,
|
|
815
|
-
line_start: finding.line_start || null,
|
|
816
|
-
line_end: finding.line_end || null,
|
|
817
|
-
side: 'RIGHT',
|
|
818
|
-
reasoning: null
|
|
819
|
-
};
|
|
820
|
-
}
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
window.chatPanel.open({
|
|
824
|
-
reviewId: window.prManager?.currentPR?.id,
|
|
825
|
-
suggestionId: findingId ? String(findingId) : (commentId ? String(commentId) : undefined),
|
|
826
|
-
suggestionContext
|
|
827
|
-
});
|
|
796
|
+
this.openQuickActionChat(btn);
|
|
828
797
|
});
|
|
829
798
|
});
|
|
830
799
|
|
|
@@ -887,6 +856,79 @@ class AIPanel {
|
|
|
887
856
|
}
|
|
888
857
|
}
|
|
889
858
|
|
|
859
|
+
/**
|
|
860
|
+
* Handle chat button clicks from review panel quick actions.
|
|
861
|
+
* Suggestions use suggestionContext; comments use commentContext.
|
|
862
|
+
* @param {HTMLButtonElement} btn - The clicked chat button
|
|
863
|
+
*/
|
|
864
|
+
openQuickActionChat(btn) {
|
|
865
|
+
if (!window.chatPanel) return;
|
|
866
|
+
|
|
867
|
+
const findingId = btn.dataset.findingId ? parseInt(btn.dataset.findingId, 10) : null;
|
|
868
|
+
const commentId = btn.dataset.commentId ? parseInt(btn.dataset.commentId, 10) : null;
|
|
869
|
+
const reviewId = window.prManager?.currentPR?.id;
|
|
870
|
+
|
|
871
|
+
const buildCommentContext = (comment, fallbackDataset = {}) => ({
|
|
872
|
+
commentId: comment?.id ? String(comment.id) : String(commentId),
|
|
873
|
+
body: comment?.body || '',
|
|
874
|
+
file: comment?.file || fallbackDataset.commentFile || '',
|
|
875
|
+
line_start: comment?.line_start ?? (fallbackDataset.commentLineStart ? parseInt(fallbackDataset.commentLineStart, 10) : null),
|
|
876
|
+
line_end: comment?.line_end ?? (fallbackDataset.commentLineEnd ? parseInt(fallbackDataset.commentLineEnd, 10) : null),
|
|
877
|
+
parentId: comment?.parent_id ?? (fallbackDataset.commentParentId ? parseInt(fallbackDataset.commentParentId, 10) : null),
|
|
878
|
+
source: 'user',
|
|
879
|
+
isFileLevel: comment?.is_file_level === 1 || comment?.is_file_level === true
|
|
880
|
+
});
|
|
881
|
+
|
|
882
|
+
if (commentId) {
|
|
883
|
+
const comment = this.comments?.find(c => c.id === commentId);
|
|
884
|
+
|
|
885
|
+
window.chatPanel.open({
|
|
886
|
+
reviewId,
|
|
887
|
+
commentContext: buildCommentContext(comment, btn.dataset)
|
|
888
|
+
});
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
const file = btn.dataset.findingFile || '';
|
|
893
|
+
const title = btn.dataset.findingTitle || '';
|
|
894
|
+
let suggestionContext = { title, file };
|
|
895
|
+
|
|
896
|
+
if (findingId && this.findings) {
|
|
897
|
+
const finding = this.findings.find(f => f.id === findingId);
|
|
898
|
+
if (finding) {
|
|
899
|
+
if (finding.status === 'adopted') {
|
|
900
|
+
const adoptedComment = this.comments?.find(c => c.parent_id === findingId && c.status !== 'inactive')
|
|
901
|
+
|| this.comments?.find(c => c.parent_id === findingId);
|
|
902
|
+
if (adoptedComment) {
|
|
903
|
+
window.chatPanel.open({
|
|
904
|
+
reviewId,
|
|
905
|
+
commentContext: buildCommentContext(adoptedComment)
|
|
906
|
+
});
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
suggestionContext = {
|
|
912
|
+
suggestionId: String(findingId),
|
|
913
|
+
title: finding.title || title,
|
|
914
|
+
body: finding.formattedBody || finding.body || '',
|
|
915
|
+
type: finding.type || '',
|
|
916
|
+
file: finding.file || file,
|
|
917
|
+
line_start: finding.line_start ?? null,
|
|
918
|
+
line_end: finding.line_end ?? null,
|
|
919
|
+
side: 'RIGHT',
|
|
920
|
+
reasoning: null
|
|
921
|
+
};
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
window.chatPanel.open({
|
|
926
|
+
reviewId,
|
|
927
|
+
suggestionId: findingId ? String(findingId) : undefined,
|
|
928
|
+
suggestionContext
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
|
|
890
932
|
onFindingClick(item) {
|
|
891
933
|
const itemId = item.dataset.id;
|
|
892
934
|
const itemType = item.dataset.itemType;
|
|
@@ -1204,9 +1246,9 @@ class AIPanel {
|
|
|
1204
1246
|
`;
|
|
1205
1247
|
}
|
|
1206
1248
|
|
|
1207
|
-
// Chat button for
|
|
1249
|
+
// Chat button for all findings when chat is available
|
|
1208
1250
|
let chatAction = '';
|
|
1209
|
-
if (
|
|
1251
|
+
if (document.documentElement.getAttribute('data-chat') === 'available') {
|
|
1210
1252
|
chatAction = `
|
|
1211
1253
|
<div class="finding-chat-action">
|
|
1212
1254
|
<button class="quick-action-btn quick-action-chat" data-finding-id="${finding.id}" data-finding-file="${finding.file || ''}" data-finding-title="${this.escapeHtml(title)}" title="Chat" aria-label="Chat about suggestion">
|
|
@@ -1279,12 +1321,12 @@ class AIPanel {
|
|
|
1279
1321
|
`;
|
|
1280
1322
|
}
|
|
1281
1323
|
|
|
1282
|
-
// Chat button for active
|
|
1324
|
+
// Chat button for active comments
|
|
1283
1325
|
let chatAction = '';
|
|
1284
|
-
if (!isDismissed &&
|
|
1326
|
+
if (!isDismissed && document.documentElement.getAttribute('data-chat') === 'available') {
|
|
1285
1327
|
chatAction = `
|
|
1286
1328
|
<div class="finding-chat-action">
|
|
1287
|
-
<button class="quick-action-btn quick-action-chat" data-comment-id="${comment.id}" data-
|
|
1329
|
+
<button class="quick-action-btn quick-action-chat" data-comment-id="${comment.id}" data-comment-file="${this.escapeHtml(comment.file || '')}" data-comment-line-start="${comment.line_start ?? ''}" data-comment-line-end="${comment.line_end ?? ''}" data-comment-parent-id="${comment.parent_id || ''}" title="Chat" aria-label="Chat about comment">
|
|
1288
1330
|
<svg viewBox="0 0 16 16" fill="currentColor" width="12" height="12"><path d="M1.75 1h8.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 10.25 10H7.061l-2.574 2.573A1.458 1.458 0 0 1 2 11.543V10h-.25A1.75 1.75 0 0 1 0 8.25v-5.5C0 1.784.784 1 1.75 1ZM1.5 2.75v5.5c0 .138.112.25.25.25h1a.75.75 0 0 1 .75.75v2.19l2.72-2.72a.749.749 0 0 1 .53-.22h3.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25h-8.5a.25.25 0 0 0-.25.25Zm13 2a.25.25 0 0 0-.25-.25h-.5a.75.75 0 0 1 0-1.5h.5c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 14.25 12H14v1.543a1.458 1.458 0 0 1-2.487 1.03L9.22 12.28a.749.749 0 0 1 .326-1.275.749.749 0 0 1 .734.215l2.22 2.22v-2.19a.75.75 0 0 1 .75-.75h1a.25.25 0 0 0 .25-.25Z"/></svg>
|
|
1289
1331
|
</button>
|
|
1290
1332
|
</div>
|
|
@@ -44,11 +44,13 @@ class DiffOptionsDropdown {
|
|
|
44
44
|
* @param {{start:string,end:string}} [callbacks.initialScope]
|
|
45
45
|
* @param {boolean} [callbacks.branchAvailable]
|
|
46
46
|
*/
|
|
47
|
-
constructor(buttonElement, { onToggleWhitespace, onToggleMinimize, onScopeChange, initialScope, branchAvailable }) {
|
|
47
|
+
constructor(buttonElement, { onToggleWhitespace, onToggleMinimize, onScopeChange, initialScope, branchAvailable, worktreePath }) {
|
|
48
48
|
this._btn = buttonElement;
|
|
49
49
|
this._onToggleWhitespace = onToggleWhitespace;
|
|
50
50
|
this._onToggleMinimize = onToggleMinimize || (() => {});
|
|
51
51
|
this._onScopeChange = onScopeChange || null;
|
|
52
|
+
this._worktreePath = worktreePath || null;
|
|
53
|
+
this._worktreeName = null;
|
|
52
54
|
|
|
53
55
|
this._popoverEl = null;
|
|
54
56
|
this._checkbox = null;
|
|
@@ -169,6 +171,18 @@ class DiffOptionsDropdown {
|
|
|
169
171
|
this._updateScopeUI();
|
|
170
172
|
}
|
|
171
173
|
|
|
174
|
+
/** Set the worktree display name (relative path from base dir). */
|
|
175
|
+
set worktreeName(value) {
|
|
176
|
+
this._worktreeName = value || null;
|
|
177
|
+
this._updateWorktreeRow();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Set the worktree path (updates UI if popover already exists). */
|
|
181
|
+
set worktreePath(value) {
|
|
182
|
+
this._worktreePath = value || null;
|
|
183
|
+
this._updateWorktreeRow();
|
|
184
|
+
}
|
|
185
|
+
|
|
172
186
|
/** Clear the scope status indicator (call after scope change completes). */
|
|
173
187
|
clearScopeStatus() {
|
|
174
188
|
if (this._scopeStatusEl) {
|
|
@@ -231,12 +245,18 @@ class DiffOptionsDropdown {
|
|
|
231
245
|
const minCheckbox = minLabel.querySelector('input');
|
|
232
246
|
popover.appendChild(minLabel);
|
|
233
247
|
|
|
248
|
+
// Worktree path row (placeholder — populated by _updateWorktreeRow)
|
|
249
|
+
this._worktreeRowEl = null;
|
|
250
|
+
|
|
234
251
|
document.body.appendChild(popover);
|
|
235
252
|
|
|
236
253
|
this._popoverEl = popover;
|
|
237
254
|
this._checkbox = wsCheckbox;
|
|
238
255
|
this._minimizeCheckbox = minCheckbox;
|
|
239
256
|
|
|
257
|
+
// Render worktree row if path is already known
|
|
258
|
+
this._updateWorktreeRow();
|
|
259
|
+
|
|
240
260
|
// Respond to checkbox changes
|
|
241
261
|
wsCheckbox.addEventListener('change', () => {
|
|
242
262
|
this._hideWhitespace = wsCheckbox.checked;
|
|
@@ -281,6 +301,69 @@ class DiffOptionsDropdown {
|
|
|
281
301
|
return label;
|
|
282
302
|
}
|
|
283
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Render or update the worktree path row at the bottom of the popover.
|
|
306
|
+
*/
|
|
307
|
+
_updateWorktreeRow() {
|
|
308
|
+
if (!this._popoverEl) return;
|
|
309
|
+
|
|
310
|
+
// Remove existing row if any
|
|
311
|
+
if (this._worktreeRowEl) {
|
|
312
|
+
this._worktreeRowEl.remove();
|
|
313
|
+
this._worktreeRowEl = null;
|
|
314
|
+
}
|
|
315
|
+
if (!this._worktreePath) return;
|
|
316
|
+
|
|
317
|
+
const displayName = this._worktreeName || this._worktreePath.split('/').pop();
|
|
318
|
+
|
|
319
|
+
// Divider
|
|
320
|
+
const divider = document.createElement('div');
|
|
321
|
+
divider.style.height = '1px';
|
|
322
|
+
divider.style.background = 'var(--color-border-primary, #d0d7de)';
|
|
323
|
+
divider.style.margin = '0 20px';
|
|
324
|
+
|
|
325
|
+
// Row container
|
|
326
|
+
const row = document.createElement('div');
|
|
327
|
+
row.style.display = 'flex';
|
|
328
|
+
row.style.alignItems = 'center';
|
|
329
|
+
row.style.gap = '6px';
|
|
330
|
+
row.style.padding = '8px 12px';
|
|
331
|
+
row.style.fontSize = '0.8125rem';
|
|
332
|
+
row.style.color = 'var(--color-fg-muted, #656d76)';
|
|
333
|
+
|
|
334
|
+
const text = document.createElement('span');
|
|
335
|
+
text.textContent = `Worktree: ${displayName}`;
|
|
336
|
+
text.style.overflow = 'hidden';
|
|
337
|
+
text.style.textOverflow = 'ellipsis';
|
|
338
|
+
text.style.whiteSpace = 'nowrap';
|
|
339
|
+
text.style.minWidth = '0';
|
|
340
|
+
|
|
341
|
+
const copyBtn = document.createElement('button');
|
|
342
|
+
copyBtn.title = 'Copy full path';
|
|
343
|
+
copyBtn.style.cssText = 'background:none;border:none;cursor:pointer;padding:2px;color:inherit;display:flex;flex-shrink:0;';
|
|
344
|
+
copyBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"/><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"/></svg>';
|
|
345
|
+
|
|
346
|
+
const fullPath = this._worktreePath;
|
|
347
|
+
copyBtn.addEventListener('click', (e) => {
|
|
348
|
+
e.stopPropagation();
|
|
349
|
+
navigator.clipboard.writeText(fullPath).then(() => {
|
|
350
|
+
const orig = text.textContent;
|
|
351
|
+
text.textContent = 'Copied!';
|
|
352
|
+
setTimeout(() => { text.textContent = orig; }, 1500);
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
row.appendChild(text);
|
|
357
|
+
row.appendChild(copyBtn);
|
|
358
|
+
|
|
359
|
+
// Use a container so we can remove both elements via one reference
|
|
360
|
+
const container = document.createElement('div');
|
|
361
|
+
container.appendChild(divider);
|
|
362
|
+
container.appendChild(row);
|
|
363
|
+
this._popoverEl.appendChild(container);
|
|
364
|
+
this._worktreeRowEl = container;
|
|
365
|
+
}
|
|
366
|
+
|
|
284
367
|
_renderScopeSelector(popover) {
|
|
285
368
|
const LS = this._localScope;
|
|
286
369
|
|
package/public/js/index.js
CHANGED
|
@@ -141,11 +141,12 @@
|
|
|
141
141
|
*/
|
|
142
142
|
function setFormLoading(tab, loading, text) {
|
|
143
143
|
const ids = tab === 'pr'
|
|
144
|
-
? { input: 'pr-url-input', btn: 'start-review-btn', loadingEl: 'start-review-loading-pr', loadingText: 'start-review-loading-text-pr', errorEl: 'start-review-error-pr', btnLabel: '
|
|
145
|
-
: { input: 'local-path-input', btn: 'start-local-btn', loadingEl: 'start-review-loading-local', loadingText: 'start-review-loading-text-local', errorEl: 'start-review-error-local', btnLabel: '
|
|
144
|
+
? { input: 'pr-url-input', btn: 'start-review-btn', analyzeBtn: 'analyze-review-btn', loadingEl: 'start-review-loading-pr', loadingText: 'start-review-loading-text-pr', errorEl: 'start-review-error-pr', btnLabel: 'Open' }
|
|
145
|
+
: { input: 'local-path-input', btn: 'start-local-btn', analyzeBtn: 'analyze-local-btn', loadingEl: 'start-review-loading-local', loadingText: 'start-review-loading-text-local', errorEl: 'start-review-error-local', btnLabel: 'Open' };
|
|
146
146
|
|
|
147
147
|
const inputEl = document.getElementById(ids.input);
|
|
148
148
|
const btnEl = document.getElementById(ids.btn);
|
|
149
|
+
const analyzeBtnEl = document.getElementById(ids.analyzeBtn);
|
|
149
150
|
const loadingEl = document.getElementById(ids.loadingEl);
|
|
150
151
|
const loadingTextEl = document.getElementById(ids.loadingText);
|
|
151
152
|
const errorEl = document.getElementById(ids.errorEl);
|
|
@@ -153,12 +154,14 @@
|
|
|
153
154
|
if (loading) {
|
|
154
155
|
if (inputEl) inputEl.disabled = true;
|
|
155
156
|
if (btnEl) { btnEl.disabled = true; btnEl.textContent = 'Starting...'; }
|
|
157
|
+
if (analyzeBtnEl) analyzeBtnEl.disabled = true;
|
|
156
158
|
if (loadingEl) loadingEl.classList.add('visible');
|
|
157
159
|
if (loadingTextEl && text) loadingTextEl.textContent = text;
|
|
158
160
|
if (errorEl) errorEl.classList.remove('visible', 'info');
|
|
159
161
|
} else {
|
|
160
162
|
if (inputEl) inputEl.disabled = false;
|
|
161
163
|
if (btnEl) { btnEl.disabled = false; btnEl.textContent = ids.btnLabel; }
|
|
164
|
+
if (analyzeBtnEl) analyzeBtnEl.disabled = false;
|
|
162
165
|
if (loadingEl) loadingEl.classList.remove('visible');
|
|
163
166
|
}
|
|
164
167
|
}
|
|
@@ -688,8 +691,10 @@
|
|
|
688
691
|
* Navigates to the setup page which shows step-by-step progress,
|
|
689
692
|
* matching the flow used when reviews are started from the MCP/CLI.
|
|
690
693
|
* @param {Event} event - Form submit event
|
|
694
|
+
* @param {Object} [options] - Optional settings
|
|
695
|
+
* @param {boolean} [options.analyze=false] - When true, append analyze=true to the navigation URL
|
|
691
696
|
*/
|
|
692
|
-
async function handleStartLocal(event) {
|
|
697
|
+
async function handleStartLocal(event, { analyze = false } = {}) {
|
|
693
698
|
event.preventDefault();
|
|
694
699
|
|
|
695
700
|
const input = document.getElementById('local-path-input');
|
|
@@ -707,7 +712,9 @@
|
|
|
707
712
|
|
|
708
713
|
// Navigate to the setup page which shows step-by-step progress
|
|
709
714
|
// The /local?path= route serves setup.html which handles the full setup flow
|
|
710
|
-
|
|
715
|
+
let href = '/local?path=' + encodeURIComponent(pathValue);
|
|
716
|
+
if (analyze) href += '&analyze=true';
|
|
717
|
+
window.location.href = href;
|
|
711
718
|
}
|
|
712
719
|
|
|
713
720
|
// ─── Browse Directory ──────────────────────────────────────────────────────
|
|
@@ -1080,7 +1087,7 @@
|
|
|
1080
1087
|
* directly for PRs that already exist in the database.
|
|
1081
1088
|
* @param {Event} event - Form submit event
|
|
1082
1089
|
*/
|
|
1083
|
-
async function handleStartReview(event) {
|
|
1090
|
+
async function handleStartReview(event, { analyze = false } = {}) {
|
|
1084
1091
|
event.preventDefault();
|
|
1085
1092
|
|
|
1086
1093
|
const input = document.getElementById('pr-url-input');
|
|
@@ -1111,7 +1118,9 @@
|
|
|
1111
1118
|
|
|
1112
1119
|
// Navigate to the PR route which serves setup.html (with step-by-step progress)
|
|
1113
1120
|
// for new PRs, or pr.html directly for PRs already in the database
|
|
1114
|
-
|
|
1121
|
+
let href = '/pr/' + encodeURIComponent(parsed.owner) + '/' + encodeURIComponent(parsed.repo) + '/' + encodeURIComponent(parsed.prNumber);
|
|
1122
|
+
if (analyze) href += '?analyze=true';
|
|
1123
|
+
window.location.href = href;
|
|
1115
1124
|
}
|
|
1116
1125
|
|
|
1117
1126
|
// ─── Config & Command Examples ──────────────────────────────────────────────
|
|
@@ -1870,6 +1879,22 @@
|
|
|
1870
1879
|
browseBtn.addEventListener('click', handleBrowseLocal);
|
|
1871
1880
|
}
|
|
1872
1881
|
|
|
1882
|
+
// Set up PR Analyze button handler
|
|
1883
|
+
const analyzeReviewBtn = document.getElementById('analyze-review-btn');
|
|
1884
|
+
if (analyzeReviewBtn) {
|
|
1885
|
+
analyzeReviewBtn.addEventListener('click', function (event) {
|
|
1886
|
+
handleStartReview(event, { analyze: true });
|
|
1887
|
+
});
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
// Set up Local Analyze button handler
|
|
1891
|
+
const analyzeLocalBtn = document.getElementById('analyze-local-btn');
|
|
1892
|
+
if (analyzeLocalBtn) {
|
|
1893
|
+
analyzeLocalBtn.addEventListener('click', function (event) {
|
|
1894
|
+
handleStartLocal(event, { analyze: true });
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1873
1898
|
// Note: No explicit Enter keypress handlers are needed here.
|
|
1874
1899
|
// Both inputs are inside <form> elements, so pressing Enter
|
|
1875
1900
|
// natively triggers form submission.
|
|
@@ -205,9 +205,11 @@ class AnalysisHistoryManager {
|
|
|
205
205
|
/**
|
|
206
206
|
* Load analysis runs from the API (initial load only)
|
|
207
207
|
*
|
|
208
|
-
* This method is intended for initial page load
|
|
209
|
-
*
|
|
210
|
-
*
|
|
208
|
+
* This method is intended for initial page load. On first load it selects the
|
|
209
|
+
* latest run; on subsequent calls it preserves the current selection when that
|
|
210
|
+
* run still exists. For refreshing after a new analysis completes, use
|
|
211
|
+
* refresh(), which has logic to optionally preserve the user's current
|
|
212
|
+
* selection while surfacing a new run indicator.
|
|
211
213
|
*
|
|
212
214
|
* @returns {Promise<Array>} The loaded runs
|
|
213
215
|
*/
|
|
@@ -227,11 +229,13 @@ class AnalysisHistoryManager {
|
|
|
227
229
|
return [];
|
|
228
230
|
}
|
|
229
231
|
|
|
230
|
-
// Always select the latest run (first in the list since they're ordered by date DESC)
|
|
231
|
-
// This ensures that after a new analysis completes, its results are displayed
|
|
232
232
|
const latestRun = this.runs[0];
|
|
233
|
-
const
|
|
234
|
-
|
|
233
|
+
const currentRun = this.selectedRunId
|
|
234
|
+
? this.runs.find(run => String(run.id) === String(this.selectedRunId))
|
|
235
|
+
: null;
|
|
236
|
+
const runToSelect = currentRun || latestRun;
|
|
237
|
+
const shouldTriggerCallback = String(this.selectedRunId) !== String(runToSelect.id);
|
|
238
|
+
await this.selectRun(runToSelect.id, shouldTriggerCallback);
|
|
235
239
|
|
|
236
240
|
// Render the dropdown (after selecting so the selected state is correct)
|
|
237
241
|
this.renderDropdown(this.runs);
|
package/public/js/pr.js
CHANGED
|
@@ -506,6 +506,22 @@ class PRManager {
|
|
|
506
506
|
}
|
|
507
507
|
}
|
|
508
508
|
|
|
509
|
+
/**
|
|
510
|
+
* Sync worktree name/path to the diff options dropdown.
|
|
511
|
+
* Clears both fields when worktree_path is absent.
|
|
512
|
+
* @param {Object} prData - PR data object from the API
|
|
513
|
+
*/
|
|
514
|
+
_syncWorktreeDropdown(prData) {
|
|
515
|
+
if (!this.diffOptionsDropdown) return;
|
|
516
|
+
if (prData.worktree_path) {
|
|
517
|
+
this.diffOptionsDropdown.worktreeName = prData.worktree_name || null;
|
|
518
|
+
this.diffOptionsDropdown.worktreePath = prData.worktree_path;
|
|
519
|
+
} else {
|
|
520
|
+
this.diffOptionsDropdown.worktreeName = null;
|
|
521
|
+
this.diffOptionsDropdown.worktreePath = null;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
509
525
|
/**
|
|
510
526
|
* Load PR data from the API
|
|
511
527
|
* @param {string} owner - Repository owner
|
|
@@ -529,6 +545,9 @@ class PRManager {
|
|
|
529
545
|
const prData = responseData.data || responseData;
|
|
530
546
|
this.currentPR = prData;
|
|
531
547
|
|
|
548
|
+
// Update diff options dropdown with worktree path and display name
|
|
549
|
+
this._syncWorktreeDropdown(prData);
|
|
550
|
+
|
|
532
551
|
// Render PR header with metadata
|
|
533
552
|
this.renderPRHeader(prData);
|
|
534
553
|
|
|
@@ -5129,6 +5148,9 @@ class PRManager {
|
|
|
5129
5148
|
);
|
|
5130
5149
|
}
|
|
5131
5150
|
|
|
5151
|
+
// Sync worktree label to dropdown (may have changed after refresh)
|
|
5152
|
+
this._syncWorktreeDropdown(data.data);
|
|
5153
|
+
|
|
5132
5154
|
// Save scroll position and expanded state
|
|
5133
5155
|
const scrollPosition = window.scrollY;
|
|
5134
5156
|
const expandedFolders = new Set(this.expandedFolders);
|