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
109 changes: 90 additions & 19 deletions src/core/tools/ExecuteCommandTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,15 @@ export function canRetryShellIntegrationError(error: unknown): error is ShellInt
return error instanceof ShellIntegrationError && !error.commandSubmitted
}

/**
* Grace period before a foreground command may trigger a `command_output` ask.
* Short commands that emit output and exit within this window never prompt the
* user; the ask only fires when the command is still running once the delay
* elapses, so users can still interrupt or provide feedback on long-running
* commands.
*/
export const COMMAND_OUTPUT_ASK_DELAY_MS = 5_000

export function getTerminalProviderForExecution(terminalShellIntegrationDisabled: boolean): {
terminalProvider: RooTerminalProvider
isCmdExeFallback: boolean
Expand Down Expand Up @@ -340,6 +349,58 @@ export async function executeCommandInTerminal(
resolveOnCompleted = resolve
})

// Delay the `command_output` ask so short foreground commands that emit
// output and exit normally never prompt the user. The ask only fires if the
// command is still running once COMMAND_OUTPUT_ASK_DELAY_MS has elapsed
// since execution started, preserving the interrupt/feedback path for
// long-running commands. The anchor is re-based to onShellExecutionStarted
// (falling back to the pre-runCommand timestamp when that event never
// fires) so shell-integration startup on cold terminals does not consume
// the grace period.
let commandStartedAt = 0
let commandOutputAskTimer: NodeJS.Timeout | undefined

const askForCommandOutput = async (process: RooTerminalProcess): Promise<void> => {
if (runInBackground || hasAskedForCommandOutput || completed) {
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

if (response === "messageResponse") {
message = { text, images }
}

// Any answer means the command should keep running in the background;
// continue the process so the tool resolves now instead of blocking
// until the command actually completes.
process.continue()
} catch (_error) {
// Silently handle ask errors (e.g., "Current ask promise was ignored")
}
}

const scheduleCommandOutputAsk = (process: RooTerminalProcess): void => {
if (runInBackground || hasAskedForCommandOutput || completed || commandOutputAskTimer) {
return
}

const remainingDelay = COMMAND_OUTPUT_ASK_DELAY_MS - (Date.now() - commandStartedAt)

commandOutputAskTimer = setTimeout(
() => {
commandOutputAskTimer = undefined
void askForCommandOutput(process)
},
Math.max(remainingDelay, 0),
)
}

const callbacks: RooTerminalCallbacks = {
onLine: async (lines: string, process: RooTerminalProcess) => {
accumulatedOutput += lines
Expand All @@ -359,26 +420,19 @@ export async function executeCommandInTerminal(
provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) })
schedulePartialCommandOutputUpdate()

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

if (response === "messageResponse") {
message = { text, images }
process.continue()
}
} catch (_error) {
// Silently handle ask errors (e.g., "Current ask promise was ignored")
}
scheduleCommandOutputAsk(process)
},
onCompleted: async (output: string | undefined) => {
clearTimeout(commandOutputAskTimer)
Comment thread
zoomote[bot] marked this conversation as resolved.
commandOutputAskTimer = undefined

// If an interactive command_output ask is still pending, supersede it
// so it resolves immediately instead of lingering until the next
// interactive message bumps lastMessageTs.
if (hasAskedForCommandOutput && !runInBackground) {
task.supersedePendingAsk()
}

clearTimeout(pendingCommandOutputEmitTimer)
pendingCommandOutputEmitTimer = undefined

Expand Down Expand Up @@ -412,9 +466,21 @@ export async function executeCommandInTerminal(
console.error("[ExecuteCommandTool] Failed to flush final command_output:", error)
})
},
onShellExecutionStarted: (pid: number | undefined) => {
onShellExecutionStarted: (pid: number | undefined, process: RooTerminalProcess) => {
const status: CommandExecutionStatus = { executionId, status: "started", pid, command }
provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) })

// Re-anchor the ask delay to actual execution start so the shell
// integration startup wait does not count against the grace period.
commandStartedAt = Date.now()

// Output should not precede this event, but if it did, reschedule
// the pending ask against the corrected anchor.
if (commandOutputAskTimer) {
clearTimeout(commandOutputAskTimer)
commandOutputAskTimer = undefined
scheduleCommandOutputAsk(process)
}
},
onShellExecutionComplete: (details: ExitCodeDetails) => {
const status: CommandExecutionStatus = { executionId, status: "exited", exitCode: details.exitCode }
Expand All @@ -441,6 +507,8 @@ export async function executeCommandInTerminal(
workingDir = terminal.getCurrentWorkingDirectory()
}

// Fallback anchor for providers that never fire onShellExecutionStarted.
commandStartedAt = Date.now()
const process = terminal.runCommand(command, callbacks)
task.terminalProcess = process

Expand All @@ -462,6 +530,8 @@ export async function executeCommandInTerminal(
new Promise<void>((resolve) => {
agentTimeoutId = setTimeout(() => {
runInBackground = true
clearTimeout(commandOutputAskTimer)
commandOutputAskTimer = undefined
process.continue()
task.supersedePendingAsk()
resolve()
Expand Down Expand Up @@ -501,6 +571,7 @@ export async function executeCommandInTerminal(
} finally {
clearTimeout(agentTimeoutId)
clearTimeout(userTimeoutId)
clearTimeout(commandOutputAskTimer)
clearTimeout(pendingCommandOutputEmitTimer)
task.terminalProcess = undefined
}
Expand Down
Loading
Loading