Skip to content

Commit f97b424

Browse files
ndemiancclaude
andcommitted
Atom++ AI: Background tasks tray (collapsible, live, cross-turn)
A "Background tasks" bar in the chat that appears only when commands are running via run_command background:true. Collapsed by default (a pulsing "· N running" pill); expand to see one row per background process with its live status ([running on :PORT] once sniffed, else ready/running…) and a per-row Stop. Persists across agent turns (unlike the inline terminal card), self-clears when a task exits, hides when nothing runs, and is reaped on New Chat. - chat.html: #bgTasksBar in the sticky region (next to plan/review bars), renderBgTasks/applyBgTask, a 'bgTask' switch case, cleared on reset. - agent.js: posts 'bgTask' events on background start, port/readiness detection, and exit. Reuses the bgRuns registry + the existing stopCommand → commandStops process-group kill (tray Stop == card Stop). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 3fc2eb9 commit f97b424

2 files changed

Lines changed: 66 additions & 5 deletions

File tree

‎extensions/atom-ai/agent.js‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -293,20 +293,21 @@ async function runTool(tu, ctx) {
293293
if (entry) {
294294
entry.ring = (entry.ring + chunk).slice(-100000); // bounded tail for read_command_output
295295
entry.totalBytes += chunk.length;
296-
if (!entry.port) { const p = sniffPort(chunk); if (p) { entry.port = p; } }
297-
if (!entry.ready && looksReady(chunk)) { entry.ready = true; }
296+
if (!entry.port) { const p = sniffPort(chunk); if (p) { entry.port = p; ctx.post({ type: 'bgTask', id: runId, port: p }); } }
297+
if (!entry.ready && looksReady(chunk)) { entry.ready = true; ctx.post({ type: 'bgTask', id: runId, ready: true }); }
298298
}
299299
};
300300
const onExit = (code, ms, how) => {
301301
if (stops) { stops.delete(runId); }
302-
if (entry) { entry.status = how === 'exit' ? 'exited' : how; entry.code = code; entry.how = how; } // exited | stopped | timeout
302+
if (entry) { entry.status = how === 'exit' ? 'exited' : how; entry.code = code; entry.how = how; ctx.post({ type: 'bgTask', id: runId, status: entry.status, code: code, done: true }); } // exited | stopped | timeout — drop from tray
303303
ctx.post({ type: 'termExit', id: runId, code: code, ms: ms, how: how });
304304
};
305305
const onStart = (child, stop) => { if (stops) { stops.set(runId, stop); } };
306306
if (bg) {
307307
// Fire-and-forget: keep streaming + tracking, but return NOW so the agent loop isn't blocked.
308308
// No timeout — a background server is meant to run long (Stop / New Chat reap it).
309-
(async () => { try { await runCommand(root, cmd, onChunk, onExit, onStart, 0); } catch (e) { entry.status = 'error'; entry.how = 'error'; dbg('bg.error', { id: runId, msg: String((e && e.message) || e) }); } })();
309+
ctx.post({ type: 'bgTask', id: runId, command: cmd, status: 'running' }); // add to the Background tasks tray
310+
(async () => { try { await runCommand(root, cmd, onChunk, onExit, onStart, 0); } catch (e) { entry.status = 'error'; entry.how = 'error'; ctx.post({ type: 'bgTask', id: runId, status: 'error', done: true }); dbg('bg.error', { id: runId, msg: String((e && e.message) || e) }); } })();
310311
return 'Started in the background as id "' + runId + '" — it keeps running while you continue. Use read_command_output with this id to watch its output; wait for a readiness/port line before testing against it. Do not start it again.';
311312
}
312313
return await runCommand(root, cmd, onChunk, onExit, onStart, ctx.commandTimeout);

