Files
continue/core/core.ts
2026-01-19 19:48:07 +05:30

1542 lines
48 KiB
TypeScript

import { fetchwithRequestOptions } from "@continuedev/fetch";
import * as URI from "uri-js";
import { v4 as uuidv4 } from "uuid";
import { CompletionProvider } from "./autocomplete/CompletionProvider";
import {
openedFilesLruCache,
prevFilepaths,
} from "./autocomplete/util/openedFilesLruCache";
import { ConfigHandler } from "./config/ConfigHandler";
import { addModel, deleteModel } from "./config/util";
import { getAuthUrlForTokenPage } from "./control-plane/auth/index";
import { getControlPlaneEnv } from "./control-plane/env";
import { DevDataSqliteDb } from "./data/devdataSqlite";
import { DataLogger } from "./data/log";
import { CodebaseIndexer } from "./indexing/CodebaseIndexer";
import DocsService from "./indexing/docs/DocsService";
import { countTokens } from "./llm/countTokens";
import Lemonade from "./llm/llms/Lemonade";
import Ollama from "./llm/llms/Ollama";
import { EditAggregator } from "./nextEdit/context/aggregateEdits";
import { createNewPromptFileV2 } from "./promptFiles/createNewPromptFile";
import { callTool } from "./tools/callTool";
import { ChatDescriber } from "./util/chatDescriber";
import { compactConversation } from "./util/conversationCompaction";
import { GlobalContext } from "./util/GlobalContext";
import historyManager from "./util/history";
import { editConfigFile, migrateV1DevDataFiles } from "./util/paths";
import { Telemetry } from "./util/posthog";
import {
isProcessBackgrounded,
killTerminalProcess,
markProcessAsBackgrounded,
} from "./util/processTerminalStates";
import { getSymbolsForManyFiles } from "./util/treeSitter";
import { TTS } from "./util/tts";
import {
CompleteOnboardingPayload,
ContextItemId,
ContextItemWithId,
IdeSettings,
ModelDescription,
Position,
RangeInFile,
ToolCall,
type ContextItem,
type IDE,
} from ".";
import { ConfigYaml } from "@continuedev/config-yaml";
import { getDiffFn, GitDiffCache } from "./autocomplete/snippets/gitDiffCache";
import { stringifyMcpPrompt } from "./commands/slash/mcpSlashCommand";
import { createNewAssistantFile } from "./config/createNewAssistantFile";
import {
isColocatedRulesFile,
isContinueAgentConfigFile,
isContinueConfigRelatedUri,
} from "./config/loadLocalAssistants";
import { CodebaseRulesCache } from "./config/markdown/loadCodebaseRules";
import {
setupLocalConfig,
setupProviderConfig,
setupQuickstartConfig,
} from "./config/onboarding";
import {
createNewGlobalRuleFile,
createNewWorkspaceBlockFile,
} from "./config/workspace/workspaceBlocks";
import { MCPManagerSingleton } from "./context/mcp/MCPManagerSingleton";
import { performAuth, removeMCPAuth } from "./context/mcp/MCPOauth";
import { setMdmLicenseKey } from "./control-plane/mdm/mdm";
import { myersDiff } from "./diff/myers";
import { ApplyAbortManager } from "./edit/applyAbortManager";
import { streamDiffLines } from "./edit/streamDiffLines";
import { shouldIgnore } from "./indexing/shouldIgnore";
import { walkDirCache } from "./indexing/walkDir";
import { LLMLogger } from "./llm/logger";
import { llmStreamChat } from "./llm/streamChat";
import { BeforeAfterDiff } from "./nextEdit/context/diffFormatting";
import { processSmallEdit } from "./nextEdit/context/processSmallEdit";
import { PrefetchQueue } from "./nextEdit/NextEditPrefetchQueue";
import { NextEditProvider } from "./nextEdit/NextEditProvider";
import type { FromCoreProtocol, ToCoreProtocol } from "./protocol";
import { OnboardingModes } from "./protocol/core";
import type { IMessenger, Message } from "./protocol/messenger";
import { ContinueError, ContinueErrorReason } from "./util/errors";
import { shareSession } from "./util/historyUtils";
import { Logger } from "./util/Logger.js";
export class Core {
configHandler: ConfigHandler;
codeBaseIndexer: CodebaseIndexer;
completionProvider: CompletionProvider;
nextEditProvider: NextEditProvider;
private docsService: DocsService;
private globalContext = new GlobalContext();
llmLogger = new LLMLogger();
private messageAbortControllers = new Map<string, AbortController>();
private addMessageAbortController(id: string): AbortController {
const controller = new AbortController();
this.messageAbortControllers.set(id, controller);
controller.signal.addEventListener("abort", () => {
this.messageAbortControllers.delete(id);
});
return controller;
}
private abortById(messageId: string) {
this.messageAbortControllers.get(messageId)?.abort();
}
invoke<T extends keyof ToCoreProtocol>(
messageType: T,
data: ToCoreProtocol[T][0],
): ToCoreProtocol[T][1] {
return this.messenger.invoke(messageType, data);
}
send<T extends keyof FromCoreProtocol>(
messageType: T,
data: FromCoreProtocol[T][0],
messageId?: string,
): string {
return this.messenger.send(messageType, data, messageId);
}
// TODO: It shouldn't actually need an IDE type, because this can happen
// through the messenger (it does in the case of any non-VS Code IDEs already)
constructor(
private readonly messenger: IMessenger<ToCoreProtocol, FromCoreProtocol>,
private readonly ide: IDE,
) {
try {
// Ensure .continue directory is created
migrateV1DevDataFiles();
const ideInfoPromise = messenger.request("getIdeInfo", undefined);
const ideSettingsPromise = messenger.request("getIdeSettings", undefined);
const initialSessionInfoPromise = messenger.request(
"getControlPlaneSessionInfo",
{
silent: true,
useOnboarding: false,
},
);
this.configHandler = new ConfigHandler(
this.ide,
this.llmLogger,
initialSessionInfoPromise,
);
this.docsService = DocsService.createSingleton(
this.configHandler,
this.ide,
this.messenger,
);
MCPManagerSingleton.getInstance().onConnectionsRefreshed = () => {
void this.configHandler.reloadConfig("MCP Connections refreshed");
// Refresh @mention dropdown submenu items for MCP providers
const mcpManager = MCPManagerSingleton.getInstance();
const mcpProviderNames = Array.from(mcpManager.connections.keys()).map(
(mcpId) => `mcp-${mcpId}`,
);
if (mcpProviderNames.length > 0) {
this.messenger.send("refreshSubmenuItems", {
providers: mcpProviderNames,
});
}
};
this.codeBaseIndexer = new CodebaseIndexer(
this.configHandler,
this.ide,
this.messenger,
this.globalContext.get("indexingPaused"),
);
this.configHandler.onConfigUpdate((result) => {
void (async () => {
const serializedResult =
await this.configHandler.getSerializedConfig();
this.messenger.send("configUpdate", {
result: serializedResult,
profileId:
this.configHandler.currentProfile?.profileDescription.id || null,
organizations: this.configHandler.getSerializedOrgs(),
selectedOrgId: this.configHandler.currentOrg?.id ?? null,
});
if (await this.codeBaseIndexer.wasAnyOneIndexAdded()) {
await this.codeBaseIndexer.refreshCodebaseIndex(
await this.ide.getWorkspaceDirs(),
);
}
// update additional submenu context providers registered via VSCode API
const additionalProviders =
this.configHandler.getAdditionalSubmenuContextProviders();
if (additionalProviders.length > 0) {
this.messenger.send("refreshSubmenuItems", {
providers: additionalProviders,
});
}
})();
});
// Dev Data Logger
const dataLogger = DataLogger.getInstance();
dataLogger.core = this;
dataLogger.ideInfoPromise = ideInfoPromise;
dataLogger.ideSettingsPromise = ideSettingsPromise;
void ideSettingsPromise.then((ideSettings) => {
// Index on initialization
void this.ide.getWorkspaceDirs().then(async (dirs) => {
// Respect pauseCodebaseIndexOnStart user settings
if (ideSettings.pauseCodebaseIndexOnStart) {
this.codeBaseIndexer.paused = true;
void this.messenger.request("indexProgress", {
progress: 0,
desc: "Initial Indexing Skipped",
status: "paused",
});
return;
}
// Check for disableIndexing to prevent race condition
const { config } = await this.configHandler.loadConfig();
if (!config || config.disableIndexing) {
void this.messenger.request("indexProgress", {
progress: 0,
desc: "Indexing is disabled",
status: "disabled",
});
return;
}
void this.codeBaseIndexer.refreshCodebaseIndex(dirs);
});
});
const getLlm = async () => {
const { config } = await this.configHandler.loadConfig();
if (!config) {
return undefined;
}
return config.selectedModelByRole.autocomplete ?? undefined;
};
this.completionProvider = new CompletionProvider(
this.configHandler,
ide,
getLlm,
(e) => {},
(..._) => Promise.resolve([]),
);
const codebaseRulesCache = CodebaseRulesCache.getInstance();
void codebaseRulesCache
.refresh(ide)
.catch((e) =>
Logger.error("Failed to initialize colocated rules cache"),
)
.then(() => {
void this.configHandler.reloadConfig(
"Initial codebase rules post-walkdir/load reload",
);
});
this.nextEditProvider = NextEditProvider.initialize(
this.configHandler,
ide,
getLlm,
(e) => {},
(..._) => Promise.resolve([]),
"fineTuned",
);
this.registerMessageHandlers(ideSettingsPromise);
} catch (error) {
Logger.error(error);
throw error; // Re-throw to prevent partially initialized core
}
}
/* eslint-disable max-lines-per-function */
private registerMessageHandlers(ideSettingsPromise: Promise<IdeSettings>) {
const on = this.messenger.on.bind(this.messenger);
// Note, VsCode's in-process messenger doesn't do anything with this
// It will only show for jetbrains
this.messenger.onError((message, err) => {
void Telemetry.capture("core_messenger_error", {
message: err.message,
stack: err.stack,
});
// just to prevent duplicate error messages in jetbrains (same logic in webview protocol)
if (
["llm/streamChat", "chatDescriber/describe"].includes(
message.messageType,
)
) {
return;
} else {
void this.ide.showToast("error", err.message);
}
});
on("abort", (msg) => {
this.abortById(msg.data ?? msg.messageId);
});
on("ping", (msg) => {
if (msg.data !== "ping") {
throw new Error("ping message incorrect");
}
return "pong";
});
// History
on("history/list", async (msg) => {
const localSessions = historyManager.list(msg.data);
// Check if remote sessions should be enabled based on feature flags
const shouldFetchRemote =
await this.configHandler.controlPlaneClient.shouldEnableRemoteSessions();
// Get remote sessions from control plane if feature is enabled
const remoteSessions = shouldFetchRemote
? await this.configHandler.controlPlaneClient.listRemoteSessions()
: [];
// Combine and sort by date (most recent first)
const allSessions = [...localSessions, ...remoteSessions].sort(
(a, b) =>
new Date(b.dateCreated).getTime() - new Date(a.dateCreated).getTime(),
);
// Apply limit if specified
const limit = msg.data?.limit ?? 100;
return allSessions.slice(0, limit);
});
on("history/delete", (msg) => {
historyManager.delete(msg.data.id);
});
on("history/load", (msg) => {
return historyManager.load(msg.data.id);
});
on("history/loadRemote", async (msg) => {
return this.configHandler.controlPlaneClient.loadRemoteSession(
msg.data.remoteId,
);
});
on("history/save", (msg) => {
historyManager.save(msg.data);
});
on("history/share", async (msg) => {
const session = historyManager.load(msg.data.id);
const outputDir = msg.data.outputDir;
const history = session.history.map((msg) => msg.message);
await shareSession(this.ide, history, outputDir);
});
on("history/clear", (msg) => {
historyManager.clearAll();
});
on("devdata/log", async (msg) => {
void DataLogger.getInstance().logDevData(msg.data);
});
on("config/addModel", (msg) => {
const model = msg.data.model;
addModel(model, msg.data.role);
void this.configHandler.reloadConfig(
"Model added (config/addModel message)",
);
});
on("config/deleteModel", (msg) => {
deleteModel(msg.data.title);
void this.configHandler.reloadConfig(
"Model removed (config/deleteModel message)",
);
});
on("config/newPromptFile", async (msg) => {
const { config } = await this.configHandler.loadConfig();
await createNewPromptFileV2(this.ide, config?.experimental?.promptPath);
await this.configHandler.reloadConfig(
"Prompt file created (config/newPromptFile message)",
);
});
on("config/newAssistantFile", async (msg) => {
await createNewAssistantFile(this.ide, undefined);
await this.configHandler.reloadConfig(
"Assistant file created (config/newAssistantFile message)",
);
});
on("config/addLocalWorkspaceBlock", async (msg) => {
await createNewWorkspaceBlockFile(
this.ide,
msg.data.blockType,
msg.data.baseFilename,
);
walkDirCache.invalidate();
await this.configHandler.reloadConfig(
"Local block created (config/addLocalWorkspaceBlock message)",
);
});
on("config/addGlobalRule", async (msg) => {
try {
await createNewGlobalRuleFile(this.ide, msg.data?.baseFilename);
walkDirCache.invalidate();
await this.configHandler.reloadConfig(
"Global rule created (config/addGlobalRule message)",
);
} catch (error) {
throw error;
}
});
on("config/deleteRule", async (msg) => {
try {
const filepath = msg.data.filepath;
if (
!isColocatedRulesFile(filepath) &&
!isContinueConfigRelatedUri(filepath)
) {
throw new Error("Only rule files can be deleted");
}
const fileExists = await this.ide.fileExists(filepath);
if (fileExists) {
await this.ide.removeFile(filepath);
walkDirCache.invalidate();
await this.configHandler.reloadConfig(
"Rule file deleted (config/deleteRule message)",
);
}
} catch (error) {
console.error("Failed to delete rule file:", error);
throw error;
}
});
on("config/openProfile", async (msg) => {
await this.configHandler.openConfigProfile(msg.data.profileId);
});
on("config/ideSettingsUpdate", async (msg) => {
await this.configHandler.updateIdeSettings(msg.data);
});
on("config/refreshProfiles", async (msg) => {
// User force reloading will retrigger colocated rules
const codebaseRulesCache = CodebaseRulesCache.getInstance();
await codebaseRulesCache.refresh(this.ide);
const { selectOrgId, selectProfileId, reason } = msg.data ?? {};
await this.configHandler.refreshAll(reason);
if (selectOrgId) {
await this.configHandler.setSelectedOrgId(selectOrgId, selectProfileId);
} else if (selectProfileId) {
await this.configHandler.setSelectedProfileId(selectProfileId);
}
});
on("config/updateSharedConfig", async (msg) => {
const newSharedConfig = this.globalContext.updateSharedConfig(msg.data);
await this.configHandler.reloadConfig(
"Shared config update (config/updateSharedConfig message)",
);
return newSharedConfig;
});
on("config/updateSelectedModel", async (msg) => {
const newSelectedModels = this.globalContext.updateSelectedModel(
msg.data.profileId,
msg.data.role,
msg.data.title,
);
await this.configHandler.reloadConfig(
"Selected model update (config/updateSelectedModel message)",
);
return newSelectedModels;
});
on("controlPlane/openUrl", async (msg) => {
const env = await getControlPlaneEnv(this.ide.getIdeSettings());
const urlPath = msg.data.path.startsWith("/")
? msg.data.path.slice(1)
: msg.data.path;
let url;
if (msg.data.orgSlug) {
url = `${env.APP_URL}organizations/${msg.data.orgSlug}/${urlPath}`;
} else {
url = `${env.APP_URL}${urlPath}`;
}
await this.messenger.request("openUrl", url);
});
on("controlPlane/getEnvironment", async (msg) => {
return await getControlPlaneEnv(this.ide.getIdeSettings());
});
on("controlPlane/getCreditStatus", async (msg) => {
return this.configHandler.controlPlaneClient.getCreditStatus();
});
on("mcp/reloadServer", async (msg) => {
await MCPManagerSingleton.getInstance().refreshConnection(msg.data.id);
});
on("mcp/setServerEnabled", async (msg) => {
const { id, enabled } = msg.data;
await MCPManagerSingleton.getInstance().setEnabled(id, enabled);
});
on("mcp/getPrompt", async (msg) => {
const { serverName, promptName, args } = msg.data;
const prompt = await MCPManagerSingleton.getInstance().getPrompt(
serverName,
promptName,
args,
);
const stringifiedPrompt = stringifyMcpPrompt(prompt);
return {
prompt: stringifiedPrompt,
description: prompt.description,
};
});
on("mcp/startAuthentication", async (msg) => {
await new Promise((resolve) => setTimeout(resolve, 5000));
MCPManagerSingleton.getInstance().setStatus(
msg.data.serverId,
"authenticating",
);
const status = await performAuth(
msg.data.serverId,
msg.data.serverUrl,
this.ide,
);
if (status === "AUTHORIZED") {
await MCPManagerSingleton.getInstance().refreshConnection(
msg.data.serverId,
);
}
});
on("mcp/removeAuthentication", async (msg) => {
removeMCPAuth(msg.data.serverUrl, this.ide);
await MCPManagerSingleton.getInstance().refreshConnection(
msg.data.serverId,
);
});
// Context providers
on("context/addDocs", async (msg) => {
void this.docsService.indexAndAdd(msg.data);
});
on("context/removeDocs", async (msg) => {
await this.docsService.delete(msg.data.startUrl);
});
on("context/indexDocs", async (msg) => {
await this.docsService.syncDocsWithPrompt(msg.data.reIndex);
});
on("context/loadSubmenuItems", async (msg) => {
const { config } = await this.configHandler.loadConfig();
if (!config) {
return [];
}
try {
const items = await config.contextProviders
?.find((provider) => provider.description.title === msg.data.title)
?.loadSubmenuItems({
config,
ide: this.ide,
fetch: (url, init) =>
fetchwithRequestOptions(url, init, config.requestOptions),
});
return items || [];
} catch (e) {
Logger.error(e);
return [];
}
});
on("context/getContextItems", this.getContextItems.bind(this));
on("context/getSymbolsForFiles", async (msg) => {
const { uris } = msg.data;
return await getSymbolsForManyFiles(uris, this.ide);
});
on("config/getSerializedProfileInfo", async (msg) => {
return {
result: await this.configHandler.getSerializedConfig(),
profileId:
this.configHandler.currentProfile?.profileDescription.id ?? null,
organizations: this.configHandler.getSerializedOrgs(),
selectedOrgId: this.configHandler.currentOrg?.id ?? null,
};
});
on("llm/streamChat", (msg) => {
const abortController = this.addMessageAbortController(msg.messageId);
return llmStreamChat(
this.configHandler,
abortController,
msg,
this.ide,
this.messenger,
);
});
on("llm/complete", async (msg) => {
const { config } = await this.configHandler.loadConfig();
const model = config?.selectedModelByRole.chat;
if (!model) {
throw new Error("No chat model selected");
}
const abortController = this.addMessageAbortController(msg.messageId);
const completion = await model.complete(
msg.data.prompt,
abortController.signal,
msg.data.completionOptions,
);
return completion;
});
on("llm/listModels", this.handleListModels.bind(this));
on("llm/compileChat", async (msg) => {
const { messages, options } = msg.data;
const model = (await this.configHandler.loadConfig()).config
?.selectedModelByRole.chat;
if (!model) {
throw new Error("No chat model selected");
}
return model.compileChatMessages(messages, options);
});
// Provide messenger to utils so they can interact with GUI + state
TTS.messenger = this.messenger;
ChatDescriber.messenger = this.messenger;
on("tts/kill", async () => {
void TTS.kill();
});
on("chatDescriber/describe", async (msg) => {
const currentModel = (await this.configHandler.loadConfig()).config
?.selectedModelByRole.chat;
if (!currentModel) {
throw new Error("No chat model selected");
}
return await ChatDescriber.describe(currentModel, {}, msg.data.text);
});
on("conversation/compact", async (msg) => {
const currentModel = (await this.configHandler.loadConfig()).config
?.selectedModelByRole.chat;
if (!currentModel) {
throw new Error("No chat model selected");
}
try {
await compactConversation({
sessionId: msg.data.sessionId,
index: msg.data.index,
historyManager,
currentModel,
});
return undefined;
} catch (error) {
Logger.error(`Error compacting conversation: ${error}`);
return undefined;
}
});
// Autocomplete
on("autocomplete/complete", async (msg) => {
const outcome =
await this.completionProvider.provideInlineCompletionItems(
msg.data,
undefined,
);
return outcome ? [outcome.completion] : [];
});
on("autocomplete/accept", async (msg) => {
this.completionProvider.accept(msg.data.completionId);
});
on("autocomplete/cancel", async (msg) => {
this.completionProvider.cancel();
});
// Next Edit
on("nextEdit/predict", async (msg) => {
const outcome = await this.nextEditProvider.provideInlineCompletionItems(
msg.data.input,
undefined,
{
withChain: msg.data.options?.withChain ?? false,
usingFullFileDiff: msg.data.options?.usingFullFileDiff ?? true,
},
);
return outcome;
// ? [outcome.completion, outcome.originalEditableRange]
});
on("nextEdit/accept", async (msg) => {
console.log("nextEdit/accept");
this.nextEditProvider.accept(msg.data.completionId);
});
on("nextEdit/reject", async (msg) => {
console.log("nextEdit/reject");
this.nextEditProvider.reject(msg.data.completionId);
});
on("nextEdit/startChain", async (msg) => {
console.log("nextEdit/startChain");
NextEditProvider.getInstance().startChain();
return;
});
on("nextEdit/deleteChain", async (msg) => {
console.log("nextEdit/deleteChain");
await NextEditProvider.getInstance().deleteChain();
return;
});
on("nextEdit/isChainAlive", async (msg) => {
console.log("nextEdit/isChainAlive");
return NextEditProvider.getInstance().chainExists();
});
on("nextEdit/queue/getProcessedCount", async (msg) => {
console.log("nextEdit/queue/getProcessedCount");
const queue = PrefetchQueue.getInstance();
console.log(queue.processedCount);
return queue.processedCount;
});
on("nextEdit/queue/dequeueProcessed", async (msg) => {
console.log("nextEdit/queue/dequeueProcessed");
const queue = PrefetchQueue.getInstance();
return queue.dequeueProcessed() || null;
});
// NOTE: This is not used unless prefetch is used.
// At this point this is not used because I opted to rely on the model to return multiple diffs than to use prefetching.
on("nextEdit/queue/processOne", async (msg) => {
console.log("nextEdit/queue/processOne");
const { ctx, recentlyVisitedRanges, recentlyEditedRanges } = msg.data;
const queue = PrefetchQueue.getInstance();
await queue.process({
...ctx,
recentlyVisitedRanges,
recentlyEditedRanges,
});
return;
});
on("nextEdit/queue/clear", async (msg) => {
console.log("nextEdit/queue/clear");
const queue = PrefetchQueue.getInstance();
queue.clear();
return;
});
on("nextEdit/queue/abort", async (msg) => {
console.log("nextEdit/queue/abort");
const queue = PrefetchQueue.getInstance();
queue.abort();
return;
});
on("streamDiffLines", async (msg) => {
const { config } = await this.configHandler.loadConfig();
if (!config) {
throw new Error("Failed to load config");
}
const { data } = msg;
// Title can be an edit, chat, or apply model
// Fall back to chat
const llm =
config.modelsByRole.edit.find((m) => m.title === data.modelTitle) ??
config.modelsByRole.apply.find((m) => m.title === data.modelTitle) ??
config.modelsByRole.chat.find((m) => m.title === data.modelTitle) ??
config.selectedModelByRole.chat;
if (!llm) {
throw new Error("No model selected");
}
const abortManager = ApplyAbortManager.getInstance();
const abortController = abortManager.get(
data.fileUri ?? "current-file-stream",
); // not super important since currently cancelling apply will cancel all streams it's one file at a time
return streamDiffLines(
data,
llm,
abortController,
undefined,
data.includeRulesInSystemMessage ? config.rules : undefined,
);
});
on("getDiffLines", (msg) => {
return myersDiff(msg.data.oldContent, msg.data.newContent);
});
on("cancelApply", async (msg) => {
const abortManager = ApplyAbortManager.getInstance();
abortManager.clear(); // for now abort all streams
});
on("onboarding/complete", this.handleCompleteOnboarding.bind(this));
on("addAutocompleteModel", this.handleAddAutocompleteModel.bind(this));
on("stats/getTokensPerDay", async (msg) => {
const rows = await DevDataSqliteDb.getTokensPerDay();
return rows;
});
on("stats/getTokensPerModel", async (msg) => {
const rows = await DevDataSqliteDb.getTokensPerModel();
return rows;
});
on("index/forceReIndex", async ({ data }) => {
const { config } = await this.configHandler.loadConfig();
if (!config || config.disableIndexing) {
return; // TODO silent in case of commands?
}
walkDirCache.invalidate();
if (data?.shouldClearIndexes) {
await this.codeBaseIndexer.clearIndexes();
}
const dirs = data?.dirs ?? (await this.ide.getWorkspaceDirs());
await this.codeBaseIndexer.refreshCodebaseIndex(dirs);
});
on("index/setPaused", (msg) => {
this.globalContext.update("indexingPaused", msg.data);
// Update using the new setter instead of token
this.codeBaseIndexer.paused = msg.data;
});
on("index/indexingProgressBarInitialized", async (msg) => {
// Triggered when progress bar is initialized.
// If a non-default state has been stored, update the indexing display to that state
const currentState = this.codeBaseIndexer.currentIndexingState;
if (currentState.status !== "loading") {
void this.messenger.request("indexProgress", currentState);
}
});
// File changes - TODO - remove remaining logic for these from IDEs where possible
on("files/changed", this.handleFilesChanged.bind(this));
const refreshIfNotIgnored = async (uris: string[]) => {
const toRefresh: string[] = [];
for (const uri of uris) {
const ignore = await shouldIgnore(uri, this.ide);
if (!ignore) {
toRefresh.push(uri);
}
}
if (toRefresh.length > 0) {
this.messenger.send("refreshSubmenuItems", {
providers: ["file"],
});
const { config } = await this.configHandler.loadConfig();
if (config && !config.disableIndexing) {
await this.codeBaseIndexer.refreshCodebaseIndexFiles(toRefresh);
}
}
};
on("files/created", async ({ data }) => {
if (!data?.uris?.length) {
return;
}
walkDirCache.invalidate();
void refreshIfNotIgnored(data.uris);
const colocatedRulesUris = data.uris.filter(isColocatedRulesFile);
const nonColocatedRuleUris = data.uris.filter(
(uri) => !isColocatedRulesFile(uri),
);
if (colocatedRulesUris) {
const rulesCache = CodebaseRulesCache.getInstance();
void Promise.all(
colocatedRulesUris.map((uri) => rulesCache.update(this.ide, uri)),
).then(() => {
void this.configHandler.reloadConfig("Codebase rule file created");
});
}
// If it's a local config being created, we want to reload all configs so it shows up in the list
if (nonColocatedRuleUris.some(isContinueAgentConfigFile)) {
await this.configHandler.refreshAll("Local config file created");
} else if (nonColocatedRuleUris.some(isContinueConfigRelatedUri)) {
await this.configHandler.reloadConfig(
".continue config-related file created",
);
}
});
on("files/deleted", async ({ data }) => {
if (!data?.uris?.length) {
return;
}
walkDirCache.invalidate();
void refreshIfNotIgnored(data.uris);
const colocatedRulesUris = data.uris.filter(isColocatedRulesFile);
const nonColocatedRuleUris = data.uris.filter(
(uri) => !isColocatedRulesFile(uri),
);
if (colocatedRulesUris) {
const rulesCache = CodebaseRulesCache.getInstance();
void Promise.all(
colocatedRulesUris.map((uri) => rulesCache.remove(uri)),
).then(() => {
void this.configHandler.reloadConfig("Codebase rule file deleted");
});
}
// If it's a local config being deleted, we want to reload all configs so it disappears from the list
if (nonColocatedRuleUris.some(isContinueAgentConfigFile)) {
await this.configHandler.refreshAll("Local config file deleted");
} else if (nonColocatedRuleUris.some(isContinueConfigRelatedUri)) {
await this.configHandler.reloadConfig(
".continue config-related file deleted",
);
}
});
on("files/closed", async ({ data }) => {
console.debug("deleteChain called from files/closed");
await NextEditProvider.getInstance().deleteChain();
try {
const fileUris = await this.ide.getOpenFiles();
if (fileUris) {
const filepaths = fileUris.map((uri) => uri.toString());
if (!prevFilepaths.filepaths.length) {
prevFilepaths.filepaths = filepaths;
}
// If there is a removal, including if the number of tabs is the same (which can happen with temp tabs)
if (filepaths.length <= prevFilepaths.filepaths.length) {
// Remove files from cache that are no longer open (i.e. in the cache but not in the list of opened tabs)
for (const [key, _] of openedFilesLruCache.entriesDescending()) {
if (!filepaths.includes(key)) {
openedFilesLruCache.delete(key);
}
}
}
prevFilepaths.filepaths = filepaths;
}
} catch (e) {
Logger.error(
`didChangeVisibleTextEditors: failed to update openedFilesLruCache`,
);
}
if (data.uris) {
this.messenger.send("didCloseFiles", {
uris: data.uris,
});
}
});
on("files/opened", async ({ data: { uris } }) => {
if (uris) {
for (const filepath of uris) {
try {
const ignore = await shouldIgnore(filepath, this.ide);
if (!ignore) {
// Set the active file as most recently used (need to force recency update by deleting and re-adding)
if (openedFilesLruCache.has(filepath)) {
openedFilesLruCache.delete(filepath);
}
openedFilesLruCache.set(filepath, filepath);
}
} catch (e) {
Logger.error(
`files/opened: failed to update openedFiles cache for ${filepath}`,
);
}
}
}
});
on("files/smallEdit", async ({ data }) => {
const EDIT_AGGREGATION_OPTIONS = {
deltaT: 1.0,
deltaL: 5,
maxEdits: 500,
maxDuration: 120.0,
contextSize: 5,
};
EditAggregator.getInstance(
EDIT_AGGREGATION_OPTIONS,
(
beforeAfterdiff: BeforeAfterDiff,
cursorPosBeforeEdit: Position,
cursorPosAfterPrevEdit: Position,
) => {
void processSmallEdit(
beforeAfterdiff,
cursorPosBeforeEdit,
cursorPosAfterPrevEdit,
data.configHandler,
data.getDefsFromLspFunction,
this.ide,
);
},
);
const workspaceDir =
data.actions.length > 0 ? data.actions[0].workspaceDir : undefined;
// Store the latest context data
const instance = EditAggregator.getInstance();
(instance as any).latestContextData = {
configHandler: data.configHandler,
getDefsFromLspFunction: data.getDefsFromLspFunction,
recentlyEditedRanges: data.recentlyEditedRanges,
recentlyVisitedRanges: data.recentlyVisitedRanges,
workspaceDir: workspaceDir,
};
// queueMicrotask prevents blocking the UI thread during typing
queueMicrotask(() => {
void EditAggregator.getInstance().processEdits(data.actions);
});
});
// Docs, etc. indexing
on("indexing/reindex", async (msg) => {
if (msg.data.type === "docs") {
void this.docsService.reindexDoc(msg.data.id);
}
});
on("indexing/abort", async (msg) => {
if (msg.data.type === "docs") {
this.docsService.abort(msg.data.id);
}
});
on("indexing/setPaused", async (msg) => {
if (msg.data.type === "docs") {
}
});
on("docs/initStatuses", async (msg) => {
void this.docsService.initStatuses();
});
on("docs/getDetails", async (msg) => {
return await this.docsService.getDetails(msg.data.startUrl);
});
on("docs/getIndexedPages", async (msg) => {
const pages = await this.docsService.getIndexedPages(msg.data.startUrl);
return Array.from(pages);
});
on("didChangeSelectedProfile", async (msg) => {
if (msg.data.id) {
await this.configHandler.setSelectedProfileId(msg.data.id);
}
});
on("didChangeSelectedOrg", async (msg) => {
if (msg.data.id) {
await this.configHandler.setSelectedOrgId(
msg.data.id,
msg.data.profileId || undefined,
);
}
});
on("didChangeControlPlaneSessionInfo", async (msg) => {
this.messenger.send("sessionUpdate", {
sessionInfo: msg.data.sessionInfo,
});
await this.configHandler.updateControlPlaneSessionInfo(
msg.data.sessionInfo,
);
});
on("auth/getAuthUrl", async (msg) => {
const url = await getAuthUrlForTokenPage(
ideSettingsPromise,
msg.data.useOnboarding,
);
return { url };
});
on("tools/call", async ({ data: { toolCall } }) =>
this.handleToolCall(toolCall),
);
on(
"tools/evaluatePolicy",
async ({ data: { toolName, basePolicy, parsedArgs, processedArgs } }) => {
const { config } = await this.configHandler.loadConfig();
if (!config) {
throw new Error("Config not loaded");
}
const tool = config.tools.find((t) => t.function.name === toolName);
if (!tool) {
return { policy: basePolicy };
}
// Extract display value for specific tools
let displayValue: string | undefined;
if (toolName === "runTerminalCommand" && parsedArgs.command) {
displayValue = parsedArgs.command as string;
}
if (tool.evaluateToolCallPolicy) {
const evaluatedPolicy = tool.evaluateToolCallPolicy(
basePolicy,
parsedArgs,
processedArgs,
);
return { policy: evaluatedPolicy, displayValue };
}
return { policy: basePolicy, displayValue };
},
);
on("tools/preprocessArgs", async ({ data: { toolName, args } }) => {
const { config } = await this.configHandler.loadConfig();
if (!config) {
throw new Error("Config not loaded");
}
const tool = config?.tools.find((t) => t.function.name === toolName);
if (!tool) {
throw new Error(`Tool ${toolName} not found`);
}
try {
const preprocessedArgs = await tool.preprocessArgs?.(args, {
ide: this.ide,
});
return {
preprocessedArgs,
};
} catch (e) {
let errorReason =
e instanceof ContinueError ? e.reason : ContinueErrorReason.Unknown;
let errorMessage =
e instanceof Error
? e.message
: `Error preprocessing tool call args for ${toolName}\n${JSON.stringify(args)}`;
return {
preprocessedArgs: undefined,
errorReason,
errorMessage,
};
}
});
on("isItemTooBig", async ({ data: { item } }) => {
return this.isItemTooBig(item);
});
// Process state handlers
on("process/markAsBackgrounded", async ({ data: { toolCallId } }) => {
markProcessAsBackgrounded(toolCallId);
});
on(
"process/isBackgrounded",
async ({ data: { toolCallId }, messageId }) => {
const isBackgrounded = isProcessBackgrounded(toolCallId);
return isBackgrounded; // Return true to indicate the message was handled successfully
},
);
on("process/killTerminalProcess", async ({ data: { toolCallId } }) => {
await killTerminalProcess(toolCallId);
});
on("mdm/setLicenseKey", ({ data: { licenseKey } }) => {
const isValid = setMdmLicenseKey(licenseKey);
return isValid;
});
}
private async handleToolCall(toolCall: ToolCall) {
const { config } = await this.configHandler.loadConfig();
if (!config) {
throw new Error("Config not loaded");
}
const tool = config.tools.find(
(t) => t.function.name === toolCall.function.name,
);
if (!tool) {
throw new Error(`Tool ${toolCall.function.name} not found`);
}
if (!config.selectedModelByRole.chat) {
throw new Error("No chat model selected");
}
// Define a callback for streaming output updates
const onPartialOutput = (params: {
toolCallId: string;
contextItems: ContextItem[];
}) => {
this.messenger.send("toolCallPartialOutput", params);
};
const result = await callTool(tool, toolCall, {
config,
ide: this.ide,
llm: config.selectedModelByRole.chat,
fetch: (url, init) =>
fetchwithRequestOptions(url, init, config.requestOptions),
tool,
toolCallId: toolCall.id,
onPartialOutput,
codeBaseIndexer: this.codeBaseIndexer,
});
return result;
}
private async isItemTooBig(item: ContextItemWithId) {
const { config } = await this.configHandler.loadConfig();
if (!config) {
return false;
}
const llm = config?.selectedModelByRole.chat;
if (!llm) {
throw new Error("No chat model selected");
}
const tokens = countTokens(item.content, llm.model);
if (tokens > llm.contextLength - llm.completionOptions!.maxTokens!) {
return true;
}
return false;
}
private handleAddAutocompleteModel(
msg: Message<{
model: ModelDescription;
}>,
) {
const model = msg.data.model;
editConfigFile(
(config) => {
return {
...config,
tabAutocompleteModel: model,
};
},
(config) => ({
...config,
models: [
...(config.models ?? []),
{
name: model.title,
provider: model.provider,
model: model.model,
apiKey: model.apiKey,
roles: ["autocomplete"],
apiBase: model.apiBase,
},
],
}),
);
void this.configHandler.reloadConfig("Autocomplete model added");
}
private async handleFilesChanged({
data,
}: Message<{
uris?: string[];
}>): Promise<void> {
if (data?.uris?.length) {
const diffCache = GitDiffCache.getInstance(getDiffFn(this.ide));
diffCache.invalidate();
walkDirCache.invalidate(); // safe approach for now - TODO - only invalidate on relevant changes
const currentProfileUri =
this.configHandler.currentProfile?.profileDescription.uri ?? "";
for (const uri of data.uris) {
if (URI.equal(uri, currentProfileUri)) {
// Trigger a toast notification to provide UI feedback that config has been updated
const showToast =
this.globalContext.get("showConfigUpdateToast") ?? true;
if (showToast) {
const selection = await this.ide.showToast(
"info",
"Config updated",
"Don't show again",
);
if (selection === "Don't show again") {
this.globalContext.update("showConfigUpdateToast", false);
}
}
await this.configHandler.reloadConfig(
"Current profile config file updated",
);
continue;
}
if (isColocatedRulesFile(uri)) {
try {
const codebaseRulesCache = CodebaseRulesCache.getInstance();
void codebaseRulesCache.update(this.ide, uri).then(() => {
void this.configHandler.reloadConfig("Codebase rule update");
});
} catch (e) {
Logger.error(`Failed to update codebase rule: ${e}`);
}
} else if (isContinueConfigRelatedUri(uri)) {
await this.configHandler.reloadConfig(
"Local config-related file updated",
);
} else if (
uri.endsWith(".continueignore") ||
uri.endsWith(".gitignore")
) {
// Reindex the workspaces
this.invoke("index/forceReIndex", {
shouldClearIndexes: true,
});
} else {
const { config } = await this.configHandler.loadConfig();
if (config && !config.disableIndexing) {
// Reindex the file
const ignore = await shouldIgnore(uri, this.ide);
if (!ignore) {
await this.codeBaseIndexer.refreshCodebaseIndexFiles([uri]);
}
}
}
}
}
}
private async handleListModels(msg: Message<{ title: string }>) {
const { config } = await this.configHandler.loadConfig();
if (!config) {
return [];
}
const model =
config.modelsByRole.chat.find(
(model) => model.title === msg.data.title,
) ??
config.modelsByRole.chat.find((model) =>
model.title?.startsWith(msg.data.title),
);
try {
if (model) {
return await model.listModels();
} else {
if (msg.data.title === "Ollama") {
const models = await new Ollama({ model: "" }).listModels();
return models;
} else if (msg.data.title === "Lemonade") {
const models = await new Lemonade({ model: "" }).listModels();
return models;
} else {
return undefined;
}
}
} catch (e) {
console.debug(`Error listing Ollama models: ${e}`);
return undefined;
}
}
private async handleCompleteOnboarding(
msg: Message<CompleteOnboardingPayload>,
) {
const { mode, provider, apiKey } = msg.data;
let editConfigYamlCallback: (config: ConfigYaml) => ConfigYaml;
switch (mode) {
case OnboardingModes.LOCAL:
editConfigYamlCallback = setupLocalConfig;
break;
case OnboardingModes.API_KEY:
if (provider && apiKey) {
editConfigYamlCallback = (config: ConfigYaml) =>
setupProviderConfig(config, provider, apiKey);
} else {
editConfigYamlCallback = setupQuickstartConfig;
}
break;
default:
Logger.error(`Invalid mode: ${mode}`);
editConfigYamlCallback = (config) => config;
}
editConfigFile((c) => c, editConfigYamlCallback);
void this.configHandler.reloadConfig("Onboarding completed");
}
private getContextItems = async (
msg: Message<{
name: string;
query: string;
fullInput: string;
selectedCode: RangeInFile[];
isInAgentMode: boolean;
}>,
) => {
const { config } = await this.configHandler.loadConfig();
if (!config) {
return [];
}
const { name, query, fullInput, selectedCode } = msg.data;
const llm = (await this.configHandler.loadConfig()).config
?.selectedModelByRole.chat;
if (!llm) {
throw new Error("No chat model selected");
}
const provider = config.contextProviders?.find(
(provider) => provider.description.title === name,
);
if (!provider) {
return [];
}
try {
void Telemetry.capture("context_provider_get_context_items", {
name: provider.description.title,
});
const items = await provider.getContextItems(query, {
config,
llm,
embeddingsProvider: config.selectedModelByRole.embed,
fullInput,
ide: this.ide,
selectedCode,
reranker: config.selectedModelByRole.rerank,
fetch: (url, init) =>
// Important note: context providers fetch uses global request options not LLM request options
// Because LLM calls are handled separately
fetchwithRequestOptions(url, init, config.requestOptions),
isInAgentMode: msg.data.isInAgentMode,
});
void Telemetry.capture(
"useContextProvider",
{
name: provider.description.title,
},
true,
);
return items.map((item) => {
const id: ContextItemId = {
providerTitle: provider.description.title,
itemId: uuidv4(),
};
return { ...item, id };
});
} catch (e) {
let knownError = false;
if (e instanceof Error) {
// After removing transformers JS embeddings provider from jetbrains
// Should no longer see this error
// if (e.message.toLowerCase().includes("embeddings provider")) {
// knownError = true;
// const toastOption = "See Docs";
// void this.ide
// .showToast(
// "error",
// `Set up an embeddings model to use @${name}`,
// toastOption,
// )
// .then((userSelection) => {
// if (userSelection === toastOption) {
// void this.ide.openUrl(
// "https://docs.continue.dev/customize/model-roles/embeddings",
// );
// }
// });
// }
}
if (!knownError) {
void this.ide.showToast(
"error",
`Error getting context items from ${name}: ${e}`,
);
}
return [];
}
};
}