error state, sendLLMMessage fix, continue adding state for provider/model pair selection

This commit is contained in:
Andrew Pareles 2024-12-10 23:29:39 -08:00
parent 49b43820b2
commit bb4f99f687
37 changed files with 333 additions and 196 deletions

View file

@ -1245,7 +1245,6 @@ export class CodeApplication extends Disposable {
// Void
const metricsChannel = ProxyChannel.fromService(accessor.get(IMetricsService), disposables);
mainProcessElectronServer.registerChannel('void-channel-metrics', metricsChannel);
const sendLLMMessageChannel = new LLMMessageChannel(accessor.get(IMetricsService));
mainProcessElectronServer.registerChannel('void-channel-sendLLMMessage', sendLLMMessageChannel);

View file

@ -11,6 +11,8 @@ import { generateUuid } from '../../../base/common/uuid.js';
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { Event } from '../../../base/common/event.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { IVoidConfigStateService } from '../common/voidConfigService.js';
import { INotificationService } from '../../notification/common/notification.js';
// BROWSER IMPLEMENTATION OF SENDLLMMESSAGE
@ -19,7 +21,7 @@ export const ISendLLMMessageService = createDecorator<ISendLLMMessageService>('s
// defines an interface that node/ creates and browser/ uses
export interface ISendLLMMessageService {
readonly _serviceBrand: undefined;
sendLLMMessage: (params: LLMMessageServiceParams) => string;
sendLLMMessage: (params: LLMMessageServiceParams) => string | null;
abort: (requestId: string) => void;
}
@ -34,12 +36,14 @@ export class SendLLMMessageService extends Disposable implements ISendLLMMessage
private readonly onErrorHooks: { [eventId: string]: ((params: ProxyOnErrorPayload) => void) } = {}
constructor(
@IMainProcessService mainProcessService: IMainProcessService // used as a renderer (only usable on client side)
@IMainProcessService private readonly mainProcessService: IMainProcessService, // used as a renderer (only usable on client side)
@IVoidConfigStateService private readonly voidConfigStateService: IVoidConfigStateService,
@INotificationService private readonly notificationService: INotificationService,
) {
super()
// const service = ProxyChannel.toService<LLMMessageChannel>(mainProcessService.getChannel('void-channel-sendLLMMessage')); // lets you call it like a service
this.channel = mainProcessService.getChannel('void-channel-sendLLMMessage')
this.channel = this.mainProcessService.getChannel('void-channel-sendLLMMessage')
// this sets up an IPC channel and takes a few ms, so we set up listeners immediately and add hooks to them instead
const onTextEvent: Event<ProxyOnTextPayload> = this.channel.listen('onText')
@ -77,9 +81,25 @@ export class SendLLMMessageService extends Disposable implements ISendLLMMessage
this.onFinalMessageHooks[requestId_] = onFinalMessage
this.onErrorHooks[requestId_] = onError
const { featureName } = params
// params will be stripped of all its functions
this.channel.call('sendLLMMessage', { ...proxyParams, requestId: requestId_ } satisfies ProxyLLMMessageParams);
const stateOfFeature = this.voidConfigStateService.state.modelSelectionOfFeature[featureName]
if (stateOfFeature === null) {
this.notificationService.warn('Please add a Provider in Settings!')
return null
}
const { providerName, modelName } = stateOfFeature
const { settingsOfProvider } = this.voidConfigStateService.state
this.channel.call('sendLLMMessage', {
...proxyParams,
requestId: requestId_,
providerName,
modelName,
settingsOfProvider,
} satisfies ProxyLLMMessageParams);
return requestId_
}

View file

@ -3,11 +3,10 @@
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
import { ProviderName, VoidProviderState } from './configTypes'
import { IRange } from '../../../editor/common/core/range'
import { ProviderName, SettingsOfProvider } from './voidConfigTypes'
// ---------- type definitions ----------
export type OnText = (p: { newText: string, fullText: string }) => void
export type OnFinalMessage = (p: { fullText: string }) => void
@ -21,42 +20,67 @@ export type LLMMessage = {
content: string;
}
export type LLMFeatureSelection = {
featureName: 'Ctrl+K',
range: IRange
} | {
featureName: 'Ctrl+L',
} | {
featureName: 'Autocomplete',
range: IRange
}
export type LLMMessageServiceParams = {
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: OnError;
messages: LLMMessage[];
voidConfig: VoidProviderState | null;
logging: {
loggingName: string,
};
providerName: ProviderName;
}
} & LLMFeatureSelection
// params to the true sendLLMMessage function
export type SendLLMMMessageParams = {
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: OnError;
abortRef: AbortRef;
messages: LLMMessage[];
voidConfig: VoidProviderState | null;
logging: {
loggingName: string,
};
providerName: ProviderName;
abortRef: AbortRef;
modelName: string;
settingsOfProvider: SettingsOfProvider;
}
// can't send functions across a proxy, use listeners instead
export const listenerNames = ['onText', 'onFinalMessage', 'onError'] as const
export type ProxyLLMMessageParams = Omit<LLMMessageServiceParams, typeof listenerNames[number]> & { requestId: string }
export type BlockedProxyParams = 'onText' | 'onFinalMessage' | 'onError' | 'abortRef'
export type ProxyLLMMessageParams = Omit<SendLLMMMessageParams, BlockedProxyParams> & { requestId: string }
export type ProxyOnTextPayload = Parameters<OnText>[0] & { requestId: string }
export type ProxyOnFinalMessagePayload = Parameters<OnFinalMessage>[0] & { requestId: string }
export type ProxyOnErrorPayload = Parameters<OnError>[0] & { requestId: string }
export type ProxyLLMMessageAbortParams = { requestId: string }
export type SendLLMMessageFnTypeInternal = (params: {
messages: LLMMessage[];
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: OnError;
settingsOfProvider: SettingsOfProvider;
providerName: ProviderName;
modelName: string;
_setAborter: (aborter: () => void) => void;
}) => void

View file

@ -1,3 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from '../../instantiation/common/instantiation.js';
export interface IMetricsService {

View file

@ -3,15 +3,14 @@
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
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 { defaultVoidProviderState, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider } from '../../../../platform/void/common/configTypes.js';
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 '../../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 { defaultVoidProviderState, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider } from './voidConfigTypes.js';
const CONFIG_STORAGE_KEY = 'void.voidConfigStateII'
@ -89,7 +88,7 @@ class VoidConfigStateService extends Disposable implements IVoidConfigStateServi
private async _storeVoidConfigState(voidConfigState: VoidConfigState) {
const encryptedVoidConfigStr = await this._encryptionService.encrypt(JSON.stringify(voidConfigState))
this._storageService.store(CONFIG_STORAGE_KEY, encryptedVoidConfigStr, StorageScope.APPLICATION, StorageTarget.USER)
this._storageService.store(CONFIG_STORAGE_KEY, encryptedVoidConfigStr, StorageScope.APPLICATION, StorageTarget.USER);
}
setSettingOfProvider: SetSettingOfProviderFn = async (providerName, option, newVal) => {

View file

@ -1,14 +1,15 @@
import Anthropic from '@anthropic-ai/sdk';
import { parseMaxTokensStr, SendLLMMessageFnTypeInternal } from './util.js';
import { parseMaxTokensStr } from './util.js';
import { SendLLMMessageFnTypeInternal } from '../../common/llmMessageTypes.js';
// Anthropic
type LLMMessageAnthropic = {
role: 'user' | 'assistant';
content: string;
}
export const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
export const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
const thisConfig = voidConfig.anthropic
const thisConfig = settingsOfProvider.anthropic
const anthropic = new Anthropic({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true });
@ -24,7 +25,7 @@ export const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onTex
const stream = anthropic.messages.stream({
system: systemMessage,
messages: anthropicMessages,
model: thisConfig.model,
model: modelName,
max_tokens: parseMaxTokensStr(thisConfig.maxTokens)!, // this might be undefined, but it will just throw an error for the user to see
});

View file

@ -1,15 +1,15 @@
import { Content, GoogleGenerativeAI, GoogleGenerativeAIFetchError } from '@google/generative-ai';
import { SendLLMMessageFnTypeInternal } from './util';
import { SendLLMMessageFnTypeInternal } from '../../common/llmMessageTypes.js';
// Gemini
export const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
export const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
let fullText = ''
const thisConfig = voidConfig.gemini
const thisConfig = settingsOfProvider.gemini
const genAI = new GoogleGenerativeAI(thisConfig.apiKey);
const model = genAI.getGenerativeModel({ model: thisConfig.model });
const model = genAI.getGenerativeModel({ model: modelName });
// remove system messages that get sent to Gemini
// str of all system messages

View file

@ -3,13 +3,13 @@
// // https://docs.greptile.com/api-reference/query
// // https://docs.greptile.com/quickstart#sample-response-streamed
// import { SendLLMMessageFnTypeInternal } from './util';
// import { SendLLMMessageFnTypeInternal } from '../../common/llmMessageTypes.js';
// export const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
// export const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, _setAborter }) => {
// let fullText = ''
// const thisConfig = voidConfig.greptile
// const thisConfig = settingsOfProvider.greptile
// fetch('https://api.greptile.com/v2/query', {
// method: 'POST',

View file

@ -1,12 +1,12 @@
import Groq from 'groq-sdk';
import { SendLLMMessageFnTypeInternal } from './util';
import { SendLLMMessageFnTypeInternal } from '../../common/llmMessageTypes.js';
import { parseMaxTokensStr } from './util.js';
// Groq
export const sendGroqMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
export const sendGroqMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
let fullText = '';
const thisConfig = voidConfig.groq
const thisConfig = settingsOfProvider.groq
const groq = new Groq({
apiKey: thisConfig.apiKey,
@ -16,7 +16,7 @@ export const sendGroqMsg: SendLLMMessageFnTypeInternal = async ({ messages, onTe
await groq.chat.completions
.create({
messages: messages,
model: thisConfig.model,
model: modelName,
stream: true,
temperature: 0.7,
max_tokens: parseMaxTokensStr(thisConfig.maxTokens),

View file

@ -1,18 +1,18 @@
import { Ollama } from 'ollama';
import { SendLLMMessageFnTypeInternal } from './util';
import { SendLLMMessageFnTypeInternal } from '../../common/llmMessageTypes.js';
import { parseMaxTokensStr } from './util.js';
// Ollama
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
const thisConfig = voidConfig.ollama
const thisConfig = settingsOfProvider.ollama
let fullText = ''
const ollama = new Ollama({ host: thisConfig.endpoint })
ollama.chat({
model: thisConfig.model,
model: modelName,
messages: messages,
stream: true,
options: { num_predict: parseMaxTokensStr(thisConfig.maxTokens) } // this is max_tokens

View file

@ -1,10 +1,10 @@
import OpenAI from 'openai';
import { SendLLMMessageFnTypeInternal } from './util';
import { SendLLMMessageFnTypeInternal } from '../../common/llmMessageTypes.js';
import { parseMaxTokensStr } from './util.js';
// OpenAI, OpenRouter, OpenAICompatible
export const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, providerName }) => {
export const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) => {
let fullText = ''
@ -13,12 +13,12 @@ export const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText,
if (providerName === 'openAI') {
const thisConfig = voidConfig.openAI
const thisConfig = settingsOfProvider.openAI
openai = new OpenAI({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true });
options = { model: thisConfig.model, messages: messages, stream: true, max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens) }
options = { model: modelName, messages: messages, stream: true, max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens) }
}
else if (providerName === 'openRouter') {
const thisConfig = voidConfig.openRouter
const thisConfig = settingsOfProvider.openRouter
openai = new OpenAI({
baseURL: 'https://openrouter.ai/api/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true,
defaultHeaders: {
@ -26,12 +26,12 @@ export const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText,
'X-Title': 'Void Editor', // Optional. Shows in rankings on openrouter.ai.
},
});
options = { model: thisConfig.model, messages: messages, stream: true, max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens) }
options = { model: modelName, messages: messages, stream: true, max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens) }
}
else if (providerName === 'openAICompatible') {
const thisConfig = voidConfig.openAICompatible
const thisConfig = settingsOfProvider.openAICompatible
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) }
options = { model: modelName, messages: messages, stream: true, max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens) }
}
else {
console.error(`sendOpenAIMsg: invalid providerName: ${providerName}`)

View file

@ -13,14 +13,14 @@ export const sendLLMMessage = ({
onFinalMessage: onFinalMessage_,
onError: onError_,
abortRef: abortRef_,
voidConfig,
logging: { loggingName },
providerName
settingsOfProvider,
providerName,
modelName,
}: SendLLMMMessageParams,
metricsService: IMetricsService
) => {
if (!voidConfig) return;
// trim message content (Anthropic and other providers give an error if there is trailing whitespace)
messages = messages.map(m => ({ ...m, content: m.content.trim() }))
@ -74,21 +74,21 @@ export const sendLLMMessage = ({
try {
switch (providerName) {
case 'anthropic':
sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, providerName });
sendAnthropicMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
break;
case 'openAI':
case 'openRouter':
case 'openAICompatible':
sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, providerName });
sendOpenAIMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
break;
case 'gemini':
sendGeminiMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, providerName });
sendGeminiMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
break;
case 'ollama':
sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, providerName });
sendOllamaMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
break;
case 'groq':
sendGroqMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, providerName });
sendGroqMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
break;
default:
onError({ error: `Error: whichApi was "${providerName}", which is not recognized!` })

