diff --git a/.gitignore b/.gitignore index fcdb9b6f..17d12e30 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,5 @@ product.overrides.json *.snap.actual .vscode-test .tmp/ +.tmp2/ .tool-versions diff --git a/remote/package-lock.json b/remote/package-lock.json index 0fa0d8e4..0d0df312 100644 --- a/remote/package-lock.json +++ b/remote/package-lock.json @@ -29,6 +29,7 @@ "@xterm/headless": "^5.6.0-beta.64", "@xterm/xterm": "^5.6.0-beta.64", "cookie": "^0.4.0", + "debounced": "1.0.2", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.2", "jschardet": "3.1.3", @@ -396,6 +397,12 @@ "node": ">= 0.6" } }, + "node_modules/debounced": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/debounced/-/debounced-1.0.2.tgz", + "integrity": "sha512-6GPv+l/OOtdb1DKNY70k5ubuJhVjtBjUnujC5vQAHHrMuvBpDXsTc91xEMTdeA3/v4swYHamtdB9XIN7DcKxpw==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", diff --git a/src/vs/platform/void/common/llmMessageService.ts b/src/vs/platform/void/common/llmMessageService.ts index fcec27f9..4718e6c2 100644 --- a/src/vs/platform/void/common/llmMessageService.ts +++ b/src/vs/platform/void/common/llmMessageService.ts @@ -12,6 +12,7 @@ import { createDecorator } from '../../instantiation/common/instantiation.js'; import { Event } from '../../../base/common/event.js'; import { Disposable } from '../../../base/common/lifecycle.js'; import { IVoidSettingsService } from './voidSettingsService.js'; +import { displayInfoOfProviderName, isFeatureNameDisabled } from './voidSettingsTypes.js'; // import { INotificationService } from '../../notification/common/notification.js'; // calls channel to implement features @@ -90,10 +91,24 @@ export class LLMMessageService extends Disposable implements ILLMMessageService const { onText, onFinalMessage, onError, ...proxyParams } = params; const { useProviderFor: featureName } = proxyParams - // end early if no provider + // throw an error if no model/provider selected (this should usually never be reached, the UI should check this first, but might happen in cases like Apply where we haven't built much UI/checks yet, good practice to have check logic on backend) + const isDisabled = isFeatureNameDisabled(featureName, this.voidSettingsService.state) const modelSelection = this.voidSettingsService.state.modelSelectionOfFeature[featureName] - if (modelSelection === null) { - onError({ message: 'Please add a Provider in Settings!', fullError: null }) + if (isDisabled || modelSelection === null) { + let message: string + + if (isDisabled === 'addProvider' || isDisabled === 'providerNotAutoDetected') + message = `Please add a provider in Void Settings.` + else if (isDisabled === 'addModel') + message = `Please add a model.` + else if (isDisabled === 'needToEnableModel') + message = `Please enable a model.` + else if (isDisabled === 'notFilledIn') + message = `Please fill in Void Settings${modelSelection !== null ? ` for ${displayInfoOfProviderName(modelSelection.providerName).title}` : ''}.` + else + message = 'Please add a provider in Void Settings.' + + onError({ message, fullError: null }) return null } diff --git a/src/vs/platform/void/common/llmMessageTypes.ts b/src/vs/platform/void/common/llmMessageTypes.ts index 5966f1b1..fb6a94fc 100644 --- a/src/vs/platform/void/common/llmMessageTypes.ts +++ b/src/vs/platform/void/common/llmMessageTypes.ts @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { ProviderName, SettingsOfProvider } from './voidSettingsTypes.js' +import { FeatureName, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js' export const errorDetails = (fullError: Error | null): string | null => { @@ -11,6 +11,7 @@ export const errorDetails = (fullError: Error | null): string | null => { return null } else if (typeof fullError === 'object') { + if (Object.keys(fullError).length === 0) return null return JSON.stringify(fullError, null, 2) } else if (typeof fullError === 'string') { @@ -24,28 +25,28 @@ export type OnFinalMessage = (p: { fullText: string }) => void export type OnError = (p: { message: string, fullError: Error | null }) => void export type AbortRef = { current: (() => void) | null } -export type LLMMessage = { +export type LLMChatMessage = { role: 'system' | 'user' | 'assistant'; content: string; } -export type _InternalLLMMessage = { +export type _InternalLLMChatMessage = { role: 'user' | 'assistant'; content: string; } -type _InternalOllamaFIMMessages = { +type _InternalSendFIMMessage = { prefix: string; suffix: string; stopTokens: string[]; } type SendLLMType = { - type: 'sendLLMMessage'; - messages: LLMMessage[]; + messagesType: 'chatMessages'; + messages: LLMChatMessage[]; } | { - type: 'ollamaFIM'; - messages: _InternalOllamaFIMMessages; + messagesType: 'FIMMessage'; + messages: _InternalSendFIMMessage; } // service types @@ -54,7 +55,7 @@ export type ServiceSendLLMMessageParams = { onFinalMessage: OnFinalMessage; onError: OnError; logging: { loggingName: string, }; - useProviderFor: 'Ctrl+K' | 'Ctrl+L' | 'Autocomplete'; + useProviderFor: FeatureName; } & SendLLMType // params to the true sendLLMMessage function @@ -85,7 +86,7 @@ export type EventLLMMessageOnFinalMessageParams = Parameters[0] export type EventLLMMessageOnErrorParams = Parameters[0] & { requestId: string } -export type _InternalSendLLMMessageFnType = ( +export type _InternalSendLLMChatMessageFnType = ( params: { onText: OnText; onFinalMessage: OnFinalMessage; @@ -95,11 +96,11 @@ export type _InternalSendLLMMessageFnType = ( modelName: string; _setAborter: (aborter: () => void) => void; - messages: _InternalLLMMessage[]; + messages: _InternalLLMChatMessage[]; } ) => void -export type _InternalOllamaFIMMessageFnType = ( +export type _InternalSendLLMFIMMessageFnType = ( params: { onText: OnText; onFinalMessage: OnFinalMessage; @@ -109,7 +110,7 @@ export type _InternalOllamaFIMMessageFnType = ( modelName: string; _setAborter: (aborter: () => void) => void; - messages: _InternalOllamaFIMMessages; + messages: _InternalSendFIMMessage; } ) => void diff --git a/src/vs/platform/void/common/refreshModelService.ts b/src/vs/platform/void/common/refreshModelService.ts index 811db0db..7ef6a068 100644 --- a/src/vs/platform/void/common/refreshModelService.ts +++ b/src/vs/platform/void/common/refreshModelService.ts @@ -44,8 +44,8 @@ export type RefreshModelStateOfProvider = Record -type EventProp = T extends 'globalSettings' ? [T, keyof VoidSettingsState[T]] : T | 'all' +// type RealVoidSettings = Exclude +// type EventProp = T extends 'globalSettings' ? [T, keyof VoidSettingsState[T]] : T | 'all' export interface IVoidSettingsService { @@ -51,7 +51,7 @@ export interface IVoidSettingsService { readonly state: VoidSettingsState; // in order to play nicely with react, you should immutably change state readonly waitForInitState: Promise; - onDidChangeState: Event; + onDidChangeState: Event; setSettingOfProvider: SetSettingOfProviderFn; setModelSelectionOfFeature: SetModelSelectionOfFeatureFn; @@ -64,26 +64,76 @@ export interface IVoidSettingsService { } -let _computeModelOptions = (settingsOfProvider: SettingsOfProvider) => { - let modelOptions: ModelOption[] = [] + +const _updatedValidatedState = (state: Omit) => { + + let newSettingsOfProvider = state.settingsOfProvider + + // recompute _didFillInProviderSettings for (const providerName of providerNames) { - const providerConfig = settingsOfProvider[providerName] - if (!providerConfig._enabled) continue // if disabled, don't display model options - for (const { modelName, isHidden } of providerConfig.models) { - if (isHidden) continue - modelOptions.push({ name: `${modelName} (${providerName})`, selection: { providerName, modelName } }) + const settingsAtProvider = newSettingsOfProvider[providerName] + + const didFillInProviderSettings = Object.keys(defaultProviderSettings[providerName]).every(key => !!settingsAtProvider[key as keyof typeof settingsAtProvider]) + + if (didFillInProviderSettings === settingsAtProvider._didFillInProviderSettings) continue + + newSettingsOfProvider = { + ...newSettingsOfProvider, + [providerName]: { + ...settingsAtProvider, + _didFillInProviderSettings: didFillInProviderSettings, + }, } } - return modelOptions + + // update model options + let newModelOptions: ModelOption[] = [] + for (const providerName of providerNames) { + const providerTitle = displayInfoOfProviderName(providerName).title.toLowerCase() // looks better lowercase, best practice to not use raw providerName + if (!newSettingsOfProvider[providerName]._didFillInProviderSettings) continue // if disabled, don't display model options + for (const { modelName, isHidden } of newSettingsOfProvider[providerName].models) { + if (isHidden) continue + newModelOptions.push({ name: `${modelName} (${providerTitle})`, selection: { providerName, modelName } }) + } + } + + // now that model options are updated, make sure the selection is valid + // if the user-selected model is no longer in the list, update the selection for each feature that needs it to something relevant (the 0th model available, or null) + let newModelSelectionOfFeature = state.modelSelectionOfFeature + for (const featureName of featureNames) { + + const modelSelectionAtFeature = newModelSelectionOfFeature[featureName] + const selnIdx = modelSelectionAtFeature === null ? -1 : newModelOptions.findIndex(m => modelSelectionsEqual(m.selection, modelSelectionAtFeature)) + + if (selnIdx !== -1) continue + + newModelSelectionOfFeature = { + ...newModelSelectionOfFeature, + [featureName]: newModelOptions.length === 0 ? null : newModelOptions[0].selection + } + } + + + const newState = { + ...state, + settingsOfProvider: newSettingsOfProvider, + modelSelectionOfFeature: newModelSelectionOfFeature, + _modelOptions: newModelOptions, + } satisfies VoidSettingsState + + return newState } + + + const defaultState = () => { const d: VoidSettingsState = { settingsOfProvider: deepClone(defaultSettingsOfProvider), modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null, 'FastApply': null }, globalSettings: deepClone(defaultGlobalSettings), - _modelOptions: _computeModelOptions(defaultSettingsOfProvider), // computed + _modelOptions: [], // computed later } return d } @@ -93,8 +143,8 @@ export const IVoidSettingsService = createDecorator('VoidS class VoidSettingsService extends Disposable implements IVoidSettingsService { _serviceBrand: undefined; - private readonly _onDidChangeState = new Emitter(); - readonly onDidChangeState: Event = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes + private readonly _onDidChangeState = new Emitter(); + readonly onDidChangeState: Event = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes state: VoidSettingsState; waitForInitState: Promise // await this if you need a valid state initially @@ -118,39 +168,47 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { this._readState().then(readS => { // the stored data structure might be outdated, so we need to update it here (can do a more general solution later when we need to) - readS = { - ...readS, - settingsOfProvider: { - // A HACK BECAUSE WE ADDED DEEPSEEK (did not exist before, comes before readS) - ...{ deepseek: defaultSettingsOfProvider.deepseek }, + const newSettingsOfProvider = { + // A HACK BECAUSE WE ADDED DEEPSEEK (did not exist before, comes before readS) + ...{ deepseek: defaultSettingsOfProvider.deepseek }, - // A HACK BECAUSE WE ADDED MISTRAL (did not exist before, comes before readS) - ...{ mistral: defaultSettingsOfProvider.mistral }, + // A HACK BECAUSE WE ADDED MISTRAL (did not exist before, comes before readS) + ...{ mistral: defaultSettingsOfProvider.mistral }, - ...readS.settingsOfProvider, + ...readS.settingsOfProvider, - // A HACK BECAUSE WE ADDED NEW GEMINI MODELS (existed before, comes after readS) - gemini: { - ...readS.settingsOfProvider.gemini, - models: [ - ...readS.settingsOfProvider.gemini.models, - ...defaultSettingsOfProvider.gemini.models.filter(m => /* if cant find the model in readS (yes this is O(n^2), very small) */ !readS.settingsOfProvider.gemini.models.find(m2 => m2.modelName === m.modelName)) - ] - } - }, - modelSelectionOfFeature: { - // A HACK BECAUSE WE ADDED FastApply - ...{ 'FastApply': null }, - ...readS.modelSelectionOfFeature, + // A HACK BECAUSE WE ADDED NEW GEMINI MODELS (existed before, comes after readS) + gemini: { + ...readS.settingsOfProvider.gemini, + models: [ + ...readS.settingsOfProvider.gemini.models, + ...defaultSettingsOfProvider.gemini.models.filter(m => /* if cant find the model in readS (yes this is O(n^2), very small) */ !readS.settingsOfProvider.gemini.models.find(m2 => m2.modelName === m.modelName)) + ] } } - this.state = readS + const newModelSelectionOfFeature = { + // A HACK BECAUSE WE ADDED FastApply + ...{ 'FastApply': null }, + ...readS.modelSelectionOfFeature, + } + + readS = { + ...readS, + settingsOfProvider: newSettingsOfProvider, + modelSelectionOfFeature: newModelSelectionOfFeature, + } + + this.state = _updatedValidatedState(readS) + resolver() - this._onDidChangeState.fire('all') + this._onDidChangeState.fire() }) + + } + private async _readState(): Promise { const encryptedState = this._storageService.get(STORAGE_KEY, StorageScope.APPLICATION) @@ -172,7 +230,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { const newModelSelectionOfFeature = this.state.modelSelectionOfFeature - const newSettingsOfProvider = { + const newSettingsOfProvider: SettingsOfProvider = { ...this.state.settingsOfProvider, [providerName]: { ...this.state.settingsOfProvider[providerName], @@ -182,38 +240,17 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { const newGlobalSettings = this.state.globalSettings - // if changed models or enabled a provider, recompute models list - const modelsListChanged = settingName === 'models' || settingName === '_enabled' - const newModelsList = modelsListChanged ? _computeModelOptions(newSettingsOfProvider) : this.state._modelOptions - - const newState: VoidSettingsState = { + const newState = { modelSelectionOfFeature: newModelSelectionOfFeature, settingsOfProvider: newSettingsOfProvider, globalSettings: newGlobalSettings, - _modelOptions: newModelsList, } - // this must go above this.setanythingelse() - this.state = newState - - // if the user-selected model is no longer in the list, update the selection for each feature that needs it to something relevant (the 0th model available, or null) - if (modelsListChanged) { - for (const featureName of featureNames) { - - const currentSelection = newModelSelectionOfFeature[featureName] - const selnIdx = currentSelection === null ? -1 : newModelsList.findIndex(m => modelSelectionsEqual(m.selection, currentSelection)) - - if (selnIdx === -1) { - if (newModelsList.length !== 0) - this.setModelSelectionOfFeature(featureName, newModelsList[0].selection, { doNotApplyEffects: true }) - else - this.setModelSelectionOfFeature(featureName, null, { doNotApplyEffects: true }) - } - } - } + this.state = _updatedValidatedState(newState) await this._storeState() - this._onDidChangeState.fire('settingsOfProvider') + this._onDidChangeState.fire() + } @@ -227,7 +264,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { } this.state = newState await this._storeState() - this._onDidChangeState.fire(['globalSettings', settingName]) + this._onDidChangeState.fire() } @@ -247,7 +284,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { return await this._storeState() - this._onDidChangeState.fire('modelSelectionOfFeature') + this._onDidChangeState.fire() } @@ -256,23 +293,23 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { const { models } = this.state.settingsOfProvider[providerName] - const old_names = models.map(m => m.modelName) + const oldModelNames = models.map(m => m.modelName) - const newDefaultModels = modelInfoOfDefaultNames(newDefaultModelNames, { isAutodetected: true, existingModels: models }) - const newModels = [ - ...newDefaultModels, - ...models.filter(m => !m.isDefault), // keep any non-default models + const newDefaultModelInfo = modelInfoOfDefaultModelNames(newDefaultModelNames, { isAutodetected: true, existingModels: models }) + const newModelInfo = [ + ...newDefaultModelInfo, // swap out all the default models for the new default models + ...models.filter(m => !m.isDefault), // keep any non-defaul (custom) models ] - this.setSettingOfProvider(providerName, 'models', newModels) + this.setSettingOfProvider(providerName, 'models', newModelInfo) // if the models changed, log it - const new_names = newModels.map(m => m.modelName) - if (!(old_names.length === new_names.length - && old_names.every((_, i) => old_names[i] === new_names[i]) - )) { - this._metricsService.capture('Autodetect Models', { providerName, newModels, ...logging }) + const new_names = newModelInfo.map(m => m.modelName) + if (!(oldModelNames.length === new_names.length + && oldModelNames.every((_, i) => oldModelNames[i] === new_names[i])) + ) { + this._metricsService.capture('Autodetect Models', { providerName, newModels: newModelInfo, ...logging }) } } toggleModelHidden(providerName: ProviderName, modelName: string) { diff --git a/src/vs/platform/void/common/voidSettingsTypes.ts b/src/vs/platform/void/common/voidSettingsTypes.ts index 8caea3d2..fed10807 100644 --- a/src/vs/platform/void/common/voidSettingsTypes.ts +++ b/src/vs/platform/void/common/voidSettingsTypes.ts @@ -4,28 +4,28 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ - +import { VoidSettingsState } from './voidSettingsService.js' export type VoidModelInfo = { modelName: string, isDefault: boolean, // whether or not it's a default for its provider - isHidden: boolean, // whether or not the user is hiding it + isHidden: boolean, // whether or not the user is hiding it (switched off) isAutodetected?: boolean, // whether the model was autodetected by polling } // creates `modelInfo` from `modelNames` -export const modelInfoOfDefaultNames = (modelNames: string[], options?: { isAutodetected: true, existingModels: VoidModelInfo[] }): VoidModelInfo[] => { +export const modelInfoOfDefaultModelNames = (defaultModelNames: string[], options?: { isAutodetected: true, existingModels: VoidModelInfo[] }): VoidModelInfo[] => { const { isAutodetected, existingModels } = options ?? {} if (!existingModels) { // default settings - return modelNames.map((modelName, i) => ({ + return defaultModelNames.map((modelName, i) => ({ modelName, isDefault: true, isAutodetected: isAutodetected, - isHidden: modelNames.length >= 10 // hide all models if there are a ton of them, and make user enable them individually + isHidden: defaultModelNames.length >= 10 // hide all models if there are a ton of them, and make user enable them individually })) } else { // settings if there are existing models (keep existing `isHidden` property) @@ -35,7 +35,7 @@ export const modelInfoOfDefaultNames = (modelNames: string[], options?: { isAuto existingModelsMap[existingModel.modelName] = existingModel } - return modelNames.map((modelName, i) => ({ + return defaultModelNames.map((modelName, i) => ({ modelName, isDefault: true, isAutodetected: isAutodetected, @@ -47,7 +47,7 @@ export const modelInfoOfDefaultNames = (modelNames: string[], options?: { isAuto } // https://docs.anthropic.com/en/docs/about-claude/models -export const defaultAnthropicModels = modelInfoOfDefaultNames([ +export const defaultAnthropicModels = modelInfoOfDefaultModelNames([ 'claude-3-5-sonnet-20241022', 'claude-3-5-haiku-20241022', 'claude-3-opus-20240229', @@ -57,9 +57,10 @@ export const defaultAnthropicModels = modelInfoOfDefaultNames([ // https://platform.openai.com/docs/models/gp -export const defaultOpenAIModels = modelInfoOfDefaultNames([ - 'o1-preview', +export const defaultOpenAIModels = modelInfoOfDefaultModelNames([ + 'o1', 'o1-mini', + 'o3-mini', 'gpt-4o', 'gpt-4o-mini', // 'gpt-4o-2024-05-13', @@ -78,14 +79,14 @@ export const defaultOpenAIModels = modelInfoOfDefaultNames([ ]) // https://platform.openai.com/docs/models/gp -export const defaultDeepseekModels = modelInfoOfDefaultNames([ +export const defaultDeepseekModels = modelInfoOfDefaultModelNames([ 'deepseek-chat', 'deepseek-reasoner', ]) // https://console.groq.com/docs/models -export const defaultGroqModels = modelInfoOfDefaultNames([ +export const defaultGroqModels = modelInfoOfDefaultModelNames([ "llama3-70b-8192", "llama-3.3-70b-versatile", "llama-3.1-8b-instant", @@ -94,7 +95,7 @@ export const defaultGroqModels = modelInfoOfDefaultNames([ ]) -export const defaultGeminiModels = modelInfoOfDefaultNames([ +export const defaultGeminiModels = modelInfoOfDefaultModelNames([ 'gemini-1.5-flash', 'gemini-1.5-pro', 'gemini-1.5-flash-8b', @@ -103,7 +104,7 @@ export const defaultGeminiModels = modelInfoOfDefaultNames([ 'learnlm-1.5-pro-experimental' ]) -export const defaultMistralModels = modelInfoOfDefaultNames([ +export const defaultMistralModels = modelInfoOfDefaultModelNames([ "codestral-latest", "open-codestral-mamba", "open-mistral-nemo", @@ -187,20 +188,22 @@ export const customSettingNamesOfProvider = (providerName: ProviderName) => { } + + type CommonProviderSettings = { - _enabled: boolean | undefined, // undefined initially, computed when user types in all fields + _didFillInProviderSettings: boolean | undefined, // undefined initially, computed when user types in all fields models: VoidModelInfo[], } -export type SettingsForProvider = CustomProviderSettings & CommonProviderSettings +export type SettingsAtProvider = CustomProviderSettings & CommonProviderSettings // part of state export type SettingsOfProvider = { - [providerName in ProviderName]: SettingsForProvider + [providerName in ProviderName]: SettingsAtProvider } -export type SettingName = keyof SettingsForProvider +export type SettingName = keyof SettingsAtProvider @@ -309,7 +312,7 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName undefined, } } - else if (settingName === '_enabled') { + else if (settingName === '_didFillInProviderSettings') { return { title: '(never)', placeholder: '(never)', @@ -373,56 +376,56 @@ export const defaultSettingsOfProvider: SettingsOfProvider = { ...defaultCustomSettings, ...defaultProviderSettings.anthropic, ...voidInitModelOptions.anthropic, - _enabled: undefined, + _didFillInProviderSettings: undefined, }, openAI: { ...defaultCustomSettings, ...defaultProviderSettings.openAI, ...voidInitModelOptions.openAI, - _enabled: undefined, + _didFillInProviderSettings: undefined, }, deepseek: { ...defaultCustomSettings, ...defaultProviderSettings.deepseek, ...voidInitModelOptions.deepseek, - _enabled: undefined, + _didFillInProviderSettings: undefined, }, gemini: { ...defaultCustomSettings, ...defaultProviderSettings.gemini, ...voidInitModelOptions.gemini, - _enabled: undefined, - }, - groq: { - ...defaultCustomSettings, - ...defaultProviderSettings.groq, - ...voidInitModelOptions.groq, - _enabled: undefined, - }, - ollama: { - ...defaultCustomSettings, - ...defaultProviderSettings.ollama, - ...voidInitModelOptions.ollama, - _enabled: undefined, - }, - openRouter: { - ...defaultCustomSettings, - ...defaultProviderSettings.openRouter, - ...voidInitModelOptions.openRouter, - _enabled: undefined, - }, - openAICompatible: { - ...defaultCustomSettings, - ...defaultProviderSettings.openAICompatible, - ...voidInitModelOptions.openAICompatible, - _enabled: undefined, + _didFillInProviderSettings: undefined, }, mistral: { ...defaultCustomSettings, ...defaultProviderSettings.mistral, ...voidInitModelOptions.mistral, - _enabled: undefined, - } + _didFillInProviderSettings: undefined, + }, + groq: { // aggregator + ...defaultCustomSettings, + ...defaultProviderSettings.groq, + ...voidInitModelOptions.groq, + _didFillInProviderSettings: undefined, + }, + openRouter: { // aggregator + ...defaultCustomSettings, + ...defaultProviderSettings.openRouter, + ...voidInitModelOptions.openRouter, + _didFillInProviderSettings: undefined, + }, + openAICompatible: { // aggregator + ...defaultCustomSettings, + ...defaultProviderSettings.openAICompatible, + ...voidInitModelOptions.openAICompatible, + _didFillInProviderSettings: undefined, + }, + ollama: { // aggregator + ...defaultCustomSettings, + ...defaultProviderSettings.ollama, + ...voidInitModelOptions.ollama, + _didFillInProviderSettings: undefined, + }, } @@ -441,20 +444,16 @@ export const displayInfoOfFeatureName = (featureName: FeatureName) => { if (featureName === 'Autocomplete') return 'Autocomplete' else if (featureName === 'Ctrl+K') - return 'Quick Edit' + return 'Quick-Edit' else if (featureName === 'Ctrl+L') - return 'Sidebar Chat' + return 'Chat' else if (featureName === 'FastApply') - return 'Fast Apply' + return 'Apply' else throw new Error(`Feature Name ${featureName} not allowed`) } - - - - // the models of these can be refreshed (in theory all can, but not all should) export const refreshableProviderNames = localProviderNames export type RefreshableProviderName = typeof refreshableProviderNames[number] @@ -464,6 +463,45 @@ export type RefreshableProviderName = typeof refreshableProviderNames[number] +// use this in isFeatuerNameDissbled +export const isProviderNameDisabled = (providerName: ProviderName, settingsState: VoidSettingsState) => { + + const settingsAtProvider = settingsState.settingsOfProvider[providerName] + const isAutodetected = (refreshableProviderNames as string[]).includes(providerName) + + const isDisabled = settingsAtProvider.models.length === 0 + if (isDisabled) { + return isAutodetected ? 'providerNotAutoDetected' : (!settingsAtProvider._didFillInProviderSettings ? 'notFilledIn' : 'addModel') + } + return false +} + +export const isFeatureNameDisabled = (featureName: FeatureName, settingsState: VoidSettingsState) => { + // if has a selected provider, check if it's enabled + const selectedProvider = settingsState.modelSelectionOfFeature[featureName] + + if (selectedProvider) { + const { providerName } = selectedProvider + return isProviderNameDisabled(providerName, settingsState) + } + + // if there are any models they can turn on, tell them that + const canTurnOnAModel = !!providerNames.find(providerName => settingsState.settingsOfProvider[providerName].models.filter(m => m.isHidden).length !== 0) + if (canTurnOnAModel) return 'needToEnableModel' + + // if there are any providers filled in, then they just need to add a model + const anyFilledIn = !!providerNames.find(providerName => settingsState.settingsOfProvider[providerName]._didFillInProviderSettings) + if (anyFilledIn) return 'addModel' + + return 'addProvider' +} + + + + + + + export type GlobalSettings = { @@ -479,3 +517,88 @@ export type GlobalSettingName = keyof GlobalSettings export const globalSettingNames = Object.keys(defaultGlobalSettings) as GlobalSettingName[] + + + + + + + + + + + + + + +export const recognizedModels = [ + + // chat + 'OpenAI 4o', + 'Anthropic Claude', + 'Llama 3.x', + 'Deepseek Chat', // deepseek coder v2 is now merged into chat (V3) https://api-docs.deepseek.com/updates#deepseek-coder--deepseek-chat-upgraded-to-deepseek-v25-model + // 'xAI Grok', + // 'Google Gemini, Gemma', + // 'Microsoft Phi4', + + + // coding (autocomplete) + 'Alibaba Qwen2.5 Coder Instruct', // we recommend this over Qwen2.5 + 'Mistral Codestral', + + // thinking + 'OpenAI o1, o3', + 'Deepseek R1', + + // general + '' + // 'Mixtral 8x7b' + // 'Qwen2.5', + +] as const + + + + +type RecognizedModel = (typeof recognizedModels)[number] + + +// const modelCapabilities: { [recognizedModel in RecognizedModel]: ({ }) => string } = { +// 'OpenAI 4o': { +// template: ({ prefix, suffix, }: { prefix: string; suffix: string; }) => `\ +// ` +// } +// } + +export function getRecognizedModel(modelName: string): RecognizedModel { + const lower = modelName.toLowerCase(); + + if (lower.includes('gpt-4o')) { + return 'OpenAI 4o'; + } + if (lower.includes('claude')) { + return 'Anthropic Claude'; + } + if (lower.includes('llama')) { + return 'Llama 3.x'; + } + if (lower.includes('qwen2.5-coder')) { + return 'Alibaba Qwen2.5 Coder Instruct'; + } + if (lower.includes('mistral')) { + return 'Mistral Codestral'; + } + // Check for "o1" or "o3" + if (/\bo1\b/.test(lower) || /\bo3\b/.test(lower)) { + return 'OpenAI o1, o3'; + } + if (lower.includes('deepseek-r1') || lower.includes('deepseek-reasoner')) { + return 'Deepseek R1'; + } + + + + // Fallback: + return ''; +} diff --git a/src/vs/platform/void/electron-main/llmMessage/anthropic.ts b/src/vs/platform/void/electron-main/llmMessage/anthropic.ts index 59d4c04a..ea220eed 100644 --- a/src/vs/platform/void/electron-main/llmMessage/anthropic.ts +++ b/src/vs/platform/void/electron-main/llmMessage/anthropic.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------*/ import Anthropic from '@anthropic-ai/sdk'; -import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js'; +import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js'; import { anthropicMaxPossibleTokens } from '../../common/voidSettingsTypes.js'; -export const sendAnthropicMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { +export const sendAnthropicChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { const thisConfig = settingsOfProvider.anthropic diff --git a/src/vs/platform/void/electron-main/llmMessage/gemini.ts b/src/vs/platform/void/electron-main/llmMessage/gemini.ts index 936a68f0..eef8cc3a 100644 --- a/src/vs/platform/void/electron-main/llmMessage/gemini.ts +++ b/src/vs/platform/void/electron-main/llmMessage/gemini.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------*/ import { Content, GoogleGenerativeAI } from '@google/generative-ai'; -import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js'; +import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js'; // Gemini -export const sendGeminiMsg: _InternalSendLLMMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { +export const sendGeminiChat: _InternalSendLLMChatMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { let fullText = '' diff --git a/src/vs/platform/void/electron-main/llmMessage/groq.ts b/src/vs/platform/void/electron-main/llmMessage/groq.ts index 70a4250b..b8c29981 100644 --- a/src/vs/platform/void/electron-main/llmMessage/groq.ts +++ b/src/vs/platform/void/electron-main/llmMessage/groq.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------*/ import Groq from 'groq-sdk'; -import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js'; +import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js'; // Groq -export const sendGroqMsg: _InternalSendLLMMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { +export const sendGroqChat: _InternalSendLLMChatMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { let fullText = ''; const thisConfig = settingsOfProvider.groq diff --git a/src/vs/platform/void/electron-main/llmMessage/mistral.ts b/src/vs/platform/void/electron-main/llmMessage/mistral.ts index 74f9233e..8fddaf2e 100644 --- a/src/vs/platform/void/electron-main/llmMessage/mistral.ts +++ b/src/vs/platform/void/electron-main/llmMessage/mistral.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------*/ import { Mistral } from '@mistralai/mistralai'; -import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js'; +import { _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js'; // Mistral -export const sendMistralMsg: _InternalSendLLMMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { +export const sendMistralChat: _InternalSendLLMChatMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { let fullText = ''; const thisConfig = settingsOfProvider.mistral; diff --git a/src/vs/platform/void/electron-main/llmMessage/ollama.ts b/src/vs/platform/void/electron-main/llmMessage/ollama.ts index e76b6186..43c817a3 100644 --- a/src/vs/platform/void/electron-main/llmMessage/ollama.ts +++ b/src/vs/platform/void/electron-main/llmMessage/ollama.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------*/ import { Ollama } from 'ollama'; -import { _InternalModelListFnType, _InternalOllamaFIMMessageFnType, _InternalSendLLMMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js'; +import { _InternalModelListFnType, _InternalSendLLMFIMMessageFnType, _InternalSendLLMChatMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js'; import { defaultProviderSettings } from '../../common/voidSettingsTypes.js'; export const ollamaList: _InternalModelListFnType = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => { @@ -38,7 +38,7 @@ export const ollamaList: _InternalModelListFnType = async ( } -export const sendOllamaFIM: _InternalOllamaFIMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { +export const sendOllamaFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { const thisConfig = settingsOfProvider.ollama // if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in @@ -54,10 +54,11 @@ export const sendOllamaFIM: _InternalOllamaFIMMessageFnType = ({ messages, onTex suffix: messages.suffix, options: { stop: messages.stopTokens, + num_predict: 300, // max tokens + // repeat_penalty: 1, }, raw: true, stream: true, - // options: { num_predict: parseMaxTokensStr(thisConfig.maxTokens) } // this is max_tokens }) .then(async stream => { _setAborter(() => stream.abort()) @@ -77,7 +78,7 @@ export const sendOllamaFIM: _InternalOllamaFIMMessageFnType = ({ messages, onTex // Ollama -export const sendOllamaMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { +export const sendOllamaChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => { const thisConfig = settingsOfProvider.ollama // if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in diff --git a/src/vs/platform/void/electron-main/llmMessage/openai.ts b/src/vs/platform/void/electron-main/llmMessage/openai.ts index 5160fa02..268eadfb 100644 --- a/src/vs/platform/void/electron-main/llmMessage/openai.ts +++ b/src/vs/platform/void/electron-main/llmMessage/openai.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------*/ import OpenAI from 'openai'; -import { _InternalModelListFnType, _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js'; +import { _InternalModelListFnType, _InternalSendLLMFIMMessageFnType, _InternalSendLLMChatMessageFnType } from '../../common/llmMessageTypes.js'; import { Model } from 'openai/resources/models.js'; // import { parseMaxTokensStr } from './util.js'; @@ -43,49 +43,81 @@ export const openaiCompatibleList: _InternalModelListFnType = async ({ on - -// OpenAI, OpenRouter, OpenAICompatible -export const sendOpenAIMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) => { - - let fullText = '' - - let openai: OpenAI - let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming - +type NewParams = Pick[0] & Parameters<_InternalSendLLMFIMMessageFnType>[0], 'settingsOfProvider' | 'providerName'> +const newOpenAI = ({ settingsOfProvider, providerName }: NewParams) => { if (providerName === 'openAI') { const thisConfig = settingsOfProvider.openAI - openai = new OpenAI({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true }); - options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ } + return new OpenAI({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true }); } else if (providerName === 'openRouter') { const thisConfig = settingsOfProvider.openRouter - openai = new OpenAI({ + return new OpenAI({ baseURL: 'https://openrouter.ai/api/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, defaultHeaders: { 'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings. 'X-Title': 'Void Editor', // Optional. Shows in rankings on openrouter.ai. }, - }); - options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ } + }) } else if (providerName === 'deepseek') { const thisConfig = settingsOfProvider.deepseek - openai = new OpenAI({ + return new OpenAI({ baseURL: 'https://api.deepseek.com/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true, - }); - options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ } + }) } else if (providerName === 'openAICompatible') { const thisConfig = settingsOfProvider.openAICompatible - openai = new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true }) - options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ } + return new OpenAI({ + baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true + }) } else { console.error(`sendOpenAIMsg: invalid providerName: ${providerName}`) throw new Error(`providerName was invalid: ${providerName}`) } +} + + + +export const sendOpenAIFIM: _InternalSendLLMFIMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) => { + + + const openai: OpenAI = newOpenAI({ providerName, settingsOfProvider }) + + const options: OpenAI.Completions.CompletionCreateParamsStreaming = { + prompt: messages.prefix, + suffix: messages.suffix, + model: modelName, + stream: true, + // max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens) + } + + + openai.completions + .create(options) + .then(async response => { + // TODO!!! + console.log('RESPONSE', response) + }) + +} + + + +// OpenAI, OpenRouter, OpenAICompatible +export const sendOpenAIChat: _InternalSendLLMChatMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) => { + + let fullText = '' + + const openai: OpenAI = newOpenAI({ providerName, settingsOfProvider }) + const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = { + model: modelName, + messages: messages, + stream: true, + // max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens) + } openai.chat.completions .create(options) diff --git a/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts index ab64fd85..7cea9d5a 100644 --- a/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts @@ -3,18 +3,19 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { SendLLMMessageParams, OnText, OnFinalMessage, OnError, LLMMessage, _InternalLLMMessage } from '../../common/llmMessageTypes.js'; +import { SendLLMMessageParams, OnText, OnFinalMessage, OnError, LLMChatMessage, _InternalLLMChatMessage } from '../../common/llmMessageTypes.js'; import { IMetricsService } from '../../common/metricsService.js'; -import { sendAnthropicMsg } from './anthropic.js'; -import { sendOllamaFIM, sendOllamaMsg } from './ollama.js'; -import { sendOpenAIMsg } from './openai.js'; -import { sendGeminiMsg } from './gemini.js'; -import { sendGroqMsg } from './groq.js'; -import { sendMistralMsg } from './mistral.js'; +import { sendAnthropicChat } from './anthropic.js'; +import { sendOllamaFIM, sendOllamaChat } from './ollama.js'; +import { sendOpenAIChat } from './openai.js'; +import { sendGeminiChat } from './gemini.js'; +import { sendGroqChat } from './groq.js'; +import { sendMistralChat } from './mistral.js'; +import { displayInfoOfProviderName } from '../../common/voidSettingsTypes.js'; -const cleanMessages = (messages: LLMMessage[]): _InternalLLMMessage[] => { +const cleanChatMessages = (messages: LLMChatMessage[]): _InternalLLMChatMessage[] => { // trim message content (Anthropic and other providers give an error if there is trailing whitespace) messages = messages.map(m => ({ ...m, content: m.content.trim() })) @@ -26,7 +27,7 @@ const cleanMessages = (messages: LLMMessage[]): _InternalLLMMessage[] => { // remove all system messages const noSystemMessages = messages - .filter(msg => msg.role !== 'system') as _InternalLLMMessage[] + .filter(msg => msg.role !== 'system') as _InternalLLMChatMessage[] // add system mesasges to first message (should be a user message) if (systemMessage && (noSystemMessages.length !== 0)) { @@ -49,7 +50,7 @@ const cleanMessages = (messages: LLMMessage[]): _InternalLLMMessage[] => { export const sendLLMMessage = ({ - type, + messagesType, aiInstructions, messages: messages_, onText: onText_, @@ -66,21 +67,22 @@ export const sendLLMMessage = ({ ) => { // messages.unshift({ role: 'system', content: aiInstructions }) - const messagesArr = type === 'sendLLMMessage' ? cleanMessages(messages_) : [] + const messagesArr = messagesType === 'chatMessages' ? cleanChatMessages(messages_) : [] // only captures number of messages and message "shape", no actual code, instructions, prompts, etc const captureLLMEvent = (eventId: string, extras?: object) => { metricsService.capture(eventId, { providerName, modelName, - ...type === 'sendLLMMessage' ? { + ...messagesType === 'chatMessages' ? { numMessages: messagesArr?.length, messagesShape: messagesArr?.map(msg => ({ role: msg.role, length: msg.content.length })), origNumMessages: messages_?.length, origMessagesShape: messages_?.map(msg => ({ role: msg.role, length: msg.content.length })), - } : type === 'ollamaFIM' ? { - + } : messagesType === 'FIMMessage' ? { + prefixLength: messages_.prefix.length, + suffixLength: messages_.suffix.length, } : {}, ...extras, @@ -108,6 +110,11 @@ export const sendLLMMessage = ({ const onError: OnError = ({ message: error, fullError }) => { if (_didAbort) return console.error('sendLLMMessage onError:', error) + + // handle failed to fetch errors, which give 0 information by design + if (error === 'TypeError: fetch failed') + error = `Failed to fetch from ${displayInfoOfProviderName(providerName).title}. This likely means you specified the wrong endpoint in Void Settings, or your local model provider like Ollama is powered off.` + captureLLMEvent(`${loggingName} - Error`, { error }) onError_({ message: error, fullError }) } @@ -125,28 +132,32 @@ export const sendLLMMessage = ({ try { switch (providerName) { case 'anthropic': - sendAnthropicMsg({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); + sendAnthropicChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); break; case 'openAI': case 'openRouter': case 'deepseek': case 'openAICompatible': - sendOpenAIMsg({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); + sendOpenAIChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); break; case 'gemini': - sendGeminiMsg({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); + sendGeminiChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); break; case 'ollama': - if (type === 'ollamaFIM') + if ( // TODO @andrew in future we want to use our own templates instead of using ollamaFIM + messagesType === 'FIMMessage' + && settingsOfProvider['ollama']._didFillInProviderSettings + && settingsOfProvider['ollama'].models.some(m => !m.isHidden) + ) sendOllamaFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) else - sendOllamaMsg({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); + sendOllamaChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); break; case 'groq': - sendGroqMsg({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); + sendGroqChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); break; case 'mistral': - sendMistralMsg({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); + sendMistralChat({ messages: messagesArr, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }); break; default: onError({ message: `Error: Void provider was "${providerName}", which is not recognized.`, fullError: null }) diff --git a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts index 975f494c..d16183a3 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts @@ -289,6 +289,7 @@ export interface IFileTemplateData { readonly templateDisposables: DisposableStore; readonly elementDisposables: DisposableStore; readonly label: IResourceLabel; + // readonly voidLabels: IResourceLabel; readonly container: HTMLElement; readonly contribs: IExplorerFileContribution[]; currentContext?: ExplorerItem; @@ -347,15 +348,25 @@ export class FilesRenderer implements ICompressibleTreeRenderer { + // // console.log('ON CLICK', templateData.currentContext?.children) + // // }) + // const voidLabels = this.labels.create(voidButtonsContainer, { supportHighlights: false, supportIcons: false, }); + // voidLabels.element.textContent = 'hi333' + const label = templateDisposables.add(this.labels.create(container, { supportHighlights: true })); templateDisposables.add(label.onDidRender(() => { - try { - if (templateData.currentContext) { - this.updateWidth(templateData.currentContext); - } - } catch (e) { - // noop since the element might no longer be in the tree, no update of width necessary - } + try { if (templateData.currentContext) this.updateWidth(templateData.currentContext); } + catch (e) { /* noop since the element might no longer be in the tree, no update of width necessary*/ } })); const contribs = explorerFileContribRegistry.create(this.instantiationService, container, templateDisposables); @@ -365,10 +376,15 @@ export class FilesRenderer implements ICompressibleTreeRenderer, index: number, templateData: IFileTemplateData): void { const stat = node.element; templateData.currentContext = stat; @@ -382,8 +398,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer c.setResource(undefined)); @@ -477,6 +492,13 @@ export class FilesRenderer implements ICompressibleTreeRenderer { label: { container: label, onDidRender: emitter.event - } + }, + // voidLabels: { + // container: label, + // onDidRender: emitter.event + // }, + }, 1, false); ds.add(navigationController); diff --git a/src/vs/workbench/contrib/void/browser/autocompleteService.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts index eb4d9f7b..6b67c6be 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -8,7 +8,7 @@ import { ILanguageFeaturesService } from '../../../../editor/common/services/lan import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { ITextModel } from '../../../../editor/common/model.js'; import { Position } from '../../../../editor/common/core/position.js'; -import { InlineCompletion, InlineCompletionContext, LocationLink } from '../../../../editor/common/languages.js'; +import { InlineCompletion, InlineCompletionContext, } from '../../../../editor/common/languages.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { Range } from '../../../../editor/common/core/range.js'; import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js'; @@ -19,6 +19,7 @@ import { IModelService } from '../../../../editor/common/services/model.js'; import { extractCodeFromRegular } from './helpers/extractCodeFromResult.js'; import { isWindows } from '../../../../base/common/platform.js'; import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; +// import { IContextGatheringService } from './contextGatheringService.js'; // The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts @@ -155,6 +156,7 @@ type Autocompletion = { llmPromise: Promise | undefined, insertText: string, requestId: string | null, + _newlineCount: number, } const DEBOUNCE_TIME = 500 @@ -163,13 +165,16 @@ const MAX_CACHE_SIZE = 20 const MAX_PENDING_REQUESTS = 2 // postprocesses the result -const joinSpaces = (result: string) => { +const processStartAndEndSpaces = (result: string) => { // trim all whitespace except for a single leading/trailing space // return result.trim() + [result,] = extractCodeFromRegular({ text: result, recentlyAddedTextLen: result.length }) + const hasLeadingSpace = result.startsWith(' '); const hasTrailingSpace = result.endsWith(' '); + return (hasLeadingSpace ? ' ' : '') + result.trim() + (hasTrailingSpace ? ' ' : ''); @@ -196,22 +201,26 @@ const removeLeftTabsAndTrimEnds = (s: string): string => { const removeAllWhitespace = (str: string): string => str.replace(/\s+/g, ''); -function isSubsequence({ of, subsequence }: { of: string, subsequence: string }): boolean { - if (subsequence.length === 0) return true; - if (of.length === 0) return false; + + +function getIsSubsequence({ of, subsequence }: { of: string, subsequence: string }): [boolean, string] { + if (subsequence.length === 0) return [true, '']; + if (of.length === 0) return [false, '']; let subsequenceIndex = 0; + let lastMatchChar = ''; for (let i = 0; i < of.length; i++) { if (of[i] === subsequence[subsequenceIndex]) { + lastMatchChar = of[i]; subsequenceIndex++; } if (subsequenceIndex === subsequence.length) { - return true; + return [true, lastMatchChar]; } } - return false; + return [false, lastMatchChar]; } @@ -251,7 +260,6 @@ function getStringUpToUnbalancedClosingParenthesis(s: string, prefix: string): s } - // further trim the autocompletion const postprocessAutocompletion = ({ autocompletionMatchup, autocompletion, prefixAndSuffix }: { autocompletionMatchup: AutocompletionMatchupBounds, autocompletion: Autocompletion, prefixAndSuffix: PrefixAndSuffixInfo }) => { @@ -357,15 +365,24 @@ const toInlineCompletions = ({ autocompletionMatchup, autocompletion, prefixAndS // if we redid the suffix, replace the suffix if (autocompletion.type === 'single-line-redo-suffix') { - if (isSubsequence({ // check that the old text contains the same brackets + symbols as the new text - subsequence: removeAllWhitespace(prefixAndSuffix.suffixToTheRightOfCursor), // old suffix - of: removeAllWhitespace(autocompletion.insertText), // new suffix (note that this should not be `trimmedInsertText`) - })) { + + const oldSuffix = prefixAndSuffix.suffixToTheRightOfCursor + const newSuffix = autocompletion.insertText + + const [isSubsequence, lastMatchingChar] = getIsSubsequence({ // check that the old text contains the same brackets + symbols as the new text + subsequence: removeAllWhitespace(oldSuffix), // old suffix + of: removeAllWhitespace(newSuffix), // new suffix + }) + if (isSubsequence) { rangeToReplace = new Range(position.lineNumber, position.column, position.lineNumber, Number.MAX_SAFE_INTEGER) } else { - // TODO redo the autocompletion - trimmedInsertText = '' // for now set the mismatched text to '' + + const lastMatchupIdx = trimmedInsertText.lastIndexOf(lastMatchingChar) + trimmedInsertText = trimmedInsertText.slice(0, lastMatchupIdx + 1) + const numCharsToReplace = oldSuffix.lastIndexOf(lastMatchingChar) + 1 + rangeToReplace = new Range(position.lineNumber, position.column, position.lineNumber, position.column + numCharsToReplace) + // console.log('show____', trimmedInsertText, rangeToReplace) } } @@ -504,12 +521,7 @@ const getAutocompletionMatchup = ({ prefix, autocompletion }: { prefix: string, } -// const x = [] -// const -// c[[]] -// asd[[]] = -// const [{{}}] -// + type CompletionOptions = { predictionType: AutocompletionPredictionType, shouldGenerate: boolean, @@ -519,7 +531,13 @@ type CompletionOptions = { } const getCompletionOptions = (prefixAndSuffix: PrefixAndSuffixInfo, relevantContext: string, justAcceptedAutocompletion: boolean): CompletionOptions => { - const { prefix, suffix, prefixToTheLeftOfCursor, suffixToTheRightOfCursor, suffixLines } = prefixAndSuffix + let { prefix, suffix, prefixToTheLeftOfCursor, suffixToTheRightOfCursor, suffixLines, prefixLines } = prefixAndSuffix + + // trim prefix and suffix to not be very large + suffixLines = suffix.split(_ln).slice(0, 25) + prefixLines = prefix.split(_ln).slice(-25) + prefix = prefixLines.join(_ln) + suffix = suffixLines.join(_ln) let completionOptions: CompletionOptions @@ -552,7 +570,7 @@ const getCompletionOptions = (prefixAndSuffix: PrefixAndSuffixInfo, relevantCont stopTokens: allLinebreakSymbols } } - // if suffix is 3 or less characters, attempt to complete the line ignorning it + // if suffix is 3 or fewer characters, attempt to complete the line ignorning it else if (removeAllWhitespace(suffixToTheRightOfCursor).length <= 3) { const suffixLinesIgnoringThisLine = suffixLines.slice(1) const suffixStringIgnoringThisLine = suffixLinesIgnoringThisLine.length === 0 ? '' : _ln + suffixLinesIgnoringThisLine.join(_ln) @@ -615,8 +633,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ token: CancellationToken, ): Promise { - console.log('START_0') - const testMode = false const docUriStr = model.uri.toString(); @@ -733,12 +749,11 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ // gather relevant context from the code around the user's selection and definitions - const relevantContext = await this._gatherRelevantContextForPosition( - model, - position, - 3, //recursion depth - 1 // number of lines to view in each recursion - ); + // const relevantSnippetsList = await this._contextGatheringService.readCachedSnippets(model, position, 3); + // const relevantSnippetsList = this._contextGatheringService.getCachedSnippets(); + // const relevantSnippets = relevantSnippetsList.map((text) => `${text}`).join('\n-------------------------------\n') + // console.log('@@---------------------\n' + relevantSnippets) + const relevantContext = '' const { shouldGenerate, predictionType, llmPrefix, llmSuffix, stopTokens } = getCompletionOptions(prefixAndSuffix, relevantContext, justAcceptedAutocompletion) @@ -766,44 +781,52 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ llmPromise: undefined, insertText: '', requestId: null, + _newlineCount: 0, } console.log('BB') - console.log(predictionType) + console.log('type', predictionType) // set parameters of `newAutocompletion` appropriately newAutocompletion.llmPromise = new Promise((resolve, reject) => { const requestId = this._llmMessageService.sendLLMMessage({ - type: 'ollamaFIM', + messagesType: 'FIMMessage', messages: { prefix: llmPrefix, suffix: llmSuffix, stopTokens: stopTokens, }, + useProviderFor: 'Autocomplete', logging: { loggingName: 'Autocomplete' }, - onText: async ({ fullText }) => { + onText: async ({ fullText, newText }) => { newAutocompletion.insertText = fullText - // if generation doesn't match the prefix for the first few tokens generated, reject it + // count newlines in newText + const numNewlines = newText.match(/\n|\r\n/g)?.length || 0 + newAutocompletion._newlineCount += numNewlines + + // if too many newlines, resolve up to last newline + if (newAutocompletion._newlineCount > 10) { + const lastNewlinePos = fullText.lastIndexOf('\n') + newAutocompletion.insertText = fullText.substring(0, lastNewlinePos) + resolve(newAutocompletion.insertText) + return + } + // if (!getAutocompletionMatchup({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) { - // reject('LLM response did not match user\'s text.') + // reject('LLM response did not match user\'s text.') // } }, onFinalMessage: ({ fullText }) => { - console.log('____res: ', JSON.stringify(newAutocompletion.insertText)) + // console.log('____res: ', JSON.stringify(newAutocompletion.insertText)) - // newAutocompletion.prefix = prefix - // newAutocompletion.suffix = suffix - // newAutocompletion.startTime = Date.now() newAutocompletion.endTime = Date.now() - // newAutocompletion.abortRef = { current: () => { } } newAutocompletion.status = 'finished' - // newAutocompletion.promise = undefined const [text, _] = extractCodeFromRegular({ text: fullText, recentlyAddedTextLen: 0 }) - newAutocompletion.insertText = joinSpaces(text) + newAutocompletion.insertText = processStartAndEndSpaces(text) // handle special case for predicting starting on the next line, add a newline character if (newAutocompletion.type === 'multi-line-start-on-next-line') { @@ -818,7 +841,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ newAutocompletion.status = 'error' reject(message) }, - useProviderFor: 'Autocomplete', }) newAutocompletion.requestId = requestId @@ -853,89 +875,12 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ } - // helper method to gather ~N lines above and below the user's current line, - // and recursively gather lines around any symbol definitions encountered. - private async _gatherRelevantContextForPosition( - model: ITextModel, - position: Position, - recursionDepth: number, - linesAround: number - ): Promise { - // We'll do a BFS-like approach: for each position or definition, gather lines around it, - // then attempt to find the definition of any symbols in that range, up to 'recursionDepth' times. - - // A set of "key" strings to avoid repeating the same location or line chunk - const visitedRanges = new Set(); - const collectedSnippets: string[] = []; - - // A queue of tasks, each being a tuple of: (model, position, depth) - const tasks: Array<{ model: ITextModel, position: Position, depth: number }> = []; - tasks.push({ model, position, depth: recursionDepth }); - - const getSnippetAroundLine = (model: ITextModel, lineNumber: number, linesAround: number): string => { - const startLine = Math.max(1, lineNumber - linesAround); - const endLine = Math.min(model.getLineCount(), lineNumber + linesAround); - const lines: string[] = []; - for (let i = startLine; i <= endLine; i++) { - lines.push(model.getLineContent(i)); - } - return lines.join('\n'); - }; - - while (tasks.length > 0) { - const { model: currentModel, position: currentPos, depth } = tasks.shift()!; - - if (depth < 0) { - continue; - } - - // Gather snippet around the current line - const snippet = getSnippetAroundLine(currentModel, currentPos.lineNumber, linesAround); - const snippetKey = `${currentModel.uri.toString()}:${currentPos.lineNumber}`; - if (!visitedRanges.has(snippetKey)) { - visitedRanges.add(snippetKey); - collectedSnippets.push(`-- Snippet around line ${currentPos.lineNumber} --\n${snippet}\n`); - } - - // Attempt to gather definitions for the symbol at this position - // We just pick all definition providers and see if any has a definition - const providers = this._langFeatureService.definitionProvider.ordered(currentModel); - for (const provider of providers) { - try { - const definitions = await provider.provideDefinition(currentModel, currentPos, CancellationToken.None); - if (!definitions) continue; - - // definitions can be a single LocationLink or an array - const defArray: LocationLink[] = Array.isArray(definitions) ? definitions : [definitions]; - for (const def of defArray) { - if (!def.uri) continue; - if (typeof def.range === 'undefined') continue; - const definitionModel = this._modelService.getModel(def.uri); - if (!definitionModel) continue; - - // We'll queue up a new task for that definition range - const defPos = new Position(def.range.startLineNumber, def.range.startColumn); - const defKey = `${def.uri.toString()}:${defPos.lineNumber}`; - if (!visitedRanges.has(defKey)) { - tasks.push({ model: definitionModel, position: defPos, depth: depth - 1 }); - } - } - } catch (err) { - // If a provider fails, ignore - } - } - } - - // Return the joined context - return collectedSnippets.join('\n'); - } - - constructor( @ILanguageFeaturesService private _langFeatureService: ILanguageFeaturesService, @ILLMMessageService private readonly _llmMessageService: ILLMMessageService, @IEditorService private readonly _editorService: IEditorService, @IModelService private readonly _modelService: IModelService, + // @IContextGatheringService private readonly _contextGatheringService: IContextGatheringService, ) { super() @@ -966,8 +911,10 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ // go through cached items and remove matching ones // autocompletion.prefix + autocompletion.insertedText ~== insertedText this._autocompletionsOfDocument[docUriStr].items.forEach((autocompletion: Autocompletion) => { - // const matchup = getAutocompletionMatchup({ prefix, autocompletion }) + + // we can do this more efficiently, I just didn't want to deal with all of the edge cases const matchup = removeAllWhitespace(prefix) === removeAllWhitespace(autocompletion.prefix + autocompletion.insertText) + if (matchup) { console.log('ACCEPT', autocompletion.id) this._lastCompletionAccept = Date.now() diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 468708b3..d0ca28b2 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -70,7 +70,7 @@ export type ThreadsState = { export type ThreadStreamState = { [threadId: string]: undefined | { - error?: { message: string, fullError: Error | null }; + error?: { message: string, fullError: Error | null, }; messageSoFar?: string; streamingToken?: string; } @@ -202,11 +202,12 @@ class ChatThreadService extends Disposable implements IChatThreadService { this._setStreamState(threadId, { error: undefined }) const llmCancelToken = this._llmMessageService.sendLLMMessage({ - type: 'sendLLMMessage', + messagesType: 'chatMessages', logging: { loggingName: 'Chat' }, + useProviderFor: 'Ctrl+L', messages: [ { role: 'system', content: chat_systemMessage }, - ...this.getCurrentThread().messages.map(m => ({ role: m.role, content: m.content || '(null)' })), + ...this.getCurrentThread().messages.map(m => ({ role: m.role, content: m.content || '(empty model output)' })), ], onText: ({ newText, fullText }) => { this._setStreamState(threadId, { messageSoFar: fullText }) @@ -217,7 +218,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { onError: (error) => { this.finishStreaming(threadId, this.streamState[threadId]?.messageSoFar ?? '', error) }, - useProviderFor: 'Ctrl+L', }) if (llmCancelToken === null) return diff --git a/src/vs/workbench/contrib/void/browser/contextGatheringService.ts b/src/vs/workbench/contrib/void/browser/contextGatheringService.ts new file mode 100644 index 00000000..c20e8fb8 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/contextGatheringService.ts @@ -0,0 +1,354 @@ +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { Position } from '../../../../editor/common/core/position.js'; +import { DocumentSymbol, SymbolKind } from '../../../../editor/common/languages.js'; +import { ITextModel } from '../../../../editor/common/model.js'; +import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { Range, IRange } from '../../../../editor/common/core/range.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { IModelService } from '../../../../editor/common/services/model.js'; +import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; +import { URI } from '../../../../base/common/uri.js'; + + +// make sure snippet logic works +// change logic for `visited` to intervals +// atomically set new snippets at end +// throttle cache setting + +interface IVisitedInterval { + uri: string; + startLine: number; + endLine: number; +} + +export interface IContextGatheringService { + readonly _serviceBrand: undefined; + updateCache(model: ITextModel, pos: Position): Promise; + getCachedSnippets(): string[]; +} + +export const IContextGatheringService = createDecorator('contextGatheringService'); + +class ContextGatheringService extends Disposable implements IContextGatheringService { + _serviceBrand: undefined; + private readonly _NUM_LINES = 3; + private readonly _MAX_SNIPPET_LINES = 7; // Reasonable size for context + // Cache holds the most recent list of snippets. + private _cache: string[] = []; + private _snippetIntervals: IVisitedInterval[] = []; + + constructor( + @ILanguageFeaturesService private readonly _langFeaturesService: ILanguageFeaturesService, + @IModelService private readonly _modelService: IModelService, + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService + ) { + super(); + this._modelService.getModels().forEach(model => this._subscribeToModel(model)); + this._register(this._modelService.onModelAdded(model => this._subscribeToModel(model))); + } + + private _subscribeToModel(model: ITextModel): void { + console.log("Subscribing to model:", model.uri.toString()); + this._register(model.onDidChangeContent(() => { + const editor = this._codeEditorService.getFocusedCodeEditor(); + if (editor && editor.getModel() === model) { + const pos = editor.getPosition(); + console.log("updateCache called at position:", pos); + if (pos) { + this.updateCache(model, pos); + } + } + })); + } + + public async updateCache(model: ITextModel, pos: Position): Promise { + const snippets = new Set(); + this._snippetIntervals = []; // Reset intervals for new cache update + + await this._gatherNearbySnippets(model, pos, this._NUM_LINES, 3, snippets, this._snippetIntervals); + await this._gatherParentSnippets(model, pos, this._NUM_LINES, 3, snippets, this._snippetIntervals); + + // Convert to array and filter overlapping snippets + this._cache = Array.from(snippets); + console.log("Cache updated:", this._cache); + } + + public getCachedSnippets(): string[] { + return this._cache; + } + + // Basic snippet extraction. + private _getSnippetForRange(model: ITextModel, range: IRange, numLines: number): string { + const startLine = Math.max(range.startLineNumber - numLines, 1); + const endLine = Math.min(range.endLineNumber + numLines, model.getLineCount()); + + // Enforce maximum snippet size + const totalLines = endLine - startLine + 1; + const adjustedStartLine = totalLines > this._MAX_SNIPPET_LINES + ? endLine - this._MAX_SNIPPET_LINES + 1 + : startLine; + + const snippetRange = new Range(adjustedStartLine, 1, endLine, model.getLineMaxColumn(endLine)); + return this._cleanSnippet(model.getValueInRange(snippetRange)); + } + + private _cleanSnippet(snippet: string): string { + return snippet + .split('\n') + // Remove empty lines and lines with only comments + .filter(line => { + const trimmed = line.trim(); + return trimmed && !/^\/\/+$/.test(trimmed); + }) + // Rejoin with newlines + .join('\n') + // Remove excess whitespace + .trim(); + } + + private _normalizeSnippet(snippet: string): string { + return snippet + // Remove multiple newlines + .replace(/\n{2,}/g, '\n') + // Remove trailing whitespace + .trim(); + } + + private _addSnippetIfNotOverlapping( + model: ITextModel, + range: IRange, + snippets: Set, + visited: IVisitedInterval[] + ): void { + const startLine = range.startLineNumber; + const endLine = range.endLineNumber; + const uri = model.uri.toString(); + + if (!this._isRangeVisited(uri, startLine, endLine, visited)) { + visited.push({ uri, startLine, endLine }); + const snippet = this._normalizeSnippet(this._getSnippetForRange(model, range, this._NUM_LINES)); + if (snippet.length > 0) { + snippets.add(snippet); + } + } + } + + private async _gatherNearbySnippets( + model: ITextModel, + pos: Position, + numLines: number, + depth: number, + snippets: Set, + visited: IVisitedInterval[] + ): Promise { + if (depth <= 0) return; + + const startLine = Math.max(pos.lineNumber - numLines, 1); + const endLine = Math.min(pos.lineNumber + numLines, model.getLineCount()); + const range = new Range(startLine, 1, endLine, model.getLineMaxColumn(endLine)); + + this._addSnippetIfNotOverlapping(model, range, snippets, visited); + + const symbols = await this._getSymbolsNearPosition(model, pos, numLines); + for (const sym of symbols) { + const defs = await this._getDefinitionSymbols(model, sym); + for (const def of defs) { + const defModel = this._modelService.getModel(def.uri); + if (defModel) { + const defPos = new Position(def.range.startLineNumber, def.range.startColumn); + this._addSnippetIfNotOverlapping(defModel, def.range, snippets, visited); + await this._gatherNearbySnippets(defModel, defPos, numLines, depth - 1, snippets, visited); + } + } + } + } + + private async _gatherParentSnippets( + model: ITextModel, + pos: Position, + numLines: number, + depth: number, + snippets: Set, + visited: IVisitedInterval[] + ): Promise { + if (depth <= 0) return; + + const container = await this._findContainerFunction(model, pos); + if (!container) return; + + const containerRange = container.kind === SymbolKind.Method ? container.selectionRange : container.range; + this._addSnippetIfNotOverlapping(model, containerRange, snippets, visited); + + const symbols = await this._getSymbolsNearRange(model, containerRange, numLines); + for (const sym of symbols) { + const defs = await this._getDefinitionSymbols(model, sym); + for (const def of defs) { + const defModel = this._modelService.getModel(def.uri); + if (defModel) { + const defPos = new Position(def.range.startLineNumber, def.range.startColumn); + this._addSnippetIfNotOverlapping(defModel, def.range, snippets, visited); + await this._gatherNearbySnippets(defModel, defPos, numLines, depth - 1, snippets, visited); + } + } + } + + const containerPos = new Position(containerRange.startLineNumber, containerRange.startColumn); + await this._gatherParentSnippets(model, containerPos, numLines, depth - 1, snippets, visited); + } + + private _isRangeVisited(uri: string, startLine: number, endLine: number, visited: IVisitedInterval[]): boolean { + return visited.some(interval => + interval.uri === uri && + !(endLine < interval.startLine || startLine > interval.endLine) + ); + } + + private async _getSymbolsNearPosition(model: ITextModel, pos: Position, numLines: number): Promise { + const startLine = Math.max(pos.lineNumber - numLines, 1); + const endLine = Math.min(pos.lineNumber + numLines, model.getLineCount()); + const range = new Range(startLine, 1, endLine, model.getLineMaxColumn(endLine)); + return this._getSymbolsInRange(model, range); + } + + private async _getSymbolsNearRange(model: ITextModel, range: IRange, numLines: number): Promise { + const centerLine = Math.floor((range.startLineNumber + range.endLineNumber) / 2); + const startLine = Math.max(centerLine - numLines, 1); + const endLine = Math.min(centerLine + numLines, model.getLineCount()); + const searchRange = new Range(startLine, 1, endLine, model.getLineMaxColumn(endLine)); + return this._getSymbolsInRange(model, searchRange); + } + + private async _getSymbolsInRange(model: ITextModel, range: IRange): Promise { + const symbols: DocumentSymbol[] = []; + const providers = this._langFeaturesService.documentSymbolProvider.ordered(model); + for (const provider of providers) { + try { + const result = await provider.provideDocumentSymbols(model, CancellationToken.None); + if (result) { + const flat = this._flattenSymbols(result); + const intersecting = flat.filter(sym => this._rangesIntersect(sym.range, range)); + symbols.push(...intersecting); + } + } catch (e) { + console.warn("Symbol provider error:", e); + } + } + // Also check reference providers. + const refProviders = this._langFeaturesService.referenceProvider.ordered(model); + for (let line = range.startLineNumber; line <= range.endLineNumber; line++) { + const content = model.getLineContent(line); + const words = content.match(/[a-zA-Z_]\w*/g) || []; + for (const word of words) { + const startColumn = content.indexOf(word) + 1; + const pos = new Position(line, startColumn); + if (!this._positionInRange(pos, range)) continue; + for (const provider of refProviders) { + try { + const refs = await provider.provideReferences(model, pos, { includeDeclaration: true }, CancellationToken.None); + if (refs) { + const filtered = refs.filter(ref => this._rangesIntersect(ref.range, range)); + for (const ref of filtered) { + symbols.push({ + name: word, + detail: '', + kind: SymbolKind.Variable, + range: ref.range, + selectionRange: ref.range, + children: [], + tags: [] + }); + } + } + } catch (e) { + console.warn("Reference provider error:", e); + } + } + } + } + return symbols; + } + + private _flattenSymbols(symbols: DocumentSymbol[]): DocumentSymbol[] { + const flat: DocumentSymbol[] = []; + for (const sym of symbols) { + flat.push(sym); + if (sym.children && sym.children.length > 0) { + flat.push(...this._flattenSymbols(sym.children)); + } + } + return flat; + } + + private _rangesIntersect(a: IRange, b: IRange): boolean { + return !( + a.endLineNumber < b.startLineNumber || + a.startLineNumber > b.endLineNumber || + (a.endLineNumber === b.startLineNumber && a.endColumn < b.startColumn) || + (a.startLineNumber === b.endLineNumber && a.endColumn > b.endColumn) + ); + } + + private _positionInRange(pos: Position, range: IRange): boolean { + return pos.lineNumber >= range.startLineNumber && + pos.lineNumber <= range.endLineNumber && + (pos.lineNumber !== range.startLineNumber || pos.column >= range.startColumn) && + (pos.lineNumber !== range.endLineNumber || pos.column <= range.endColumn); + } + + // Get definition symbols for a given symbol. + private async _getDefinitionSymbols(model: ITextModel, symbol: DocumentSymbol): Promise<(DocumentSymbol & { uri: URI })[]> { + const pos = new Position(symbol.range.startLineNumber, symbol.range.startColumn); + const providers = this._langFeaturesService.definitionProvider.ordered(model); + const defs: (DocumentSymbol & { uri: URI })[] = []; + for (const provider of providers) { + try { + const res = await provider.provideDefinition(model, pos, CancellationToken.None); + if (res) { + const links = Array.isArray(res) ? res : [res]; + defs.push(...links.map(link => ({ + name: symbol.name, + detail: symbol.detail, + kind: symbol.kind, + range: link.range, + selectionRange: link.range, + children: [], + tags: symbol.tags || [], + uri: link.uri // Now keeping it as URI instead of converting to string + }))); + } + } catch (e) { + console.warn("Definition provider error:", e); + } + } + return defs; + } + + private async _findContainerFunction(model: ITextModel, pos: Position): Promise { + const searchRange = new Range( + Math.max(pos.lineNumber - 1, 1), 1, + Math.min(pos.lineNumber + 1, model.getLineCount()), + model.getLineMaxColumn(pos.lineNumber) + ); + const symbols = await this._getSymbolsInRange(model, searchRange); + const funcs = symbols.filter(s => + (s.kind === SymbolKind.Function || s.kind === SymbolKind.Method) && + this._positionInRange(pos, s.range) + ); + if (!funcs.length) return null; + return funcs.reduce((innermost, current) => { + if (!innermost) return current; + const moreInner = + (current.range.startLineNumber > innermost.range.startLineNumber || + (current.range.startLineNumber === innermost.range.startLineNumber && + current.range.startColumn > innermost.range.startColumn)) && + (current.range.endLineNumber < innermost.range.endLineNumber || + (current.range.endLineNumber === innermost.range.endLineNumber && + current.range.endColumn < innermost.range.endColumn)); + return moreInner ? current : innermost; + }, null as DocumentSymbol | null); + } +} + +registerSingleton(IContextGatheringService, ContextGatheringService, InstantiationType.Eager); diff --git a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts index b7d43a6d..d4d7d065 100644 --- a/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts +++ b/src/vs/workbench/contrib/void/browser/inlineDiffsService.ts @@ -30,7 +30,7 @@ import { ILLMMessageService } from '../../../../platform/void/common/llmMessageS import { mountCtrlK } from '../browser/react/out/quick-edit-tsx/index.js' import { QuickEditPropsType } from './quickEditActions.js'; -import { errorDetails, LLMMessage } from '../../../../platform/void/common/llmMessageTypes.js'; +import { errorDetails, LLMChatMessage } from '../../../../platform/void/common/llmMessageTypes.js'; import { IModelContentChangedEvent } from '../../../../editor/common/textModelEvents.js'; import { extractCodeFromFIM, extractCodeFromRegular } from './helpers/extractCodeFromResult.js'; import { IMetricsService } from '../../../../platform/void/common/metricsService.js'; @@ -102,13 +102,13 @@ const getLeadingWhitespacePx = (editor: ICodeEditor, startLine: number): number // similar to ServiceLLM export type StartApplyingOpts = { - featureName: 'Ctrl+K'; + from: 'QuickEdit'; diffareaid: number; // id of the CtrlK area (contains text selection) } | { - featureName: 'Ctrl+L'; + from: 'Chat'; applyStr: string; } | { - featureName: 'Autocomplete'; + from: 'Autocomplete'; range: IRange; userMessage: string; } @@ -1209,13 +1209,13 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { private _initializeStartApplying(opts: StartApplyingOpts): DiffZone | undefined { - const { featureName } = opts + const { from } = opts let startLine: number let endLine: number let uri: URI - if (featureName === 'Ctrl+L') { + if (from === 'Chat') { const uri_ = this._getActiveEditorURI() if (!uri_) return @@ -1231,7 +1231,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { endLine = numLines } - else if (featureName === 'Ctrl+K') { + else if (from === 'QuickEdit') { const { diffareaid } = opts const ctrlKZone = this.diffAreaOfId[diffareaid] if (ctrlKZone.type !== 'CtrlKZone') return @@ -1242,7 +1242,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { endLine = endLine_ } else { - throw new Error(`Void: diff.type not recognized on: ${featureName}`) + throw new Error(`Void: diff.type not recognized on: ${from}`) } const currentFileStr = this._readURI(uri) @@ -1278,7 +1278,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) this._onDidAddOrDeleteDiffZones.fire({ uri }) - if (featureName === 'Ctrl+K') { + if (from === 'QuickEdit') { const { diffareaid } = opts const ctrlKZone = this.diffAreaOfId[diffareaid] if (ctrlKZone.type !== 'CtrlKZone') return @@ -1287,16 +1287,16 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { } // now handle messages - let messages: LLMMessage[] + let messages: LLMChatMessage[] - if (featureName === 'Ctrl+L') { + if (from === 'Chat') { const userContent = fastApply_userMessage({ originalCode, applyStr: opts.applyStr, uri }) messages = [ { role: 'system', content: fastApply_systemMessage, }, { role: 'user', content: userContent, } ] } - else if (featureName === 'Ctrl+K') { + else if (from === 'QuickEdit') { const { diffareaid } = opts const ctrlKZone = this.diffAreaOfId[diffareaid] if (ctrlKZone.type !== 'CtrlKZone') return @@ -1323,14 +1323,14 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { ] // } } - else { throw new Error(`featureName ${featureName} is invalid`) } + else { throw new Error(`featureName ${from} is invalid`) } const onDone = (hadError: boolean) => { diffZone._streamState = { isStreaming: false, } this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid }) - if (featureName === 'Ctrl+K') { + if (from === 'QuickEdit') { const ctrlKZone = this.diffAreaOfId[opts.diffareaid] as CtrlKZone ctrlKZone._linkedStreamingDiffZone = null @@ -1350,11 +1350,11 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { const extractText = (fullText: string, recentlyAddedTextLen: number) => { - if (featureName === 'Ctrl+K') { + if (from === 'QuickEdit') { if (isOllamaFIM) return fullText return extractCodeFromFIM({ text: fullText, recentlyAddedTextLen, midTag: modelFimTags.midTag }) } - else if (featureName === 'Ctrl+L') { + else if (from === 'Chat') { return extractCodeFromRegular({ text: fullText, recentlyAddedTextLen }) } throw 1 @@ -1367,9 +1367,9 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService { let prevIgnoredSuffix = '' streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ - type: 'sendLLMMessage', - useProviderFor: featureName, - logging: { loggingName: `startApplying - ${featureName}` }, + messagesType: 'chatMessages', + useProviderFor: opts.from === 'Chat' ? 'FastApply' : 'Ctrl+K', + logging: { loggingName: `startApplying - ${from}` }, messages, onText: ({ newText: newText_ }) => { diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index 6d7fc46b..28cd2539 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -45,7 +45,7 @@ const CodeButtonsOnHover = ({ text }: { text: string }) => { const onApply = useCallback(() => { inlineDiffService.startApplying({ - featureName: 'Ctrl+L', + from: 'Chat', applyStr: text, }) metricsService.capture('Apply Code', { length: text.length }) // capture the length only diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx index da2a0d70..7edf1e5b 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx @@ -7,11 +7,12 @@ import React, { FormEvent, useCallback, useEffect, useRef, useState } from 'reac import { useSettingsState, useSidebarState, useChatThreadsState, useQuickEditState, useAccessor } from '../util/services.js'; import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js'; import { QuickEditPropsType } from '../../../quickEditActions.js'; -import { ButtonStop, ButtonSubmit, IconX } from '../sidebar-tsx/SidebarChat.js'; +import { ButtonStop, ButtonSubmit, IconX, VoidChatArea } from '../sidebar-tsx/SidebarChat.js'; import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js'; import { VOID_CTRL_K_ACTION_ID } from '../../../actionIDs.js'; import { useRefState } from '../util/helpers.js'; import { useScrollbarStyles } from '../util/useScrollbarStyles.js'; +import { isFeatureNameDisabled } from '../../../../../../../platform/void/common/voidSettingsTypes.js'; export const QuickEditChat = ({ diffareaid, @@ -42,21 +43,22 @@ export const QuickEditChat = ({ }, [onChangeHeight]); + const settingsState = useSettingsState() + // state of current message const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!(initText ?? '')) // the user's instructions - const isDisabled = instructionsAreEmpty + const isDisabled = instructionsAreEmpty || !!isFeatureNameDisabled('Ctrl+K', settingsState) const [currStreamingDiffZoneRef, setCurrentlyStreamingDiffZone] = useRefState(initStreamingDiffZoneId) const isStreaming = currStreamingDiffZoneRef.current !== null - const onSubmit = useCallback((e: FormEvent) => { + const onSubmit = useCallback(() => { if (isDisabled) return if (currStreamingDiffZoneRef.current !== null) return textAreaFnsRef.current?.disable() - const instructions = textAreaRef.current?.value ?? '' const id = inlineDiffsService.startApplying({ - featureName: 'Ctrl+K', + from: 'QuickEdit', diffareaid: diffareaid, }) setCurrentlyStreamingDiffZone(id ?? null) @@ -79,110 +81,45 @@ export const QuickEditChat = ({ const keybindingString = accessor.get('IKeybindingService').lookupKeybinding(VOID_CTRL_K_ACTION_ID)?.getLabel() + const chatAreaRef = useRef(null) return
-
{ - textAreaRef.current?.focus() - }} + { textAreaRef.current?.focus() }} > - - {/* // this div is used to position the input box properly */} -
-
- - {/* input */} -
- {/* text input */} - { - textAreaRef.current = r - textAreaRef_(r) - - // if presses the esc key, X - r?.addEventListener('keydown', (e) => { - if (e.key === 'Escape') - onX() - }) - - }, [textAreaRef_, onX])} - - fnsRef={textAreaFnsRef} - - placeholder={`Enter instructions...`} - // ${keybindingString} to select. - - onChangeText={useCallback((newStr: string) => { - setInstructionsAreEmpty(!newStr) - onChangeText_(newStr) - }, [onChangeText_])} - - onKeyDown={(e) => { - if (e.key === 'Enter' && !e.shiftKey) { - onSubmit(e) - return - } - }} - - multiline={true} - /> -
- - {/* X button */} -
- -
-
- - - {/* bottom row */} -
- {/* submit options */} -
- -
- - {/* submit / stop button */} - {isStreaming ? - // stop button - - : - // submit button (up arrow) - + { + textAreaRef.current = r + textAreaRef_(r) + r?.addEventListener('keydown', (e) => { + if (e.key === 'Escape') + onX() + }) + }, [textAreaRef_, onX])} + fnsRef={textAreaFnsRef} + placeholder="Enter instructions..." + onChangeText={useCallback((newStr: string) => { + setInstructionsAreEmpty(!newStr) + onChangeText_(newStr) + }, [onChangeText_])} + onKeyDown={(e) => { + if (e.key === 'Enter' && !e.shiftKey) { + onSubmit() + return } -
-
- - - + }} + multiline={true} + /> +
diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx index 689d55c1..425ce3c9 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx @@ -6,6 +6,7 @@ import React, { useEffect, useState } from 'react'; import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react'; import { errorDetails } from '../../../../../../../platform/void/common/llmMessageTypes.js'; +import { useSettingsState } from '../util/services.js'; export const ErrorDisplay = ({ @@ -22,9 +23,9 @@ export const ErrorDisplay = ({ const [isExpanded, setIsExpanded] = useState(false); const details = errorDetails(fullError) + const isExpandable = !!details - const message = message_ === 'TypeError: fetch failed' ? 'TypeError: fetch failed. This likely means you specified the wrong endpoint in Void Settings.' : message_ + '' - + const message = message_ + '' return (
@@ -45,7 +46,7 @@ export const ErrorDisplay = ({
- {details && ( + {isExpandable && (