better model seln UI + error handling works much better!

This commit is contained in:
Andrew Pareles 2024-12-11 19:53:36 -08:00
parent 62cdaba44b
commit 07fcb3a72c
24 changed files with 291 additions and 332 deletions

View file

@ -273,7 +273,7 @@ function createServerHost(
try {
fs.createDirectory(pathMapper.toResource(path));
} catch (error) {
logger.logNormal('Error fs.createDirectory', { path, error: error + '' });
logger.logNormal('Error fs.createDirectory', { path, error });
}
},
getExecutingFilePath(): string {
@ -323,7 +323,7 @@ function createServerHost(
try {
fs.delete(pathMapper.toResource(path));
} catch (error) {
logger.logNormal('Error fs.deleteFile', { path, error: error + '' });
logger.logNormal('Error fs.deleteFile', { path, error });
}
},
createHash: generateDjb2Hash,

12
package-lock.json generated
View file

@ -72,6 +72,7 @@
"@types/cookie": "^0.3.3",
"@types/debug": "^4.1.5",
"@types/diff": "^6.0.0",
"@types/eslint": "^9.6.1",
"@types/gulp-svgmin": "^1.2.1",
"@types/http-proxy-agent": "^2.0.1",
"@types/kerberos": "^1.1.2",
@ -3017,6 +3018,17 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/eslint": {
"version": "9.6.1",
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
"integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "*",
"@types/json-schema": "*"
}
},
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",

View file