View file

@ -1,6 +1,3 @@
import { ProviderName, VoidProviderState } from '../../common/configTypes'
import { LLMMessage, OnText, OnFinalMessage, OnError } from '../../common/llmMessageTypes'
export const parseMaxTokensStr = (maxTokensStr: string) => {
// parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN
const int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr)
@ -10,13 +7,3 @@ export const parseMaxTokensStr = (maxTokensStr: string) => {
}
export type SendLLMMessageFnTypeInternal = (params: {
messages: LLMMessage[];
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: OnError;
voidConfig: VoidProviderState;
providerName: ProviderName;
_setAborter: (aborter: () => void) => void;
}) => void

View file

@ -8,7 +8,7 @@
import { IServerChannel } from '../../../base/parts/ipc/common/ipc.js';
import { Emitter, Event } from '../../../base/common/event.js';
import { listenerNames, ProxyOnTextPayload, ProxyOnErrorPayload, ProxyOnFinalMessagePayload, ProxyLLMMessageParams, AbortRef, SendLLMMMessageParams, ProxyLLMMessageAbortParams } from '../common/llmMessageTypes.js';
import { BlockedProxyParams, ProxyOnTextPayload, ProxyOnErrorPayload, ProxyOnFinalMessagePayload, ProxyLLMMessageParams, AbortRef, SendLLMMMessageParams, ProxyLLMMessageAbortParams } from '../common/llmMessageTypes.js';
import { sendLLMMessage } from './llmMessage/sendLLMMessage.js'
import { IMetricsService } from '../common/metricsService.js';
@ -28,12 +28,15 @@ export class LLMMessageChannel implements IServerChannel {
private readonly _abortRefOfRequestId: Record<string, AbortRef> = {}
// stupidly, channels can't take in @IService
constructor(
private readonly metricsService: IMetricsService
) { }
private readonly metricsService: IMetricsService,
) {
}
// browser uses this to listen for changes
listen(_: unknown, event: typeof listenerNames[number]): Event<any> {
listen(_: unknown, event: BlockedProxyParams): Event<any> {
if (event === 'onText') {
return this.onText;
}

View file

@ -742,18 +742,6 @@ export class CodeWindow extends BaseWindow implements ICodeWindow {
cb({ cancel: false, requestHeaders: Object.assign(details.requestHeaders, headers) });
});
// // Void: send from https://
// this._win.webContents.session.webRequest.onBeforeSendHeaders({ urls }, async (details, cb) => {
// // const voidConfig = this.voidConfigStateService.state.voidConfig
// // const whichApi = voidConfig.default['whichApi']
// const endpoint = 'http://127.' //string | undefined = voidConfig[whichApi as VoidConfigField].endpoint
// if (endpoint && details.url.startsWith(endpoint)) {
// details.requestHeaders['Origin'] = 'https://app.voideditor.com'
// }
// cb({ cancel: false, requestHeaders: details.requestHeaders });
// });
}

