Skip to content

Commit 2a4cafc

Browse files
committed
fix(terminal): retry via execa silently when shell integration fails before submission
1 parent f03465a commit 2a4cafc

2 files changed

Lines changed: 58 additions & 13 deletions

File tree

src/core/tools/ExecuteCommandTool.ts

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ import { Task } from "../task/Task"
1212
import { ToolUse, ToolResponse } from "../../shared/tools"
1313
import { formatResponse } from "../prompts/responses"
1414
import { unescapeHtmlEntities } from "../../utils/text-normalization"
15-
import { ExitCodeDetails, RooTerminalCallbacks, RooTerminalProcess } from "../../integrations/terminal/types"
15+
import {
16+
ExitCodeDetails,
17+
RooTerminalCallbacks,
18+
RooTerminalProcess,
19+
ShellIntegrationErrorDetails,
20+
} from "../../integrations/terminal/types"
1621
import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry"
1722
import { Terminal } from "../../integrations/terminal/Terminal"
1823
import { OutputInterceptor } from "../../integrations/terminal/OutputInterceptor"
@@ -21,7 +26,18 @@ import { t } from "../../i18n"
2126
import { getTaskDirectoryPath } from "../../utils/storage"
2227
import { BaseTool, ToolCallbacks } from "./BaseTool"
2328

24-
class ShellIntegrationError extends Error {}
29+
export class ShellIntegrationError extends Error {
30+
constructor(
31+
message: string,
32+
public readonly commandSubmitted: boolean,
33+
) {
34+
super(message)
35+
}
36+
}
37+
38+
export function canRetryShellIntegrationError(error: unknown): error is ShellIntegrationError {
39+
return error instanceof ShellIntegrationError && !error.commandSubmitted
40+
}
2541

2642
interface ExecuteCommandParams {
2743
command: string
@@ -116,14 +132,14 @@ export class ExecuteCommandTool extends BaseTool<"execute_command"> {
116132

117133
pushToolResult(result)
118134
} catch (error: unknown) {
119-
const status: CommandExecutionStatus = { executionId, status: "fallback" }
120-
provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) })
121-
await task.say("shell_integration_warning")
122-
123135
// Invalidate pending ask from first execution to prevent race condition
124136
task.supersedePendingAsk()
125137

126-
if (error instanceof ShellIntegrationError) {
138+
if (canRetryShellIntegrationError(error)) {
139+
// Silent retry via execa — shell startup race, command was not submitted.
140+
const status: CommandExecutionStatus = { executionId, status: "fallback" }
141+
provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) })
142+
127143
const [rejected, result] = await executeCommandInTerminal(task, {
128144
...options,
129145
terminalShellIntegrationDisabled: true,
@@ -135,7 +151,16 @@ export class ExecuteCommandTool extends BaseTool<"execute_command"> {
135151

136152
pushToolResult(result)
137153
} else {
138-
pushToolResult(`Command failed to execute in terminal due to a shell integration error.`)
154+
// Command was submitted but shell integration lost track of it — show warning.
155+
await task.say("shell_integration_warning")
156+
157+
if (error instanceof ShellIntegrationError) {
158+
pushToolResult(
159+
"Command was submitted in the VS Code terminal, but shell integration did not report its output or completion status. Do not run the command again automatically.",
160+
)
161+
} else {
162+
pushToolResult(`Command failed to execute in terminal due to a shell integration error.`)
163+
}
139164
}
140165
}
141166

@@ -196,12 +221,20 @@ export async function executeCommandInTerminal(
196221
let result: string = ""
197222
let persistedResult: PersistedCommandOutput | undefined
198223
let exitDetails: ExitCodeDetails | undefined
199-
let shellIntegrationError: string | undefined
224+
let shellIntegrationError: ShellIntegrationError | undefined
200225
let hasAskedForCommandOutput = false
201226

202-
const terminalProvider = terminalShellIntegrationDisabled ? "execa" : "vscode"
227+
const isCmdExeFallback = !terminalShellIntegrationDisabled && Terminal.isActiveShellCmdExe()
228+
const terminalProvider = terminalShellIntegrationDisabled || isCmdExeFallback ? "execa" : "vscode"
203229
const provider = await task.providerRef.deref()
204230

231+
// cmd.exe can't use shell integration — tell the webview to expand the output
232+
// panel immediately (same effect as the retry-fallback path).
233+
if (isCmdExeFallback) {
234+
const status: CommandExecutionStatus = { executionId, status: "fallback" }
235+
provider?.postMessageToWebview({ type: "commandExecutionStatus", text: JSON.stringify(status) })
236+
}
237+
205238
// Get global storage path for persisted output artifacts
206239
const globalStoragePath = provider?.context?.globalStorageUri?.fsPath
207240
let interceptor: OutputInterceptor | undefined
@@ -360,9 +393,9 @@ export async function executeCommandInTerminal(
360393
}
361394

362395
if (terminalProvider === "vscode") {
363-
callbacks.onNoShellIntegration = async (error: string) => {
396+
callbacks.onNoShellIntegration = async (details: ShellIntegrationErrorDetails) => {
364397
TelemetryService.instance.captureShellIntegrationError(task.taskId)
365-
shellIntegrationError = error
398+
shellIntegrationError = new ShellIntegrationError(details.message, details.commandSubmitted)
366399
}
367400
}
368401

@@ -442,7 +475,7 @@ export async function executeCommandInTerminal(
442475
}
443476

444477
if (shellIntegrationError) {
445-
throw new ShellIntegrationError(shellIntegrationError)
478+
throw shellIntegrationError
446479
}
447480

448481
// Wait for a short delay to ensure all messages are sent to the webview.

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,18 @@ describe("executeCommandTool", () => {
256256
expect(mockAskApproval).not.toHaveBeenCalled()
257257
// executeCommandInTerminal should not be called since rooignore blocked it
258258
})
259+
260+
it("allows Execa retry when shell integration fails before command submission", () => {
261+
const error = new executeCommandModule.ShellIntegrationError("startup failed", false)
262+
263+
expect(executeCommandModule.canRetryShellIntegrationError(error)).toBe(true)
264+
})
265+
266+
it("prevents Execa retry when shell integration fails after command submission", () => {
267+
const error = new executeCommandModule.ShellIntegrationError("stream missing", true)
268+
269+
expect(executeCommandModule.canRetryShellIntegrationError(error)).toBe(false)
270+
})
259271
})
260272

261273
describe("Command execution timeout configuration", () => {

0 commit comments

Comments
 (0)