summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-03-14 00:38:07 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-03-14 00:38:07 +0000
commit98ccde12b08ad0b7f53e42de959a72d8382179e3 (patch)
tree46af05f401aa41b182d3bc948faa9a1d7466a3df
parent6ac15be438e3692cbc2ae2f36ab2d69468fc6372 (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>
-rw-r--r--web/app.js39
-rw-r--r--web/style.css44
2 files changed, 82 insertions, 1 deletions
diff --git a/web/app.js b/web/app.js
index ad60f34..6bcdf57 100644
--- a/web/app.js
+++ b/web/app.js
@@ -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);