View file

@ -1,4 +1,9 @@
.monaco-editor .void-sweepIdxBG {
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
.monaco-editor .void-sweepIdxBG {
background-color: var(--vscode-void-sweepIdxBG);
}

View file

@ -1,3 +1,7 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
import { CodeSelection } from '../registerThreads.js';

View file

@ -1,5 +1,7 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
// // used for ctrl+l
// const partialGenerationInstructions = ``
@ -9,6 +11,31 @@
// const fimInstructions = ``
// CTRL+K prompt:
// const promptContent = `Here is the user's original selection:
// \`\`\`
// <MID>${selection}</MID>
// \`\`\`
// The user wants to apply the following instructions to the selection:
// ${instructions}
// Please rewrite the selection following the user's instructions.
// Instructions to follow:
// 1. Follow the user's instructions
// 2. You may ONLY CHANGE the selection, and nothing else in the file
// 3. Make sure all brackets in the new selection are balanced the same was as in the original selection
// 3. Be careful not to duplicate or remove variables, comments, or other syntax by mistake
// Complete the following:
// \`\`\`
// <PRE>${prefix}</PRE>
// <SUF>${suffix}</SUF>
// <MID>`;
export const generateDiffInstructions = `
You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`.

View file

@ -1,3 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
import React, { JSX, useCallback, useEffect, useState } from 'react'
import { marked, MarkedToken, Token } from 'marked'
import { BlockCode } from './BlockCode.js'
@ -44,7 +49,7 @@ const CodeButtonsOnHover = ({ diffRepr: text }: { diffRepr: string }) => {
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
onClick={async () => {
inlineDiffService.startStreaming({ type: 'ctrl+l', providerName: 'anthropic' }, text)
inlineDiffService.startStreaming({ featureName: 'Ctrl+L' }, text)
}}
>
Apply

View file

@ -0,0 +1,65 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
import React, { Component, ErrorInfo, ReactNode } from 'react';
import { ErrorDisplay } from './ErrorDisplay.js';
interface Props {
children: ReactNode;
fallback?: ReactNode;
onDismiss?: () => void;
}
interface State {
hasError: boolean;
error: Error | null;
errorInfo: ErrorInfo | null;
}
class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null
};
}
static getDerivedStateFromError(error: Error): Partial<State> {
return {
hasError: true,
error
};
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
this.setState({
error,
errorInfo
});
}
render(): ReactNode {
if (this.state.hasError && this.state.error) {
// If a custom fallback is provided, use it
if (this.props.fallback) {
return this.props.fallback;
}
// Use ErrorDisplay component as the default error UI
return (
<ErrorDisplay
error={this.state.error}
onDismiss={this.props.onDismiss || null}
/>
);
}
return this.props.children;
}
}
export default ErrorBoundary;

