Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 45 additions & 26 deletions core/tools/implementations/runTerminalCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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");
Expand All @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) => {
Expand Down
Loading