mirror of
https://github.com/voideditor/void
synced 2026-05-24 01:48:25 +00:00
consistent error messages
This commit is contained in:
parent
f20f41c804
commit
72deba43dd
13 changed files with 64 additions and 241 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onTex
|
|||
onError({ error: 'Invalid API key.' })
|
||||
}
|
||||
else {
|
||||
onError({ error })
|
||||
onError({ error: error + '' })
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, on
|
|||
onError({ error: 'Invalid API key.' });
|
||||
}
|
||||
else {
|
||||
onError({ error });
|
||||
onError({ error: error + '' });
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ export const sendGroqMsg: SendLLMMessageFnTypeInternal = async ({ messages, onTe
|
|||
onFinalMessage({ fullText });
|
||||
})
|
||||
.catch(error => {
|
||||
onError({ error });
|
||||
onError({ error: error + '' });
|
||||
})
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText,
|
|||
onError({ error: 'Invalid API key.' });
|
||||
}
|
||||
else {
|
||||
onError({ error });
|
||||
onError({ error: error + '' });
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
Loading…
Reference in a new issue