View file

@ -1,3 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
import React, { useState } from 'react';
import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react';

View file

@ -17,6 +17,7 @@ import { SidebarThreadSelector } from './SidebarThreadSelector.js';
import { SidebarChat } from './SidebarChat.js';
import { SidebarModelSettings } from './SidebarModelSettings.js';
import { SidebarProviderSettings } from './SidebarProviderSettings.js';
import ErrorBoundary from './ErrorBoundary.js';
const Sidebar = () => {
const sidebarState = useSidebarState()
@ -33,17 +34,25 @@ const Sidebar = () => {
}}>clickme {tab}</span> */}
<div className={`mb-2 ${isHistoryOpen ? '' : 'hidden'}`}>
<SidebarThreadSelector />
<ErrorBoundary>
<SidebarThreadSelector />
</ErrorBoundary>
</div>
<div className={`${tab === 'chat' ? '' : 'hidden'}`}>
<SidebarChat />
<ErrorBoundary>
<SidebarChat />
</ErrorBoundary>
</div>
<div className={`${tab === 'settings' ? '' : 'hidden'}`}>
<SidebarModelSettings />
<ErrorBoundary>
<SidebarModelSettings />
</ErrorBoundary>
--------
<SidebarProviderSettings />
<ErrorBoundary>
<SidebarProviderSettings />
</ErrorBoundary>
</div>
</div>

