From 1f36e2312d316969db65a601ac7d9793fbc3bc4c Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Sun, 8 Mar 2026 20:16:00 +0000 Subject: feat: rename working_dir→project_dir; git sandbox execution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ClaudeConfig.WorkingDir → ProjectDir (json: project_dir) - UnmarshalJSON fallback reads legacy working_dir from DB records - New executions with project_dir clone into a temp sandbox via git clone --local - Non-git project_dirs get git init + initial commit before clone - After success: verify clean working tree, merge --ff-only back to project_dir, remove sandbox - On failure/BLOCKED: sandbox preserved, path included in error message - Resume executions run directly in project_dir (no re-clone) Co-Authored-By: Claude Sonnet 4.6 --- web/app.js | 38 ++++++++++++++++++++------------------ web/index.html | 5 +++-- 2 files changed, 23 insertions(+), 20 deletions(-) (limited to 'web') diff --git a/web/app.js b/web/app.js index 3b807c4..28d438a 100644 --- a/web/app.js +++ b/web/app.js @@ -228,9 +228,10 @@ function sortTasksByDate(tasks) { // ── Filter ──────────────────────────────────────────────────────────────────── -const HIDE_STATES = new Set(['COMPLETED', 'FAILED']); -const ACTIVE_STATES = new Set(['PENDING', 'QUEUED', 'RUNNING', 'READY', 'BLOCKED']); -const DONE_STATES = new Set(['COMPLETED', 'FAILED', 'TIMED_OUT', 'CANCELLED', 'BUDGET_EXCEEDED']); +const HIDE_STATES = new Set(['COMPLETED', 'FAILED']); +const ACTIVE_STATES = new Set(['PENDING', 'QUEUED', 'RUNNING', 'READY', 'BLOCKED']); +const INTERRUPTED_STATES = new Set(['CANCELLED', 'FAILED']); +const DONE_STATES = new Set(['COMPLETED', 'TIMED_OUT', 'BUDGET_EXCEEDED']); // filterActiveTasks uses its own set (excludes PENDING — tasks "in-flight" only) const _PANEL_ACTIVE_STATES = new Set(['RUNNING', 'READY', 'QUEUED', 'BLOCKED']); @@ -245,8 +246,9 @@ export function filterActiveTasks(tasks) { } export function filterTasksByTab(tasks, tab) { - if (tab === 'active') return tasks.filter(t => ACTIVE_STATES.has(t.state)); - if (tab === 'done') return tasks.filter(t => DONE_STATES.has(t.state)); + if (tab === 'active') return tasks.filter(t => ACTIVE_STATES.has(t.state)); + if (tab === 'interrupted') return tasks.filter(t => INTERRUPTED_STATES.has(t.state)); + if (tab === 'done') return tasks.filter(t => DONE_STATES.has(t.state)); return tasks; } @@ -517,7 +519,7 @@ function createEditForm(task) { form.appendChild(makeField('Description', 'textarea', { name: 'description', rows: '2', value: task.description || '' })); form.appendChild(makeField('Instructions', 'textarea', { name: 'instructions', rows: '4', value: c.instructions || '' })); form.appendChild(makeField('Model', 'input', { type: 'text', name: 'model', value: c.model || 'sonnet' })); - form.appendChild(makeField('Working Directory', 'input', { type: 'text', name: 'working_dir', value: c.working_dir || '', placeholder: '/path/to/repo' })); + form.appendChild(makeField('Working Directory', 'input', { type: 'text', name: 'project_dir', value: c.project_dir || '', placeholder: '/path/to/repo' })); form.appendChild(makeField('Max Budget (USD)', 'input', { type: 'number', name: 'max_budget_usd', step: '0.01', value: c.max_budget_usd != null ? String(c.max_budget_usd) : '1.00' })); form.appendChild(makeField('Timeout', 'input', { type: 'text', name: 'timeout', value: formatDurationForInput(task.timeout) || '15m', placeholder: '15m' })); @@ -569,7 +571,7 @@ async function handleEditSave(taskId, form, saveBtn) { claude: { model: get('model'), instructions: get('instructions'), - working_dir: get('working_dir'), + project_dir: get('project_dir'), max_budget_usd: parseFloat(get('max_budget_usd')), }, timeout: get('timeout'), @@ -1018,7 +1020,7 @@ async function elaborateTask(prompt, workingDir) { const res = await fetch(`${API_BASE}/api/tasks/elaborate`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ prompt, working_dir: workingDir }), + body: JSON.stringify({ prompt, project_dir: workingDir }), }); if (!res.ok) { let msg = `HTTP ${res.status}`; @@ -1048,13 +1050,13 @@ function buildValidatePayload() { const f = document.getElementById('task-form'); const name = f.querySelector('[name="name"]').value; const instructions = f.querySelector('[name="instructions"]').value; - const working_dir = f.querySelector('[name="working_dir"]').value; + const project_dir = f.querySelector('[name="project_dir"]').value; const model = f.querySelector('[name="model"]').value; const allowedToolsEl = f.querySelector('[name="allowed_tools"]'); const allowed_tools = allowedToolsEl ? allowedToolsEl.value.split(',').map(s => s.trim()).filter(Boolean) : []; - return { name, claude: { instructions, working_dir, model, allowed_tools } }; + return { name, claude: { instructions, project_dir, model, allowed_tools } }; } function renderValidationResult(result) { @@ -1167,7 +1169,7 @@ function closeTaskModal() { } async function createTask(formData) { - const selectVal = formData.get('working_dir'); + const selectVal = formData.get('project_dir'); const workingDir = selectVal === '__new__' ? document.getElementById('new-project-input').value.trim() : selectVal; @@ -1177,7 +1179,7 @@ async function createTask(formData) { claude: { model: formData.get('model'), instructions: formData.get('instructions'), - working_dir: workingDir, + project_dir: workingDir, max_budget_usd: parseFloat(formData.get('max_budget_usd')), }, timeout: formData.get('timeout'), @@ -1221,7 +1223,7 @@ async function saveTemplate(formData) { claude: { model: formData.get('model'), instructions: formData.get('instructions'), - working_dir: formData.get('working_dir'), + project_dir: formData.get('project_dir'), max_budget_usd: parseFloat(formData.get('max_budget_usd')), allowed_tools: splitTrim(formData.get('allowed_tools') || ''), }, @@ -1401,7 +1403,7 @@ function renderTaskPanel(task, executions) { claudeGrid.append( makeMetaItem('Model', c.model), makeMetaItem('Max Budget', c.max_budget_usd != null ? `$${c.max_budget_usd.toFixed(2)}` : '—'), - makeMetaItem('Working Dir', c.working_dir), + makeMetaItem('Project Dir', c.project_dir), makeMetaItem('Permission Mode', c.permission_mode || 'default'), ); if (c.allowed_tools && c.allowed_tools.length > 0) { @@ -2071,15 +2073,15 @@ if (typeof document !== 'undefined') document.addEventListener('DOMContentLoaded f.querySelector('[name="name"]').value = result.name; if (result.claude && result.claude.instructions) f.querySelector('[name="instructions"]').value = result.claude.instructions; - if (result.claude && result.claude.working_dir) { + if (result.claude && result.claude.project_dir) { const sel = document.getElementById('project-select'); - const exists = [...sel.options].some(o => o.value === result.claude.working_dir); + const exists = [...sel.options].some(o => o.value === result.claude.project_dir); if (exists) { - sel.value = result.claude.working_dir; + sel.value = result.claude.project_dir; } else { sel.value = '__new__'; document.getElementById('new-project-row').hidden = false; - document.getElementById('new-project-input').value = result.claude.working_dir; + document.getElementById('new-project-input').value = result.claude.project_dir; } } if (result.claude && result.claude.model) diff --git a/web/index.html b/web/index.html index 3b7901c..842c272 100644 --- a/web/index.html +++ b/web/index.html @@ -24,6 +24,7 @@
@@ -101,7 +102,7 @@ - + -- cgit v1.2.3