diff --git a/core/tools/implementations/runTerminalCommand.ts b/core/tools/implementations/runTerminalCommand.ts index 351ee75cae6..f675818c25d 100644 --- a/core/tools/implementations/runTerminalCommand.ts +++ b/core/tools/implementations/runTerminalCommand.ts @@ -93,17 +93,14 @@ const getColorEnv = () => ({ CLICOLOR_FORCE: "1", }); -const ENABLED_FOR_REMOTES = [ - "", - "local", - "wsl", - "dev-container", - "devcontainer", - "ssh-remote", - "attached-container", - "codespaces", - "tunnel", -]; +// Only spawn processes locally when there is no remote workspace. +// With extensionKind: ["ui", "workspace"], the extension host almost always +// runs on the local machine. childProcess.spawn() executes on the extension +// host, so for any remote workspace it would run commands on the wrong machine +// (or fail with ENOENT when the local shell doesn't match the remote OS). +// All remote types delegate to ide.runCommand() which routes through VS Code's +// integrated terminal and executes in the correct remote environment. +const LOCAL_ONLY = ["", "local"]; export const runTerminalCommandImpl: ToolImpl = async (args, extras) => { const command = getStringArg(args, "command"); @@ -114,17 +111,7 @@ export const runTerminalCommandImpl: ToolImpl = async (args, extras) => { const ideInfo = await extras.ide.getIdeInfo(); const toolCallId = extras.toolCallId || ""; - // When the extension host runs on Windows but connects to a remote workspace - // (WSL, Dev Container, SSH, etc.), we can't spawn shells directly — the - // platform is "win32" but commands should run in the remote's Linux/macOS. - // Use ide.runCommand() instead to let VS Code handle the remote execution. - const isWindowsHostWithRemote = - process.platform === "win32" && !["", "local"].includes(ideInfo.remoteName); - - if ( - ENABLED_FOR_REMOTES.includes(ideInfo.remoteName) && - !isWindowsHostWithRemote - ) { + if (LOCAL_ONLY.includes(ideInfo.remoteName)) { // For streaming output if (extras.onPartialOutput) { try { @@ -453,16 +440,17 @@ export const runTerminalCommandImpl: ToolImpl = async (args, extras) => { } } - // For remote environments, just run the command - // Note: waitForCompletion is not supported in remote environments yet + // For remote environments (SSH, WSL, Dev Container, Codespaces, etc.), + // delegate to VS Code's integrated terminal which handles remote execution. + // Note: output capture and waitForCompletion are not yet supported for remotes. await extras.ide.runCommand(command); return [ { name: "Terminal", description: "Terminal command output", content: - "Terminal output not available. This is only available in local development environments and not in SSH environments for example.", - status: "Command failed", + "Command executed in remote terminal. Output capture is not yet available for remote environments.", + status: "Command executed", }, ]; }; diff --git a/core/tools/implementations/runTerminalCommand.vitest.ts b/core/tools/implementations/runTerminalCommand.vitest.ts index ea502f7c015..1f5b668db9e 100644 --- a/core/tools/implementations/runTerminalCommand.vitest.ts +++ b/core/tools/implementations/runTerminalCommand.vitest.ts @@ -271,11 +271,8 @@ describe("runTerminalCommandImpl", () => { // In remote environments, it should use the IDE's runCommand expect(mockRunCommand).toHaveBeenCalledWith("echo 'test'"); - // Match the actual output message - expect(result[0].content).toContain("Terminal output not available"); - expect(result[0].content).toContain("SSH environments"); - // Verify status field indicates command failed in remote environments - expect(result[0].status).toBe("Command failed"); + expect(result[0].content).toContain("Command executed in remote terminal"); + expect(result[0].status).toBe("Command executed"); }); it("should handle errors when executing invalid commands", async () => { @@ -608,7 +605,7 @@ describe("runTerminalCommandImpl", () => { }); describe("remote environment handling", () => { - it("should use ide.runCommand for non-enabled remote environments", async () => { + it("should use ide.runCommand for remote environments", async () => { const extras = createMockExtras({ remoteName: "some-unsupported-remote", }); @@ -619,7 +616,9 @@ describe("runTerminalCommandImpl", () => { ); expect(mockRunCommand).toHaveBeenCalledWith("echo test"); - expect(result[0].content).toContain("Terminal output not available"); + expect(result[0].content).toContain( + "Command executed in remote terminal", + ); }); it("should handle local environment with file URIs", async () => { @@ -633,50 +632,33 @@ describe("runTerminalCommandImpl", () => { ).resolves.toBeDefined(); }); - it("should handle WSL environment", async () => { - mockGetWorkspaceDirs.mockResolvedValue(["file:///home/user/workspace"]); - - await expect( - runTerminalCommandImpl( - { command: "echo test", waitForCompletion: false }, - createMockExtras({ remoteName: "wsl" }), - ), - ).resolves.toBeDefined(); - }); + it("should use ide.runCommand for WSL environment", async () => { + // WSL is a remote — commands should execute in WSL, not on the host + const extras = createMockExtras({ remoteName: "wsl" }); - it("should use ide.runCommand when Windows host connects to WSL", async () => { - // When extension runs on Windows but connects to WSL, we can't spawn - // shells directly - must use ide.runCommand instead - const originalPlatform = process.platform; - Object.defineProperty(process, "platform", { value: "win32" }); - - try { - const extras = createMockExtras({ remoteName: "wsl" }); - - const result = await runTerminalCommandImpl( - { command: "echo test" }, - extras, - ); + const result = await runTerminalCommandImpl( + { command: "echo test" }, + extras, + ); - // Should fall back to ide.runCommand, not try to spawn powershell.exe - expect(mockRunCommand).toHaveBeenCalledWith("echo test"); - expect(result[0].content).toContain("Terminal output not available"); - } finally { - Object.defineProperty(process, "platform", { - value: originalPlatform, - }); - } + expect(mockRunCommand).toHaveBeenCalledWith("echo test"); + expect(result[0].content).toContain( + "Command executed in remote terminal", + ); }); - it("should handle dev-container environment", async () => { - mockGetWorkspaceDirs.mockResolvedValue(["file:///workspace"]); + it("should use ide.runCommand for dev-container environment", async () => { + const extras = createMockExtras({ remoteName: "dev-container" }); - await expect( - runTerminalCommandImpl( - { command: "echo test", waitForCompletion: false }, - createMockExtras({ remoteName: "dev-container" }), - ), - ).resolves.toBeDefined(); + const result = await runTerminalCommandImpl( + { command: "echo test" }, + extras, + ); + + expect(mockRunCommand).toHaveBeenCalledWith("echo test"); + expect(result[0].content).toContain( + "Command executed in remote terminal", + ); }); });