diff --git a/packages/core/src/codewhispererChat/tools/executeBash.ts b/packages/core/src/codewhispererChat/tools/executeBash.ts index 616ea49ed47..605d6bbeb8f 100644 --- a/packages/core/src/codewhispererChat/tools/executeBash.ts +++ b/packages/core/src/codewhispererChat/tools/executeBash.ts @@ -117,6 +117,14 @@ export interface CommandValidation { warning?: string } +// Interface for timestamped output chunks +interface TimestampedChunk { + timestamp: number + isStdout: boolean + content: string + isFirst: boolean +} + export class ExecuteBash { private readonly command: string private readonly workingDirectory?: string @@ -207,8 +215,40 @@ export class ExecuteBash { const stdoutBuffer: string[] = [] const stderrBuffer: string[] = [] - let firstChunk = true - let firstStderrChunk = true + // Use a closure boolean value firstChunk and a function to get and set its value + let isFirstChunk = true + const getAndSetFirstChunk = (newValue: boolean): boolean => { + const oldValue = isFirstChunk + isFirstChunk = newValue + return oldValue + } + + // Use a queue to maintain chronological order of chunks + // This ensures that the output is processed in the exact order it was generated by the child process. + const outputQueue: TimestampedChunk[] = [] + let processingQueue = false + + // Process the queue in order + const processQueue = () => { + if (processingQueue || outputQueue.length === 0) { + return + } + + processingQueue = true + + try { + // Sort by timestamp to ensure chronological order + outputQueue.sort((a, b) => a.timestamp - b.timestamp) + + while (outputQueue.length > 0) { + const chunk = outputQueue.shift()! + ExecuteBash.handleTimestampedChunk(chunk, stdoutBuffer, stderrBuffer, updates) + } + } finally { + processingQueue = false + } + } + const childProcessOptions: ChildProcessOptions = { spawnOptions: { cwd: this.workingDirectory, @@ -216,13 +256,27 @@ export class ExecuteBash { }, collect: false, waitForStreams: true, - onStdout: (chunk: string) => { - ExecuteBash.handleChunk(firstChunk ? '```console\n' + chunk : chunk, stdoutBuffer, updates) - firstChunk = false + onStdout: async (chunk: string) => { + const isFirst = getAndSetFirstChunk(false) + const timestamp = Date.now() + outputQueue.push({ + timestamp, + isStdout: true, + content: chunk, + isFirst, + }) + processQueue() }, - onStderr: (chunk: string) => { - ExecuteBash.handleChunk(firstStderrChunk ? '```console\n' + chunk : chunk, stderrBuffer, updates) - firstStderrChunk = false + onStderr: async (chunk: string) => { + const isFirst = getAndSetFirstChunk(false) + const timestamp = Date.now() + outputQueue.push({ + timestamp, + isStdout: false, + content: chunk, + isFirst, + }) + processQueue() }, } @@ -261,6 +315,17 @@ export class ExecuteBash { }) } + private static handleTimestampedChunk( + chunk: TimestampedChunk, + stdoutBuffer: string[], + stderrBuffer: string[], + updates?: Writable + ): void { + const buffer = chunk.isStdout ? stdoutBuffer : stderrBuffer + const content = chunk.isFirst ? '```console\n' + chunk.content : chunk.content + ExecuteBash.handleChunk(content, buffer, updates) + } + private static handleChunk(chunk: string, buffer: string[], updates?: Writable) { try { updates?.write(chunk)