refactor(core): move background completion consumption from UI to agent loop

The agent loop in local-executor now listens via onInjection (all sources)
instead of onUserHint (steering only), picking up background completions
between turns. This removes the separate bg completion useEffect, refs,
state, and callback from AppContainer entirely.
This commit is contained in:
Adam Weidman
2026-03-15 15:43:34 -04:00
parent 8b7321ea8d
commit f46a1c7e8b
2 changed files with 26 additions and 61 deletions
+5 -55
View File
@@ -85,7 +85,6 @@ import {
buildUserSteeringHintPrompt,
logBillingEvent,
ApiKeyUpdatedEvent,
type InjectionSource,
} from '@google/gemini-cli-core';
import { validateAuthMethod } from '../config/auth.js';
import process from 'node:process';
@@ -1078,8 +1077,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
const pendingHintsRef = useRef<string[]>([]);
const [pendingHintCount, setPendingHintCount] = useState(0);
const pendingBackgroundCompletionsRef = useRef<string[]>([]);
const [pendingBgCompletionCount, setPendingBgCompletionCount] = useState(0);
const consumePendingHints = useCallback(() => {
if (pendingHintsRef.current.length === 0) {
@@ -1091,29 +1088,14 @@ Logging in with Google... Restarting Gemini CLI to continue.
return hint;
}, []);
const consumePendingBackgroundCompletions = useCallback(() => {
if (pendingBackgroundCompletionsRef.current.length === 0) {
return null;
}
const output = pendingBackgroundCompletionsRef.current.join('\n');
pendingBackgroundCompletionsRef.current = [];
setPendingBgCompletionCount(0);
return output;
}, []);
useEffect(() => {
const injectionListener = (text: string, source: InjectionSource) => {
if (source === 'user_steering') {
pendingHintsRef.current.push(text);
setPendingHintCount((prev) => prev + 1);
} else if (source === 'background_completion') {
pendingBackgroundCompletionsRef.current.push(text);
setPendingBgCompletionCount((prev) => prev + 1);
}
const hintListener = (hint: string) => {
pendingHintsRef.current.push(hint);
setPendingHintCount((prev) => prev + 1);
};
config.injectionService.onInjection(injectionListener);
config.injectionService.onUserHint(hintListener);
return () => {
config.injectionService.offInjection(injectionListener);
config.injectionService.offUserHint(hintListener);
};
}, [config]);
@@ -2148,38 +2130,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
pendingHintCount,
]);
// Reinject completed background execution output into the model conversation.
// Unlike user steering hints, this is NOT gated on model steering being enabled.
useEffect(() => {
if (
!isConfigInitialized ||
streamingState !== StreamingState.Idle ||
!isMcpReady ||
isToolAwaitingConfirmation(pendingHistoryItems)
) {
return;
}
const bgOutput = consumePendingBackgroundCompletions();
if (!bgOutput) {
return;
}
void submitQuery([
{
text: `Background execution update:\n${bgOutput}\n\nThe above background execution has completed. Review the output and continue your work accordingly.`,
},
]);
}, [
isConfigInitialized,
isMcpReady,
streamingState,
submitQuery,
consumePendingBackgroundCompletions,
pendingHistoryItems,
pendingBgCompletionCount,
]);
const allToolCalls = useMemo(
() =>
pendingHistoryItems
+21 -6
View File
@@ -65,6 +65,7 @@ import { getToolCallContext } from '../utils/toolCallContext.js';
import { scheduleAgentTools } from './agent-scheduler.js';
import { DeadlineTimer } from '../utils/deadlineTimer.js';
import { formatUserHintsForModel } from '../utils/fastAckHelper.js';
import type { InjectionSource } from '../config/injectionService.js';
/** A callback function to report on agent activity. */
export type ActivityCallback = (activity: SubagentActivityEvent) => void;
@@ -526,14 +527,19 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
: DEFAULT_QUERY_STRING;
const pendingHintsQueue: string[] = [];
const hintListener = (hint: string) => {
pendingHintsQueue.push(hint);
const pendingBgCompletionsQueue: string[] = [];
const injectionListener = (text: string, source: InjectionSource) => {
if (source === 'user_steering') {
pendingHintsQueue.push(text);
} else if (source === 'background_completion') {
pendingBgCompletionsQueue.push(text);
}
};
// Capture the index of the last hint before starting to avoid re-injecting old hints.
// NOTE: Hints added AFTER this point will be broadcast to all currently running
// local agents via the listener below.
const startIndex = this.config.injectionService.getLatestHintIndex();
this.config.injectionService.onUserHint(hintListener);
this.config.injectionService.onInjection(injectionListener);
try {
const initialHints =
@@ -585,20 +591,29 @@ export class LocalAgentExecutor<TOutput extends z.ZodTypeAny> {
// If status is 'continue', update message for the next loop
currentMessage = turnResult.nextMessage;
// Check for new user steering hints collected via subscription
// Inject background completion output into the next turn.
if (pendingBgCompletionsQueue.length > 0) {
const bgText = pendingBgCompletionsQueue.join('\n');
pendingBgCompletionsQueue.length = 0;
currentMessage.parts ??= [];
currentMessage.parts.unshift({
text: `Background execution update:\n${bgText}\n\nThe above background execution has completed. Review the output and continue your work accordingly.`,
});
}
// Check for new user steering hints collected via subscription.
if (pendingHintsQueue.length > 0) {
const hintsToProcess = [...pendingHintsQueue];
pendingHintsQueue.length = 0;
const formattedHints = formatUserHintsForModel(hintsToProcess);
if (formattedHints) {
// Append hints to the current message (next turn)
currentMessage.parts ??= [];
currentMessage.parts.unshift({ text: formattedHints });
}
}
}
} finally {
this.config.injectionService.offUserHint(hintListener);
this.config.injectionService.offInjection(injectionListener);
}
// === UNIFIED RECOVERY BLOCK ===