Merge pull request #182 from voideditor/model-selection

UI Improvements and Model Selection
This commit is contained in:
Andrew Pareles 2024-12-18 19:33:57 -08:00 committed by GitHub
commit e4bc21ffb1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 786 additions and 273 deletions

View file

@ -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
},
});
}

View file

@ -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<OllamaModelResponse>) => void;
openAICompatibleList: (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => 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<OllamaModelResponse>) => void) } = {}
private readonly onError_ollama: { [eventId: string]: ((params: EventModelListOnErrorParams<OllamaModelResponse>) => void) } = {}
// openAICompatibleList
private readonly onSuccess_openAICompatible: { [eventId: string]: ((params: EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>) => void) } = {}
private readonly onError_openAICompatible: { [eventId: string]: ((params: EventModelListOnErrorParams<OpenaiCompatibleModelResponse>) => 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<EventOllamaListOnSuccessParams>)(e => {
this._register((this.channel.listen('onSuccess_ollama') satisfies Event<EventModelListOnSuccessParams<OllamaModelResponse>>)(e => {
this.onSuccess_ollama[e.requestId]?.(e)
}))
this._register((this.channel.listen('onError_ollama') satisfies Event<EventOllamaListOnErrorParams>)(e => {
this._register((this.channel.listen('onError_ollama') satisfies Event<EventModelListOnErrorParams<OllamaModelResponse>>)(e => {
this.onError_ollama[e.requestId]?.(e)
}))
// openaiCompatible
this._register((this.channel.listen('onSuccess_openAICompatible') satisfies Event<EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>>)(e => {
this.onSuccess_openAICompatible[e.requestId]?.(e)
}))
this._register((this.channel.listen('onError_openAICompatible') satisfies Event<EventModelListOnErrorParams<OpenaiCompatibleModelResponse>>)(e => {
this.onError_openAICompatible[e.requestId]?.(e)
}))
}
@ -113,7 +124,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
}
ollamaList = (params: ServiceOllamaListParams) => {
ollamaList = (params: ServiceModelListParams<OllamaModelResponse>) => {
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<OllamaModelResponse>)
}
openAICompatibleList = (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => {
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<OpenaiCompatibleModelResponse>)
}

View file

@ -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<modelResponse> = {
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<modelResponse> = {
onSuccess: (param: { models: modelResponse[] }) => void;
onError: (param: { error: any }) => void;
}
type BlockedMainOllamaListParams = 'onSuccess' | 'onError'
export type MainOllamaListParams = Omit<OllamaListParams, BlockedMainOllamaListParams> & { requestId: string }
type BlockedMainModelListParams = 'onSuccess' | 'onError'
export type MainModelListParams<modelResponse> = Omit<ModelListParams<modelResponse>, BlockedMainModelListParams> & { requestId: string }
export type EventOllamaListOnSuccessParams = Parameters<OllamaListParams['onSuccess']>[0] & { requestId: string }
export type EventOllamaListOnErrorParams = Parameters<OllamaListParams['onError']>[0] & { requestId: string }
export type EventModelListOnSuccessParams<modelResponse> = Parameters<ModelListParams<modelResponse>['onSuccess']>[0] & { requestId: string }
export type EventModelListOnErrorParams<modelResponse> = Parameters<ModelListParams<modelResponse>['onError']>[0] & { requestId: string }
export type _InternalOllamaListFnType = (params: OllamaListParams) => void
export type _InternalModelListFnType<modelResponse> = (params: ModelListParams<modelResponse>) => void

View file

@ -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<RefreshableProviderName, RefreshableState>
const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvider[k])[] } = {
ollama: ['enabled', 'endpoint'],
openAICompatible: ['enabled', 'endpoint', 'apiKey'],
}
const REFRESH_INTERVAL = 5000
// element-wise equals
function eq<T>(a: T[], b: T[]): boolean {
@ -23,9 +50,9 @@ function eq<T>(a: T[], b: T[]): boolean {
}
export interface IRefreshModelService {
readonly _serviceBrand: undefined;
refreshOllamaModels(): void;
onDidChangeState: Event<void>;
state: RefreshModelState;
refreshModels: (providerName: RefreshableProviderName) => Promise<void>;
onDidChangeState: Event<RefreshableProviderName>;
state: RefreshModelStateOfProvider;
}
export const IRefreshModelService = createDecorator<IRefreshModelService>('RefreshModelService');
@ -34,8 +61,8 @@ export class RefreshModelService extends Disposable implements IRefreshModelServ
readonly _serviceBrand: undefined;
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
private readonly _onDidChangeState = new Emitter<RefreshableProviderName>();
readonly onDidChangeState: Event<RefreshableProviderName> = 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<IDisposable> = 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)
}
}

View file

@ -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 = <S extends SettingName>(
newVal: SettingsOfProvider[ProviderName][S extends keyof SettingsOfProvider[ProviderName] ? S : never],
) => Promise<void>;
type SetModelSelectionOfFeature = <K extends FeatureName>(
type SetModelSelectionOfFeatureFn = <K extends FeatureName>(
featureName: K,
newVal: ModelSelectionOfFeature[K],
options?: { doNotApplyEffects?: true }
) => Promise<void>;
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<keyof VoidSettingsState, '_modelOptions'> | 'all'
export interface IVoidSettingsService {
readonly _serviceBrand: undefined;
readonly state: VoidSettingsState;
onDidChangeState: Event<void>;
readonly state: VoidSettingsState; // in order to play nicely with react, you should immutably change state
readonly waitForInitState: Promise<void>;
onDidChangeState: Event<EventProp>;
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<IVoidSettingsService>('VoidS
class VoidSettingsService extends Disposable implements IVoidSettingsService {
_serviceBrand: undefined;
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
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
state: VoidSettingsState;
waitForInitState: Promise<void> // 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)

View file

@ -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> = 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<typeof customProviderSettingsDefaults[ProviderName]>
type CustomSettingName = UnionOfKeys<typeof customProviderSettings[ProviderName]>
type CustomProviderSettings<providerName extends ProviderName> = {
[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<providerName extends ProviderName> = CustomProviderSettings<providerName> & CommonProviderSettings
export type SettingsForProvider<providerName extends ProviderName> = CustomProviderSettings<providerName> & CommonProviderSettings
// part of state
export type SettingsOfProvider = {
@ -148,6 +149,14 @@ export type SettingName = keyof SettingsForProvider<ProviderName>
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}"`)
}

View file

@ -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<OllamaModelResponse> = 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 })

View file

@ -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<Model> = 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 => {

View file

@ -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<string, AbortRef> = {}
// ollamaList
private readonly _onSuccess_ollama = new Emitter<EventOllamaListOnSuccessParams>();
private readonly _onError_ollama = new Emitter<EventOllamaListOnErrorParams>();
private readonly _onSuccess_ollama = new Emitter<EventModelListOnSuccessParams<OllamaModelResponse>>();
private readonly _onError_ollama = new Emitter<EventModelListOnErrorParams<OllamaModelResponse>>();
// openaiCompatibleList
private readonly _onSuccess_openAICompatible = new Emitter<EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>>();
private readonly _onError_openAICompatible = new Emitter<EventModelListOnErrorParams<OpenaiCompatibleModelResponse>>();
// 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<OllamaModelResponse>) {
const { requestId } = params;
const mainThreadParams: OllamaListParams = {
const mainThreadParams: ModelListParams<OllamaModelResponse> = {
...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<OpenaiCompatibleModelResponse>) {
const { requestId } = params;
const mainThreadParams: ModelListParams<OpenaiCompatibleModelResponse> = {
...params,
onSuccess: ({ models }) => { this._onSuccess_openAICompatible.fire({ requestId, models }); },
onError: ({ error }) => { this._onError_openAICompatible.fire({ requestId, error }); },
}
openaiCompatibleList(mainThreadParams)
}
}

View file

@ -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<KeybindingLabel>();
private currentDisposables = new Set<IDisposable>();
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<string>();
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<string>();
// 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<boolean>('workbench.tips.enabled');
// const enabled = this.configurationService.getValue<boolean>('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());
}
}

View file

@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 795 KiB

View file

@ -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<void> {
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!')
}
});

View file

@ -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);
}

