feat(agents): show the agent's background tasks above the composer (#1860)

* fix(agent): don't release an idle native agent while background tasks are running

A turn's background tasks (Claude Code's run_in_background commands) outlive
the turn: release_when_idle only checked for a running turn and open
permission requests, so five minutes after the turn ended it stopped the
agent process and orphaned whatever still ran, its result never reaching
the conversation.

Claude Code reports its live background tasks in stream-json mode: a
system/background_tasks_changed event carries the full list on every
change. The driver now translates it into an openidea_background_tasks
session update (like openidea_retry), the task keeps the list, and the
idle release waits while it isn't empty: it checks again after the usual
delay, and when the last task is done its report voids the pending
schedule and starts the delay over from then. A (re)started process
starts with no tasks, so a stale list can never hold the agent up.

Signed-off-by: Inf1mum <[email protected]>

* feat(agents): show the agent's background tasks above the composer

A grey bar above the composer lists what the agent runs in the
background (shells, monitors), with how long each has been running and
a Stop button per task. The task now carries the background tasks the
agent reports, with when each was first seen, and clears them when the
process ends. Stop asks Claude Code to end that one task through a new
_openidea/stop_task request; the composer's Stop keeps ending only the
turn (perTaskStopAffordance).

Closes #1788

---------

Signed-off-by: Inf1mum <[email protected]>
Co-authored-by: Inf1mum <[email protected]>
Co-authored-by: NealimeKenna <[email protected]>
Цей коміт міститься в:
authored and GitHub committed 2026-11-07 05:26:28 +08:00
1 джерело c4b9912036
коміт 0eca4366d0
16 files changed
+386 -6

No files matched your search

+25
Переглянути файл
@@ -566,6 +566,31 @@
padding: 6px 12px 12px;
}
.background {
display: flex;
flex-direction: column;
gap: 2px;
}
.backgroundTask {
display: flex;
align-items: center;
gap: 6px;
min-height: 24px;
padding: 0 4px 0 8px;
border-radius: var(--radius-row);
background: var(--bg-elev);
color: var(--fg-muted);
font-size: var(--fs-small);
font-variant-numeric: tabular-nums;
}
.backgroundText {
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.queue {
display: flex;
flex-direction: column;
+35
Переглянути файл
@@ -33,6 +33,7 @@ import {
selectableText,
useSelectionBoundary,
showContextMenu,
type IconName,
} from "@/ui";
import { useTx, tx } from "@/platform/i18n";
import { isImeKey } from "@/platform/ime";
@@ -59,6 +60,7 @@ import {
} from "./model";
import {
cancelTask,
stopBackgroundTask,
loadEntries,
markVisible,
refreshChanges,
@@ -1318,6 +1320,7 @@ function SessionComposer({
</div>
)}
<WorkingSubagents entries={entries} onOpen={onOpenSubagent} />
<BackgroundTasks task={task} />
{task.queue.length > 0 && <Queue task={task} />}
<Composer
id={task.id}
@@ -1349,6 +1352,38 @@ function SessionComposer({
}
/** Messages waiting for the turn to end: send now, edit, or drop. */
const BACKGROUND_ICON: Record<string, IconName> = { local_bash: "terminal", monitor: "eye" };
/** What the agent runs in the background; its subagents show as chips of their own. */
function BackgroundTasks({ task }: { task: AgentTask }) {
const tx = useTx();
const running = (task.background ?? []).filter((background) => background.kind !== "local_agent");
if (!running.length) return null;
const fail = (e: unknown) =>
notifications().notify({ level: "error", title: "Couldn't stop the background task", body: String(e) });
return (
<div className={styles.background} aria-label={tx("Running in the background")}>
{running.map((background) => (
<div key={background.id} className={styles.backgroundTask}>
<Icon name={BACKGROUND_ICON[background.kind] ?? "clock"} size={12} className={styles.muted} />
<span className={styles.backgroundText} title={background.description}>
{background.description || background.kind}
</span>
{background.startedMs != null && <Elapsed since={background.startedMs} />}
{task.capabilities?.backgroundTasks && (
<IconButton
icon="stop"
title="Stop"
size="small"
onClick={() => void stopBackgroundTask(task.id, background.id).catch(fail)}
/>
)}
</div>
))}
</div>
);
}
function Queue({ task }: { task: AgentTask }) {
const fail = (e: unknown) =>
notifications().notify({ level: "error", title: "Couldn't update the queue", body: String(e) });
+2
Переглянути файл
@@ -625,6 +625,8 @@ export function addReferences(refs: Array<{ path: string; rel: string; mention:
/** Stops the task's turn, or with `subagent` only that subagent's work. */
export const cancelTask = (task: string, subagent: string | null = null) => call(commands.agentCancel(task, subagent));
export const stopBackgroundTask = (task: string, background: string) =>
call(commands.agentStopBackground(task, background));
export const respond = (task: string, permission: number, option: string | null) =>
call(commands.agentRespond(task, permission, option));
export const unarchiveTask = (task: string) => call(commands.agentUnarchive(task));