View file

@ -2,6 +2,7 @@
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
import React, { FormEvent, Fragment, useCallback, useEffect, useRef, useState } from 'react';
@ -219,8 +220,8 @@ export const SidebarChat = () => {
setLatestError(error)
},
voidConfig: voidConfigState,
providerName: 'anthropic',
featureName: 'Ctrl+L',
}
const latestRequestId = sendLLMMessageService.sendLLMMessage(object)

View file

@ -3,14 +3,17 @@
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
import { FeatureName, featureNames, providerNames } from '../../../../../../../platform/void/common/configTypes.js'
import { useConfigState } from '../util/services.js'
import { FeatureName, featureNames, providerNames } from '../../../../../../../platform/void/common/voidConfigTypes.js'
import { useConfigState, useService } from '../util/services.js'
import ErrorBoundary from './ErrorBoundary.js'
import { VoidSelectBox } from './inputs.js'
export const SidebarModelSettingsForFeature = ({ featureName }: { featureName: FeatureName }) => {
const voidConfigService = useService('configStateService')
const voidConfigState = useConfigState()
const models: [string, string][] = []
@ -22,10 +25,21 @@ export const SidebarModelSettingsForFeature = ({ featureName }: { featureName: F
})
}
return <>
<h1>Settings - {featureName}</h1>
{models.map(([providerName, model], i) => <p key={i}>{providerName} - {model}</p>)}
</>
return <><ErrorBoundary>
<h2>{'Models'}</h2>
{models.length === 0 ?
<p>{'Please add a provider!'}</p>
:
<VoidSelectBox
initVal={models[0].join(' - ')}
options={models.map(s => s.join(' - '))}
onChangeSelection={(newVal) => { /*voidConfigService.setFeatureState(providerName, 'model', newVal)*/ }}
selectBoxRef={{ current: null }}
/>}
{/* <h1>Settings - {featureName}</h1> */}
{/* {models.map(([providerName, model], i) => <p key={i}>{providerName} - {model}</p>)} */}
</ErrorBoundary></>
}
export const SidebarModelSettings = () => {

View file

@ -4,10 +4,11 @@
*--------------------------------------------------------------------------------------------*/
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import { displayInfoOfSettingName, ProviderName, providerNames, ProviderSettingName, VoidProviderState } from '../../../../../../../platform/void/common/configTypes.js'
import { displayInfoOfSettingName, ProviderName, providerNames } from '../../../../../../../platform/void/common/voidConfigTypes.js'
import { VoidCheckBox, VoidInputBox, VoidSelectBox } from './inputs.js'
import { useConfigState, useService } from '../util/services.js'
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'
import ErrorBoundary from './ErrorBoundary.js'
const Setting = ({ providerName, settingName }: { providerName: ProviderName, settingName: any }) => {
@ -21,8 +22,12 @@ const Setting = ({ providerName, settingName }: { providerName: ProviderName, se
// this is really just to sync the state on initial mount, when init value hasn't been set yet
const syncState = () => {
if (!instanceRef.current) return
const settingsAtProvider = voidConfigService.state.settingsOfProvider[providerName];
// @ts-ignore
const stateVal = voidConfigService.state[providerName][settingName]
const stateVal = settingsAtProvider[settingName]
if (instanceRef.current.value !== stateVal)
instanceRef.current.value = stateVal
}
@ -31,44 +36,30 @@ const Setting = ({ providerName, settingName }: { providerName: ProviderName, se
return () => disposable.dispose()
}, [instanceRef, voidConfigService])
return <>
return <><ErrorBoundary>
<h2>{title}</h2>
{<VoidInputBox
placeholder={placeholder}
onChangeText={useCallback((newVal) => {
voidConfigService.setState(providerName, settingName, newVal)
voidConfigService.setSettingOfProvider(providerName, settingName, newVal)
}, [voidConfigService, providerName, settingName])
}
onCreateInstance={instanceRef}
multiline={false}
/>}
</>
</ErrorBoundary></>
}
const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) => {
const voidConfigState = useConfigState()
const { models, model, ...others } = voidConfigState[providerName]
const voidConfigService = useService('configStateService')
const { models, ...others } = voidConfigState[providerName]
return <>
<h1>{providerName}</h1>
{/* other settings (e.g. api key) */}
{/* settings besides models (e.g. api key) */}
{Object.keys(others).map((settingName, i) => {
return <Setting key={settingName} providerName={providerName} settingName={settingName} />
})}
<h2>{'Models'}</h2>
{models === null ?
<p>{'No models available.'}</p>
: <VoidSelectBox
initVal={models[0]}
options={models}
onChangeSelection={(newVal) => { voidConfigService.setState(providerName, 'model', newVal) }}
selectBoxRef={{ current: null }}
/>}
</>
}

View file

@ -2,6 +2,7 @@
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
import React from "react";
import { useService, useThreadsState } from '../util/services.js';

View file

@ -1,3 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
import React, { useCallback, useEffect, useRef } from 'react';
import { useService } from '../util/services.js';
import { HistoryInputBox, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
@ -102,7 +107,7 @@ export const VoidSelectBox = ({ onChangeSelection, initVal, selectBoxRef, option
const defaultIndex = options.indexOf(initVal);
selectBoxRef.current = new SelectBox(
options.map(opt => ({ text: opt })),
options.map(opt => ({ text: opt, detail: 'detail', description: 'description' })),
defaultIndex,
contextViewProvider,
unthemedSelectBoxStyles

View file

@ -1,3 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
import { diffLines, Change } from 'diff';
export { diffLines, Change }

View file

@ -1,3 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
import React, { useEffect, useState } from 'react';
import * as ReactDOM from 'react-dom/client'
import { ReactServicesType, VoidSidebarState } from '../../../registerSidebar.js';

View file

@ -1,7 +1,12 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
import { useState, useEffect } from 'react'
import { VoidSidebarState, ReactServicesType } from '../../../registerSidebar.js'
import { ThreadsState } from '../../../registerThreads.js'
import { VoidProviderState } from '../../../../../../../platform/void/common/configTypes.js'
import { SettingsOfProvider } from '../../../../../../../platform/void/common/voidConfigTypes.js'
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
@ -10,13 +15,13 @@ let services: ReactServicesType
// even if React hasn't mounted yet, these variables are always updated to the latest state:
let sidebarState: VoidSidebarState
let configState: VoidProviderState
let threadsState: ThreadsState
let settingsOfProvider: SettingsOfProvider
// React listens by adding a setState function to these:
const sidebarStateListeners: Set<(s: VoidSidebarState) => void> = new Set()
const configStateListeners: Set<(s: VoidProviderState) => void> = new Set()
const threadsStateListeners: Set<(s: ThreadsState) => void> = new Set()
const settingsOfProviderListeners: Set<(s: SettingsOfProvider) => void> = new Set()
// must call this before you can use any of the hooks below
// this should only be called ONCE! this is the only place you don't need to dispose onDidChange. If you use state.onDidChange anywhere else, make sure to dispose it!
@ -25,7 +30,7 @@ let wasCalled = false
export const _registerServices = (services_: ReactServicesType) => {
if (wasCalled) console.error(`void _registerServices was called again! It should only be called once.`)
if (wasCalled) console.error(`⚠️ Void _registerServices was called again! It should only be called once.`)
wasCalled = true
services = services_
@ -37,11 +42,6 @@ export const _registerServices = (services_: ReactServicesType) => {
sidebarStateListeners.forEach(l => l(sidebarState))
})
configState = configStateService.state
configStateService.onDidChangeState(() => {
configState = configStateService.state
configStateListeners.forEach(l => l(configState))
})
threadsState = threadsStateService.state
threadsStateService.onDidChangeCurrentThread(() => {
@ -49,15 +49,20 @@ export const _registerServices = (services_: ReactServicesType) => {
threadsStateListeners.forEach(l => l(threadsState))
})
settingsOfProvider = configStateService.state.settingsOfProvider
configStateService.onDidChangeState(() => {
settingsOfProvider = configStateService.state.settingsOfProvider
settingsOfProviderListeners.forEach(l => l(settingsOfProvider))
})
}
// -- services --
export const useService = <T extends keyof ReactServicesType,>(serviceName: T) => {
export const useService = <T extends keyof ReactServicesType,>(serviceName: T): ReactServicesType[T] => {
if (services === null) {
throw new Error('useAccessor must be used within an AccessorProvider')
}
return services[serviceName] as ReactServicesType[T]
return services[serviceName]
}
// -- state of services --
@ -73,11 +78,11 @@ export const useSidebarState = () => {
}
export const useConfigState = () => {
const [s, ss] = useState(configState)
const [s, ss] = useState(settingsOfProvider)
useEffect(() => {
ss(configState)
configStateListeners.add(ss)
return () => { configStateListeners.delete(ss) }
ss(settingsOfProvider)
settingsOfProviderListeners.add(ss)
return () => { settingsOfProviderListeners.delete(ss) }
}, [ss])
return s
}

View file

@ -12,7 +12,6 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
import { CodeStagingSelection, IThreadHistoryService } from './registerThreads.js';
// import { IVoidConfigService } from './registerSettings.js';
// import { IEditorService } from '../../../services/editor/common/editorService.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';

View file

@ -7,7 +7,6 @@ import { Disposable } from '../../../../base/common/lifecycle.js';
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IVoidConfigStateService } from './registerConfig.js';
import { ITextModel } from '../../../../editor/common/model.js';
import { Position } from '../../../../editor/common/core/position.js';
import { InlineCompletion, InlineCompletionContext } from '../../../../editor/common/languages.js';
@ -516,7 +515,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
const disabled = true
const testMode = false
if (disabled) { return []; }
if (disabled) return [];
const docUriStr = model.uri.toString();
@ -676,8 +675,8 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
newAutocompletion.status = 'error'
reject(error)
},
providerName: 'anthropic',
voidConfig: this._voidConfigStateService.state,
featureName: 'Autocomplete',
range: { startLineNumber: position.lineNumber, startColumn: position.column, endLineNumber: position.lineNumber, endColumn: position.column },
})
newAutocompletion.requestId = requestId
@ -714,7 +713,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
constructor(
@ILanguageFeaturesService private _langFeatureService: ILanguageFeaturesService,
@IVoidConfigStateService private readonly _voidConfigStateService: IVoidConfigStateService,
@ISendLLMMessageService private readonly _sendLLMMessageService: ISendLLMMessageService,
@IEditorService private readonly _editorService: IEditorService,
@IModelService private readonly _modelService: IModelService,

View file

@ -11,7 +11,7 @@ import { ICodeEditor, IOverlayWidget, IViewZone } from '../../../../editor/brows
// import { IUndoRedoService } from '../../../../platform/undoRedo/common/undoRedo.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
// import { throttle } from '../../../../base/common/decorators.js';
import { IVoidConfigStateService } from './registerConfig.js';
// import { IVoidConfigStateService } from './registerConfig.js';
import { writeFileWithDiffInstructions } from './prompt/systemPrompts.js';
import { ComputedDiff, findDiffs } from './findDiffs.js';
import { EndOfLinePreference, ITextModel } from '../../../../editor/common/model.js';
@ -28,9 +28,8 @@ import { ILanguageService } from '../../../../editor/common/languages/language.j
import * as dom from '../../../../base/browser/dom.js';
import { Widget } from '../../../../base/browser/ui/widget.js';
import { URI } from '../../../../base/common/uri.js';
import { LLMMessageServiceParams } from '../../../../platform/void/common/llmMessageTypes.js';
import { LLMFeatureSelection, 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
@ -104,25 +103,17 @@ type HistorySnapshot = {
entireFileCode: string;
} &
({
type: 'ctrl+k';
type: 'Ctrl+K';
ctrlKText: string;
} | {
type: 'ctrl+l';
type: 'Ctrl+L';
})
type StartStreamingOptions = {
type: 'ctrl+k',
providerName: ProviderName,
range: IRange
} | {
type: 'ctrl+l',
providerName: ProviderName
}
export interface IInlineDiffsService {
readonly _serviceBrand: undefined;
startStreaming(params: StartStreamingOptions, str: string): void;
startStreaming(params: LLMFeatureSelection, str: string): void;
}
export const IInlineDiffsService = createDecorator<IInlineDiffsService>('inlineDiffAreasService');
@ -153,7 +144,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
constructor(
// @IHistoryService private readonly _historyService: IHistoryService, // history service is the history of pressing alt left/right
@IVoidConfigStateService private readonly _voidConfigStateService: IVoidConfigStateService,
// @IVoidConfigStateService private readonly _voidConfigStateService: IVoidConfigStateService,
@ICodeEditorService private readonly _editorService: ICodeEditorService,
@IModelService private readonly _modelService: IModelService,
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService, // undoRedo service is the history of pressing ctrl+z
@ -379,7 +370,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
return {
snapshottedDiffAreaOfId,
entireFileCode: this._readURI(uri) ?? '', // the whole file's code
type: 'ctrl+l',
type: 'Ctrl+L',
}
}
@ -646,7 +637,7 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
private async _initializeStream(uri: URI, diffRepr: string, providerName: ProviderName) {
private async _initializeStream(opts: LLMFeatureSelection, diffRepr: string, uri: URI,) {
// diff area begin and end line
const numLines = this._getNumLines(uri)
@ -698,7 +689,6 @@ class InlineDiffsService extends Disposable implements IInlineDiffsService {
this.diffAreaOfId[diffArea.diffareaid] = diffArea
// actually call the LLM
const voidConfigState = this._voidConfigStateService.state
const promptContent = `\
ORIGINAL_CODE
\`\`\`
@ -715,29 +705,6 @@ Please finish writing the new file by applying the diff to the original file. Re
`
// CTRL+K prompt:
// const promptContent = `Here is the user's original selection:
// \`\`\`
// <MID>${selection}</MID>
// \`\`\`
// The user wants to apply the following instructions to the selection:
// ${instructions}
// Please rewrite the selection following the user's instructions.
// Instructions to follow:
// 1. Follow the user's instructions
// 2. You may ONLY CHANGE the selection, and nothing else in the file
// 3. Make sure all brackets in the new selection are balanced the same was as in the original selection
// 3. Be careful not to duplicate or remove variables, comments, or other syntax by mistake
// Complete the following:
// \`\`\`
// <PRE>${prefix}</PRE>
// <SUF>${suffix}</SUF>
// <MID>`;
await new Promise<void>((resolve, reject) => {
let streamRequestId: string | null = null
@ -770,8 +737,7 @@ Please finish writing the new file by applying the diff to the original file. Re
diffArea._sweepState = { isStreaming: false, line: null }
resolve();
},
voidConfig: voidConfigState,
providerName,
...opts
}
streamRequestId = this._sendLLMMessageService.sendLLMMessage(object)
@ -786,7 +752,7 @@ Please finish writing the new file by applying the diff to the original file. Re
async startStreaming(params: StartStreamingOptions, userMessage: string) {
async startStreaming(opts: LLMFeatureSelection, userMessage: string) {
const editor = this._editorService.getActiveCodeEditor()
if (!editor) return
@ -798,7 +764,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, params.providerName)
this._initializeStream(opts, userMessage, uri)
}

View file

@ -42,7 +42,7 @@ import { IHoverService } from '../../../../platform/hover/browser/hover.js';
import mountFn from './react/out/sidebar-tsx/Sidebar.js';
import { IVoidConfigStateService } from './registerConfig.js';
import { IVoidConfigStateService } from '../../../../platform/void/common/voidConfigService.js';
import { IFileService } from '../../../../platform/files/common/files.js';
import { IInlineDiffsService } from './registerInlineDiffs.js';
import { IModelService } from '../../../../editor/common/services/model.js';

View file

@ -7,7 +7,7 @@
import './registerActions.js'
// register Settings
import './registerConfig.js' // TODO move this to platform
import '../../../../platform/void/common/voidConfigService.js' // TODO move this to platform
// register inline diffs
import './registerInlineDiffs.js'

View file

@ -18,6 +18,7 @@ import './browser/workbench.contribution.js';
// Void added this:
import './contrib/void/browser/void.contribution.js';
import '../platform/void/browser/llmMessageService.js';
import '../platform/void/common/voidConfigService.js';
//#endregion