diff --git a/core/tools/implementations/runTerminalCommand.ts b/core/tools/implementations/runTerminalCommand.ts index 8f6201bd12c..bda8775ccca 100644 --- a/core/tools/implementations/runTerminalCommand.ts +++ b/core/tools/implementations/runTerminalCommand.ts @@ -2,6 +2,16 @@ import iconv from "iconv-lite"; import childProcess from "node:child_process"; import os from "node:os"; import { ContinueError, ContinueErrorReason } from "../../util/errors"; +import { fileURLToPath } from "node:url"; +import { ToolImpl } from "."; +import { + isProcessBackgrounded, + markProcessAsRunning, + removeBackgroundedProcess, + removeRunningProcess, + updateProcessOutput, +} from "../../util/processTerminalStates"; +import { getBooleanArg, getStringArg } from "../parseArgs"; // Default timeout for terminal commands (2 minutes) const DEFAULT_TOOL_TIMEOUT_MS = 120_000; @@ -30,22 +40,12 @@ function getShellCommand(command: string): { shell: string; args: string[] } { args: ["-NoLogo", "-ExecutionPolicy", "Bypass", "-Command", command], }; } else { - // Unix/macOS: Use login shell to source .bashrc/.zshrc etc. + // Unix/macOS: Use standard shell without login profile to ensure reliable agent output const userShell = process.env.SHELL || "/bin/bash"; - return { shell: userShell, args: ["-l", "-c", command] }; + return { shell: userShell, args: ["-c", command] }; } } -import { fileURLToPath } from "node:url"; -import { ToolImpl } from "."; -import { - isProcessBackgrounded, - markProcessAsRunning, - removeBackgroundedProcess, - removeRunningProcess, - updateProcessOutput, -} from "../../util/processTerminalStates"; -import { getBooleanArg, getStringArg } from "../parseArgs"; /** * Resolves the working directory from workspace dirs. @@ -87,24 +87,28 @@ function resolveWorkingDirectory(workspaceDirs: string[]): string { } } -// Add color-supporting environment variables +// Provide a clean environment for agents (no ANSI escape codes) const getColorEnv = () => ({ ...process.env, - FORCE_COLOR: "1", - COLORTERM: "truecolor", - TERM: "xterm-256color", - CLICOLOR: "1", - CLICOLOR_FORCE: "1", + NO_COLOR: "1", + TERM: "dumb", }); -// Only spawn processes locally when there is no remote workspace. +// Spawn processes locally unless the workspace is a known remote environment. // 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"]; +// host, so for a truly 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 known remote types delegate to ide.runCommand() which routes +// through the integrated terminal and executes in the correct remote env. +// +// We use a blocklist instead of an allowlist because VS Code forks like Cursor +// may report a non-standard remoteName even when running locally. The +// previous allowlist (["", "local"]) caused those forks to fall into the +// remote path, silently dropping stdout from tool results. +const KNOWN_REMOTE_NAMES = ["wsl", "ssh-remote", "dev-container", "codespaces", "tunnel"]; +const isLocalEnvironment = (remoteName: string) => + !KNOWN_REMOTE_NAMES.includes(remoteName); export const runTerminalCommandImpl: ToolImpl = async (args, extras) => { const command = getStringArg(args, "command"); @@ -115,7 +119,7 @@ export const runTerminalCommandImpl: ToolImpl = async (args, extras) => { const ideInfo = await extras.ide.getIdeInfo(); const toolCallId = extras.toolCallId || ""; - if (LOCAL_ONLY.includes(ideInfo.remoteName)) { + if (isLocalEnvironment(ideInfo.remoteName)) { // For streaming output if (extras.onPartialOutput) { try { @@ -208,6 +212,14 @@ export const runTerminalCommandImpl: ToolImpl = async (args, extras) => { const newOutput = getDecodedOutput(data); terminalOutput += newOutput; + + // LOGGING + try { + require("fs").appendFileSync( + "/tmp/continue-agent-output.log", + `[STDOUT] newOutput: ${JSON.stringify(newOutput)} | terminalOutput: ${JSON.stringify(terminalOutput)}\n` + ); + } catch (e) {} // Update the tracked output for potential cancellation notifications if (toolCallId && waitForCompletion) { @@ -423,7 +435,14 @@ export const runTerminalCommandImpl: ToolImpl = async (args, extras) => { }, DEFAULT_TOOL_TIMEOUT_MS); childProc.stdout?.on("data", (data) => { - stdout += getDecodedOutput(data); + const newOut = getDecodedOutput(data); + stdout += newOut; + try { + require("fs").appendFileSync( + "/tmp/continue-agent-output.log", + `[STDOUT NON-STREAM] newOut: ${JSON.stringify(newOut)} | stdout: ${JSON.stringify(stdout)}\n` + ); + } catch (e) {} }); childProc.stderr?.on("data", (data) => {