diff --git a/core/config/ConfigHandler.ts b/core/config/ConfigHandler.ts index 97f12d0e60..3c217bbb5b 100644 --- a/core/config/ConfigHandler.ts +++ b/core/config/ConfigHandler.ts @@ -16,10 +16,9 @@ import Ollama from "../llm/llms/Ollama.js"; import { GlobalContext } from "../util/GlobalContext.js"; import { getConfigJsonPath, getConfigYamlPath } from "../util/paths.js"; -import { ConfigResult, ConfigYaml } from "@continuedev/config-yaml"; +import { ConfigResult, ConfigYaml, FullSlug } from "@continuedev/config-yaml"; import * as YAML from "yaml"; -import { controlPlaneEnv } from "../control-plane/env.js"; -import { usePlatform } from "../control-plane/flags.js"; +import { getControlPlaneEnv, useHub } from "../control-plane/env.js"; import { localPathToUri } from "../util/pathToUri.js"; import { LOCAL_ONBOARDING_CHAT_MODEL, @@ -27,7 +26,7 @@ import { } from "./onboarding.js"; import ControlPlaneProfileLoader from "./profile/ControlPlaneProfileLoader.js"; import LocalProfileLoader from "./profile/LocalProfileLoader.js"; -import PlatformProfileLoader from "./profile/PlatformProfileLoader.js"; +import PlatformProfileLoader from "./profile/PlatformProfileLoader.1.js"; import { ProfileDescription, ProfileLifecycleManager, @@ -63,7 +62,7 @@ export class ConfigHandler { controlPlaneClient, writeLog, ); - this.profiles = [new ProfileLifecycleManager(localProfileLoader)]; + this.profiles = [new ProfileLifecycleManager(localProfileLoader, this.ide)]; this.selectedProfileId = localProfileLoader.description.id; // Always load local profile immediately in case control plane doesn't load @@ -107,7 +106,8 @@ export class ConfigHandler { await this.ide.openFile(localPathToUri(getConfigJsonPath())); } } else { - await this.ide.openUrl(`${controlPlaneEnv.APP_URL}${openProfileId}`); + const env = await getControlPlaneEnv(this.ide.getIdeSettings()); + await this.ide.openUrl(`${env.APP_URL}${openProfileId}`); } } @@ -145,7 +145,7 @@ export class ConfigHandler { ...this.profiles.filter( (profile) => profile.profileDescription.id === "local", ), - new ProfileLifecycleManager(profileLoader), + new ProfileLifecycleManager(profileLoader, this.ide), ]; }), ); @@ -177,15 +177,49 @@ export class ConfigHandler { } private platformProfilesRefreshInterval: NodeJS.Timeout | undefined; + // We use this to keep track of whether we should reload the assistants + private lastFullSlugsList: FullSlug[] = []; + + private fullSlugsListsDiffer(a: FullSlug[], b: FullSlug[]): boolean { + if (a.length !== b.length) { + return true; + } + for (let i = 0; i < a.length; i++) { + if (a[i].ownerSlug !== b[i].ownerSlug) { + return true; + } + if (a[i].packageSlug !== b[i].packageSlug) { + return true; + } + if (a[i].versionSlug !== b[i].versionSlug) { + return true; + } + } + return false; + } private async fetchControlPlaneProfiles() { - if (usePlatform()) { + if (await useHub(this.ideSettingsPromise)) { clearInterval(this.platformProfilesRefreshInterval); await this.loadPlatformProfiles(); - this.platformProfilesRefreshInterval = setInterval( - this.loadPlatformProfiles.bind(this), - PlatformProfileLoader.RELOAD_INTERVAL, - ); + + // Every 5 seconds we ask the platform whether there are any assistant updates in the last 5 seconds + // If so, we do the full (more expensive) reload + this.platformProfilesRefreshInterval = setInterval(async () => { + const newFullSlugsList = + await this.controlPlaneClient.listAssistantFullSlugs(); + + if (newFullSlugsList) { + const shouldReload = this.fullSlugsListsDiffer( + newFullSlugsList, + this.lastFullSlugsList, + ); + if (shouldReload) { + await this.loadPlatformProfiles(); + } + this.lastFullSlugsList = newFullSlugsList; + } + }, PlatformProfileLoader.RELOAD_INTERVAL); } else { this.controlPlaneClient .listWorkspaces() @@ -203,7 +237,9 @@ export class ConfigHandler { this.writeLog, this.reloadConfig.bind(this), ); - this.profiles.push(new ProfileLifecycleManager(profileLoader)); + this.profiles.push( + new ProfileLifecycleManager(profileLoader, this.ide), + ); }); this.notifyProfileListeners( @@ -258,11 +294,12 @@ export class ConfigHandler { void this.reloadConfig(); } - updateControlPlaneSessionInfo( + async updateControlPlaneSessionInfo( sessionInfo: ControlPlaneSessionInfo | undefined, ) { this.controlPlaneClient = new ControlPlaneClient( Promise.resolve(sessionInfo), + this.ideSettingsPromise, ); this.fetchControlPlaneProfiles().catch((e) => { console.error("Failed to fetch control plane profiles: ", e); diff --git a/core/config/ProfileLifecycleManager.ts b/core/config/ProfileLifecycleManager.ts index 2d3f57da06..fa76c6b55a 100644 --- a/core/config/ProfileLifecycleManager.ts +++ b/core/config/ProfileLifecycleManager.ts @@ -7,6 +7,7 @@ import { BrowserSerializedContinueConfig, ContinueConfig, IContextProvider, + IDE, } from "../index.js"; import { finalToBrowserConfig } from "./load.js"; @@ -14,7 +15,7 @@ import { IProfileLoader } from "./profile/IProfileLoader.js"; export interface ProfileDescription { fullSlug: FullSlug; - profileType: string; + profileType: "control-plane" | "local" | "platform"; title: string; id: string; errors: ConfigValidationError[] | undefined; @@ -25,7 +26,10 @@ export class ProfileLifecycleManager { private savedBrowserConfigResult?: ConfigResult; private pendingConfigPromise?: Promise>; - constructor(private readonly profileLoader: IProfileLoader) {} + constructor( + private readonly profileLoader: IProfileLoader, + private readonly ide: IDE, + ) {} get profileDescription(): ProfileDescription { return this.profileLoader.description; @@ -93,7 +97,10 @@ export class ProfileLifecycleManager { config: undefined, }; } - const serializedConfig = finalToBrowserConfig(result.config); + const serializedConfig = await finalToBrowserConfig( + result.config, + this.ide, + ); return { ...result, config: serializedConfig, diff --git a/core/config/load.ts b/core/config/load.ts index c1c3c42617..af71ca1725 100644 --- a/core/config/load.ts +++ b/core/config/load.ts @@ -63,7 +63,6 @@ import { } from "../util/paths"; import { ConfigResult, ConfigValidationError } from "@continuedev/config-yaml"; -import { usePlatform } from "../control-plane/flags"; import { defaultContextProvidersJetBrains, defaultContextProvidersVsCode, @@ -72,8 +71,9 @@ import { } from "./default"; import { getSystemPromptDotFile } from "./getSystemPromptDotFile"; // import { isSupportedLanceDbCpuTarget } from "./util"; -import { validateConfig } from "./validation.js"; +import { useHub } from "../control-plane/env"; import { localPathToUri } from "../util/pathToUri"; +import { validateConfig } from "./validation.js"; function resolveSerializedConfig(filepath: string): SerializedContinueConfig { let content = fs.readFileSync(filepath, "utf8"); @@ -547,9 +547,10 @@ async function intermediateToFinalConfig( return { config: continueConfig, errors }; } -function finalToBrowserConfig( +async function finalToBrowserConfig( final: ContinueConfig, -): BrowserSerializedContinueConfig { + ide: IDE, +): Promise { return { allowAnonymousTelemetry: final.allowAnonymousTelemetry, models: final.models.map((m) => ({ @@ -582,7 +583,7 @@ function finalToBrowserConfig( experimental: final.experimental, docs: final.docs, tools: final.tools, - usePlatform: usePlatform(), + usePlatform: await useHub(ide.getIdeSettings()), }; } diff --git a/core/config/profile/LocalProfileLoader.ts b/core/config/profile/LocalProfileLoader.ts index bcc82408dc..cb8694bbc8 100644 --- a/core/config/profile/LocalProfileLoader.ts +++ b/core/config/profile/LocalProfileLoader.ts @@ -10,7 +10,7 @@ export default class LocalProfileLoader implements IProfileLoader { static ID = "local"; description: ProfileDescription = { id: LocalProfileLoader.ID, - profileType: LocalProfileLoader.ID, + profileType: "local", fullSlug: { ownerSlug: "", packageSlug: "", diff --git a/core/config/profile/PlatformProfileLoader.ts b/core/config/profile/PlatformProfileLoader.ts index 92e9456302..f703b2a2c2 100644 --- a/core/config/profile/PlatformProfileLoader.ts +++ b/core/config/profile/PlatformProfileLoader.ts @@ -1,12 +1,10 @@ import { ConfigYaml } from "@continuedev/config-yaml/dist/schemas/index.js"; -import * as YAML from "yaml"; import { ControlPlaneClient } from "../../control-plane/client.js"; import { ContinueConfig, IDE, IdeSettings } from "../../index.js"; import { ConfigResult } from "@continuedev/config-yaml"; import { ProfileDescription } from "../ProfileLifecycleManager.js"; -import { clientRenderHelper } from "../yaml/clientRender.js"; import doLoadConfig from "./doLoadConfig.js"; import { IProfileLoader } from "./IProfileLoader.js"; @@ -21,7 +19,7 @@ export interface PlatformConfigMetadata { } export default class PlatformProfileLoader implements IProfileLoader { - static RELOAD_INTERVAL = 1000 * 60 * 15; // every 15 minutes + static RELOAD_INTERVAL = 1000 * 5; // 5 seconds description: ProfileDescription; @@ -47,34 +45,6 @@ export default class PlatformProfileLoader implements IProfileLoader { title: `${ownerSlug}/${packageSlug}@${versionSlug}`, errors: configResult.errors, }; - - setInterval(async () => { - const assistants = await this.controlPlaneClient.listAssistants(); - const newConfigResult = assistants.find( - (assistant) => - assistant.packageSlug === this.packageSlug && - assistant.ownerSlug === this.ownerSlug, - )?.configResult; - if (!newConfigResult) { - return; - } - - let renderedConfig: ConfigYaml | undefined = undefined; - if (newConfigResult.config) { - renderedConfig = await clientRenderHelper( - YAML.stringify(newConfigResult.config), - this.ide, - this.controlPlaneClient, - ); - } - - this.configResult = { - config: renderedConfig, - errors: newConfigResult.errors, - configLoadInterrupted: false, - }; - this.onReload(); - }, PlatformProfileLoader.RELOAD_INTERVAL); } async doLoadConfig(): Promise> { diff --git a/core/config/profile/doLoadConfig.ts b/core/config/profile/doLoadConfig.ts index 8e2326e3db..3a64ef71cb 100644 --- a/core/config/profile/doLoadConfig.ts +++ b/core/config/profile/doLoadConfig.ts @@ -14,7 +14,7 @@ import { } from "../../"; import { ControlPlaneProxyInfo } from "../../control-plane/analytics/IAnalyticsProvider.js"; import { ControlPlaneClient } from "../../control-plane/client.js"; -import { controlPlaneEnv } from "../../control-plane/env.js"; +import { getControlPlaneEnv } from "../../control-plane/env.js"; import { TeamAnalytics } from "../../control-plane/TeamAnalytics.js"; import ContinueProxy from "../../llm/llms/stubs/ContinueProxy"; import { getConfigYamlPath } from "../../util/paths"; @@ -99,9 +99,11 @@ export default async function doLoadConfig( const controlPlane = (newConfig as any).controlPlane; const useOnPremProxy = controlPlane?.useContinueForTeamsProxy === false && controlPlane?.proxyUrl; + + const env = await getControlPlaneEnv(ideSettingsPromise); let controlPlaneProxyUrl: string = useOnPremProxy ? controlPlane?.proxyUrl - : controlPlaneEnv.DEFAULT_CONTROL_PLANE_PROXY_URL; + : env.DEFAULT_CONTROL_PLANE_PROXY_URL; if (!controlPlaneProxyUrl.endsWith("/")) { controlPlaneProxyUrl += "/"; diff --git a/core/context/providers/ContinueProxyContextProvider.ts b/core/context/providers/ContinueProxyContextProvider.ts index 2dcb91cc80..278a126784 100644 --- a/core/context/providers/ContinueProxyContextProvider.ts +++ b/core/context/providers/ContinueProxyContextProvider.ts @@ -1,4 +1,4 @@ -import { controlPlaneEnv } from "../../control-plane/env.js"; +import { getControlPlaneEnv } from "../../control-plane/env.js"; import { ContextItem, ContextProviderDescription, @@ -35,11 +35,9 @@ class ContinueProxyContextProvider extends BaseContextProvider { async loadSubmenuItems( args: LoadSubmenuItemsArgs, ): Promise { + const env = await getControlPlaneEnv(args.ide.getIdeSettings()); const response = await args.fetch( - new URL( - `/proxy/context/${this.options.id}/list`, - controlPlaneEnv.CONTROL_PLANE_URL, - ), + new URL(`/proxy/context/${this.options.id}/list`, env.CONTROL_PLANE_URL), { method: "GET", headers: { @@ -56,10 +54,11 @@ class ContinueProxyContextProvider extends BaseContextProvider { query: string, extras: ContextProviderExtras, ): Promise { + const env = await getControlPlaneEnv(extras.ide.getIdeSettings()); const response = await extras.fetch( new URL( `/proxy/context/${this.options.id}/retrieve`, - controlPlaneEnv.CONTROL_PLANE_URL, + env.CONTROL_PLANE_URL, ), { method: "POST", diff --git a/core/context/providers/_context-providers.test.ts b/core/context/providers/_context-providers.test.ts index 24cac2c444..22f1dffbc0 100644 --- a/core/context/providers/_context-providers.test.ts +++ b/core/context/providers/_context-providers.test.ts @@ -31,7 +31,7 @@ async function getContextProviderExtras( ide, ideSettingsPromise, async (text) => {}, - new ControlPlaneClient(Promise.resolve(undefined)), + new ControlPlaneClient(Promise.resolve(undefined), ideSettingsPromise), ); const { config } = await configHandler.loadConfig(); if (!config) { diff --git a/core/control-plane/auth/index.ts b/core/control-plane/auth/index.ts index f7d423f83b..8965f4fdb3 100644 --- a/core/control-plane/auth/index.ts +++ b/core/control-plane/auth/index.ts @@ -1,13 +1,16 @@ import { v4 as uuidv4 } from "uuid"; +import { IdeSettings } from "../.."; +import { getControlPlaneEnv } from "../env"; -import { controlPlaneEnv } from "../env"; - -export async function getAuthUrlForTokenPage(): Promise { +export async function getAuthUrlForTokenPage( + ideSettingsPromise: Promise, +): Promise { + const env = await getControlPlaneEnv(ideSettingsPromise); const url = new URL("https://api.workos.com/user_management/authorize"); const params = { response_type: "code", - client_id: controlPlaneEnv.WORKOS_CLIENT_ID, - redirect_uri: `${controlPlaneEnv.APP_URL}tokens/callback`, + client_id: env.WORKOS_CLIENT_ID, + redirect_uri: `${env.APP_URL}tokens/callback`, // redirect_uri: "http://localhost:3000/tokens/callback", state: uuidv4(), provider: "authkit", diff --git a/core/control-plane/client.ts b/core/control-plane/client.ts index bb7661ee83..044006882e 100644 --- a/core/control-plane/client.ts +++ b/core/control-plane/client.ts @@ -2,10 +2,15 @@ import { ConfigJson } from "@continuedev/config-types"; import { ConfigYaml } from "@continuedev/config-yaml/dist/schemas/index.js"; import fetch, { RequestInit, Response } from "node-fetch"; -import { ModelDescription } from "../index.js"; +import { IdeSettings, ModelDescription } from "../index.js"; -import { ConfigResult, FQSN, SecretResult } from "@continuedev/config-yaml"; -import { controlPlaneEnv } from "./env.js"; +import { + ConfigResult, + FQSN, + FullSlug, + SecretResult, +} from "@continuedev/config-yaml"; +import { getControlPlaneEnv } from "./env.js"; export interface ControlPlaneSessionInfo { accessToken: string; @@ -27,7 +32,6 @@ export const TRIAL_PROXY_URL = "https://proxy-server-blue-l6vsfbzhba-uw.a.run.app"; export class ControlPlaneClient { - private static URL = controlPlaneEnv.CONTROL_PLANE_URL; private static ACCESS_TOKEN_VALID_FOR_MS = 1000 * 60 * 5; // 5 minutes private lastAccessTokenRefresh = 0; @@ -36,6 +40,7 @@ export class ControlPlaneClient { private readonly sessionInfoPromise: Promise< ControlPlaneSessionInfo | undefined >, + private readonly ideSettingsPromise: Promise, ) {} async resolveFQSNs(fqsns: FQSN[]): Promise<(SecretResult | undefined)[]> { @@ -66,7 +71,9 @@ export class ControlPlaneClient { if (!accessToken) { throw new Error("No access token"); } - const url = new URL(path, ControlPlaneClient.URL).toString(); + + const env = await getControlPlaneEnv(this.ideSettingsPromise); + const url = new URL(path, env.CONTROL_PLANE_URL).toString(); const resp = await fetch(url, { ...init, headers: { @@ -123,6 +130,23 @@ export class ControlPlaneClient { } } + public async listAssistantFullSlugs(): Promise { + const userId = await this.userId; + if (!userId) { + return null; + } + + try { + const resp = await this.request("ide/list-assistant-full-slugs", { + method: "GET", + }); + const { fullSlugs } = (await resp.json()) as any; + return fullSlugs; + } catch (e) { + return null; + } + } + async getSettingsForWorkspace(workspaceId: string): Promise { const userId = await this.userId; if (!userId) { diff --git a/core/control-plane/env.ts b/core/control-plane/env.ts index 16adf0990f..fd37c31a8c 100644 --- a/core/control-plane/env.ts +++ b/core/control-plane/env.ts @@ -1,7 +1,6 @@ -import { readUsePlatform } from "../util/paths"; -import { usePlatform } from "./flags"; +import { IdeSettings } from ".."; -interface ControlPlaneEnv { +export interface ControlPlaneEnv { DEFAULT_CONTROL_PLANE_PROXY_URL: string; CONTROL_PLANE_URL: string; AUTH_TYPE: string; @@ -27,6 +26,14 @@ const PRODUCTION_ENV: ControlPlaneEnv = { APP_URL: "https://app.continue.dev/", }; +const PRODUCTION_HUB_ENV: ControlPlaneEnv = { + DEFAULT_CONTROL_PLANE_PROXY_URL: "https://api.continue.dev/", + CONTROL_PLANE_URL: "https://api.continue.dev/", + AUTH_TYPE: WORKOS_ENV_ID_PRODUCTION, + WORKOS_CLIENT_ID: WORKOS_CLIENT_ID_PRODUCTION, + APP_URL: "https://hub.continue.dev/", +}; + const STAGING_ENV: ControlPlaneEnv = { DEFAULT_CONTROL_PLANE_PROXY_URL: "https://control-plane-api-service-537175798139.us-central1.run.app/", @@ -53,17 +60,37 @@ const LOCAL_ENV: ControlPlaneEnv = { APP_URL: "http://localhost:3000/", }; -function getControlPlaneEnv(): ControlPlaneEnv { - const usePlatformFileEnv = readUsePlatform(); - const env = usePlatformFileEnv || process.env.CONTROL_PLANE_ENV; +export async function getControlPlaneEnv( + ideSettingsPromise: Promise, +): Promise { + const ideSettings = await ideSettingsPromise; + return getControlPlaneEnvSync(ideSettings.continueTestEnvironment); +} + +export function getControlPlaneEnvSync( + ideTestEnvironment: IdeSettings["continueTestEnvironment"], +): ControlPlaneEnv { + const env = + ideTestEnvironment === "production" + ? "hub" + : ideTestEnvironment === "test" + ? "test" + : process.env.CONTROL_PLANE_ENV; return env === "local" ? LOCAL_ENV : env === "staging" ? STAGING_ENV - : env === "test" || usePlatform() + : env === "test" ? TEST_ENV - : PRODUCTION_ENV; + : env === "hub" + ? PRODUCTION_HUB_ENV + : PRODUCTION_ENV; } -export const controlPlaneEnv = getControlPlaneEnv(); +export async function useHub( + ideSettingsPromise: Promise, +): Promise { + const ideSettings = await ideSettingsPromise; + return ideSettings.continueTestEnvironment !== "none"; +} diff --git a/core/control-plane/flags.ts b/core/control-plane/flags.ts deleted file mode 100644 index 7b8ff6799e..0000000000 --- a/core/control-plane/flags.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { readUsePlatform, usePlatformPathExists } from "../util/paths"; - -export function usePlatform(): boolean { - return usePlatformPathExists(); -} - -export function getEnvFromUsePlatformFile(): string | undefined { - const contents = readUsePlatform(); - if (contents && contents.trim().length > 0) { - return contents.trim(); - } - return undefined; -} diff --git a/core/core.ts b/core/core.ts index c74f2e539d..39a52a6df6 100644 --- a/core/core.ts +++ b/core/core.ts @@ -52,7 +52,6 @@ import { type IndexingProgressUpdate, } from "."; -import { usePlatform } from "./control-plane/flags"; import type { FromCoreProtocol, ToCoreProtocol } from "./protocol"; import type { IMessenger, Message } from "./protocol/messenger"; @@ -111,10 +110,13 @@ export class Core { const ideSettingsPromise = messenger.request("getIdeSettings", undefined); const sessionInfoPromise = messenger.request("getControlPlaneSessionInfo", { silent: true, - useOnboarding: usePlatform(), + useOnboarding: false, }); - this.controlPlaneClient = new ControlPlaneClient(sessionInfoPromise); + this.controlPlaneClient = new ControlPlaneClient( + sessionInfoPromise, + ideSettingsPromise, + ); this.configHandler = new ConfigHandler( this.ide, @@ -865,7 +867,7 @@ export class Core { this.configHandler.updateControlPlaneSessionInfo(msg.data.sessionInfo); }); on("auth/getAuthUrl", async (msg) => { - const url = await getAuthUrlForTokenPage(); + const url = await getAuthUrlForTokenPage(ideSettingsPromise); return { url }; }); diff --git a/core/index.d.ts b/core/index.d.ts index 30c66a2b0f..998ccb312a 100644 --- a/core/index.d.ts +++ b/core/index.d.ts @@ -583,6 +583,7 @@ export interface IdeSettings { remoteConfigSyncPeriod: number; userToken: string; enableControlServerBeta: boolean; + continueTestEnvironment: "none" | "production" | "test"; pauseCodebaseIndexOnStart: boolean; enableDebugLogs: boolean; } diff --git a/core/indexing/docs/DocsService.skip.ts b/core/indexing/docs/DocsService.skip.ts index 6f9853afe2..0064f064cb 100644 --- a/core/indexing/docs/DocsService.skip.ts +++ b/core/indexing/docs/DocsService.skip.ts @@ -4,7 +4,6 @@ import { ConfigHandler } from "../../config/ConfigHandler.js"; import { ControlPlaneClient } from "../../control-plane/client.js"; import { SiteIndexingConfig } from "../../index.js"; -import FreeTrial from "../../llm/llms/FreeTrial.js"; import FileSystemIde from "../../util/filesystem.js"; import { editConfigJson } from "../../util/paths.js"; @@ -37,17 +36,19 @@ describe.skip("DocsService Integration Tests", () => { beforeEach(async () => { ide = new FileSystemIde(process.cwd()); + const ideSettingsPromise = Promise.resolve({ + remoteConfigSyncPeriod: 60, + userToken: "", + enableControlServerBeta: false, + continueTestEnvironment: "none" as const, + pauseCodebaseIndexOnStart: false, + ideSettings: {} as any, + enableDebugLogs: false, + remoteConfigServerUrl: "", + }); configHandler = new ConfigHandler( ide, - Promise.resolve({ - remoteConfigSyncPeriod: 60, - userToken: "", - enableControlServerBeta: false, - pauseCodebaseIndexOnStart: false, - ideSettings: {} as any, - enableDebugLogs: false, - remoteConfigServerUrl: "", - }), + ideSettingsPromise, async () => {}, new ControlPlaneClient( Promise.resolve({ @@ -57,6 +58,7 @@ describe.skip("DocsService Integration Tests", () => { label: "", }, }), + ideSettingsPromise, ), ); diff --git a/core/test/fixtures.ts b/core/test/fixtures.ts index 78ee601504..4f86013034 100644 --- a/core/test/fixtures.ts +++ b/core/test/fixtures.ts @@ -11,6 +11,7 @@ export const ideSettingsPromise = testIde.getIdeSettings(); export const testControlPlaneClient = new ControlPlaneClient( Promise.resolve(undefined), + ideSettingsPromise, ); export const testConfigHandler = new ConfigHandler( diff --git a/core/util/filesystem.ts b/core/util/filesystem.ts index ccb550ec93..be33346e7a 100644 --- a/core/util/filesystem.ts +++ b/core/util/filesystem.ts @@ -53,6 +53,7 @@ class FileSystemIde implements IDE { remoteConfigSyncPeriod: 60, userToken: "", enableControlServerBeta: false, + continueTestEnvironment: "none", pauseCodebaseIndexOnStart: false, enableDebugLogs: false, }; diff --git a/core/util/paths.ts b/core/util/paths.ts index 562d2e819c..836bc84b93 100644 --- a/core/util/paths.ts +++ b/core/util/paths.ts @@ -301,18 +301,6 @@ export function getPathToRemoteConfig(remoteConfigServerUrl: string): string { return dir; } -export function usePlatformPathExists(): boolean { - const sPath = path.join(getContinueGlobalPath(), ".use_platform"); - return fs.existsSync(sPath); -} - -export function readUsePlatform(): string | undefined { - const sPath = path.join(getContinueGlobalPath(), ".use_platform"); - if (fs.existsSync(sPath)) { - return fs.readFileSync(sPath, "utf8"); - } -} - export function getConfigJsonPathForRemote( remoteConfigServerUrl: string, ): string { diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index 23c151699b..a6eba5ce7c 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -96,6 +96,16 @@ "default": false, "markdownDescription": "Enable Continue for teams beta features. To sign in, click the person icon in the bottom right of the sidebar." }, + "continue.continueTestEnvironment": { + "type": "string", + "enum": [ + "none", + "production", + "test" + ], + "default": "none", + "description": "Continue test environment" + }, "continue.showInlineTip": { "type": "boolean", "default": true, @@ -529,12 +539,12 @@ { "command": "continue.signInToControlPlane", "group": "navigation@5", - "when": "(view == continue.continueGUIView) && config.continue.enableContinueForTeams && !continue.isSignedInToControlPlane" + "when": "(view == continue.continueGUIView) && (config.continue.enableContinueForTeams || config.continue.continueTestEnvironment !== 'none') && !continue.isSignedInToControlPlane" }, { "command": "continue.openAccountDialog", "group": "navigation@5", - "when": "(view == continue.continueGUIView) && config.continue.enableContinueForTeams && continue.isSignedInToControlPlane" + "when": "(view == continue.continueGUIView) && (config.continue.enableContinueForTeams || config.continue.continueTestEnvironment !== 'none') && continue.isSignedInToControlPlane" }, { "command": "continue.openMorePage", diff --git a/extensions/vscode/src/VsCodeIde.ts b/extensions/vscode/src/VsCodeIde.ts index 51008db8ad..fbd2077a12 100644 --- a/extensions/vscode/src/VsCodeIde.ts +++ b/extensions/vscode/src/VsCodeIde.ts @@ -636,6 +636,9 @@ class VsCodeIde implements IDE { "enableContinueForTeams", false, ), + continueTestEnvironment: settings.get< + IdeSettings["continueTestEnvironment"] + >("continueTestEnvironment", "none"), pauseCodebaseIndexOnStart: settings.get( "pauseCodebaseIndexOnStart", false, diff --git a/extensions/vscode/src/extension/VsCodeExtension.ts b/extensions/vscode/src/extension/VsCodeExtension.ts index 9e6da728db..b499609196 100644 --- a/extensions/vscode/src/extension/VsCodeExtension.ts +++ b/extensions/vscode/src/extension/VsCodeExtension.ts @@ -1,8 +1,8 @@ import fs from "fs"; -import { IContextProvider } from "core"; +import { IContextProvider, IdeSettings } from "core"; import { ConfigHandler } from "core/config/ConfigHandler"; -import { controlPlaneEnv, EXTENSION_NAME } from "core/control-plane/env"; +import { EXTENSION_NAME, getControlPlaneEnv } from "core/control-plane/env"; import { Core } from "core/core"; import { FromCoreProtocol, ToCoreProtocol } from "core/protocol"; import { InProcessMessenger } from "core/protocol/messenger"; @@ -294,7 +294,8 @@ export class VsCodeExtension { // When GitHub sign-in status changes, reload config vscode.authentication.onDidChangeSessions(async (e) => { - if (e.provider.id === controlPlaneEnv.AUTH_TYPE) { + const env = await getControlPlaneEnv(this.ide.getIdeSettings()); + if (e.provider.id === env.AUTH_TYPE) { vscode.commands.executeCommand( "setContext", "continue.isSignedInToControlPlane", @@ -374,6 +375,9 @@ export class VsCodeExtension { void this.core.invoke("didChangeActiveTextEditor", { filepath }); }); + const originalValue = vscode.workspace + .getConfiguration(EXTENSION_NAME) + .get("continueTestEnvironment"); vscode.workspace.onDidChangeConfiguration(async (event) => { if (event.affectsConfiguration(EXTENSION_NAME)) { const settings = this.ide.getIdeSettingsSync(); @@ -381,6 +385,10 @@ export class VsCodeExtension { void webviewProtocol.request("didChangeIdeSettings", { settings, }); + + if (settings.continueTestEnvironment !== originalValue) { + await vscode.commands.executeCommand("workbench.action.reloadWindow"); + } } }); } diff --git a/extensions/vscode/src/stubs/WorkOsAuthProvider.ts b/extensions/vscode/src/stubs/WorkOsAuthProvider.ts index a22c4934a2..a150d2cd89 100644 --- a/extensions/vscode/src/stubs/WorkOsAuthProvider.ts +++ b/extensions/vscode/src/stubs/WorkOsAuthProvider.ts @@ -1,7 +1,7 @@ import crypto from "crypto"; import { ControlPlaneSessionInfo } from "core/control-plane/client"; -import { controlPlaneEnv } from "core/control-plane/env"; +import { EXTENSION_NAME, getControlPlaneEnvSync } from "core/control-plane/env"; import fetch from "node-fetch"; import { v4 as uuidv4 } from "uuid"; import { @@ -17,13 +17,22 @@ import { Uri, UriHandler, window, + workspace, } from "vscode"; +import { IdeSettings } from "core"; import { PromiseAdapter, promiseFromEvent } from "./promiseUtils"; import { SecretStorage } from "./SecretStorage"; const AUTH_NAME = "Continue"; +const controlPlaneEnv = getControlPlaneEnvSync( + workspace + .getConfiguration(EXTENSION_NAME) + .get("continueTestEnvironment") ?? + "none", +); + const SESSIONS_SECRET_KEY = `${controlPlaneEnv.AUTH_TYPE}.sessions`; class UriEventHandler extends EventEmitter implements UriHandler { diff --git a/gui/src/context/Auth.tsx b/gui/src/context/Auth.tsx index 361ee8e0c2..c81aa1bc11 100644 --- a/gui/src/context/Auth.tsx +++ b/gui/src/context/Auth.tsx @@ -135,13 +135,16 @@ export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ useState(false); useEffect(() => { - ideMessenger.ide.getIdeSettings().then(({ enableControlServerBeta }) => { - setControlServerBetaEnabled(enableControlServerBeta); - dispatch(setLastControlServerBetaEnabledStatus(enableControlServerBeta)); - - const shouldShowPopup = - !lastControlServerBetaEnabledStatus && enableControlServerBeta; - }); + ideMessenger.ide + .getIdeSettings() + .then(({ enableControlServerBeta, continueTestEnvironment }) => { + const enabled = + enableControlServerBeta || continueTestEnvironment !== "none"; + setControlServerBetaEnabled(enabled); + dispatch(setLastControlServerBetaEnabledStatus(enabled)); + + const shouldShowPopup = !lastControlServerBetaEnabledStatus && enabled; + }); }, []); useWebviewListener( diff --git a/gui/src/redux/slices/sessionSlice.ts b/gui/src/redux/slices/sessionSlice.ts index 10414fbb71..33ba33f590 100644 --- a/gui/src/redux/slices/sessionSlice.ts +++ b/gui/src/redux/slices/sessionSlice.ts @@ -90,6 +90,12 @@ const initialState: SessionState = { id: "local", title: "Local", errors: undefined, + profileType: "local", + fullSlug: { + ownerSlug: "", + packageSlug: "", + versionSlug: "", + }, }, ], curCheckpointIndex: 0,