Fixes #6242, #7810 When Windows VS Code connects to WSL, workspace files are accessed via vscode-remote:// URIs. Previously, getAllDotContinueDefinitionFiles() correctly read content via ide.readFile(), but loadYaml.ts discarded the content and passed only the URI path to RegistryClient, which then tried to re-read using fs.readFileSync() - failing for remote URIs. Changes: - Add optional content field to FileIdentifier (non-breaking) - RegistryClient.getContent() returns pre-read content when present - loadYaml.ts passes content along with file identifiers Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
476 lines
14 KiB
TypeScript
476 lines
14 KiB
TypeScript
import {
|
|
AssistantUnrolled,
|
|
BLOCK_TYPES,
|
|
ConfigResult,
|
|
ConfigValidationError,
|
|
isAssistantUnrolledNonNullable,
|
|
mergeConfigYamlRequestOptions,
|
|
mergeUnrolledAssistants,
|
|
ModelRole,
|
|
PackageIdentifier,
|
|
RegistryClient,
|
|
unrollAssistant,
|
|
validateConfigYaml,
|
|
} from "@continuedev/config-yaml";
|
|
import { dirname } from "node:path";
|
|
|
|
import {
|
|
ContinueConfig,
|
|
IDE,
|
|
IdeInfo,
|
|
IdeSettings,
|
|
ILLMLogger,
|
|
InternalMcpOptions,
|
|
} from "../..";
|
|
import { MCPManagerSingleton } from "../../context/mcp/MCPManagerSingleton";
|
|
import { ControlPlaneClient } from "../../control-plane/client";
|
|
import TransformersJsEmbeddingsProvider from "../../llm/llms/TransformersJsEmbeddingsProvider";
|
|
import { getAllPromptFiles } from "../../promptFiles/getPromptFiles";
|
|
import { GlobalContext } from "../../util/GlobalContext";
|
|
import { modifyAnyConfigWithSharedConfig } from "../sharedConfig";
|
|
|
|
import { convertPromptBlockToSlashCommand } from "../../commands/slash/promptBlockSlashCommand";
|
|
import { slashCommandFromPromptFile } from "../../commands/slash/promptFileSlashCommand";
|
|
import { loadJsonMcpConfigs } from "../../context/mcp/json/loadJsonMcpConfigs";
|
|
import { getControlPlaneEnvSync } from "../../control-plane/env";
|
|
import { PolicySingleton } from "../../control-plane/PolicySingleton";
|
|
import { getBaseToolDefinitions } from "../../tools";
|
|
import { getCleanUriPath } from "../../util/uri";
|
|
import { loadConfigContextProviders } from "../loadContextProviders";
|
|
import { getAllDotContinueDefinitionFiles } from "../loadLocalAssistants";
|
|
import { unrollLocalYamlBlocks } from "./loadLocalYamlBlocks";
|
|
import { LocalPlatformClient } from "./LocalPlatformClient";
|
|
import { llmsFromModelConfig } from "./models";
|
|
import {
|
|
convertYamlMcpConfigToInternalMcpOptions,
|
|
convertYamlRuleToContinueRule,
|
|
} from "./yamlToContinueConfig";
|
|
|
|
async function loadConfigYaml(options: {
|
|
overrideConfigYaml: AssistantUnrolled | undefined;
|
|
controlPlaneClient: ControlPlaneClient;
|
|
orgScopeId: string | null;
|
|
ideSettings: IdeSettings;
|
|
ide: IDE;
|
|
packageIdentifier: PackageIdentifier;
|
|
}): Promise<ConfigResult<AssistantUnrolled>> {
|
|
const {
|
|
overrideConfigYaml,
|
|
controlPlaneClient,
|
|
orgScopeId,
|
|
ideSettings,
|
|
ide,
|
|
packageIdentifier,
|
|
} = options;
|
|
|
|
// Add local .continue blocks
|
|
// Use "content" field to pass pre-read content directly, avoiding
|
|
// fs.readFileSync which fails for vscode-remote:// URIs in WSL (#6242, #7810)
|
|
const localBlockPromises = BLOCK_TYPES.map(async (blockType) => {
|
|
const localBlocks = await getAllDotContinueDefinitionFiles(
|
|
ide,
|
|
{ includeGlobal: true, includeWorkspace: true, fileExtType: "yaml" },
|
|
blockType,
|
|
);
|
|
return localBlocks.map((b) => ({
|
|
uriType: "file" as const,
|
|
fileUri: b.path,
|
|
content: b.content,
|
|
}));
|
|
});
|
|
const localPackageIdentifiers: PackageIdentifier[] = (
|
|
await Promise.all(localBlockPromises)
|
|
).flat();
|
|
|
|
// logger.info(
|
|
// `Loading config.yaml from ${JSON.stringify(packageIdentifier)} with root path ${rootPath}`,
|
|
// );
|
|
|
|
// Registry client is only used if local blocks are present, but logic same for hub/local assistants
|
|
const getRegistryClient = async () => {
|
|
const rootPath =
|
|
packageIdentifier.uriType === "file"
|
|
? dirname(getCleanUriPath(packageIdentifier.fileUri))
|
|
: undefined;
|
|
return new RegistryClient({
|
|
accessToken: await controlPlaneClient.getAccessToken(),
|
|
apiBase: getControlPlaneEnvSync(ideSettings.continueTestEnvironment)
|
|
.CONTROL_PLANE_URL,
|
|
rootPath,
|
|
});
|
|
};
|
|
|
|
const errors: ConfigValidationError[] = [];
|
|
|
|
let config: AssistantUnrolled | undefined;
|
|
|
|
if (overrideConfigYaml) {
|
|
config = overrideConfigYaml;
|
|
if (localPackageIdentifiers.length > 0) {
|
|
const unrolledLocal = await unrollLocalYamlBlocks(
|
|
localPackageIdentifiers,
|
|
ide,
|
|
await getRegistryClient(),
|
|
orgScopeId,
|
|
controlPlaneClient,
|
|
);
|
|
if (unrolledLocal.errors) {
|
|
errors.push(...unrolledLocal.errors);
|
|
}
|
|
if (unrolledLocal.config) {
|
|
config = mergeUnrolledAssistants(config, unrolledLocal.config);
|
|
}
|
|
}
|
|
} else {
|
|
// This is how we allow use of blocks locally
|
|
const unrollResult = await unrollAssistant(
|
|
packageIdentifier,
|
|
await getRegistryClient(),
|
|
{
|
|
renderSecrets: true,
|
|
currentUserSlug: "",
|
|
onPremProxyUrl: null,
|
|
orgScopeId,
|
|
platformClient: new LocalPlatformClient(
|
|
orgScopeId,
|
|
controlPlaneClient,
|
|
ide,
|
|
),
|
|
injectBlocks: localPackageIdentifiers,
|
|
},
|
|
);
|
|
config = unrollResult.config;
|
|
if (unrollResult.errors) {
|
|
errors.push(...unrollResult.errors);
|
|
}
|
|
}
|
|
|
|
if (config && isAssistantUnrolledNonNullable(config)) {
|
|
errors.push(...validateConfigYaml(config));
|
|
}
|
|
|
|
if (errors?.some((error) => error.fatal)) {
|
|
return {
|
|
errors,
|
|
config: undefined,
|
|
configLoadInterrupted: true,
|
|
};
|
|
}
|
|
|
|
// Set defaults if undefined (this lets us keep config.json uncluttered for new users)
|
|
return {
|
|
config,
|
|
errors,
|
|
configLoadInterrupted: false,
|
|
};
|
|
}
|
|
|
|
export async function configYamlToContinueConfig(options: {
|
|
config: AssistantUnrolled;
|
|
ide: IDE;
|
|
ideInfo: IdeInfo;
|
|
uniqueId: string;
|
|
llmLogger: ILLMLogger;
|
|
workOsAccessToken: string | undefined;
|
|
}): Promise<{ config: ContinueConfig; errors: ConfigValidationError[] }> {
|
|
let { config, ide, ideInfo, uniqueId, llmLogger } = options;
|
|
|
|
const localErrors: ConfigValidationError[] = [];
|
|
|
|
const continueConfig: ContinueConfig = {
|
|
slashCommands: [],
|
|
tools: getBaseToolDefinitions(),
|
|
mcpServerStatuses: [],
|
|
contextProviders: [],
|
|
modelsByRole: {
|
|
chat: [],
|
|
edit: [],
|
|
apply: [],
|
|
embed: [],
|
|
autocomplete: [],
|
|
rerank: [],
|
|
summarize: [],
|
|
subagent: [],
|
|
},
|
|
selectedModelByRole: {
|
|
chat: null,
|
|
edit: null, // not currently used
|
|
apply: null,
|
|
embed: null,
|
|
autocomplete: null,
|
|
rerank: null,
|
|
summarize: null,
|
|
subagent: null,
|
|
},
|
|
rules: [],
|
|
requestOptions: { ...config.requestOptions },
|
|
};
|
|
|
|
// Right now, if there are any missing packages in the config, then we will just throw an error
|
|
if (!isAssistantUnrolledNonNullable(config)) {
|
|
return {
|
|
config: continueConfig,
|
|
errors: [
|
|
{
|
|
message:
|
|
"Failed to load config due to missing blocks, see which blocks are missing below",
|
|
fatal: true,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
for (const rule of config.rules ?? []) {
|
|
const convertedRule = convertYamlRuleToContinueRule(rule);
|
|
continueConfig.rules.push(convertedRule);
|
|
}
|
|
|
|
continueConfig.data = config.data?.map((d) => ({
|
|
...d,
|
|
requestOptions: mergeConfigYamlRequestOptions(
|
|
d.requestOptions,
|
|
continueConfig.requestOptions,
|
|
),
|
|
}));
|
|
continueConfig.docs = config.docs?.map((doc) => ({
|
|
title: doc.name,
|
|
startUrl: doc.startUrl,
|
|
rootUrl: doc.rootUrl,
|
|
faviconUrl: doc.faviconUrl,
|
|
useLocalCrawling: doc.useLocalCrawling,
|
|
sourceFile: doc.sourceFile,
|
|
}));
|
|
|
|
// Prompt files -
|
|
try {
|
|
const promptFiles = await getAllPromptFiles(ide, undefined, true);
|
|
|
|
promptFiles.forEach((file) => {
|
|
try {
|
|
const slashCommand = slashCommandFromPromptFile(
|
|
file.path,
|
|
file.content,
|
|
);
|
|
if (slashCommand) {
|
|
continueConfig.slashCommands?.push(slashCommand);
|
|
}
|
|
} catch (e) {
|
|
localErrors.push({
|
|
fatal: false,
|
|
message: `Failed to convert prompt file ${file.path} to slash command: ${e instanceof Error ? e.message : e}`,
|
|
});
|
|
}
|
|
});
|
|
} catch (e) {
|
|
localErrors.push({
|
|
fatal: false,
|
|
message: `Error loading local prompt files: ${e instanceof Error ? e.message : e}`,
|
|
});
|
|
}
|
|
|
|
config.prompts?.forEach((prompt) => {
|
|
try {
|
|
const slashCommand = convertPromptBlockToSlashCommand(prompt);
|
|
continueConfig.slashCommands?.push(slashCommand);
|
|
} catch (e) {
|
|
localErrors.push({
|
|
message: `Error loading prompt ${prompt.name}: ${e instanceof Error ? e.message : e}`,
|
|
fatal: false,
|
|
});
|
|
}
|
|
});
|
|
|
|
// Models
|
|
let warnAboutFreeTrial = false;
|
|
const defaultModelRoles: ModelRole[] = ["chat", "summarize", "apply", "edit"];
|
|
for (const model of config.models ?? []) {
|
|
model.roles = model.roles ?? defaultModelRoles; // Default to all 4 chat-esque roles if not specified
|
|
|
|
if (model.provider === "free-trial") {
|
|
warnAboutFreeTrial = true;
|
|
}
|
|
try {
|
|
const llms = await llmsFromModelConfig({
|
|
model,
|
|
uniqueId,
|
|
llmLogger,
|
|
config: continueConfig,
|
|
});
|
|
|
|
if (model.roles?.includes("chat")) {
|
|
continueConfig.modelsByRole.chat.push(...llms);
|
|
}
|
|
|
|
if (model.roles?.includes("summarize")) {
|
|
continueConfig.modelsByRole.summarize.push(...llms);
|
|
}
|
|
|
|
if (model.roles?.includes("apply")) {
|
|
continueConfig.modelsByRole.apply.push(...llms);
|
|
}
|
|
|
|
if (model.roles?.includes("edit")) {
|
|
continueConfig.modelsByRole.edit.push(...llms);
|
|
}
|
|
|
|
if (model.roles?.includes("autocomplete")) {
|
|
continueConfig.modelsByRole.autocomplete.push(...llms);
|
|
}
|
|
|
|
if (model.roles?.includes("embed")) {
|
|
const { provider } = model;
|
|
if (provider === "transformers.js") {
|
|
if (ideInfo.ideType === "vscode") {
|
|
continueConfig.modelsByRole.embed.push(
|
|
new TransformersJsEmbeddingsProvider(),
|
|
);
|
|
} else {
|
|
localErrors.push({
|
|
fatal: false,
|
|
message: `Transformers.js embeddings provider not supported in this IDE.`,
|
|
});
|
|
}
|
|
} else {
|
|
continueConfig.modelsByRole.embed.push(...llms);
|
|
}
|
|
}
|
|
|
|
if (model.roles?.includes("rerank")) {
|
|
continueConfig.modelsByRole.rerank.push(...llms);
|
|
}
|
|
|
|
if (model.roles?.includes("subagent")) {
|
|
continueConfig.modelsByRole.subagent.push(...llms);
|
|
}
|
|
} catch (e) {
|
|
localErrors.push({
|
|
fatal: false,
|
|
message: `Failed to load model:\nName: ${model.name}\nModel: ${model.model}\nProvider: ${model.provider}\n${e instanceof Error ? e.message : e}`,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Add transformers js to the embed models in vs code if not already added
|
|
if (
|
|
ideInfo.ideType === "vscode" &&
|
|
!continueConfig.modelsByRole.embed.find(
|
|
(m) => m.providerName === "transformers.js",
|
|
)
|
|
) {
|
|
continueConfig.modelsByRole.embed.push(
|
|
new TransformersJsEmbeddingsProvider(),
|
|
);
|
|
}
|
|
|
|
if (warnAboutFreeTrial) {
|
|
localErrors.push({
|
|
fatal: false,
|
|
message:
|
|
"Model provider 'free-trial' is no longer supported, will be ignored.",
|
|
});
|
|
}
|
|
|
|
const { providers, errors: contextErrors } = loadConfigContextProviders(
|
|
config.context,
|
|
!!config.docs?.length,
|
|
ideInfo.ideType,
|
|
);
|
|
|
|
continueConfig.contextProviders = providers;
|
|
localErrors.push(...contextErrors);
|
|
|
|
// Trigger MCP server refreshes (Config is reloaded again once connected!)
|
|
const mcpManager = MCPManagerSingleton.getInstance();
|
|
|
|
const orgPolicy = PolicySingleton.getInstance().policy;
|
|
if (orgPolicy?.policy?.allowMcpServers === false) {
|
|
await mcpManager.shutdown();
|
|
} else {
|
|
const mcpOptions: InternalMcpOptions[] = (config.mcpServers ?? []).map(
|
|
(server) =>
|
|
convertYamlMcpConfigToInternalMcpOptions(server, config.requestOptions),
|
|
);
|
|
const { errors: jsonMcpErrors, mcpServers } = await loadJsonMcpConfigs(
|
|
ide,
|
|
true,
|
|
config.requestOptions,
|
|
);
|
|
localErrors.push(...jsonMcpErrors);
|
|
mcpOptions.push(...mcpServers);
|
|
mcpManager.setConnections(mcpOptions, false, { ide });
|
|
}
|
|
|
|
return { config: continueConfig, errors: localErrors };
|
|
}
|
|
|
|
export async function loadContinueConfigFromYaml(options: {
|
|
ide: IDE;
|
|
ideSettings: IdeSettings;
|
|
ideInfo: IdeInfo;
|
|
uniqueId: string;
|
|
llmLogger: ILLMLogger;
|
|
workOsAccessToken: string | undefined;
|
|
overrideConfigYaml: AssistantUnrolled | undefined;
|
|
controlPlaneClient: ControlPlaneClient;
|
|
orgScopeId: string | null;
|
|
packageIdentifier: PackageIdentifier;
|
|
}): Promise<ConfigResult<ContinueConfig>> {
|
|
const {
|
|
ide,
|
|
ideSettings,
|
|
ideInfo,
|
|
uniqueId,
|
|
llmLogger,
|
|
workOsAccessToken,
|
|
overrideConfigYaml,
|
|
controlPlaneClient,
|
|
orgScopeId,
|
|
packageIdentifier,
|
|
} = options;
|
|
|
|
const configYamlResult = await loadConfigYaml({
|
|
overrideConfigYaml,
|
|
controlPlaneClient,
|
|
orgScopeId,
|
|
ideSettings,
|
|
ide,
|
|
packageIdentifier,
|
|
});
|
|
|
|
if (!configYamlResult.config || configYamlResult.configLoadInterrupted) {
|
|
return {
|
|
errors: configYamlResult.errors,
|
|
config: undefined,
|
|
configLoadInterrupted: true,
|
|
};
|
|
}
|
|
|
|
const { config: continueConfig, errors: localErrors } =
|
|
await configYamlToContinueConfig({
|
|
config: configYamlResult.config,
|
|
ide,
|
|
ideInfo,
|
|
uniqueId,
|
|
llmLogger,
|
|
workOsAccessToken,
|
|
});
|
|
|
|
// Apply shared config
|
|
// TODO: override several of these values with user/org shared config
|
|
// Don't try catch this - has security implications and failure should be fatal
|
|
const sharedConfig = new GlobalContext().getSharedConfig();
|
|
const withShared = modifyAnyConfigWithSharedConfig(
|
|
continueConfig,
|
|
sharedConfig,
|
|
);
|
|
if (withShared.allowAnonymousTelemetry === undefined) {
|
|
withShared.allowAnonymousTelemetry = true;
|
|
}
|
|
|
|
return {
|
|
config: withShared,
|
|
errors: [...(configYamlResult.errors ?? []), ...localErrors],
|
|
configLoadInterrupted: false,
|
|
};
|
|
}
|