View file

@ -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 = <T,>({ onChangeSelection, onCreateInstance, selectB
let containerRef = useRef<HTMLDivElement | null>(null);
return <WidgetComponent
className='text-ellipsis whitespace-nowrap pr-6'
className='@@select-ellipsis'
ctor={SelectBox}
propsFn={useCallback((container) => {
containerRef.current = container

View file

@ -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 = () => {

View file

@ -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 <div className='flex items-center py-1 px-3 rounded-sm overflow-hidden gap-2 hover:bg-black/10 dark:hover:bg-gray-200/10'>
<button className='flex items-center' disabled={isRefreshing || justFinished} onClick={() => { refreshModelService.refreshModels(providerName) }}>
{isRefreshing ? <Loader2 className='size-3 animate-spin' /> : (justFinished ? <Check className='stroke-green-500 size-3' /> : <RefreshCw className='size-3' />)}
</button>
<span className='opacity-50'>Refresh Default Models for {providerTitle}.</span>
</div>
}
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 <RefreshModelButton key={providerName} providerName={providerName} />
})
return <>
{buttons}
</>
return <div>
<button onClick={() => refreshModelService.refreshOllamaModels()}>refresh Ollama built-in models</button>
{refreshModelState === 'loading' ? 'loading...' : 'good!'}
</div>
}
@ -93,15 +123,15 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
}
const AddModelButton = () => {
const AddModelMenuFull = () => {
const [open, setOpen] = useState(false)
return <>
return <div className='my-2 hover:bg-black/10 dark:hover:bg-gray-200/10 py-1 px-3 rounded-sm overflow-hidden '>
{open ?
<AddModelMenu onSubmit={() => { setOpen(false) }} />
: <button onClick={() => setOpen(true)}>Add Model</button>
: <button className='' onClick={() => setOpen(true)}>Add Model</button>
}
</>
</div>
}
@ -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 <div className=''>
@ -130,7 +160,11 @@ export const ModelDump = () => {
{/* right part is anything that fits */}
<div className='w-fit flex items-center gap-4'>
<span className='opacity-50 whitespace-nowrap'>{isDefault ? '' : '(custom model)'}</span>
<button disabled={!providerEnabled} onClick={() => { settingsStateService.toggleModelHidden(providerName, modelName) }}>{(!providerEnabled || isHidden) ? '❌' : '✅'}</button>
<button disabled={!providerEnabled} onClick={() => { settingsStateService.toggleModelHidden(providerName, modelName) }}>
{!providerEnabled ? '🌑' // provider disabled
: isHidden ? '❌' // model is disabled
: '✅'}
</button>
<div className='w-5 flex items-center justify-center'>
{isDefault ? null : <button onClick={() => { settingsStateService.deleteModel(providerName, modelName) }}><X className='size-4' /></button>}
</div>
@ -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 <><ErrorBoundary>
<label>{title}</label>
<VoidInputBox
placeholder={placeholder}
onChangeText={useCallback((newVal) => {
if (weChangedTextRef) return
voidSettingsService.setSettingOfProvider(providerName, settingName, newVal)
}, [voidSettingsService, providerName, settingName])}
return <ErrorBoundary>
<div className='my-1'>
<VoidInputBox
placeholder={`Enter your ${title} here (${placeholder}).`}
onChangeText={useCallback((newVal) => {
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}
/>
</ErrorBoundary></>
// 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}
/>
</div>
</ErrorBoundary>
}
@ -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 <>
<div className='flex items-center gap-4'>
<h3 className='text-xl'>{titleOfProviderName(providerName)}</h3>
<button onClick={() => { voidSettingsService.setSettingOfProvider(providerName, 'enabled', !enabled) }}>{enabled ? '✅' : '❌'}</button>
</div>
{/* 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 <ProviderSetting key={settingName} providerName={providerName} settingName={settingName} />
})}
</>
@ -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 <div key={flagName} className='hover:bg-black/10 hover:dark:bg-gray-200/10 rounded-sm overflow-hidden py-1 px-3 my-1'>
<div className='flex items-center gap-4'>
<button onClick={() => { voidSettingsService.setFeatureFlag(flagName, !value) }}>
{value ? '✅' : '❌'}
</button>
<h4 className='text-sm'>{description}</h4>
</div>
</div>
})}
</>
}
// full settings
@ -233,10 +287,10 @@ export const Settings = () => {
{/* tabs */}
<div className='flex flex-col w-full max-w-32'>
<button className={`text-left p-1 my-0.5 rounded-sm overflow-hidden ${tab === 'models' ? 'bg-vscode-button-hover-bg' : 'bg-vscode-button-active-bg'} hover:bg-vscode-button-hover-bg active:bg-vscode-button-active-bg`}
<button className={`text-left p-1 my-0.5 rounded-sm overflow-hidden ${tab === 'models' ? 'bg-black/10 dark:bg-gray-200/10' : ''} hover:bg-black/10 hover:dark:bg-gray-200/10 active:bg-black/10 active:dark:bg-gray-200/10 `}
onClick={() => { setTab('models') }}
>Models</button>
<button className={`text-left p-1 my-0.5 rounded-sm overflow-hidden ${tab === 'features' ? 'bg-vscode-button-hover-bg' : 'bg-vscode-button-active-bg'} hover:bg-vscode-button-hover-bg active:bg-vscode-button-active-bg`}
<button className={`text-left p-1 my-0.5 rounded-sm overflow-hidden ${tab === 'features' ? 'bg-black/10 dark:bg-gray-200/10' : ''} hover:bg-black/10 hover:dark:bg-gray-200/10 active:bg-black/10 active:dark:bg-gray-200/10 `}
onClick={() => { setTab('features') }}
>Features</button>
</div>
@ -252,16 +306,20 @@ export const Settings = () => {
<h2 className={`text-3xl mb-2`}>Models</h2>
<ErrorBoundary>
<ModelDump />
<AddModelButton />
<AddModelMenuFull />
<RefreshableModels />
</ErrorBoundary>
<ErrorBoundary>
<VoidProviderSettings />
</ErrorBoundary>
<h2 className={`text-3xl mt-4 mb-2`}>Providers</h2>
<div className='px-3'>
<ErrorBoundary>
<VoidProviderSettings />
</ErrorBoundary>
</div>
</div>
<div className={`${tab !== 'features' ? 'hidden' : ''}`}>
<h2 className={`text-3xl mb-2`} onClick={() => { setTab('features') }}>Features</h2>
<VoidFeatureFlagSettings />
</div>
</div>

View file

@ -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<void> {
@ -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()

View file

@ -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'