diff --git a/.gitignore b/.gitignore index eeafe223e94..b4bcc43c7af 100644 --- a/.gitignore +++ b/.gitignore @@ -165,6 +165,7 @@ extensions/intellij/bin extensions/.continue-debug/ *.vsix +local-artifacts/ # intellij module library files *.iml diff --git a/core/config/onboarding.ts b/core/config/onboarding.ts index dde0fefe70a..6244506da5d 100644 --- a/core/config/onboarding.ts +++ b/core/config/onboarding.ts @@ -1,12 +1,153 @@ import { ConfigYaml } from "@continuedev/config-yaml"; -export const LOCAL_ONBOARDING_PROVIDER_TITLE = "Ollama"; -export const LOCAL_ONBOARDING_FIM_MODEL = "qwen2.5-coder:1.5b-base"; -export const LOCAL_ONBOARDING_FIM_TITLE = "Qwen2.5-Coder 1.5B"; -export const LOCAL_ONBOARDING_CHAT_MODEL = "llama3.1:8b"; -export const LOCAL_ONBOARDING_CHAT_TITLE = "Llama 3.1 8B"; -export const LOCAL_ONBOARDING_EMBEDDINGS_MODEL = "nomic-embed-text:latest"; -export const LOCAL_ONBOARDING_EMBEDDINGS_TITLE = "Nomic Embed"; +export type LocalOnboardingProvider = "ollama" | "lmstudio"; + +export const DEFAULT_LOCAL_ONBOARDING_PROVIDER: LocalOnboardingProvider = + "ollama"; + +type LocalOnboardingModel = { + name: string; + provider: LocalOnboardingProvider; + model: string; + roles: ( + | "chat" + | "autocomplete" + | "embed" + | "edit" + | "apply" + | "summarize" + | "subagent" + | "rerank" + )[]; +}; + +type LocalOnboardingConfig = { + providerTitle: string; + chatTitle: string; + chatModel: string; + autocompleteTitle?: string; + autocompleteModel?: string; + embeddingsTitle?: string; + embeddingsModel?: string; +}; + +const OLLAMA_LOCAL_ONBOARDING_CONFIG: LocalOnboardingConfig = { + providerTitle: "Ollama", + chatTitle: "Llama 3.1 8B", + chatModel: "llama3.1:8b", + autocompleteTitle: "Qwen2.5-Coder 1.5B", + autocompleteModel: "qwen2.5-coder:1.5b-base", + embeddingsTitle: "Nomic Embed", + embeddingsModel: "nomic-embed-text:latest", +}; + +const LMSTUDIO_LOCAL_ONBOARDING_CONFIG: LocalOnboardingConfig = { + providerTitle: "LM Studio", + chatTitle: "LM Studio", + chatModel: "AUTODETECT", +}; + +export function getLocalOnboardingConfig( + provider: LocalOnboardingProvider = DEFAULT_LOCAL_ONBOARDING_PROVIDER, +): LocalOnboardingConfig { + return provider === "lmstudio" + ? LMSTUDIO_LOCAL_ONBOARDING_CONFIG + : OLLAMA_LOCAL_ONBOARDING_CONFIG; +} + +function dedupeModels(models?: string[]) { + if (!models?.length) { + return []; + } + + return Array.from( + new Set(models.map((model) => model.trim()).filter(Boolean)), + ); +} + +function getPreferredModel( + models: string[], + matchers: RegExp[], + fallback: string, +): string { + return ( + models.find((model) => matchers.some((matcher) => matcher.test(model))) ?? + fallback + ); +} + +function getLmStudioOnboardingModels( + models?: string[], +): LocalOnboardingModel[] { + const availableModels = dedupeModels(models); + const fallbackConfig = getLocalOnboardingConfig("lmstudio"); + const fallbackModel = availableModels[0] ?? fallbackConfig.chatModel; + const chatModel = + availableModels.find( + (model) => + [/instruct/i, /chat/i, /assistant/i].some((matcher) => + matcher.test(model), + ) && !/(coder|code|codestral)/i.test(model), + ) ?? + getPreferredModel( + availableModels, + [/instruct/i, /chat/i, /assistant/i], + fallbackModel, + ); + const autocompleteModel = getPreferredModel( + availableModels, + [/coder/i, /code/i, /codestral/i, /deepseek/i, /qwen/i], + chatModel, + ); + const embeddingsModel = getPreferredModel( + availableModels, + [/embed/i, /embedding/i, /nomic/i, /bge/i, /\be5\b/i], + "", + ); + + const localModels: LocalOnboardingModel[] = [ + { + name: chatModel, + provider: "lmstudio", + model: chatModel, + roles: + autocompleteModel === chatModel + ? ["chat", "edit", "apply", "autocomplete"] + : ["chat", "edit", "apply"], + }, + ]; + + if (autocompleteModel !== chatModel) { + localModels.push({ + name: autocompleteModel, + provider: "lmstudio", + model: autocompleteModel, + roles: ["autocomplete"], + }); + } + + if (embeddingsModel && embeddingsModel !== chatModel) { + localModels.push({ + name: embeddingsModel, + provider: "lmstudio", + model: embeddingsModel, + roles: ["embed"], + }); + } + + return localModels; +} + +export function getLocalOnboardingPrimaryModelTitle( + provider: LocalOnboardingProvider = DEFAULT_LOCAL_ONBOARDING_PROVIDER, + availableModels?: string[], +) { + if (provider === "lmstudio") { + return getLmStudioOnboardingModels(availableModels)[0]?.name ?? "LM Studio"; + } + + return getLocalOnboardingConfig(provider).chatTitle; +} const ANTHROPIC_MODEL_CONFIG = { slugs: ["anthropic/claude-3-7-sonnet", "anthropic/claude-4-sonnet"], @@ -34,30 +175,39 @@ export function setupBestConfig(config: ConfigYaml): ConfigYaml { }; } -export function setupLocalConfig(config: ConfigYaml): ConfigYaml { +export function setupLocalConfig( + config: ConfigYaml, + provider: LocalOnboardingProvider = DEFAULT_LOCAL_ONBOARDING_PROVIDER, + availableModels?: string[], +): ConfigYaml { + const onboardingConfig = getLocalOnboardingConfig(provider); + const localModels: LocalOnboardingModel[] = + provider === "lmstudio" + ? getLmStudioOnboardingModels(availableModels) + : [ + { + name: onboardingConfig.chatTitle, + provider: "ollama", + model: onboardingConfig.chatModel, + roles: ["chat", "edit", "apply"], + }, + { + name: onboardingConfig.autocompleteTitle!, + provider: "ollama", + model: onboardingConfig.autocompleteModel!, + roles: ["autocomplete"], + }, + { + name: onboardingConfig.embeddingsTitle!, + provider: "ollama", + model: onboardingConfig.embeddingsModel!, + roles: ["embed"], + }, + ]; + return { ...config, - models: [ - { - name: LOCAL_ONBOARDING_CHAT_TITLE, - provider: "ollama", - model: LOCAL_ONBOARDING_CHAT_MODEL, - roles: ["chat", "edit", "apply"], - }, - { - name: LOCAL_ONBOARDING_FIM_TITLE, - provider: "ollama", - model: LOCAL_ONBOARDING_FIM_MODEL, - roles: ["autocomplete"], - }, - { - name: LOCAL_ONBOARDING_EMBEDDINGS_TITLE, - provider: "ollama", - model: LOCAL_ONBOARDING_EMBEDDINGS_MODEL, - roles: ["embed"], - }, - ...(config.models ?? []), - ], + models: [...localModels, ...(config.models ?? [])], }; } diff --git a/core/config/onboarding.vitest.ts b/core/config/onboarding.vitest.ts new file mode 100644 index 00000000000..631a76023e6 --- /dev/null +++ b/core/config/onboarding.vitest.ts @@ -0,0 +1,87 @@ +import { describe, expect, it } from "vitest"; + +import { + getLocalOnboardingPrimaryModelTitle, + setupLocalConfig, +} from "./onboarding"; + +describe("setupLocalConfig", () => { + it("adds the default Ollama onboarding models", () => { + const result = setupLocalConfig({ + name: "Local Config", + version: "0.0.1", + schema: "v1", + models: [], + }); + + expect(result.models?.slice(0, 3)).toEqual([ + { + name: "Llama 3.1 8B", + provider: "ollama", + model: "llama3.1:8b", + roles: ["chat", "edit", "apply"], + }, + { + name: "Qwen2.5-Coder 1.5B", + provider: "ollama", + model: "qwen2.5-coder:1.5b-base", + roles: ["autocomplete"], + }, + { + name: "Nomic Embed", + provider: "ollama", + model: "nomic-embed-text:latest", + roles: ["embed"], + }, + ]); + }); + + it("builds an LM Studio config from detected local models", () => { + const result = setupLocalConfig( + { + name: "Local Config", + version: "0.0.1", + schema: "v1", + models: [], + }, + "lmstudio", + [ + "text-embedding-nomic-embed-text-v1.5", + "Qwen2.5-Coder-7B-Instruct-GGUF", + "Meta-Llama-3.1-8B-Instruct-GGUF", + ], + ); + + expect(result.models).toEqual([ + { + name: "Meta-Llama-3.1-8B-Instruct-GGUF", + provider: "lmstudio", + model: "Meta-Llama-3.1-8B-Instruct-GGUF", + roles: ["chat", "edit", "apply"], + }, + { + name: "Qwen2.5-Coder-7B-Instruct-GGUF", + provider: "lmstudio", + model: "Qwen2.5-Coder-7B-Instruct-GGUF", + roles: ["autocomplete"], + }, + { + name: "text-embedding-nomic-embed-text-v1.5", + provider: "lmstudio", + model: "text-embedding-nomic-embed-text-v1.5", + roles: ["embed"], + }, + ]); + }); +}); + +describe("getLocalOnboardingPrimaryModelTitle", () => { + it("returns the chosen LM Studio chat model title", () => { + expect( + getLocalOnboardingPrimaryModelTitle("lmstudio", [ + "Qwen2.5-Coder-7B-Instruct-GGUF", + "Meta-Llama-3.1-8B-Instruct-GGUF", + ]), + ).toBe("Meta-Llama-3.1-8B-Instruct-GGUF"); + }); +}); diff --git a/core/core.ts b/core/core.ts index 3d1b2cc3c57..206103fc2a4 100644 --- a/core/core.ts +++ b/core/core.ts @@ -17,6 +17,7 @@ import { CodebaseIndexer } from "./indexing/CodebaseIndexer"; import DocsService from "./indexing/docs/DocsService"; import { countTokens } from "./llm/countTokens"; import Lemonade from "./llm/llms/Lemonade"; +import LMStudio from "./llm/llms/LMStudio"; import Ollama from "./llm/llms/Ollama"; import { EditAggregator } from "./nextEdit/context/aggregateEdits"; import { createNewPromptFileV2 } from "./promptFiles/createNewPromptFile"; @@ -1395,6 +1396,9 @@ export class Core { if (msg.data.title === "Ollama") { const models = await new Ollama({ model: "" }).listModels(); return models; + } else if (msg.data.title === "LM Studio") { + const models = await new LMStudio({ model: "" }).listModels(); + return models; } else if (msg.data.title === "Lemonade") { const models = await new Lemonade({ model: "" }).listModels(); return models; @@ -1403,7 +1407,7 @@ export class Core { } } } catch (e) { - console.debug(`Error listing Ollama models: ${e}`); + console.debug(`Error listing models for ${msg.data.title}: ${e}`); return undefined; } } @@ -1411,13 +1415,18 @@ export class Core { private async handleCompleteOnboarding( msg: Message, ) { - const { mode, provider, apiKey } = msg.data; + const { mode, provider, apiKey, localModelTitles } = msg.data; let editConfigYamlCallback: (config: ConfigYaml) => ConfigYaml; switch (mode) { case OnboardingModes.LOCAL: - editConfigYamlCallback = setupLocalConfig; + editConfigYamlCallback = (config: ConfigYaml) => + setupLocalConfig( + config, + provider === "lmstudio" ? "lmstudio" : "ollama", + localModelTitles, + ); break; case OnboardingModes.API_KEY: diff --git a/core/index.d.ts b/core/index.d.ts index 82941d189e7..0475f6da6e0 100644 --- a/core/index.d.ts +++ b/core/index.d.ts @@ -1948,6 +1948,7 @@ export interface CompleteOnboardingPayload { mode: OnboardingModes; provider?: string; apiKey?: string; + localModelTitles?: string[]; } export interface CompiledMessagesResult { diff --git a/extensions/vscode/package-lock.json b/extensions/vscode/package-lock.json index 978c80880c2..95f52c2561a 100644 --- a/extensions/vscode/package-lock.json +++ b/extensions/vscode/package-lock.json @@ -1,12 +1,12 @@ { "name": "continue", - "version": "1.3.32", + "version": "1.3.33", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "continue", - "version": "1.3.32", + "version": "1.3.33", "license": "Apache-2.0", "dependencies": { "@continuedev/config-types": "file:../../packages/config-types", @@ -776,6 +776,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/@electron/rebuild/-/rebuild-4.0.3.tgz", "integrity": "sha512-u9vpTHRMkOYCs/1FLiSVAFZ7FbjsXK+bQuzviJZa+lG7BHZl1nz52/IcGvwa3sk80/fc3llutBkbCq10Vh8WQA==", + "license": "MIT", "dependencies": { "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.1.1", @@ -802,6 +803,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=18" } @@ -810,6 +812,7 @@ "version": "3.1.5", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.5.tgz", "integrity": "sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=18" } @@ -818,6 +821,7 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -826,6 +830,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", "dependencies": { "minipass": "^7.1.2" }, @@ -837,6 +842,7 @@ "version": "4.26.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-4.26.0.tgz", "integrity": "sha512-8QwIZqikRvDIkXS2S93LjzhsSPJuIbfaMETWH+Bx8oOT9Sa9UsUtBFQlc3gBNd1+QINjaTloitXr1W3dQLi9Iw==", + "license": "MIT", "dependencies": { "semver": "^7.6.3" }, @@ -848,6 +854,7 @@ "version": "11.5.0", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.5.0.tgz", "integrity": "sha512-ra7Kvlhxn5V9Slyus0ygMa2h+UqExPqUIkfk7Pc8QTLT956JLSy51uWFwHtIYy0vI8cB4BDhc/S03+880My/LQ==", + "license": "MIT", "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", @@ -871,6 +878,7 @@ "version": "7.5.11", "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz", "integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", @@ -886,6 +894,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "license": "ISC", "dependencies": { "isexe": "^3.1.1" }, @@ -900,6 +909,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=18" } @@ -1495,6 +1505,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", "dependencies": { "minipass": "^7.0.4" }, @@ -1506,6 +1517,7 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -1801,6 +1813,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "license": "ISC", "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", @@ -1815,12 +1828,14 @@ "node_modules/@npmcli/agent/node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" }, "node_modules/@npmcli/fs": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "license": "ISC", "dependencies": { "semver": "^7.3.5" }, @@ -4212,6 +4227,7 @@ "version": "19.0.1", "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "license": "ISC", "dependencies": { "@npmcli/fs": "^4.0.0", "fs-minipass": "^3.0.0", @@ -4234,6 +4250,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=18" } @@ -4242,6 +4259,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, @@ -4252,12 +4270,14 @@ "node_modules/cacache/node_modules/lru-cache": { "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" }, "node_modules/cacache/node_modules/minipass": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -4266,6 +4286,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, @@ -4277,6 +4298,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", "dependencies": { "minipass": "^7.1.2" }, @@ -4288,6 +4310,7 @@ "version": "7.0.4", "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "license": "MIT", "engines": { "node": ">=18" }, @@ -4299,6 +4322,7 @@ "version": "7.5.11", "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.11.tgz", "integrity": "sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", @@ -4314,6 +4338,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=18" } @@ -6352,7 +6377,8 @@ "node_modules/exponential-backoff": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", - "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==" + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==", + "license": "Apache-2.0" }, "node_modules/express": { "version": "4.21.2", @@ -7068,6 +7094,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -7098,6 +7125,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -7106,6 +7134,7 @@ "version": "9.0.9", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.2" }, @@ -7120,6 +7149,7 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -8713,6 +8743,7 @@ "version": "14.0.3", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "license": "ISC", "dependencies": { "@npmcli/agent": "^3.0.0", "cacache": "^19.0.1", @@ -8734,6 +8765,7 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -8742,6 +8774,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -9448,6 +9481,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", + "license": "MIT", "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^1.0.3", @@ -9464,6 +9498,7 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -9472,6 +9507,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", "dependencies": { "minipass": "^7.1.2" }, @@ -9904,6 +9940,7 @@ "version": "0.2.1", "resolved": "https://registry.npmjs.org/node-api-version/-/node-api-version-0.2.1.tgz", "integrity": "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==", + "license": "MIT", "dependencies": { "semver": "^7.3.5" } @@ -10235,6 +10272,7 @@ "version": "8.1.0", "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", + "license": "ISC", "dependencies": { "abbrev": "^3.0.0" }, @@ -10249,6 +10287,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", + "license": "ISC", "engines": { "node": "^18.17.0 || >=20.5.0" } @@ -11174,6 +11213,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "license": "ISC", "engines": { "node": "^18.17.0 || >=20.5.0" } @@ -12340,6 +12380,7 @@ "version": "8.0.5", "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "license": "MIT", "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", @@ -12456,6 +12497,7 @@ "version": "12.0.0", "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, @@ -12467,6 +12509,7 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -13662,6 +13705,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "license": "ISC", "dependencies": { "unique-slug": "^5.0.0" }, @@ -13673,6 +13717,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4" }, diff --git a/extensions/vscode/package.json b/extensions/vscode/package.json index d62667219f8..228e3270a08 100644 --- a/extensions/vscode/package.json +++ b/extensions/vscode/package.json @@ -2,7 +2,7 @@ "name": "continue", "icon": "media/icon.png", "author": "Continue Dev, Inc", - "version": "1.3.32", + "version": "1.3.33", "repository": { "type": "git", "url": "https://github.com/continuedev/continue" diff --git a/extensions/vscode/src/activation/activate.ts b/extensions/vscode/src/activation/activate.ts index 45fc24d4902..5b7595c127a 100644 --- a/extensions/vscode/src/activation/activate.ts +++ b/extensions/vscode/src/activation/activate.ts @@ -9,7 +9,19 @@ import { GlobalContext } from "core/util/GlobalContext"; import { VsCodeContinueApi } from "./api"; import setupInlineTips from "./InlineTipManager"; +async function isDuplicateHostActivation(): Promise { + const registeredCommands = await vscode.commands.getCommands(true); + return registeredCommands.includes("continue.focusContinueInput"); +} + export async function activateExtension(context: vscode.ExtensionContext) { + if (await isDuplicateHostActivation()) { + console.warn( + "Continue commands already exist; skipping duplicate extension-host activation.", + ); + return {}; + } + const platformCheck = isUnsupportedPlatform(); const globalContext = new GlobalContext(); const hasShownUnsupportedPlatformWarning = globalContext.get( diff --git a/extensions/vscode/src/commands.ts b/extensions/vscode/src/commands.ts index c9374ea9a13..a00f12b2481 100644 --- a/extensions/vscode/src/commands.ts +++ b/extensions/vscode/src/commands.ts @@ -952,8 +952,20 @@ export function registerAllCommands( editDecorationManager, ), )) { - context.subscriptions.push( - vscode.commands.registerCommand(command, callback), - ); + try { + context.subscriptions.push( + vscode.commands.registerCommand(command, callback), + ); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + if (message.includes("already registered")) { + console.warn( + `Skipping duplicate Continue command registration: ${command}`, + ); + continue; + } + + throw error; + } } } diff --git a/extensions/vscode/src/extension.ts b/extensions/vscode/src/extension.ts index 2b8114e5ed9..e0614816abb 100644 --- a/extensions/vscode/src/extension.ts +++ b/extensions/vscode/src/extension.ts @@ -37,7 +37,15 @@ export function activate(context: vscode.ExtensionContext) { ) .then((selection) => { if (selection === "View Logs") { - vscode.commands.executeCommand("continue.viewLogs"); + vscode.commands.getCommands(true).then((commands) => { + if (commands.includes("continue.viewLogs")) { + return vscode.commands.executeCommand("continue.viewLogs"); + } + + return vscode.commands.executeCommand( + "workbench.action.toggleDevTools", + ); + }); } else if (selection === "Retry") { // Reload VS Code window vscode.commands.executeCommand("workbench.action.reloadWindow"); diff --git a/gui/src/components/AssistantAndOrgListbox/index.tsx b/gui/src/components/AssistantAndOrgListbox/index.tsx index cf767c6c3fa..397f85c8a25 100644 --- a/gui/src/components/AssistantAndOrgListbox/index.tsx +++ b/gui/src/components/AssistantAndOrgListbox/index.tsx @@ -149,10 +149,7 @@ export function AssistantAndOrgListbox({ variant={variant} /> - +
Configs diff --git a/gui/src/components/OnboardingCard/components/OnboardingLocalTab.tsx b/gui/src/components/OnboardingCard/components/OnboardingLocalTab.tsx index 9e654a574a3..60d56f0933a 100644 --- a/gui/src/components/OnboardingCard/components/OnboardingLocalTab.tsx +++ b/gui/src/components/OnboardingCard/components/OnboardingLocalTab.tsx @@ -1,15 +1,15 @@ import { - LOCAL_ONBOARDING_CHAT_MODEL, - LOCAL_ONBOARDING_CHAT_TITLE, - LOCAL_ONBOARDING_EMBEDDINGS_MODEL, - LOCAL_ONBOARDING_FIM_MODEL, - LOCAL_ONBOARDING_PROVIDER_TITLE, + DEFAULT_LOCAL_ONBOARDING_PROVIDER, + getLocalOnboardingConfig, + getLocalOnboardingPrimaryModelTitle, + type LocalOnboardingProvider, } from "core/config/onboarding"; import { OnboardingModes } from "core/protocol/core"; import { useContext, useEffect, useState } from "react"; import { Button } from "../.."; import { useAuth } from "../../../context/Auth"; import { IdeMessengerContext } from "../../../context/IdeMessenger"; +import { providers } from "../../../pages/AddNewModel/configs/providers"; import { useAppDispatch } from "../../../redux/hooks"; import { setDialogMessage, setShowDialog } from "../../../redux/slices/uiSlice"; import { updateSelectedModelByRole } from "../../../redux/thunks/updateSelectedModelByRole"; @@ -31,67 +31,90 @@ export function OnboardingLocalTab({ isDialog }: OnboardingLocalTabProps) { isDialog, ); const [hasLoadedChatModel, setHasLoadedChatModel] = useState(false); - const [downloadedOllamaModels, setDownloadedOllamaModels] = useState< - string[] - >([]); + const [localProvider, setLocalProvider] = useState( + DEFAULT_LOCAL_ONBOARDING_PROVIDER, + ); + const [availableLocalModels, setAvailableLocalModels] = useState( + [], + ); const { selectedProfile } = useAuth(); - const [isOllamaConnected, setIsOllamaConnected] = useState(false); + const [isProviderConnected, setIsProviderConnected] = useState(false); + + const onboardingConfig = getLocalOnboardingConfig(localProvider); + const isOllamaProvider = localProvider === "ollama"; + const providerDownloadUrl = isOllamaProvider + ? providers.ollama?.downloadUrl + : providers.lmstudio?.downloadUrl; - const hasDownloadedChatModel = Array.isArray(downloadedOllamaModels) - ? downloadedOllamaModels.some( - (ollamaModel) => ollamaModel === LOCAL_ONBOARDING_CHAT_MODEL, + const hasDownloadedChatModel = Array.isArray(availableLocalModels) + ? availableLocalModels.some( + (ollamaModel) => ollamaModel === onboardingConfig.chatModel, ) : false; - const hasDownloadedAutocompleteModel = Array.isArray(downloadedOllamaModels) - ? downloadedOllamaModels.some( - (ollamaModel) => ollamaModel === LOCAL_ONBOARDING_FIM_MODEL, + const hasDownloadedAutocompleteModel = Array.isArray(availableLocalModels) + ? availableLocalModels.some( + (ollamaModel) => ollamaModel === onboardingConfig.autocompleteModel, ) : false; - const hasDownloadedEmbeddingsModel = Array.isArray(downloadedOllamaModels) - ? downloadedOllamaModels.some( - (ollamaModel) => ollamaModel === LOCAL_ONBOARDING_EMBEDDINGS_MODEL, + const hasDownloadedEmbeddingsModel = Array.isArray(availableLocalModels) + ? availableLocalModels.some( + (ollamaModel) => ollamaModel === onboardingConfig.embeddingsModel, ) : false; - const allDownloaded = - hasDownloadedAutocompleteModel && - hasDownloadedChatModel && - hasDownloadedEmbeddingsModel; + const allDownloaded = isOllamaProvider + ? hasDownloadedAutocompleteModel && + hasDownloadedChatModel && + hasDownloadedEmbeddingsModel + : availableLocalModels.length > 0; /** * The first time we detect that a chat model has been loaded, * we send an empty request to load it */ useEffect(() => { - if (!hasLoadedChatModel && hasDownloadedChatModel) { + if (isOllamaProvider && !hasLoadedChatModel && hasDownloadedChatModel) { ideMessenger.post("llm/complete", { completionOptions: {}, prompt: "", - title: LOCAL_ONBOARDING_PROVIDER_TITLE, + title: onboardingConfig.providerTitle, }); setHasLoadedChatModel(true); } - }, [downloadedOllamaModels, isOllamaConnected]); + }, [ + availableLocalModels, + hasDownloadedChatModel, + hasLoadedChatModel, + ideMessenger, + isOllamaProvider, + onboardingConfig.providerTitle, + ]); + + useEffect(() => { + setHasLoadedChatModel(false); + }, [localProvider]); useEffect(() => { const fetchDownloadedModels = async () => { try { const result = await ideMessenger.request("llm/listModels", { - title: LOCAL_ONBOARDING_PROVIDER_TITLE, + title: onboardingConfig.providerTitle, }); if (result.status === "success") { - setDownloadedOllamaModels(result.content ?? []); - setIsOllamaConnected(!!result.content); + const models = result.content ?? []; + setAvailableLocalModels(models); + setIsProviderConnected(Array.isArray(result.content)); } else { throw new Error("Failed to fetch models"); } } catch (error) { console.error("Error fetching models:", error); - setIsOllamaConnected(false); + setAvailableLocalModels([]); + setIsProviderConnected(false); } }; @@ -103,10 +126,10 @@ export function OnboardingLocalTab({ isDialog }: OnboardingLocalTabProps) { void fetchDownloadedModels(); return () => clearInterval(intervalId); - }, []); + }, [ideMessenger, onboardingConfig.providerTitle]); const onClickSubmitOnboarding = () => { - submitOnboarding(); + submitOnboarding(localProvider, undefined, availableLocalModels); if (isDialog) { dispatch(setDialogMessage(undefined)); @@ -117,7 +140,10 @@ export function OnboardingLocalTab({ isDialog }: OnboardingLocalTabProps) { updateSelectedModelByRole({ selectedProfile, role: "chat", - modelTitle: LOCAL_ONBOARDING_CHAT_TITLE, + modelTitle: getLocalOnboardingPrimaryModelTitle( + localProvider, + availableLocalModels, + ), }), ); }; @@ -139,30 +165,113 @@ export function OnboardingLocalTab({ isDialog }: OnboardingLocalTabProps) {
+
+ {(["ollama", "lmstudio"] as LocalOnboardingProvider[]).map( + (providerKey) => { + const isSelected = providerKey === localProvider; + const providerTitle = + getLocalOnboardingConfig(providerKey).providerTitle; + + return ( + + ); + }, + )} +
+

- Install Ollama + {isOllamaProvider ? "Install Ollama" : "Install LM Studio"}

- + {isOllamaProvider ? ( + + ) : ( +
+ +
+ {isProviderConnected ? ( + + Connected to LM Studio at{" "} + http://localhost:1234/v1 + + ) : ( + + Start the LM Studio local inference server at{" "} + http://localhost:1234/v1 + + )} +
+
+ )}
- - - - - + {isOllamaProvider ? ( + <> + + + + + + + ) : ( +
+

+ Available LM Studio models +

+
+ {availableLocalModels.length > 0 ? ( +
+ {availableLocalModels.slice(0, 4).map((modelName) => ( + + {modelName} + + ))} + {availableLocalModels.length > 4 && ( + + +{availableLocalModels.length - 4} more + + )} +
+ ) : ( + + No models detected yet. Load a model in LM Studio and start + the local server. + + )} +
+
+ )}