@@ -35,6 +35,15 @@ export function canRetryShellIntegrationError(error: unknown): error is ShellInt
3535 return error instanceof ShellIntegrationError && ! error . commandSubmitted
3636}
3737
38+ /**
39+ * Grace period before a foreground command may trigger a `command_output` ask.
40+ * Short commands that emit output and exit within this window never prompt the
41+ * user; the ask only fires when the command is still running once the delay
42+ * elapses, so users can still interrupt or provide feedback on long-running
43+ * commands.
44+ */
45+ export const COMMAND_OUTPUT_ASK_DELAY_MS = 5_000
46+
3847export function getTerminalProviderForExecution ( terminalShellIntegrationDisabled : boolean ) : {
3948 terminalProvider : RooTerminalProvider
4049 isCmdExeFallback : boolean
@@ -340,6 +349,58 @@ export async function executeCommandInTerminal(
340349 resolveOnCompleted = resolve
341350 } )
342351
352+ // Delay the `command_output` ask so short foreground commands that emit
353+ // output and exit normally never prompt the user. The ask only fires if the
354+ // command is still running once COMMAND_OUTPUT_ASK_DELAY_MS has elapsed
355+ // since execution started, preserving the interrupt/feedback path for
356+ // long-running commands. The anchor is re-based to onShellExecutionStarted
357+ // (falling back to the pre-runCommand timestamp when that event never
358+ // fires) so shell-integration startup on cold terminals does not consume
359+ // the grace period.
360+ let commandStartedAt = 0
361+ let commandOutputAskTimer : NodeJS . Timeout | undefined
362+
363+ const askForCommandOutput = async ( process : RooTerminalProcess ) : Promise < void > => {
364+ if ( runInBackground || hasAskedForCommandOutput || completed ) {
365+ return
366+ }
367+
368+ // Mark that we've asked to prevent multiple concurrent asks
369+ hasAskedForCommandOutput = true
370+
371+ try {
372+ const { response, text, images } = await task . ask ( "command_output" , "" )
373+ runInBackground = true
374+
375+ if ( response === "messageResponse" ) {
376+ message = { text, images }
377+ }
378+
379+ // Any answer means the command should keep running in the background;
380+ // continue the process so the tool resolves now instead of blocking
381+ // until the command actually completes.
382+ process . continue ( )
383+ } catch ( _error ) {
384+ // Silently handle ask errors (e.g., "Current ask promise was ignored")
385+ }
386+ }
387+
388+ const scheduleCommandOutputAsk = ( process : RooTerminalProcess ) : void => {
389+ if ( runInBackground || hasAskedForCommandOutput || completed || commandOutputAskTimer ) {
390+ return
391+ }
392+
393+ const remainingDelay = COMMAND_OUTPUT_ASK_DELAY_MS - ( Date . now ( ) - commandStartedAt )
394+
395+ commandOutputAskTimer = setTimeout (
396+ ( ) => {
397+ commandOutputAskTimer = undefined
398+ void askForCommandOutput ( process )
399+ } ,
400+ Math . max ( remainingDelay , 0 ) ,
401+ )
402+ }
403+
343404 const callbacks : RooTerminalCallbacks = {
344405 onLine : async ( lines : string , process : RooTerminalProcess ) => {
345406 accumulatedOutput += lines
@@ -359,26 +420,19 @@ export async function executeCommandInTerminal(
359420 provider ?. postMessageToWebview ( { type : "commandExecutionStatus" , text : JSON . stringify ( status ) } )
360421 schedulePartialCommandOutputUpdate ( )
361422
362- if ( runInBackground || hasAskedForCommandOutput ) {
363- return
364- }
365-
366- // Mark that we've asked to prevent multiple concurrent asks
367- hasAskedForCommandOutput = true
368-
369- try {
370- const { response, text, images } = await task . ask ( "command_output" , "" )
371- runInBackground = true
372-
373- if ( response === "messageResponse" ) {
374- message = { text, images }
375- process . continue ( )
376- }
377- } catch ( _error ) {
378- // Silently handle ask errors (e.g., "Current ask promise was ignored")
379- }
423+ scheduleCommandOutputAsk ( process )
380424 } ,
381425 onCompleted : async ( output : string | undefined ) => {
426+ clearTimeout ( commandOutputAskTimer )
427+ commandOutputAskTimer = undefined
428+
429+ // If an interactive command_output ask is still pending, supersede it
430+ // so it resolves immediately instead of lingering until the next
431+ // interactive message bumps lastMessageTs.
432+ if ( hasAskedForCommandOutput && ! runInBackground ) {
433+ task . supersedePendingAsk ( )
434+ }
435+
382436 clearTimeout ( pendingCommandOutputEmitTimer )
383437 pendingCommandOutputEmitTimer = undefined
384438
@@ -412,9 +466,21 @@ export async function executeCommandInTerminal(
412466 console . error ( "[ExecuteCommandTool] Failed to flush final command_output:" , error )
413467 } )
414468 } ,
415- onShellExecutionStarted : ( pid : number | undefined ) => {
469+ onShellExecutionStarted : ( pid : number | undefined , process : RooTerminalProcess ) => {
416470 const status : CommandExecutionStatus = { executionId, status : "started" , pid, command }
417471 provider ?. postMessageToWebview ( { type : "commandExecutionStatus" , text : JSON . stringify ( status ) } )
472+
473+ // Re-anchor the ask delay to actual execution start so the shell
474+ // integration startup wait does not count against the grace period.
475+ commandStartedAt = Date . now ( )
476+
477+ // Output should not precede this event, but if it did, reschedule
478+ // the pending ask against the corrected anchor.
479+ if ( commandOutputAskTimer ) {
480+ clearTimeout ( commandOutputAskTimer )
481+ commandOutputAskTimer = undefined
482+ scheduleCommandOutputAsk ( process )
483+ }
418484 } ,
419485 onShellExecutionComplete : ( details : ExitCodeDetails ) => {
420486 const status : CommandExecutionStatus = { executionId, status : "exited" , exitCode : details . exitCode }
@@ -441,6 +507,8 @@ export async function executeCommandInTerminal(
441507 workingDir = terminal . getCurrentWorkingDirectory ( )
442508 }
443509
510+ // Fallback anchor for providers that never fire onShellExecutionStarted.
511+ commandStartedAt = Date . now ( )
444512 const process = terminal . runCommand ( command , callbacks )
445513 task . terminalProcess = process
446514
@@ -462,6 +530,8 @@ export async function executeCommandInTerminal(
462530 new Promise < void > ( ( resolve ) => {
463531 agentTimeoutId = setTimeout ( ( ) => {
464532 runInBackground = true
533+ clearTimeout ( commandOutputAskTimer )
534+ commandOutputAskTimer = undefined
465535 process . continue ( )
466536 task . supersedePendingAsk ( )
467537 resolve ( )
@@ -501,6 +571,7 @@ export async function executeCommandInTerminal(
501571 } finally {
502572 clearTimeout ( agentTimeoutId )
503573 clearTimeout ( userTimeoutId )
574+ clearTimeout ( commandOutputAskTimer )
504575 clearTimeout ( pendingCommandOutputEmitTimer )
505576 task . terminalProcess = undefined
506577 }
0 commit comments