feat: structured artifact rendering, UX improvements
- Render structured artifacts from agent results with type-aware formatting: code blocks with syntax highlighting and copy button, terminal-style command output, search result cards, and text findings - Make FinalResult panel collapsible (default collapsed) with scrollable content (max-h-96) to prevent dominating the chat view - Add clickable URL detection in summaries and artifact content - Fix code block contrast for both light and dark mode - Animate progress bar with pulse ring on active step and gradient shimmer on connecting lines - Fix tab-switching bug: use module-level orchestrationStore singleton so orchestration state survives route navigation - Remove sample/demo data seeding and clean up persisted localStorage entries from previous sample sessions - Remove showSampleBadge prop from PageHeader - Regenerate proto types for new Artifact message and ArtifactType enum - Update README project structure (remove deleted data/ directory) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { createClient } from '@connectrpc/connect';
|
||||
import { createClient, ConnectError, Code } from '@connectrpc/connect';
|
||||
import { createGrpcWebTransport } from '@connectrpc/connect-web';
|
||||
import { OrchestratorService } from '$lib/proto/llm_multiverse/v1/orchestrator_pb';
|
||||
import type {
|
||||
@@ -9,6 +9,7 @@ import { create } from '@bufbuild/protobuf';
|
||||
import { ProcessRequestRequestSchema } from '$lib/proto/llm_multiverse/v1/orchestrator_pb';
|
||||
import { connectionStore } from '$lib/stores/connection.svelte';
|
||||
import { toastStore } from '$lib/stores/toast.svelte';
|
||||
import { logger } from '$lib/utils/logger';
|
||||
|
||||
/**
|
||||
* Application-level error wrapping gRPC status codes.
|
||||
@@ -112,10 +113,35 @@ function backoffDelay(attempt: number): number {
|
||||
|
||||
const MAX_RETRIES = 3;
|
||||
|
||||
/**
|
||||
* Map numeric Code enum values to lowercase string names matching GRPC_USER_MESSAGES keys.
|
||||
*/
|
||||
const CODE_TO_STRING: Record<number, string> = {
|
||||
[Code.Canceled]: 'cancelled',
|
||||
[Code.Unknown]: 'unknown',
|
||||
[Code.InvalidArgument]: 'failed_precondition',
|
||||
[Code.DeadlineExceeded]: 'deadline_exceeded',
|
||||
[Code.NotFound]: 'not_found',
|
||||
[Code.AlreadyExists]: 'already_exists',
|
||||
[Code.PermissionDenied]: 'permission_denied',
|
||||
[Code.ResourceExhausted]: 'resource_exhausted',
|
||||
[Code.FailedPrecondition]: 'failed_precondition',
|
||||
[Code.Aborted]: 'aborted',
|
||||
[Code.OutOfRange]: 'failed_precondition',
|
||||
[Code.Unimplemented]: 'unimplemented',
|
||||
[Code.Internal]: 'internal',
|
||||
[Code.Unavailable]: 'unavailable',
|
||||
[Code.DataLoss]: 'data_loss',
|
||||
[Code.Unauthenticated]: 'unauthenticated'
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract gRPC error code from an error, normalising to lowercase string.
|
||||
*/
|
||||
function extractCode(err: unknown): string {
|
||||
if (err instanceof ConnectError) {
|
||||
return CODE_TO_STRING[err.code] ?? 'unknown';
|
||||
}
|
||||
if (err instanceof Error && 'code' in err) {
|
||||
const raw = (err as { code: unknown }).code;
|
||||
if (typeof raw === 'string') return raw.toLowerCase();
|
||||
@@ -129,6 +155,14 @@ function extractCode(err: unknown): string {
|
||||
*/
|
||||
function toOrchestratorError(err: unknown): OrchestratorError {
|
||||
if (err instanceof OrchestratorError) return err;
|
||||
if (err instanceof ConnectError) {
|
||||
const code = extractCode(err);
|
||||
const details = [
|
||||
err.rawMessage,
|
||||
err.cause ? String(err.cause) : ''
|
||||
].filter(Boolean).join('; ');
|
||||
return new OrchestratorError(friendlyMessage(code), code, details || undefined);
|
||||
}
|
||||
if (err instanceof Error) {
|
||||
const code = extractCode(err);
|
||||
return new OrchestratorError(friendlyMessage(code), code, err.message);
|
||||
@@ -136,6 +170,13 @@ function toOrchestratorError(err: unknown): OrchestratorError {
|
||||
return new OrchestratorError(friendlyMessage('unknown'), 'unknown');
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a diagnostic code suffix to a message, e.g. "(code: unavailable)".
|
||||
*/
|
||||
function diagnosticSuffix(err: OrchestratorError): string {
|
||||
return err.code && err.code !== 'unknown' ? ` (code: ${err.code})` : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a request to the orchestrator and yield streaming responses.
|
||||
*
|
||||
@@ -158,6 +199,11 @@ export async function* processRequest(
|
||||
sessionConfig
|
||||
});
|
||||
|
||||
logger.debug('orchestrator', 'processRequest', {
|
||||
sessionId,
|
||||
messageLength: userMessage.length
|
||||
});
|
||||
|
||||
let lastError: OrchestratorError | null = null;
|
||||
|
||||
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
||||
@@ -169,6 +215,11 @@ export async function* processRequest(
|
||||
if (attempt > 0) {
|
||||
connectionStore.setReconnecting();
|
||||
const delay = backoffDelay(attempt - 1);
|
||||
logger.warn('orchestrator', `Retry attempt ${attempt}/${MAX_RETRIES}`, {
|
||||
sessionId,
|
||||
previousCode: lastError?.code,
|
||||
delay
|
||||
});
|
||||
await sleep(delay);
|
||||
}
|
||||
|
||||
@@ -178,9 +229,11 @@ export async function* processRequest(
|
||||
connectionStore.reportSuccess();
|
||||
yield response;
|
||||
}
|
||||
logger.debug('orchestrator', 'Stream completed', { sessionId });
|
||||
// Completed successfully — no retry needed
|
||||
return;
|
||||
} catch (err: unknown) {
|
||||
logger.grpcError('orchestrator', `Request failed (attempt ${attempt + 1}/${MAX_RETRIES + 1})`, err);
|
||||
lastError = toOrchestratorError(err);
|
||||
const code = lastError.code;
|
||||
|
||||
@@ -192,7 +245,12 @@ export async function* processRequest(
|
||||
|
||||
// Non-transient or exhausted retries
|
||||
connectionStore.reportFailure();
|
||||
toastStore.addToast({ message: lastError.message, type: 'error' });
|
||||
const suffix = diagnosticSuffix(lastError);
|
||||
logger.error('orchestrator', 'Request failed permanently', {
|
||||
code: lastError.code,
|
||||
details: lastError.details
|
||||
});
|
||||
toastStore.addToast({ message: lastError.message + suffix, type: 'error' });
|
||||
throw lastError;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user