дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
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]>
Цей коміт міститься в:
16 files changed
+386
-6
No files matched your search
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) });
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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": "无法停止后台任务"
|
||||
}
|
||||
Reference in new issue
Заблокувати користувача