add new config type for each feature, UI dump

This commit is contained in:
Andrew Pareles 2024-12-09 02:41:27 -08:00
parent 77fc2d2d4a
commit e5ac36facd
21 changed files with 667 additions and 552 deletions

View file

@ -4,19 +4,19 @@
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
const voidProviderDefaults = {
anthropic: {
apiKey: '',
export const voidInitModelOptions = {
anthropic: () => ({
model: 'claude-3-5-sonnet-20240620',
models: [
'claude-3-5-sonnet-20240620',
'claude-3-opus-20240229',
'claude-3-sonnet-20240229',
'claude-3-haiku-20240307'
],
model: 'claude-3-5-sonnet-20240620',
},
openAI: {
apiKey: '',
}),
openAI: () => ({
model: 'gpt-4o',
models: [
'o1-preview',
'o1-mini',
@ -34,241 +34,256 @@ const voidProviderDefaults = {
'gpt-4-0613',
'gpt-3.5-turbo-0125',
'gpt-3.5-turbo',
'gpt-3.5-turbo-1106'
'gpt-3.5-turbo-1106',
],
model: 'gpt-4o',
},
ollama: {
endpoint: 'http://127.0.0.1:11434', //'The endpoint of your Ollama instance.',
models: ['codestral', 'qwen2.5-coder', 'qwen2.5-coder:0.5b', 'qwen2.5-coder:1.5b', 'qwen2.5-coder:3b', 'qwen2.5-coder:7b', 'qwen2.5-coder:14b', 'qwen2.5-coder:32b', 'codegemma', 'codegemma:2b', 'codegemma:7b', 'codellama', 'codellama:7b', 'codellama:13b', 'codellama:34b', 'codellama:70b', 'codellama:code', 'codellama:python', 'command-r', 'command-r:35b', 'command-r-plus', 'command-r-plus:104b', 'deepseek-coder-v2', 'deepseek-coder-v2:16b', 'deepseek-coder-v2:236b', 'falcon2', 'falcon2:11b', 'firefunction-v2', 'firefunction-v2:70b', 'gemma', 'gemma:2b', 'gemma:7b', 'gemma2', 'gemma2:2b', 'gemma2:9b', 'gemma2:27b', 'llama2', 'llama2:7b', 'llama2:13b', 'llama2:70b', 'llama3', 'llama3:8b', 'llama3:70b', 'llama3-chatqa', 'llama3-chatqa:8b', 'llama3-chatqa:70b', 'llama3-gradient', 'llama3-gradient:8b', 'llama3-gradient:70b', 'llama3.1', 'llama3.1:8b', 'llama3.1:70b', 'llama3.1:405b', 'llava', 'llava:7b', 'llava:13b', 'llava:34b', 'llava-llama3', 'llava-llama3:8b', 'llava-phi3', 'llava-phi3:3.8b', 'mistral', 'mistral:7b', 'mistral-large', 'mistral-large:123b', 'mistral-nemo', 'mistral-nemo:12b', 'mixtral', 'mixtral:8x7b', 'mixtral:8x22b', 'moondream', 'moondream:1.8b', 'openhermes', 'openhermes:v2.5', 'phi3', 'phi3:3.8b', 'phi3:14b', 'phi3.5', 'phi3.5:3.8b', 'qwen', 'qwen:7b', 'qwen:14b', 'qwen:32b', 'qwen:72b', 'qwen:110b', 'qwen2', 'qwen2:0.5b', 'qwen2:1.5b', 'qwen2:7b', 'qwen2:72b', 'smollm', 'smollm:135m', 'smollm:360m', 'smollm:1.7b'] as const,
}),
ollama: () => ({ // TODO make this do a fetch to get the models
model: 'codestral',
},
openRouter: {
apiKey: '',
models: ['openai/gpt-4o'],
models: [
'codestral',
'qwen2.5-coder',
'qwen2.5-coder:0.5b',
'qwen2.5-coder:1.5b',
'qwen2.5-coder:3b',
'qwen2.5-coder:7b',
'qwen2.5-coder:14b',
'qwen2.5-coder:32b',
'codegemma',
'codegemma:2b',
'codegemma:7b',
'codellama',
'codellama:7b',
'codellama:13b',
'codellama:34b',
'codellama:70b',
'codellama:code',
'codellama:python',
'command-r',
'command-r:35b',
'command-r-plus',
'command-r-plus:104b',
'deepseek-coder-v2',
'deepseek-coder-v2:16b',
'deepseek-coder-v2:236b',
'falcon2',
'falcon2:11b',
'firefunction-v2',
'firefunction-v2:70b',
'gemma',
'gemma:2b',
'gemma:7b',
'gemma2',
'gemma2:2b',
'gemma2:9b',
'gemma2:27b',
'llama2',
'llama2:7b',
'llama2:13b',
'llama2:70b',
'llama3',
'llama3:8b',
'llama3:70b',
'llama3-chatqa',
'llama3-chatqa:8b',
'llama3-chatqa:70b',
'llama3-gradient',
'llama3-gradient:8b',
'llama3-gradient:70b',
'llama3.1',
'llama3.1:8b',
'llama3.1:70b',
'llama3.1:405b',
'llava',
'llava:7b',
'llava:13b',
'llava:34b',
'llava-llama3',
'llava-llama3:8b',
'llava-phi3',
'llava-phi3:3.8b',
'mistral',
'mistral:7b',
'mistral-large',
'mistral-large:123b',
'mistral-nemo',
'mistral-nemo:12b',
'mixtral',
'mixtral:8x7b',
'mixtral:8x22b',
'moondream',
'moondream:1.8b',
'openhermes',
'openhermes:v2.5',
'phi3',
'phi3:3.8b',
'phi3:14b',
'phi3.5',
'phi3.5:3.8b',
'qwen',
'qwen:7b',
'qwen:14b',
'qwen:32b',
'qwen:72b',
'qwen:110b',
'qwen2',
'qwen2:0.5b',
'qwen2:1.5b',
'qwen2:7b',
'qwen2:72b',
'smollm',
'smollm:135m',
'smollm:360m',
'smollm:1.7b',
],
}),
openRouter: () => ({
model: 'openai/gpt-4o',
},
openAICompatible: {
apiKey: '',
endpoint: 'http://127.0.0.1:11434/v1', //'The baseUrl (exluding /chat/completions).',
models: ['gpt-4o'],
model: 'gpt-4o',
},
gemini: {
apiKey: '',
models: null, // any
}),
openAICompatible: () => ({
model: 'openai/gpt-4o',
models: null, // any
}),
gemini: () => ({
model: 'gemini-1.5-flash',
models: [
'gemini-1.5-flash',
'gemini-1.5-pro',
'gemini-1.5-flash-8b',
'gemini-1.0-pro'
],
model: 'gemini-1.5-flash',
}),
groq: () => ({
model: 'mixtral-8x7b-32768',
models: [
"mixtral-8x7b-32768",
"llama2-70b-4096",
"gemma-7b-it"
]
})
}
export const voidProviderDefaults = {
anthropic: {
apiKey: '',
},
openAI: {
apiKey: '',
},
ollama: {
endpoint: 'http://127.0.0.1:11434',
},
openRouter: {
apiKey: '',
},
openAICompatible: {
apiKey: '',
endpoint: 'http://127.0.0.1:11434/v1',
},
gemini: {
apiKey: '',
},
groq: {
apiKey: ''
}
} as const
type VoidSettings = typeof voidProviderDefaults
export type ProviderName = keyof typeof voidProviderDefaults
export const providerNames = Object.keys(voidProviderDefaults) as ProviderName[]
export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete']
export type FeatureName = typeof featureNames[number]
// was whichApi:
const providerOptions = _uiConfig(
'API Provider.',
'anthropic',
allowedProviders,
)
const voidFeatureOptions = {
maxTokens: _uiConfig(
'Max number of tokens to output.',
'undefined',
[
'undefined',
'1024',
'2048',
'4096',
'8192'
] as const,
)
} as const
type AllVoidProvidersState = {
[providerName in ProviderName]: {
[option in keyof typeof voidProviderOptions[providerName]['providerOptions']]: string // optionName (e.g. apikey) -> string
}
export type VoidConfigState = {
[providerName in ProviderName]: ({
enabled: string, // 'true' | 'false'
models: string[],
model: string,
maxTokens: string,
} & {
[optionName in keyof typeof voidProviderDefaults[providerName]]: string
})
}
// const features = ['ctrl+L', 'ctrl+K', 'autocomplete'] as const
// type FeatureName = (typeof features)[number]
type UnionOfKeys<T> = T extends T ? keyof T : never;
export const descOfSettingName = (providerName: ProviderName, settingName: UnionOfKeys<VoidConfigState[ProviderName]>) => {
if (settingName === 'apiKey')
return 'API Key'
else if (settingName === 'endpoint') {
if (providerName === 'ollama') return 'The endpoint of your Ollama instance.'
if (providerName === 'openAICompatible') return 'The baseUrl (exluding /chat/completions).'
}
else if (settingName === 'maxTokens')
return 'Max Tokens'
else if (settingName === 'model')
return 'Model'
else if (settingName === 'enabled')
return 'Enabled'
else if (settingName === 'models')
return 'Available Models'
throw new Error(`Unknown setting name: "${settingName}"`)
}
// not very important (remember past user options):
// type AllVoidFeaturesState = {
// [featureName in FeatureName]: {
// [providerName in ProviderName]: {
// [modelName in (typeof voidProviderOptions)[providerName]['modelOptions']['defaultVal']]: {
// options: { [option in keyof typeof voidProviderOptions[providerName]]: string }
// }
// }
// }
// }
type VoidFeatureState<
CtrlLProvider extends ProviderName,
CtrlKProvider extends ProviderName,
AutocompleteProvider extends ProviderName,
> = {
'ctrl+L': {
provider: CtrlLProvider,
model: (typeof voidProviderOptions)[CtrlLProvider]['modelOptions']['defaultVal'],
// promptTemplate?
// systemTemplate?
// maxTokens?
export const defaultVoidConfigState: VoidConfigState = {
anthropic: {
enabled: 'false',
models: [],
model: '',
apiKey: '',
maxTokens: '1000',
},
'ctrl+K': {
provider: CtrlKProvider,
model: (typeof voidProviderOptions)[CtrlKProvider]['modelOptions']['defaultVal'],
openAI: {
enabled: 'false',
models: [],
model: '',
apiKey: '',
maxTokens: '1000',
},
'autocomplete': {
provider: AutocompleteProvider,
model: (typeof voidProviderOptions)[AutocompleteProvider]['modelOptions']['defaultVal'],
ollama: {
enabled: 'false',
models: [],
model: '',
endpoint: '',
maxTokens: '1000',
},
}
const PartialVoidState = {
}
const VoidState = {}
// this is the type that comes with metadata like desc, default val, etc
export type VoidConfigInfo = typeof voidProviderOptions
export type VoidConfigField = keyof typeof voidProviderOptions // typeof configFields[number]
// this is the type that specifies the user's actual config
export type PartialVoidConfig = {
[K in keyof typeof voidProviderOptions]?: {
[P in keyof typeof voidProviderOptions[K]]?: typeof voidProviderOptions[K][P]['defaultVal']
openRouter: {
enabled: 'false',
models: [],
model: '',
apiKey: '',
maxTokens: '1000',
},
openAICompatible: {
enabled: 'false',
models: [],
model: '',
apiKey: '',
endpoint: '',
maxTokens: '1000',
},
gemini: {
enabled: 'false',
models: [],
model: '',
apiKey: '',
maxTokens: '1000',
},
groq: {
enabled: 'false',
models: [],
model: '',
apiKey: '',
maxTokens: '1000',
}
}
export type VoidConfig = {
[K in keyof typeof voidProviderOptions]: {
[P in keyof typeof voidProviderOptions[K]]: typeof voidProviderOptions[K][P]['defaultVal']
}
}
const getVoidConfig = (partialVoidConfig: PartialVoidConfig): VoidConfig => {
const config = {} as PartialVoidConfig
for (const field of [...allowedProviders, 'default'] as const) {
config[field] = {}
for (const prop in voidProviderOptions[field]) {
config[field][prop] = partialVoidConfig[field]?.[prop]?.trim() || voidProviderOptions[field][prop].defaultVal
}
}
return config as VoidConfig
}
const VOID_CONFIG_KEY = 'void.partialVoidConfig'
export type SetFieldFnType = <K extends VoidConfigField>(field: K, param: keyof VoidConfigInfo[K], newVal: string) => Promise<void>;
export type ConfigState = {
partialVoidConfig: PartialVoidConfig; // free parameter
voidConfig: VoidConfig; // computed from partialVoidConfig
}
export interface IVoidConfigStateService {
readonly _serviceBrand: undefined;
readonly state: ConfigState;
readonly voidConfigInfo: VoidConfigInfo;
onDidChangeState: Event<void>;
setField: SetFieldFnType;
}
export const IVoidConfigStateService = createDecorator<IVoidConfigStateService>('VoidConfigStateService');
class VoidConfigStateService extends Disposable implements IVoidConfigStateService {
_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
state: ConfigState;
readonly voidConfigInfo: VoidConfigInfo = voidProviderOptions; // just putting this here for simplicity, it's static though
constructor(
@IStorageService private readonly _storageService: IStorageService,
@IEncryptionService private readonly _encryptionService: IEncryptionService,
// could have used this, but it's clearer the way it is (+ slightly different eg StorageTarget.USER)
// @ISecretStorageService private readonly _secretStorageService: ISecretStorageService,
) {
super()
// at the start, we haven't read the partial config yet, but we need to set state to something, just treat partialVoidConfig like it's empty
this.state = {
partialVoidConfig: {},
voidConfig: getVoidConfig({}),
}
// read and update the actual state immediately
this._readPartialVoidConfig().then(partialVoidConfig => {
this._setState(partialVoidConfig)
})
}
private async _readPartialVoidConfig(): Promise<PartialVoidConfig> {
const encryptedPartialConfig = this._storageService.get(VOID_CONFIG_KEY, StorageScope.APPLICATION)
if (!encryptedPartialConfig)
return {}
const partialVoidConfigStr = await this._encryptionService.decrypt(encryptedPartialConfig)
return JSON.parse(partialVoidConfigStr)
}
private async _storePartialVoidConfig(partialVoidConfig: PartialVoidConfig) {
const encryptedPartialConfigStr = await this._encryptionService.encrypt(JSON.stringify(partialVoidConfig))
this._storageService.store(VOID_CONFIG_KEY, encryptedPartialConfigStr, StorageScope.APPLICATION, StorageTarget.USER)
}
// Set field on PartialVoidConfig
setField: SetFieldFnType = async <K extends VoidConfigField>(field: K, param: keyof VoidConfigInfo[K], newVal: string) => {
const { partialVoidConfig } = this.state
const newPartialConfig: PartialVoidConfig = {
...partialVoidConfig,
[field]: {
...partialVoidConfig[field],
[param]: newVal
}
}
await this._storePartialVoidConfig(newPartialConfig)
this._setState(newPartialConfig)
}
// internal function to update state, should be called every time state changes
private async _setState(partialVoidConfig: PartialVoidConfig) {
this.state = {
partialVoidConfig: partialVoidConfig,
voidConfig: getVoidConfig(partialVoidConfig),
}
this._onDidChangeState.fire()
}
}
registerSingleton(IVoidConfigStateService, VoidConfigStateService, InstantiationType.Eager);

View file

@ -3,7 +3,7 @@
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
import { VoidConfig } from './configTypes.js'
import { ProviderName, VoidConfigState } from './configTypes'
// ---------- type definitions ----------
@ -27,11 +27,13 @@ export type LLMMessageServiceParams = {
onError: OnError;
messages: LLMMessage[];
voidConfig: VoidConfig | null;
voidConfig: VoidConfigState | null;
logging: {
loggingName: string,
};
providerName: ProviderName;
}
export type SendLLMMMessageParams = {
@ -40,11 +42,12 @@ export type SendLLMMMessageParams = {
onError: OnError;
messages: LLMMessage[];
voidConfig: VoidConfig | null;
voidConfig: VoidConfigState | null;
logging: {
loggingName: string,
};
providerName: ProviderName;
abortRef: AbortRef;
}

View file

@ -10,7 +10,7 @@ export const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onTex
const thisConfig = voidConfig.anthropic
const anthropic = new Anthropic({ apiKey: thisConfig.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"]
const anthropic = new Anthropic({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true });
// find system messages and concatenate them
const systemMessage = messages
@ -25,7 +25,7 @@ export const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onTex
system: systemMessage,
messages: anthropicMessages,
model: thisConfig.model,
max_tokens: parseMaxTokensStr(voidConfig.default.maxTokens)!, // this might be undefined, but it will just throw an error for the user
max_tokens: parseMaxTokensStr(thisConfig.maxTokens)!, // this might be undefined, but it will just throw an error for the user to see
});

View file

@ -8,7 +8,7 @@ export const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, on
const thisConfig = voidConfig.gemini
const genAI = new GoogleGenerativeAI(thisConfig.apikey);
const genAI = new GoogleGenerativeAI(thisConfig.apiKey);
const model = genAI.getGenerativeModel({ model: thisConfig.model });
// remove system messages that get sent to Gemini

View file

@ -1,64 +1,64 @@
// Greptile
// https://docs.greptile.com/api-reference/query
// https://docs.greptile.com/quickstart#sample-response-streamed
// // Greptile
// // https://docs.greptile.com/api-reference/query
// // https://docs.greptile.com/quickstart#sample-response-streamed
import { SendLLMMessageFnTypeInternal } from './util';
// import { SendLLMMessageFnTypeInternal } from './util';
export const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
// export const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
let fullText = ''
// let fullText = ''
const thisConfig = voidConfig.greptile
// const thisConfig = voidConfig.greptile
fetch('https://api.greptile.com/v2/query', {
method: 'POST',
headers: {
'Authorization': `Bearer ${thisConfig.apikey}`,
'X-Github-Token': `${thisConfig.githubPAT}`,
'Content-Type': `application/json`,
},
body: JSON.stringify({
messages,
stream: true,
repositories: [thisConfig.repoinfo],
}),
})
// this is {message}\n{message}\n{message}...\n
.then(async response => {
const text = await response.text()
console.log('got greptile', text)
return JSON.parse(`[${text.trim().split('\n').join(',')}]`)
})
// TODO make this actually stream, right now it just sends one message at the end
// TODO add _setAborter() when add streaming
.then(async responseArr => {
// fetch('https://api.greptile.com/v2/query', {
// method: 'POST',
// headers: {
// 'Authorization': `Bearer ${thisConfig.apikey}`,
// 'X-Github-Token': `${thisConfig.githubPAT}`,
// 'Content-Type': `application/json`,
// },
// body: JSON.stringify({
// messages,
// stream: true,
// repositories: [thisConfig.repoinfo],
// }),
// })
// // this is {message}\n{message}\n{message}...\n
// .then(async response => {
// const text = await response.text()
// console.log('got greptile', text)
// return JSON.parse(`[${text.trim().split('\n').join(',')}]`)
// })
// // TODO make this actually stream, right now it just sends one message at the end
// // TODO add _setAborter() when add streaming
// .then(async responseArr => {
for (const response of responseArr) {
const type: string = response['type']
const message = response['message']
// for (const response of responseArr) {
// const type: string = response['type']
// const message = response['message']
// when receive text
if (type === 'message') {
fullText += message
onText({ newText: message, fullText })
}
else if (type === 'sources') {
const { filepath, linestart: _, lineend: _2 } = message as { filepath: string; linestart: number | null; lineend: number | null }
fullText += filepath
onText({ newText: filepath, fullText })
}
// type: 'status' with an empty 'message' means last message
else if (type === 'status') {
if (!message) {
onFinalMessage({ fullText })
}
}
}
// // when receive text
// if (type === 'message') {
// fullText += message
// onText({ newText: message, fullText })
// }
// else if (type === 'sources') {
// const { filepath, linestart: _, lineend: _2 } = message as { filepath: string; linestart: number | null; lineend: number | null }
// fullText += filepath
// onText({ newText: filepath, fullText })
// }
// // type: 'status' with an empty 'message' means last message
// else if (type === 'status') {
// if (!message) {
// onFinalMessage({ fullText })
// }
// }
// }
})
.catch(error => {
onError({ error })
});
// })
// .catch(error => {
// onError({ error })
// });
}
// }

View file

@ -6,18 +6,20 @@ import { parseMaxTokensStr } from './util.js';
export const sendGroqMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
let fullText = '';
const thisConfig = voidConfig.groq
const groq = new Groq({
apiKey: voidConfig.groq.apikey,
apiKey: thisConfig.apiKey,
dangerouslyAllowBrowser: true
});
await groq.chat.completions
.create({
messages: messages,
model: voidConfig.groq.model,
model: thisConfig.model,
stream: true,
temperature: 0.7,
max_tokens: parseMaxTokensStr(voidConfig.default.maxTokens),
max_tokens: parseMaxTokensStr(thisConfig.maxTokens),
})
.then(async response => {
_setAborter(() => response.controller.abort())

View file

@ -15,7 +15,7 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText,
model: thisConfig.model,
messages: messages,
stream: true,
options: { num_predict: parseMaxTokensStr(voidConfig.default.maxTokens) } // this is max_tokens
options: { num_predict: parseMaxTokensStr(thisConfig.maxTokens) } // this is max_tokens
})
.then(async stream => {
_setAborter(() => stream.abort())

View file

@ -4,39 +4,38 @@ import { parseMaxTokensStr } from './util.js';
// OpenAI, OpenRouter, OpenAICompatible
export const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
export const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, providerName }) => {
let fullText = ''
let openai: OpenAI
let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
const maxTokens = parseMaxTokensStr(voidConfig.default.maxTokens)
if (voidConfig.default.whichApi === 'openAI') {
if (providerName === 'openAI') {
const thisConfig = voidConfig.openAI
openai = new OpenAI({ apiKey: thisConfig.apikey, dangerouslyAllowBrowser: true });
options = { model: thisConfig.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
openai = new OpenAI({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true });
options = { model: thisConfig.model, messages: messages, stream: true, max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens) }
}
else if (voidConfig.default.whichApi === 'openRouter') {
else if (providerName === 'openRouter') {
const thisConfig = voidConfig.openRouter
openai = new OpenAI({
baseURL: 'https://openrouter.ai/api/v1', apiKey: thisConfig.apikey, dangerouslyAllowBrowser: true,
baseURL: 'https://openrouter.ai/api/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true,
defaultHeaders: {
'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings.
'X-Title': 'Void Editor', // Optional. Shows in rankings on openrouter.ai.
},
});
options = { model: thisConfig.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
options = { model: thisConfig.model, messages: messages, stream: true, max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens) }
}
else if (voidConfig.default.whichApi === 'openAICompatible') {
else if (providerName === 'openAICompatible') {
const thisConfig = voidConfig.openAICompatible
openai = new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apikey, dangerouslyAllowBrowser: true })
options = { model: thisConfig.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
openai = new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true })
options = { model: thisConfig.model, messages: messages, stream: true, max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens) }
}
else {
console.error(`sendOpenAIMsg: invalid whichApi: ${voidConfig.default.whichApi}`)
throw new Error(`voidConfig.whichAPI was invalid: ${voidConfig.default.whichApi}`)
console.error(`sendOpenAIMsg: invalid providerName: ${providerName}`)
throw new Error(`providerName was invalid: ${providerName}`)
}
openai.chat.completions

View file

@ -2,18 +2,21 @@ import { SendLLMMMessageParams, OnText, OnFinalMessage, OnError } from '../../co
import { IMetricsService } from '../../common/metricsService.js';
import { sendAnthropicMsg } from './anthropic.js';
import { sendGeminiMsg } from './gemini.js';
import { sendGreptileMsg } from './greptile.js';
import { sendGroqMsg } from './groq.js';
import { sendOllamaMsg } from './ollama.js';
import { sendOpenAIMsg } from './openai.js';
import { sendGeminiMsg } from './gemini.js';
import { sendGroqMsg } from './groq.js';
export const sendLLMMessage = (
{
messages, onText: onText_, onFinalMessage: onFinalMessage_, onError: onError_,
abortRef: abortRef_, voidConfig, logging: { loggingName },
}: SendLLMMMessageParams,
export const sendLLMMessage = ({
messages,
onText: onText_,
onFinalMessage: onFinalMessage_,
onError: onError_,
abortRef: abortRef_,
voidConfig,
logging: { loggingName },
providerName
}: SendLLMMMessageParams,
metricsService: IMetricsService
) => {
@ -25,7 +28,7 @@ export const sendLLMMessage = (
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
const captureChatEvent = (eventId: string, extras?: object) => {
metricsService.capture(eventId, {
whichApi: voidConfig.default['whichApi'],
providerName,
numMessages: messages?.length,
messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.content.length })),
version: '2024-11-14',
@ -69,29 +72,29 @@ export const sendLLMMessage = (
captureChatEvent(`${loggingName} - Sending Message`, { messageLength: messages[messages.length - 1]?.content.length })
try {
switch (voidConfig.default.whichApi) {
switch (providerName) {
case 'anthropic':
sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, providerName });
break;
case 'openAI':
case 'openRouter':
case 'openAICompatible':
sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, providerName });
break;
case 'gemini':
sendGeminiMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
sendGeminiMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, providerName });
break;
case 'ollama':
sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
break;
case 'greptile':
sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, providerName });
break;
// case 'greptile':
// sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, providerName });
// break;
case 'groq':
sendGroqMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
sendGroqMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, providerName });
break;
default:
onError({ error: `Error: whichApi was "${voidConfig.default.whichApi}", which is not recognized!` })
onError({ error: `Error: whichApi was "${providerName}", which is not recognized!` })
break;
}
}

View file

@ -1,4 +1,4 @@
import { VoidConfig } from '../../common/configTypes'
import { ProviderName, VoidConfigState } from '../../common/configTypes'
import { LLMMessage, OnText, OnFinalMessage, OnError } from '../../common/llmMessageTypes'
export const parseMaxTokensStr = (maxTokensStr: string) => {
@ -15,7 +15,8 @@ export type SendLLMMessageFnTypeInternal = (params: {
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: OnError;
voidConfig: VoidConfig;
voidConfig: VoidConfigState;
providerName: ProviderName;
_setAborter: (aborter: () => void) => void;
}) => void

View file

@ -44,7 +44,7 @@ const CodeButtonsOnHover = ({ diffRepr: text }: { diffRepr: string }) => {
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
onClick={async () => {
inlineDiffService.startStreaming('ctrl+l', text)
inlineDiffService.startStreaming({ type: 'ctrl+l', providerName: 'anthropic' }, text)
}}
>
Apply
@ -125,7 +125,7 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
{item.task && (
<input type="checkbox" checked={item.checked} readOnly />
)}
<MarkdownRender string={item.text} nested={true} />
<ChatMarkdownRender string={item.text} nested={true} />
</li>
))}
</ListTag>
@ -209,7 +209,7 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
)
}
export const MarkdownRender = ({ string, nested = false }: { string: string, nested?: boolean }) => {
export const ChatMarkdownRender = ({ string, nested = false }: { string: string, nested?: boolean }) => {
const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer
return (
<>

View file

@ -5,7 +5,9 @@
import React, { useEffect, useState } from 'react'
import { mountFnGenerator } from '../util/mountFnGenerator.js'
import { SidebarSettings } from './SidebarSettings.js';
// import { SidebarSettings } from './SidebarSettings.js';
import { useSidebarState } from '../util/services.js';
// import { SidebarThreadSelector } from './SidebarThreadSelector.js';
// import { SidebarChat } from './SidebarChat.js';
@ -13,6 +15,8 @@ import { useSidebarState } from '../util/services.js';
import '../styles.css'
import { SidebarThreadSelector } from './SidebarThreadSelector.js';
import { SidebarChat } from './SidebarChat.js';
import { SidebarModelSettings } from './SidebarModelSettings.js';
import { SidebarProviderSettings } from './SidebarProviderSettings.js';
const Sidebar = () => {
const sidebarState = useSidebarState()
@ -37,7 +41,8 @@ const Sidebar = () => {
</div>
<div className={`${tab === 'settings' ? '' : 'hidden'}`}>
<SidebarSettings />
{false && <SidebarModelSettings />}
<SidebarProviderSettings />
</div>
</div>

View file

@ -11,7 +11,7 @@ import { userInstructionsStr } from '../../../prompt/stringifyFiles.js';
import { ChatMessage, CodeSelection, CodeStagingSelection } from '../../../registerThreads.js';
import { BlockCode } from '../markdown/BlockCode.js';
import { MarkdownRender } from '../markdown/MarkdownRender.js';
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
import { IModelService } from '../../../../../../../editor/common/services/model.js';
import { URI } from '../../../../../../../base/common/uri.js';
import { EndOfLinePreference } from '../../../../../../../editor/common/model.js';
@ -110,7 +110,7 @@ const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
</>
}
else if (role === 'assistant') {
chatbubbleContents = <MarkdownRender string={children} /> // sectionsHTML
chatbubbleContents = <ChatMarkdownRender string={children} /> // sectionsHTML
}
return <div className={`${role === 'user' ? 'text-right' : 'text-left'}`}>
@ -141,8 +141,8 @@ export const SidebarChat = () => {
}, [sidebarStateService, chatInputRef])
// config state
const configState = useConfigState()
const { voidConfig } = configState
const voidConfigState = useConfigState()
// threads state
const threadsState = useThreadsState()
@ -220,7 +220,8 @@ export const SidebarChat = () => {
setLatestError(error)
},
voidConfig,
voidConfig: voidConfigState,
providerName: 'anthropic',
}
const latestRequestId = sendLLMMessageService.sendLLMMessage(object)

View file

@ -0,0 +1,37 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
import { FeatureName, featureNames, providerNames } from '../../../../../../../platform/void/common/configTypes.js'
import { useConfigState } from '../util/services.js'
export const SidebarModelSettingsForFeature = ({ featureName }: { featureName: FeatureName }) => {
const voidConfigState = useConfigState()
const models: [string,string][] = []
for (const providerName of providerNames) {
const providerConfig = voidConfigState[providerName]
if (!providerConfig.enabled) continue
providerConfig.models.forEach(model => {
models.push([providerName,model])
})
}
return <>
<h1>Settings - {featureName}</h1>
{models.map(([providerName,model], i) => <span key={i}>{providerName} - {model}</span>)}
</>
}
export const SidebarModelSettings = () => {
return <>
{featureNames.map(featureName => <SidebarModelSettingsForFeature key={featureName} featureName={featureName} />)}
</>
}

View file

@ -0,0 +1,62 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
import React, { Fragment } from 'react'
import { descOfSettingName, ProviderName, providerNames, voidProviderDefaults } from '../../../../../../../platform/void/common/configTypes.js'
import { VoidInputBox, VoidSelectBox } from './inputs.js'
import { useConfigState, useService } from '../util/services.js'
const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) => {
const voidConfigState = useConfigState()
const voidConfigService = useService('configStateService')
console.log('CONFIG', voidConfigState)
console.log('provider:', providerName, voidConfigState[providerName])
const { models, model, ...others } = voidConfigState[providerName]
return <>
<h1>{providerName}</h1>
{/* other settings (e.g. api key) */}
{Object.entries(others).map(([settingName, defaultVal], i) => {
console.log('--- entry:', providerName, settingName, defaultVal)
const sName = settingName as keyof typeof others
return <Fragment key={i}>
<h2>{descOfSettingName(providerName, sName)}</h2>
<VoidInputBox
initVal={defaultVal}
onChangeText={(newVal) => { () => { voidConfigService.setState(providerName, sName, newVal) } }}
placeholder={settingName}
multiline={false}
inputBoxRef={{ current: null }}
/>
</Fragment>
})}
<h2>{'Models'}</h2>
<VoidSelectBox
initVal={models[0]}
options={models}
onChangeSelection={(newVal) => { () => { } }}
selectBoxRef={{ current: null }}
/>
<h2>{'Enabled'}</h2>
todo
</>
}
export const SidebarProviderSettings = () => {
return <>
{providerNames.map(providerName =>
<SettingsForProvider key={providerName} providerName={providerName} />
)}
</>
}

View file

@ -1,150 +1,150 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useConfigState, useService } from '../util/services.js';
// /*---------------------------------------------------------------------------------------------
// * Copyright (c) Glass Devtools, Inc. All rights reserved.
// * Void Editor additions licensed under the AGPLv3 License.
// *--------------------------------------------------------------------------------------------*/
// import React, { useCallback, useEffect, useRef, useState } from 'react';
// import { useConfigState, useService } from '../util/services.js';
import { HistoryInputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js';
import { ConfigState, IVoidConfigStateService } from '../../../registerConfig.js';
import { nonDefaultConfigFields, VoidConfigField } from '../../../../../../../platform/void/common/configTypes.js';
// import { HistoryInputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
// import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js';
// import { ConfigState, IVoidConfigStateService } from '../../../registerConfig.js';
// import { nonDefaultConfigFields, VoidConfigField } from '../../../../../../../platform/void/common/configTypes.js';
const SettingOfFieldAndParam = ({ field, param, configState, configStateService }:
{ field: VoidConfigField; param: string; configState: ConfigState; configStateService: IVoidConfigStateService }) => {
// const SettingOfFieldAndParam = ({ field, param, configState, configStateService }:
// { field: VoidConfigField; param: string; configState: ConfigState; configStateService: IVoidConfigStateService }) => {
const { partialVoidConfig } = configState
// const { partialVoidConfig } = configState
const { enumArr, defaultVal, description } = configStateService.voidConfigInfo[field][param]
const val = partialVoidConfig[field]?.[param] ?? defaultVal // current value of this item
const initValRef = useRef(val)
// const { enumArr, defaultVal, description } = configStateService.voidConfigInfo[field][param]
// const val = partialVoidConfig[field]?.[param] ?? defaultVal // current value of this item
// const initValRef = useRef(val)
const updateState = useCallback((newValue: string) => {
configStateService.setField(field, param, newValue)
}, [configStateService, field, param])
// const updateState = useCallback((newValue: string) => {
// configStateService.setField(field, param, newValue)
// }, [configStateService, field, param])
const inputBoxRef = useRef<HistoryInputBox | null>(null);
const selectBoxRef = useRef<SelectBox | null>(null);
const forceState = useCallback((newValue: string) => {
if (inputBoxRef.current) {
inputBoxRef.current.value = newValue;
}
if (selectBoxRef.current) {
selectBoxRef.current.select(enumArr?.indexOf(newValue) ?? 0);
}
// updateState is called automatically when the change happens
}, [enumArr, updateState])
// const inputBoxRef = useRef<HistoryInputBox | null>(null);
// const selectBoxRef = useRef<SelectBox | null>(null);
// const forceState = useCallback((newValue: string) => {
// if (inputBoxRef.current) {
// inputBoxRef.current.value = newValue;
// }
// if (selectBoxRef.current) {
// selectBoxRef.current.select(enumArr?.indexOf(newValue) ?? 0);
// }
// // updateState is called automatically when the change happens
// }, [enumArr, updateState])
const resetButton = <button
disabled={val === defaultVal}
title={val === defaultVal ? 'This is the default value.' : `Revert value to '${defaultVal}'?`}
className='group btn btn-sm disabled:opacity-75 disabled:cursor-default'
onClick={() => forceState(defaultVal)}
>
<svg
className='size-5 group-disabled:stroke-current group-disabled:fill-current group-hover:stroke-red-600 group-hover:fill-red-600 duration-200'
fill='currentColor' strokeWidth='0' viewBox='0 0 16 16' height='200px' width='200px' xmlns='http://www.w3.org/2000/svg'><path fillRule='evenodd' clipRule='evenodd' d='M3.5 2v3.5L4 6h3.5V5H4.979l.941-.941a3.552 3.552 0 1 1 5.023 5.023L5.746 14.28l.72.72 5.198-5.198A4.57 4.57 0 0 0 5.2 3.339l-.7.7V2h-1z'></path>
</svg>
</button>
// const resetButton = <button
// disabled={val === defaultVal}
// title={val === defaultVal ? 'This is the default value.' : `Revert value to '${defaultVal}'?`}
// className='group btn btn-sm disabled:opacity-75 disabled:cursor-default'
// onClick={() => forceState(defaultVal)}
// >
// <svg
// className='size-5 group-disabled:stroke-current group-disabled:fill-current group-hover:stroke-red-600 group-hover:fill-red-600 duration-200'
// fill='currentColor' strokeWidth='0' viewBox='0 0 16 16' height='200px' width='200px' xmlns='http://www.w3.org/2000/svg'><path fillRule='evenodd' clipRule='evenodd' d='M3.5 2v3.5L4 6h3.5V5H4.979l.941-.941a3.552 3.552 0 1 1 5.023 5.023L5.746 14.28l.72.72 5.198-5.198A4.57 4.57 0 0 0 5.2 3.339l-.7.7V2h-1z'></path>
// </svg>
// </button>
const inputElement = enumArr === undefined ?
// string
// (<VoidInputBox
// onChangeText={updateState}
// initVal={initValRef.current}
// multiline={false}
// placeholder=''
// inputBoxRef={inputBoxRef}
// />)
<input
className='input p-1 w-full'
type='text'
value={val}
onChange={(e) => updateState(e.target.value)}
/>
:
// enum
// (<VoidSelectBox
// onChangeSelection={updateState}
// initVal={initValRef.current}
// options={enumArr}
// selectBoxRef={selectBoxRef}
// />)
(<select
className='dropdown p-1 w-full'
value={val}
onChange={(e) => updateState(e.target.value)}
>
{enumArr.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>)
// const inputElement = enumArr === undefined ?
// // string
// // (<VoidInputBox
// // onChangeText={updateState}
// // initVal={initValRef.current}
// // multiline={false}
// // placeholder=''
// // inputBoxRef={inputBoxRef}
// // />)
// <input
// className='input p-1 w-full'
// type='text'
// value={val}
// onChange={(e) => updateState(e.target.value)}
// />
// :
// // enum
// // (<VoidSelectBox
// // onChangeSelection={updateState}
// // initVal={initValRef.current}
// // options={enumArr}
// // selectBoxRef={selectBoxRef}
// // />)
// (<select
// className='dropdown p-1 w-full'
// value={val}
// onChange={(e) => updateState(e.target.value)}
// >
// {enumArr.map((option) => (
// <option key={option} value={option}>
// {option}
// </option>
// ))}
// </select>)
return <div>
<label className='hidden'>{param}</label>
<span>{description}</span>
<div className='flex items-center'>
{inputElement}
{resetButton}
</div>
</div>
}
// return <div>
// <label className='hidden'>{param}</label>
// <span>{description}</span>
// <div className='flex items-center'>
// {inputElement}
// {resetButton}
// </div>
// </div>
// }
export const SidebarSettings = () => {
// export const SidebarSettings = () => {
const configState = useConfigState()
const configStateService = useService('configStateService')
// const configState = useConfigState()
// const configStateService = useService('configStateService')
const { voidConfig } = configState
const current_field = voidConfig.default['whichApi'] as VoidConfigField
// const { voidConfig } = configState
// const current_field = voidConfig.default['whichApi'] as VoidConfigField
return (
<div className='space-y-4 py-2 overflow-y-auto'>
// return (
// <div className='space-y-4 py-2 overflow-y-auto'>
{/* choose the field */}
<div className='outline-vscode-input-bg'>
<SettingOfFieldAndParam
configState={configState}
configStateService={configStateService}
field='default'
param='whichApi'
/>
<SettingOfFieldAndParam
configState={configState}
configStateService={configStateService}
field='default'
param='maxTokens'
/>
</div>
// {/* choose the field */}
// <div className='outline-vscode-input-bg'>
// <SettingOfFieldAndParam
// configState={configState}
// configStateService={configStateService}
// field='default'
// param='whichApi'
// />
// <SettingOfFieldAndParam
// configState={configState}
// configStateService={configStateService}
// field='default'
// param='maxTokens'
// />
// </div>
<hr />
// <hr />
{/* render all fields, but hide the ones not visible for fast tab switching */}
{nonDefaultConfigFields.map(field => {
return <div
key={field}
className={`flex flex-col gap-y-2 ${field !== current_field ? 'hidden' : ''}`}
>
{Object.keys(configStateService.voidConfigInfo[field]).map((param) => (
<SettingOfFieldAndParam
key={param}
configState={configState}
configStateService={configStateService}
field={field}
param={param}
/>
))}
</div>
})}
</div>
)
}
// {/* render all fields, but hide the ones not visible for fast tab switching */}
// {nonDefaultConfigFields.map(field => {
// return <div
// key={field}
// className={`flex flex-col gap-y-2 ${field !== current_field ? 'hidden' : ''}`}
// >
// {Object.keys(configStateService.voidConfigInfo[field]).map((param) => (
// <SettingOfFieldAndParam
// key={param}
// configState={configState}
// configStateService={configStateService}
// field={field}
// param={param}
// />
// ))}
// </div>
// })}
// </div>
// )
// }

View file

@ -61,44 +61,44 @@ export const VoidInputBox = ({ onChangeText, initVal, placeholder, inputBoxRef,
// export const VoidSelectBox = ({ onChangeSelection, initVal, selectBoxRef, options }: {
// onChangeSelection: (value: string) => void;
// initVal: string;
// selectBoxRef: React.MutableRefObject<SelectBox | null>;
// options: readonly string[];
export const VoidSelectBox = ({ onChangeSelection, initVal, selectBoxRef, options }: {
onChangeSelection: (value: string) => void;
initVal: string;
selectBoxRef: React.MutableRefObject<SelectBox | null>;
options: readonly string[];
// }) => {
// const containerRef = useRef<HTMLDivElement>(null);
// const contextViewProvider = useService('contextViewService');
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const contextViewProvider = useService('contextViewService');
// useEffect(() => {
// if (!containerRef.current) return;
useEffect(() => {
if (!containerRef.current) return;
// const defaultIndex = options.indexOf(initVal);
const defaultIndex = options.indexOf(initVal);
// selectBoxRef.current = new SelectBox(
// options.map(opt => ({ text: opt })),
// defaultIndex,
// contextViewProvider,
// unthemedSelectBoxStyles
// );
selectBoxRef.current = new SelectBox(
options.map(opt => ({ text: opt })),
defaultIndex,
contextViewProvider,
unthemedSelectBoxStyles
);
// selectBoxRef.current.render(containerRef.current);
selectBoxRef.current.render(containerRef.current);
// selectBoxRef.current.onDidSelect(e => { onChangeSelection(e.selected); });
selectBoxRef.current.onDidSelect(e => { onChangeSelection(e.selected); });
// // cleanup
// return () => {
// if (selectBoxRef.current) {
// selectBoxRef.current.dispose();
// if (containerRef.current) {
// while (containerRef.current.firstChild) {
// containerRef.current.removeChild(containerRef.current.firstChild);
// }
// }
// }
// };
// }, [options, initVal, onChangeSelection, contextViewProvider, selectBoxRef]);
// cleanup
return () => {
if (selectBoxRef.current) {
selectBoxRef.current.dispose();
if (containerRef.current) {
while (containerRef.current.firstChild) {
containerRef.current.removeChild(containerRef.current.firstChild);
}
}
}
};
}, [options, initVal, onChangeSelection, contextViewProvider, selectBoxRef]);
// return <div ref={containerRef} className="w-full" />;
// };
return <div ref={containerRef} className="w-full" />;
};

View file

@ -1,7 +1,7 @@
import { useState, useEffect } from 'react'
import { ConfigState } from '../../../registerConfig.js'
import { VoidSidebarState, ReactServicesType } from '../../../registerSidebar.js'
import { ThreadsState } from '../../../registerThreads.js'
import { VoidConfigState } from '../../../../../../../platform/void/common/configTypes.js'
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
@ -10,12 +10,12 @@ let services: ReactServicesType
// even if React hasn't mounted yet, these variables are always updated to the latest state:
let sidebarState: VoidSidebarState
let configState: ConfigState
let configState: VoidConfigState
let threadsState: ThreadsState
// React listens by adding a setState function to these:
const sidebarStateListeners: Set<(s: VoidSidebarState) => void> = new Set()
const configStateListeners: Set<(s: ConfigState) => void> = new Set()
const configStateListeners: Set<(s: VoidConfigState) => void> = new Set()
const threadsStateListeners: Set<(s: ThreadsState) => void> = new Set()
// must call this before you can use any of the hooks below

View file

@ -5,40 +5,29 @@
import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { deepClone } from '../../../../base/common/objects.js';
import { IEncryptionService } from '../../../../platform/encryption/common/encryptionService.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { PartialVoidConfig, VoidConfig, nonDefaultConfigFields, voidConfigInfo, VoidConfigField, VoidConfigInfo } from '../../../../platform/void/common/configTypes.js';
import { defaultVoidConfigState, ProviderName, VoidConfigState } from '../../../../platform/void/common/configTypes.js';
const getVoidConfig = (partialVoidConfig: PartialVoidConfig): VoidConfig => {
const config = {} as PartialVoidConfig
for (const field of [...nonDefaultConfigFields, 'default'] as const) {
config[field] = {}
for (const prop in voidConfigInfo[field]) {
config[field][prop] = partialVoidConfig[field]?.[prop]?.trim() || voidConfigInfo[field][prop].defaultVal
}
}
return config as VoidConfig
}
const VOID_CONFIG_KEY = 'void.partialVoidConfig'
export type SetFieldFnType = <K extends VoidConfigField>(field: K, param: keyof VoidConfigInfo[K], newVal: string) => Promise<void>;
type SetStateFn = <K extends ProviderName>(
providerName: K,
option: keyof VoidConfigState[K],
newVal: string
) => Promise<void>;
export type ConfigState = {
partialVoidConfig: PartialVoidConfig; // free parameter
voidConfig: VoidConfig; // computed from partialVoidConfig
}
export interface IVoidConfigStateService {
readonly _serviceBrand: undefined;
readonly state: ConfigState;
readonly voidConfigInfo: VoidConfigInfo;
readonly state: VoidConfigState;
onDidChangeState: Event<void>;
setField: SetFieldFnType;
setState: SetStateFn;
}
export const IVoidConfigStateService = createDecorator<IVoidConfigStateService>('VoidConfigStateService');
@ -48,8 +37,9 @@ class VoidConfigStateService extends Disposable implements IVoidConfigStateServi
private readonly _onDidChangeState = new Emitter<void>();
readonly onDidChangeState: Event<void> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
state: ConfigState;
readonly voidConfigInfo: VoidConfigInfo = voidConfigInfo; // just putting this here for simplicity, it's static though
state: VoidConfigState;
// readonly voidConfigInfo: VoidConfigInfo = voidConfigInfo; // just putting this here for simplicity, it's static though
constructor(
@IStorageService private readonly _storageService: IStorageService,
@ -60,56 +50,48 @@ class VoidConfigStateService extends Disposable implements IVoidConfigStateServi
super()
// at the start, we haven't read the partial config yet, but we need to set state to something, just treat partialVoidConfig like it's empty
this.state = {
partialVoidConfig: {},
voidConfig: getVoidConfig({}),
}
this.state = deepClone(defaultVoidConfigState)
// read and update the actual state immediately
this._readPartialVoidConfig().then(partialVoidConfig => {
this._setState(partialVoidConfig)
this._readVoidConfigState().then(voidConfigState => {
this._setState(voidConfigState)
})
}
private async _readPartialVoidConfig(): Promise<PartialVoidConfig> {
private async _readVoidConfigState(): Promise<VoidConfigState> {
const encryptedPartialConfig = this._storageService.get(VOID_CONFIG_KEY, StorageScope.APPLICATION)
if (!encryptedPartialConfig)
return {}
return deepClone(defaultVoidConfigState)
const partialVoidConfigStr = await this._encryptionService.decrypt(encryptedPartialConfig)
return JSON.parse(partialVoidConfigStr)
const voidConfigStateStr = await this._encryptionService.decrypt(encryptedPartialConfig)
return JSON.parse(voidConfigStateStr)
}
private async _storePartialVoidConfig(partialVoidConfig: PartialVoidConfig) {
const encryptedPartialConfigStr = await this._encryptionService.encrypt(JSON.stringify(partialVoidConfig))
this._storageService.store(VOID_CONFIG_KEY, encryptedPartialConfigStr, StorageScope.APPLICATION, StorageTarget.USER)
private async _storeVoidConfigState(voidConfigState: VoidConfigState) {
const encryptedVoidConfigStr = await this._encryptionService.encrypt(JSON.stringify(voidConfigState))
this._storageService.store(VOID_CONFIG_KEY, encryptedVoidConfigStr, StorageScope.APPLICATION, StorageTarget.USER)
}
// Set field on PartialVoidConfig
setField: SetFieldFnType = async <K extends VoidConfigField>(field: K, param: keyof VoidConfigInfo[K], newVal: string) => {
const { partialVoidConfig } = this.state
const newPartialConfig: PartialVoidConfig = {
...partialVoidConfig,
[field]: {
...partialVoidConfig[field],
[param]: newVal
setState: SetStateFn = async (providerName, option, newVal) => {
const newState: VoidConfigState = {
...this.state,
[providerName]: {
...this.state[providerName],
[option]: newVal,
}
}
await this._storePartialVoidConfig(newPartialConfig)
this._setState(newPartialConfig)
await this._storeVoidConfigState(newState)
this._setState(newState)
}
// internal function to update state, should be called every time state changes
private async _setState(partialVoidConfig: PartialVoidConfig) {
this.state = {
partialVoidConfig: partialVoidConfig,
voidConfig: getVoidConfig(partialVoidConfig),
}
private async _setState(voidConfigState: VoidConfigState) {
this.state = voidConfigState
this._onDidChangeState.fire()
}

View file

@ -30,6 +30,7 @@ import { Widget } from '../../../../base/browser/ui/widget.js';
import { URI } from '../../../../base/common/uri.js';
import { LLMMessageServiceParams } from '../../../../platform/void/common/llmMessageTypes.js';
import { ISendLLMMessageService } from '../../../../platform/void/browser/llmMessageService.js';
import { ProviderName } from '../../../../platform/void/common/configTypes.js';
// gets converted to --vscode-void-greenBG, see void.css
@ -110,10 +111,18 @@ type HistorySnapshot = {
})
type StartStreamingOptions = {
type: 'ctrl+k',
providerName: ProviderName,
range: IRange
} | {
type: 'ctrl+l',
providerName: ProviderName
}
export interface IInlineDiffsService {
readonly _serviceBrand: undefined;
startStreaming(type: 'ctrl+k' | 'ctrl+l', userMessage: string): void;
startStreaming(params: StartStreamingOptions, str: string): void;
}
export const IInlineDiffsService = createDecorator<IInlineDiffsService>('inlineDiffAreasService');
@ -637,7 +646,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
private async _initializeStream(uri: URI, diffRepr: string) {
private async _initializeStream(uri: URI, diffRepr: string, providerName: ProviderName) {
// diff area begin and end line
const numLines = this._getNumLines(uri)
@ -689,7 +698,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
this.diffAreaOfId[diffArea.diffareaid] = diffArea
// actually call the LLM
const { voidConfig } = this._voidConfigStateService.state
const voidConfigState = this._voidConfigStateService.state
const promptContent = `\
ORIGINAL_CODE
\`\`\`
@ -761,7 +770,8 @@ Please finish writing the new file by applying the diff to the original file. Re
diffArea._sweepState = { isStreaming: false, line: null }
resolve();
},
voidConfig,
voidConfig: voidConfigState,
providerName,
}
streamRequestId = this._sendLLMMessageService.sendLLMMessage(object)
@ -776,7 +786,7 @@ Please finish writing the new file by applying the diff to the original file. Re
async startStreaming(type: 'ctrl+k' | 'ctrl+l', userMessage: string) {
async startStreaming(params: StartStreamingOptions, userMessage: string) {
const editor = this._editorService.getActiveCodeEditor()
if (!editor) return
@ -788,7 +798,7 @@ Please finish writing the new file by applying the diff to the original file. Re
// TODO deselect user's cursor
this._initializeStream(uri, userMessage)
this._initializeStream(uri, userMessage, params.providerName)
}

View file

@ -53,8 +53,7 @@ import { IClipboardService } from '../../../../platform/clipboard/common/clipboa
// import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
// compare against search.contribution.ts and https://app.greptile.com/chat/w1nsmt3lauwzculipycpn?repo=github%3Amain%3Amicrosoft%2Fvscode
// and debug.contribution.ts, scm.contribution.ts (source control)
// compare against search.contribution.ts and debug.contribution.ts, scm.contribution.ts (source control)
export type VoidSidebarState = {
isHistoryOpen: boolean;
@ -91,10 +90,6 @@ class VoidSidebarViewPane extends ViewPane {
@IOpenerService openerService: IOpenerService,
@ITelemetryService telemetryService: ITelemetryService,
@IHoverService hoverService: IHoverService,
// Void:
// @IVoidSidebarStateService private readonly _voidSidebarStateService: IVoidSidebarStateService,
// @IThreadHistoryService private readonly _threadHistoryService: IThreadHistoryService,
// TODO chat service
) {
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, hoverService)