@ -9,7 +9,7 @@
"main": "./out/main",
"private": true,
"scripts": {
"react": "cd ./src/vs/workbench/contrib/void/browser/react/ && node build.js && cd ../../../../../../../",
"buildreact": "cd ./src/vs/workbench/contrib/void/browser/react/ && node build.js && cd ../../../../../../../",
"test": "echo Please run any of the test scripts from the scripts folder.",
"test-browser": "npx playwright install && node test/unit/browser/index.js",
"test-browser-amd": "npx playwright install && node test/unit/browser/index.amd.js",
@ -135,6 +135,7 @@
"@types/cookie": "^0.3.3",
"@types/debug": "^4.1.5",
"@types/diff": "^6.0.0",
"@types/eslint": "^9.6.1",
"@types/gulp-svgmin": "^1.2.1",
"@types/http-proxy-agent": "^2.0.1",
"@types/kerberos": "^1.1.2",

View file

@ -81,7 +81,7 @@ export class SendLLMMessageService extends Disposable implements ISendLLMMessage
const modelSelection = this.voidConfigStateService.state.modelSelectionOfFeature[featureName]
if (modelSelection === null) {
this.notificationService.warn('Please add a Provider in Settings!')
setTimeout(() => onError({ error: 'Please add a Provider in Settings!' }), 100)
onError({ message: 'Please add a Provider in Settings!', fullError: null })
return null
}
const { providerName, modelName } = modelSelection

View file

@ -11,7 +11,7 @@ export type OnText = (p: { newText: string, fullText: string }) => void
export type OnFinalMessage = (p: { fullText: string }) => void
export type OnError = (p: { error: string }) => void
export type OnError = (p: { message: string, fullError: Error | null }) => void
export type AbortRef = { current: (() => void) | null }

View file

@ -13,7 +13,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../storage/comm
import { defaultVoidProviderState, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider } from './voidConfigTypes.js';
const CONFIG_STORAGE_KEY = 'void.voidConfigStateII'
const STORAGE_KEY = 'void.voidConfigStateII'
type SetSettingOfProviderFn = <K extends ProviderName>(
providerName: K,
@ -36,6 +36,7 @@ export interface IVoidConfigStateService {
readonly _serviceBrand: undefined;
readonly state: VoidConfigState;
onDidChangeState: Event<void>;
onDidGetInitState: Event<void>;
setSettingOfProvider: SetSettingOfProviderFn;
setModelSelectionOfFeature: SetModelSelectionOfFeature;
}
@ -57,6 +58,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
private readonly _onDidGetInitState = new Emitter<void>();
readonly onDidGetInitState: Event<void> = this._onDidGetInitState.event;
state: VoidConfigState;
constructor(
@ -71,12 +75,12 @@ class VoidConfigStateService extends Disposable implements IVoidConfigStateServi
this.state = defaultState()
// read and update the actual state immediately
this._readVoidConfigState().then(voidConfigState => { this._setState(voidConfigState) })
this._readVoidConfigState().then(voidConfigState => { this._setState(voidConfigState, 'initialState') })
}
private async _readVoidConfigState(): Promise<VoidConfigState> {
const encryptedPartialConfig = this._storageService.get(CONFIG_STORAGE_KEY, StorageScope.APPLICATION)
const encryptedPartialConfig = this._storageService.get(STORAGE_KEY, StorageScope.APPLICATION)
if (!encryptedPartialConfig)
return defaultState()
@ -88,7 +92,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(STORAGE_KEY, encryptedVoidConfigStr, StorageScope.APPLICATION, StorageTarget.USER);
}
setSettingOfProvider: SetSettingOfProviderFn = async (providerName, option, newVal) => {
@ -125,9 +129,12 @@ class VoidConfigStateService extends Disposable implements IVoidConfigStateServi
// internal function to update state, should be called every time state changes
private async _setState(voidConfigState: VoidConfigState) {
private async _setState(voidConfigState: VoidConfigState, type: 'usual' | 'initialState' = 'usual') {
this.state = voidConfigState
this._onDidChangeState.fire()
if (type === 'usual')
this._onDidChangeState.fire()
else if (type === 'initialState')
this._onDidGetInitState.fire()
}
}

View file

@ -238,6 +238,25 @@ type DisplayInfo = {
placeholder: string,
}
export const titleOfProviderName = (providerName: ProviderName) => {
if (providerName === 'anthropic')
return 'Anthropic'
else if (providerName === 'openAI')
return 'OpenAI'
else if (providerName === 'ollama')
return 'Ollama'
else if (providerName === 'openRouter')
return 'OpenRouter'
else if (providerName === 'openAICompatible')
return 'OpenAI-Compatible'
else if (providerName === 'gemini')
return 'Gemini'
else if (providerName === 'groq')
return 'Groq'
throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`)
}
export const displayInfoOfSettingName = (providerName: ProviderName, settingName: SettingName): DisplayInfo => {
if (settingName === 'apiKey') {
return {
@ -254,8 +273,8 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
}
else if (settingName === 'endpoint') {
return {
title: providerName === 'ollama' ? 'The endpoint of your Ollama instance.' :
providerName === 'openAICompatible' ? 'The baseUrl (exluding /chat/completions).'
title: providerName === 'ollama' ? 'Your Ollama endpoint' :
providerName === 'openAICompatible' ? 'Endpoint compatible with OpenAI API' // (do not include /chat/completions)
: '(never)',
type: 'string',
placeholder: providerName === 'ollama' || providerName === 'openAICompatible' ?

View file

@ -6,6 +6,7 @@
import Anthropic from '@anthropic-ai/sdk';
import { parseMaxTokensStr } from './util.js';
import { SendLLMMessageFnTypeInternal } from '../../common/llmMessageTypes.js';
import { displayInfoOfSettingName } from '../../common/voidConfigTypes.js';
// Anthropic
type LLMMessageAnthropic = {
@ -16,6 +17,12 @@ export const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onTex
const thisConfig = settingsOfProvider.anthropic
const maxTokens = parseMaxTokensStr(thisConfig.maxTokens)
if (maxTokens === undefined) {
onError({ message: `Please set a value for ${displayInfoOfSettingName('anthropic', 'maxTokens').title}.`, fullError: null })
return
}
const anthropic = new Anthropic({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true });
// find system messages and concatenate them
@ -27,11 +34,13 @@ export const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onTex
// remove system messages for Anthropic
const anthropicMessages = messages.filter(msg => msg.role !== 'system') as LLMMessageAnthropic[]
const stream = anthropic.messages.stream({
system: systemMessage,
messages: anthropicMessages,
model: modelName,
max_tokens: parseMaxTokensStr(thisConfig.maxTokens)!, // this might be undefined, but it will just throw an error for the user to see
max_tokens: maxTokens,
});
@ -50,10 +59,10 @@ export const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onTex
stream.on('error', (error) => {
// the most common error will be invalid API key (401), so we handle this with a nice message
if (error instanceof Anthropic.APIError && error.status === 401) {
onError({ error: 'Invalid API key.' })
onError({ message: 'Invalid API key.', fullError: error })
}
else {
onError({ error: error + '' })
onError({ message: error + '', fullError: error }) // anthropic errors can be stringified nicely like this
}
})

View file

@ -44,10 +44,10 @@ export const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, on
})
.catch((error) => {
if (error instanceof GoogleGenerativeAIFetchError && error.status === 400) {
onError({ error: 'Invalid API key.' });
onError({ message: 'Invalid API key.', fullError: null });
}
else {
onError({ error: error + '' });
onError({ message: error + '', fullError: error });
}
})
}

View file

@ -40,7 +40,7 @@ export const sendGroqMsg: SendLLMMessageFnTypeInternal = async ({ messages, onTe
onFinalMessage({ fullText });
})
.catch(error => {
onError({ error: error + '' });
onError({ message: error + '', fullError: error });
})

View file

@ -43,7 +43,7 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText,
// return;
// }
// }
onError({ error })
onError({ message: error + '', fullError: error })
})
};

View file

@ -58,10 +58,10 @@ export const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText,
// when error/fail - this catches errors of both .create() and .then(for await)
.catch(error => {
if (error instanceof OpenAI.APIError && error.status === 401) {
onError({ error: 'Invalid API key.' });
onError({ message: 'Invalid API key.', fullError: error });
}
else {
onError({ error: error + '' });
onError({ message: error, fullError: error });
}
})

View file

@ -59,11 +59,12 @@ export const sendLLMMessage = ({
onFinalMessage_({ fullText })
}
const onError: OnError = ({ error }) => {
const onError: OnError = ({ message: error, fullError }) => {
if (_didAbort) return
console.log("ERROR!!!!!", error)
console.error('sendLLMMessage onError:', error)
captureChatEvent(`${loggingName} - Error`, { error })
onError_({ error })
onError_({ message: error, fullError })
}
const onAbort = () => {
@ -96,14 +97,14 @@ export const sendLLMMessage = ({
sendGroqMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
break;
default:
onError({ error: `Error: Void provider was "${providerName}", which is not recognized.` })
onError({ message: `Error: Void provider was "${providerName}", which is not recognized.`, fullError: null })
break;
}
}
catch (error) {
if (error instanceof Error) { onError({ error: error + '' }) }
else { onError({ error: `Unexpected Error in sendLLMMessage: ${error}` }); }
if (error instanceof Error) { onError({ message: error + '', fullError: error }) }
else { onError({ message: `Unexpected Error in sendLLMMessage: ${error}`, fullError: error }); }
// ; (_aborter as any)?.()
// _didAbort = true
}

View file

@ -81,7 +81,7 @@ export class LLMMessageChannel implements IServerChannel {
...params,
onText: ({ newText, fullText }) => { this._onText.fire({ requestId, newText, fullText }); },
onFinalMessage: ({ fullText }) => { this._onFinalMessage.fire({ requestId, fullText }); },
onError: ({ error }) => { console.log('sendLLM: firing err'); this._onError.fire({ requestId, error }); },
onError: ({ message: error, fullError }) => { console.log('sendLLM: firing err'); this._onError.fire({ requestId, message: error, fullError }); },
abortRef: this._abortRefOfRequestId[requestId],
}
sendLLMMessage(mainThreadParams, this.metricsService);

View file

@ -7,59 +7,60 @@ import React, { Component, ErrorInfo, ReactNode } from 'react';
import { ErrorDisplay } from './ErrorDisplay.js';
interface Props {
children: ReactNode;
fallback?: ReactNode;
onDismiss?: () => void;
children: ReactNode;
fallback?: ReactNode;
onDismiss?: () => void;
}
interface State {
hasError: boolean;
error: Error | null;
errorInfo: ErrorInfo | null;
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
};
}
constructor(props: Props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null
};
}
static getDerivedStateFromError(error: Error): Partial<State> {
return {
hasError: true,
error
};
}
static getDerivedStateFromError(error: Error): Partial<State> {
return {
hasError: true,
error
};
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
this.setState({
error,
errorInfo
});
}
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;
}
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}
/>
);
}
// Use ErrorDisplay component as the default error UI
return (
<ErrorDisplay
message={this.state.error + ''}
fullError={this.state.error}
onDismiss={this.props.onDismiss || null}
/>
);
}
return this.props.children;
}
return this.props.children;
}
}
export default ErrorBoundary;

View file

@ -7,82 +7,31 @@ import React, { useState } from 'react';
import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react';
// const opaqueMessage = `\
// Unfortunately, Void can't see the full error. However, you should be able to find more details by pressing ${getCmdKey()}+Shift+P, typing "Toggle Developer Tools", and looking at the console.\n
// This error often means you have an incorrect API key. If you're self-hosting your own server, it might mean your CORS headers are off, and you should make sure your server's response has the header "Access-Control-Allow-Origins" set to "*", or at least allows "vscode-file://vscode-app".`
// if ((error instanceof Error) && (error.cause + '').includes('TypeError: Failed to fetch')) {
// e = error as any
// e['Void Team'] = opaqueMessage
// }
type Details = {
message: string,
name: string,
stack: string | null,
cause: string | null,
code: string | null,
additional: Record<string, any>
}
// Get detailed error information
const getErrorDetails = (error: unknown) => {
let details: Details;
let e: Error & { [other: string]: undefined | any }
// If fetch() fails, it gives an opaque message. We add extra details to the error.
if (error instanceof Error) {
e = error
}
// sometimes error is an object but not an Error
else if (typeof error === 'object') {
e = new Error(`More details below.`, { cause: JSON.stringify(error) })
}
else {
e = new Error(String(error))
}
// console.log('error display', JSON.stringify(e))
const message = e.message && e.error ?
(e.message + ':\n' + e.error)
: e.message || e.error || JSON.stringify(error)
details = {
name: e.name || 'Error',
message: message,
stack: null, // e.stack is ignored because it's ugly and not very useful
cause: e.cause ? String(e.cause) : null,
code: e.code || null,
additional: {}
}
// Collect any additional properties from e
for (let prop of Object.getOwnPropertyNames(e).filter((prop) => !Object.keys(details).includes(prop)))
details.additional[prop] = (e as any)[prop]
return details;
};
export const ErrorDisplay = ({
error,
onDismiss = null,
showDismiss = true,
message,
fullError,
onDismiss,
showDismiss,
}: {
error: Error | object | string,
message: string,
fullError: Error | null,
onDismiss: (() => void) | null,
showDismiss?: boolean,
className?: string
}) => {
const [isExpanded, setIsExpanded] = useState(false);
const details = getErrorDetails(error);
const hasDetails = details.cause || Object.keys(details.additional).length > 0;
let details: string | null = null;
if (fullError === null) {
details = null
}
else if (typeof fullError === 'object') {
details = JSON.stringify(fullError, null, 2)
}
else if (typeof fullError === 'string') {
details = null
}
return (
<div className={`rounded-lg border border-red-200 bg-red-50 p-4 overflow-auto`}>
@ -92,16 +41,18 @@ export const ErrorDisplay = ({
<AlertCircle className="h-5 w-5 text-red-500 mt-0.5" />
<div className="flex-1">
<h3 className="font-semibold text-red-800">
{details.name}
{/* eg Error */}
Error
</h3>
<p className="text-red-700 mt-1">
{details.message}
{/* eg Something went wrong */}
{message}
</p>
</div>
</div>
<div className="flex gap-2">
{hasDetails && (
{details && (
<button
onClick={() => setIsExpanded(!isExpanded)}
className="text-red-600 hover:text-red-800 p-1 rounded"
@ -125,38 +76,12 @@ export const ErrorDisplay = ({
</div>
{/* Expandable Details */}
{isExpanded && hasDetails && (
{isExpanded && details && (
<div className="mt-4 space-y-3 border-t border-red-200 pt-3">
{details.code && (
<div>
<span className="font-semibold text-red-800">Error Code: </span>
<span className="text-red-700">{details.code}</span>
</div>
)}
{details.cause && (
<div>
<span className="font-semibold text-red-800">Cause: </span>
<span className="text-red-700">{details.cause}</span>
</div>
)}
{Object.keys(details.additional).length > 0 && (
<div>
<span className="font-semibold text-red-800">Additional Information:</span>
<pre className="mt-1 text-sm text-red-700 overflow-x-auto whitespace-pre-wrap">
{Object.keys(details.additional).map(key => `${key}:\n${details.additional[key]}`).join('\n')}
</pre>
</div>
)}
{/* {details.stack && (
<div>
<span className="font-semibold text-red-800">Stack Trace:</span>
<pre className="mt-1 text-sm text-red-700 overflow-x-auto whitespace-pre-wrap">
{details.stack}
</pre>
</div>
)} */}
<div>
<span className="font-semibold text-red-800">Full Error: </span>
<pre className="text-red-700">{details}</pre>
</div>
</div>
)}
</div>

View file

@ -0,0 +1,68 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import { useCallback, useEffect, useRef } from 'react'
import { FeatureName, featureNames, ProviderName, providerNames } from '../../../../../../../platform/void/common/voidConfigTypes.js'
import { useConfigState, useService } from '../util/services.js'
import ErrorBoundary from './ErrorBoundary.js'
import { VoidSelectBox } from './inputs.js'
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js'
export const ModelSelectionOfFeature = ({ featureName }: { featureName: FeatureName }) => {
const voidConfigService = useService('configStateService')
const voidConfigState = useConfigState()
const modelOptions: { text: string, value: [string, string] }[] = []
modelOptions.push({ text: 'Select a Provider', value: ['MyProvider', 'MyModel'] })
for (const providerName of providerNames) {
const providerConfig = voidConfigState[providerName]
if (providerConfig.enabled !== 'true') continue
providerConfig.models?.forEach(model => {
modelOptions.push({ text: `${providerName} - ${model}`, value: [providerName, model] })
})
}
return <>
<h2>{featureName}</h2>
{
<VoidSelectBox
options={modelOptions}
onChangeSelection={(newVal) => { voidConfigService.setModelSelectionOfFeature(featureName, { providerName: newVal[0] as ProviderName, modelName: newVal[1] }) }}
// we are responsible for setting the initial state here
onCreateInstance={useCallback((instance: SelectBox) => {
const updateState = () => {
const settingsAtProvider = voidConfigService.state.modelSelectionOfFeature[featureName]
const index = modelOptions.findIndex(v => v.value[0] === settingsAtProvider?.providerName && v.value[1] === settingsAtProvider?.modelName)
if (index !== -1)
instance.select(index)
}
updateState()
const disposable = voidConfigService.onDidGetInitState(updateState)
return [disposable]
}, [voidConfigService, modelOptions, featureName])}
/>}
{/* <h1>Settings - {featureName}</h1> */}
{/* {models.map(([providerName, model], i) => <p key={i}>{providerName} - {model}</p>)} */}
</>
}
export const ModelSelectionSettings = () => {
return <>
{featureNames.map(featureName => <ModelSelectionOfFeature
key={featureName}
featureName={featureName}
/>)}
</>
}

View file

@ -15,8 +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';
import { ModelSelectionSettings } from './ModelSelectionSettings.js';
import { VoidProviderSettings } from './VoidProviderSettings.js';
import ErrorBoundary from './ErrorBoundary.js';
const Sidebar = () => {
@ -43,15 +43,15 @@ const Sidebar = () => {
<ErrorBoundary>
<SidebarChat />
</ErrorBoundary>
<ErrorBoundary>
<ModelSelectionSettings />
</ErrorBoundary>
</div>
<div className={`${tab === 'settings' ? '' : 'hidden'}`}>
<ErrorBoundary>
<SidebarModelSettings />
</ErrorBoundary>
--------
<ErrorBoundary>
<SidebarProviderSettings />
<VoidProviderSettings />
</ErrorBoundary>
</div>

View file

@ -18,7 +18,7 @@ import { URI } from '../../../../../../../base/common/uri.js';
import { EndOfLinePreference } from '../../../../../../../editor/common/model.js';
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
import { ErrorDisplay } from './ErrorDisplay.js';
import { LLMMessageServiceParams } from '../../../../../../../platform/void/common/llmMessageTypes.js';
import { LLMMessageServiceParams, OnError } from '../../../../../../../platform/void/common/llmMessageTypes.js';
import { getCmdKey } from '../../../getCmdKey.js'
import { HistoryInputBox, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
import { VoidInputBox } from './inputs.js';
@ -158,7 +158,7 @@ export const SidebarChat = () => {
const [isLoading, setIsLoading] = useState(false)
const latestRequestIdRef = useRef<string | null>(null)
const [latestError, setLatestError] = useState<Error | string | null>(null)
const [latestError, setLatestError] = useState<Parameters<OnError>[0] | null>(null)
const sendLLMMessageService = useService('sendLLMMessageService')
@ -195,6 +195,11 @@ export const SidebarChat = () => {
// send message to LLM
setIsLoading(true) // must come before message is sent so onError will work
setLatestError(null)
if (inputBoxRef.current) {
inputBoxRef.current.value = ''; // this triggers onDidChangeText
inputBoxRef.current.blur();
}
const object: LLMMessageServiceParams = {
logging: { loggingName: 'Chat' },
@ -209,8 +214,8 @@ export const SidebarChat = () => {
setMessageStream(null)
setIsLoading(false)
},
onError: ({ error }) => {
console.log('chat: running error', error)
onError: ({ message, fullError }) => {
console.log('chat: running error', message, fullError)
// add assistant's message to chat history, and clear selection
let content = messageStream ?? ''; // just use the current content
@ -220,7 +225,7 @@ export const SidebarChat = () => {
setMessageStream('')
setIsLoading(false)
setLatestError(error)
setLatestError({ message, fullError })
},
featureName: 'Ctrl+L',
@ -229,13 +234,7 @@ export const SidebarChat = () => {
const latestRequestId = sendLLMMessageService.sendLLMMessage(object)
latestRequestIdRef.current = latestRequestId
if (inputBoxRef.current) {
inputBoxRef.current.value = ''; // this triggers onDidChangeText
inputBoxRef.current.blur();
}
threadsStateService.setStaging([]) // clear staging
setLatestError(null)
}
@ -282,7 +281,8 @@ export const SidebarChat = () => {
{/* error message */}
{latestError === null ? null :
<ErrorDisplay
error={latestError}
message={latestError.message}
fullError={latestError.fullError}
onDismiss={() => { setLatestError(null) }}
/>}
@ -300,7 +300,7 @@ export const SidebarChat = () => {
<VoidInputBox
placeholder={`${getCmdKey()}+L to select`}
onChangeText={onChangeText}
onCreateInstance={inputBoxRef}
inputBoxRef={inputBoxRef}
multiline={true}
/>

View file

@ -1,77 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import { useEffect, useRef } from 'react'
import { FeatureName, featureNames, ProviderName, providerNames } from '../../../../../../../platform/void/common/voidConfigTypes.js'
import { useConfigState, useService } from '../util/services.js'
import ErrorBoundary from './ErrorBoundary.js'
import { VoidSelectBox } from './inputs.js'
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js'
export const SidebarModelSettingsForFeature = ({ featureName }: { featureName: FeatureName }) => {
const voidConfigService = useService('configStateService')
const voidConfigState = useConfigState()
const models: [string, string][] = []
for (const providerName of providerNames) {
const providerConfig = voidConfigState[providerName]
if (providerConfig.enabled !== 'true') continue
providerConfig.models?.forEach(model => {
models.push([providerName, model])
})
}
const wasEmpty = models.length === 0
if (wasEmpty) {
models.push(['Provider', 'Model'])
}
const selectBoxRef = useRef<SelectBox | null>(null)
useEffect(() => {
// this is really just to sync the state on initial mount, when init value hasn't been set yet
let synced = false
const syncStateOnMount = () => {
if (!selectBoxRef.current) return
if (synced) return
synced = true
const settingsAtProvider = voidConfigService.state.modelSelectionOfFeature[featureName]
const index = models.findIndex(v => v[0] === settingsAtProvider?.providerName && v[1] === settingsAtProvider?.modelName)
if (index !== -1)
selectBoxRef.current.select(index)
}
syncStateOnMount()
synced = false // sync the next time state changes (but not after that - the "current.value = ..." triggers a state change, causing an infinite loop!)
const disposable = voidConfigService.onDidChangeState(syncStateOnMount)
return () => disposable.dispose()
}, [selectBoxRef, voidConfigService, models, featureName])
return <>
<h2>{featureName}</h2>
{
<VoidSelectBox
initVal={models[0]}
options={wasEmpty ? [{ text: 'Please add a Provider!', value: models[0] }] : models.map(s => ({ text: s.join(' - '), value: s }))}
onChangeSelection={(newVal) => { voidConfigService.setModelSelectionOfFeature(featureName, { providerName: newVal[0] as ProviderName, modelName: newVal[1] }) }}
selectBoxRef={selectBoxRef}
/>}
{/* <h1>Settings - {featureName}</h1> */}
{/* {models.map(([providerName, model], i) => <p key={i}>{providerName} - {model}</p>)} */}
</>
}
export const SidebarModelSettings = () => {
return <>
{featureNames.map(featureName => <SidebarModelSettingsForFeature key={featureName} featureName={featureName} />)}
</>
}

View file

@ -4,8 +4,8 @@
*--------------------------------------------------------------------------------------------*/
import React, { Fragment, useCallback, useEffect, useRef, useState } from 'react'
import { displayInfoOfSettingName, ProviderName, providerNames } from '../../../../../../../platform/void/common/voidConfigTypes.js'
import { VoidInputBox, VoidSelectBox } from './inputs.js'
import { titleOfProviderName, displayInfoOfSettingName, ProviderName, providerNames } from '../../../../../../../platform/void/common/voidConfigTypes.js'
import { VoidInputBox } from './inputs.js'
import { useConfigState, useService } from '../util/services.js'
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'
import ErrorBoundary from './ErrorBoundary.js'
@ -16,52 +16,39 @@ const Setting = ({ providerName, settingName }: { providerName: ProviderName, se
const { title, type, placeholder } = displayInfoOfSettingName(providerName, settingName)
const voidConfigService = useService('configStateService')
const instanceRef = useRef<InputBox | null>(null)
// set init val to the current state
useEffect(() => {
// this is really just to sync the state on initial mount, when init value hasn't been set yet
let synced = false
const syncStateOnMount = () => {
if (!instanceRef.current) return
if (synced) return
synced = true
const settingsAtProvider = voidConfigService.state.settingsOfProvider[providerName];
// @ts-ignore
const stateVal = settingsAtProvider[settingName]
if (instanceRef.current.value !== stateVal) {
instanceRef.current.value = stateVal // triggers onChangeText
}
}
syncStateOnMount()
synced = false // sync the next time state changes (but not after that - the "current.value = ..." triggers a state change, causing an infinite loop!)
const disposable = voidConfigService.onDidChangeState(syncStateOnMount)
return () => disposable.dispose()
}, [instanceRef, voidConfigService, providerName, settingName])
return <><ErrorBoundary>
<h2>{title}</h2>
{<VoidInputBox
<label>{title}</label>
<VoidInputBox
placeholder={placeholder}
onChangeText={useCallback((newVal) => {
voidConfigService.setSettingOfProvider(providerName, settingName, newVal)
}, [voidConfigService, providerName, settingName])
}
onCreateInstance={instanceRef}
}, [voidConfigService, providerName, settingName])}
// we are responsible for setting the initial value here
onCreateInstance={useCallback((instance: InputBox) => {
const updateInstanceState = () => {
const settingsAtProvider = voidConfigService.state.settingsOfProvider[providerName];
// @ts-ignore
const stateVal = settingsAtProvider[settingName]
instance.value = stateVal
}
updateInstanceState()
const disposable = voidConfigService.onDidGetInitState(updateInstanceState)
return [disposable]
}, [voidConfigService, providerName, settingName])}
multiline={false}
/>}
/>
</ErrorBoundary></>
}
const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) => {
const voidConfigState = useConfigState()
const { models, ...others } = voidConfigState[providerName]
return <>
<h1>{providerName}</h1>
<h1 className='text-xl'>{titleOfProviderName(providerName)}</h1>
{/* settings besides models (e.g. api key) */}
{Object.keys(others).map((settingName, i) => {
return <Setting key={settingName} providerName={providerName} settingName={settingName} />
@ -70,7 +57,7 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) =
}
export const SidebarProviderSettings = () => {
export const VoidProviderSettings = () => {
return <>
{providerNames.map(providerName =>

View file

@ -36,9 +36,10 @@ export const WidgetComponent = <CtorParams extends any[], Instance>({ ctor, prop
export const VoidInputBox = ({ onChangeText, onCreateInstance, placeholder, multiline }: {
export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, placeholder, multiline }: {
onChangeText: (value: string) => void;
onCreateInstance?: { current: InputBox | null } | ((instance: InputBox) => void | IDisposable[]);
onCreateInstance?: (instance: InputBox) => void | IDisposable[];
inputBoxRef?: { current: InputBox | null };
placeholder: string;
multiline: boolean;
}) => {
@ -71,15 +72,15 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, placeholder, mult
disposables.push(
instance.onDidChange((newText) => onChangeText(newText))
)
if (typeof onCreateInstance === 'function') {
if (onCreateInstance) {
const ds = onCreateInstance(instance) ?? []
disposables.push(...ds)
}
if (typeof onCreateInstance === 'object') {
onCreateInstance.current = instance
}
if (inputBoxRef)
inputBoxRef.current = instance;
return disposables
}, [onChangeText, onCreateInstance])
}, [onChangeText, onCreateInstance, inputBoxRef])
}
/>
};
@ -87,11 +88,11 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, placeholder, mult
export const VoidSelectBox = <T,>({ onChangeSelection, initVal, selectBoxRef, options }: {
initVal: T;
selectBoxRef: React.MutableRefObject<SelectBox | null>;
options: readonly { text: string, value: T }[];
export const VoidSelectBox = <T,>({ onChangeSelection, onCreateInstance, selectBoxRef, options }: {
onChangeSelection: (value: T) => void;
onCreateInstance?: ((instance: SelectBox) => void | IDisposable[]);
selectBoxRef?: React.MutableRefObject<SelectBox | null>;
options: readonly { text: string, value: T }[];
}) => {
const contextViewProvider = useService('contextViewService');
@ -101,14 +102,14 @@ export const VoidSelectBox = <T,>({ onChangeSelection, initVal, selectBoxRef, op
ctor={SelectBox}
propsFn={useCallback((container) => {
containerRef.current = container
const defaultIndex = options.findIndex(opt => opt.value === initVal);
const defaultIndex = 0;
return [
options.map(opt => ({ text: opt.text })),
defaultIndex,
contextViewProvider,
defaultSelectBoxStyles
] as const;
}, [containerRef, options, initVal, contextViewProvider])}
}, [containerRef, options, contextViewProvider])}
dispose={useCallback((instance: SelectBox) => {
instance.dispose();
@ -117,16 +118,24 @@ export const VoidSelectBox = <T,>({ onChangeSelection, initVal, selectBoxRef, op
}, [containerRef])}
onCreateInstance={useCallback((instance: SelectBox) => {
selectBoxRef.current = instance;
if (containerRef.current) instance.render(containerRef.current)
const disposables = [
instance.onDidSelect(e => {
console.log('e.selected', JSON.stringify(e));
onChangeSelection(options[e.index].value);
})
];
const disposables: IDisposable[] = []
if (containerRef.current)
instance.render(containerRef.current)
disposables.push(
instance.onDidSelect(e => { onChangeSelection(options[e.index].value); })
)
if (onCreateInstance) {
const ds = onCreateInstance(instance) ?? []
disposables.push(...ds)
}
if (selectBoxRef)
selectBoxRef.current = instance;
return disposables;
}, [containerRef, selectBoxRef, options, onChangeSelection])}
}, [containerRef, onChangeSelection, options, onCreateInstance, selectBoxRef])}
/>;
};

View file

@ -670,10 +670,10 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
resolve(newAutocompletion.insertText)
},
onError: ({ error }) => {
onError: ({ message }) => {
newAutocompletion.endTime = Date.now()
newAutocompletion.status = 'error'
reject(error)
reject(message)
},
featureName: 'Autocomplete',
range: { startLineNumber: position.lineNumber, startColumn: position.column, endLineNumber: position.lineNumber, endColumn: position.column },

View file

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