consistent error messages

This commit is contained in:
Andrew Pareles 2024-12-10 17:41:13 -08:00
parent f20f41c804
commit 72deba43dd
13 changed files with 64 additions and 241 deletions

View file

@ -70,7 +70,7 @@ If you ran `npm run watch`, the build is done when you see something like this:
<!-- 3. Press <kbd>Ctrl+Shift+B</kbd> to start the build process. -->
4. In a new terminal, run `./scripts/code.sh` (Mac/Linux) or `./scripts/code.bat` (Windows). This should open up the built IDE!
4. In a new terminal, run `./scripts/code.sh` (Mac/Linux) or `./scripts/code.bat` (Windows). This should open up the built IDE.
You can always press <kbd>Ctrl+Shift+P</kbd> and run "Reload Window" inside the new window to see changes without re-building.
Now that you're set up, feel free to check out our [Issues](https://github.com/voideditor/void/issues) page.

View file

@ -10,7 +10,7 @@ import { InstantiationType, registerSingleton } from '../../instantiation/common
import { generateUuid } from '../../../base/common/uuid.js';
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { Event } from '../../../base/common/event.js';
import { IDisposable } from '../../../base/common/lifecycle.js';
import { Disposable } from '../../../base/common/lifecycle.js';
// BROWSER IMPLEMENTATION OF SENDLLMMESSAGE
@ -24,72 +24,59 @@ export interface ISendLLMMessageService {
}
export class SendLLMMessageService implements ISendLLMMessageService {
export class SendLLMMessageService extends Disposable implements ISendLLMMessageService {
readonly _serviceBrand: undefined;
private readonly channel: IChannel // LLMMessageChannel
private readonly _disposablesOfRequestId: Record<string, IDisposable[]> = {}
private readonly onTextHooks: { [eventId: string]: ((params: ProxyOnTextPayload) => void) } = {}
private readonly onFinalMessageHooks: { [eventId: string]: ((params: ProxyOnFinalMessagePayload) => void) } = {}
private readonly onErrorHooks: { [eventId: string]: ((params: ProxyOnErrorPayload) => void) } = {}
private readonly onTextEvent: Event<ProxyOnTextPayload>
private readonly onFinalMessageEvent: Event<ProxyOnFinalMessagePayload>
private readonly onErrorEvent: Event<ProxyOnErrorPayload>
constructor(
@IMainProcessService mainProcessService: IMainProcessService // used as a renderer (only usable on client side)
) {
this.channel = mainProcessService.getChannel('void-channel-sendLLMMessage')
console.log('setting up IPC')
// this sets up an IPC channel and takes a few ms, so should happen immediately
this.onTextEvent = this.channel.listen('onText')
this.onFinalMessageEvent = this.channel.listen('onFinalMessage')
this.onErrorEvent = this.channel.listen('onError')
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')
_addDisposable(requestId: string, disposable: IDisposable) {
if (!this._disposablesOfRequestId[requestId]) {
this._disposablesOfRequestId[requestId] = []
}
this._disposablesOfRequestId[requestId].push(disposable)
}
// 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')
const onFinalMessageEvent: Event<ProxyOnFinalMessagePayload> = this.channel.listen('onFinalMessage')
const onErrorEvent: Event<ProxyOnErrorPayload> = this.channel.listen('onError')
this._register(
onTextEvent(e => {
this.onTextHooks[e.requestId]?.(e)
})
)
this._register(
onFinalMessageEvent(e => {
this.onFinalMessageHooks[e.requestId]?.(e)
this._onRequestIdDone(e.requestId)
})
)
this._register(
onErrorEvent(e => {
console.log('Error in SendLLMMessageService:', JSON.stringify(e))
this.onErrorHooks[e.requestId]?.(e)
this._onRequestIdDone(e.requestId)
})
)
}
sendLLMMessage(params: LLMMessageServiceParams) {
const requestId_ = generateUuid();
const { onText, onFinalMessage, onError, ...proxyParams } = params;
// listen for listenerName='onText' | 'onFinalMessage' | 'onError', and call the original function on it
this.onTextHooks[requestId_] = onText
this.onFinalMessageHooks[requestId_] = onFinalMessage
this.onErrorHooks[requestId_] = onError
this._addDisposable(requestId_,
this.onTextEvent(e => {
if (requestId_ !== e.requestId) return;
onText(e)
})
)
this._addDisposable(requestId_,
this.onFinalMessageEvent(e => {
if (requestId_ !== e.requestId) return;
onFinalMessage(e)
this._dispose(requestId_)
})
)
this._addDisposable(requestId_,
this.onErrorEvent(e => {
console.log('sendLLMMessageService - error event received (havent checked req)')
if (requestId_ !== e.requestId) return;
console.log('sendLLMMessageService - error event received', JSON.stringify(e))
onError(e)
this._dispose(requestId_)
})
)
// params will be stripped of all its functions
this.channel.call('sendLLMMessage', { ...proxyParams, requestId: requestId_ } satisfies ProxyLLMMessageParams);
@ -97,17 +84,16 @@ export class SendLLMMessageService implements ISendLLMMessageService {
return requestId_
}
private _dispose(requestId: string) {
if (!(requestId in this._disposablesOfRequestId)) return
for (const disposable of this._disposablesOfRequestId[requestId]) {
disposable.dispose()
}
delete this._disposablesOfRequestId[requestId]
}
abort(requestId: string) {
this.channel.call('abort', { requestId } satisfies ProxyLLMMessageAbortParams);
this._dispose(requestId)
this._onRequestIdDone(requestId)
}
_onRequestIdDone(requestId: string) {
delete this.onTextHooks[requestId]
delete this.onFinalMessageHooks[requestId]
delete this.onErrorHooks[requestId]
}
}

View file

@ -34,7 +34,7 @@ export const voidProviderDefaults = {
},
openAICompatible: {
apiKey: '',
endpoint: 'http://127.0.0.1:11434/v1',
endpoint: 'https://my-website.com/v1',
},
gemini: {
apiKey: '',
@ -248,7 +248,8 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
providerName === 'openRouter' ? 'sk-or-abc123...' : // sk-or-v1-abc123
providerName === 'gemini' ? 'abc123...' :
providerName === 'groq' ? 'gsk_abc123...' :
'(never)',
providerName === 'openAICompatible' ? 'sk-abc123...' :
'(never)',
}
}
else if (settingName === 'endpoint') {

View file

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

View file

@ -47,7 +47,7 @@ export const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onTex
onError({ error: 'Invalid API key.' })
}
else {
onError({ error })
onError({ error: error + '' })
}
})

