@@ -8,6 +8,7 @@ import type {
88 RooTerminalProcess ,
99 RooTerminalProcessResultPromise ,
1010 ExitCodeDetails ,
11+ CompoundProcessCompletion ,
1112} from "./types"
1213
1314export abstract class BaseTerminal implements RooTerminal {
@@ -23,13 +24,21 @@ export abstract class BaseTerminal implements RooTerminal {
2324 public process ?: RooTerminalProcess
2425 public completedProcesses : RooTerminalProcess [ ] = [ ]
2526
27+ // Compound command tracking
28+ public isCompoundCommand : boolean = false
29+ public compoundProcessCompletions : CompoundProcessCompletion [ ] = [ ]
30+ private expectedCompoundProcessCount : number = 0
31+ private compoundCommandWaitTimeout ?: NodeJS . Timeout
32+
2633 constructor ( provider : RooTerminalProvider , id : number , cwd : string ) {
2734 this . provider = provider
2835 this . id = id
2936 this . initialCwd = cwd
3037 this . busy = false
3138 this . running = false
3239 this . streamClosed = false
40+ this . isCompoundCommand = false
41+ this . compoundProcessCompletions = [ ]
3342 }
3443
3544 public getCurrentWorkingDirectory ( ) : string {
@@ -66,6 +75,156 @@ export abstract class BaseTerminal implements RooTerminal {
6675 }
6776 }
6877
78+ /**
79+ * Detects if a command is a compound command (contains operators like &&, ||, ;)
80+ * @param command The command to check
81+ */
82+ public detectCompoundCommand ( command : string ) : void {
83+ // Common shell operators that create compound commands
84+ const compoundOperators = [ "&&" , "||" , ";" , "|" , "&" ]
85+
86+ // Check if command contains any compound operators
87+ this . isCompoundCommand = compoundOperators . some ( ( op ) => command . includes ( op ) )
88+
89+ if ( this . isCompoundCommand ) {
90+ // Estimate the number of processes (this is a heuristic, not exact)
91+ // For && and ||, each operator adds one process
92+ // For ;, each semicolon adds one process
93+ // For |, each pipe adds one process
94+ // For &, it's a background process indicator
95+ let processCount = 1
96+
97+ // Count && and || operators
98+ const andMatches = command . match ( / & & / g)
99+ const orMatches = command . match ( / \| \| / g)
100+ const semiMatches = command . match ( / ; / g)
101+ const pipeMatches = command . match ( / \| (? ! \| ) / g) // Match single | but not ||
102+ const bgMatches = command . match ( / & (? ! & ) / g) // Match single & but not &&
103+
104+ if ( andMatches ) processCount += andMatches . length
105+ if ( orMatches ) processCount += orMatches . length
106+ if ( semiMatches ) processCount += semiMatches . length
107+ if ( pipeMatches ) processCount += pipeMatches . length
108+ if ( bgMatches ) processCount += bgMatches . length
109+
110+ this . expectedCompoundProcessCount = processCount
111+
112+ console . info (
113+ `[Terminal ${ this . id } ] Detected compound command with estimated ${ processCount } processes:` ,
114+ command ,
115+ )
116+
117+ // Set a timeout to handle cases where we don't receive all expected completions
118+ this . compoundCommandWaitTimeout = setTimeout ( ( ) => {
119+ if ( this . compoundProcessCompletions . length > 0 ) {
120+ console . warn (
121+ `[Terminal ${ this . id } ] Compound command timeout - processing ${ this . compoundProcessCompletions . length } completions` ,
122+ )
123+ this . finalizeCompoundCommand ( )
124+ }
125+ } , 10000 ) // 10 second timeout for compound commands
126+ } else {
127+ this . compoundProcessCompletions = [ ]
128+ this . expectedCompoundProcessCount = 0
129+ }
130+ }
131+
132+ /**
133+ * Adds a compound process completion
134+ * @param exitDetails The exit details of the completed process
135+ * @param command The command that completed
136+ */
137+ public addCompoundProcessCompletion ( exitDetails : ExitCodeDetails , command : string ) : void {
138+ if ( ! this . isCompoundCommand ) {
139+ console . warn ( `[Terminal ${ this . id } ] Received compound process completion but not tracking compound command` )
140+ return
141+ }
142+
143+ this . compoundProcessCompletions . push ( {
144+ exitDetails,
145+ command,
146+ timestamp : Date . now ( ) ,
147+ } )
148+
149+ console . info (
150+ `[Terminal ${ this . id } ] Added compound process completion ${ this . compoundProcessCompletions . length } /${ this . expectedCompoundProcessCount } :` ,
151+ command ,
152+ )
153+
154+ // Check if all expected processes have completed
155+ if ( this . compoundProcessCompletions . length >= this . expectedCompoundProcessCount ) {
156+ console . info ( `[Terminal ${ this . id } ] All compound processes complete, finalizing` )
157+ this . finalizeCompoundCommand ( )
158+ }
159+ }
160+
161+ /**
162+ * Checks if all compound processes have completed
163+ * @returns True if all expected processes have completed
164+ */
165+ public allCompoundProcessesComplete ( ) : boolean {
166+ // If we're not tracking a compound command, consider it complete
167+ if ( ! this . isCompoundCommand ) {
168+ return true
169+ }
170+
171+ // Check if we've received completions for all expected processes
172+ // We use >= because sometimes we might get more completions than expected
173+ const isComplete = this . compoundProcessCompletions . length >= this . expectedCompoundProcessCount
174+
175+ console . info (
176+ `[Terminal ${ this . id } ] Checking compound completion: ${ this . compoundProcessCompletions . length } /${ this . expectedCompoundProcessCount } = ${ isComplete } ` ,
177+ )
178+
179+ return isComplete
180+ }
181+
182+ /**
183+ * Gets the combined output from all compound processes
184+ * @returns The combined output string
185+ */
186+ public getCompoundProcessOutputs ( ) : string {
187+ // Combine outputs from all completed processes
188+ const outputs : string [ ] = [ ]
189+
190+ for ( const completion of this . compoundProcessCompletions ) {
191+ outputs . push ( `[Command: ${ completion . command } ]` )
192+ outputs . push ( `[Exit Code: ${ completion . exitDetails . exitCode } ]` )
193+ if ( completion . exitDetails . signalName ) {
194+ outputs . push ( `[Signal: ${ completion . exitDetails . signalName } ]` )
195+ }
196+ }
197+
198+ return outputs . join ( "\n" )
199+ }
200+
201+ /**
202+ * Finalizes a compound command execution
203+ */
204+ private finalizeCompoundCommand ( ) : void {
205+ // Clear the timeout if it exists
206+ if ( this . compoundCommandWaitTimeout ) {
207+ clearTimeout ( this . compoundCommandWaitTimeout )
208+ this . compoundCommandWaitTimeout = undefined
209+ }
210+
211+ // Get the last exit details (from the final process in the chain)
212+ const lastCompletion = this . compoundProcessCompletions [ this . compoundProcessCompletions . length - 1 ]
213+ const finalExitDetails = lastCompletion ?. exitDetails || { exitCode : 0 }
214+
215+ console . info (
216+ `[Terminal ${ this . id } ] Finalizing compound command with ${ this . compoundProcessCompletions . length } processes` ,
217+ )
218+
219+ // Reset compound command tracking
220+ this . isCompoundCommand = false
221+ this . compoundProcessCompletions = [ ]
222+ this . expectedCompoundProcessCount = 0
223+
224+ // Complete the terminal process with the final exit details
225+ this . shellExecutionComplete ( finalExitDetails )
226+ }
227+
69228 /**
70229 * Handles shell execution completion for this terminal.
71230 * @param exitDetails The exit details of the shell execution
0 commit comments