Tl;Dr
The main application's AbortController signal, which is triggered when the user cancels an ongoing operation (e.g., by pressing the Escape key), is not propagated to the action function of slash commands.
As a result, any long-running asynchronous operations within a slash command (such as network requests, file system access, or child processes) will continue to execute even after the user has requested cancellation. This can lead to unexpected behavior, orphaned processes, and unnecessary resource consumption.
Analysis of the Current Implementation
-
Top-Level Abort Controller: In packages/cli/src/ui/hooks/useGeminiStream.ts, a new AbortController is correctly created in the submitQuery function for each user request. The cancelOngoingRequest function properly calls .abort() on this controller.
-
Signal Propagation Gap: The abortSignal from this controller is passed to the geminiClient.sendMessageStream, but it is not passed down to the handleSlashCommand function located in packages/cli/src/ui/hooks/slashCommandProcessor.ts.
-
Missing Context Property: The CommandContext interface, defined in packages/cli/src/ui/commands/types.ts, does not have a property to hold an AbortSignal. This is the primary architectural gap, as there is no standard mechanism to make the signal available to command actions.
-
Isolated Command Logic: As seen in packages/cli/src/ui/commands/setupGithubCommand.ts, commands that need cancellation logic are forced to create their own internal AbortController. This controller is completely disconnected from the main application's cancellation flow, making it impossible to respond to a global cancel event.
Proposed Solution
To fix this, we need to plumb the AbortSignal from the top-level stream handler down into the execution context of every slash command.
-
Update CommandContext:
Modify the CommandContext interface in packages/cli/src/ui/commands/types.ts to include the signal.
// In packages/cli/src/ui/commands/types.ts
export interface CommandContext {
// An abort signal that is tied to the lifecycle of the current user prompt.
signal: AbortSignal;
// ... existing properties
}
-
Pass the Signal Down the Call Stack:
- In
useGeminiStream.ts, update the call to handleSlashCommand to pass the abortSignal.
// In useGeminiStream.ts, inside `prepareQueryForGemini`
const slashCommandResult = await handleSlashCommand(trimmedQuery, abortSignal);
- Update the
handleSlashCommand function signature in slashCommandProcessor.ts to accept the signal.
// In slashCommandProcessor.ts
const handleSlashCommand = useCallback(
async (
rawQuery: PartListUnion,
signal: AbortSignal, // Add this parameter
oneTimeShellAllowlist?: Set<string>,
overwriteConfirmed?: boolean,
): Promise<SlashCommandProcessorResult | false> => {
// ...
}
);
- In
slashCommandProcessor.ts, include the signal when creating the fullCommandContext.
// In slashCommandProcessor.ts
const fullCommandContext: CommandContext = {
...commandContext,
signal, // Add the signal here
invocation: { /* ... */ },
overwriteConfirmed,
};
-
Refactor Slash Commands to Use the Context Signal:
Update commands with long-running operations to use the signal from the context instead of creating their own.
Example: Refactoring setupGithubCommand.ts
// Before
const abortController = new AbortController();
const response = await fetch(endpoint, {
// ...
signal: AbortSignal.any([
AbortSignal.timeout(30_000),
abortController.signal,
]),
});
// After
const response = await fetch(endpoint, {
// ...
signal: AbortSignal.any([
AbortSignal.timeout(30_000),
context.signal, // Use the signal from the context
]),
});
Acceptance Criteria
- The
AbortSignal from useGeminiStream is successfully passed down and is available in the CommandContext provided to all slash command action functions.
- Long-running slash commands, such as
/setup-github, correctly use the context.signal for their asynchronous operations. We should evaluate each slash command including commands via Custom Commands to ensure abort works properly.
- The application remains stable, and other commands are unaffected.
Tl;Dr
The main application's
AbortControllersignal, which is triggered when the user cancels an ongoing operation (e.g., by pressing theEscapekey), is not propagated to theactionfunction of slash commands.As a result, any long-running asynchronous operations within a slash command (such as network requests, file system access, or child processes) will continue to execute even after the user has requested cancellation. This can lead to unexpected behavior, orphaned processes, and unnecessary resource consumption.
Analysis of the Current Implementation
Top-Level Abort Controller: In
packages/cli/src/ui/hooks/useGeminiStream.ts, a newAbortControlleris correctly created in thesubmitQueryfunction for each user request. ThecancelOngoingRequestfunction properly calls.abort()on this controller.Signal Propagation Gap: The
abortSignalfrom this controller is passed to thegeminiClient.sendMessageStream, but it is not passed down to thehandleSlashCommandfunction located inpackages/cli/src/ui/hooks/slashCommandProcessor.ts.Missing Context Property: The
CommandContextinterface, defined inpackages/cli/src/ui/commands/types.ts, does not have a property to hold anAbortSignal. This is the primary architectural gap, as there is no standard mechanism to make the signal available to command actions.Isolated Command Logic: As seen in
packages/cli/src/ui/commands/setupGithubCommand.ts, commands that need cancellation logic are forced to create their own internalAbortController. This controller is completely disconnected from the main application's cancellation flow, making it impossible to respond to a global cancel event.Proposed Solution
To fix this, we need to plumb the
AbortSignalfrom the top-level stream handler down into the execution context of every slash command.Update
CommandContext:Modify the
CommandContextinterface inpackages/cli/src/ui/commands/types.tsto include the signal.Pass the Signal Down the Call Stack:
useGeminiStream.ts, update the call tohandleSlashCommandto pass theabortSignal.handleSlashCommandfunction signature inslashCommandProcessor.tsto accept the signal.slashCommandProcessor.ts, include the signal when creating thefullCommandContext.Refactor Slash Commands to Use the Context Signal:
Update commands with long-running operations to use the signal from the context instead of creating their own.
Example: Refactoring
setupGithubCommand.tsAcceptance Criteria
AbortSignalfromuseGeminiStreamis successfully passed down and is available in theCommandContextprovided to all slash commandactionfunctions./setup-github, correctly use thecontext.signalfor their asynchronous operations. We should evaluate each slash command including commands via Custom Commands to ensure abort works properly.