‎extensions/atom-ai/media/chat.html‎

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,29 @@
293293
#ctxBar .ctxnew { margin-left: 5px; cursor: pointer; border: none; border-radius: 5px; padding: 3px 10px; font-size: 11px; background: var(--vscode-button-background); color: var(--vscode-button-foreground); }
294294
#ctxBar .ctxnew:hover { background: var(--vscode-button-hoverBackground); }
295295
/* sticky active-plan bar (sits just above the review bar) — opaque so the log can't show behind it */
296+
/* Background tasks tray — collapsed by default; lists commands started with background:true */
297+
#bgTasksBar { flex: 0 0 auto; margin: 0 8px 6px; border: 1px solid var(--border); border-radius: 8px; overflow: hidden;
298+
background: var(--vscode-editorWidget-background, rgba(127,127,127,.08)); font-size: 12px; }
299+
.bgt-head { display: flex; align-items: center; gap: 7px; padding: 8px 11px; cursor: pointer; user-select: none; color: var(--vscode-foreground); }
300+
.bgt-head:hover { background: var(--vscode-toolbar-hoverBackground, rgba(127,127,127,.12)); }
301+
.bgt-livedot { width: 7px; height: 7px; border-radius: 50%; flex: 0 0 auto; background: var(--vscode-terminal-ansiBrightGreen, #7ee787); animation: bgtpulse 1.8s infinite; }
302+
@keyframes bgtpulse { 0%{ box-shadow: 0 0 0 0 rgba(126,231,135,.5); } 70%{ box-shadow: 0 0 0 5px rgba(126,231,135,0); } 100%{ box-shadow: 0 0 0 0 rgba(126,231,135,0); } }
303+
.bgt-caret { display: inline-flex; transition: transform .15s; color: var(--muted); }
304+
.bgt-caret.open { transform: rotate(90deg); }
305+
.bgt-caret svg { width: 11px; height: 11px; display: block; }
306+
.bgt-title { font-weight: 600; }
307+
.bgt-count { color: var(--muted); }
308+
.bgt-list { border-top: 1px solid var(--border); }
309+
.bgt-row { display: flex; align-items: center; gap: 9px; padding: 7px 11px; border-bottom: 1px solid color-mix(in srgb, var(--border) 50%, transparent); }
310+
.bgt-row:last-child { border-bottom: none; }
311+
.bgt-cmd { flex: 1 1 auto; min-width: 0; font-family: var(--vscode-editor-font-family, monospace); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
312+
.bgt-status { flex: 0 0 auto; color: var(--muted); font-variant-numeric: tabular-nums; }
313+
.bgt-status .port { color: var(--vscode-terminal-ansiBrightGreen, var(--vscode-terminal-ansiGreen, #7ee787)); }
314+
.bgt-stop { flex: 0 0 auto; cursor: pointer; border: 1px solid var(--border); border-radius: 5px; padding: 2px 9px; font-size: 11px; display: inline-flex; align-items: center; gap: 4px;
315+
background: var(--vscode-button-secondaryBackground, rgba(127,127,127,.22)); color: var(--vscode-foreground); }
316+
.bgt-stop:hover { background: var(--vscode-errorForeground, #f14c4c); color: #fff; border-color: transparent; }
317+
.bgt-stop[disabled] { opacity: .6; cursor: default; }
318+
.bgt-stop svg { width: 1em; height: 1em; }
296319
#planBar { flex: 0 0 auto; padding: 8px 8px 6px; max-height: 38vh; overflow-y: auto; scrollbar-gutter: stable;
297320
background: var(--vscode-sideBar-background, var(--vscode-editor-background, #1e1e1e));
298321
border-top: 1px solid var(--border); box-shadow: 0 -8px 14px -8px rgba(0,0,0,.4); }
@@ -509,6 +532,7 @@
509532
<button id="jumpLatest" title="Jump to latest" aria-label="Jump to latest">&#8595;</button>
510533

511534
<!-- context-window usage meter (appears as the conversation fills the model's window) -->
535+
<div id="bgTasksBar" style="display:none"></div>
512536
<div id="ctxBar" style="display:none"></div>
513537

514538
<!-- active agent plan — sticky, just above the Keep all / Undo all bar -->
@@ -1266,6 +1290,41 @@
12661290
// 3) live-update the breakdown popover if it's open
12671291
renderCtxCard();
12681292
}
1293+
// Background tasks tray: aggregates commands started with run_command background:true. Collapsed by
1294+
// default; lives across turns; each row has Stop. Fed by 'bgTask' events from the host.
1295+
const bgTasksBar = document.getElementById('bgTasksBar');
1296+
const bgTasks = {}; // id -> { id, command, status, port, ready }
1297+
let bgTasksCollapsed = true;
1298+
const CARET = '<svg viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M6 4l5 4-5 4z"/></svg>';
1299+
function renderBgTasks(){
1300+
const running = Object.keys(bgTasks).map(k => bgTasks[k]).filter(t => t.status === 'running');
1301+
if (!running.length){ bgTasksBar.style.display = 'none'; bgTasksBar.innerHTML = ''; return; }
1302+
let html = '<div class="bgt-head"><span class="bgt-livedot"></span>'
1303+
+ '<span class="bgt-caret' + (bgTasksCollapsed ? '' : ' open') + '">' + CARET + '</span>'
1304+
+ '<span class="bgt-title">Background tasks</span><span class="bgt-count">· ' + running.length + ' running</span></div>';
1305+
if (!bgTasksCollapsed){
1306+
html += '<div class="bgt-list">' + running.map(t => {
1307+
const status = t.port ? '<span class="port">running on :' + esc(t.port) + '</span>' : (t.ready ? 'ready' : 'running…');
1308+
return '<div class="bgt-row"><span class="bgt-cmd" title="' + esc(t.command) + '">' + esc(t.command) + '</span>'
1309+
+ '<span class="bgt-status">' + status + '</span>'
1310+
+ '<button class="bgt-stop" data-id="' + esc(t.id) + '">' + codicon('stop-circle') + ' Stop</button></div>';
1311+
}).join('') + '</div>';
1312+
}
1313+
bgTasksBar.innerHTML = html;
1314+
bgTasksBar.style.display = 'block';
1315+
const head = bgTasksBar.querySelector('.bgt-head'); if (head){ head.onclick = () => { bgTasksCollapsed = !bgTasksCollapsed; renderBgTasks(); }; }
1316+
bgTasksBar.querySelectorAll('.bgt-stop').forEach(b => { b.onclick = (e) => { e.stopPropagation(); b.disabled = true; b.textContent = 'stopping…'; vscode.postMessage({ type: 'stopCommand', id: b.getAttribute('data-id') }); }; });
1317+
}
1318+
function applyBgTask(m){
1319+
if (!m.id){ return; }
1320+
const t = bgTasks[m.id] || (bgTasks[m.id] = { id: m.id, command: '', status: 'running', port: null, ready: false });
1321+
if (m.command != null){ t.command = m.command; }
1322+
if (m.status){ t.status = m.status; }
1323+
if (m.port){ t.port = m.port; }
1324+
if (m.ready){ t.ready = true; }
1325+
if (m.done){ delete bgTasks[m.id]; } // finished → drop from the running tray
1326+
renderBgTasks();
1327+
}
12691328
const planBar = document.getElementById('planBar');
12701329
function renderPlan(todos){
12711330
if (!todos || !todos.length){ planBar.style.display = 'none'; planBar.innerHTML = ''; return; }
@@ -1475,6 +1534,7 @@
14751534
else if (m.type === 'termRun'){ addTermRun(m); }
14761535
else if (m.type === 'termOutput'){ termOutputAppend(m); }
14771536
else if (m.type === 'termExit'){ termExitFinish(m); }
1537+
else if (m.type === 'bgTask'){ applyBgTask(m); }
14781538
else if (m.type === 'verifyRun'){ addVerifyRun(m); }
14791539
else if (m.type === 'verifyOutput'){ verifyOutputAppend(m); }
14801540
else if (m.type === 'verifyDone'){ verifyDoneFinish(m); }
@@ -1493,7 +1553,7 @@
14931553
else if (m.type === 'reset'){
14941554
log.innerHTML = '<div id="empty"><div class="big">New chat</div>Ask about your code, or describe what to build.</div>';
14951555
selLabel = null; ctxFiles = []; renderChips();
1496-
for (const k in editCards) { delete editCards[k]; } for (const k in termCards) { delete termCards[k]; } for (const k in verifyCards) { delete verifyCards[k]; } updateReviewBar(0); renderPlan([]); renderContext({ input: 0 });
1556+
for (const k in editCards) { delete editCards[k]; } for (const k in termCards) { delete termCards[k]; } for (const k in verifyCards) { delete verifyCards[k]; } for (const k in bgTasks) { delete bgTasks[k]; } renderBgTasks(); updateReviewBar(0); renderPlan([]); renderContext({ input: 0 });
14971557
pending = ''; shown = ''; doneSignaled = false; flushAll = false; current = null; setStreaming(false);
14981558
forceStick();
14991559
}

0 commit comments

Comments
 (0)