Skip to content

Slash commands do not respect top-level cancellation signal #6066

Description

@abhipatel12

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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  1. 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
    }
  2. 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,
    };
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Stalearea/coreIssues related to User Interface, OS Support, Core Functionality

    Type

    No fields configured for Task.

    Projects

    Status
    Closed

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions