agent-ui: running bash-tasks pill + flyout on the agent page
Adds a 'tasks' header pill (hidden at zero, like inbox/loose-ends) that opens a side-panel flyout listing the agent's in-flight bash tasks from GET /api/bash-tasks. Each row shows status (running/queued), task id, elapsed time, and a truncated single-line command preview. Polled on the same /api/state cycle as loose-ends (tasks complete async between turns, so the count stays live); clicking the pill opens the flyout. Snapshot-only v1 — SSE live-push is a possible follow-up.
This commit is contained in:
parent
ceb852e5b4
commit
4ce919a19f
3 changed files with 94 additions and 0 deletions
|
|
@ -334,6 +334,10 @@ h2, h3 {
|
||||||
background: color-mix(in srgb, var(--red) 18%, transparent);
|
background: color-mix(in srgb, var(--red) 18%, transparent);
|
||||||
color: var(--red);
|
color: var(--red);
|
||||||
}
|
}
|
||||||
|
.header-pill-tasks .header-pill-count {
|
||||||
|
background: color-mix(in srgb, var(--green) 18%, transparent);
|
||||||
|
color: var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
.agent-main {
|
.agent-main {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
@ -528,6 +532,14 @@ pre.diff {
|
||||||
padding-left: 0.8em;
|
padding-left: 0.8em;
|
||||||
border-left: 2px solid var(--purple-dim);
|
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 {
|
.agent-inbox li.inbox-reply {
|
||||||
padding-left: 1em;
|
padding-left: 1em;
|
||||||
border-left: 2px solid var(--border);
|
border-left: 2px solid var(--border);
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,13 @@ window.marked = marked;
|
||||||
buildLooseEndsList(lastLooseEnds));
|
buildLooseEndsList(lastLooseEnds));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const bashPill = $('bash-tasks-pill');
|
||||||
|
if (bashPill) {
|
||||||
|
bashPill.addEventListener('click', () => {
|
||||||
|
Panel.open('bash-tasks', 'tasks · ' + lastBashTasks.length,
|
||||||
|
buildBashTasksList(lastBashTasks));
|
||||||
|
});
|
||||||
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// ─── state rendering ────────────────────────────────────────────────────
|
// ─── state rendering ────────────────────────────────────────────────────
|
||||||
|
|
@ -909,6 +916,71 @@ window.marked = marked;
|
||||||
reconcileAskBinds();
|
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`
|
/** Walk `pendingAskBinds` against the latest `lastLooseEnds`
|
||||||
* snapshot, pair unbound slots with the first unclaimed pending
|
* snapshot, pair unbound slots with the first unclaimed pending
|
||||||
* operator-bound question whose text matches, and flip already-
|
* 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
|
// db, fetched via the per-agent socket). Cold-load fetches
|
||||||
// it here; turn_end refreshes it via the renderer below.
|
// it here; turn_end refreshes it via the renderer below.
|
||||||
refreshLooseEnds();
|
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
|
// Skip the re-render if nothing structurally changed. The most
|
||||||
// common case is `online` polling itself — without this guard, the
|
// common case is `online` polling itself — without this guard, the
|
||||||
// operator's <input value> gets clobbered every cycle.
|
// operator's <input value> gets clobbered every cycle.
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,12 @@
|
||||||
<span class="header-pill-label">loose ends</span>
|
<span class="header-pill-label">loose ends</span>
|
||||||
<span class="header-pill-count" id="loose-ends-count">0</span>
|
<span class="header-pill-count" id="loose-ends-count">0</span>
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" id="bash-tasks-pill" class="header-pill header-pill-tasks" hidden
|
||||||
|
title="open running bash-tasks flyout">
|
||||||
|
<span class="header-pill-icon" aria-hidden="true">⚙</span>
|
||||||
|
<span class="header-pill-label">tasks</span>
|
||||||
|
<span class="header-pill-count" id="bash-tasks-count">0</span>
|
||||||
|
</button>
|
||||||
<button type="button" id="overflow-btn" class="overflow-btn"
|
<button type="button" id="overflow-btn" class="overflow-btn"
|
||||||
aria-haspopup="menu" aria-expanded="false"
|
aria-haspopup="menu" aria-expanded="false"
|
||||||
title="more actions">⋯</button>
|
title="more actions">⋯</button>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue