feat(core): agnostic background task UI with CompletionBehavior (#22740)

Co-authored-by: mkorwel <matt.korwel@gmail.com>
This commit is contained in:
Adam Weidman
2026-03-28 17:27:51 -04:00
committed by GitHub
parent 07ab16dbbe
commit 3eebb75b7a
54 changed files with 1467 additions and 875 deletions

View File

@@ -4,9 +4,9 @@
* 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 {
export interface BackgroundTask {
pid: number;
command: string;
output: string | AnsiOutput;
@@ -14,13 +14,14 @@ export interface BackgroundShell {
binaryBytesReceived: number;
status: 'running' | 'exited';
exitCode?: number;
completionBehavior?: CompletionBehavior;
}
export interface ShellState {
activeShellPtyId: number | null;
lastShellOutputTime: number;
backgroundShells: Map<number, BackgroundShell>;
isBackgroundShellVisible: boolean;
backgroundTasks: Map<number, BackgroundTask>;
isBackgroundTaskVisible: boolean;
}
export type ShellAction =
@@ -29,21 +30,22 @@ export type ShellAction =
| { type: 'SET_VISIBILITY'; visible: boolean }
| { type: 'TOGGLE_VISIBILITY' }
| {
type: 'REGISTER_SHELL';
type: 'REGISTER_TASK';
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 }
| { type: 'SYNC_BACKGROUND_SHELLS' }
| { type: 'DISMISS_SHELL'; pid: number };
| { type: 'UPDATE_TASK'; pid: number; update: Partial<BackgroundTask> }
| { type: 'APPEND_TASK_OUTPUT'; pid: number; chunk: string | AnsiOutput }
| { type: 'SYNC_BACKGROUND_TASKS' }
| { type: 'DISMISS_TASK'; pid: number };
export const initialState: ShellState = {
activeShellPtyId: null,
lastShellOutputTime: 0,
backgroundShells: new Map(),
isBackgroundShellVisible: false,
backgroundTasks: new Map(),
isBackgroundTaskVisible: false,
};
export function shellReducer(
@@ -56,75 +58,76 @@ export function shellReducer(
case 'SET_OUTPUT_TIME':
return { ...state, lastShellOutputTime: action.time };
case 'SET_VISIBILITY':
return { ...state, isBackgroundShellVisible: action.visible };
return { ...state, isBackgroundTaskVisible: action.visible };
case 'TOGGLE_VISIBILITY':
return {
...state,
isBackgroundShellVisible: !state.isBackgroundShellVisible,
isBackgroundTaskVisible: !state.isBackgroundTaskVisible,
};
case 'REGISTER_SHELL': {
if (state.backgroundShells.has(action.pid)) return state;
const nextShells = new Map(state.backgroundShells);
nextShells.set(action.pid, {
case 'REGISTER_TASK': {
if (state.backgroundTasks.has(action.pid)) return state;
const nextTasks = new Map(state.backgroundTasks);
nextTasks.set(action.pid, {
pid: action.pid,
command: action.command,
output: action.initialOutput,
isBinary: false,
binaryBytesReceived: 0,
status: 'running',
completionBehavior: action.completionBehavior,
});
return { ...state, backgroundShells: nextShells };
return { ...state, backgroundTasks: nextTasks };
}
case 'UPDATE_SHELL': {
const shell = state.backgroundShells.get(action.pid);
if (!shell) return state;
const nextShells = new Map(state.backgroundShells);
const updatedShell = { ...shell, ...action.update };
case 'UPDATE_TASK': {
const task = state.backgroundTasks.get(action.pid);
if (!task) return state;
const nextTasks = new Map(state.backgroundTasks);
const updatedTask = { ...task, ...action.update };
// Maintain insertion order, move to end if status changed to exited
if (action.update.status === 'exited') {
nextShells.delete(action.pid);
nextTasks.delete(action.pid);
}
nextShells.set(action.pid, updatedShell);
return { ...state, backgroundShells: nextShells };
nextTasks.set(action.pid, updatedTask);
return { ...state, backgroundTasks: nextTasks };
}
case 'APPEND_SHELL_OUTPUT': {
const shell = state.backgroundShells.get(action.pid);
if (!shell) return state;
// Note: we mutate the shell object in the map for background updates
case 'APPEND_TASK_OUTPUT': {
const task = state.backgroundTasks.get(action.pid);
if (!task) return state;
// Note: we mutate the task object in the map for background updates
// to avoid re-rendering if the drawer is not visible.
// This is an intentional performance optimization for the CLI.
let newOutput = shell.output;
let newOutput = task.output;
if (typeof action.chunk === 'string') {
newOutput =
typeof shell.output === 'string'
? shell.output + action.chunk
typeof task.output === 'string'
? task.output + action.chunk
: action.chunk;
} else {
newOutput = action.chunk;
}
shell.output = newOutput;
task.output = newOutput;
const nextState = { ...state, lastShellOutputTime: Date.now() };
if (state.isBackgroundShellVisible) {
if (state.isBackgroundTaskVisible) {
return {
...nextState,
backgroundShells: new Map(state.backgroundShells),
backgroundTasks: new Map(state.backgroundTasks),
};
}
return nextState;
}
case 'SYNC_BACKGROUND_SHELLS': {
return { ...state, backgroundShells: new Map(state.backgroundShells) };
case 'SYNC_BACKGROUND_TASKS': {
return { ...state, backgroundTasks: new Map(state.backgroundTasks) };
}
case 'DISMISS_SHELL': {
const nextShells = new Map(state.backgroundShells);
nextShells.delete(action.pid);
case 'DISMISS_TASK': {
const nextTasks = new Map(state.backgroundTasks);
nextTasks.delete(action.pid);
return {
...state,
backgroundShells: nextShells,
isBackgroundShellVisible:
nextShells.size === 0 ? false : state.isBackgroundShellVisible,
backgroundTasks: nextTasks,
isBackgroundTaskVisible:
nextTasks.size === 0 ? false : state.isBackgroundTaskVisible,
};
}
default: