summaryrefslogtreecommitdiff
path: root/web/app.js
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-03-18 23:56:34 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-03-18 23:56:34 +0000
commit599a26d556df52b364b5b540762a521d22eb5b7b (patch)
tree740c141c52764604fc8d4c036733e5f47368b26a /web/app.js
parent0db05b0fa6de318f164a1d73ddc55db9c59f1fc3 (diff)
parent7df4f06ae0e3ae80bd967bf53cbec36e58b4a3bd (diff)
Merge feat/container-execution into master
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'web/app.js')
-rw-r--r--web/app.js350
1 files changed, 155 insertions, 195 deletions
diff --git a/web/app.js b/web/app.js
index f785605..6ddb23c 100644
--- a/web/app.js
+++ b/web/app.js
@@ -407,6 +407,14 @@ export function setTaskFilterTab(tab) {
localStorage.setItem('taskFilterTab', tab);
}
+export function getActiveMainTab() {
+ return localStorage.getItem('activeMainTab') ?? 'queue';
+}
+
+export function setActiveMainTab(tab) {
+ localStorage.setItem('activeMainTab', tab);
+}
+
// ── Tab badge counts ───────────────────────────────────────────────────────────
/**
@@ -1470,12 +1478,13 @@ function buildValidatePayload() {
const f = document.getElementById('task-form');
const name = f.querySelector('[name="name"]').value;
const instructions = f.querySelector('[name="instructions"]').value;
- const project_dir = f.querySelector('#project-select').value;
+ const repository_url = document.getElementById('repository-url').value;
+ const container_image = document.getElementById('container-image').value;
const allowedToolsEl = f.querySelector('[name="allowed_tools"]');
const allowed_tools = allowedToolsEl
? allowedToolsEl.value.split(',').map(s => s.trim()).filter(Boolean)
: [];
- return { name, agent: { instructions, project_dir, allowed_tools } };
+ return { name, repository_url, agent: { instructions, container_image, allowed_tools } };
}
function renderValidationResult(result) {
@@ -1533,49 +1542,6 @@ function renderValidationResult(result) {
async function openTaskModal() {
document.getElementById('task-modal').showModal();
- await populateProjectSelect();
-}
-
-async function populateProjectSelect() {
- const select = document.getElementById('project-select');
- const current = select.value;
- try {
- const res = await fetch(`${API_BASE}/api/workspaces`);
- const dirs = await res.json();
- select.innerHTML = '';
- dirs.forEach(dir => {
- const opt = document.createElement('option');
- opt.value = dir;
- opt.textContent = dir;
- if (dir === current || dir === '/workspace/claudomator') opt.selected = true;
- select.appendChild(opt);
- });
- } catch {
- // keep whatever options are already there
- }
- // Ensure "Create new project…" option is always last
- const newOpt = document.createElement('option');
- newOpt.value = '__new__';
- newOpt.textContent = 'Create new project…';
- select.appendChild(newOpt);
-}
-
-function initProjectSelect() {
- const select = document.getElementById('project-select');
- const newRow = document.getElementById('new-project-row');
- const newInput = document.getElementById('new-project-input');
- if (!select) return;
- select.addEventListener('change', () => {
- if (select.value === '__new__') {
- newRow.hidden = false;
- newInput.required = true;
- newInput.focus();
- } else {
- newRow.hidden = true;
- newInput.required = false;
- newInput.value = '';
- }
- });
}
function closeTaskModal() {
@@ -1589,20 +1555,20 @@ function closeTaskModal() {
}
async function createTask(formData) {
- const selectVal = formData.get('project_dir');
- const workingDir = selectVal === '__new__'
- ? document.getElementById('new-project-input').value.trim()
- : selectVal;
+ const repository_url = formData.get('repository_url');
+ const container_image = formData.get('container_image');
const elaboratePromptEl = document.getElementById('elaborate-prompt');
const elaborationInput = elaboratePromptEl ? elaboratePromptEl.value.trim() : '';
const body = {
name: formData.get('name'),
description: '',
elaboration_input: elaborationInput || undefined,
+ repository_url: repository_url,
agent: {
instructions: formData.get('instructions'),
- project_dir: workingDir,
+ container_image: container_image,
max_budget_usd: parseFloat(formData.get('max_budget_usd')),
+ type: 'container',
},
timeout: formData.get('timeout'),
priority: formData.get('priority'),
@@ -1743,7 +1709,7 @@ function makeMetaItem(label, valueText, opts = {}) {
return item;
}
-function renderTaskPanel(task, executions) {
+export function renderTaskPanel(task, executions) {
document.getElementById('task-panel-title').textContent = task.name;
const content = document.getElementById('task-panel-content');
content.innerHTML = '';
@@ -2729,6 +2695,8 @@ async function renderDropsPanel() {
// ── Tab switching ─────────────────────────────────────────────────────────────
function switchTab(name) {
+ setActiveMainTab(name);
+
// Update tab button active state
document.querySelectorAll('.tab').forEach(btn => {
btn.classList.toggle('active', btn.dataset.tab === name);
@@ -2749,169 +2717,161 @@ function switchTab(name) {
// ── Boot ──────────────────────────────────────────────────────────────────────
-if (typeof document !== 'undefined') document.addEventListener('DOMContentLoaded', () => {
- document.getElementById('btn-start-next').addEventListener('click', function() {
- handleStartNextTask(this);
- });
-
- switchTab('queue');
- startPolling();
- connectWebSocket();
+if (typeof document !== 'undefined') {
+ document.addEventListener('DOMContentLoaded', () => {
+ document.getElementById('btn-start-next').addEventListener('click', function() {
+ handleStartNextTask(this);
+ });
- // Side panel close
- document.getElementById('btn-close-panel').addEventListener('click', closeTaskPanel);
- document.getElementById('task-panel-backdrop').addEventListener('click', closeTaskPanel);
+ switchTab(getActiveMainTab());
+ startPolling();
+ connectWebSocket();
- // Execution logs modal close
- document.getElementById('btn-close-logs').addEventListener('click', () => {
- document.getElementById('logs-modal').close();
- });
+ // Side panel close
+ document.getElementById('btn-close-panel').addEventListener('click', closeTaskPanel);
+ document.getElementById('task-panel-backdrop').addEventListener('click', closeTaskPanel);
- // Tab bar
- document.querySelectorAll('.tab').forEach(btn => {
- btn.addEventListener('click', () => switchTab(btn.dataset.tab));
- });
+ // Execution logs modal close
+ document.getElementById('btn-close-logs').addEventListener('click', () => {
+ document.getElementById('logs-modal').close();
+ });
- // Task modal
- document.getElementById('btn-new-task').addEventListener('click', openTaskModal);
- document.getElementById('btn-cancel-task').addEventListener('click', closeTaskModal);
- initProjectSelect();
+ // Tab bar
+ document.querySelectorAll('.tab').forEach(btn => {
+ btn.addEventListener('click', () => switchTab(btn.dataset.tab));
+ });
- // Push notifications button
- const btnNotify = document.getElementById('btn-notifications');
- if (btnNotify) {
- btnNotify.addEventListener('click', () => enableNotifications(btnNotify));
- }
+ // Task modal
+ document.getElementById('btn-new-task').addEventListener('click', openTaskModal);
+ document.getElementById('btn-cancel-task').addEventListener('click', closeTaskModal);
- // Validate button
- document.getElementById('btn-validate').addEventListener('click', async () => {
- const btn = document.getElementById('btn-validate');
- const resultDiv = document.getElementById('validate-result');
- btn.disabled = true;
- btn.textContent = 'Checking…';
- try {
- const payload = buildValidatePayload();
- const result = await validateTask(payload);
- renderValidationResult(result);
- } catch (err) {
- resultDiv.removeAttribute('hidden');
- resultDiv.textContent = 'Validation failed: ' + err.message;
- } finally {
- btn.disabled = false;
- btn.textContent = 'Validate Instructions';
+ // Push notifications button
+ const btnNotify = document.getElementById('btn-notifications');
+ if (btnNotify) {
+ btnNotify.addEventListener('click', () => enableNotifications(btnNotify));
}
- });
- // Draft with AI button
- const btnElaborate = document.getElementById('btn-elaborate');
- btnElaborate.addEventListener('click', async () => {
- const prompt = document.getElementById('elaborate-prompt').value.trim();
- if (!prompt) {
- const form = document.getElementById('task-form');
- // Remove previous error
- const prev = form.querySelector('.form-error');
- if (prev) prev.remove();
- const errEl = document.createElement('p');
- errEl.className = 'form-error';
- errEl.textContent = 'Please enter a description before drafting.';
- form.querySelector('.elaborate-section').appendChild(errEl);
- return;
- }
+ // Validate button
+ document.getElementById('btn-validate').addEventListener('click', async () => {
+ const btn = document.getElementById('btn-validate');
+ const resultDiv = document.getElementById('validate-result');
+ btn.disabled = true;
+ btn.textContent = 'Checking…';
+ try {
+ const payload = buildValidatePayload();
+ const result = await validateTask(payload);
+ renderValidationResult(result);
+ } catch (err) {
+ resultDiv.removeAttribute('hidden');
+ resultDiv.textContent = 'Validation failed: ' + err.message;
+ } finally {
+ btn.disabled = false;
+ btn.textContent = 'Validate Instructions';
+ }
+ });
+
+ // Draft with AI button
+ const btnElaborate = document.getElementById('btn-elaborate');
+ btnElaborate.addEventListener('click', async () => {
+ const prompt = document.getElementById('elaborate-prompt').value.trim();
+ if (!prompt) {
+ const form = document.getElementById('task-form');
+ // Remove previous error
+ const prev = form.querySelector('.form-error');
+ if (prev) prev.remove();
+ const errEl = document.createElement('p');
+ errEl.className = 'form-error';
+ errEl.textContent = 'Please enter a description before drafting.';
+ form.querySelector('.elaborate-section').appendChild(errEl);
+ return;
+ }
- btnElaborate.disabled = true;
- btnElaborate.textContent = 'Drafting…';
+ btnElaborate.disabled = true;
+ btnElaborate.textContent = 'Drafting…';
- // Remove any previous errors or banners
- const form = document.getElementById('task-form');
- form.querySelectorAll('.form-error, .elaborate-banner').forEach(el => el.remove());
+ // Remove any previous errors or banners
+ const form = document.getElementById('task-form');
+ form.querySelectorAll('.form-error, .elaborate-banner').forEach(el => el.remove());
- try {
- const sel = document.getElementById('project-select');
- const workingDir = sel.value === '__new__'
- ? document.getElementById('new-project-input').value.trim()
- : sel.value;
- const result = await elaborateTask(prompt, workingDir);
-
- // Populate form fields
- const f = document.getElementById('task-form');
- if (result.name)
- f.querySelector('[name="name"]').value = result.name;
- if (result.agent && result.agent.instructions)
- f.querySelector('[name="instructions"]').value = result.agent.instructions;
- if (result.agent && (result.agent.project_dir || result.agent.working_dir)) {
- const pDir = result.agent.project_dir || result.agent.working_dir;
- const pSel = document.getElementById('project-select');
- const exists = [...pSel.options].some(o => o.value === pDir);
- if (exists) {
- pSel.value = pDir;
- } else {
- pSel.value = '__new__';
- document.getElementById('new-project-row').hidden = false;
- document.getElementById('new-project-input').value = pDir;
+ try {
+ const repoUrl = document.getElementById('repository-url').value.trim();
+ const result = await elaborateTask(prompt, repoUrl);
+
+ // Populate form fields
+ const f = document.getElementById('task-form');
+ if (result.name)
+ f.querySelector('[name="name"]').value = result.name;
+ if (result.agent && result.agent.instructions)
+ f.querySelector('[name="instructions"]').value = result.agent.instructions;
+ if (result.repository_url || result.agent?.repository_url) {
+ document.getElementById('repository-url').value = result.repository_url || result.agent.repository_url;
}
- }
- if (result.agent && result.agent.max_budget_usd != null)
- f.querySelector('[name="max_budget_usd"]').value = result.agent.max_budget_usd;
- if (result.timeout)
- f.querySelector('[name="timeout"]').value = result.timeout;
- if (result.priority) {
- const sel = f.querySelector('[name="priority"]');
- if ([...sel.options].some(o => o.value === result.priority)) {
- sel.value = result.priority;
+ if (result.agent && result.agent.container_image) {
+ document.getElementById('container-image').value = result.agent.container_image;
+ }
+ if (result.agent && result.agent.max_budget_usd != null)
+ f.querySelector('[name="max_budget_usd"]').value = result.agent.max_budget_usd;
+ if (result.timeout)
+ f.querySelector('[name="timeout"]').value = result.timeout;
+ if (result.priority) {
+ const sel = f.querySelector('[name="priority"]');
+ if ([...sel.options].some(o => o.value === result.priority)) {
+ sel.value = result.priority;
+ }
}
- }
-
- // Show success banner
- const banner = document.createElement('p');
- banner.className = 'elaborate-banner';
- banner.textContent = 'AI draft ready — review and submit.';
- document.getElementById('task-form').querySelector('.elaborate-section').appendChild(banner);
- // Auto-validate after elaboration
- try {
- const result = await validateTask(buildValidatePayload());
- renderValidationResult(result);
- } catch (_) {
- // silent - elaboration already succeeded, validation is bonus
+ // Show success banner
+ const banner = document.createElement('p');
+ banner.className = 'elaborate-banner';
+ banner.textContent = 'AI draft ready — review and submit.';
+ document.getElementById('task-form').querySelector('.elaborate-section').appendChild(banner);
+
+ // Auto-validate after elaboration
+ try {
+ const result = await validateTask(buildValidatePayload());
+ renderValidationResult(result);
+ } catch (_) {
+ // silent - elaboration already succeeded, validation is bonus
+ }
+ } catch (err) {
+ const errEl = document.createElement('p');
+ errEl.className = 'form-error';
+ errEl.textContent = `Elaboration failed: ${err.message}`;
+ document.getElementById('task-form').querySelector('.elaborate-section').appendChild(errEl);
+ } finally {
+ btnElaborate.disabled = false;
+ btnElaborate.textContent = 'Draft with AI ✦';
}
- } catch (err) {
- const errEl = document.createElement('p');
- errEl.className = 'form-error';
- errEl.textContent = `Elaboration failed: ${err.message}`;
- document.getElementById('task-form').querySelector('.elaborate-section').appendChild(errEl);
- } finally {
- btnElaborate.disabled = false;
- btnElaborate.textContent = 'Draft with AI ✦';
- }
- });
+ });
- document.getElementById('task-form').addEventListener('submit', async e => {
- e.preventDefault();
+ document.getElementById('task-form').addEventListener('submit', async e => {
+ e.preventDefault();
- // Remove any previous error
- const prev = e.target.querySelector('.form-error');
- if (prev) prev.remove();
+ // Remove any previous error
+ const prev = e.target.querySelector('.form-error');
+ if (prev) prev.remove();
- const btn = e.submitter;
- btn.disabled = true;
- btn.textContent = 'Creating…';
+ const btn = e.submitter;
+ btn.disabled = true;
+ btn.textContent = 'Creating…';
- try {
- const validateResult = document.getElementById('validate-result');
- if (!validateResult.hasAttribute('hidden') && validateResult.dataset.clarity && validateResult.dataset.clarity !== 'clear') {
- if (!window.confirm('The validator flagged issues. Create task anyway?')) {
- return;
+ try {
+ const validateResult = document.getElementById('validate-result');
+ if (!validateResult.hasAttribute('hidden') && validateResult.dataset.clarity && validateResult.dataset.clarity !== 'clear') {
+ if (!window.confirm('The validator flagged issues. Create task anyway?')) {
+ return;
+ }
}
+ await createTask(new FormData(e.target));
+ } catch (err) {
+ const errEl = document.createElement('p');
+ errEl.className = 'form-error';
+ errEl.textContent = err.message;
+ e.target.appendChild(errEl);
+ } finally {
+ btn.disabled = false;
+ btn.textContent = 'Create & Queue';
}
- await createTask(new FormData(e.target));
- } catch (err) {
- const errEl = document.createElement('p');
- errEl.className = 'form-error';
- errEl.textContent = err.message;
- e.target.appendChild(errEl);
- } finally {
- btn.disabled = false;
- btn.textContent = 'Create & Queue';
- }
+ });
});
-});
+}