feat: add error handling, toast notifications, connection status, and retry logic (#20)
Add global error boundary, toast notification system for gRPC errors, connection status indicator in chat header, and automatic retry with exponential backoff for transient failures. Map gRPC status codes to user-friendly messages and add a Retry button on failed requests. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,15 +16,18 @@
|
||||
import SessionSidebar from '$lib/components/SessionSidebar.svelte';
|
||||
import ConfigSidebar from '$lib/components/ConfigSidebar.svelte';
|
||||
import ThemeToggle from '$lib/components/ThemeToggle.svelte';
|
||||
import { processRequest, OrchestratorError } from '$lib/services/orchestrator';
|
||||
import ConnectionStatus from '$lib/components/ConnectionStatus.svelte';
|
||||
import { processRequest, OrchestratorError, friendlyMessage } from '$lib/services/orchestrator';
|
||||
import { OrchestrationState } from '$lib/proto/llm_multiverse/v1/orchestrator_pb';
|
||||
import { sessionStore } from '$lib/stores/sessions.svelte';
|
||||
import { memoryStore } from '$lib/stores/memory.svelte';
|
||||
import { auditStore } from '$lib/stores/audit.svelte';
|
||||
import { toastStore } from '$lib/stores/toast.svelte';
|
||||
|
||||
let messages: ChatMessage[] = $state([]);
|
||||
let isStreaming = $state(false);
|
||||
let error: string | null = $state(null);
|
||||
let lastFailedContent: string | null = $state(null);
|
||||
let orchestrationState: OrchestrationState = $state(OrchestrationState.UNSPECIFIED);
|
||||
let intermediateResult: string = $state('');
|
||||
let finalResult: SubagentResult | null = $state(null);
|
||||
@@ -79,6 +82,7 @@
|
||||
|
||||
async function handleSend(content: string) {
|
||||
error = null;
|
||||
lastFailedContent = null;
|
||||
orchestrationState = OrchestrationState.UNSPECIFIED;
|
||||
intermediateResult = '';
|
||||
finalResult = null;
|
||||
@@ -143,25 +147,37 @@
|
||||
};
|
||||
}
|
||||
} catch (err) {
|
||||
const msg =
|
||||
const friendlyMsg =
|
||||
err instanceof OrchestratorError
|
||||
? `Error (${err.code}): ${err.message}`
|
||||
? friendlyMessage(err.code)
|
||||
: 'An unexpected error occurred';
|
||||
error = msg;
|
||||
error = friendlyMsg;
|
||||
lastFailedContent = content;
|
||||
toastStore.addToast({ message: friendlyMsg, type: 'error' });
|
||||
auditStore.addEvent(sessionId, {
|
||||
eventType: 'error',
|
||||
details: msg
|
||||
details: friendlyMsg
|
||||
});
|
||||
const idx = messages.length - 1;
|
||||
messages[idx] = {
|
||||
...messages[idx],
|
||||
content: `\u26A0 ${msg}`
|
||||
content: `\u26A0 ${friendlyMsg}`
|
||||
};
|
||||
} finally {
|
||||
isStreaming = false;
|
||||
sessionStore.updateMessages(sessionId, messages);
|
||||
}
|
||||
}
|
||||
|
||||
function handleRetry() {
|
||||
if (!lastFailedContent) return;
|
||||
const content = lastFailedContent;
|
||||
// Remove the failed assistant message before retrying
|
||||
if (messages.length >= 2) {
|
||||
messages = messages.slice(0, -2);
|
||||
}
|
||||
handleSend(content);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-screen overflow-hidden bg-white dark:bg-gray-900">
|
||||
@@ -194,6 +210,7 @@
|
||||
</svg>
|
||||
</button>
|
||||
<h1 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Chat</h1>
|
||||
<ConnectionStatus />
|
||||
</div>
|
||||
<div class="flex items-center gap-1 md:gap-2">
|
||||
<!-- eslint-disable svelte/no-navigation-without-resolve -- resolveRoute is resolve; plugin does not recognize the alias -->
|
||||
@@ -263,8 +280,18 @@
|
||||
{/if}
|
||||
|
||||
{#if error}
|
||||
<div class="mx-4 mb-2 rounded-lg bg-red-50 dark:bg-red-900/30 px-4 py-2 text-sm text-red-600 dark:text-red-400">
|
||||
{error}
|
||||
<div class="mx-4 mb-2 flex items-center justify-between gap-3 rounded-lg bg-red-50 dark:bg-red-900/30 px-4 py-2 text-sm text-red-600 dark:text-red-400">
|
||||
<span>{error}</span>
|
||||
{#if lastFailedContent}
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleRetry}
|
||||
disabled={isStreaming}
|
||||
class="shrink-0 rounded-md bg-red-100 px-3 py-1 text-xs font-medium text-red-700 hover:bg-red-200 dark:bg-red-800/50 dark:text-red-300 dark:hover:bg-red-800 disabled:opacity-50"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user