diff --git a/src/vs/editor/contrib/lineSelection/browser/lineSelection.ts b/src/vs/editor/contrib/lineSelection/browser/lineSelection.ts index 1ad614b8..5d65964e 100644 --- a/src/vs/editor/contrib/lineSelection/browser/lineSelection.ts +++ b/src/vs/editor/contrib/lineSelection/browser/lineSelection.ts @@ -22,7 +22,7 @@ export class ExpandLineSelectionAction extends EditorAction { kbOpts: { weight: KeybindingWeight.EditorCore, kbExpr: EditorContextKeys.textInputFocus, - primary: KeyMod.CtrlCmd | KeyCode.KeyL + primary: KeyMod.CtrlCmd | KeyCode.KeyM // Void changed this to Cmd+M }, }); } diff --git a/src/vs/platform/void/common/llmMessageService.ts b/src/vs/platform/void/common/llmMessageService.ts index e0ec2b12..606f4487 100644 --- a/src/vs/platform/void/common/llmMessageService.ts +++ b/src/vs/platform/void/common/llmMessageService.ts @@ -3,7 +3,7 @@ * Void Editor additions licensed under the AGPL 3.0 License. *--------------------------------------------------------------------------------------------*/ -import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainLLMMessageParams, MainLLMMessageAbortParams, ServiceOllamaListParams, EventOllamaListOnSuccessParams, EventOllamaListOnErrorParams, MainOllamaListParams } from './llmMessageTypes.js'; +import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from './llmMessageTypes.js'; import { IChannel } from '../../../base/parts/ipc/common/ipc.js'; import { IMainProcessService } from '../../ipc/common/mainProcessService.js'; import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js'; @@ -21,7 +21,8 @@ export interface ILLMMessageService { readonly _serviceBrand: undefined; sendLLMMessage: (params: ServiceSendLLMMessageParams) => string | null; abort: (requestId: string) => void; - ollamaList: (params: ServiceOllamaListParams) => void; + ollamaList: (params: ServiceModelListParams) => void; + openAICompatibleList: (params: ServiceModelListParams) => void; } export class LLMMessageService extends Disposable implements ILLMMessageService { @@ -36,9 +37,12 @@ export class LLMMessageService extends Disposable implements ILLMMessageService // ollamaList - private readonly onSuccess_ollama: { [eventId: string]: ((params: EventOllamaListOnSuccessParams) => void) } = {} - private readonly onError_ollama: { [eventId: string]: ((params: EventOllamaListOnErrorParams) => void) } = {} + private readonly onSuccess_ollama: { [eventId: string]: ((params: EventModelListOnSuccessParams) => void) } = {} + private readonly onError_ollama: { [eventId: string]: ((params: EventModelListOnErrorParams) => void) } = {} + // openAICompatibleList + private readonly onSuccess_openAICompatible: { [eventId: string]: ((params: EventModelListOnSuccessParams) => void) } = {} + private readonly onError_openAICompatible: { [eventId: string]: ((params: EventModelListOnErrorParams) => void) } = {} constructor( @IMainProcessService private readonly mainProcessService: IMainProcessService, // used as a renderer (only usable on client side) @@ -65,12 +69,19 @@ export class LLMMessageService extends Disposable implements ILLMMessageService this._onRequestIdDone(e.requestId) })) // ollama - this._register((this.channel.listen('onSuccess_ollama') satisfies Event)(e => { + this._register((this.channel.listen('onSuccess_ollama') satisfies Event>)(e => { this.onSuccess_ollama[e.requestId]?.(e) })) - this._register((this.channel.listen('onError_ollama') satisfies Event)(e => { + this._register((this.channel.listen('onError_ollama') satisfies Event>)(e => { this.onError_ollama[e.requestId]?.(e) })) + // openaiCompatible + this._register((this.channel.listen('onSuccess_openAICompatible') satisfies Event>)(e => { + this.onSuccess_openAICompatible[e.requestId]?.(e) + })) + this._register((this.channel.listen('onError_openAICompatible') satisfies Event>)(e => { + this.onError_openAICompatible[e.requestId]?.(e) + })) } @@ -113,7 +124,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService } - ollamaList = (params: ServiceOllamaListParams) => { + ollamaList = (params: ServiceModelListParams) => { const { onSuccess, onError, ...proxyParams } = params const { settingsOfProvider } = this.voidSettingsService.state @@ -127,7 +138,24 @@ export class LLMMessageService extends Disposable implements ILLMMessageService ...proxyParams, settingsOfProvider, requestId: requestId_, - } satisfies MainOllamaListParams) + } satisfies MainModelListParams) + } + + openAICompatibleList = (params: ServiceModelListParams) => { + const { onSuccess, onError, ...proxyParams } = params + + const { settingsOfProvider } = this.voidSettingsService.state + + // add state for request id + const requestId_ = generateUuid(); + this.onSuccess_openAICompatible[requestId_] = onSuccess + this.onError_openAICompatible[requestId_] = onError + + this.channel.call('openAICompatibleList', { + ...proxyParams, + settingsOfProvider, + requestId: requestId_, + } satisfies MainModelListParams) } diff --git a/src/vs/platform/void/common/llmMessageTypes.ts b/src/vs/platform/void/common/llmMessageTypes.ts index b4e3c54b..5bc92e24 100644 --- a/src/vs/platform/void/common/llmMessageTypes.ts +++ b/src/vs/platform/void/common/llmMessageTypes.ts @@ -97,7 +97,7 @@ export type _InternalSendLLMMessageFnType = (params: { // These are from 'ollama' SDK -interface ModelDetails { +interface OllamaModelDetails { parent_model: string; format: string; family: string; @@ -106,35 +106,44 @@ interface ModelDetails { quantization_level: string; } -export type ModelResponse = { +export type OllamaModelResponse = { name: string; modified_at: Date; size: number; digest: string; - details: ModelDetails; + details: OllamaModelDetails; expires_at: Date; size_vram: number; } +export type OpenaiCompatibleModelResponse = { + id: string; + created: number; + object: 'model'; + owned_by: string; +} + // params to the true list fn -export type OllamaListParams = { +export type ModelListParams = { settingsOfProvider: SettingsOfProvider; - onSuccess: (param: { models: ModelResponse[] }) => void; + onSuccess: (param: { models: modelResponse[] }) => void; onError: (param: { error: string }) => void; } -export type ServiceOllamaListParams = { - onSuccess: (param: { models: ModelResponse[] }) => void; +// params to the service +export type ServiceModelListParams = { + onSuccess: (param: { models: modelResponse[] }) => void; onError: (param: { error: any }) => void; } -type BlockedMainOllamaListParams = 'onSuccess' | 'onError' -export type MainOllamaListParams = Omit & { requestId: string } +type BlockedMainModelListParams = 'onSuccess' | 'onError' +export type MainModelListParams = Omit, BlockedMainModelListParams> & { requestId: string } -export type EventOllamaListOnSuccessParams = Parameters[0] & { requestId: string } -export type EventOllamaListOnErrorParams = Parameters[0] & { requestId: string } +export type EventModelListOnSuccessParams = Parameters['onSuccess']>[0] & { requestId: string } +export type EventModelListOnErrorParams = Parameters['onError']>[0] & { requestId: string } -export type _InternalOllamaListFnType = (params: OllamaListParams) => void + +export type _InternalModelListFnType = (params: ModelListParams) => void diff --git a/src/vs/platform/void/common/refreshModelService.ts b/src/vs/platform/void/common/refreshModelService.ts index 1f6b08f3..e552e6d0 100644 --- a/src/vs/platform/void/common/refreshModelService.ts +++ b/src/vs/platform/void/common/refreshModelService.ts @@ -8,10 +8,37 @@ import { InstantiationType, registerSingleton } from '../../instantiation/common import { IVoidSettingsService } from './voidSettingsService.js'; import { ILLMMessageService } from './llmMessageService.js'; import { Emitter, Event } from '../../../base/common/event.js'; -import { Disposable } from '../../../base/common/lifecycle.js'; +import { Disposable, IDisposable } from '../../../base/common/lifecycle.js'; +import { ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'; +import { OllamaModelResponse, OpenaiCompatibleModelResponse } from './llmMessageTypes.js'; -export type RefreshModelState = 'done' | 'loading' +export const refreshableProviderNames = ['ollama', 'openAICompatible'] satisfies ProviderName[] + +export type RefreshableProviderName = typeof refreshableProviderNames[number] + + +type RefreshableState = { + state: 'init', + timeoutId: null, +} | { + state: 'refreshing', + timeoutId: NodeJS.Timeout | null, +} | { + state: 'success', + timeoutId: null, +} + + +export type RefreshModelStateOfProvider = Record + + + +const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvider[k])[] } = { + ollama: ['enabled', 'endpoint'], + openAICompatible: ['enabled', 'endpoint', 'apiKey'], +} +const REFRESH_INTERVAL = 5000 // element-wise equals function eq(a: T[], b: T[]): boolean { @@ -23,9 +50,9 @@ function eq(a: T[], b: T[]): boolean { } export interface IRefreshModelService { readonly _serviceBrand: undefined; - refreshOllamaModels(): void; - onDidChangeState: Event; - state: RefreshModelState; + refreshModels: (providerName: RefreshableProviderName) => Promise; + onDidChangeState: Event; + state: RefreshModelStateOfProvider; } export const IRefreshModelService = createDecorator('RefreshModelService'); @@ -34,8 +61,8 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ readonly _serviceBrand: undefined; - private readonly _onDidChangeState = new Emitter(); - readonly onDidChangeState: Event = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes + private readonly _onDidChangeState = new Emitter(); + readonly onDidChangeState: Event = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes constructor( @IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService, @@ -43,62 +70,111 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ ) { super() - // on mount, refresh ollama models - this.refreshOllamaModels() - // every time ollama.enabled changes, refresh ollama models, like useEffect - let relevantVals = () => [this.voidSettingsService.state.settingsOfProvider.ollama.enabled, this.voidSettingsService.state.settingsOfProvider.ollama.endpoint] - let prevVals = relevantVals() - this._register( - this.voidSettingsService.onDidChangeState(() => { // we might want to debounce this - const newVals = relevantVals() - if (!eq(prevVals, newVals)) { - this.refreshOllamaModels() - prevVals = newVals + const disposables: Set = new Set() + + + const startRefreshing = () => { + this._clearAllTimeouts() + disposables.forEach(d => d.dispose()) + disposables.clear() + + if (!voidSettingsService.state.featureFlagSettings.autoRefreshModels) return + + for (const providerName of refreshableProviderNames) { + + const refresh = () => { + // const { enabled } = this.voidSettingsService.state.settingsOfProvider[providerName] + this.refreshModels(providerName, { enableProviderOnSuccess: true }) // enable the provider on success } - }) - ) + + refresh() + + // every time providerName.enabled changes, refresh models too, like a useEffect + let relevantVals = () => refreshBasedOn[providerName].map(settingName => this.voidSettingsService.state.settingsOfProvider[providerName][settingName]) + let prevVals = relevantVals() // each iteration of a for loop has its own context and vars, so this is ok + disposables.add( + this.voidSettingsService.onDidChangeState(() => { // we might want to debounce this + const newVals = relevantVals() + if (!eq(prevVals, newVals)) { + refresh() + prevVals = newVals + } + }) + ) + } + } + + // on mount (when get init settings state), and if a relevant feature flag changes (detected natively right now by refreshing if any flag changes), start refreshing models + voidSettingsService.waitForInitState.then(() => { + startRefreshing() + this._register( + voidSettingsService.onDidChangeState((type) => { if (type === 'featureFlagSettings') startRefreshing() }) + ) + }) } - state: RefreshModelState = 'done' - - private _timeoutId: NodeJS.Timeout | null = null - private _cancelTimeout = () => { - if (this._timeoutId) { - clearTimeout(this._timeoutId) - this._timeoutId = null - } + state: RefreshModelStateOfProvider = { + ollama: { state: 'init', timeoutId: null }, + openAICompatible: { state: 'init', timeoutId: null }, } - async refreshOllamaModels() { - // cancel any existing poll - this._cancelTimeout() - // if ollama is disabled, obivously done - if (!this.voidSettingsService.state.settingsOfProvider.ollama.enabled) { - this._setState('done') - return - } + + // start listening for models (and don't stop until success) + async refreshModels(providerName: RefreshableProviderName, options?: { enableProviderOnSuccess?: boolean }) { + this._clearProviderTimeout(providerName) // start loading models - this._setState('loading') + this._setRefreshState(providerName, 'refreshing') - this.llmMessageService.ollamaList({ + const fn = providerName === 'ollama' ? this.llmMessageService.ollamaList + : providerName === 'openAICompatible' ? this.llmMessageService.openAICompatibleList + : () => { } + + fn({ onSuccess: ({ models }) => { - this.voidSettingsService.setDefaultModels('ollama', models.map(model => model.name)) - this._setState('done') + this.voidSettingsService.setDefaultModels(providerName, models.map(model => { + if (providerName === 'ollama') return (model as OllamaModelResponse).name + else if (providerName === 'openAICompatible') return (model as OpenaiCompatibleModelResponse).id + else throw new Error('refreshMode fn: unknown provider', providerName) + })) + + if (options?.enableProviderOnSuccess) + this.voidSettingsService.setSettingOfProvider(providerName, 'enabled', true) + + this._setRefreshState(providerName, 'success') }, onError: ({ error }) => { // poll - console.log('retrying ollamaList:', error) - this._timeoutId = setTimeout(() => this.refreshOllamaModels(), 5000) + console.log('retrying list models:', providerName, error) + const timeoutId = setTimeout(() => this.refreshModels(providerName, options), REFRESH_INTERVAL) + this._setTimeoutId(providerName, timeoutId) } }) } - private _setState(state: RefreshModelState) { - this.state = state - this._onDidChangeState.fire() + _clearAllTimeouts() { + for (const providerName of refreshableProviderNames) { + this._clearProviderTimeout(providerName) + } + } + + _clearProviderTimeout(providerName: RefreshableProviderName) { + // cancel any existing poll + if (this.state[providerName].timeoutId) { + clearTimeout(this.state[providerName].timeoutId) + this._setTimeoutId(providerName, null) + } + } + + private _setTimeoutId(providerName: RefreshableProviderName, timeoutId: NodeJS.Timeout | null) { + this.state[providerName].timeoutId = timeoutId + } + + private _setRefreshState(providerName: RefreshableProviderName, state: RefreshableState['state']) { + this.state[providerName].state = state + this._onDidChangeState.fire(providerName) } } diff --git a/src/vs/platform/void/common/voidSettingsService.ts b/src/vs/platform/void/common/voidSettingsService.ts index ab9d28f8..0c0ceb28 100644 --- a/src/vs/platform/void/common/voidSettingsService.ts +++ b/src/vs/platform/void/common/voidSettingsService.ts @@ -10,7 +10,7 @@ import { IEncryptionService } from '../../encryption/common/encryptionService.js import { registerSingleton, InstantiationType } from '../../instantiation/common/extensions.js'; import { createDecorator } from '../../instantiation/common/instantiation.js'; import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js'; -import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultNames, ModelInfo } from './voidSettingsTypes.js'; +import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultNames, VoidModelInfo, FeatureFlagSettings, FeatureFlagName, defaultFeatureFlagSettings } from './voidSettingsTypes.js'; const STORAGE_KEY = 'void.voidSettingsI' @@ -21,13 +21,13 @@ type SetSettingOfProviderFn = ( newVal: SettingsOfProvider[ProviderName][S extends keyof SettingsOfProvider[ProviderName] ? S : never], ) => Promise; -type SetModelSelectionOfFeature = ( +type SetModelSelectionOfFeatureFn = ( featureName: K, newVal: ModelSelectionOfFeature[K], options?: { doNotApplyEffects?: true } ) => Promise; - +type SetFeatureFlagFn = (flagName: FeatureFlagName, newVal: boolean) => void; export type ModelOption = { text: string, value: ModelSelection } @@ -36,18 +36,24 @@ export type ModelOption = { text: string, value: ModelSelection } export type VoidSettingsState = { readonly settingsOfProvider: SettingsOfProvider; // optionsOfProvider readonly modelSelectionOfFeature: ModelSelectionOfFeature; // stateOfFeature + readonly featureFlagSettings: FeatureFlagSettings; readonly _modelOptions: ModelOption[] // computed based on the two above items } +type EventProp = Exclude | 'all' export interface IVoidSettingsService { readonly _serviceBrand: undefined; - readonly state: VoidSettingsState; - onDidChangeState: Event; + readonly state: VoidSettingsState; // in order to play nicely with react, you should immutably change state + readonly waitForInitState: Promise; + + onDidChangeState: Event; + setSettingOfProvider: SetSettingOfProviderFn; - setModelSelectionOfFeature: SetModelSelectionOfFeature; + setModelSelectionOfFeature: SetModelSelectionOfFeatureFn; + setFeatureFlag: SetFeatureFlagFn; setDefaultModels(providerName: ProviderName, modelNames: string[]): void; toggleModelHidden(providerName: ProviderName, modelName: string): void; @@ -74,6 +80,7 @@ const defaultState = () => { const d: VoidSettingsState = { settingsOfProvider: deepClone(defaultSettingsOfProvider), modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null }, + featureFlagSettings: deepClone(defaultFeatureFlagSettings), _modelOptions: _computeModelOptions(defaultSettingsOfProvider), // computed } return d @@ -84,10 +91,11 @@ export const IVoidSettingsService = createDecorator('VoidS class VoidSettingsService extends Disposable implements IVoidSettingsService { _serviceBrand: undefined; - private readonly _onDidChangeState = new Emitter(); - readonly onDidChangeState: Event = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes + private readonly _onDidChangeState = new Emitter(); + readonly onDidChangeState: Event = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes state: VoidSettingsState; + waitForInitState: Promise // await this if you need a valid state initially constructor( @IStorageService private readonly _storageService: IStorageService, @@ -100,10 +108,14 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { // at the start, we haven't read the partial config yet, but we need to set state to something this.state = defaultState() + let resolver: () => void = () => { } + this.waitForInitState = new Promise((res, rej) => resolver = res) + // read and update the actual state immediately this._readState().then(s => { this.state = s - this._onDidChangeState.fire() + resolver() + this._onDidChangeState.fire('all') }) } @@ -136,6 +148,8 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { } } + const newFeatureFlags = this.state.featureFlagSettings + // if changed models or enabled a provider, recompute models list const modelsListChanged = settingName === 'models' || settingName === 'enabled' const newModelsList = modelsListChanged ? _computeModelOptions(newSettingsOfProvider) : this.state._modelOptions @@ -143,6 +157,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { const newState: VoidSettingsState = { modelSelectionOfFeature: newModelSelectionOfFeature, settingsOfProvider: newSettingsOfProvider, + featureFlagSettings: newFeatureFlags, _modelOptions: newModelsList, } @@ -166,11 +181,26 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { } await this._storeState() - this._onDidChangeState.fire() + this._onDidChangeState.fire('settingsOfProvider') } - setModelSelectionOfFeature: SetModelSelectionOfFeature = async (featureName, newVal, options) => { + setFeatureFlag: SetFeatureFlagFn = async (flagName, newVal) => { + const newState = { + ...this.state, + featureFlagSettings: { + ...this.state.featureFlagSettings, + [flagName]: newVal + } + } + this.state = newState + await this._storeState() + this._onDidChangeState.fire('featureFlagSettings') + + } + + + setModelSelectionOfFeature: SetModelSelectionOfFeatureFn = async (featureName, newVal, options) => { const newState: VoidSettingsState = { ...this.state, modelSelectionOfFeature: { @@ -185,7 +215,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { return await this._storeState() - this._onDidChangeState.fire() + this._onDidChangeState.fire('modelSelectionOfFeature') } @@ -203,7 +233,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { const { models } = this.state.settingsOfProvider[providerName] const modelIdx = models.findIndex(m => m.modelName === modelName) if (modelIdx === -1) return - const newModels: ModelInfo[] = [ + const newModels: VoidModelInfo[] = [ ...models.slice(0, modelIdx), { ...models[modelIdx], isHidden: !models[modelIdx].isHidden }, ...models.slice(modelIdx + 1, Infinity) diff --git a/src/vs/platform/void/common/voidSettingsTypes.ts b/src/vs/platform/void/common/voidSettingsTypes.ts index 6de2f47f..7aa1eb78 100644 --- a/src/vs/platform/void/common/voidSettingsTypes.ts +++ b/src/vs/platform/void/common/voidSettingsTypes.ts @@ -7,14 +7,14 @@ -export type ModelInfo = { +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 } -export const modelInfoOfDefaultNames = (modelNames: string[]): ModelInfo[] => { +export const modelInfoOfDefaultNames = (modelNames: string[]): VoidModelInfo[] => { const isHidden = modelNames.length >= 10 // hide all models if there are a ton of them, and make user enable them individually return modelNames.map((modelName, i) => ({ modelName, isDefault: true, isHidden })) } @@ -96,7 +96,7 @@ type UnionOfKeys = T extends T ? keyof T : never; -export const customProviderSettingsDefaults = { +export const customProviderSettings = { anthropic: { apiKey: '', }, @@ -121,22 +121,23 @@ export const customProviderSettingsDefaults = { } } as const -export type ProviderName = keyof typeof customProviderSettingsDefaults -export const providerNames = Object.keys(customProviderSettingsDefaults) as ProviderName[] + +export type ProviderName = keyof typeof customProviderSettings +export const providerNames = Object.keys(customProviderSettings) as ProviderName[] -type CustomSettingName = UnionOfKeys +type CustomSettingName = UnionOfKeys type CustomProviderSettings = { - [k in CustomSettingName]: k extends keyof typeof customProviderSettingsDefaults[providerName] ? string : undefined + [k in CustomSettingName]: k extends keyof typeof customProviderSettings[providerName] ? string : undefined } type CommonProviderSettings = { - enabled: boolean, - models: ModelInfo[], // if null, user can type in any string as a model + enabled: boolean | undefined, // undefined initially + models: VoidModelInfo[], } -type SettingsForProvider = CustomProviderSettings & CommonProviderSettings +export type SettingsForProvider = CustomProviderSettings & CommonProviderSettings // part of state export type SettingsOfProvider = { @@ -148,6 +149,14 @@ export type SettingName = keyof SettingsForProvider + +export const customSettingNamesOfProvider = (providerName: ProviderName) => { + return Object.keys(customProviderSettings[providerName]) as CustomSettingName[] +} + + + + export const titleOfProviderName = (providerName: ProviderName) => { if (providerName === 'anthropic') return 'Anthropic' @@ -170,6 +179,9 @@ export const titleOfProviderName = (providerName: ProviderName) => { type DisplayInfo = { title: string, placeholder: string, + + helpfulUrl?: string, + urlPurpose?: string, } export const displayInfoOfSettingName = (providerName: ProviderName, settingName: SettingName): DisplayInfo => { if (settingName === 'apiKey') { @@ -182,6 +194,16 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName providerName === 'groq' ? 'gsk_key...' : providerName === 'openAICompatible' ? 'sk-key...' : '(never)', + + helpfulUrl: providerName === 'anthropic' ? 'https://console.anthropic.com/settings/keys' : + providerName === 'openAI' ? 'https://platform.openai.com/api-keys' : + providerName === 'openRouter' ? 'https://openrouter.ai/settings/keys' : + providerName === 'gemini' ? 'https://aistudio.google.com/apikey' : + providerName === 'groq' ? 'https://console.groq.com/keys' : + providerName === 'openAICompatible' ? undefined : + undefined, + + urlPurpose: 'to get your API key.', } } else if (settingName === 'endpoint') { @@ -189,9 +211,16 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName title: providerName === 'ollama' ? 'Your Ollama endpoint' : providerName === 'openAICompatible' ? 'baseURL' // (do not include /chat/completions) : '(never)', - placeholder: providerName === 'ollama' ? customProviderSettingsDefaults.ollama.endpoint + + placeholder: providerName === 'ollama' ? customProviderSettings.ollama.endpoint : providerName === 'openAICompatible' ? 'https://my-website.com/v1' : '(never)', + + helpfulUrl: providerName === 'ollama' ? 'https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network' + : providerName === 'openAICompatible' ? undefined + : undefined, + + urlPurpose: 'for more information.', } } else if (settingName === 'enabled') { @@ -247,46 +276,46 @@ export const voidInitModelOptions = { // used when waiting and for a type reference export const defaultSettingsOfProvider: SettingsOfProvider = { anthropic: { + enabled: undefined, ...defaultCustomSettings, - ...customProviderSettingsDefaults.anthropic, + ...customProviderSettings.anthropic, ...voidInitModelOptions.anthropic, - enabled: false, }, openAI: { + enabled: undefined, ...defaultCustomSettings, - ...customProviderSettingsDefaults.openAI, + ...customProviderSettings.openAI, ...voidInitModelOptions.openAI, - enabled: false, }, gemini: { ...defaultCustomSettings, - ...customProviderSettingsDefaults.gemini, + ...customProviderSettings.gemini, ...voidInitModelOptions.gemini, - enabled: false, + enabled: undefined, }, groq: { ...defaultCustomSettings, - ...customProviderSettingsDefaults.groq, + ...customProviderSettings.groq, ...voidInitModelOptions.groq, - enabled: false, + enabled: undefined, }, ollama: { ...defaultCustomSettings, - ...customProviderSettingsDefaults.ollama, + ...customProviderSettings.ollama, ...voidInitModelOptions.ollama, - enabled: false, + enabled: undefined, }, openRouter: { ...defaultCustomSettings, - ...customProviderSettingsDefaults.openRouter, + ...customProviderSettings.openRouter, ...voidInitModelOptions.openRouter, - enabled: false, + enabled: undefined, }, openAICompatible: { ...defaultCustomSettings, - ...customProviderSettingsDefaults.openAICompatible, + ...customProviderSettings.openAICompatible, ...voidInitModelOptions.openAICompatible, - enabled: false, + enabled: undefined, }, } @@ -306,3 +335,35 @@ export type ModelSelectionOfFeature = { export type FeatureName = keyof ModelSelectionOfFeature export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete'] as const + + + + + + + + + +export type FeatureFlagSettings = { + autoRefreshModels: boolean; // automatically scan for local models and enable when found +} +export const defaultFeatureFlagSettings: FeatureFlagSettings = { + autoRefreshModels: true, +} + +export type FeatureFlagName = keyof FeatureFlagSettings +export const featureFlagNames = Object.keys(defaultFeatureFlagSettings) as FeatureFlagName[] + +type FeatureFlagDisplayInfo = { + description: string, +} +export const displayInfoOfFeatureFlag = (featureFlag: FeatureFlagName): FeatureFlagDisplayInfo => { + if (featureFlag === 'autoRefreshModels') { + return { + description: 'Automatically scan for and enable local models.', + } + } + throw new Error(`featureFlagInfo: Unknown feature flag: "${featureFlag}"`) +} + + diff --git a/src/vs/platform/void/electron-main/llmMessage/ollama.ts b/src/vs/platform/void/electron-main/llmMessage/ollama.ts index 03cf29dc..d9184157 100644 --- a/src/vs/platform/void/electron-main/llmMessage/ollama.ts +++ b/src/vs/platform/void/electron-main/llmMessage/ollama.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { Ollama } from 'ollama'; -import { _InternalOllamaListFnType, _InternalSendLLMMessageFnType, ModelResponse } from '../../common/llmMessageTypes.js'; +import { _InternalModelListFnType, _InternalSendLLMMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js'; -export const ollamaList: _InternalOllamaListFnType = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => { +export const ollamaList: _InternalModelListFnType = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => { - const onSuccess = ({ models }: { models: ModelResponse[] }) => { + const onSuccess = ({ models }: { models: OllamaModelResponse[] }) => { onSuccess_({ models }) } @@ -16,7 +16,6 @@ export const ollamaList: _InternalOllamaListFnType = async ({ onSuccess: onSucce onError_({ error }) } - try { const thisConfig = settingsOfProvider.ollama const ollama = new Ollama({ host: thisConfig.endpoint }) diff --git a/src/vs/platform/void/electron-main/llmMessage/openai.ts b/src/vs/platform/void/electron-main/llmMessage/openai.ts index 03eb7761..fa6ac5f3 100644 --- a/src/vs/platform/void/electron-main/llmMessage/openai.ts +++ b/src/vs/platform/void/electron-main/llmMessage/openai.ts @@ -4,10 +4,46 @@ *--------------------------------------------------------------------------------------------*/ import OpenAI from 'openai'; -import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js'; +import { _InternalModelListFnType, _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js'; +import { Model } from 'openai/resources/models.js'; // import { parseMaxTokensStr } from './util.js'; + +export const openaiCompatibleList: _InternalModelListFnType = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => { + const onSuccess = ({ models }: { models: Model[] }) => { + onSuccess_({ models }) + } + + const onError = ({ error }: { error: string }) => { + onError_({ error }) + } + + try { + const thisConfig = settingsOfProvider.openAICompatible + const openai = new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true }) + + openai.models.list() + .then(async (response) => { + const models: Model[] = [] + models.push(...response.data) + while (response.hasNextPage()) { + models.push(...(await response.getNextPage()).data) + } + onSuccess({ models }) + }) + .catch((error) => { + onError({ error: error + '' }) + }) + } + catch (error) { + onError({ error: error + '' }) + } +} + + + + // OpenAI, OpenRouter, OpenAICompatible export const sendOpenAIMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) => { @@ -43,6 +79,7 @@ export const sendOpenAIMsg: _InternalSendLLMMessageFnType = ({ messages, onText, throw new Error(`providerName was invalid: ${providerName}`) } + openai.models.list() openai.chat.completions .create(options) .then(async response => { diff --git a/src/vs/platform/void/electron-main/llmMessageChannel.ts b/src/vs/platform/void/electron-main/llmMessageChannel.ts index 803539d9..bf08934e 100644 --- a/src/vs/platform/void/electron-main/llmMessageChannel.ts +++ b/src/vs/platform/void/electron-main/llmMessageChannel.ts @@ -8,10 +8,11 @@ import { IServerChannel } from '../../../base/parts/ipc/common/ipc.js'; import { Emitter, Event } from '../../../base/common/event.js'; -import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainLLMMessageParams, AbortRef, LLMMMessageParams, MainLLMMessageAbortParams, MainOllamaListParams, OllamaListParams, EventOllamaListOnSuccessParams, EventOllamaListOnErrorParams } from '../common/llmMessageTypes.js'; +import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainLLMMessageParams, AbortRef, LLMMMessageParams, MainLLMMessageAbortParams, MainModelListParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from '../common/llmMessageTypes.js'; import { sendLLMMessage } from './llmMessage/sendLLMMessage.js' import { IMetricsService } from '../common/metricsService.js'; import { ollamaList } from './llmMessage/ollama.js'; +import { openaiCompatibleList } from './llmMessage/openai.js'; // NODE IMPLEMENTATION - calls actual sendLLMMessage() and returns listeners to it @@ -25,8 +26,12 @@ export class LLMMessageChannel implements IServerChannel { private readonly _abortRefOfRequestId_llm: Record = {} // ollamaList - private readonly _onSuccess_ollama = new Emitter(); - private readonly _onError_ollama = new Emitter(); + private readonly _onSuccess_ollama = new Emitter>(); + private readonly _onError_ollama = new Emitter>(); + + // openaiCompatibleList + private readonly _onSuccess_openAICompatible = new Emitter>(); + private readonly _onError_openAICompatible = new Emitter>(); // stupidly, channels can't take in @IService constructor( @@ -50,6 +55,12 @@ export class LLMMessageChannel implements IServerChannel { else if (event === 'onError_ollama') { return this._onError_ollama.event; } + else if (event === 'onSuccess_openAICompatible') { + return this._onSuccess_openAICompatible.event; + } + else if (event === 'onError_openAICompatible') { + return this._onError_openAICompatible.event; + } else { throw new Error(`Event not found: ${event}`); } @@ -67,6 +78,9 @@ export class LLMMessageChannel implements IServerChannel { else if (command === 'ollamaList') { this._callOllamaList(params) } + else if (command === 'openAICompatibleList') { + this._callOpenAICompatibleList(params) + } else { throw new Error(`Void sendLLM: command "${command}" not recognized.`) } @@ -100,10 +114,10 @@ export class LLMMessageChannel implements IServerChannel { delete this._abortRefOfRequestId_llm[requestId] } - private _callOllamaList(params: MainOllamaListParams) { + private _callOllamaList(params: MainModelListParams) { const { requestId } = params; - const mainThreadParams: OllamaListParams = { + const mainThreadParams: ModelListParams = { ...params, onSuccess: ({ models }) => { this._onSuccess_ollama.fire({ requestId, models }); }, onError: ({ error }) => { this._onError_ollama.fire({ requestId, error }); }, @@ -111,5 +125,16 @@ export class LLMMessageChannel implements IServerChannel { ollamaList(mainThreadParams) } + private _callOpenAICompatibleList(params: MainModelListParams) { + const { requestId } = params; + + const mainThreadParams: ModelListParams = { + ...params, + onSuccess: ({ models }) => { this._onSuccess_openAICompatible.fire({ requestId, models }); }, + onError: ({ error }) => { this._onError_openAICompatible.fire({ requestId, error }); }, + } + openaiCompatibleList(mainThreadParams) + } + } diff --git a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts index db8ede5f..88c9cf34 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupWatermark.ts @@ -4,82 +4,112 @@ *--------------------------------------------------------------------------------------------*/ import { localize } from '../../../../nls.js'; -import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; -import { isMacintosh, isWeb, OS } from '../../../../base/common/platform.js'; +import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { append, clearNode, $, h } from '../../../../base/browser/dom.js'; import { KeybindingLabel } from '../../../../base/browser/ui/keybindingLabel/keybindingLabel.js'; -import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; -import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { defaultKeybindingLabelStyles } from '../../../../platform/theme/browser/defaultStyles.js'; import { editorForeground, registerColor, transparent } from '../../../../platform/theme/common/colorRegistry.js'; +import { IThemeService } from '../../../../platform/theme/common/themeService.js'; +import { ColorScheme } from '../../../../platform/theme/common/theme.js'; +import { isRecentFolder, IWorkspacesService } from '../../../../platform/workspaces/common/workspaces.js'; +// import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { OpenFileFolderAction, OpenFolderAction } from '../../actions/workspaceActions.js'; +import { isMacintosh, isNative, OS } from '../../../../base/common/platform.js'; +import { VOID_CTRL_L_ACTION_ID } from '../../../contrib/void/browser/sidebarActions.js'; +import { VOID_CTRL_K_ACTION_ID } from '../../../contrib/void/browser/quickEditActions.js'; +import { defaultKeybindingLabelStyles } from '../../../../platform/theme/browser/defaultStyles.js'; +import { IWindowOpenable } from '../../../../platform/window/common/window.js'; +import { ILabelService, Verbosity } from '../../../../platform/label/common/label.js'; +import { splitRecentLabel } from '../../../../base/common/labels.js'; +import { IHostService } from '../../../services/host/browser/host.js'; +// import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; registerColor('editorWatermark.foreground', { dark: transparent(editorForeground, 0.6), light: transparent(editorForeground, 0.68), hcDark: editorForeground, hcLight: editorForeground }, localize('editorLineHighlight', 'Foreground color for the labels in the editor watermark.')); -interface WatermarkEntry { - readonly text: string; - readonly id: string; - readonly mac?: boolean; - readonly when?: ContextKeyExpression; -} +// interface WatermarkEntry { +// readonly text: string; +// readonly id: string; +// readonly mac?: boolean; +// readonly when?: ContextKeyExpression; +// } -const showCommands: WatermarkEntry = { text: localize('watermark.showCommands', "Show All Commands"), id: 'workbench.action.showCommands' }; -const quickAccess: WatermarkEntry = { text: localize('watermark.quickAccess', "Go to File"), id: 'workbench.action.quickOpen' }; -const openFileNonMacOnly: WatermarkEntry = { text: localize('watermark.openFile', "Open File"), id: 'workbench.action.files.openFile', mac: false }; -const openFolderNonMacOnly: WatermarkEntry = { text: localize('watermark.openFolder', "Open Folder"), id: 'workbench.action.files.openFolder', mac: false }; -const openFileOrFolderMacOnly: WatermarkEntry = { text: localize('watermark.openFileFolder', "Open File or Folder"), id: 'workbench.action.files.openFileFolder', mac: true }; -const openRecent: WatermarkEntry = { text: localize('watermark.openRecent', "Open Recent"), id: 'workbench.action.openRecent' }; -const newUntitledFileMacOnly: WatermarkEntry = { text: localize('watermark.newUntitledFile', "New Untitled Text File"), id: 'workbench.action.files.newUntitledFile', mac: true }; -const findInFiles: WatermarkEntry = { text: localize('watermark.findInFiles', "Find in Files"), id: 'workbench.action.findInFiles' }; -const toggleTerminal: WatermarkEntry = { text: localize({ key: 'watermark.toggleTerminal', comment: ['toggle is a verb here'] }, "Toggle Terminal"), id: 'workbench.action.terminal.toggleTerminal', when: ContextKeyExpr.equals('terminalProcessSupported', true) }; -const startDebugging: WatermarkEntry = { text: localize('watermark.startDebugging', "Start Debugging"), id: 'workbench.action.debug.start', when: ContextKeyExpr.equals('terminalProcessSupported', true) }; -const toggleFullscreen: WatermarkEntry = { text: localize({ key: 'watermark.toggleFullscreen', comment: ['toggle is a verb here'] }, "Toggle Full Screen"), id: 'workbench.action.toggleFullScreen' }; -const showSettings: WatermarkEntry = { text: localize('watermark.showSettings', "Show Settings"), id: 'workbench.action.openSettings' }; +// const showCommands: WatermarkEntry = { text: localize('watermark.showCommands', "Show All Commands"), id: 'workbench.action.showCommands' }; +// const quickAccess: WatermarkEntry = { text: localize('watermark.quickAccess', "Go to File"), id: 'workbench.action.quickOpen' }; +// const openFileNonMacOnly: WatermarkEntry = { text: localize('watermark.openFile', "Open File"), id: 'workbench.action.files.openFile', mac: false }; +// const openFolderNonMacOnly: WatermarkEntry = { text: localize('watermark.openFolder', "Open Folder"), id: 'workbench.action.files.openFolder', mac: false }; +// const openFileOrFolderMacOnly: WatermarkEntry = { text: localize('watermark.openFileFolder', "Open File or Folder"), id: 'workbench.action.files.openFileFolder', mac: true }; +// const openRecent: WatermarkEntry = { text: localize('watermark.openRecent', "Open Recent"), id: 'workbench.action.openRecent' }; +// const newUntitledFileMacOnly: WatermarkEntry = { text: localize('watermark.newUntitledFile', "New Untitled Text File"), id: 'workbench.action.files.newUntitledFile', mac: true }; +// const findInFiles: WatermarkEntry = { text: localize('watermark.findInFiles', "Find in Files"), id: 'workbench.action.findInFiles' }; +// const toggleTerminal: WatermarkEntry = { text: localize({ key: 'watermark.toggleTerminal', comment: ['toggle is a verb here'] }, "Toggle Terminal"), id: 'workbench.action.terminal.toggleTerminal', when: ContextKeyExpr.equals('terminalProcessSupported', true) }; +// const startDebugging: WatermarkEntry = { text: localize('watermark.startDebugging', "Start Debugging"), id: 'workbench.action.debug.start', when: ContextKeyExpr.equals('terminalProcessSupported', true) }; +// const toggleFullscreen: WatermarkEntry = { text: localize({ key: 'watermark.toggleFullscreen', comment: ['toggle is a verb here'] }, "Toggle Full Screen"), id: 'workbench.action.toggleFullScreen' }; +// const showSettings: WatermarkEntry = { text: localize('watermark.showSettings', "Show Settings"), id: 'workbench.action.openSettings' }; -const noFolderEntries = [ - showCommands, - openFileNonMacOnly, - openFolderNonMacOnly, - openFileOrFolderMacOnly, - openRecent, - newUntitledFileMacOnly -]; +// // shown when Void is emtpty +// const noFolderEntries = [ +// // showCommands, +// openFileNonMacOnly, +// openFolderNonMacOnly, +// openFileOrFolderMacOnly, +// openRecent, +// // newUntitledFileMacOnly +// ]; -const folderEntries = [ - showCommands, - quickAccess, - findInFiles, - startDebugging, - toggleTerminal, - toggleFullscreen, - showSettings -]; +// const folderEntries = [ +// showCommands, +// // quickAccess, +// // findInFiles, +// // startDebugging, +// // toggleTerminal, +// // toggleFullscreen, +// // showSettings +// ]; export class EditorGroupWatermark extends Disposable { private readonly shortcuts: HTMLElement; private readonly transientDisposables = this._register(new DisposableStore()); - private enabled: boolean = false; + // private enabled: boolean = false; private workbenchState: WorkbenchState; - private keybindingLabels = new Set(); + private currentDisposables = new Set(); constructor( container: HTMLElement, @IKeybindingService private readonly keybindingService: IKeybindingService, @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IConfigurationService private readonly configurationService: IConfigurationService + // @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IThemeService private readonly themeService: IThemeService, + @IWorkspacesService private readonly workspacesService: IWorkspacesService, + @ICommandService private readonly commandService: ICommandService, + @IHostService private readonly hostService: IHostService, + @ILabelService private readonly labelService: ILabelService, ) { super(); const elements = h('.editor-group-watermark', [ - h('.letterpress'), + h('.letterpress@icon'), h('.shortcuts@shortcuts'), ]); append(container, elements.root); - this.shortcuts = elements.shortcuts; + this.shortcuts = elements.shortcuts; // shortcuts div is modified on render() + + // void icon style + const updateTheme = () => { + const theme = this.themeService.getColorTheme().type + const isDark = theme === ColorScheme.DARK || theme === ColorScheme.HIGH_CONTRAST_DARK + elements.icon.style.maxWidth = '220px' + elements.icon.style.opacity = '50%' + elements.icon.style.filter = isDark ? 'brightness(.5)' : 'invert(1)' + } + updateTheme() + this._register( + this.themeService.onDidColorThemeChange(updateTheme) + ) this.registerListeners(); @@ -103,56 +133,164 @@ export class EditorGroupWatermark extends Disposable { this.render(); })); - const allEntriesWhenClauses = [...noFolderEntries, ...folderEntries].filter(entry => entry.when !== undefined).map(entry => entry.when!); - const allKeys = new Set(); - allEntriesWhenClauses.forEach(when => when.keys().forEach(key => allKeys.add(key))); - this._register(this.contextKeyService.onDidChangeContext(e => { - if (e.affectsSome(allKeys)) { - this.render(); - } - })); + // const allEntriesWhenClauses = [...noFolderEntries, ...folderEntries].filter(entry => entry.when !== undefined).map(entry => entry.when!); + // const allKeys = new Set(); + // allEntriesWhenClauses.forEach(when => when.keys().forEach(key => allKeys.add(key))); + // this._register(this.contextKeyService.onDidChangeContext(e => { + // if (e.affectsSome(allKeys)) { + // this.render(); + // } + // })); } + + private render(): void { - const enabled = this.configurationService.getValue('workbench.tips.enabled'); + // const enabled = this.configurationService.getValue('workbench.tips.enabled'); - if (enabled === this.enabled) { - return; - } + // if (enabled === this.enabled) { + // return; + // } + + // this.enabled = enabled; + + + // if (!enabled) { + // return; + // } + + // const hasFolder = this.workbenchState !== WorkbenchState.EMPTY; + // const selected = (hasFolder ? folderEntries : noFolderEntries) + // .filter(entry => !('when' in entry) || this.contextKeyService.contextMatchesRules(entry.when)) + // .filter(entry => !('mac' in entry) || entry.mac === (isMacintosh && !isWeb)) + // .filter(entry => !!CommandsRegistry.getCommand(entry.id)) + // .filter(entry => !!this.keybindingService.lookupKeybinding(entry.id)); - this.enabled = enabled; this.clear(); - - if (!enabled) { - return; - } - const box = append(this.shortcuts, $('.watermark-box')); - const folder = this.workbenchState !== WorkbenchState.EMPTY; - const selected = (folder ? folderEntries : noFolderEntries) - .filter(entry => !('when' in entry) || this.contextKeyService.contextMatchesRules(entry.when)) - .filter(entry => !('mac' in entry) || entry.mac === (isMacintosh && !isWeb)) - .filter(entry => !!CommandsRegistry.getCommand(entry.id)) - .filter(entry => !!this.keybindingService.lookupKeybinding(entry.id)); + const boxBelow = append(this.shortcuts, $('')) + + + const update = async () => { - const update = () => { clearNode(box); - this.keybindingLabels.forEach(label => label.dispose()); - this.keybindingLabels.clear(); + clearNode(boxBelow); - for (const entry of selected) { - const keys = this.keybindingService.lookupKeybinding(entry.id); - if (!keys) { - continue; + this.currentDisposables.forEach(label => label.dispose()); + this.currentDisposables.clear(); + + + // Void - if the workbench is empty, show open + if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) { + + // Open Folder + const button = h('button') + button.root.textContent = 'Open Folder' + button.root.onclick = () => { + this.commandService.executeCommand(isMacintosh && isNative ? OpenFileFolderAction.ID : OpenFolderAction.ID) + // if (this.contextKeyService.contextMatchesRules(ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace')))) { + // this.commandService.executeCommand(OpenFolderViaWorkspaceAction.ID); + // } else { + // this.commandService.executeCommand(isMacintosh ? 'workbench.action.files.openFileFolder' : 'workbench.action.files.openFolder'); + // } } + box.appendChild(button.root); + + // Recents + const recentlyOpened = await this.workspacesService.getRecentlyOpened() + .catch(() => ({ files: [], workspaces: [] })).then(w => w.workspaces); + + + + box.append( + ...recentlyOpened.map(w => { + + let fullPath: string; + let windowOpenable: IWindowOpenable; + if (isRecentFolder(w)) { + windowOpenable = { folderUri: w.folderUri }; + fullPath = w.label || this.labelService.getWorkspaceLabel(w.folderUri, { verbose: Verbosity.LONG }); + } + else { + return null + // fullPath = w.label || this.labelService.getWorkspaceLabel(w.workspace, { verbose: Verbosity.LONG }); + // windowOpenable = { workspaceUri: w.workspace.configPath }; + } + + + + const { name, parentPath } = splitRecentLabel(fullPath); + + const li = $('li'); + const link = $('button.button-link'); + + link.innerText = name; + link.title = fullPath; + link.setAttribute('aria-label', localize('welcomePage.openFolderWithPath', "Open folder {0} with path {1}", name, parentPath)); + link.addEventListener('click', e => { + this.hostService.openWindow([windowOpenable], { + forceNewWindow: e.ctrlKey || e.metaKey, + remoteAuthority: w.remoteAuthority || null // local window if remoteAuthority is not set or can not be deducted from the openable + }); + e.preventDefault(); + e.stopPropagation(); + }); + li.appendChild(link); + + const span = $('span'); + span.classList.add('path'); + span.classList.add('detail'); + span.innerText = parentPath; + span.title = fullPath; + li.appendChild(span); + + + return li + }).filter(v => !!v) + ) + + + + } + else { + + // show them Void keybindings + const keys = this.keybindingService.lookupKeybinding(VOID_CTRL_L_ACTION_ID); const dl = append(box, $('dl')); const dt = append(dl, $('dt')); - dt.textContent = entry.text; + dt.textContent = 'Chat' const dd = append(dl, $('dd')); const label = new KeybindingLabel(dd, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles }); - label.set(keys); - this.keybindingLabels.add(label); + if (keys) + label.set(keys); + this.currentDisposables.add(label); + + + const keys2 = this.keybindingService.lookupKeybinding(VOID_CTRL_K_ACTION_ID); + const dl2 = append(box, $('dl')); + const dt2 = append(dl2, $('dt')); + dt2.textContent = 'Quick Edit' + const dd2 = append(dl2, $('dd')); + const label2 = new KeybindingLabel(dd2, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles }); + if (keys2) + label2.set(keys2); + this.currentDisposables.add(label2); + + const keys3 = this.keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings'); + const button3 = append(boxBelow, $('button')); + button3.textContent = 'Change Keybindings' + const label3 = new KeybindingLabel(button3, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles }); + if (keys3) + label3.set(keys3); + button3.onclick = () => { + this.commandService.executeCommand('workbench.action.openGlobalKeybindings') + } + this.currentDisposables.add(label3); + + + } + }; update(); @@ -167,6 +305,6 @@ export class EditorGroupWatermark extends Disposable { override dispose(): void { super.dispose(); this.clear(); - this.keybindingLabels.forEach(label => label.dispose()); + this.currentDisposables.forEach(label => label.dispose()); } } diff --git a/src/vs/workbench/browser/parts/editor/media/editorgroupview.css b/src/vs/workbench/browser/parts/editor/media/editorgroupview.css index 76c8a561..726a39c9 100644 --- a/src/vs/workbench/browser/parts/editor/media/editorgroupview.css +++ b/src/vs/workbench/browser/parts/editor/media/editorgroupview.css @@ -9,13 +9,15 @@ height: 100%; } -.monaco-workbench .part.editor > .content .editor-group-container.empty { - opacity: 0.5; /* dimmed to indicate inactive state */ +.monaco-workbench .part.editor > .content .editor-group-container.empty { + opacity: 0.5; + /* dimmed to indicate inactive state */ } .monaco-workbench .part.editor > .content .editor-group-container.empty.active, .monaco-workbench .part.editor > .content .editor-group-container.empty.dragged-over { - opacity: 1; /* indicate active/dragged-over group through undimmed state */ + opacity: 1; + /* indicate active/dragged-over group through undimmed state */ } .monaco-workbench .part.editor > .content:not(.empty) .editor-group-container.empty.active:focus { @@ -24,12 +26,13 @@ } .monaco-workbench .part.editor > .content.empty .editor-group-container.empty.active:focus { - outline: none; /* never show outline for empty group if it is the last */ + outline: none; + /* never show outline for empty group if it is the last */ } /* Watermark & shortcuts */ -.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark { +.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark { display: flex; height: 100%; max-width: 290px; @@ -49,26 +52,27 @@ height: calc(100% - 70px); } +/* light */ .monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark > .letterpress { width: 100%; max-height: 100%; aspect-ratio: 1/1; - background-image: url('./letterpress-light.svg'); + background-image: url('./void_cube_noshadow.png'); background-size: contain; background-position-x: center; background-repeat: no-repeat; } .monaco-workbench.vs-dark .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress { - background-image: url('./letterpress-dark.svg'); + background-image: url('./void_cube_noshadow.png'); } .monaco-workbench.hc-light .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress { - background-image: url('./letterpress-hcLight.svg'); + background-image: url('./void_cube_noshadow.png'); } .monaco-workbench.hc-black .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress { - background-image: url('./letterpress-hcDark.svg'); + background-image: url('./void_cube_noshadow.png'); } .monaco-workbench .part.editor > .content:not(.empty) .editor-group-container > .editor-group-watermark > .shortcuts, @@ -109,12 +113,13 @@ .monaco-workbench .part.editor > .content .editor-group-container > .title { position: relative; - box-sizing: border-box; + box-sizing: border-box; overflow: hidden; } .monaco-workbench .part.editor > .content .editor-group-container > .title:not(.tabs) { - display: flex; /* when tabs are not shown, use flex layout */ + display: flex; + /* when tabs are not shown, use flex layout */ flex-wrap: nowrap; } @@ -144,7 +149,8 @@ .monaco-workbench .part.editor > .content .editor-group-container.empty.locked > .editor-group-container-toolbar, .monaco-workbench .part.editor > .content:not(.empty) .editor-group-container.empty > .editor-group-container-toolbar, .monaco-workbench .part.editor > .content.auxiliary .editor-group-container.empty > .editor-group-container-toolbar { - display: block; /* show toolbar when more than one editor group or always when auxiliary or locked */ + display: block; + /* show toolbar when more than one editor group or always when auxiliary or locked */ } .monaco-workbench .part.editor > .content .editor-group-container > .editor-group-container-toolbar .actions-container { @@ -157,7 +163,7 @@ /* Editor */ -.monaco-workbench .part.editor > .content .editor-group-container.empty > .editor-container { +.monaco-workbench .part.editor > .content .editor-group-container.empty > .editor-container { display: none; } diff --git a/src/vs/workbench/browser/parts/editor/media/slice_of_void.png b/src/vs/workbench/browser/parts/editor/media/slice_of_void.png new file mode 100644 index 00000000..3b2c5319 Binary files /dev/null and b/src/vs/workbench/browser/parts/editor/media/slice_of_void.png differ diff --git a/src/vs/workbench/browser/parts/editor/media/void_cube_noshadow.png b/src/vs/workbench/browser/parts/editor/media/void_cube_noshadow.png new file mode 100644 index 00000000..225179f8 Binary files /dev/null and b/src/vs/workbench/browser/parts/editor/media/void_cube_noshadow.png differ diff --git a/src/vs/workbench/contrib/void/browser/quickEditActions.ts b/src/vs/workbench/contrib/void/browser/quickEditActions.ts new file mode 100644 index 00000000..fcef7270 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/quickEditActions.ts @@ -0,0 +1,28 @@ +import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; +import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; +import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; +import { IMetricsService } from '../../../../platform/void/common/metricsService.js'; + + +export const VOID_CTRL_K_ACTION_ID = 'void.ctrlKAction' +registerAction2(class extends Action2 { + constructor() { + super({ id: VOID_CTRL_K_ACTION_ID, title: 'Void: Quick Edit', keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyK, weight: KeybindingWeight.BuiltinExtension } }); + } + async run(accessor: ServicesAccessor): Promise { + console.log('hello111!') + + const model = accessor.get(ICodeEditorService).getActiveCodeEditor()?.getModel() + if (!model) + return + + console.log('hello!') + + const metricsService = accessor.get(IMetricsService) + metricsService.capture('User Action', { type: 'Ctrl+K' }) + + console.log('bye!') + } +}); diff --git a/src/vs/workbench/contrib/void/browser/react/src/styles.css b/src/vs/workbench/contrib/void/browser/react/src/styles.css index 9e0d47e7..2ceabda6 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/styles.css +++ b/src/vs/workbench/contrib/void/browser/react/src/styles.css @@ -8,6 +8,15 @@ @tailwind utilities; +@layer components { + .select-ellipsis select { + text-overflow: ellipsis; + white-space: nowrap; + padding-right: 24px; + } +} + + /* html { font-size: var(--vscode-font-size); } diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx index 504b88d2..70be6772 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx @@ -9,8 +9,6 @@ import { IInputBoxStyles, InputBox } from '../../../../../../../base/browser/ui/ import { defaultInputBoxStyles, defaultSelectBoxStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js'; import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js'; import { IDisposable } from '../../../../../../../base/common/lifecycle.js'; -import { DomScrollableElement } from '../../../../../../../base/browser/ui/scrollbar/scrollableElement.js'; -import { ScrollableElementCreationOptions } from '../../../../../../../base/browser/ui/scrollbar/scrollableElementOptions.js'; @@ -106,7 +104,7 @@ export const VoidSelectBox = ({ onChangeSelection, onCreateInstance, selectB let containerRef = useRef(null); return { containerRef.current = container diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx index 250363ce..234f8cd3 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/services.tsx @@ -6,12 +6,12 @@ import { useState, useEffect } from 'react' import { ThreadsState } from '../../../threadHistoryService.js' import { SettingsOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js' -import { RefreshModelState } from '../../../../../../../platform/void/common/refreshModelService.js' import { IDisposable } from '../../../../../../../base/common/lifecycle.js' import { ReactServicesType } from '../../../helpers/reactServicesHelper.js' import { VoidSidebarState } from '../../../sidebarStateService.js' import { VoidSettingsState } from '../../../../../../../platform/void/common/voidSettingsService.js' import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js' +import { RefreshableProviderName, RefreshModelStateOfProvider } from '../../../../../../../platform/void/common/refreshModelService.js' // normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes @@ -29,8 +29,9 @@ const threadsStateListeners: Set<(s: ThreadsState) => void> = new Set() let settingsState: VoidSettingsState const settingsStateListeners: Set<(s: VoidSettingsState) => void> = new Set() -let refreshModelState: RefreshModelState -const refreshModelStateListeners: Set<(s: RefreshModelState) => void> = new Set() +let refreshModelState: RefreshModelStateOfProvider +const refreshModelStateListeners: Set<(s: RefreshModelStateOfProvider) => void> = new Set() +const refreshModelProviderListeners: Set<(p: RefreshableProviderName, s: RefreshModelStateOfProvider) => void> = new Set() let colorThemeState: ColorScheme const colorThemeStateListeners: Set<(s: ColorScheme) => void> = new Set() @@ -78,9 +79,10 @@ export const _registerServices = (services_: ReactServicesType) => { refreshModelState = refreshModelService.state disposables.push( - refreshModelService.onDidChangeState(() => { + refreshModelService.onDidChangeState((providerName) => { refreshModelState = refreshModelService.state refreshModelStateListeners.forEach(l => l(refreshModelState)) + refreshModelProviderListeners.forEach(l => l(providerName, refreshModelState)) }) ) @@ -148,7 +150,12 @@ export const useRefreshModelState = () => { } - +export const useRefreshModelListener = (listener: (providerName: RefreshableProviderName, s: RefreshModelStateOfProvider) => void) => { + useEffect(() => { + refreshModelProviderListeners.add(listener) + return () => { refreshModelProviderListeners.delete(listener) } + }, [listener]) +} export const useIsDark = () => { diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index d9c34da9..66b11f3d 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -1,28 +1,58 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js' -import { ProviderName, SettingName, displayInfoOfSettingName, titleOfProviderName, providerNames, ModelInfo } from '../../../../../../../platform/void/common/voidSettingsTypes.js' +import { ProviderName, SettingName, displayInfoOfSettingName, titleOfProviderName, providerNames, VoidModelInfo, featureFlagNames, displayInfoOfFeatureFlag, customSettingNamesOfProvider } from '../../../../../../../platform/void/common/voidSettingsTypes.js' import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js' import { VoidInputBox, VoidSelectBox } from '../util/inputs.js' -import { useIsDark, useRefreshModelState, useService, useSettingsState } from '../util/services.js' -import { X } from 'lucide-react' +import { useIsDark, useRefreshModelListener, useRefreshModelState, useService, useSettingsState } from '../util/services.js' +import { X, RefreshCw, Loader2, Check } from 'lucide-react' +import { RefreshableProviderName, refreshableProviderNames } from '../../../../../../../platform/void/common/refreshModelService.js' // models +const RefreshModelButton = ({ providerName }: { providerName: RefreshableProviderName }) => { + const refreshModelState = useRefreshModelState() + const refreshModelService = useService('refreshModelService') + + const [justFinished, setJustSucceeded] = useState(false) + + useRefreshModelListener( + useCallback((providerName2, refreshModelState) => { + if (providerName2 !== providerName) return + const { state } = refreshModelState[providerName] + if (state !== 'success') return + // now we know we just entered 'success' state for this providerName + setJustSucceeded(true) + const tid = setTimeout(() => { setJustSucceeded(false) }, 2000) + return () => clearTimeout(tid) + }, [providerName]) + ) + + const { state } = refreshModelState[providerName] + const isRefreshing = state === 'refreshing' + + const providerTitle = titleOfProviderName(providerName) + return
+ + Refresh Default Models for {providerTitle}. +
+} const RefreshableModels = () => { const settingsState = useSettingsState() - const refreshModelState = useRefreshModelState() - const refreshModelService = useService('refreshModelService') - if (!settingsState.settingsOfProvider.ollama.enabled) - return null + const buttons = refreshableProviderNames.map(providerName => { + if (!settingsState.settingsOfProvider[providerName].enabled) return null + return + }) + + return <> + {buttons} + - return
- - {refreshModelState === 'loading' ? 'loading...' : 'good!'} -
} @@ -93,15 +123,15 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => { } -const AddModelButton = () => { +const AddModelMenuFull = () => { const [open, setOpen] = useState(false) - return <> + return
{open ? { setOpen(false) }} /> - : + : } - +
} @@ -111,11 +141,11 @@ export const ModelDump = () => { const settingsState = useSettingsState() // a dump of all the enabled providers' models - const modelDump: (ModelInfo & { providerName: ProviderName, providerEnabled: boolean })[] = [] + const modelDump: (VoidModelInfo & { providerName: ProviderName, providerEnabled: boolean })[] = [] 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.enabled }))) } return
@@ -130,7 +160,11 @@ export const ModelDump = () => { {/* right part is anything that fits */}
{isDefault ? '' : '(custom model)'} - +
{isDefault ? null : }
@@ -146,38 +180,39 @@ export const ModelDump = () => { const ProviderSetting = ({ providerName, settingName }: { providerName: ProviderName, settingName: SettingName }) => { - const { title, placeholder } = displayInfoOfSettingName(providerName, settingName) + const { title, placeholder, } = displayInfoOfSettingName(providerName, settingName) const voidSettingsService = useService('settingsStateService') let weChangedTextRef = false - return <> - - { - if (weChangedTextRef) return - voidSettingsService.setSettingOfProvider(providerName, settingName, newVal) - }, [voidSettingsService, providerName, settingName])} + return +
+ { + if (weChangedTextRef) return + voidSettingsService.setSettingOfProvider(providerName, settingName, newVal) + }, [voidSettingsService, providerName, settingName])} - // we are responsible for setting the initial value. always sync the instance whenever there's a change to state. - onCreateInstance={useCallback((instance: InputBox) => { - const syncInstance = () => { - const settingsAtProvider = voidSettingsService.state.settingsOfProvider[providerName]; - const stateVal = settingsAtProvider[settingName as SettingName] - // console.log('SYNCING TO', providerName, settingName, stateVal) - weChangedTextRef = true - instance.value = stateVal as string - weChangedTextRef = false - } - syncInstance() - const disposable = voidSettingsService.onDidChangeState(syncInstance) - return [disposable] - }, [voidSettingsService, providerName, settingName])} - multiline={false} - /> - + // we are responsible for setting the initial value. always sync the instance whenever there's a change to state. + onCreateInstance={useCallback((instance: InputBox) => { + const syncInstance = () => { + const settingsAtProvider = voidSettingsService.state.settingsOfProvider[providerName]; + const stateVal = settingsAtProvider[settingName as SettingName] + // console.log('SYNCING TO', providerName, settingName, stateVal) + weChangedTextRef = true + instance.value = stateVal as string + weChangedTextRef = false + } + syncInstance() + const disposable = voidSettingsService.onDidChangeState(syncInstance) + return [disposable] + }, [voidSettingsService, providerName, settingName])} + multiline={false} + /> +
+
} @@ -185,17 +220,16 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) = const voidSettingsState = useSettingsState() const voidSettingsService = useService('settingsStateService') - const { models, enabled, ...others } = voidSettingsState.settingsOfProvider[providerName] + const { enabled } = voidSettingsState.settingsOfProvider[providerName] + const settingNames = customSettingNamesOfProvider(providerName) return <> -

