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
15 changes: 15 additions & 0 deletions core/llm/llms/OpenRouter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ChatCompletionCreateParams } from "openai/resources/index";

import { OPENROUTER_HEADERS } from "@continuedev/openai-adapters";

import { LLMOptions } from "../../index.js";
import { osModelsEditPrompt } from "../templates/edit.js";

Expand All @@ -18,6 +20,19 @@ class OpenRouter extends OpenAI {
useLegacyCompletionsEndpoint: false,
};

constructor(options: LLMOptions) {
super({
...options,
requestOptions: {
...options.requestOptions,
headers: {
...OPENROUTER_HEADERS,
...options.requestOptions?.headers,
},
},
});
}

private isAnthropicModel(model?: string): boolean {
if (!model) return false;
const modelLower = model.toLowerCase();
Expand Down
18 changes: 2 additions & 16 deletions extensions/vscode/src/VsCodeIde.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import { Repository } from "./otherExtensions/git";
import { SecretStorage } from "./stubs/SecretStorage";
import { VsCodeIdeUtils } from "./util/ideUtils";
import { runCommandInTerminal } from "./util/runCommandInTerminal";
import { getExtensionVersion, isExtensionPrerelease } from "./util/util";
import { getExtensionUri, openEditorAndRevealRange } from "./util/vscode";
import { VsCodeWebviewProtocol } from "./webviewProtocol";
Expand Down Expand Up @@ -154,7 +155,7 @@
case "error":
return showErrorMessage(message, "Show logs").then((selection) => {
if (selection === "Show logs") {
vscode.commands.executeCommand("workbench.action.toggleDevTools");

Check warning on line 158 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 +323,7 @@
new vscode.Position(startLine, 0),
new vscode.Position(endLine, 0),
);
openEditorAndRevealRange(vscode.Uri.parse(fileUri), range).then(

Check warning on line 326 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 All @@ -337,22 +338,7 @@
command: string,
options: TerminalOptions = { reuseTerminal: true },
): Promise<void> {
let terminal: vscode.Terminal | undefined;
if (vscode.window.terminals.length && options.reuseTerminal) {
if (options.terminalName) {
terminal = vscode.window.terminals.find(
(t) => t?.name === options.terminalName,
);
} else {
terminal = vscode.window.activeTerminal ?? vscode.window.terminals[0];
}
}

if (!terminal) {
terminal = vscode.window.createTerminal(options?.terminalName);
}
terminal.show();
terminal.sendText(command, false);
await runCommandInTerminal(command, options);
}

async saveFile(fileUri: string): Promise<void> {
Expand Down
133 changes: 133 additions & 0 deletions extensions/vscode/src/util/runCommandInTerminal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import type { TerminalOptions } from "core";
import * as vscode from "vscode";

const REMOTE_TERMINAL_TIMEOUT_MS = 5000;

const terminalCacheByName = new Map<string, vscode.Terminal>();

function getNewActiveTerminal(
existingTerminals: Set<vscode.Terminal>,
): vscode.Terminal | undefined {
const activeTerminal = vscode.window.activeTerminal;
if (!activeTerminal || existingTerminals.has(activeTerminal)) {
return undefined;
}

return activeTerminal;
}

function getReusableTerminal(
options: TerminalOptions,
): vscode.Terminal | undefined {
if (!vscode.window.terminals.length || !options.reuseTerminal) {
return undefined;
}

if (options.terminalName) {
const cachedTerminal = terminalCacheByName.get(options.terminalName);
if (cachedTerminal && vscode.window.terminals.includes(cachedTerminal)) {
return cachedTerminal;
}

terminalCacheByName.delete(options.terminalName);
return vscode.window.terminals.find(
(terminal) => terminal?.name === options.terminalName,
);
}

return vscode.window.activeTerminal ?? vscode.window.terminals[0];
}

async function createTerminal(
options: TerminalOptions,
): Promise<vscode.Terminal> {
if (!vscode.env.remoteName) {
return vscode.window.createTerminal(options.terminalName);
}

const existingTerminals = new Set(vscode.window.terminals);
await vscode.commands.executeCommand("workbench.action.terminal.new");

const newActiveTerminal = getNewActiveTerminal(existingTerminals);
if (newActiveTerminal) {
return newActiveTerminal;
}

return await new Promise<vscode.Terminal>((resolve, reject) => {
let settled = false;
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;

const cleanup = () => {
terminalOpenListener.dispose();
activeTerminalListener.dispose();
if (timeoutHandle) {
clearTimeout(timeoutHandle);
}
};

const resolveIfNewActiveTerminalExists = () => {
const newTerminal = getNewActiveTerminal(existingTerminals);
if (!newTerminal) {
return false;
}

settled = true;
cleanup();
resolve(newTerminal);
return true;
};

const terminalOpenListener = vscode.window.onDidOpenTerminal(() => {
if (settled) {
return;
}

resolveIfNewActiveTerminalExists();
});

const activeTerminalListener = vscode.window.onDidChangeActiveTerminal(
(terminal) => {
if (settled || !terminal || existingTerminals.has(terminal)) {
return;
}

settled = true;
cleanup();
resolve(terminal);
},
);

if (resolveIfNewActiveTerminalExists()) {
return;
}

// `workbench.action.terminal.new` should focus the new terminal in remote
// workspaces. If another terminal opens concurrently, wait until VS Code
// actually switches the active terminal instead of grabbing the first one
// that appears.
timeoutHandle = setTimeout(() => {
if (settled) {
return;
}

settled = true;
cleanup();
reject(new Error("Timed out waiting for remote terminal to open"));
}, REMOTE_TERMINAL_TIMEOUT_MS);
});
}

export async function runCommandInTerminal(
command: string,
options: TerminalOptions = { reuseTerminal: true },
): Promise<void> {
const terminal =
getReusableTerminal(options) ?? (await createTerminal(options));

if (options.terminalName) {
terminalCacheByName.set(options.terminalName, terminal);
}

terminal.show();
terminal.sendText(command, true);
}
203 changes: 203 additions & 0 deletions extensions/vscode/src/util/runCommandInTerminal.vitest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

type MockTerminal = {
name: string;
show: ReturnType<typeof vi.fn>;
sendText: ReturnType<typeof vi.fn>;
};

const terminalListeners = new Set<(terminal: MockTerminal) => void>();
const activeTerminalListeners = new Set<
(terminal: MockTerminal | undefined) => void
>();
const terminals: MockTerminal[] = [];

const windowMock = {
terminals,
activeTerminal: undefined as MockTerminal | undefined,
createTerminal: vi.fn<(name?: string) => MockTerminal>(),
onDidOpenTerminal: vi.fn((listener: (terminal: MockTerminal) => void) => {
terminalListeners.add(listener);
return {
dispose: vi.fn(() => terminalListeners.delete(listener)),
};
}),
onDidChangeActiveTerminal: vi.fn(
(listener: (terminal: MockTerminal | undefined) => void) => {
activeTerminalListeners.add(listener);
return {
dispose: vi.fn(() => activeTerminalListeners.delete(listener)),
};
},
),
};

const commandsMock = {
executeCommand: vi.fn<(command: string) => Promise<void>>(),
};

const envMock = {
remoteName: undefined as string | undefined,
};

vi.mock("vscode", () => ({
window: windowMock,
commands: commandsMock,
env: envMock,
}));

function createTerminal(name: string): MockTerminal {
return {
name,
show: vi.fn(),
sendText: vi.fn(),
};
}

function notifyTerminalOpened(terminal: MockTerminal) {
for (const listener of terminalListeners) {
listener(terminal);
}
}

function setActiveTerminal(terminal: MockTerminal | undefined) {
windowMock.activeTerminal = terminal;
for (const listener of activeTerminalListeners) {
listener(terminal);
}
}

describe("runCommandInTerminal", () => {
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();

terminals.length = 0;
terminalListeners.clear();
activeTerminalListeners.clear();
setActiveTerminal(undefined);
envMock.remoteName = undefined;

windowMock.createTerminal.mockImplementation((name?: string) => {
const terminal = createTerminal(
name ?? "Terminal " + (terminals.length + 1),
);
terminals.push(terminal);
return terminal;
});

commandsMock.executeCommand.mockResolvedValue();
});

it("reuses the active terminal and sends an executing command", async () => {
const terminal = createTerminal("Active");
terminals.push(terminal);
setActiveTerminal(terminal);

const { runCommandInTerminal } = await import("./runCommandInTerminal");

await runCommandInTerminal("echo hello");

expect(windowMock.createTerminal).not.toHaveBeenCalled();
expect(commandsMock.executeCommand).not.toHaveBeenCalled();
expect(terminal.show).toHaveBeenCalledOnce();
expect(terminal.sendText).toHaveBeenCalledWith("echo hello", true);
});

it("creates a named local terminal when no reusable terminal exists", async () => {
const { runCommandInTerminal } = await import("./runCommandInTerminal");

await runCommandInTerminal("npm test", {
reuseTerminal: true,
terminalName: "Start Ollama",
});

expect(windowMock.createTerminal).toHaveBeenCalledWith("Start Ollama");
expect(commandsMock.executeCommand).not.toHaveBeenCalled();

const createdTerminal = terminals[0];
expect(createdTerminal.show).toHaveBeenCalledOnce();
expect(createdTerminal.sendText).toHaveBeenCalledWith("npm test", true);
});

it("creates remote terminals through the remote-aware VS Code command", async () => {
envMock.remoteName = "ssh-remote";
commandsMock.executeCommand.mockImplementation(async (command: string) => {
expect(command).toBe("workbench.action.terminal.new");
const terminal = createTerminal("Remote Shell");
terminals.push(terminal);
setActiveTerminal(terminal);
notifyTerminalOpened(terminal);
});

const { runCommandInTerminal } = await import("./runCommandInTerminal");

await runCommandInTerminal("pwd", { reuseTerminal: true });

expect(windowMock.createTerminal).not.toHaveBeenCalled();
expect(commandsMock.executeCommand).toHaveBeenCalledWith(
"workbench.action.terminal.new",
);

const createdTerminal = terminals[0];
expect(createdTerminal.show).toHaveBeenCalledOnce();
expect(createdTerminal.sendText).toHaveBeenCalledWith("pwd", true);
});

it("reuses cached remote terminals for named commands", async () => {
envMock.remoteName = "dev-container";

let createdCount = 0;
commandsMock.executeCommand.mockImplementation(async () => {
createdCount += 1;
const terminal = createTerminal("Remote " + createdCount);
terminals.push(terminal);
setActiveTerminal(terminal);
notifyTerminalOpened(terminal);
});

const { runCommandInTerminal } = await import("./runCommandInTerminal");

await runCommandInTerminal("ollama serve", {
reuseTerminal: true,
terminalName: "Start Ollama",
});
await runCommandInTerminal("ollama serve", {
reuseTerminal: true,
terminalName: "Start Ollama",
});

expect(commandsMock.executeCommand).toHaveBeenCalledTimes(1);
expect(terminals[0].sendText).toHaveBeenNthCalledWith(
1,
"ollama serve",
true,
);
expect(terminals[0].sendText).toHaveBeenNthCalledWith(
2,
"ollama serve",
true,
);
});

it("ignores unrelated remote terminals that open before the command terminal becomes active", async () => {
envMock.remoteName = "dev-container";
commandsMock.executeCommand.mockImplementation(async () => {
const unrelatedTerminal = createTerminal("Unrelated");
terminals.push(unrelatedTerminal);
notifyTerminalOpened(unrelatedTerminal);

const commandTerminal = createTerminal("Remote Command");
terminals.push(commandTerminal);
setActiveTerminal(commandTerminal);
notifyTerminalOpened(commandTerminal);
});

const { runCommandInTerminal } = await import("./runCommandInTerminal");

await runCommandInTerminal("pwd", { reuseTerminal: true });

expect(terminals[0].sendText).not.toHaveBeenCalled();
expect(terminals[1].sendText).toHaveBeenCalledWith("pwd", true);
});
});
Loading
Loading