Skip to content

Commit faa933e

Browse files
committed
fix(terminal): address CodeRabbit review findings
1 parent 987bfbb commit faa933e

4 files changed

Lines changed: 33 additions & 8 deletions

File tree

apps/vscode-e2e/src/suite/tools/fast-exit-shell-race.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ suite("Fast-exit shell integration race", function () {
9090
},
9191
text: "FAST_EXIT_SHELL_RACE_E2E",
9292
}),
93-
timeout: 100_000, // TEMP: diagnostic probe, see if the marker ever arrives given patience
93+
timeout: 60_000,
9494
})
9595

9696
const elapsedMs = Date.now() - startedAt

src/core/tools/__tests__/executeCommandTool.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -187,12 +187,12 @@ describe("executeCommandTool", () => {
187187
pushToolResult: mockPushToolResult as unknown as PushToolResult,
188188
})
189189

190-
// Verify - confirm the command was approved and result was pushed
191-
// The custom path handling is tested in integration tests
190+
// Verify - command approved, result pushed, and custom cwd passed to terminal
192191
expect(mockAskApproval).toHaveBeenCalledWith("command", "echo test")
193192
expect(mockPushToolResult).toHaveBeenCalled()
194-
const result = mockPushToolResult.mock.calls[0][0]
195-
expect(result).toContain("Command")
193+
const { TerminalRegistry } = await import("../../../integrations/terminal/TerminalRegistry")
194+
const firstArg = (TerminalRegistry.getOrCreateTerminal as ReturnType<typeof vitest.fn>).mock.calls[0][0]
195+
expect(firstArg).toBe("/custom/path")
196196
})
197197
})
198198

src/integrations/terminal/TerminalProcess.ts

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,8 +290,12 @@ export class TerminalProcess extends BaseTerminalProcess {
290290
// Only applies before any data arrives (chunkCount === 0).
291291
const IDLE_TIMEOUT_MS = 3_000
292292

293+
// Hoist nextChunk outside the loop so that re-arming after an idle timeout
294+
// reuses the same pending .next() promise instead of issuing a second call
295+
// while the first is still unresolved (violates the async-iterator protocol
296+
// and silently drops the first output chunk).
297+
let nextChunk = iterator.next()
293298
while (true) {
294-
const nextChunk = iterator.next()
295299
const racers: Promise<typeof DONE_SENTINEL | typeof IDLE_SENTINEL | IteratorResult<string>>[] = [
296300
nextChunk,
297301
shellExecutionComplete.then(() => DONE_SENTINEL as typeof DONE_SENTINEL),
@@ -325,6 +329,7 @@ export class TerminalProcess extends BaseTerminalProcess {
325329
// silent command like `sleep 5` can legitimately produce zero output —
326330
// elapsed time alone is not proof of completion. Re-arm the idle timer
327331
// and keep waiting for a real chunk, the D marker, or the end event.
332+
// Reuse the existing nextChunk promise — do NOT call iterator.next() again.
328333
console.info(
329334
`[Terminal Process] idle timeout fired but shell execution is running — re-arming (${chunkCount} chunks so far)`,
330335
)
@@ -396,6 +401,11 @@ export class TerminalProcess extends BaseTerminalProcess {
396401
)
397402
break
398403
}
404+
405+
// Advance to the next chunk only after the current one has been fully
406+
// consumed. Calling iterator.next() here (not at the top of the loop)
407+
// ensures there is never more than one pending .next() call at a time.
408+
nextChunk = iterator.next()
399409
}
400410

401411
if (!sawEndMarker && !streamEndedByEvent) {
@@ -546,7 +556,9 @@ export class TerminalProcess extends BaseTerminalProcess {
546556
return `begin\n${command}\nend`
547557
}
548558

549-
const scriptPath = path.join(os.tmpdir(), `roo-cmd-${Date.now()}-${Math.random().toString(36).slice(2)}.sh`)
559+
const scriptDir = fs.mkdtempSync(path.join(os.tmpdir(), "roo-cmd-"))
560+
this.scriptDir = scriptDir
561+
const scriptPath = path.join(scriptDir, "cmd.sh")
550562
fs.writeFileSync(scriptPath, command, { mode: 0o700 })
551563
this.scriptPath = scriptPath
552564

@@ -556,6 +568,7 @@ export class TerminalProcess extends BaseTerminalProcess {
556568
}
557569

558570
private scriptPath: string | undefined
571+
private scriptDir: string | undefined
559572

560573
private cleanupScriptFile() {
561574
if (this.scriptPath) {
@@ -566,6 +579,14 @@ export class TerminalProcess extends BaseTerminalProcess {
566579
}
567580
this.scriptPath = undefined
568581
}
582+
if (this.scriptDir) {
583+
try {
584+
fs.rmdirSync(this.scriptDir)
585+
} catch {
586+
// Best-effort: ignore if already removed or non-empty.
587+
}
588+
this.scriptDir = undefined
589+
}
569590
}
570591

571592
public override continue() {

src/integrations/terminal/TerminalRegistry.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,11 @@ export class TerminalRegistry {
6969
const process = terminal.process
7070
const isOwnExecution =
7171
!(process instanceof TerminalProcess) ||
72-
process.ownExecution === undefined ||
72+
// Allow undefined only when the process hasn't started yet (cold
73+
// terminal: process is assigned but run() hasn't called executeCommand).
74+
// Once isHot is true, ownExecution is always set — a stale start
75+
// event on a reused terminal must match exactly.
76+
(!process.isHot && process.ownExecution === undefined) ||
7377
process.ownExecution === e.execution
7478
if (!isOwnExecution) {
7579
console.info(

0 commit comments

Comments
 (0)