Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion packages/types/src/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
17 changes: 12 additions & 5 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
isIdleAsk,
isInteractiveAsk,
isResumableAsk,
isNonBlockingAsk,
QueuedMessage,
DEFAULT_CHECKPOINT_TIMEOUT_SECONDS,
MAX_CHECKPOINT_TIMEOUT_SECONDS,
Expand Down Expand Up @@ -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(() => {
Expand Down Expand Up @@ -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) {
Expand Down
11 changes: 8 additions & 3 deletions src/core/tools/executeCommandTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand All @@ -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(
Expand All @@ -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) })
},
Expand Down