Просмотр исходного кода

fix: prevent command_output ask from blocking in cloud/headless environments (#9152)

Daniel 1 месяц назад
Родитель
Сommit
4cd5c9022e
3 измененных файлов с 35 добавлено и 9 удалено
  1. 15 1
      packages/types/src/message.ts
  2. 12 5
      src/core/task/Task.ts
  3. 8 3
      src/core/tools/executeCommandTool.ts

+ 15 - 1
packages/types/src/message.ts

@@ -46,7 +46,21 @@ export type ClineAsk = z.infer<typeof clineAskSchema>
 
 // 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

+ 12 - 5
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<TaskEvents> 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<TaskEvents> 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) {

+ 8 - 3
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) })
 		},