View file

@ -42,7 +42,7 @@ export const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, on
onError({ error: 'Invalid API key.' });
}
else {
onError({ error });
onError({ error: error + '' });
}
})
}

View file

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

View file

@ -1,4 +1,4 @@
import { Ollama, ErrorResponse } from 'ollama';
import { Ollama } from 'ollama';
import { SendLLMMessageFnTypeInternal } from './util';
import { parseMaxTokensStr } from './util.js';
@ -30,14 +30,14 @@ export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText,
})
// when error/fail
.catch((error) => {
if (typeof error === 'object') {
const e = error.error as ErrorResponse['error']
if (e) {
const name = error.name ?? 'Error'
onError({ error: `${name}: ${e}` })
return;
}
}
// if (typeof error === 'object') {
// const e = error.error as ErrorResponse['error']
// if (e) {
// const name = error.name ?? 'Error'
// onError({ error: `${name}: ${e}` })
// return;
// }
// }
onError({ error })
})

View file

@ -56,7 +56,7 @@ export const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText,
onError({ error: 'Invalid API key.' });
}
else {
onError({ error });
onError({ error: error + '' });
}
})

View file

@ -87,9 +87,6 @@ export const sendLLMMessage = ({
case 'ollama':
sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, providerName });
break;
// case 'greptile':
// sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, providerName });
// break;
case 'groq':
sendGroqMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, providerName });
break;
@ -100,7 +97,7 @@ export const sendLLMMessage = ({
}
catch (error) {
if (error instanceof Error) { onError({ error }) }
if (error instanceof Error) { onError({ error: error + '' }) }
else { onError({ error: `Unexpected Error in sendLLMMessage: ${error}` }); }
// ; (_aborter as any)?.()
// _didAbort = true

View file

@ -76,8 +76,8 @@ export class LLMMessageChannel implements IServerChannel {
const mainThreadParams: SendLLMMMessageParams = {
...params,
onText: ({ newText, fullText }) => { console.log('sendLLM: firing onText'); this._onText.fire({ requestId, newText, fullText }); },
onFinalMessage: ({ fullText }) => { console.log('sendLLM: firing finalMsg'); this._onFinalMessage.fire({ requestId, fullText }); },
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 }); },
abortRef: this._abortRefOfRequestId[requestId],
}

View file

@ -55,7 +55,7 @@ const getErrorDetails = (error: unknown) => {
}
// Collect any additional properties from the e
// 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]

View file

@ -1,161 +0,0 @@
import React, { useState } from 'react';
import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react';
import { getCmdKey } from '../../../getCmdKey.js';
// 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(`The server didn't give a very useful error message. 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 the 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,
className = ''
}: {
error: Error | object | string,
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;
return (
<div className={`rounded-lg border border-red-200 bg-red-50 p-4 ${className}`}>
{/* Header */}
<div className="flex items-start justify-between">
<div className="flex gap-3">
<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}
</h3>
<p className="text-red-700 mt-1">
{details.message}
</p>
</div>
</div>
<div className="flex gap-2">
{hasDetails && (
<button
onClick={() => setIsExpanded(!isExpanded)}
className="text-red-600 hover:text-red-800 p-1 rounded"
>
{isExpanded ? (
<ChevronUp className="h-5 w-5" />
) : (
<ChevronDown className="h-5 w-5" />
)}
</button>
)}
{showDismiss && onDismiss && (
<button
onClick={onDismiss}
className="text-red-600 hover:text-red-800 p-1 rounded"
>
<X className="h-5 w-5" />
</button>
)}
</div>
</div>
{/* Expandable Details */}
{isExpanded && hasDetails && (
<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>
)}
</div>
);
};