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

+16
Переглянути файл
@@ -155,6 +155,8 @@ export const commands = {
agentProbe: (provider: string, refresh: boolean) => typedError<AgentProbe, IdeError>(__TAURI_INVOKE("agent_probe", { provider, refresh })),
/** Stops the task's current turn, or with `subagent` only that subagent's work. */
agentCancel: (task: string, subagent: string | null) => typedError<null, IdeError>(__TAURI_INVOKE("agent_cancel", { task, subagent })),
/** Stops one of the task's background tasks. */
agentStopBackground: (task: string, background: string) => typedError<null, IdeError>(__TAURI_INVOKE("agent_stop_background", { task, background })),
/** Answers a permission request; `option: null` cancels it. */
agentRespond: (task: string, permission: number, option: string | null) => typedError<null, IdeError>(__TAURI_INVOKE("agent_respond", { task, permission, option })),
agentArchive: (task: string) => typedError<null, IdeError>(__TAURI_INVOKE("agent_archive", { task })),
@@ -1362,6 +1364,16 @@ export type ActiveTheme = {
theme: ThemeDef,
};
/** A task the agent runs in the background while the conversation goes on. */
export type AgentBackgroundTask = {
id: string,
/** What it is, as the agent names it (`local_bash`, `local_agent`, `monitor`…). */
kind: string,
description: string,
/** When the client first saw it (ms since the epoch). */
startedMs: number | null,
};
/**
* What a task's agent can do beyond a plain conversation, as it reported when its
* session started.
@@ -1375,6 +1387,8 @@ export type AgentCapabilities = {
rollback?: boolean,
/** A message can join the running turn instead of waiting for it. */
steer?: boolean,
/** It reports its background tasks and stops them one by one. */
backgroundTasks?: boolean,
};
/** A file the task changed, relative to [`AgentTask::base_commit`]. */
@@ -1665,6 +1679,8 @@ export type AgentTask = {
* by its next output or any status change.
*/
retry?: AgentRetry | null,
/** What the agent runs in the background (shells, subagents, monitors), oldest first. */
background?: AgentBackgroundTask[],
};
export type AgentToolDiff = {
+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));
+3 -1
Переглянути файл
@@ -50,5 +50,7 @@
"Remote connection cancelled": "Remote connection cancelled",
"Xdebug is not loaded in PHP {version}": "Xdebug is not loaded in PHP {version}",
"{php} runs without Xdebug, so breakpoints are skipped. Install Xdebug 3 for this PHP or choose another PHP interpreter.": "{php} runs without Xdebug, so breakpoints are skipped. Install Xdebug 3 for this PHP or choose another PHP interpreter.",
"Open PHP Settings": "Open PHP Settings"
"Open PHP Settings": "Open PHP Settings",
"Running in the background": "Running in the background",
"Couldn't stop the background task": "Couldn't stop the background task"
}
+3 -1
Переглянути файл
@@ -6410,5 +6410,7 @@
"Remote connection cancelled": "远程连接已取消",
"Xdebug is not loaded in PHP {version}": "PHP {version} 未加载 Xdebug",
"{php} runs without Xdebug, so breakpoints are skipped. Install Xdebug 3 for this PHP or choose another PHP interpreter.": "{php} 运行时没有 Xdebug,断点会被跳过。请为此 PHP 安装 Xdebug 3,或选择其他 PHP 解释器。",
"Open PHP Settings": "打开 PHP 设置"
"Open PHP Settings": "打开 PHP 设置",
"Running in the background": "后台运行中",
"Couldn't stop the background task": "无法停止后台任务"
}