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:
shahondin1624
2026-03-12 13:27:40 +01:00
parent 284f84bd39
commit 14c83832f5
10 changed files with 493 additions and 23 deletions

View File

@@ -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}