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
141 changes: 85 additions & 56 deletions core/config/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,7 +464,7 @@
}
if (name === "llm") {
const llm = models.find((model) => model.title === params?.modelTitle);
if (!llm) {

Check warning on line 467 in core/config/load.ts

View workflow job for this annotation

GitHub Actions / core-checks

Unexpected negated condition
errors.push({
fatal: false,
message: `Unknown reranking model ${params?.modelTitle}`,
Expand Down Expand Up @@ -560,7 +560,7 @@
id: `continue-mcp-server-${index + 1}`,
name: `MCP Server`,
requestOptions: mergeConfigYamlRequestOptions(
server.transport.type !== "stdio"

Check warning on line 563 in core/config/load.ts

View workflow job for this annotation

GitHub Actions / core-checks

Unexpected negated condition
? server.transport.requestOptions
: undefined,
config.requestOptions,
Expand Down Expand Up @@ -822,6 +822,86 @@
return readConfigJs();
}

async function loadConfigModule(configJsPath: string) {
try {
return await import(configJsPath);
} catch (e) {
console.log(e);
console.log(
"Could not load config.ts as absolute path, retrying as file url ...",
);
try {
return await import(localPathToUri(configJsPath));
} catch (fileUrlError) {
throw new Error("Could not load config.ts as file url either", {
cause: fileUrlError,
});
}
}
}

function clearConfigModuleCache(configJsPath: string) {
if (typeof require === "undefined") {
return;
}

try {
delete require.cache[require.resolve(configJsPath)];
} catch {
// Dynamic imports via file URLs may not populate require cache.
}
}

async function applyConfigModule<T>(
config: T,
configJsPath: string,
): Promise<T> {
const module = await loadConfigModule(configJsPath);
clearConfigModuleCache(configJsPath);

if (!module.modifyConfig) {
throw new Error("config.ts does not export a modifyConfig function.");
}

return module.modifyConfig(config);
}

export async function applyConfigTsAndRemoteConfig<T>(options: {
config: T;
ide: IDE;
ideType: IdeType;
remoteConfigServerUrl?: string;
}): Promise<T> {
const { config, ide, ideType, remoteConfigServerUrl } = options;

let modifiedConfig = config;

const configJsContents = await buildConfigTsandReadConfigJs(ide, ideType);
if (configJsContents) {
try {
modifiedConfig = await applyConfigModule(
modifiedConfig,
getConfigJsPath(),
);
} catch (e) {
console.log("Error loading config.ts: ", e);
}
}

if (remoteConfigServerUrl) {
try {
modifiedConfig = await applyConfigModule(
modifiedConfig,
getConfigJsPathForRemote(remoteConfigServerUrl),
);
} catch (e) {
console.log("Error loading remotely set config.js: ", e);
}
}

return modifiedConfig;
}

async function loadContinueConfigFromJson(
ide: IDE,
ideSettings: IdeSettings,
Expand Down Expand Up @@ -857,63 +937,12 @@
// Convert serialized to intermediate config
let intermediate = await serializedToIntermediateConfig(withShared, ide);

// Apply config.ts to modify intermediate config
const configJsContents = await buildConfigTsandReadConfigJs(
intermediate = await applyConfigTsAndRemoteConfig({
config: intermediate,
ide,
ideInfo.ideType,
);
if (configJsContents) {
try {
// Try config.ts first
const configJsPath = getConfigJsPath();
let module: any;

try {
module = await import(configJsPath);
} catch (e) {
console.log(e);
console.log(
"Could not load config.ts as absolute path, retrying as file url ...",
);
try {
module = await import(localPathToUri(configJsPath));
} catch (e) {
throw new Error("Could not load config.ts as file url either", {
cause: e,
});
}
}

if (typeof require !== "undefined") {
delete require.cache[require.resolve(configJsPath)];
}
if (!module.modifyConfig) {
throw new Error("config.ts does not export a modifyConfig function.");
}
intermediate = module.modifyConfig(intermediate);
} catch (e) {
console.log("Error loading config.ts: ", e);
}
}

// Apply remote config.js to modify intermediate config
if (ideSettings.remoteConfigServerUrl) {
try {
const configJsPathForRemote = getConfigJsPathForRemote(
ideSettings.remoteConfigServerUrl,
);
const module = await import(configJsPathForRemote);
if (typeof require !== "undefined") {
delete require.cache[require.resolve(configJsPathForRemote)];
}
if (!module.modifyConfig) {
throw new Error("config.ts does not export a modifyConfig function.");
}
intermediate = module.modifyConfig(intermediate);
} catch (e) {
console.log("Error loading remotely set config.js: ", e);
}
}
ideType: ideInfo.ideType,
remoteConfigServerUrl: ideSettings.remoteConfigServerUrl,
});

// Convert to final config format
const { config: finalConfig, errors: finalErrors } =
Expand Down
114 changes: 114 additions & 0 deletions core/config/yaml/loadContinueConfigFromYaml.vitest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { describe, expect, it, vi } from "vitest";

const { mockApplyConfigTsAndRemoteConfig } = vi.hoisted(() => ({
mockApplyConfigTsAndRemoteConfig: vi.fn(async ({ config }) => ({
...config,
allowAnonymousTelemetry: false,
})),
}));

vi.mock("../load", () => ({
applyConfigTsAndRemoteConfig: mockApplyConfigTsAndRemoteConfig,
}));

vi.mock("../../context/mcp/MCPManagerSingleton", () => ({
MCPManagerSingleton: {
getInstance: () => ({
setConnections: vi.fn(),
shutdown: vi.fn(),
getStatuses: () => [],
}),
},
}));

vi.mock("../../promptFiles/getPromptFiles", () => ({
getAllPromptFiles: vi.fn().mockResolvedValue([]),
}));

vi.mock("../../context/mcp/json/loadJsonMcpConfigs", () => ({
loadJsonMcpConfigs: vi.fn().mockResolvedValue({ errors: [], mcpServers: [] }),
}));

vi.mock("../../tools", () => ({
getBaseToolDefinitions: vi.fn().mockReturnValue([]),
}));

vi.mock("../loadContextProviders", () => ({
loadConfigContextProviders: vi.fn().mockReturnValue({
providers: [],
errors: [],
}),
}));

vi.mock("../loadLocalAssistants", () => ({
getAllDotContinueDefinitionFiles: vi.fn().mockResolvedValue([]),
}));

vi.mock("./models", () => ({
llmsFromModelConfig: vi.fn().mockResolvedValue([]),
}));

vi.mock("../../util/GlobalContext", () => ({
GlobalContext: class {
getSharedConfig() {
return {};
}

get() {
return {};
}

update() {}
},
}));

vi.mock("../../control-plane/env", () => ({
getControlPlaneEnvSync: vi.fn().mockReturnValue({
CONTROL_PLANE_URL: "https://api.example.com",
}),
}));

vi.mock("../../control-plane/PolicySingleton", () => ({
PolicySingleton: {
getInstance: () => ({ policy: null }),
},
}));

import { loadContinueConfigFromYaml } from "./loadYaml";

describe("loadContinueConfigFromYaml", () => {
it("applies config.ts modifiers for YAML configs", async () => {
const result = await loadContinueConfigFromYaml({
ide: {} as any,
ideSettings: {
remoteConfigServerUrl: "",
remoteConfigSyncPeriod: 60,
userToken: "",
continueTestEnvironment: "none",
pauseCodebaseIndexOnStart: false,
},
ideInfo: { ideType: "vscode" } as any,
uniqueId: "test-unique-id",
llmLogger: {} as any,
workOsAccessToken: undefined,
overrideConfigYaml: {
name: "Local Config",
version: "1.0.0",
schema: "v1",
models: [],
},
controlPlaneClient: {
getAccessToken: vi.fn(),
} as any,
orgScopeId: null,
packageIdentifier: {
uriType: "file",
fileUri: "/tmp/config.yaml",
content: "name: Local Config\nversion: 1.0.0\nschema: v1\n",
},
});

expect(mockApplyConfigTsAndRemoteConfig).toHaveBeenCalledTimes(1);
expect(result.config?.allowAnonymousTelemetry).toBe(false);
});
});
10 changes: 9 additions & 1 deletion core/config/yaml/loadYaml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { ControlPlaneClient } from "../../control-plane/client";
import TransformersJsEmbeddingsProvider from "../../llm/llms/TransformersJsEmbeddingsProvider";
import { getAllPromptFiles } from "../../promptFiles/getPromptFiles";
import { GlobalContext } from "../../util/GlobalContext";
import { applyConfigTsAndRemoteConfig } from "../load";
import { modifyAnyConfigWithSharedConfig } from "../sharedConfig";

import { convertPromptBlockToSlashCommand } from "../../commands/slash/promptBlockSlashCommand";
Expand Down Expand Up @@ -492,8 +493,15 @@ export async function loadContinueConfigFromYaml(options: {
withShared.allowAnonymousTelemetry = true;
}

return {
const withConfigTs = await applyConfigTsAndRemoteConfig({
config: withShared,
ide,
Comment on lines +496 to +498
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Apply config.ts before converting YAML to ContinueConfig

applyConfigTsAndRemoteConfig is being called with withShared here, but withShared is a ContinueConfig (with modelsByRole), while user config.ts files are authored against Config (with models, tabAutocompleteModel, embeddingsProvider, etc.). For YAML-based profiles, common modifyConfig logic like config.models.push(...) will throw or no-op at runtime, and the error is swallowed in applyConfigTsAndRemoteConfig, so user config mutations are silently skipped. This means the new YAML support only works for overlapping fields and breaks the primary model-editing use case.

Useful? React with 👍 / 👎.

ideType: ideInfo.ideType,
remoteConfigServerUrl: ideSettings.remoteConfigServerUrl,
});

return {
config: withConfigTs,
errors: [...(configYamlResult.errors ?? []), ...localErrors],
configLoadInterrupted: false,
configName: configYamlResult.configName,
Expand Down
Loading