@@ -945,15 +945,14 @@ export const AppContainer = (props: AppContainerProps) => {
945945 historyManagerRef . current = historyManager ;
946946 const submitQueryRef = useRef ( submitQuery ) ;
947947 submitQueryRef . current = submitQuery ;
948- const streamingStateRef = useRef ( streamingState ) ;
949- streamingStateRef . current = streamingState ;
950948
951- // Pending loop prompt waiting for Idle before submission (set when timer
952- // fires during active streaming).
953- const loopSubmitRef = useRef < Array < { text : string } > | null > ( null ) ;
949+ // Pending loop prompt (string) waiting for Idle before submission.
950+ // Using state (not ref) so that setting it triggers a re-render and the
951+ // drain effect fires — even when streamingState is already Idle.
952+ const [ pendingLoopPrompt , setPendingLoopPrompt ] = useState < string | null > (
953+ null ,
954+ ) ;
954955 // Whether the current streaming response was initiated by the loop.
955- // Set true when a loop prompt is submitted; cleared by handleFinalSubmit
956- // (user-initiated) or when the completion effect processes the response.
957956 const loopInitiatedStreamRef = useRef ( false ) ;
958957
959958 useEffect ( ( ) => {
@@ -978,17 +977,10 @@ export const AppContainer = (props: AppContainerProps) => {
978977 } ,
979978 Date . now ( ) ,
980979 ) ;
981- // Submit as Part[] to bypass slash-command parsing (consistent with
982- // the first-iteration submit_prompt path in loopCommand.ts).
983- const parts : Array < { text : string } > = [ { text : prompt } ] ;
984- if ( streamingStateRef . current === StreamingState . Idle ) {
985- // Common path: timer fires while idle — submit immediately.
986- loopInitiatedStreamRef . current = true ;
987- void submitQueryRef . current ( parts ) ;
988- } else {
989- // Edge case: timer fires during active streaming — queue for later.
990- loopSubmitRef . current = parts ;
991- }
980+ // Queue the prompt as a string. Submitting as string (not Part[])
981+ // allows nested slash commands (e.g. /loop 5m /review) to be parsed
982+ // and executed on each iteration via isSlashCommand().
983+ setPendingLoopPrompt ( prompt ) ;
992984 } ) ;
993985 return ( ) => {
994986 const lm = getLoopManager ( ) ;
@@ -1012,13 +1004,9 @@ export const AppContainer = (props: AppContainerProps) => {
10121004 return ;
10131005 }
10141006 // loopInitiatedStreamRef is true for iterations submitted via the
1015- // callback (2nd+). For the FIRST iteration, submitted via the
1016- // slash-command's submit_prompt return value, the ref is never set —
1017- // but waitingForResponse is already true from manager.start(), so we
1018- // accept it as a loop response when no queued prompt is pending.
1019- if ( ! loopInitiatedStreamRef . current && loopSubmitRef . current !== null ) {
1020- // A queued prompt exists but hasn't been submitted yet — the stream
1021- // that just finished is NOT from the loop (e.g. user's message).
1007+ // drain effect. When false but a pending prompt exists, the stream
1008+ // that just finished is NOT from the loop (e.g. user's message).
1009+ if ( ! loopInitiatedStreamRef . current && pendingLoopPrompt !== null ) {
10221010 return ;
10231011 }
10241012 loopInitiatedStreamRef . current = false ;
@@ -1092,21 +1080,21 @@ export const AppContainer = (props: AppContainerProps) => {
10921080 // eslint-disable-next-line react-hooks/exhaustive-deps -- only run on streamingState transitions
10931081 } , [ streamingState ] ) ;
10941082
1095- // Drain queued loop prompt when streaming becomes Idle.
1096- // This only fires for the edge case where the timer fired during streaming .
1083+ // Drain pending loop prompt when streaming is Idle.
1084+ // Submits as string so nested slash commands (e.g. /review) are parsed .
10971085 useEffect ( ( ) => {
1098- if ( streamingState !== StreamingState . Idle || ! loopSubmitRef . current ) {
1086+ if ( streamingState !== StreamingState . Idle || pendingLoopPrompt === null ) {
10991087 return ;
11001088 }
1101- const parts = loopSubmitRef . current ;
1102- loopSubmitRef . current = null ;
1089+ const prompt = pendingLoopPrompt ;
1090+ setPendingLoopPrompt ( null ) ;
11031091 // If the loop was stopped while the prompt was queued, discard it.
11041092 if ( ! getLoopManager ( ) . isActive ( ) ) {
11051093 return ;
11061094 }
11071095 loopInitiatedStreamRef . current = true ;
1108- void submitQueryRef . current ( parts ) ;
1109- } , [ streamingState ] ) ;
1096+ void submitQueryRef . current ( prompt ) ;
1097+ } , [ streamingState , pendingLoopPrompt ] ) ;
11101098
11111099 const [ idePromptAnswered , setIdePromptAnswered ] = useState ( false ) ;
11121100 const [ currentIDE , setCurrentIDE ] = useState < IdeInfo | null > ( null ) ;
0 commit comments