diff --git a/frontend/packages/agent/src/agent.css b/frontend/packages/agent/src/agent.css index b491c8e5..8f980e57 100644 --- a/frontend/packages/agent/src/agent.css +++ b/frontend/packages/agent/src/agent.css @@ -334,6 +334,10 @@ h2, h3 { background: color-mix(in srgb, var(--red) 18%, transparent); color: var(--red); } +.header-pill-tasks .header-pill-count { + background: color-mix(in srgb, var(--green) 18%, transparent); + color: var(--green); +} .agent-main { position: absolute; @@ -528,6 +532,14 @@ pre.diff { padding-left: 0.8em; border-left: 2px solid var(--purple-dim); } +/* Running bash-tasks flyout rows (buildBashTasksList). */ +.agent-inbox .bash-task-status { font-weight: bold; font-size: 0.9em; } +.agent-inbox .bash-task-running { color: var(--green); } +.agent-inbox .bash-task-pending { color: var(--blue); } +.agent-inbox .bash-task-cmd { + font-family: monospace; + font-size: 0.92em; +} .agent-inbox li.inbox-reply { padding-left: 1em; border-left: 2px solid var(--border); diff --git a/frontend/packages/agent/src/app.js b/frontend/packages/agent/src/app.js index 3798351a..cbc5ab4c 100644 --- a/frontend/packages/agent/src/app.js +++ b/frontend/packages/agent/src/app.js @@ -135,6 +135,13 @@ window.marked = marked; buildLooseEndsList(lastLooseEnds)); }); } + const bashPill = $('bash-tasks-pill'); + if (bashPill) { + bashPill.addEventListener('click', () => { + Panel.open('bash-tasks', 'tasks · ' + lastBashTasks.length, + buildBashTasksList(lastBashTasks)); + }); + } })(); // ─── state rendering ──────────────────────────────────────────────────── @@ -909,6 +916,71 @@ window.marked = marked; reconcileAskBinds(); } + // Running bash tasks: `GET /api/bash-tasks` returns the in-flight + // (Pending/Running) TaskFiles from the in-container bash-tasks dir. Same + // best-effort, silent-failure contract as loose-ends — a fetch miss keeps + // the pill at zero rather than surfacing stale chrome. + let lastBashTasks = []; + async function refreshBashTasks() { + try { + const resp = await fetch('api/bash-tasks'); + if (!resp.ok) { + renderBashTasks([]); + return; + } + const data = await resp.json(); + renderBashTasks(data.tasks || []); + } catch (err) { + console.warn('bash-tasks fetch failed', err); + renderBashTasks([]); + } + } + + function buildBashTasksList(tasks) { + const wrap = el('div', { class: 'agent-inbox' }); + if (!tasks.length) { + wrap.append(el('p', { class: 'side-panel-empty' }, + 'no running bash tasks.')); + return wrap; + } + const list = el('ul'); + const fmtAge = (s) => { + if (s < 60) return s + 's'; + if (s < 3600) return Math.floor(s / 60) + 'm'; + if (s < 86400) return Math.floor(s / 3600) + 'h'; + return Math.floor(s / 86400) + 'd'; + }; + const now = Math.floor(Date.now() / 1000); + for (const t of tasks) { + const li = el('li'); + const running = t.status === 'running'; + // Elapsed since the task started (running) or was queued (pending). + const since = running ? (t.started_at || t.created_at || now) : (t.created_at || now); + const elapsed = Math.max(0, now - since); + // Single-line, truncated command preview (the cmd can be multi-line). + const cmdPreview = (t.cmd || '').replace(/\s+/g, ' ').trim().slice(0, 100); + li.append( + el('span', { class: 'bash-task-status bash-task-' + t.status }, running ? '▶ running' : '◷ queued'), ' ', + el('span', { class: 'inbox-from' }, t.id), ' ', + el('span', { class: 'inbox-ts' }, (running ? '' : 'queued ') + fmtAge(elapsed) + (running ? ' elapsed' : '')), + el('div', { class: 'inbox-body bash-task-cmd' }, cmdPreview), + ); + list.append(li); + } + wrap.append(list); + return wrap; + } + + function renderBashTasks(tasks) { + lastBashTasks = tasks; + const pill = $('bash-tasks-pill'); + const count = $('bash-tasks-count'); + if (count) count.textContent = tasks.length; + if (pill) pill.hidden = tasks.length === 0; + Panel.refresh('bash-tasks', 'tasks · ' + tasks.length, + buildBashTasksList(tasks)); + } + /** Walk `pendingAskBinds` against the latest `lastLooseEnds` * snapshot, pair unbound slots with the first unclaimed pending * operator-bound question whose text matches, and flip already- @@ -1282,6 +1354,10 @@ window.marked = marked; // db, fetched via the per-agent socket). Cold-load fetches // it here; turn_end refreshes it via the renderer below. refreshLooseEnds(); + // Running bash tasks live in the in-container bash-tasks dir (not + // /api/state); same poll cadence as loose-ends. They complete async + // between turns, so polling on each state cycle keeps the count live. + refreshBashTasks(); // Skip the re-render if nothing structurally changed. The most // common case is `online` polling itself — without this guard, the // operator's gets clobbered every cycle. diff --git a/frontend/packages/agent/src/index.html b/frontend/packages/agent/src/index.html index aad9bf90..3e0ce46a 100644 --- a/frontend/packages/agent/src/index.html +++ b/frontend/packages/agent/src/index.html @@ -64,6 +64,12 @@ loose ends 0 +