mirror of
https://github.com/voideditor/void
synced 2026-05-23 09:28:23 +00:00
Merge pull request #273 from voideditor/model-selection
UI improvements
This commit is contained in:
commit
5fa21be9d2
30 changed files with 1275 additions and 697 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -22,4 +22,5 @@ product.overrides.json
|
|||
*.snap.actual
|
||||
.vscode-test
|
||||
.tmp/
|
||||
.tmp2/
|
||||
.tool-versions
|
||||
|
|
|
|||
7
remote/package-lock.json
generated
7
remote/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<OnFinalMessage>[0]
|
|||
export type EventLLMMessageOnErrorParams = Parameters<OnError>[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
|
||||
|
||||
|
|
|
|||
|
|
@ -44,8 +44,8 @@ export type RefreshModelStateOfProvider = Record<RefreshableProviderName, Refres
|
|||
|
||||
|
||||
const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvider[k])[] } = {
|
||||
ollama: ['_enabled', 'endpoint'],
|
||||
// openAICompatible: ['_enabled', 'endpoint', 'apiKey'],
|
||||
ollama: ['_didFillInProviderSettings', 'endpoint'],
|
||||
// openAICompatible: ['_didFillInProviderSettings', 'endpoint', 'apiKey'],
|
||||
}
|
||||
const REFRESH_INTERVAL = 5_000
|
||||
// const COOLDOWN_TIMEOUT = 300
|
||||
|
|
@ -95,7 +95,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
|||
|
||||
for (const providerName of refreshableProviderNames) {
|
||||
|
||||
// const { _enabled: enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
|
||||
// const { '_didFillInProviderSettings': enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
|
||||
this.startRefreshingModels(providerName, autoOptions)
|
||||
|
||||
// every time providerName.enabled changes, refresh models too, like a useEffect
|
||||
|
|
@ -175,7 +175,7 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
|
|||
{ enableProviderOnSuccess: options.enableProviderOnSuccess, hideRefresh: options.doNotFire }
|
||||
)
|
||||
|
||||
if (options.enableProviderOnSuccess) this.voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
|
||||
if (options.enableProviderOnSuccess) this.voidSettingsService.setSettingOfProvider(providerName, '_didFillInProviderSettings', true)
|
||||
|
||||
this._setRefreshState(providerName, 'finished', options)
|
||||
autoPoll()
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import { registerSingleton, InstantiationType } from '../../instantiation/common
|
|||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js';
|
||||
import { IMetricsService } from './metricsService.js';
|
||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings } from './voidSettingsTypes.js';
|
||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultModelNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, displayInfoOfProviderName, defaultProviderSettings } from './voidSettingsTypes.js';
|
||||
|
||||
|
||||
const STORAGE_KEY = 'void.settingsServiceStorage'
|
||||
|
|
@ -42,8 +42,8 @@ export type VoidSettingsState = {
|
|||
readonly _modelOptions: ModelOption[] // computed based on the two above items
|
||||
}
|
||||
|
||||
type RealVoidSettings = Exclude<keyof VoidSettingsState, '_modelOptions'>
|
||||
type EventProp<T extends RealVoidSettings = RealVoidSettings> = T extends 'globalSettings' ? [T, keyof VoidSettingsState[T]] : T | 'all'
|
||||
// type RealVoidSettings = Exclude<keyof VoidSettingsState, '_modelOptions'>
|
||||
// type EventProp<T extends RealVoidSettings = RealVoidSettings> = 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<void>;
|
||||
|
||||
onDidChangeState: Event<EventProp>;
|
||||
onDidChangeState: Event<void>;
|
||||
|
||||
setSettingOfProvider: SetSettingOfProviderFn;
|
||||
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn;
|
||||
|
|
@ -64,26 +64,76 @@ export interface IVoidSettingsService {
|
|||
}
|
||||
|
||||
|
||||
let _computeModelOptions = (settingsOfProvider: SettingsOfProvider) => {
|
||||
let modelOptions: ModelOption[] = []
|
||||
|
||||
const _updatedValidatedState = (state: Omit<VoidSettingsState, '_modelOptions'>) => {
|
||||
|
||||
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<IVoidSettingsService>('VoidS
|
|||
class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private readonly _onDidChangeState = new Emitter<EventProp>();
|
||||
readonly onDidChangeState: Event<EventProp> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
|
||||
private readonly _onDidChangeState = new Emitter<void>();
|
||||
readonly onDidChangeState: Event<void> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
|
||||
|
||||
state: VoidSettingsState;
|
||||
waitForInitState: Promise<void> // 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<VoidSettingsState> {
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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<providerName extends ProviderName> = CustomProviderSettings<providerName> & CommonProviderSettings
|
||||
export type SettingsAtProvider<providerName extends ProviderName> = CustomProviderSettings<providerName> & CommonProviderSettings
|
||||
|
||||
// part of state
|
||||
export type SettingsOfProvider = {
|
||||
[providerName in ProviderName]: SettingsForProvider<providerName>
|
||||
[providerName in ProviderName]: SettingsAtProvider<providerName>
|
||||
}
|
||||
|
||||
|
||||
export type SettingName = keyof SettingsForProvider<ProviderName>
|
||||
export type SettingName = keyof SettingsAtProvider<ProviderName>
|
||||
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
'<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 '<General>';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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 = ''
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<OllamaModelResponse> = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => {
|
||||
|
|
@ -38,7 +38,7 @@ export const ollamaList: _InternalModelListFnType<OllamaModelResponse> = 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
|
||||
|
|
|
|||
|
|
@ -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<Model> = 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<Parameters<_InternalSendLLMChatMessageFnType>[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)
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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<ExplorerItem, Fu
|
|||
|
||||
renderTemplate(container: HTMLElement): IFileTemplateData {
|
||||
const templateDisposables = new DisposableStore();
|
||||
|
||||
// Void added this
|
||||
// // Create void buttons container
|
||||
// const voidButtonsContainer = DOM.append(container, DOM.$('div'));
|
||||
// voidButtonsContainer.style.position = 'absolute'
|
||||
// voidButtonsContainer.style.top = '0'
|
||||
// voidButtonsContainer.style.right = '0'
|
||||
// // const voidButtons = DOM.append(voidButtonsContainer, DOM.$('span'));
|
||||
// // voidButtons.textContent = 'voidbuttons'
|
||||
// // voidButtons.addEventListener('click', () => {
|
||||
// // 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<ExplorerItem, Fu
|
|||
contr.setResource(templateData.currentContext?.resource);
|
||||
}));
|
||||
|
||||
|
||||
// const templateData: IFileTemplateData = { templateDisposables, elementDisposables: templateDisposables.add(new DisposableStore()), label, voidLabels, container, contribs };
|
||||
const templateData: IFileTemplateData = { templateDisposables, elementDisposables: templateDisposables.add(new DisposableStore()), label, container, contribs };
|
||||
|
||||
return templateData;
|
||||
}
|
||||
|
||||
|
||||
// Void cares about this function, this is where elements in the tree are rendered
|
||||
renderElement(node: ITreeNode<ExplorerItem, FuzzyScore>, index: number, templateData: IFileTemplateData): void {
|
||||
const stat = node.element;
|
||||
templateData.currentContext = stat;
|
||||
|
|
@ -382,8 +398,7 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
|
|||
templateData.label.element.style.display = 'flex';
|
||||
this.renderStat(stat, stat.name, undefined, node.filterData, templateData);
|
||||
}
|
||||
|
||||
// Input Box
|
||||
// Input Box (Void - shown only if currently editing - this is the box that appears when user edits the name of the file)
|
||||
else {
|
||||
templateData.label.element.style.display = 'none';
|
||||
templateData.contribs.forEach(c => c.setResource(undefined));
|
||||
|
|
@ -477,6 +492,13 @@ export class FilesRenderer implements ICompressibleTreeRenderer<ExplorerItem, Fu
|
|||
separator: this.labelService.getSeparator(stat.resource.scheme, stat.resource.authority),
|
||||
domId
|
||||
});
|
||||
|
||||
// templateData.voidLabels.setResource({ resource: undefined, name: 'hi', }, {
|
||||
// hideIcon: true,
|
||||
// extraClasses: realignNestedChildren ? [...extraClasses, 'align-nest-icon-with-parent-icon'] : extraClasses,
|
||||
// forceLabel: true,
|
||||
// });
|
||||
|
||||
}
|
||||
|
||||
private renderInputBox(container: HTMLElement, stat: ExplorerItem, editableData: IEditableData): IDisposable {
|
||||
|
|
|
|||
|
|
@ -95,7 +95,12 @@ suite('Files - ExplorerView', () => {
|
|||
label: <any>{
|
||||
container: label,
|
||||
onDidRender: emitter.event
|
||||
}
|
||||
},
|
||||
// voidLabels: <any>{
|
||||
// container: label,
|
||||
// onDidRender: emitter.event
|
||||
// },
|
||||
|
||||
}, 1, false);
|
||||
|
||||
ds.add(navigationController);
|
||||
|
|
|
|||
|
|
@ -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<string> | 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<InlineCompletion[]> {
|
||||
|
||||
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<string> {
|
||||
// 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<string>();
|
||||
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()
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
354
src/vs/workbench/contrib/void/browser/contextGatheringService.ts
Normal file
354
src/vs/workbench/contrib/void/browser/contextGatheringService.ts
Normal file
|
|
@ -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<void>;
|
||||
getCachedSnippets(): string[];
|
||||
}
|
||||
|
||||
export const IContextGatheringService = createDecorator<IContextGatheringService>('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<void> {
|
||||
const snippets = new Set<string>();
|
||||
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<string>,
|
||||
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<string>,
|
||||
visited: IVisitedInterval[]
|
||||
): Promise<void> {
|
||||
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<string>,
|
||||
visited: IVisitedInterval[]
|
||||
): Promise<void> {
|
||||
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<DocumentSymbol[]> {
|
||||
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<DocumentSymbol[]> {
|
||||
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<DocumentSymbol[]> {
|
||||
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<DocumentSymbol | null> {
|
||||
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);
|
||||
|
|
@ -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_ }) => {
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<number | null>(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<HTMLDivElement | null>(null)
|
||||
return <div ref={sizerRef} style={{ maxWidth: 450 }} className={`py-2 w-full`}>
|
||||
<form
|
||||
// copied from SidebarChat.tsx
|
||||
className={`
|
||||
flex flex-col gap-2 p-2 relative input text-left shrink-0
|
||||
transition-all duration-200
|
||||
rounded-md
|
||||
bg-vscode-input-bg
|
||||
border border-void-border-3 focus-within:border-void-border-1 hover:border-void-border-1
|
||||
`}
|
||||
onClick={(e) => {
|
||||
textAreaRef.current?.focus()
|
||||
}}
|
||||
<VoidChatArea
|
||||
divRef={chatAreaRef}
|
||||
onSubmit={onSubmit}
|
||||
onAbort={onInterrupt}
|
||||
onClose={onX}
|
||||
isStreaming={isStreaming}
|
||||
isDisabled={isDisabled}
|
||||
featureName="Ctrl+K"
|
||||
className="py-2 w-full"
|
||||
onClickAnywhere={() => { textAreaRef.current?.focus() }}
|
||||
>
|
||||
|
||||
{/* // this div is used to position the input box properly */}
|
||||
<div
|
||||
className={`w-full z-[999] relative`}
|
||||
>
|
||||
<div className='flex flex-row items-center justify-between items-end gap-1'>
|
||||
|
||||
{/* input */}
|
||||
<div // copied from SidebarChat.tsx
|
||||
className={`w-full`}
|
||||
>
|
||||
{/* text input */}
|
||||
<VoidInputBox2
|
||||
className='px-1'
|
||||
initValue={initText}
|
||||
|
||||
ref={useCallback((r: HTMLTextAreaElement | null) => {
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* X button */}
|
||||
<div className='absolute -top-1 -right-1 cursor-pointer z-1'>
|
||||
<IconX
|
||||
size={12}
|
||||
className="stroke-[2] opacity-80 text-void-fg-3 hover:brightness-95"
|
||||
onClick={onX}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* bottom row */}
|
||||
<div
|
||||
className='flex flex-row justify-between items-end gap-1'
|
||||
>
|
||||
{/* submit options */}
|
||||
<div className='max-w-[150px]
|
||||
@@[&_select]:!void-border-none
|
||||
@@[&_select]:!void-outline-none'
|
||||
>
|
||||
<ModelDropdown featureName='Ctrl+K' />
|
||||
</div>
|
||||
|
||||
{/* submit / stop button */}
|
||||
{isStreaming ?
|
||||
// stop button
|
||||
<ButtonStop
|
||||
onClick={onInterrupt}
|
||||
/>
|
||||
:
|
||||
// submit button (up arrow)
|
||||
<ButtonSubmit
|
||||
onClick={onSubmit}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
<VoidInputBox2
|
||||
className='px-1'
|
||||
initValue={initText}
|
||||
ref={useCallback((r: HTMLTextAreaElement | null) => {
|
||||
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
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</form>
|
||||
}}
|
||||
multiline={true}
|
||||
/>
|
||||
</VoidChatArea>
|
||||
</div>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className={`rounded-lg border border-red-200 bg-red-50 p-4 overflow-auto`}>
|
||||
|
|
@ -45,7 +46,7 @@ export const ErrorDisplay = ({
|
|||
</div>
|
||||
|
||||
<div className='flex gap-2'>
|
||||
{details && (
|
||||
{isExpandable && (
|
||||
<button className='text-red-600 hover:text-red-800 p-1 rounded'
|
||||
onClick={() => setIsExpanded(!isExpanded)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@
|
|||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { ButtonHTMLAttributes, FormEvent, FormHTMLAttributes, Fragment, KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
|
||||
import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState } from '../util/services.js';
|
||||
import { useAccessor, useSidebarState, useChatThreadsState, useChatThreadsStreamState, useUriState, useSettingsState } from '../util/services.js';
|
||||
import { ChatMessage, StagingSelectionItem } from '../../../chatThreadService.js';
|
||||
|
||||
import { BlockCode } from '../markdown/BlockCode.js';
|
||||
|
|
@ -15,12 +15,15 @@ import { URI } from '../../../../../../../base/common/uri.js';
|
|||
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
|
||||
import { ErrorDisplay } from './ErrorDisplay.js';
|
||||
import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js';
|
||||
import { ModelDropdown, WarningBox } from '../void-settings-tsx/ModelDropdown.js';
|
||||
import { ModelDropdown, } from '../void-settings-tsx/ModelDropdown.js';
|
||||
import { SidebarThreadSelector } from './SidebarThreadSelector.js';
|
||||
import { useScrollbarStyles } from '../util/useScrollbarStyles.js';
|
||||
import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
|
||||
import { filenameToVscodeLanguage } from '../../../helpers/detectLanguage.js';
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
|
||||
import { Pencil } from 'lucide-react';
|
||||
import { FeatureName, isFeatureNameDisabled } from '../../../../../../../platform/void/common/voidSettingsTypes.js';
|
||||
import { WarningBox } from '../void-settings-tsx/WarningBox.js';
|
||||
|
||||
|
||||
export const IconX = ({ size, className = '', ...props }: { size: number, className?: string } & React.SVGProps<SVGSVGElement>) => {
|
||||
|
|
@ -132,6 +135,109 @@ export const IconLoading = ({ className = '' }: { className?: string }) => {
|
|||
|
||||
}
|
||||
|
||||
|
||||
interface VoidChatAreaProps {
|
||||
// Required
|
||||
children: React.ReactNode; // This will be the input component
|
||||
|
||||
// Form controls
|
||||
onSubmit: () => void;
|
||||
onAbort: () => void;
|
||||
isStreaming: boolean;
|
||||
isDisabled?: boolean;
|
||||
divRef: React.RefObject<HTMLDivElement>;
|
||||
|
||||
// UI customization
|
||||
featureName: FeatureName;
|
||||
className?: string;
|
||||
showModelDropdown?: boolean;
|
||||
showSelections?: boolean;
|
||||
selections?: any[];
|
||||
onSelectionsChange?: (selections: any[]) => void;
|
||||
|
||||
onClickAnywhere?: () => void;
|
||||
// Optional close button
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const VoidChatArea: React.FC<VoidChatAreaProps> = ({
|
||||
children,
|
||||
onSubmit,
|
||||
onAbort,
|
||||
onClose,
|
||||
onClickAnywhere,
|
||||
divRef,
|
||||
isStreaming = false,
|
||||
isDisabled = false,
|
||||
className = '',
|
||||
showModelDropdown = true,
|
||||
featureName,
|
||||
showSelections = false,
|
||||
selections = [],
|
||||
onSelectionsChange,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
ref={divRef}
|
||||
className={`
|
||||
flex flex-col gap-1 p-2 relative input text-left shrink-0
|
||||
transition-all duration-200
|
||||
rounded-md
|
||||
bg-vscode-input-bg
|
||||
border border-void-border-3 focus-within:border-void-border-1 hover:border-void-border-1
|
||||
${className}
|
||||
`}
|
||||
onClick={(e) => {
|
||||
onClickAnywhere?.()
|
||||
}}
|
||||
>
|
||||
{/* Selections section */}
|
||||
{showSelections && onSelectionsChange && (
|
||||
<SelectedFiles
|
||||
type='staging'
|
||||
selections={selections}
|
||||
setSelections={onSelectionsChange}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Input section */}
|
||||
<div className="relative w-full">
|
||||
{children}
|
||||
|
||||
{/* Close button (X) if onClose is provided */}
|
||||
{onClose && (
|
||||
<div className='absolute -top-1 -right-1 cursor-pointer z-1'>
|
||||
<IconX
|
||||
size={12}
|
||||
className="stroke-[2] opacity-80 text-void-fg-3 hover:brightness-95"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Bottom row */}
|
||||
<div className='flex flex-row justify-between items-end gap-1'>
|
||||
{showModelDropdown && (
|
||||
<div className='max-w-[150px] @@[&_select]:!void-border-none @@[&_select]:!void-outline-none flex-grow'
|
||||
onClick={(e) => { e.preventDefault(); e.stopPropagation() }}>
|
||||
<ModelDropdown featureName={featureName} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isStreaming ? (
|
||||
<ButtonStop onClick={onAbort} />
|
||||
) : (
|
||||
<ButtonSubmit
|
||||
onClick={onSubmit}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const useResizeObserver = () => {
|
||||
const ref = useRef(null);
|
||||
const [dimensions, setDimensions] = useState({ height: 0, width: 0 });
|
||||
|
|
@ -428,89 +534,89 @@ export const SelectedFiles = (
|
|||
}
|
||||
|
||||
|
||||
|
||||
const ChatBubble_ = ({ isEditMode, isLoading, children, role }: { role: ChatMessage['role'], children: React.ReactNode, isLoading: boolean, isEditMode: boolean }) => {
|
||||
|
||||
return <div
|
||||
// align chatbubble accoridng to role
|
||||
className={`
|
||||
relative
|
||||
${isEditMode ? 'px-2 w-full max-w-full'
|
||||
: role === 'user' ? `px-2 self-end w-fit max-w-full whitespace-pre-wrap` // user words should be pre
|
||||
: role === 'assistant' ? `px-2 self-start w-full max-w-full` : ''
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div
|
||||
// style chatbubble according to role
|
||||
className={`
|
||||
text-left rounded-lg
|
||||
overflow-x-auto max-w-full
|
||||
${role === 'user' ? 'p-2 bg-void-bg-1 text-void-fg-1' : 'px-2'}
|
||||
`}
|
||||
>
|
||||
{children}
|
||||
{isLoading && <IconLoading className='opacity-50 text-sm' />}
|
||||
</div>
|
||||
|
||||
{/* edit button */}
|
||||
{/* {role === 'user' &&
|
||||
<Pencil
|
||||
size={16}
|
||||
className={`
|
||||
absolute top-0 right-2
|
||||
translate-x-0 -translate-y-0
|
||||
cursor-pointer z-1
|
||||
`}
|
||||
onClick={() => { setIsEditMode(v => !v); }}
|
||||
/>
|
||||
} */}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
type ChatBubbleMode = 'display' | 'edit'
|
||||
const ChatBubble = ({ chatMessage, isLoading }: { chatMessage: ChatMessage, isLoading?: boolean, }) => {
|
||||
|
||||
const role = chatMessage.role
|
||||
|
||||
// edit mode state
|
||||
const [isEditMode, setIsEditMode] = useState(false)
|
||||
|
||||
const [mode, setMode] = useState<ChatBubbleMode>('display')
|
||||
const [editText, setEditText] = useState(chatMessage.displayContent ?? '')
|
||||
const [isHovered, setIsHovered] = useState(false)
|
||||
|
||||
if (!chatMessage.content && !isLoading) { // don't show if empty and not loading (if loading, want to show)
|
||||
return null
|
||||
}
|
||||
|
||||
// set chat bubble contents
|
||||
let chatbubbleContents: React.ReactNode
|
||||
|
||||
if (role === 'user') {
|
||||
chatbubbleContents = <>
|
||||
<SelectedFiles type='past' selections={chatMessage.selections || []} />
|
||||
{chatMessage.displayContent}
|
||||
|
||||
{/* {!isEditMode ? chatMessage.displayContent : <></>} */}
|
||||
{/* edit mode content */}
|
||||
{/* TODO this should be the same input box as in the Sidebar */}
|
||||
{/* <textarea
|
||||
value={editModeText}
|
||||
className={`
|
||||
if (mode === 'display') {
|
||||
chatbubbleContents = <>
|
||||
<SelectedFiles type='past' selections={chatMessage.selections || []} />
|
||||
{chatMessage.displayContent}
|
||||
</>
|
||||
}
|
||||
else if (mode === 'edit') {
|
||||
chatbubbleContents = <>
|
||||
<SelectedFiles type='past' selections={chatMessage.selections || []} />
|
||||
<textarea
|
||||
value={editText}
|
||||
onChange={(e) => setEditText(e.target.value)}
|
||||
className={`
|
||||
w-full max-w-full
|
||||
h-auto min-h-[81px] max-h-[500px]
|
||||
bg-void-bg-1 resize-none
|
||||
`}
|
||||
style={{ marginTop: 0 }}
|
||||
hidden={!isEditMode}
|
||||
/> */}
|
||||
|
||||
</>
|
||||
style={{ marginTop: 0 }}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
}
|
||||
else if (role === 'assistant') {
|
||||
chatbubbleContents = <ChatMarkdownRender string={chatMessage.displayContent ?? ''} />
|
||||
}
|
||||
|
||||
return <ChatBubble_ role={role} isEditMode={isEditMode} isLoading={!!isLoading}>
|
||||
{chatbubbleContents}
|
||||
</ChatBubble_>
|
||||
return <div
|
||||
// align chatbubble accoridng to role
|
||||
className={`
|
||||
relative
|
||||
${mode === 'edit' ? 'px-2 w-full max-w-full'
|
||||
: role === 'user' ? `px-2 self-end w-fit max-w-full whitespace-pre-wrap` // user words should be pre
|
||||
: role === 'assistant' ? `px-2 self-start w-full max-w-full` : ''
|
||||
}
|
||||
${role !== 'assistant' ? 'my-2' : ''}
|
||||
`}
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<div
|
||||
// style chatbubble according to role
|
||||
className={`
|
||||
text-left rounded-lg
|
||||
overflow-x-auto max-w-full
|
||||
${role === 'user' ? 'p-2 bg-void-bg-1 text-void-fg-1' : 'px-2'}
|
||||
`}
|
||||
>
|
||||
{chatbubbleContents}
|
||||
{isLoading && <IconLoading className='opacity-50 text-sm' />}
|
||||
</div>
|
||||
|
||||
{/* edit button */}
|
||||
{role === 'user' && <Pencil
|
||||
size={18}
|
||||
className={`
|
||||
absolute -top-1 right-1
|
||||
translate-x-0 -translate-y-0
|
||||
cursor-pointer z-1
|
||||
p-[2px]
|
||||
bg-void-bg-1 border border-void-border-1 rounded-md
|
||||
transition-opacity duration-200 ease-in-out
|
||||
${isHovered ? 'opacity-100' : 'opacity-0'}
|
||||
`}
|
||||
onClick={() => setMode(m => m === 'display' ? 'edit' : 'display')}
|
||||
/>}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -523,6 +629,7 @@ export const SidebarChat = () => {
|
|||
// const modelService = accessor.get('IModelService')
|
||||
const commandService = accessor.get('ICommandService')
|
||||
|
||||
const settingsState = useSettingsState()
|
||||
// ----- HIGHER STATE -----
|
||||
// sidebar state
|
||||
const sidebarStateService = accessor.get('ISidebarStateService')
|
||||
|
|
@ -556,16 +663,19 @@ export const SidebarChat = () => {
|
|||
// state of current message
|
||||
const initVal = ''
|
||||
const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!initVal)
|
||||
const isDisabled = instructionsAreEmpty
|
||||
|
||||
const isDisabled = instructionsAreEmpty || !!isFeatureNameDisabled('Ctrl+L', settingsState)
|
||||
|
||||
const [sidebarRef, sidebarDimensions] = useResizeObserver()
|
||||
const [formRef, formDimensions] = useResizeObserver()
|
||||
const [chatAreaRef, chatAreaDimensions] = useResizeObserver()
|
||||
const [historyRef, historyDimensions] = useResizeObserver()
|
||||
|
||||
useScrollbarStyles(sidebarRef)
|
||||
|
||||
|
||||
const onSubmit = async () => {
|
||||
const onSubmit = useCallback(async () => {
|
||||
|
||||
console.log('onSubmit')
|
||||
|
||||
if (isDisabled) return
|
||||
if (isStreaming) return
|
||||
|
|
@ -578,7 +688,7 @@ export const SidebarChat = () => {
|
|||
textAreaFnsRef.current?.setValue('')
|
||||
textAreaRef.current?.focus() // focus input after submit
|
||||
|
||||
}
|
||||
}, [chatThreadsService, isDisabled, isStreaming, textAreaRef, textAreaFnsRef])
|
||||
|
||||
const onAbort = () => {
|
||||
const threadId = currentThread.id
|
||||
|
|
@ -611,6 +721,7 @@ export const SidebarChat = () => {
|
|||
</div>
|
||||
|
||||
|
||||
|
||||
const messagesHTML = <ScrollToBottomContainer
|
||||
scrollContainerRef={scrollContainerRef}
|
||||
className={`
|
||||
|
|
@ -619,8 +730,9 @@ export const SidebarChat = () => {
|
|||
overflow-x-hidden
|
||||
overflow-y-auto
|
||||
py-4
|
||||
${prevMessagesHTML.length === 0 && !messageSoFar ? 'hidden' : ''}
|
||||
`}
|
||||
style={{ maxHeight: sidebarDimensions.height - historyDimensions.height - formDimensions.height - 36 }} // the height of the previousMessages is determined by all other heights
|
||||
style={{ maxHeight: sidebarDimensions.height - historyDimensions.height - chatAreaDimensions.height - 36 }} // the height of the previousMessages is determined by all other heights
|
||||
>
|
||||
{/* previous messages */}
|
||||
{prevMessagesHTML}
|
||||
|
|
@ -639,83 +751,43 @@ export const SidebarChat = () => {
|
|||
showDismiss={true}
|
||||
/>
|
||||
|
||||
<WarningBox className='text-sm my-2 pl-4' onClick={() => { commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) }} text='Open settings' />
|
||||
<WarningBox className='text-sm my-2 mx-4' onClick={() => { commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID) }} text='Open settings' />
|
||||
</div>
|
||||
}
|
||||
</ScrollToBottomContainer>
|
||||
|
||||
|
||||
const inputBox = <div // this div is used to position the input box properly
|
||||
className={`right-0 left-0 m-2 z-[999] overflow-hidden ${previousMessages.length > 0 ? 'absolute bottom-0' : ''}`}
|
||||
>
|
||||
<div
|
||||
ref={formRef}
|
||||
className={`
|
||||
flex flex-col gap-1 p-2 relative input text-left shrink-0
|
||||
transition-all duration-200
|
||||
rounded-md
|
||||
bg-vscode-input-bg
|
||||
max-h-[80vh] overflow-y-auto
|
||||
border border-void-border-3 focus-within:border-void-border-1 hover:border-void-border-1
|
||||
`}
|
||||
onClick={(e) => {
|
||||
textAreaRef.current?.focus()
|
||||
}}
|
||||
const onChangeText = useCallback((newStr: string) => {
|
||||
setInstructionsAreEmpty(!newStr)
|
||||
}, [setInstructionsAreEmpty])
|
||||
const onKeyDown = useCallback((e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
onSubmit()
|
||||
}
|
||||
}, [onSubmit])
|
||||
const inputForm = <div className={`right-0 left-0 m-2 z-[999] overflow-hidden ${previousMessages.length > 0 ? 'absolute bottom-0' : ''}`}>
|
||||
<VoidChatArea
|
||||
divRef={chatAreaRef}
|
||||
onSubmit={onSubmit}
|
||||
onAbort={onAbort}
|
||||
isStreaming={isStreaming}
|
||||
isDisabled={isDisabled}
|
||||
showSelections={true}
|
||||
selections={selections || []}
|
||||
onSelectionsChange={chatThreadsService.setStaging.bind(chatThreadsService)}
|
||||
onClickAnywhere={() => { textAreaRef.current?.focus() }}
|
||||
featureName="Ctrl+L"
|
||||
>
|
||||
{/* top row */}
|
||||
<>
|
||||
{/* selections */}
|
||||
<SelectedFiles type='staging' selections={selections || []} setSelections={chatThreadsService.setStaging.bind(chatThreadsService)} showProspectiveSelections={previousMessages.length === 0} />
|
||||
</>
|
||||
|
||||
{/* middle row */}
|
||||
<div>
|
||||
|
||||
{/* text input */}
|
||||
<VoidInputBox2
|
||||
className='min-h-[81px] p-1'
|
||||
placeholder={`${keybindingString ? `${keybindingString} to select. ` : ''}Enter instructions...`}
|
||||
onChangeText={useCallback((newStr: string) => { setInstructionsAreEmpty(!newStr) }, [setInstructionsAreEmpty])}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
onSubmit()
|
||||
}
|
||||
}}
|
||||
ref={textAreaRef}
|
||||
fnsRef={textAreaFnsRef}
|
||||
multiline={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* bottom row */}
|
||||
<div
|
||||
className='flex flex-row justify-between items-end gap-1'
|
||||
>
|
||||
{/* submit options */}
|
||||
<div className='max-w-[150px]
|
||||
@@[&_select]:!void-border-none
|
||||
@@[&_select]:!void-outline-none
|
||||
flex-grow
|
||||
'
|
||||
>
|
||||
<ModelDropdown featureName='Ctrl+L' />
|
||||
</div>
|
||||
|
||||
{/* submit / stop button */}
|
||||
{isStreaming ?
|
||||
// stop button
|
||||
<ButtonStop
|
||||
onClick={onAbort}
|
||||
/>
|
||||
:
|
||||
// submit button (up arrow)
|
||||
<ButtonSubmit
|
||||
onClick={onSubmit}
|
||||
disabled={isDisabled}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<VoidInputBox2
|
||||
className='min-h-[81px] p-1'
|
||||
placeholder={`${keybindingString ? `${keybindingString} to select. ` : ''}Enter instructions...`}
|
||||
onChangeText={onChangeText}
|
||||
onKeyDown={onKeyDown}
|
||||
ref={textAreaRef}
|
||||
fnsRef={textAreaFnsRef}
|
||||
multiline={true}
|
||||
/>
|
||||
</VoidChatArea>
|
||||
</div>
|
||||
|
||||
return <div ref={sidebarRef} className={`w-full h-full`}>
|
||||
|
|
@ -723,7 +795,7 @@ export const SidebarChat = () => {
|
|||
|
||||
{messagesHTML}
|
||||
|
||||
{inputBox}
|
||||
{inputForm}
|
||||
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -298,9 +298,9 @@ export const VoidCheckBox = ({ label, value, onClick, className }: { label: stri
|
|||
|
||||
|
||||
|
||||
export const VoidCustomSelectBox = <T extends any>({
|
||||
export const VoidCustomDropdownBox = <T extends any>({
|
||||
options,
|
||||
selectedOption: selectedOption_,
|
||||
selectedOption,
|
||||
onChangeOption,
|
||||
getOptionDropdownName,
|
||||
getOptionDisplayName,
|
||||
|
|
@ -311,7 +311,7 @@ export const VoidCustomSelectBox = <T extends any>({
|
|||
gap = 0,
|
||||
}: {
|
||||
options: T[];
|
||||
selectedOption?: T;
|
||||
selectedOption: T | undefined;
|
||||
onChangeOption: (newValue: T) => void;
|
||||
getOptionDropdownName: (option: T) => string;
|
||||
getOptionDisplayName: (option: T) => string;
|
||||
|
|
@ -335,7 +335,7 @@ export const VoidCustomSelectBox = <T extends any>({
|
|||
} = useFloating({
|
||||
open: isOpen,
|
||||
onOpenChange: setIsOpen,
|
||||
placement:'bottom-start',
|
||||
placement: 'bottom-start',
|
||||
|
||||
middleware: [
|
||||
offset(gap),
|
||||
|
|
@ -367,17 +367,15 @@ export const VoidCustomSelectBox = <T extends any>({
|
|||
}),
|
||||
],
|
||||
whileElementsMounted: autoUpdate,
|
||||
strategy:'fixed',
|
||||
strategy: 'fixed',
|
||||
});
|
||||
|
||||
// if the selected option is null, use the 0th option
|
||||
// if the selected option is null, set the selection to the 0th option
|
||||
useEffect(() => {
|
||||
if (!options[0]) return
|
||||
if (!selectedOption_) {
|
||||
onChangeOption(options[0]);
|
||||
}
|
||||
}, [selectedOption_, options])
|
||||
const selectedOption = !selectedOption_ ? options[0] : selectedOption_
|
||||
if (options.length === 0) return
|
||||
if (selectedOption) return
|
||||
onChangeOption(options[0])
|
||||
}, [selectedOption, onChangeOption, options])
|
||||
|
||||
// Handle clicks outside
|
||||
useEffect(() => {
|
||||
|
|
@ -404,6 +402,9 @@ export const VoidCustomSelectBox = <T extends any>({
|
|||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, [isOpen, refs.floating, refs.reference]);
|
||||
|
||||
if (!selectedOption)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className={`inline-block relative ${className}`}>
|
||||
{/* Hidden measurement div */}
|
||||
|
|
|
|||
|
|
@ -4,15 +4,14 @@
|
|||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { FeatureName, featureNames, ModelSelection, modelSelectionsEqual, ProviderName, providerNames } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
|
||||
import { FeatureName, featureNames, isFeatureNameDisabled, ModelSelection, modelSelectionsEqual, ProviderName, providerNames, SettingsOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
|
||||
import { useSettingsState, useRefreshModelState, useAccessor } from '../util/services.js'
|
||||
import { _VoidSelectBox, VoidCustomSelectBox } from '../util/inputs.js'
|
||||
import { _VoidSelectBox, VoidCustomDropdownBox } from '../util/inputs.js'
|
||||
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js'
|
||||
import { IconWarning } from '../sidebar-tsx/SidebarChat.js'
|
||||
import { VOID_OPEN_SETTINGS_ACTION_ID, VOID_TOGGLE_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'
|
||||
import { ModelOption } from '../../../../../../../platform/void/common/voidSettingsService.js'
|
||||
|
||||
|
||||
import { WarningBox } from './WarningBox.js'
|
||||
|
||||
const optionsEqual = (m1: ModelOption[], m2: ModelOption[]) => {
|
||||
if (m1.length !== m2.length) return false
|
||||
|
|
@ -27,22 +26,21 @@ const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], feat
|
|||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
||||
const selection = voidSettingsService.state.modelSelectionOfFeature[featureName]
|
||||
const selectedOption = selection ? voidSettingsService.state._modelOptions.find(v => modelSelectionsEqual(v.selection, selection)) : options[0]
|
||||
const selectedOption = selection ? voidSettingsService.state._modelOptions.find(v => modelSelectionsEqual(v.selection, selection))! : options[0]
|
||||
|
||||
const onChangeOption = useCallback((newOption: ModelOption) => {
|
||||
voidSettingsService.setModelSelectionOfFeature(featureName, newOption.selection)
|
||||
}, [voidSettingsService, featureName])
|
||||
|
||||
return <VoidCustomSelectBox
|
||||
return <VoidCustomDropdownBox
|
||||
options={options}
|
||||
selectedOption={selectedOption}
|
||||
onChangeOption={onChangeOption}
|
||||
getOptionDisplayName={(option) => option.selection.modelName}
|
||||
getOptionDropdownName={(option) => option.name}
|
||||
getOptionsEqual={(a, b) => optionsEqual([a], [b])}
|
||||
className={`text-xs text-void-fg-3 px-1`}
|
||||
className='text-xs text-void-fg-3 px-1'
|
||||
matchInputWidth={false}
|
||||
// isMenuPositionFixed={featureName === 'Ctrl+K' ? false : true}
|
||||
/>
|
||||
}
|
||||
// const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], featureName: FeatureName }) => {
|
||||
|
|
@ -76,10 +74,13 @@ const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], feat
|
|||
// />
|
||||
// }
|
||||
|
||||
const MemoizedModelSelectBox = ({ featureName }: { featureName: FeatureName }) => {
|
||||
|
||||
|
||||
const MemoizedModelDropdown = ({ featureName }: { featureName: FeatureName }) => {
|
||||
const settingsState = useSettingsState()
|
||||
const oldOptionsRef = useRef<ModelOption[]>([])
|
||||
const [memoizedOptions, setMemoizedOptions] = useState(oldOptionsRef.current)
|
||||
|
||||
useEffect(() => {
|
||||
const oldOptions = oldOptionsRef.current
|
||||
const newOptions = settingsState._modelOptions
|
||||
|
|
@ -93,30 +94,6 @@ const MemoizedModelSelectBox = ({ featureName }: { featureName: FeatureName }) =
|
|||
|
||||
}
|
||||
|
||||
export const WarningBox = ({ text, onClick, className }: { text: string; onClick?: () => void; className?: string }) => {
|
||||
|
||||
return <div
|
||||
className={`
|
||||
text-void-warning brightness-90 opacity-90
|
||||
text-xs text-ellipsis
|
||||
${onClick ? `hover:brightness-75 transition-all duration-200 cursor-pointer` : ''}
|
||||
flex items-center flex-nowrap
|
||||
${className}
|
||||
`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<IconWarning
|
||||
size={14}
|
||||
className='mr-1'
|
||||
/>
|
||||
<span>{text}</span>
|
||||
</div>
|
||||
// return <VoidSelectBox
|
||||
// options={[{ text: 'Please add a model!', value: null }]}
|
||||
// onChangeSelection={() => { }}
|
||||
// />
|
||||
}
|
||||
|
||||
export const ModelDropdown = ({ featureName }: { featureName: FeatureName }) => {
|
||||
const settingsState = useSettingsState()
|
||||
|
||||
|
|
@ -125,10 +102,14 @@ export const ModelDropdown = ({ featureName }: { featureName: FeatureName }) =>
|
|||
|
||||
const openSettings = () => { commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID); };
|
||||
|
||||
return <>
|
||||
{settingsState._modelOptions.length === 0 ?
|
||||
<WarningBox onClick={openSettings} text='Provider required' />
|
||||
: <MemoizedModelSelectBox featureName={featureName} />
|
||||
}
|
||||
</>
|
||||
const isDisabled = isFeatureNameDisabled(featureName, settingsState)
|
||||
if (isDisabled)
|
||||
return <WarningBox onClick={openSettings} text={
|
||||
isDisabled === 'needToEnableModel' ? 'Enable a model'
|
||||
: isDisabled === 'addModel' ? 'Add a model'
|
||||
: (isDisabled === 'addProvider' || isDisabled === 'notFilledIn' || isDisabled === 'providerNotAutoDetected') ? 'Provider required'
|
||||
: 'Provider required'
|
||||
} />
|
||||
|
||||
return <MemoizedModelDropdown featureName={featureName} />
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,17 +5,18 @@
|
|||
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'
|
||||
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
|
||||
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled } from '../../../../../../../platform/void/common/voidSettingsTypes.js'
|
||||
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
|
||||
import { VoidButton, VoidCheckBox, VoidCustomSelectBox, VoidInputBox, VoidInputBox2, VoidSwitch } from '../util/inputs.js'
|
||||
import { VoidButton, VoidCheckBox, VoidCustomDropdownBox, VoidInputBox, VoidInputBox2, VoidSwitch } from '../util/inputs.js'
|
||||
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
|
||||
import { X, RefreshCw, Loader2, Check, MoveRight } from 'lucide-react'
|
||||
import { useScrollbarStyles } from '../util/useScrollbarStyles.js'
|
||||
import { isWindows, isLinux, isMacintosh } from '../../../../../../../base/common/platform.js'
|
||||
import { URI } from '../../../../../../../base/common/uri.js'
|
||||
import { env } from '../../../../../../../base/common/process.js'
|
||||
import { WarningBox, ModelDropdown } from './ModelDropdown.js'
|
||||
import { ModelDropdown } from './ModelDropdown.js'
|
||||
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'
|
||||
import { WarningBox } from './WarningBox.js'
|
||||
|
||||
const SubtleButton = ({ onClick, text, icon, disabled }: { onClick: () => void, text: string, icon: React.ReactNode, disabled: boolean }) => {
|
||||
|
||||
|
|
@ -79,7 +80,7 @@ const RefreshableModels = () => {
|
|||
|
||||
|
||||
const buttons = refreshableProviderNames.map(providerName => {
|
||||
if (!settingsState.settingsOfProvider[providerName]._enabled) return null
|
||||
if (!settingsState.settingsOfProvider[providerName]._didFillInProviderSettings) return null
|
||||
return <div key={providerName} className='pb-4'>
|
||||
<RefreshModelButton providerName={providerName} />
|
||||
</div>
|
||||
|
|
@ -112,7 +113,7 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
|
|||
<div className='flex items-center gap-4'>
|
||||
|
||||
{/* provider */}
|
||||
<VoidCustomSelectBox
|
||||
<VoidCustomDropdownBox
|
||||
options={providerNames}
|
||||
selectedOption={providerName}
|
||||
onChangeOption={(pn) => setProviderName(pn)}
|
||||
|
|
@ -199,7 +200,7 @@ export const ModelDump = () => {
|
|||
for (let providerName of providerNames) {
|
||||
const providerSettings = settingsState.settingsOfProvider[providerName]
|
||||
// if (!providerSettings.enabled) continue
|
||||
modelDump.push(...providerSettings.models.map(model => ({ ...model, providerName, providerEnabled: !!providerSettings._enabled })))
|
||||
modelDump.push(...providerSettings.models.map(model => ({ ...model, providerName, providerEnabled: !!providerSettings._didFillInProviderSettings })))
|
||||
}
|
||||
|
||||
// sort by hidden
|
||||
|
|
@ -223,7 +224,6 @@ export const ModelDump = () => {
|
|||
<div className={`flex-grow flex items-center gap-4`}>
|
||||
<span className='w-full max-w-32'>{isNewProviderName ? displayInfoOfProviderName(providerName).title : ''}</span>
|
||||
<span className='w-fit truncate'>{modelName}</span>
|
||||
{/* <span>{`${modelName} (${providerName})`}</span> */}
|
||||
</div>
|
||||
{/* right part is anything that fits */}
|
||||
<div className='flex items-center gap-4'>
|
||||
|
|
@ -260,7 +260,6 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
|
|||
|
||||
const accessor = useAccessor()
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
const voidMetricsService = accessor.get('IMetricsService')
|
||||
|
||||
let weChangedTextRef = false
|
||||
|
||||
|
|
@ -284,25 +283,8 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
|
|||
weChangedTextRef = true
|
||||
instance.value = stateVal as string
|
||||
weChangedTextRef = false
|
||||
|
||||
const isEverySettingPresent = Object.keys(defaultProviderSettings[providerName]).every(key => {
|
||||
return !!settingsAtProvider[key as keyof typeof settingsAtProvider]
|
||||
})
|
||||
|
||||
const shouldEnable = isEverySettingPresent && !settingsAtProvider._enabled // enable if all settings are present and not already enabled
|
||||
const shouldDisable = !isEverySettingPresent && settingsAtProvider._enabled
|
||||
|
||||
if (shouldEnable) {
|
||||
voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
|
||||
voidMetricsService.capture('Enable Provider', { providerName })
|
||||
}
|
||||
|
||||
if (shouldDisable) {
|
||||
voidSettingsService.setSettingOfProvider(providerName, '_enabled', false)
|
||||
voidMetricsService.capture('Disable Provider', { providerName })
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
syncInstance()
|
||||
const disposable = voidSettingsService.onDidChangeState(syncInstance)
|
||||
return [disposable]
|
||||
|
|
@ -318,7 +300,10 @@ const ProviderSetting = ({ providerName, settingName }: { providerName: Provider
|
|||
}
|
||||
|
||||
const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) => {
|
||||
// const voidSettingsState = useSettingsState()
|
||||
const voidSettingsState = useSettingsState()
|
||||
|
||||
const needsModel = isProviderNameDisabled(providerName, voidSettingsState) === 'addModel'
|
||||
|
||||
// const accessor = useAccessor()
|
||||
// const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
|
||||
|
|
@ -349,6 +334,12 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) =
|
|||
{settingNames.map((settingName, i) => {
|
||||
return <ProviderSetting key={settingName} providerName={providerName} settingName={settingName} />
|
||||
})}
|
||||
|
||||
{needsModel ?
|
||||
providerName === 'ollama' ?
|
||||
<WarningBox text={`Please install an Ollama model. We'll auto-detect it.`} />
|
||||
: <WarningBox text={`Please add a model for ${providerTitle} below (Models).`} />
|
||||
: null}
|
||||
</div>
|
||||
</div >
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import { IconWarning } from '../sidebar-tsx/SidebarChat.js';
|
||||
|
||||
|
||||
export const WarningBox = ({ text, onClick, className }: { text: string; onClick?: () => void; className?: string }) => {
|
||||
|
||||
return <div
|
||||
className={`
|
||||
text-void-warning brightness-90 opacity-90 w-fit
|
||||
text-xs text-ellipsis
|
||||
${onClick ? `hover:brightness-75 transition-all duration-200 cursor-pointer` : ''}
|
||||
flex items-center flex-nowrap
|
||||
${className}
|
||||
`}
|
||||
onClick={onClick}
|
||||
>
|
||||
<IconWarning
|
||||
size={14}
|
||||
className='mr-1'
|
||||
/>
|
||||
<span>{text}</span>
|
||||
</div>
|
||||
// return <VoidSelectBox
|
||||
// options={[{ text: 'Please add a model!', value: null }]}
|
||||
// onChangeSelection={() => { }}
|
||||
// />
|
||||
}
|
||||
|
|
@ -21,6 +21,10 @@ import './chatThreadService.js'
|
|||
// register Autocomplete
|
||||
import './autocompleteService.js'
|
||||
|
||||
// register Context services
|
||||
import './contextGatheringService.js'
|
||||
// import './contextUserChangesService.js'
|
||||
|
||||
// settings pane
|
||||
import './voidSettingsPane.js'
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { EditorInput } from '../../../common/editor/editorInput.js';
|
|||
import * as nls from '../../../../nls.js';
|
||||
import { EditorExtensions } from '../../../common/editor.js';
|
||||
import { EditorPane } from '../../../browser/parts/editor/editorPane.js';
|
||||
import { IEditorGroup } from '../../../services/editor/common/editorGroupsService.js';
|
||||
import { IEditorGroup, IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';
|
||||
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
|
||||
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
|
||||
import { IStorageService } from '../../../../platform/storage/common/storage.js';
|
||||
|
|
@ -35,7 +35,7 @@ class VoidSettingsInput extends EditorInput {
|
|||
static readonly ID: string = 'workbench.input.void.settings';
|
||||
|
||||
static readonly RESOURCE = URI.from({ // I think this scheme is invalid, it just shuts up TS
|
||||
scheme: 'void', // Custom scheme for our editor
|
||||
scheme: 'void', // Custom scheme for our editor (try Schemas.https)
|
||||
path: 'settings'
|
||||
})
|
||||
readonly resource = VoidSettingsInput.RESOURCE;
|
||||
|
|
@ -141,18 +141,27 @@ registerAction2(class extends Action2 {
|
|||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const editorService = accessor.get(IEditorService);
|
||||
const editorGroupService = accessor.get(IEditorGroupsService);
|
||||
|
||||
const instantiationService = accessor.get(IInstantiationService);
|
||||
|
||||
// close all instances if found
|
||||
const openEditors = editorService.findEditors(VoidSettingsInput.RESOURCE);
|
||||
if (openEditors.length > 0) {
|
||||
await editorService.closeEditors(openEditors);
|
||||
// if is open, close it
|
||||
const openEditors = editorService.findEditors(VoidSettingsInput.RESOURCE); // should only have 0 or 1 elements...
|
||||
if (openEditors.length !== 0) {
|
||||
const openEditor = openEditors[0].editor
|
||||
const isCurrentlyOpen = editorService.activeEditor?.resource?.fsPath === openEditor.resource?.fsPath
|
||||
if (isCurrentlyOpen)
|
||||
await editorService.closeEditors(openEditors)
|
||||
else
|
||||
await editorGroupService.activeGroup.openEditor(openEditor)
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// else open it
|
||||
const input = instantiationService.createInstance(VoidSettingsInput);
|
||||
await editorService.openEditor(input);
|
||||
|
||||
await editorGroupService.activeGroup.openEditor(input);
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue