diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-03-14 00:38:07 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-03-14 00:38:07 +0000 |
| commit | 98ccde12b08ad0b7f53e42de959a72d8382179e3 (patch) | |
| tree | 46af05f401aa41b182d3bc948faa9a1d7466a3df /web | |
| parent | 6ac15be438e3692cbc2ae2f36ab2d69468fc6372 (diff) | |
feat: show subtask rollup on BLOCKED tasks waiting for subtasks
When a task is BLOCKED due to spawned subtasks (no question), the card
footer now fetches and renders a list of subtask names with their state
emoji instead of showing the question/answer input UI. The Cancel button
remains in both cases.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'web')
| -rw-r--r-- | web/app.js | 39 | ||||
| -rw-r--r-- | web/style.css | 44 |
2 files changed, 82 insertions, 1 deletions
@@ -163,7 +163,11 @@ function createTaskCard(task) { footer.appendChild(acceptBtn); footer.appendChild(rejectBtn); } else if (task.state === 'BLOCKED') { - renderQuestionFooter(task, footer); + if (task.question) { + renderQuestionFooter(task, footer); + } else { + renderSubtaskRollup(task.id, footer); + } const cancelBtn = document.createElement('button'); cancelBtn.className = 'btn-cancel'; cancelBtn.textContent = 'Cancel'; @@ -728,6 +732,39 @@ function renderQuestionFooter(task, footer) { } } +const STATE_EMOJI = { + PENDING: '⏳', QUEUED: '🕐', RUNNING: '⚡', COMPLETED: '✅', + FAILED: '❌', CANCELLED: '🚫', TIMED_OUT: '⏱', BUDGET_EXCEEDED: '💸', + READY: '👀', BLOCKED: '⏸', +}; + +async function renderSubtaskRollup(taskId, footer) { + footer.addEventListener('click', (e) => e.stopPropagation()); + const container = document.createElement('div'); + container.className = 'subtask-rollup'; + footer.prepend(container); + + try { + const res = await fetch(`${API_BASE}/api/tasks/${taskId}/subtasks`); + const subtasks = await res.json(); + if (!subtasks || subtasks.length === 0) { + container.textContent = 'Waiting for subtasks…'; + return; + } + const ul = document.createElement('ul'); + ul.className = 'subtask-list'; + for (const st of subtasks) { + const li = document.createElement('li'); + li.className = `subtask-item subtask-${st.state.toLowerCase()}`; + li.textContent = `${STATE_EMOJI[st.state] || '•'} ${st.name}`; + ul.appendChild(li); + } + container.appendChild(ul); + } catch { + container.textContent = 'Could not load subtasks.'; + } +} + async function handleAnswer(taskId, answer, footer) { const btns = footer.querySelectorAll('button, input'); btns.forEach(el => { el.disabled = true; }); diff --git a/web/style.css b/web/style.css index 09e7925..ee1b69c 100644 --- a/web/style.css +++ b/web/style.css @@ -56,6 +56,27 @@ header h1 { flex: 1; } +.header-actions { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.agent-selector { + background: var(--bg); + border: 1px solid var(--border); + border-radius: 0.375rem; + color: var(--text); + padding: 0.4em 0.6em; + font-size: 0.85rem; + cursor: pointer; +} + +.agent-selector:focus { + outline: 2px solid var(--accent); + outline-offset: 1px; +} + /* Tab bar */ .tab-bar { background: var(--surface); @@ -344,6 +365,29 @@ main { cursor: not-allowed; } +.subtask-rollup { + width: 100%; + margin-bottom: 0.4rem; +} +.subtask-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 0.2rem; +} +.subtask-item { + font-size: 0.78rem; + color: var(--text-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.subtask-item.subtask-completed { color: var(--success, #4caf50); } +.subtask-item.subtask-failed { color: var(--danger, #e53935); } +.subtask-item.subtask-running { color: var(--accent); } + .task-question-text { font-size: 0.82rem; color: var(--text); |
