@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.
Files changed (46) hide show
  1. package/README.md +7 -6
  2. package/package.json +5 -4
  3. package/plugin/.claude-plugin/plugin.json +1 -1
  4. package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
  5. package/plugin-code-critic/skills/analyze/references/orchestration-balanced.md +9 -1
  6. package/plugin-code-critic/skills/analyze/references/orchestration-fast.md +8 -1
  7. package/plugin-code-critic/skills/analyze/references/orchestration-thorough.md +8 -7
  8. package/public/css/repo-settings.css +347 -0
  9. package/public/index.html +46 -9
  10. package/public/js/components/AIPanel.js +79 -37
  11. package/public/js/components/DiffOptionsDropdown.js +84 -1
  12. package/public/js/index.js +31 -6
  13. package/public/js/modules/analysis-history.js +11 -7
  14. package/public/js/pr.js +22 -0
  15. package/public/js/repo-settings.js +334 -6
  16. package/public/repo-settings.html +29 -0
  17. package/src/ai/analyzer.js +28 -19
  18. package/src/ai/claude-cli.js +2 -0
  19. package/src/ai/claude-provider.js +4 -1
  20. package/src/ai/prompts/baseline/consolidation/balanced.js +6 -4
  21. package/src/ai/prompts/baseline/consolidation/fast.js +6 -2
  22. package/src/ai/prompts/baseline/consolidation/thorough.js +7 -6
  23. package/src/ai/prompts/baseline/orchestration/balanced.js +13 -1
  24. package/src/ai/prompts/baseline/orchestration/fast.js +12 -1
  25. package/src/ai/prompts/baseline/orchestration/thorough.js +8 -7
  26. package/src/ai/provider.js +7 -6
  27. package/src/chat/session-manager.js +6 -3
  28. package/src/config.js +230 -38
  29. package/src/database.js +766 -38
  30. package/src/git/worktree-pool-lifecycle.js +674 -0
  31. package/src/git/worktree-pool-usage.js +216 -0
  32. package/src/git/worktree.js +46 -13
  33. package/src/main.js +185 -26
  34. package/src/routes/analyses.js +48 -26
  35. package/src/routes/chat.js +27 -3
  36. package/src/routes/config.js +17 -5
  37. package/src/routes/executable-analysis.js +38 -19
  38. package/src/routes/local.js +19 -6
  39. package/src/routes/mcp.js +13 -2
  40. package/src/routes/pr.js +72 -29
  41. package/src/routes/setup.js +41 -4
  42. package/src/routes/stack-analysis.js +29 -10
  43. package/src/routes/worktrees.js +294 -9
  44. package/src/server.js +20 -3
  45. package/src/setup/pr-setup.js +161 -27
  46. 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
- if (!window.chatPanel) return;
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 active and dismissed findings (upper-right corner)
1249
+ // Chat button for all findings when chat is available
1208
1250
  let chatAction = '';
1209
- if (finding.status !== 'adopted' && document.documentElement.getAttribute('data-chat') === 'available') {
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 AI-originated comments
1324
+ // Chat button for active comments
1283
1325
  let chatAction = '';
1284
- if (!isDismissed && comment.parent_id && document.documentElement.getAttribute('data-chat') === 'available') {
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-finding-file="${comment.file || ''}" data-finding-title="${this.escapeHtml(title)}" title="Chat" aria-label="Chat about comment">
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
 
@@ -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: 'Start Review' }
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: 'Review Local' };
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
- window.location.href = '/local?path=' + encodeURIComponent(pathValue);
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
- window.location.href = '/pr/' + encodeURIComponent(parsed.owner) + '/' + encodeURIComponent(parsed.repo) + '/' + encodeURIComponent(parsed.prNumber);
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 and always selects the latest run.
209
- * For refreshing after a new analysis completes, use refresh() instead, which has
210
- * logic to optionally preserve the user's current selection.
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 shouldTriggerCallback = !this.selectedRunId || String(this.selectedRunId) !== String(latestRun.id);
234
- await this.selectRun(latestRun.id, shouldTriggerCallback);
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);