From 8b7321ea8d3d8c9ee4fc3d8fd83022773ac3c2a5 Mon Sep 17 00:00:00 2001 From: Adam Weidman Date: Sun, 15 Mar 2026 15:11:13 -0400 Subject: [PATCH] refactor(core): move background injection wiring from UI to backend Wire ExecutionLifecycleService.setInjectionService() in Config constructor so backgrounded executions inject directly via settleExecution instead of routing through a useEffect bridge in AppContainer. --- packages/cli/src/ui/AppContainer.tsx | 24 ------------------- packages/core/src/config/config.ts | 2 ++ .../src/services/executionLifecycleService.ts | 21 ++++++++++++++++ 3 files changed, 23 insertions(+), 24 deletions(-) diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 757c458b0b..241e0f3030 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -85,8 +85,6 @@ import { buildUserSteeringHintPrompt, logBillingEvent, ApiKeyUpdatedEvent, - ExecutionLifecycleService, - type BackgroundCompletionInfo, type InjectionSource, } from '@google/gemini-cli-core'; import { validateAuthMethod } from '../config/auth.js'; @@ -1119,28 +1117,6 @@ Logging in with Google... Restarting Gemini CLI to continue. }; }, [config]); - // Wire background completion events from ExecutionLifecycleService into the - // injection service so completed backgrounded executions are reinjected. - useEffect(() => { - const bgListener = (info: BackgroundCompletionInfo) => { - // Use the execution creator's custom injection text if provided. - let text = info.injectionText; - if (text === null || text === undefined) { - // Fallback: generic format for executions without a custom formatter. - const header = info.error - ? `[Background execution (ID: ${info.executionId}) completed with error: ${info.error.message}]` - : `[Background execution (ID: ${info.executionId}) completed]`; - const body = info.output ? `\n${info.output}` : ''; - text = `${header}${body}`; - } - config.injectionService.addInjection(text, 'background_completion'); - }; - ExecutionLifecycleService.onBackgroundComplete(bgListener); - return () => { - ExecutionLifecycleService.offBackgroundComplete(bgListener); - }; - }, [config]); - const { streamingState, submitQuery, diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index e07d6fce0c..294fae521b 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -148,6 +148,7 @@ import type { AgentDefinition } from '../agents/types.js'; import { fetchAdminControls } from '../code_assist/admin/admin_controls.js'; import { isSubpath, resolveToRealPath } from '../utils/paths.js'; import { InjectionService } from './injectionService.js'; +import { ExecutionLifecycleService } from '../services/executionLifecycleService.js'; import { WORKSPACE_POLICY_TIER } from '../policy/config.js'; import { loadPoliciesFromToml } from '../policy/toml-loader.js'; @@ -938,6 +939,7 @@ export class Config implements McpContext, AgentLoopContext { this.injectionService = new InjectionService(() => this.isModelSteeringEnabled(), ); + ExecutionLifecycleService.setInjectionService(this.injectionService); this.toolOutputMasking = { enabled: params.toolOutputMasking?.enabled ?? true, toolProtectionThreshold: diff --git a/packages/core/src/services/executionLifecycleService.ts b/packages/core/src/services/executionLifecycleService.ts index 0cd4df4c5d..a91d2a7b77 100644 --- a/packages/core/src/services/executionLifecycleService.ts +++ b/packages/core/src/services/executionLifecycleService.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import type { InjectionService } from '../config/injectionService.js'; import type { AnsiOutput } from '../utils/terminalSerializer.js'; export type ExecutionMethod = @@ -138,6 +139,15 @@ export class ExecutionLifecycleService { >(); private static backgroundCompletionListeners = new Set(); + private static injectionService: InjectionService | null = null; + + /** + * Wires a singleton InjectionService so that backgrounded executions + * can inject their output directly without routing through the UI layer. + */ + static setInjectionService(service: InjectionService): void { + this.injectionService = service; + } /** * Registers a listener that fires when a previously-backgrounded @@ -210,6 +220,7 @@ export class ExecutionLifecycleService { this.activeListeners.clear(); this.exitedExecutionInfo.clear(); this.backgroundCompletionListeners.clear(); + this.injectionService = null; this.nextExecutionId = NON_PROCESS_EXECUTION_ID_START; } @@ -323,6 +334,16 @@ export class ExecutionLifecycleService { error: result.error, injectionText, }; + + // Inject directly into the model conversation if injection text is + // available and the injection service has been wired up. + if (injectionText && this.injectionService) { + this.injectionService.addInjection( + injectionText, + 'background_completion', + ); + } + for (const listener of this.backgroundCompletionListeners) { listener(info); }