{titleOfProviderName(providerName)}

{/* settings besides models (e.g. api key) */} - {Object.keys(others).map((sName, i) => { - const settingName = sName as keyof typeof others + {settingNames.map((settingName, i) => { return })} @@ -211,6 +245,26 @@ export const VoidProviderSettings = () => { } +export const VoidFeatureFlagSettings = () => { + const voidSettingsService = useService('settingsStateService') + const voidSettingsState = useSettingsState() + + return <> + {featureFlagNames.map((flagName) => { + const value = voidSettingsState.featureFlagSettings[flagName] + const { description } = displayInfoOfFeatureFlag(flagName) + return
+
+ +

{description}

+
+
+ })} + +} + // full settings @@ -233,10 +287,10 @@ export const Settings = () => { {/* tabs */}
- -
@@ -252,16 +306,20 @@ export const Settings = () => {

Models

- + - - - +

Providers

+
+ + + +

{ setTab('features') }}>Features

+
diff --git a/src/vs/workbench/contrib/void/browser/sidebarActions.ts b/src/vs/workbench/contrib/void/browser/sidebarActions.ts index e44fc864..7f61c729 100644 --- a/src/vs/workbench/contrib/void/browser/sidebarActions.ts +++ b/src/vs/workbench/contrib/void/browser/sidebarActions.ts @@ -53,9 +53,10 @@ const getContentInRange = (model: ITextModel, range: IRange | null) => { } // Action: when press ctrl+L, show the sidebar chat and add to the selection +export const VOID_CTRL_L_ACTION_ID = 'void.ctrlLAction' registerAction2(class extends Action2 { constructor() { - super({ id: 'void.ctrl+l', title: 'Show Sidebar', keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyL, weight: KeybindingWeight.BuiltinExtension } }); + super({ id: VOID_CTRL_L_ACTION_ID, title: 'Void: Show Sidebar', keybinding: { primary: KeyMod.CtrlCmd | KeyCode.KeyL, weight: KeybindingWeight.BuiltinExtension } }); } async run(accessor: ServicesAccessor): Promise { @@ -66,7 +67,7 @@ registerAction2(class extends Action2 { const stateService = accessor.get(ISidebarStateService) const metricsService = accessor.get(IMetricsService) - metricsService.capture('Chat Navigation', { type: 'Ctrl+L' }) + metricsService.capture('User Action', { type: 'Ctrl+L' }) stateService.setState({ isHistoryOpen: false, currentTab: 'chat' }) stateService.fireFocusChat() diff --git a/src/vs/workbench/contrib/void/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts index a67594a7..c8dd051a 100644 --- a/src/vs/workbench/contrib/void/browser/void.contribution.ts +++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts @@ -7,11 +7,14 @@ // register inline diffs import './inlineDiffsService.js' -// register Sidebar pane, state, actions (keybinds, menus) +// register Sidebar pane, state, actions (keybinds, menus) (Ctrl+L) import './sidebarActions.js' import './sidebarPane.js' import './sidebarStateService.js' +// register quick edit (Ctrl+K) +import './quickEditActions.js' + // register Thread History import './threadHistoryService.js'