From 07fcb3a72c4403cb8fd9cc581a6284660967ec02 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Wed, 11 Dec 2024 19:53:36 -0800 Subject: [PATCH] better model seln UI + error handling works much better! --- .../web/src/serverHost.ts | 4 +- package-lock.json | 12 ++ package.json | 3 +- .../void/browser/llmMessageService.ts | 2 +- .../platform/void/common/llmMessageTypes.ts | 2 +- .../platform/void/common/voidConfigService.ts | 19 ++- .../platform/void/common/voidConfigTypes.ts | 23 ++- .../electron-main/llmMessage/anthropic.ts | 15 +- .../void/electron-main/llmMessage/gemini.ts | 4 +- .../void/electron-main/llmMessage/groq.ts | 2 +- .../void/electron-main/llmMessage/ollama.ts | 2 +- .../void/electron-main/llmMessage/openai.ts | 4 +- .../llmMessage/sendLLMMessage.ts | 11 +- .../void/electron-main/llmMessageChannel.ts | 2 +- .../react/src/sidebar-tsx/ErrorBoundary.tsx | 85 ++++++------ .../react/src/sidebar-tsx/ErrorDisplay.tsx | 131 ++++-------------- .../sidebar-tsx/ModelSelectionSettings.tsx | 68 +++++++++ .../browser/react/src/sidebar-tsx/Sidebar.tsx | 14 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 26 ++-- .../src/sidebar-tsx/SidebarModelSettings.tsx | 77 ---------- ...rSettings.tsx => VoidProviderSettings.tsx} | 57 +++----- .../browser/react/src/sidebar-tsx/inputs.tsx | 53 ++++--- .../void/browser/registerAutocomplete.ts | 4 +- .../contrib/void/browser/void.contribution.ts | 3 - 24 files changed, 291 insertions(+), 332 deletions(-) create mode 100644 src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ModelSelectionSettings.tsx delete mode 100644 src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarModelSettings.tsx rename src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/{SidebarProviderSettings.tsx => VoidProviderSettings.tsx} (54%) diff --git a/extensions/typescript-language-features/web/src/serverHost.ts b/extensions/typescript-language-features/web/src/serverHost.ts index dedec859..9a61a8f8 100644 --- a/extensions/typescript-language-features/web/src/serverHost.ts +++ b/extensions/typescript-language-features/web/src/serverHost.ts @@ -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, diff --git a/package-lock.json b/package-lock.json index bf51d2e3..82df8eed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index ed9c01ec..409cf26c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/vs/platform/void/browser/llmMessageService.ts b/src/vs/platform/void/browser/llmMessageService.ts index da7fc01c..838cf7c0 100644 --- a/src/vs/platform/void/browser/llmMessageService.ts +++ b/src/vs/platform/void/browser/llmMessageService.ts @@ -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 diff --git a/src/vs/platform/void/common/llmMessageTypes.ts b/src/vs/platform/void/common/llmMessageTypes.ts index fb63f328..a9bbb592 100644 --- a/src/vs/platform/void/common/llmMessageTypes.ts +++ b/src/vs/platform/void/common/llmMessageTypes.ts @@ -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 } diff --git a/src/vs/platform/void/common/voidConfigService.ts b/src/vs/platform/void/common/voidConfigService.ts index 3bf74e4f..8fc3aecd 100644 --- a/src/vs/platform/void/common/voidConfigService.ts +++ b/src/vs/platform/void/common/voidConfigService.ts @@ -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 = ( providerName: K, @@ -36,6 +36,7 @@ export interface IVoidConfigStateService { readonly _serviceBrand: undefined; readonly state: VoidConfigState; onDidChangeState: Event; + onDidGetInitState: Event; setSettingOfProvider: SetSettingOfProviderFn; setModelSelectionOfFeature: SetModelSelectionOfFeature; } @@ -57,6 +58,9 @@ class VoidConfigStateService extends Disposable implements IVoidConfigStateServi private readonly _onDidChangeState = new Emitter(); readonly onDidChangeState: Event = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes + private readonly _onDidGetInitState = new Emitter(); + readonly onDidGetInitState: Event = 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 { - 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() } } diff --git a/src/vs/platform/void/common/voidConfigTypes.ts b/src/vs/platform/void/common/voidConfigTypes.ts index ccb3daa2..6d7dcc47 100644 --- a/src/vs/platform/void/common/voidConfigTypes.ts +++ b/src/vs/platform/void/common/voidConfigTypes.ts @@ -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' ? diff --git a/src/vs/platform/void/electron-main/llmMessage/anthropic.ts b/src/vs/platform/void/electron-main/llmMessage/anthropic.ts index 942eefeb..b3610efb 100644 --- a/src/vs/platform/void/electron-main/llmMessage/anthropic.ts +++ b/src/vs/platform/void/electron-main/llmMessage/anthropic.ts @@ -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 } }) diff --git a/src/vs/platform/void/electron-main/llmMessage/gemini.ts b/src/vs/platform/void/electron-main/llmMessage/gemini.ts index 6c07547a..d68879cb 100644 --- a/src/vs/platform/void/electron-main/llmMessage/gemini.ts +++ b/src/vs/platform/void/electron-main/llmMessage/gemini.ts @@ -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 }); } }) } diff --git a/src/vs/platform/void/electron-main/llmMessage/groq.ts b/src/vs/platform/void/electron-main/llmMessage/groq.ts index afc75d02..cbeb8669 100644 --- a/src/vs/platform/void/electron-main/llmMessage/groq.ts +++ b/src/vs/platform/void/electron-main/llmMessage/groq.ts @@ -40,7 +40,7 @@ export const sendGroqMsg: SendLLMMessageFnTypeInternal = async ({ messages, onTe onFinalMessage({ fullText }); }) .catch(error => { - onError({ error: error + '' }); + onError({ message: error + '', fullError: error }); }) diff --git a/src/vs/platform/void/electron-main/llmMessage/ollama.ts b/src/vs/platform/void/electron-main/llmMessage/ollama.ts index e36d2802..b631aad2 100644 --- a/src/vs/platform/void/electron-main/llmMessage/ollama.ts +++ b/src/vs/platform/void/electron-main/llmMessage/ollama.ts @@ -43,7 +43,7 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, // return; // } // } - onError({ error }) + onError({ message: error + '', fullError: error }) }) }; diff --git a/src/vs/platform/void/electron-main/llmMessage/openai.ts b/src/vs/platform/void/electron-main/llmMessage/openai.ts index fd74fe34..0e7e21b0 100644 --- a/src/vs/platform/void/electron-main/llmMessage/openai.ts +++ b/src/vs/platform/void/electron-main/llmMessage/openai.ts @@ -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 }); } }) diff --git a/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts index ebb60f43..b0823547 100644 --- a/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/platform/void/electron-main/llmMessage/sendLLMMessage.ts @@ -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 } diff --git a/src/vs/platform/void/electron-main/llmMessageChannel.ts b/src/vs/platform/void/electron-main/llmMessageChannel.ts index 0cbacc80..203eebd6 100644 --- a/src/vs/platform/void/electron-main/llmMessageChannel.ts +++ b/src/vs/platform/void/electron-main/llmMessageChannel.ts @@ -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); diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorBoundary.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorBoundary.tsx index 05ef1249..a095f6fd 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorBoundary.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorBoundary.tsx @@ -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 { - 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 { - return { - hasError: true, - error - }; - } + static getDerivedStateFromError(error: Error): Partial { + 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 ( - - ); - } + // Use ErrorDisplay component as the default error UI + return ( + + ); + } - return this.props.children; - } + return this.props.children; + } } export default ErrorBoundary; diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx index e25b43e1..b48c9668 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ErrorDisplay.tsx @@ -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 -} - -// 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 (
@@ -92,16 +41,18 @@ export const ErrorDisplay = ({

- {details.name} + {/* eg Error */} + Error

- {details.message} + {/* eg Something went wrong */} + {message}

- {hasDetails && ( + {details && (
{/* Expandable Details */} - {isExpanded && hasDetails && ( + {isExpanded && details && (
- {details.code && ( -
- Error Code: - {details.code} -
- )} - - {details.cause && ( -
- Cause: - {details.cause} -
- )} - - {Object.keys(details.additional).length > 0 && ( -
- Additional Information: -
-								{Object.keys(details.additional).map(key => `${key}:\n${details.additional[key]}`).join('\n')}
-							
-
- )} - {/* {details.stack && ( -
- Stack Trace: -
-								{details.stack}
-							
-
- )} */} +
+ Full Error: +
{details}
+
)} diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ModelSelectionSettings.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ModelSelectionSettings.tsx new file mode 100644 index 00000000..ef84f752 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/ModelSelectionSettings.tsx @@ -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 <> +

{featureName}

+ { + { 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])} + />} + + {/*

Settings - {featureName}

*/} + {/* {models.map(([providerName, model], i) =>

{providerName} - {model}

)} */} + +} + +export const ModelSelectionSettings = () => { + return <> + {featureNames.map(featureName => )} + +} + diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/Sidebar.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/Sidebar.tsx index cc8bf43b..d605e2d8 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/Sidebar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/Sidebar.tsx @@ -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 = () => { + + + +
- - - -------- - - +
diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 314a51ce..218d535a 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -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(null) - const [latestError, setLatestError] = useState(null) + const [latestError, setLatestError] = useState[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 : { setLatestError(null) }} />} @@ -300,7 +300,7 @@ export const SidebarChat = () => { diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarModelSettings.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarModelSettings.tsx deleted file mode 100644 index d2d21372..00000000 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarModelSettings.tsx +++ /dev/null @@ -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(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 <> -

{featureName}

- { - ({ text: s.join(' - '), value: s }))} - onChangeSelection={(newVal) => { voidConfigService.setModelSelectionOfFeature(featureName, { providerName: newVal[0] as ProviderName, modelName: newVal[1] }) }} - selectBoxRef={selectBoxRef} - />} - - {/*

Settings - {featureName}

*/} - {/* {models.map(([providerName, model], i) =>

{providerName} - {model}

)} */} - -} - -export const SidebarModelSettings = () => { - return <> - {featureNames.map(featureName => )} - -} - diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarProviderSettings.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/VoidProviderSettings.tsx similarity index 54% rename from src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarProviderSettings.tsx rename to src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/VoidProviderSettings.tsx index 94cc1c8d..a40709f5 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarProviderSettings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/VoidProviderSettings.tsx @@ -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(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 <> -

{title}

- {{title} + { 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} - />} + />
} + const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) => { const voidConfigState = useConfigState() const { models, ...others } = voidConfigState[providerName] return <> -

{providerName}

+

{titleOfProviderName(providerName)}

{/* settings besides models (e.g. api key) */} {Object.keys(others).map((settingName, i) => { return @@ -70,7 +57,7 @@ const SettingsForProvider = ({ providerName }: { providerName: ProviderName }) = } -export const SidebarProviderSettings = () => { +export const VoidProviderSettings = () => { return <> {providerNames.map(providerName => diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/inputs.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/inputs.tsx index 1218dd65..ad4e5464 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/inputs.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/inputs.tsx @@ -36,9 +36,10 @@ export const WidgetComponent = ({ 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 = ({ onChangeSelection, initVal, selectBoxRef, options }: { - initVal: T; - selectBoxRef: React.MutableRefObject; - options: readonly { text: string, value: T }[]; +export const VoidSelectBox = ({ onChangeSelection, onCreateInstance, selectBoxRef, options }: { onChangeSelection: (value: T) => void; + onCreateInstance?: ((instance: SelectBox) => void | IDisposable[]); + selectBoxRef?: React.MutableRefObject; + options: readonly { text: string, value: T }[]; }) => { const contextViewProvider = useService('contextViewService'); @@ -101,14 +102,14 @@ export const VoidSelectBox = ({ 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 = ({ 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])} />; }; diff --git a/src/vs/workbench/contrib/void/browser/registerAutocomplete.ts b/src/vs/workbench/contrib/void/browser/registerAutocomplete.ts index 22f690a1..2714ef4e 100644 --- a/src/vs/workbench/contrib/void/browser/registerAutocomplete.ts +++ b/src/vs/workbench/contrib/void/browser/registerAutocomplete.ts @@ -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 }, diff --git a/src/vs/workbench/contrib/void/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts index 5753e942..2dde5f70 100644 --- a/src/vs/workbench/contrib/void/browser/void.contribution.ts +++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts @@ -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'