@@ -4,6 +4,7 @@ import { createInterface } from "readline"
44import { fileURLToPath } from "url"
55
66import { createElement } from "react"
7+ import pWaitFor from "p-wait-for"
78
89import { setLogger } from "@roo-code/vscode-shim"
910
@@ -306,15 +307,149 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption
306307
307308 if ( useStdinPromptStream ) {
308309 let hasReceivedStdinPrompt = false
310+ // stdin stream mode may start at most one task in this process.
311+ let startedTaskFromStdin = false
312+ let activeTaskPromise : Promise < void > | null = null
313+ let fatalStreamError : Error | null = null
314+ // Extension-owned queue depth mirrored from state pushes.
315+ // CLI does not maintain its own prompt queue.
316+ let extensionQueueDepth = 0
317+
318+ const waitForInitialState = async ( ) => {
319+ // Give the extension a brief chance to publish initial state so
320+ // we can continue an existing task instead of creating a new one.
321+ await pWaitFor (
322+ ( ) => {
323+ if ( fatalStreamError ) {
324+ throw fatalStreamError
325+ }
326+
327+ return host . client . isInitialized ( )
328+ } ,
329+ { interval : 25 , timeout : 2_000 } ,
330+ ) . catch ( ( ) => {
331+ // Best-effort wait only; continuing preserves previous behavior.
332+ } )
333+
334+ if ( fatalStreamError ) {
335+ throw fatalStreamError
336+ }
337+ }
309338
310- for await ( const stdinPrompt of readPromptsFromStdinLines ( ) ) {
311- hasReceivedStdinPrompt = true
312- await host . runTask ( stdinPrompt )
313- jsonEmitter ?. clear ( )
339+ const waitForActiveTask = async ( ) => {
340+ await pWaitFor (
341+ ( ) => {
342+ if ( fatalStreamError ) {
343+ throw fatalStreamError
344+ }
345+
346+ if ( ! host . client . hasActiveTask ( ) ) {
347+ if ( ! activeTaskPromise && startedTaskFromStdin ) {
348+ throw new Error ( "task is no longer active; cannot continue conversation from stdin" )
349+ }
350+
351+ return false
352+ }
353+
354+ return true
355+ } ,
356+ { interval : 25 , timeout : 5_000 } ,
357+ )
314358 }
315359
316- if ( ! hasReceivedStdinPrompt ) {
317- throw new Error ( "no prompt provided via stdin" )
360+ const startInitialTask = async ( taskPrompt : string ) => {
361+ startedTaskFromStdin = true
362+
363+ activeTaskPromise = host
364+ . runTask ( taskPrompt )
365+ . catch ( ( error ) => {
366+ fatalStreamError = error instanceof Error ? error : new Error ( String ( error ) )
367+ } )
368+ . finally ( ( ) => {
369+ activeTaskPromise = null
370+ } )
371+
372+ await waitForActiveTask ( )
373+ }
374+
375+ const enqueueContinuation = async ( text : string ) => {
376+ if ( ! host . client . hasActiveTask ( ) ) {
377+ await waitForActiveTask ( )
378+ }
379+
380+ // Delegate ordering/drain behavior to the extension message queue.
381+ host . sendToExtension ( { type : "queueMessage" , text } )
382+ }
383+
384+ const offClientError = host . client . on ( "error" , ( error ) => {
385+ fatalStreamError = error
386+ } )
387+
388+ const onExtensionMessage = ( message : { type ?: string ; state ?: { messageQueue ?: unknown } } ) => {
389+ if ( message . type !== "state" ) {
390+ return
391+ }
392+
393+ const messageQueue = message . state ?. messageQueue
394+ extensionQueueDepth = Array . isArray ( messageQueue ) ? messageQueue . length : 0
395+ }
396+
397+ host . on ( "extensionWebviewMessage" , onExtensionMessage )
398+
399+ try {
400+ await waitForInitialState ( )
401+
402+ for await ( const stdinPrompt of readPromptsFromStdinLines ( ) ) {
403+ hasReceivedStdinPrompt = true
404+
405+ // Start once, then always continue via extension queue.
406+ if ( ! host . client . hasActiveTask ( ) && ! startedTaskFromStdin ) {
407+ await startInitialTask ( stdinPrompt )
408+ } else {
409+ await enqueueContinuation ( stdinPrompt )
410+ }
411+
412+ if ( fatalStreamError ) {
413+ throw fatalStreamError
414+ }
415+ }
416+
417+ if ( ! hasReceivedStdinPrompt ) {
418+ throw new Error ( "no prompt provided via stdin" )
419+ }
420+
421+ await pWaitFor (
422+ ( ) => {
423+ if ( fatalStreamError ) {
424+ throw fatalStreamError
425+ }
426+
427+ const isSettled =
428+ ! host . client . hasActiveTask ( ) && ! activeTaskPromise && extensionQueueDepth === 0
429+
430+ if ( isSettled ) {
431+ return true
432+ }
433+
434+ if ( host . isWaitingForInput ( ) && extensionQueueDepth === 0 ) {
435+ const currentAsk = host . client . getCurrentAsk ( )
436+
437+ if ( currentAsk === "completion_result" ) {
438+ return true
439+ }
440+
441+ if ( currentAsk ) {
442+ throw new Error ( `stdin ended while task was waiting for input (${ currentAsk } )` )
443+ }
444+ }
445+
446+ return false
447+ } ,
448+ { interval : 50 } ,
449+ )
450+ } finally {
451+ offClientError ( )
452+ host . off ( "extensionWebviewMessage" , onExtensionMessage )
318453 }
319454 } else {
320455 await host . runTask ( prompt ! )
@@ -331,6 +466,7 @@ export async function run(promptArg: string | undefined, flagOptions: FlagOption
331466 process . stdout . write ( JSON . stringify ( errorEvent ) + "\n" )
332467 } else {
333468 console . error ( "[CLI] Error:" , errorMessage )
469+
334470 if ( error instanceof Error ) {
335471 console . error ( error . stack )
336472 }
0 commit comments