feat(core): add CompletionBehavior for background task injection control

Introduce inject/notify/silent completion behaviors that control what
happens when a backgrounded execution completes:
- inject: full output injected into conversation, auto-dismiss from UI
- notify: short pointer message injected (e.g. log file path), auto-dismiss
- silent: nothing injected, stays in Ctrl+B until manually dismissed

Shell commands use 'notify' (output saved to log file), remote agents
will use 'inject' (full output reinjected). Default is 'silent' when
no formatInjection callback is provided.
This commit is contained in:
Adam Weidman
2026-03-15 11:10:42 -04:00
parent d68cc3a88f
commit 708d0e7016
5 changed files with 199 additions and 12 deletions
@@ -9,7 +9,12 @@ import type {
IndividualToolCallDisplay,
} from '../types.js';
import { useCallback, useReducer, useRef, useEffect } from 'react';
import type { AnsiOutput, Config, GeminiClient } from '@google/gemini-cli-core';
import type {
AnsiOutput,
Config,
GeminiClient,
CompletionBehavior,
} from '@google/gemini-cli-core';
import {
isBinary,
ShellExecutionService,
@@ -238,8 +243,19 @@ export const useShellCommandProcessor = (
);
const registerBackgroundTask = useCallback(
(pid: number, command: string, initialOutput: string | AnsiOutput) => {
dispatch({ type: 'REGISTER_SHELL', pid, command, initialOutput });
(
pid: number,
command: string,
initialOutput: string | AnsiOutput,
completionBehavior?: CompletionBehavior,
) => {
dispatch({
type: 'REGISTER_SHELL',
pid,
command,
initialOutput,
completionBehavior,
});
// Subscribe to exit via ExecutionLifecycleService (works for all execution types)
const exitUnsubscribe = ExecutionLifecycleService.onExit(pid, (code) => {
@@ -248,8 +264,11 @@ export const useShellCommandProcessor = (
pid,
update: { status: 'exited', exitCode: code },
});
// Auto-dismiss completed tasks from the background panel.
dispatch({ type: 'DISMISS_SHELL', pid });
// Auto-dismiss for inject/notify (output was delivered to conversation).
// Silent tasks stay in the UI until manually dismissed.
if (completionBehavior !== 'silent') {
dispatch({ type: 'DISMISS_SHELL', pid });
}
const unsub = m.subscriptions.get(pid);
if (unsub) {
unsub();
@@ -304,12 +323,18 @@ export const useShellCommandProcessor = (
executionId: number;
label: string;
output: string;
completionBehavior: CompletionBehavior;
}) => {
// Skip if already registered (e.g. shells register via their own flow)
if (m.backgroundedPids.has(info.executionId)) {
return;
}
registerBackgroundTask(info.executionId, info.label, info.output);
registerBackgroundTask(
info.executionId,
info.label,
info.output,
info.completionBehavior,
);
};
ExecutionLifecycleService.onBackground(listener);
return () => {
@@ -489,7 +514,12 @@ export const useShellCommandProcessor = (
setPendingHistoryItem(null);
if (result.backgrounded && result.pid) {
registerBackgroundTask(result.pid, rawQuery, cumulativeStdout);
registerBackgroundTask(
result.pid,
rawQuery,
cumulativeStdout,
'notify',
);
dispatch({ type: 'SET_ACTIVE_PTY', pid: null });
}
+4 -1
View File
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { AnsiOutput } from '@google/gemini-cli-core';
import type { AnsiOutput, CompletionBehavior } from '@google/gemini-cli-core';
export interface BackgroundShell {
pid: number;
@@ -14,6 +14,7 @@ export interface BackgroundShell {
binaryBytesReceived: number;
status: 'running' | 'exited';
exitCode?: number;
completionBehavior?: CompletionBehavior;
}
export interface ShellState {
@@ -33,6 +34,7 @@ export type ShellAction =
pid: number;
command: string;
initialOutput: string | AnsiOutput;
completionBehavior?: CompletionBehavior;
}
| { type: 'UPDATE_SHELL'; pid: number; update: Partial<BackgroundShell> }
| { type: 'APPEND_SHELL_OUTPUT'; pid: number; chunk: string | AnsiOutput }
@@ -72,6 +74,7 @@ export function shellReducer(
isBinary: false,
binaryBytesReceived: 0,
status: 'running',
completionBehavior: action.completionBehavior,
});
return { ...state, backgroundShells: nextShells };
}