@in-the-loop-labs/pair-review 3.2.3 → 3.3.1
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/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/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/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 +679 -0
- package/src/git/worktree-pool-usage.js +216 -0
- package/src/git/worktree.js +157 -32
- 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
|
@@ -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.
|
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 ? ' · ' + 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;
|