Skip to content
Open
Show file tree
Hide file tree
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
4 changes: 3 additions & 1 deletion core/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -691,7 +691,9 @@ declare global {
getExternalUri?(uri: string): Promise<string>;

runCommand(command: string): Promise<void>;


runCommandWithOutput(command: string, cwd?: string): Promise<string>;

saveFile(filepath: string): Promise<void>;

readFile(filepath: string): Promise<string>;
Expand Down
5 changes: 4 additions & 1 deletion core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ type RequiredLLMOptions =
| "completionOptions";

export interface ILLM
extends Omit<LLMOptions, RequiredLLMOptions>,
extends
Omit<LLMOptions, RequiredLLMOptions>,
Required<Pick<LLMOptions, RequiredLLMOptions>> {
get providerName(): string;
get underlyingProviderName(): string;
Expand Down Expand Up @@ -872,6 +873,8 @@ export interface IDE {

runCommand(command: string, options?: TerminalOptions): Promise<void>;

runCommandWithOutput(command: string, cwd?: string): Promise<string>;

saveFile(fileUri: string): Promise<void>;

readFile(fileUri: string): Promise<string>;
Expand Down
1 change: 1 addition & 0 deletions core/protocol/ide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export type ToIdeFromWebviewOrCoreProtocol = {
openFile: [{ path: string }, void];
openUrl: [string, void];
runCommand: [{ command: string; options?: TerminalOptions }, void];
runCommandWithOutput: [{ command: string; cwd?: string }, string];
getSearchResults: [{ query: string; maxResults?: number }, string];
getFileResults: [{ pattern: string; maxResults?: number }, string[]];
subprocess: [{ command: string; cwd?: string }, [string, string]];
Expand Down
4 changes: 4 additions & 0 deletions core/protocol/messenger/messageIde.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,10 @@ export class MessageIde implements IDE {
await this.request("runCommand", { command, options });
}

async runCommandWithOutput(command: string, cwd?: string): Promise<string> {
return this.request("runCommandWithOutput", { command, cwd });
}

async saveFile(fileUri: string): Promise<void> {
await this.request("saveFile", { filepath: fileUri });
}
Expand Down
4 changes: 4 additions & 0 deletions core/protocol/messenger/reverseMessageIde.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ export class ReverseMessageIde {
return this.ide.runCommand(data.command);
});

this.on("runCommandWithOutput", (data) => {
return this.ide.runCommandWithOutput(data.command, data.cwd);
});

this.on("saveFile", (data) => {
return this.ide.saveFile(data.filepath);
});
Expand Down
28 changes: 22 additions & 6 deletions core/tools/implementations/runTerminalCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -453,16 +453,32 @@ export const runTerminalCommandImpl: ToolImpl = async (args, extras) => {
}
}

// For remote environments, just run the command
// Note: waitForCompletion is not supported in remote environments yet
await extras.ide.runCommand(command);
// For remote environments, use shell integration for output capture
const workspaceDirs = await extras.ide.getWorkspaceDirs();
const cwd = workspaceDirs.length > 0 ? workspaceDirs[0] : undefined;

if (extras.onPartialOutput) {
extras.onPartialOutput({
toolCallId,
contextItems: [
{
name: "Terminal",
description: "Terminal command output",
content: "",
status: "Running command on remote...",
},
],
});
}

const output = await extras.ide.runCommandWithOutput(command, cwd);

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",
content: output || "Command completed (no output captured)",
status: "Command completed",
},
];
};
24 changes: 12 additions & 12 deletions core/tools/implementations/runTerminalCommand.vitest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ describe("runTerminalCommandImpl", () => {
getIdeInfo: mockGetIdeInfo,
getWorkspaceDirs: mockGetWorkspaceDirs,
runCommand: mockRunCommand,
runCommandWithOutput: vi.fn().mockResolvedValue(""),
// Add stubs for other required IDE methods
getIdeSettings: vi.fn(),
getDiff: vi.fn(),
Expand Down Expand Up @@ -269,13 +270,10 @@ describe("runTerminalCommandImpl", () => {

const result = await runTerminalCommandImpl(args, extras);

// 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");
// In remote environments, it should use runCommandWithOutput
// and report completed (no double-execution via runCommand)
expect(mockRunCommand).not.toHaveBeenCalled();
expect(result[0].status).toBe("Command completed");
});

it("should handle errors when executing invalid commands", async () => {
Expand Down Expand Up @@ -370,6 +368,7 @@ describe("runTerminalCommandImpl", () => {
.mockReturnValue(Promise.resolve({ remoteName: "local" })),
getWorkspaceDirs: mockEmptyWorkspace,
runCommand: vi.fn(),
runCommandWithOutput: vi.fn().mockResolvedValue(""),
getIdeSettings: vi.fn(),
getDiff: vi.fn(),
getClipboardContent: vi.fn(),
Expand Down Expand Up @@ -430,6 +429,7 @@ describe("runTerminalCommandImpl", () => {
.mockReturnValue(Promise.resolve({ remoteName: "local" })),
getWorkspaceDirs: mockEmptyWorkspace,
runCommand: vi.fn(),
runCommandWithOutput: vi.fn().mockResolvedValue(""),
getIdeSettings: vi.fn(),
getDiff: vi.fn(),
getClipboardContent: vi.fn(),
Expand Down Expand Up @@ -618,8 +618,8 @@ describe("runTerminalCommandImpl", () => {
extras,
);

expect(mockRunCommand).toHaveBeenCalledWith("echo test");
expect(result[0].content).toContain("Terminal output not available");
expect(mockRunCommand).not.toHaveBeenCalled();
expect(result[0].status).toBe("Command completed");
});

it("should handle local environment with file URIs", async () => {
Expand Down Expand Up @@ -658,9 +658,9 @@ describe("runTerminalCommandImpl", () => {
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");
// Should use runCommandWithOutput, not try to spawn powershell.exe
expect(mockRunCommand).not.toHaveBeenCalled();
expect(result[0].status).toBe("Command completed");
} finally {
Object.defineProperty(process, "platform", {
value: originalPlatform,
Expand Down
4 changes: 4 additions & 0 deletions core/util/filesystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,10 @@ class FileSystemIde implements IDE {
return Promise.resolve();
}

runCommandWithOutput(command: string, cwd?: string): Promise<string> {
return Promise.resolve("");
}

saveFile(fileUri: string): Promise<void> {
return Promise.resolve();
}
Expand Down
86 changes: 86 additions & 0 deletions extensions/vscode/src/VsCodeIde.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@
case "error":
return showErrorMessage(message, "Show logs").then((selection) => {
if (selection === "Show logs") {
vscode.commands.executeCommand("workbench.action.toggleDevTools");

Check warning on line 157 in extensions/vscode/src/VsCodeIde.ts

View workflow job for this annotation

GitHub Actions / vscode-checks

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
}
});
case "info":
Expand Down Expand Up @@ -322,7 +322,7 @@
new vscode.Position(startLine, 0),
new vscode.Position(endLine, 0),
);
openEditorAndRevealRange(vscode.Uri.parse(fileUri), range).then(

Check warning on line 325 in extensions/vscode/src/VsCodeIde.ts

View workflow job for this annotation

GitHub Actions / vscode-checks

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
(editor) => {
// Select the lines
editor.selection = new vscode.Selection(
Expand Down Expand Up @@ -355,6 +355,92 @@
terminal.sendText(command, false);
}

async runCommandWithOutput(command: string, cwd?: string): Promise<string> {
const terminal = vscode.window.createTerminal({
name: "Continue",
cwd: cwd ? vscode.Uri.parse(cwd) : undefined,
});

const shellIntegration = await this.waitForShellIntegration(
terminal,
10000,
);

if (!shellIntegration) {
terminal.show();
terminal.sendText(command, true);
return "";
}

try {
const execution = shellIntegration.executeCommand(command);
let output = "";

for await (const chunk of execution.read()) {
output += chunk;
}

const exitCode = await execution.exitCode;
if (exitCode !== undefined && exitCode !== 0) {
output += `\n[Exit code: ${exitCode}]`;
}

// Strip VS Code shell integration OSC sequences (]633;...) but preserve
// ANSI color/formatting codes (e.g. \033[31m) which are part of command output
output = output.replace(/\x1b\]633;[^\x07]*\x07/g, "");
// Also strip OSC sequences that lost their escape byte during read()
output = output.replace(/\]633;[^\n]*/g, "");
output = output.trim();

terminal.dispose();
return output;
} catch (error) {
console.error(
"[Continue] shellIntegration.executeCommand failed:",
error,
);
terminal.dispose();
return "";
}
}

private async waitForShellIntegration(
terminal: vscode.Terminal,
timeoutMs: number,
): Promise<any> {
if ((terminal as any).shellIntegration) {
return (terminal as any).shellIntegration;
}

if (!(vscode.window as any).onDidChangeTerminalShellIntegration) {
return undefined;
}

return new Promise<any>((resolve) => {
const timeout = setTimeout(() => {
disposable.dispose();
resolve(undefined);
}, timeoutMs);

const disposable = (
vscode.window as any
).onDidChangeTerminalShellIntegration((e: any) => {
if (e.terminal === terminal) {
clearTimeout(timeout);
disposable.dispose();
resolve(e.shellIntegration);
}
});

// Race condition guard
if ((terminal as any).shellIntegration) {
clearTimeout(timeout);
disposable.dispose();
resolve((terminal as any).shellIntegration);
}
});
}

async saveFile(fileUri: string): Promise<void> {
await this.ideUtils.saveFile(vscode.Uri.parse(fileUri));
}
Expand Down
3 changes: 3 additions & 0 deletions extensions/vscode/src/extension/VsCodeMessenger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,9 @@ export class VsCodeMessenger {
this.onWebviewOrCore("runCommand", async (msg) => {
await ide.runCommand(msg.data.command);
});
this.onWebviewOrCore("runCommandWithOutput", async (msg) => {
return ide.runCommandWithOutput(msg.data.command, msg.data.cwd);
});
this.onWebviewOrCore("getSearchResults", async (msg) => {
return ide.getSearchResults(msg.data.query, msg.data.maxResults);
});
Expand Down
Loading