diff --git a/packages/types/src/message.ts b/packages/types/src/message.ts index b02078fc0d8..9fcd7e776ab 100644 --- a/packages/types/src/message.ts +++ b/packages/types/src/message.ts @@ -46,7 +46,21 @@ export type ClineAsk = z.infer // Needs classification: // - `followup` -// - `command_output + +/** + * NonBlockingAsk + * + * Asks that should not block task execution. These are informational or optional + * asks where the task can proceed even without an immediate user response. + */ + +export const nonBlockingAsks = ["command_output"] as const satisfies readonly ClineAsk[] + +export type NonBlockingAsk = (typeof nonBlockingAsks)[number] + +export function isNonBlockingAsk(ask: ClineAsk): ask is NonBlockingAsk { + return (nonBlockingAsks as readonly ClineAsk[]).includes(ask) +} /** * IdleAsk diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 7ed317da108..f0293e35c84 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -34,6 +34,7 @@ import { isIdleAsk, isInteractiveAsk, isResumableAsk, + isNonBlockingAsk, QueuedMessage, DEFAULT_CHECKPOINT_TIMEOUT_SECONDS, MAX_CHECKPOINT_TIMEOUT_SECONDS, @@ -821,13 +822,12 @@ export class Task extends EventEmitter implements TaskLike { // block (via the `pWaitFor`). const isBlocking = !(this.askResponse !== undefined || this.lastMessageTs !== askTs) const isMessageQueued = !this.messageQueueService.isEmpty() - const isStatusMutable = !partial && isBlocking && !isMessageQueued + // Non-blocking asks should not mutate task status since they don't actually block execution + const isStatusMutable = !partial && isBlocking && !isMessageQueued && !isNonBlockingAsk(type) let statusMutationTimeouts: NodeJS.Timeout[] = [] const statusMutationTimeout = 5_000 if (isStatusMutable) { - console.log(`Task#ask will block -> type: ${type}`) - if (isInteractiveAsk(type)) { statusMutationTimeouts.push( setTimeout(() => { @@ -879,14 +879,21 @@ export class Task extends EventEmitter implements TaskLike { // the message if there's text/images. this.handleWebviewAskResponse("yesButtonClicked", message.text, message.images) } else { - // For other ask types (like followup), fulfill the ask + // For other ask types (like followup or command_output), fulfill the ask // directly. this.setMessageResponse(message.text, message.images) } } } - // Wait for askResponse to be set. + // Non-blocking asks return immediately without waiting + // The ask message is created in the UI, but the task doesn't wait for a response + // This prevents blocking in cloud/headless environments + if (isNonBlockingAsk(type)) { + return { response: "yesButtonClicked" as ClineAskResponse, text: undefined, images: undefined } + } + + // Wait for askResponse to be set await pWaitFor(() => this.askResponse !== undefined || this.lastMessageTs !== askTs, { interval: 100 }) if (this.lastMessageTs !== askTs) { diff --git a/src/core/tools/executeCommandTool.ts b/src/core/tools/executeCommandTool.ts index b0ae07bf86a..8d0b39bde48 100644 --- a/src/core/tools/executeCommandTool.ts +++ b/src/core/tools/executeCommandTool.ts @@ -179,6 +179,7 @@ export async function executeCommand( let result: string = "" let exitDetails: ExitCodeDetails | undefined let shellIntegrationError: string | undefined + let hasAskedForCommandOutput = false const terminalProvider = terminalShellIntegrationDisabled ? "execa" : "vscode" const provider = await task.providerRef.deref() @@ -195,10 +196,13 @@ export async function executeCommand( const status: CommandExecutionStatus = { executionId, status: "output", output: compressedOutput } provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) }) - if (runInBackground) { + if (runInBackground || hasAskedForCommandOutput) { return } + // Mark that we've asked to prevent multiple concurrent asks + hasAskedForCommandOutput = true + try { const { response, text, images } = await task.ask("command_output", "") runInBackground = true @@ -207,7 +211,9 @@ export async function executeCommand( message = { text, images } process.continue() } - } catch (_error) {} + } catch (_error) { + // Silently handle ask errors (e.g., "Current ask promise was ignored") + } }, onCompleted: (output: string | undefined) => { result = Terminal.compressTerminalOutput( @@ -220,7 +226,6 @@ export async function executeCommand( completed = true }, onShellExecutionStarted: (pid: number | undefined) => { - console.log(`[executeCommand] onShellExecutionStarted: ${pid}`) const status: CommandExecutionStatus = { executionId, status: "started", pid, command } provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) }) },