summaryrefslogtreecommitdiff
path: root/web/app.js
diff options
context:
space:
mode:
Diffstat (limited to 'web/app.js')
-rw-r--r--web/app.js346
1 files changed, 152 insertions, 194 deletions
diff --git a/web/app.js b/web/app.js
index 9a5a460..6ddb23c 100644
--- a/web/app.js
+++ b/web/app.js
@@ -1478,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) {
@@ -1541,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() {
@@ -1597,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'),
@@ -2646,6 +2604,14 @@ async function enableNotifications(btn) {
await registerServiceWorker();
const registration = await navigator.serviceWorker.ready;
+ // Unsubscribe any stale subscription (e.g. from a VAPID key rotation).
+ // PushManager.subscribe() throws "applicationServerKey is not valid" if the
+ // existing subscription was created with a different key.
+ const existingSub = await registration.pushManager.getSubscription();
+ if (existingSub) {
+ await existingSub.unsubscribe();
+ }
+
// Subscribe via PushManager.
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
@@ -2751,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(getActiveMainTab());
- 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';
+ }
+ });
- btnElaborate.disabled = true;
- btnElaborate.textContent = 'Drafting…';
+ // 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;
+ }
- // Remove any previous errors or banners
- const form = document.getElementById('task-form');
- form.querySelectorAll('.form-error, .elaborate-banner').forEach(el => el.remove());
+ btnElaborate.disabled = true;
+ btnElaborate.textContent = 'Drafting…';
- 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;
+ // Remove any previous errors or banners
+ const form = document.getElementById('task-form');
+ form.querySelectorAll('.form-error, .elaborate-banner').forEach(el => el.remove());
+
+ 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';
- }
+ });
});
-});
+}