diff options
Diffstat (limited to 'web/app.js')
| -rw-r--r-- | web/app.js | 92 |
1 files changed, 51 insertions, 41 deletions
@@ -294,13 +294,6 @@ export function createTaskCard(task, doc = document) { badge.textContent = task.state.replace(/_/g, ' '); header.append(name, badge); - if (task.state === 'RUNNING') { - const elapsed = doc.createElement('span'); - elapsed.className = 'running-elapsed'; - elapsed.dataset.startedAt = task.updated_at ?? ''; - elapsed.textContent = formatElapsed(task.updated_at); - header.append(elapsed); - } card.appendChild(header); // Meta: priority + created_at @@ -324,6 +317,17 @@ export function createTaskCard(task, doc = document) { } if (meta.children.length) card.appendChild(meta); + // Elapsed timer for RUNNING tasks (no separate ticking interval — updated + // on each poll tick alongside the rest of the board, same convention the + // old Running-tab card used). + if (task.state === 'RUNNING') { + const elapsed = doc.createElement('span'); + elapsed.className = 'task-elapsed running-elapsed'; + elapsed.dataset.startedAt = task.updated_at ?? ''; + elapsed.textContent = formatElapsed(task.updated_at); + card.appendChild(elapsed); + } + // Description (truncated via CSS) if (task.description) { const desc = doc.createElement('div'); @@ -370,14 +374,21 @@ export function createTaskCard(task, doc = document) { if (depBadge) card.appendChild(depBadge); } - // Log-tail placeholder for PENDING/QUEUED tasks (no execution exists yet). + // Inline log tail (no click required) — see docs/superpowers/specs/2026-07-06-tasks-board-design.md + // section 4. PENDING/QUEUED tasks have no execution row yet, so there is + // nothing to tail; every other state gets a .task-log-tail element that + // Task 6's ensureTaskLogStream() attaches an SSE stream to. if (task.state === 'PENDING' || task.state === 'QUEUED') { + const placeholder = doc.createElement('div'); + placeholder.className = 'task-log-tail-placeholder'; + placeholder.textContent = 'Waiting to start…'; + card.appendChild(placeholder); + } else { const logTail = doc.createElement('div'); - logTail.className = 'task-log-tail'; - const placeholder = doc.createElement('p'); - placeholder.className = 'task-log-placeholder'; - placeholder.textContent = 'Waiting to start\u2026'; - logTail.appendChild(placeholder); + logTail.className = task.state === 'RUNNING' || task.state === 'BLOCKED' + ? 'task-log-tail running-log' + : 'task-log-tail'; + logTail.dataset.logTarget = task.id; card.appendChild(logTail); } @@ -409,7 +420,7 @@ export function createTaskCard(task, doc = document) { }); footer.appendChild(btn); } else if (task.state === 'READY') { - renderSubtaskRollup(task, footer); + renderSubtaskRollup(task, footer, doc); const acceptBtn = doc.createElement('button'); acceptBtn.className = 'btn-accept'; acceptBtn.textContent = 'Accept'; @@ -428,9 +439,9 @@ export function createTaskCard(task, doc = document) { footer.appendChild(rejectBtn); } else if (task.state === 'BLOCKED') { if (task.question) { - renderQuestionFooter(task, footer); + renderQuestionFooter(task, footer, doc); } else { - renderSubtaskRollup(task, footer); + renderSubtaskRollup(task, footer, doc); } const cancelBtn = doc.createElement('button'); cancelBtn.className = 'btn-cancel'; @@ -478,7 +489,7 @@ export function createTaskCard(task, doc = document) { if (EDITABLE_STATES.has(task.state)) { card.classList.add('task-card--editable'); - const editForm = createEditForm(task); + const editForm = createEditForm(task, doc); editForm.hidden = true; card.appendChild(editForm); card.addEventListener('click', () => { editForm.hidden = !editForm.hidden; }); @@ -950,21 +961,20 @@ async function updateTask(taskId, body) { return res.json(); } -function createEditForm(task) { +function createEditForm(task, doc = document) { const a = task.agent || {}; - const form = document.createElement('div'); + const form = doc.createElement('div'); form.className = 'task-inline-edit'; // Prevent card-level click from toggling this form while user interacts inside it. form.addEventListener('click', (e) => e.stopPropagation()); function makeField(labelText, tag, attrs) { - const label = document.createElement('label'); + const label = doc.createElement('label'); label.textContent = labelText; - const el = document.createElement(tag); + const el = doc.createElement(tag); for (const [k, v] of Object.entries(attrs)) { - if (k === 'value') el.value = v; - else el.setAttribute(k, v); + el[k] = v; } label.appendChild(el); return label; @@ -973,17 +983,17 @@ function createEditForm(task) { form.appendChild(makeField('Name', 'input', { type: 'text', name: 'name', value: task.name || '' })); form.appendChild(makeField('Description', 'textarea', { name: 'description', rows: '2', value: task.description || '' })); form.appendChild(makeField('Instructions', 'textarea', { name: 'instructions', rows: '4', value: a.instructions || '' })); - + form.appendChild(makeField('Project Directory', 'input', { type: 'text', name: 'project_dir', value: a.project_dir || a.working_dir || '', placeholder: '/path/to/repo' })); form.appendChild(makeField('Max Budget (USD)', 'input', { type: 'number', name: 'max_budget_usd', step: '0.01', value: a.max_budget_usd != null ? String(a.max_budget_usd) : '1.00' })); form.appendChild(makeField('Timeout', 'input', { type: 'text', name: 'timeout', value: formatDurationForInput(task.timeout) || '15m', placeholder: '15m' })); - const prioLabel = document.createElement('label'); + const prioLabel = doc.createElement('label'); prioLabel.textContent = 'Priority'; - const prioSel = document.createElement('select'); + const prioSel = doc.createElement('select'); prioSel.name = 'priority'; for (const val of ['high', 'normal', 'low']) { - const opt = document.createElement('option'); + const opt = doc.createElement('option'); opt.value = val; opt.textContent = val.charAt(0).toUpperCase() + val.slice(1); if (val === (task.priority || 'normal')) opt.selected = true; @@ -992,20 +1002,20 @@ function createEditForm(task) { prioLabel.appendChild(prioSel); form.appendChild(prioLabel); - const errEl = document.createElement('div'); + const errEl = doc.createElement('div'); errEl.className = 'inline-edit-error'; errEl.hidden = true; form.appendChild(errEl); - const actions = document.createElement('div'); + const actions = doc.createElement('div'); actions.className = 'inline-edit-actions'; - const cancelBtn = document.createElement('button'); + const cancelBtn = doc.createElement('button'); cancelBtn.type = 'button'; cancelBtn.textContent = 'Cancel'; cancelBtn.addEventListener('click', () => { form.hidden = true; }); - const saveBtn = document.createElement('button'); + const saveBtn = doc.createElement('button'); saveBtn.type = 'button'; saveBtn.className = 'btn-primary btn-sm'; saveBtn.textContent = 'Save'; @@ -1059,7 +1069,7 @@ async function handleEditSave(taskId, form, saveBtn) { } } -function renderQuestionFooter(task, footer) { +function renderQuestionFooter(task, footer, doc = document) { // Prevent any tap inside the question footer from opening the detail panel. footer.addEventListener('click', (e) => e.stopPropagation()); @@ -1068,14 +1078,14 @@ function renderQuestionFooter(task, footer) { try { question = JSON.parse(task.question); } catch {} } - const questionEl = document.createElement('p'); + const questionEl = doc.createElement('p'); questionEl.className = 'task-question-text'; questionEl.textContent = question.text; footer.appendChild(questionEl); if (question.options && question.options.length > 0) { question.options.forEach(opt => { - const btn = document.createElement('button'); + const btn = doc.createElement('button'); btn.className = 'btn-answer'; btn.textContent = opt; btn.addEventListener('click', (e) => { @@ -1085,13 +1095,13 @@ function renderQuestionFooter(task, footer) { footer.appendChild(btn); }); } else { - const row = document.createElement('div'); + const row = doc.createElement('div'); row.className = 'task-answer-row'; - const input = document.createElement('input'); + const input = doc.createElement('input'); input.type = 'text'; input.className = 'task-answer-input'; input.placeholder = 'Your answer…'; - const btn = document.createElement('button'); + const btn = doc.createElement('button'); btn.className = 'btn-answer'; btn.textContent = 'Submit'; btn.addEventListener('click', (e) => { @@ -1115,9 +1125,9 @@ const STATE_EMOJI = { READY: '👀', BLOCKED: '⏸', }; -async function renderSubtaskRollup(task, footer) { +async function renderSubtaskRollup(task, footer, doc = document) { footer.addEventListener('click', (e) => e.stopPropagation()); - const container = document.createElement('div'); + const container = doc.createElement('div'); container.className = 'subtask-rollup'; footer.prepend(container); @@ -1129,10 +1139,10 @@ async function renderSubtaskRollup(task, footer) { container.textContent = blurb ? truncateToWordBoundary(blurb) : 'Waiting for subtasks…'; return; } - const ul = document.createElement('ul'); + const ul = doc.createElement('ul'); ul.className = 'subtask-list'; for (const st of subtasks) { - const li = document.createElement('li'); + const li = doc.createElement('li'); li.className = `subtask-item subtask-${st.state.toLowerCase()}`; li.textContent = `${STATE_EMOJI[st.state] || '•'} ${st.name}`; ul.appendChild(li); |
