add autocomplete UI and filtering

This commit is contained in:
Andrew Pareles 2025-03-05 23:03:08 -08:00
parent 46e8fb1ea5
commit 1be8d17899
5 changed files with 104 additions and 30 deletions

View file

@ -257,7 +257,7 @@ export const VoidSwitch = ({
</div>
{label && (
<span className={`
ml-3 font-medium text-gray-900 dark:text-gray-100
ml-3 text-gray-900 dark:text-gray-100
${size === 'xs' ? 'text-xs' : 'text-sm'}
`}>
{label}

View file

@ -10,7 +10,7 @@ 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 '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
import { modelFilterOfFeatureName, ModelOption } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
import { WarningBox } from './WarningBox.js'
const optionsEqual = (m1: ModelOption[], m2: ModelOption[]) => {
@ -38,7 +38,7 @@ const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], feat
onChangeOption={onChangeOption}
getOptionDisplayName={(option) => option.selection.modelName}
getOptionDropdownName={(option) => option.selection.modelName}
getOptionDropdownDetail={(option) => option.selection.providerName }
getOptionDropdownDetail={(option) => option.selection.providerName}
getOptionsEqual={(a, b) => optionsEqual([a], [b])}
className='text-xs text-void-fg-3 px-1'
matchInputWidth={false}
@ -82,14 +82,21 @@ const MemoizedModelDropdown = ({ featureName }: { featureName: FeatureName }) =>
const oldOptionsRef = useRef<ModelOption[]>([])
const [memoizedOptions, setMemoizedOptions] = useState(oldOptionsRef.current)
const { filter, emptyMessage } = modelFilterOfFeatureName[featureName]
useEffect(() => {
const oldOptions = oldOptionsRef.current
const newOptions = settingsState._modelOptions
const newOptions = settingsState._modelOptions.filter((o) => filter(o.selection))
if (!optionsEqual(oldOptions, newOptions)) {
setMemoizedOptions(newOptions)
}
oldOptionsRef.current = newOptions
}, [settingsState._modelOptions])
}, [settingsState._modelOptions, filter])
if (memoizedOptions.length === 0) { // Pretty sure this will never be reached unless filter is enabled
return <WarningBox text={emptyMessage || 'No models available'} />
}
return <ModelSelectBox featureName={featureName} options={memoizedOptions} />
@ -103,13 +110,17 @@ export const ModelDropdown = ({ featureName }: { featureName: FeatureName }) =>
const openSettings = () => { commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID); };
const { emptyMessage } = modelFilterOfFeatureName[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'
emptyMessage ? emptyMessage :
isDisabled === 'needToEnableModel' ? 'Enable a model'
: isDisabled === 'addModel' ? 'Add a model'
: (isDisabled === 'addProvider' || isDisabled === 'notFilledIn' || isDisabled === 'providerNotAutoDetected') ? 'Provider required'
: 'Provider required'
} />
return <MemoizedModelDropdown featureName={featureName} />

View file

@ -396,6 +396,11 @@ export const AIInstructionsBox = () => {
}
export const FeaturesTab = () => {
const voidSettingsState = useSettingsState()
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
return <>
<h2 className={`text-3xl mb-2`}>Models</h2>
<ErrorBoundary>
@ -434,17 +439,27 @@ export const FeaturesTab = () => {
<h2 className={`text-3xl mb-2 mt-12`}>Feature Options</h2>
<h2 className={`text-3xl mt-12`}>Feature Options</h2>
<ErrorBoundary>
{featureNames.map(featureName =>
(['Ctrl+L', 'Ctrl+K'] as FeatureName[]).includes(featureName) ? null :
<div key={featureName}
className='mb-2'
>
<h4 className={`text-void-fg-3`}>{displayInfoOfFeatureName(featureName)}</h4>
<ModelDropdown featureName={featureName} />
<div className='flex gap-x-4 items-start justify-around mt-4 mb-16'>
<div>
<h4 className={`text-base`}>{displayInfoOfFeatureName('Autocomplete')}</h4>
<div className='text-sm italic text-void-fg-3 my-1'>Experimental. Only works with FIM models.</div>
<div className='flex items-center gap-x-2'>
<VoidSwitch size='xs' value={voidSettingsState.globalSettings.enableAutocomplete} onChange={(newVal) => voidSettingsService.setGlobalSetting('enableAutocomplete', newVal)} label={voidSettingsState.globalSettings.enableAutocomplete ? 'Enabled' : 'Disabled'} />
</div>
)}
<div className={!voidSettingsState.globalSettings.enableAutocomplete ? 'hidden' : ''}>
<ModelDropdown featureName={'Autocomplete'} />
</div>
</div>
<div>
<h4 className={`text-base`}>{displayInfoOfFeatureName('Apply')}</h4>
<ModelDropdown featureName={'Apply'} />
</div>
</div>
</ErrorBoundary>
</>
@ -649,7 +664,7 @@ export const Settings = () => {
{/* content */}
<div className='w-full min-w-[550px] overflow-auto'>
<div className='w-full min-w-[550px]'>
<div className={`${tab !== 'models' ? 'hidden' : ''}`}>
<FeaturesTab />

View file

@ -11,11 +11,18 @@ import { registerSingleton, InstantiationType } from '../../../../platform/insta
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { IMetricsService } from './metricsService.js';
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, defaultProviderSettings } from './voidSettingsTypes.js';
import { getModelCapabilities } from './modelCapabilities.js';
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, defaultProviderSettings, ModelSelectionOptions, OptionsOfModelSelection } from './voidSettingsTypes.js';
const STORAGE_KEY = 'void.settingsServiceStorage'
// name is the name in the dropdown
export type ModelOption = { name: string, selection: ModelSelection }
type SetSettingOfProviderFn = <S extends SettingName>(
providerName: ProviderName,
settingName: S,
@ -25,16 +32,17 @@ type SetSettingOfProviderFn = <S extends SettingName>(
type SetModelSelectionOfFeatureFn = <K extends FeatureName>(
featureName: K,
newVal: ModelSelectionOfFeature[K],
options?: { doNotApplyEffects?: true }
) => Promise<void>;
type SetGlobalSettingFn = <T extends GlobalSettingName>(settingName: T, newVal: GlobalSettings[T]) => void;
export type ModelOption = { name: string, selection: ModelSelection }
type SetOptionsOfModelSelection = (providerName: ProviderName, modelName: string, newVal: Partial<ModelSelectionOptions>) => void
export type VoidSettingsState = {
readonly settingsOfProvider: SettingsOfProvider; // optionsOfProvider
readonly modelSelectionOfFeature: ModelSelectionOfFeature; // stateOfFeature
readonly optionsOfModelSelection: OptionsOfModelSelection;
readonly globalSettings: GlobalSettings;
readonly _modelOptions: ModelOption[] // computed based on the two above items
@ -55,6 +63,7 @@ export interface IVoidSettingsService {
setSettingOfProvider: SetSettingOfProviderFn;
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn;
setOptionsOfModelSelection: SetOptionsOfModelSelection;
setGlobalSetting: SetGlobalSettingFn;
setAutodetectedModels(providerName: ProviderName, modelNames: string[], logging: object): void;
@ -88,6 +97,14 @@ const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], opt
}
export const modelFilterOfFeatureName: { [featureName in FeatureName]: { filter: (o: ModelSelection) => boolean; emptyMessage: string | null } } = {
'Autocomplete': { filter: o => getModelCapabilities(o.providerName, o.modelName).supportsFIM, emptyMessage: 'No models support FIM' },
'Ctrl+L': { filter: o => true, emptyMessage: null },
'Ctrl+K': { filter: o => true, emptyMessage: null },
'Apply': { filter: o => true, emptyMessage: null },
}
const _validatedState = (state: Omit<VoidSettingsState, '_modelOptions'>) => {
let newSettingsOfProvider = state.settingsOfProvider
@ -125,14 +142,17 @@ const _validatedState = (state: Omit<VoidSettingsState, '_modelOptions'>) => {
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))
const { filter } = modelFilterOfFeatureName[featureName]
const modelOptionsForThisFeature = newModelOptions.filter((o) => filter(o.selection))
if (selnIdx !== -1) continue
const modelSelectionAtFeature = newModelSelectionOfFeature[featureName]
const selnIdx = modelSelectionAtFeature === null ? -1 : modelOptionsForThisFeature.findIndex(m => modelSelectionsEqual(m.selection, modelSelectionAtFeature))
if (selnIdx !== -1) continue // no longer in list, so update to 1st in list or null
newModelSelectionOfFeature = {
...newModelSelectionOfFeature,
[featureName]: newModelOptions.length === 0 ? null : newModelOptions[0].selection
[featureName]: modelOptionsForThisFeature.length === 0 ? null : modelOptionsForThisFeature[0].selection
}
}
@ -156,6 +176,7 @@ const defaultState = () => {
settingsOfProvider: deepClone(defaultSettingsOfProvider),
modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null, 'Apply': null },
globalSettings: deepClone(defaultGlobalSettings),
optionsOfModelSelection: {},
_modelOptions: [], // computed later
}
return d
@ -260,6 +281,8 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
const newModelSelectionOfFeature = this.state.modelSelectionOfFeature
const newOptionsOfModelSelection = this.state.optionsOfModelSelection
const newSettingsOfProvider: SettingsOfProvider = {
...this.state.settingsOfProvider,
[providerName]: {
@ -272,6 +295,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
const newState = {
modelSelectionOfFeature: newModelSelectionOfFeature,
optionsOfModelSelection: newOptionsOfModelSelection,
settingsOfProvider: newSettingsOfProvider,
globalSettings: newGlobalSettings,
}
@ -299,7 +323,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
}
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn = async (featureName, newVal, options) => {
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn = async (featureName, newVal) => {
const newState: VoidSettingsState = {
...this.state,
modelSelectionOfFeature: {
@ -310,8 +334,26 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
this.state = newState
if (options?.doNotApplyEffects)
return
await this._storeState()
this._onDidChangeState.fire()
}
setOptionsOfModelSelection = async (providerName: ProviderName, modelName: string, newVal: Partial<ModelSelectionOptions>) => {
const newState: VoidSettingsState = {
...this.state,
optionsOfModelSelection: {
...this.state.optionsOfModelSelection,
[providerName]: {
...this.state.optionsOfModelSelection[providerName],
[modelName]: {
...this.state.optionsOfModelSelection[providerName]?.[modelName],
...newVal
}
}
}
}
this.state = newState
await this._storeState()
this._onDidChangeState.fire()

View file

@ -380,7 +380,7 @@ export const displayInfoOfFeatureName = (featureName: FeatureName) => {
else if (featureName === 'Ctrl+L')
return 'Chat'
else if (featureName === 'Apply')
return 'Apply'
return 'Fast Apply'
else
throw new Error(`Feature Name ${featureName} not allowed`)
}
@ -439,10 +439,12 @@ export const isFeatureNameDisabled = (featureName: FeatureName, settingsState: V
export type GlobalSettings = {
autoRefreshModels: boolean;
aiInstructions: string;
enableAutocomplete: boolean;
}
export const defaultGlobalSettings: GlobalSettings = {
autoRefreshModels: true,
aiInstructions: '',
enableAutocomplete: false,
}
export type GlobalSettingName = keyof GlobalSettings
@ -459,4 +461,8 @@ export const globalSettingNames = Object.keys(defaultGlobalSettings) as GlobalSe
export type ModelSelectionOptions = {
reasoningSelection?: { budget: number }
}
export type OptionsOfModelSelection = Partial<{ [providerName in ProviderName]: { [modelName: string]: ModelSelectionOptions } }>