From 3d944cc81e4fb222c2bf8cfd742c0510779a6e10 Mon Sep 17 00:00:00 2001 From: Brace Sproul Date: Thu, 24 Jul 2025 17:55:23 -0700 Subject: [PATCH] fix: Better component for rendering thread errors (#528) * fix: Better component for rendering thread errors * cr --- .../src/app/(v2)/chat/[thread_id]/page.tsx | 12 ++- .../src/components/v2/thread-error-card.tsx | 101 ++++++++++++++++++ apps/web/src/hooks/useThreadMetadata.ts | 3 +- apps/web/src/hooks/useThreadStatus.ts | 2 +- apps/web/src/lib/schemas/thread-status.ts | 6 ++ .../web/src/services/thread-status.service.ts | 38 +++++++ 6 files changed, 159 insertions(+), 3 deletions(-) create mode 100644 apps/web/src/components/v2/thread-error-card.tsx diff --git a/apps/web/src/app/(v2)/chat/[thread_id]/page.tsx b/apps/web/src/app/(v2)/chat/[thread_id]/page.tsx index 0d34bdbc..b8c7265b 100644 --- a/apps/web/src/app/(v2)/chat/[thread_id]/page.tsx +++ b/apps/web/src/app/(v2)/chat/[thread_id]/page.tsx @@ -2,6 +2,7 @@ import { ThreadView } from "@/components/v2/thread-view"; import { ThreadViewLoading } from "@/components/v2/thread-view-loading"; +import { ThreadErrorCard } from "@/components/v2/thread-error-card"; import { useThreadMetadata } from "@/hooks/useThreadMetadata"; import { useThreadsSWR } from "@/hooks/useThreadsSWR"; import { useStream } from "@langchain/langgraph-sdk/react"; @@ -53,7 +54,7 @@ export default function ThreadPage({ created_at: new Date().toISOString(), }; - const { metadata: currentDisplayThread } = useThreadMetadata( + const { metadata: currentDisplayThread, statusError } = useThreadMetadata( dummyThread as any, ); @@ -61,6 +62,15 @@ export default function ThreadPage({ router.push("/chat"); }; + if (statusError && "message" in statusError && "type" in statusError) { + return ( + + ); + } + if (!thread || threadsLoading) { return ; } diff --git a/apps/web/src/components/v2/thread-error-card.tsx b/apps/web/src/components/v2/thread-error-card.tsx new file mode 100644 index 00000000..2695293a --- /dev/null +++ b/apps/web/src/components/v2/thread-error-card.tsx @@ -0,0 +1,101 @@ +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; +import { cn } from "@/lib/utils"; +import { AlertCircle, ArrowLeft, Lock, Search } from "lucide-react"; + +export interface ThreadStatusError { + message: string; + type: "not_found" | "unauthorized"; +} + +interface ThreadErrorCardProps { + error: ThreadStatusError; + onGoBack: () => void; +} + +export function ThreadErrorCard({ error, onGoBack }: ThreadErrorCardProps) { + const getErrorIcon = () => { + switch (error.type) { + case "not_found": + return ( + + ); + case "unauthorized": + return ; + default: + return ( + + ); + } + }; + + const getErrorTypeLabel = () => { + switch (error.type) { + case "not_found": + return "Not Found"; + case "unauthorized": + return "Unauthorized"; + default: + return "Error"; + } + }; + + const getErrorTypeColor = () => { + switch (error.type) { + case "not_found": + return "border-amber-200 text-amber-600 dark:border-amber-800 dark:text-amber-400"; + case "unauthorized": + return "border-red-200 text-red-600 dark:border-red-800 dark:text-red-400"; + default: + return "border-red-200 text-red-600 dark:border-red-800 dark:text-red-400"; + } + }; + + return ( +
+
+
+
+ {getErrorIcon()} +

Thread Error

+
+ +
+
+ + Type + + + {getErrorTypeLabel()} + +
+ + + +
+ + Message + +

{error.message}

+
+
+ +
+ +
+
+
+
+ ); +} diff --git a/apps/web/src/hooks/useThreadMetadata.ts b/apps/web/src/hooks/useThreadMetadata.ts index b6108f61..dd82c29a 100644 --- a/apps/web/src/hooks/useThreadMetadata.ts +++ b/apps/web/src/hooks/useThreadMetadata.ts @@ -5,6 +5,7 @@ import { useThreadStatus } from "./useThreadStatus"; import { useMemo } from "react"; import { getThreadTitle, computeThreadTitle } from "@/lib/thread"; import { calculateLastActivity } from "@/lib/thread-utils"; +import { ThreadStatusError } from "@/lib/schemas/thread-status"; /** * Hook that combines thread metadata with real-time status @@ -12,7 +13,7 @@ import { calculateLastActivity } from "@/lib/thread-utils"; export function useThreadMetadata(thread: Thread): { metadata: ThreadMetadata; isStatusLoading: boolean; - statusError: Error | null; + statusError: Error | ThreadStatusError | null; } { const { status, diff --git a/apps/web/src/hooks/useThreadStatus.ts b/apps/web/src/hooks/useThreadStatus.ts index d3a5283f..b17bf4da 100644 --- a/apps/web/src/hooks/useThreadStatus.ts +++ b/apps/web/src/hooks/useThreadStatus.ts @@ -56,7 +56,7 @@ export function useThreadStatus( status: data?.status || "idle", taskPlan: data?.taskPlan, isLoading, - error, + error: data?.error ?? error, mutate, }; } diff --git a/apps/web/src/lib/schemas/thread-status.ts b/apps/web/src/lib/schemas/thread-status.ts index 4ac80e8f..2fdcaddd 100644 --- a/apps/web/src/lib/schemas/thread-status.ts +++ b/apps/web/src/lib/schemas/thread-status.ts @@ -33,6 +33,11 @@ export function mapLangGraphToUIStatus(status: ThreadStatus): ThreadUIStatus { } } +export interface ThreadStatusError { + message: string; + type: "not_found" | "unauthorized"; +} + export interface ThreadStatusData { graph: | typeof MANAGER_GRAPH_ID @@ -42,4 +47,5 @@ export interface ThreadStatusData { threadId: string; status: ThreadUIStatus; taskPlan?: TaskPlan; // Task plan data when available from programmer sessions + error?: ThreadStatusError; } diff --git a/apps/web/src/services/thread-status.service.ts b/apps/web/src/services/thread-status.service.ts index 9b562dd0..4b131f5e 100644 --- a/apps/web/src/services/thread-status.service.ts +++ b/apps/web/src/services/thread-status.service.ts @@ -11,6 +11,42 @@ import { PlannerGraphState } from "@open-swe/shared/open-swe/planner/types"; import { getActivePlanItems } from "@open-swe/shared/open-swe/tasks"; import { SessionCache, SessionCacheData } from "@/hooks/useThreadsStatus"; +function getErrorFields(error: unknown): { + message: string; + type: "not_found" | "unauthorized"; +} { + if ( + !error || + typeof error !== "object" || + !("message" in error) || + !("status" in error) + ) { + return { + message: "Unknown error", + type: "unauthorized", + }; + } + + if (error.status === 404) { + return { + message: "Thread not found", + type: "not_found", + }; + } + + if (error.status === 401) { + return { + message: "Unauthorized", + type: "unauthorized", + }; + } + + return { + message: "Unknown error", + type: "unauthorized", + }; +} + interface StatusResult { graph: "manager" | "planner" | "programmer"; runId: string; @@ -134,6 +170,7 @@ export async function fetchThreadStatus( sessionCache, ); } catch (error) { + const errorFields = getErrorFields(error); console.error(`Error fetching thread status for ${threadId}:`, error); const graph = lastPollingState?.graph || "manager"; @@ -145,6 +182,7 @@ export async function fetchThreadStatus( runId, threadId: errorThreadId, status: "error", + error: errorFields, }; } }