@in-the-loop-labs/pair-review 3.2.3 → 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.
@@ -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.
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);
@@ -14,6 +14,7 @@ class RepoSettingsPage {
14
14
  this.providers = {};
15
15
  this.selectedProvider = null;
16
16
  this.councils = [];
17
+ this.worktreeData = null;
17
18
 
18
19
  this.init();
19
20
  }
@@ -39,6 +40,9 @@ class RepoSettingsPage {
39
40
 
40
41
  // Load settings
41
42
  await this.loadSettings();
43
+
44
+ // Load worktrees (non-blocking, section stays hidden on error)
45
+ await this.loadWorktrees();
42
46
  }
43
47
 
44
48
  /**
@@ -210,6 +214,16 @@ class RepoSettingsPage {
210
214
  });
211
215
  }
212
216
 
217
+ // Load skills select
218
+ const loadSkillsSelect = document.getElementById('load-skills-select');
219
+ if (loadSkillsSelect) {
220
+ loadSkillsSelect.addEventListener('change', () => {
221
+ const val = loadSkillsSelect.value;
222
+ this.currentSettings.load_skills = val === '' ? null : parseInt(val, 10);
223
+ this.checkForChanges();
224
+ });
225
+ }
226
+
213
227
  // Analysis mode segmented control
214
228
  const modeToggle = document.getElementById('analysis-mode-toggle');
215
229
  if (modeToggle) {
@@ -249,6 +263,22 @@ class RepoSettingsPage {
249
263
  clearLocalPathBtn.addEventListener('click', () => this.handleClearLocalPath());
250
264
  }
251
265
 
266
+ // Worktree actions (delegated)
267
+ const worktreesContent = document.getElementById('worktrees-content');
268
+ if (worktreesContent) {
269
+ worktreesContent.addEventListener('click', (e) => {
270
+ const deleteBtn = e.target.closest('.worktree-delete-btn');
271
+ if (deleteBtn) {
272
+ this.deleteWorktree(deleteBtn.dataset.worktreeId);
273
+ return;
274
+ }
275
+ const deleteAllBtn = e.target.closest('.worktree-delete-all-btn');
276
+ if (deleteAllBtn) {
277
+ this.deleteAllWorktrees();
278
+ }
279
+ });
280
+ }
281
+
252
282
  // Warn before leaving with unsaved changes
253
283
  window.addEventListener('beforeunload', (e) => {
254
284
  if (this.hasUnsavedChanges) {
@@ -825,7 +855,10 @@ class RepoSettingsPage {
825
855
  default_council_id: settings.default_council_id || null,
826
856
  default_instructions: settings.default_instructions || '',
827
857
  local_path: settings.local_path || null,
828
- default_chat_instructions: settings.default_chat_instructions || ''
858
+ default_chat_instructions: settings.default_chat_instructions || '',
859
+ pool_size: settings.pool_size ?? null,
860
+ pool_fetch_interval_minutes: settings.pool_fetch_interval_minutes ?? null,
861
+ load_skills: settings.load_skills ?? null
829
862
  };
830
863
 
831
864
  // Set current settings
@@ -844,13 +877,279 @@ class RepoSettingsPage {
844
877
  default_council_id: null,
845
878
  default_instructions: '',
846
879
  local_path: null,
847
- default_chat_instructions: ''
880
+ default_chat_instructions: '',
881
+ pool_size: null,
882
+ pool_fetch_interval_minutes: null,
883
+ load_skills: null
848
884
  };
849
885
  this.currentSettings = { ...this.originalSettings };
850
886
  this.updateUI();
851
887
  }
852
888
  }
853
889
 
890
+ /**
891
+ * Load worktree data for the current repository
892
+ */
893
+ async loadWorktrees() {
894
+ if (!this.owner || !this.repo) return;
895
+
896
+ try {
897
+ const response = await fetch(`/api/repos/${this.owner}/${this.repo}/worktrees`);
898
+ if (!response.ok) {
899
+ throw new Error('Failed to load worktrees');
900
+ }
901
+ this.worktreeData = await response.json();
902
+
903
+ // Seed pool settings from resolved config values when DB has no override
904
+ const pool = this.worktreeData.pool || {};
905
+ if (this.originalSettings.pool_size == null && pool.size) {
906
+ this.originalSettings.pool_size = pool.size;
907
+ this.currentSettings.pool_size = pool.size;
908
+ }
909
+ if (this.originalSettings.pool_fetch_interval_minutes == null && pool.fetch_interval_minutes) {
910
+ this.originalSettings.pool_fetch_interval_minutes = pool.fetch_interval_minutes;
911
+ this.currentSettings.pool_fetch_interval_minutes = pool.fetch_interval_minutes;
912
+ }
913
+
914
+ this.renderWorktrees();
915
+ } catch (error) {
916
+ console.error('Error loading worktrees:', error);
917
+ }
918
+ }
919
+
920
+ /**
921
+ * Render the worktrees section content
922
+ */
923
+ renderWorktrees() {
924
+ const section = document.getElementById('worktrees-section');
925
+ const content = document.getElementById('worktrees-content');
926
+ if (!section || !content) return;
927
+
928
+ if (!this.worktreeData) return;
929
+
930
+ section.style.display = '';
931
+
932
+ const pool = this.worktreeData.pool || {};
933
+ const worktrees = this.worktreeData.worktrees || [];
934
+ let html = '';
935
+
936
+ // Pool settings (editable)
937
+ const poolSizeValue = this.currentSettings.pool_size ?? '';
938
+ const fetchIntervalValue = this.currentSettings.pool_fetch_interval_minutes ?? '';
939
+ const currentCount = pool.current_count || 0;
940
+ const countNote = poolSizeValue ? ` (${currentCount} active)` : '';
941
+
942
+ html += `<div class="worktree-pool-config">
943
+ <div class="worktree-pool-config-items">
944
+ <div class="worktree-pool-config-item">
945
+ <label class="worktree-pool-config-label" for="pool-size-input">Pool Size</label>
946
+ <div class="worktree-pool-input-group">
947
+ <input type="number" id="pool-size-input" class="worktree-pool-input"
948
+ min="0" max="20" step="1" placeholder="0"
949
+ value="${this.escapeHtml(String(poolSizeValue))}">
950
+ <span class="worktree-pool-input-note">${this.escapeHtml(countNote)}</span>
951
+ </div>
952
+ </div>
953
+ <div class="worktree-pool-config-item">
954
+ <label class="worktree-pool-config-label" for="pool-fetch-interval-input">Fetch Interval</label>
955
+ <div class="worktree-pool-input-group">
956
+ <input type="number" id="pool-fetch-interval-input" class="worktree-pool-input"
957
+ min="0" max="1440" step="1" placeholder="Off"
958
+ value="${this.escapeHtml(String(fetchIntervalValue))}">
959
+ <span class="worktree-pool-input-note">minutes</span>
960
+ </div>
961
+ </div>
962
+ </div>
963
+ <p class="worktree-pool-hint">
964
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor"><path d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm6.5-.25A.75.75 0 017.25 7h1a.75.75 0 01.75.75v2.75h.25a.75.75 0 010 1.5h-2a.75.75 0 010-1.5h.25v-2h-.25a.75.75 0 01-.75-.75zM8 6a1 1 0 100-2 1 1 0 000 2z"/></svg>
965
+ Pre-warms worktrees so PR reviews start instantly. Set size to 0 to disable.
966
+ </p>
967
+ </div>`;
968
+
969
+ // Worktree list
970
+ if (worktrees.length > 0) {
971
+ html += '<div class="worktree-list">';
972
+ for (const wt of worktrees) {
973
+ const badgeHtml = wt.is_pool
974
+ ? '<span class="worktree-pool-badge">Pool</span>'
975
+ : '<span class="worktree-adhoc-badge">Ad-hoc</span>';
976
+ const prInfo = wt.pr_number ? '#' + wt.pr_number : 'Unassigned';
977
+ const branchInfo = wt.branch ? ' &middot; ' + this.escapeHtml(wt.branch) : '';
978
+ const fullPath = wt.path || '';
979
+ const diskWarning = !wt.disk_exists
980
+ ? '<span class="worktree-disk-warning">Missing from disk</span>'
981
+ : '';
982
+ const statusIcon = this.getWorktreeStatusIcon(wt.status);
983
+ const statusLabel = this.getWorktreeStatusLabel(wt.status);
984
+ const fetchedHtml = wt.last_fetched_at
985
+ ? `<span class="worktree-timestamp">Fetched ${this.formatRelativeTime(wt.last_fetched_at)}</span>`
986
+ : '';
987
+
988
+ html += `<div class="worktree-item">
989
+ <div class="worktree-item-top">
990
+ <div class="worktree-item-left">
991
+ ${badgeHtml}
992
+ <span class="worktree-pr-info">${prInfo}${branchInfo}</span>
993
+ ${diskWarning}
994
+ </div>
995
+ <div class="worktree-item-right">
996
+ <span class="worktree-status worktree-status--${this.escapeHtml(wt.status || 'unknown')}">
997
+ ${statusIcon} ${statusLabel}
998
+ </span>
999
+ ${fetchedHtml}
1000
+ <button class="worktree-delete-btn" data-worktree-id="${this.escapeHtml(wt.id)}" title="Delete worktree">
1001
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
1002
+ <path fill-rule="evenodd" d="M6.5 1.75a.25.25 0 01.25-.25h2.5a.25.25 0 01.25.25V3h-3V1.75zm4.5 0V3h2.25a.75.75 0 010 1.5H2.75a.75.75 0 010-1.5H5V1.75C5 .784 5.784 0 6.75 0h2.5C10.216 0 11 .784 11 1.75zM4.496 6.675a.75.75 0 10-1.492.15l.66 6.6A1.75 1.75 0 005.405 15h5.19a1.75 1.75 0 001.741-1.575l.66-6.6a.75.75 0 00-1.492-.15l-.66 6.6a.25.25 0 01-.249.225h-5.19a.25.25 0 01-.249-.225l-.66-6.6z"/>
1003
+ </svg>
1004
+ </button>
1005
+ </div>
1006
+ </div>
1007
+ <div class="worktree-item-bottom">
1008
+ <span class="worktree-path">${this.escapeHtml(fullPath)}</span>
1009
+ </div>
1010
+ </div>`;
1011
+ }
1012
+ html += '</div>';
1013
+
1014
+ // Delete all button
1015
+ html += `<div class="worktree-actions">
1016
+ <button class="worktree-delete-all-btn">
1017
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
1018
+ <path fill-rule="evenodd" d="M6.5 1.75a.25.25 0 01.25-.25h2.5a.25.25 0 01.25.25V3h-3V1.75zm4.5 0V3h2.25a.75.75 0 010 1.5H2.75a.75.75 0 010-1.5H5V1.75C5 .784 5.784 0 6.75 0h2.5C10.216 0 11 .784 11 1.75zM4.496 6.675a.75.75 0 10-1.492.15l.66 6.6A1.75 1.75 0 005.405 15h5.19a1.75 1.75 0 001.741-1.575l.66-6.6a.75.75 0 00-1.492-.15l-.66 6.6a.25.25 0 01-.249.225h-5.19a.25.25 0 01-.249-.225l-.66-6.6z"/>
1019
+ </svg>
1020
+ Delete All Worktrees
1021
+ </button>
1022
+ </div>`;
1023
+ } else {
1024
+ html += '<div class="worktree-empty">No worktrees found for this repository.</div>';
1025
+ }
1026
+
1027
+ content.innerHTML = html;
1028
+
1029
+ // Wire up pool setting inputs
1030
+ const poolSizeInput = document.getElementById('pool-size-input');
1031
+ if (poolSizeInput) {
1032
+ poolSizeInput.addEventListener('input', () => {
1033
+ const val = poolSizeInput.value.trim();
1034
+ this.currentSettings.pool_size = val === '' ? null : parseInt(val, 10);
1035
+ this.checkForChanges();
1036
+ });
1037
+ }
1038
+ const poolFetchInput = document.getElementById('pool-fetch-interval-input');
1039
+ if (poolFetchInput) {
1040
+ poolFetchInput.addEventListener('input', () => {
1041
+ const val = poolFetchInput.value.trim();
1042
+ this.currentSettings.pool_fetch_interval_minutes = val === '' ? null : parseInt(val, 10);
1043
+ this.checkForChanges();
1044
+ });
1045
+ }
1046
+ }
1047
+
1048
+ /**
1049
+ * Get the SVG icon for a worktree status
1050
+ * @param {string} status
1051
+ * @returns {string} Inline SVG string
1052
+ */
1053
+ getWorktreeStatusIcon(status) {
1054
+ switch (status) {
1055
+ case 'available':
1056
+ return '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><circle cx="8" cy="8" r="4" fill="#2da44e"/></svg>';
1057
+ case 'in_use':
1058
+ return '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M4 4a4 4 0 0 1 8 0v2h.25c.966 0 1.75.784 1.75 1.75v5.5A1.75 1.75 0 0 1 12.25 15h-8.5A1.75 1.75 0 0 1 2 13.25v-5.5C2 6.784 2.784 6 3.75 6H4Zm8.25 3.5h-8.5a.25.25 0 0 0-.25.25v5.5c0 .138.112.25.25.25h8.5a.25.25 0 0 0 .25-.25v-5.5a.25.25 0 0 0-.25-.25ZM10.5 6V4a2.5 2.5 0 1 0-5 0v2Z"/></svg>';
1059
+ case 'switching':
1060
+ return '<svg class="worktree-icon-spin" width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M8 2.5a5.5 5.5 0 00-5.23 3.79.75.75 0 01-1.42-.48A7.001 7.001 0 0115 8a7 7 0 01-13.65 2.19.75.75 0 011.42-.48A5.5 5.5 0 108 2.5z"/></svg>';
1061
+ case 'creating':
1062
+ return '<svg class="worktree-icon-spin" width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><path d="M8 2.5a5.5 5.5 0 00-5.23 3.79.75.75 0 01-1.42-.48A7.001 7.001 0 0115 8a7 7 0 01-13.65 2.19.75.75 0 011.42-.48A5.5 5.5 0 108 2.5z"/></svg>';
1063
+ case 'active':
1064
+ return '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><circle cx="8" cy="8" r="4" fill="#0969da"/></svg>';
1065
+ default:
1066
+ return '<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor"><circle cx="8" cy="8" r="4" fill="#8b949e"/></svg>';
1067
+ }
1068
+ }
1069
+
1070
+ /**
1071
+ * Get the display label for a worktree status
1072
+ * @param {string} status
1073
+ * @returns {string}
1074
+ */
1075
+ getWorktreeStatusLabel(status) {
1076
+ switch (status) {
1077
+ case 'available': return 'Available';
1078
+ case 'in_use': return 'In use';
1079
+ case 'switching': return 'Switching';
1080
+ case 'creating': return 'Creating';
1081
+ case 'active': return 'Active';
1082
+ default: return status || 'Unknown';
1083
+ }
1084
+ }
1085
+
1086
+
1087
+
1088
+ /**
1089
+ * Format an ISO timestamp as a human-readable relative time string
1090
+ * @param {string} isoString
1091
+ * @returns {string}
1092
+ */
1093
+ formatRelativeTime(isoString) {
1094
+ if (!isoString) return '';
1095
+ const seconds = Math.floor((Date.now() - new Date(isoString).getTime()) / 1000);
1096
+ if (seconds < 60) return 'just now';
1097
+ const minutes = Math.floor(seconds / 60);
1098
+ if (minutes < 60) return minutes + 'm ago';
1099
+ const hours = Math.floor(minutes / 60);
1100
+ if (hours < 24) return hours + 'h ago';
1101
+ const days = Math.floor(hours / 24);
1102
+ if (days === 1) return 'yesterday';
1103
+ return days + 'd ago';
1104
+ }
1105
+
1106
+ /**
1107
+ * Delete a single worktree by ID
1108
+ * @param {string} worktreeId
1109
+ */
1110
+ async deleteWorktree(worktreeId) {
1111
+ if (!confirm('Delete this worktree? This removes it from disk and cannot be undone.')) return;
1112
+
1113
+ try {
1114
+ const response = await fetch(`/api/repos/${this.owner}/${this.repo}/worktrees/${worktreeId}`, {
1115
+ method: 'DELETE'
1116
+ });
1117
+ if (!response.ok) {
1118
+ const data = await response.json().catch(() => ({}));
1119
+ throw new Error(data.error || 'Failed to delete worktree');
1120
+ }
1121
+ this.showToast('success', 'Worktree deleted');
1122
+ await this.loadWorktrees();
1123
+ } catch (error) {
1124
+ this.showToast('error', 'Failed to delete worktree: ' + error.message);
1125
+ }
1126
+ }
1127
+
1128
+ /**
1129
+ * Delete all worktrees for the current repository
1130
+ */
1131
+ async deleteAllWorktrees() {
1132
+ const count = this.worktreeData && this.worktreeData.worktrees
1133
+ ? this.worktreeData.worktrees.length
1134
+ : 0;
1135
+ if (!confirm(`Delete all ${count} worktree(s)? This removes them from disk and cannot be undone.`)) return;
1136
+
1137
+ try {
1138
+ const response = await fetch(`/api/repos/${this.owner}/${this.repo}/worktrees`, {
1139
+ method: 'DELETE'
1140
+ });
1141
+ if (!response.ok) {
1142
+ const data = await response.json().catch(() => ({}));
1143
+ throw new Error(data.error || 'Failed to delete worktrees');
1144
+ }
1145
+ const data = await response.json();
1146
+ this.showToast('success', `Deleted ${data.deleted} worktree(s)`);
1147
+ await this.loadWorktrees();
1148
+ } catch (error) {
1149
+ this.showToast('error', 'Failed to delete worktrees: ' + error.message);
1150
+ }
1151
+ }
1152
+
854
1153
  updateUI() {
855
1154
  // Update provider selection - validate provider exists before selecting
856
1155
  let providerId = this.currentSettings.default_provider;
@@ -908,6 +1207,23 @@ class RepoSettingsPage {
908
1207
 
909
1208
  // Update local path display
910
1209
  this.updateLocalPathDisplay();
1210
+
1211
+ // Update pool setting inputs
1212
+ const poolSizeInput = document.getElementById('pool-size-input');
1213
+ if (poolSizeInput) {
1214
+ poolSizeInput.value = this.currentSettings.pool_size ?? '';
1215
+ }
1216
+ const poolFetchInput = document.getElementById('pool-fetch-interval-input');
1217
+ if (poolFetchInput) {
1218
+ poolFetchInput.value = this.currentSettings.pool_fetch_interval_minutes ?? '';
1219
+ }
1220
+
1221
+ // Update load skills select
1222
+ const loadSkillsSelect = document.getElementById('load-skills-select');
1223
+ if (loadSkillsSelect) {
1224
+ const val = this.currentSettings.load_skills;
1225
+ loadSkillsSelect.value = val === null || val === undefined ? '' : String(val);
1226
+ }
911
1227
  }
912
1228
 
913
1229
  /**
@@ -957,8 +1273,11 @@ class RepoSettingsPage {
957
1273
  const councilChanged = (this.currentSettings.default_council_id ?? null) !== (this.originalSettings.default_council_id ?? null);
958
1274
  const instructionsChanged = (this.currentSettings.default_instructions ?? '') !== (this.originalSettings.default_instructions ?? '');
959
1275
  const chatInstructionsChanged = (this.currentSettings.default_chat_instructions ?? '') !== (this.originalSettings.default_chat_instructions ?? '');
1276
+ const poolSizeChanged = (this.currentSettings.pool_size ?? null) !== (this.originalSettings.pool_size ?? null);
1277
+ const poolFetchChanged = (this.currentSettings.pool_fetch_interval_minutes ?? null) !== (this.originalSettings.pool_fetch_interval_minutes ?? null);
1278
+ const loadSkillsChanged = (this.currentSettings.load_skills ?? null) !== (this.originalSettings.load_skills ?? null);
960
1279
 
961
- this.hasUnsavedChanges = providerChanged || modelChanged || tabChanged || councilChanged || instructionsChanged || chatInstructionsChanged;
1280
+ this.hasUnsavedChanges = providerChanged || modelChanged || tabChanged || councilChanged || instructionsChanged || chatInstructionsChanged || poolSizeChanged || poolFetchChanged || loadSkillsChanged;
962
1281
 
963
1282
  // Show/hide action bar
964
1283
  const actionBar = document.getElementById('action-bar');
@@ -993,7 +1312,10 @@ class RepoSettingsPage {
993
1312
  default_tab: this.currentSettings.default_tab,
994
1313
  default_council_id: this.currentSettings.default_council_id,
995
1314
  default_instructions: this.currentSettings.default_instructions,
996
- default_chat_instructions: this.currentSettings.default_chat_instructions
1315
+ default_chat_instructions: this.currentSettings.default_chat_instructions,
1316
+ pool_size: this.currentSettings.pool_size,
1317
+ pool_fetch_interval_minutes: this.currentSettings.pool_fetch_interval_minutes,
1318
+ load_skills: this.currentSettings.load_skills
997
1319
  })
998
1320
  });
999
1321
 
@@ -1068,7 +1390,10 @@ class RepoSettingsPage {
1068
1390
  default_council_id: null,
1069
1391
  default_instructions: '',
1070
1392
  local_path: null,
1071
- default_chat_instructions: ''
1393
+ default_chat_instructions: '',
1394
+ pool_size: null,
1395
+ pool_fetch_interval_minutes: null,
1396
+ load_skills: null
1072
1397
  })
1073
1398
  });
1074
1399
 
@@ -1084,7 +1409,10 @@ class RepoSettingsPage {
1084
1409
  default_council_id: null,
1085
1410
  default_instructions: '',
1086
1411
  local_path: null,
1087
- default_chat_instructions: ''
1412
+ default_chat_instructions: '',
1413
+ pool_size: null,
1414
+ pool_fetch_interval_minutes: null,
1415
+ load_skills: null
1088
1416
  };
1089
1417
  this.currentSettings = { ...this.originalSettings };
1090
1418
  this.hasUnsavedChanges = false;