fix: Better component for rendering thread errors (#528)

* fix: Better component for rendering thread errors

* cr
This commit is contained in:
Brace Sproul 2025-07-24 17:55:23 -07:00 • committed by GitHub
parent 5c7a7dcb49
commit 3d944cc81e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 159 additions and 3 deletions

View file

@ -2,6 +2,7 @@
import { ThreadView } from "@/components/v2/thread-view"; import { ThreadView } from "@/components/v2/thread-view";
import { ThreadViewLoading } from "@/components/v2/thread-view-loading"; import { ThreadViewLoading } from "@/components/v2/thread-view-loading";
import { ThreadErrorCard } from "@/components/v2/thread-error-card";
import { useThreadMetadata } from "@/hooks/useThreadMetadata"; import { useThreadMetadata } from "@/hooks/useThreadMetadata";
import { useThreadsSWR } from "@/hooks/useThreadsSWR"; import { useThreadsSWR } from "@/hooks/useThreadsSWR";
import { useStream } from "@langchain/langgraph-sdk/react"; import { useStream } from "@langchain/langgraph-sdk/react";
@ -53,7 +54,7 @@ export default function ThreadPage({
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
}; };
const { metadata: currentDisplayThread } = useThreadMetadata( const { metadata: currentDisplayThread, statusError } = useThreadMetadata(
dummyThread as any, dummyThread as any,
); );
@ -61,6 +62,15 @@ export default function ThreadPage({
router.push("/chat"); router.push("/chat");
}; };
if (statusError && "message" in statusError && "type" in statusError) {
return (
<ThreadErrorCard
error={statusError}
onGoBack={handleBackToHome}
/>
);
}
if (!thread || threadsLoading) { if (!thread || threadsLoading) {
return <ThreadViewLoading onBackToHome={handleBackToHome} />; return <ThreadViewLoading onBackToHome={handleBackToHome} />;
} }

View file

@ -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 (
<Search className="h-4 w-4 text-amber-500 dark:text-amber-400" />
);
case "unauthorized":
return <Lock className="h-4 w-4 text-red-500 dark:text-red-400" />;
default:
return (
<AlertCircle className="h-4 w-4 text-red-500 dark:text-red-400" />
);
}
};
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 (
<div className="bg-background fixed inset-0 flex items-center justify-center p-4">
<div className="bg-card w-full max-w-md rounded-lg border p-6 shadow-lg">
<div className="space-y-4">
<div className="flex items-center gap-2">
{getErrorIcon()}
<h4 className="text-sm font-semibold">Thread Error</h4>
</div>
<div className="space-y-3">
<div className="flex items-center justify-between">
<span className="text-muted-foreground text-xs font-medium">
Type
</span>
<Badge
variant="outline"
className={cn(getErrorTypeColor())}
>
{getErrorTypeLabel()}
</Badge>
</div>
<Separator />
<div className="space-y-2">
<span className="text-muted-foreground text-xs font-medium">
Message
</span>
<p className="text-sm font-medium">{error.message}</p>
</div>
</div>
<div className="pt-2">
<Button
onClick={onGoBack}
variant="outline"
className="flex w-full items-center gap-2"
>
<ArrowLeft className="h-4 w-4" />
Go Back
</Button>
</div>
</div>
</div>
</div>
);
}

View file

@ -5,6 +5,7 @@ import { useThreadStatus } from "./useThreadStatus";
import { useMemo } from "react"; import { useMemo } from "react";
import { getThreadTitle, computeThreadTitle } from "@/lib/thread"; import { getThreadTitle, computeThreadTitle } from "@/lib/thread";
import { calculateLastActivity } from "@/lib/thread-utils"; import { calculateLastActivity } from "@/lib/thread-utils";
import { ThreadStatusError } from "@/lib/schemas/thread-status";
/** /**
* Hook that combines thread metadata with real-time 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<ManagerGraphState>): { export function useThreadMetadata(thread: Thread<ManagerGraphState>): {
metadata: ThreadMetadata; metadata: ThreadMetadata;
isStatusLoading: boolean; isStatusLoading: boolean;
statusError: Error | null; statusError: Error | ThreadStatusError | null;
} { } {
const { const {
status, status,

View file

@ -56,7 +56,7 @@ export function useThreadStatus(
status: data?.status || "idle", status: data?.status || "idle",
taskPlan: data?.taskPlan, taskPlan: data?.taskPlan,
isLoading, isLoading,
error, error: data?.error ?? error,
mutate, mutate,
}; };
} }

View file

@ -33,6 +33,11 @@ export function mapLangGraphToUIStatus(status: ThreadStatus): ThreadUIStatus {
} }
} }
export interface ThreadStatusError {
message: string;
type: "not_found" | "unauthorized";
}
export interface ThreadStatusData { export interface ThreadStatusData {
graph: graph:
| typeof MANAGER_GRAPH_ID | typeof MANAGER_GRAPH_ID
@ -42,4 +47,5 @@ export interface ThreadStatusData {
threadId: string; threadId: string;
status: ThreadUIStatus; status: ThreadUIStatus;
taskPlan?: TaskPlan; // Task plan data when available from programmer sessions taskPlan?: TaskPlan; // Task plan data when available from programmer sessions
error?: ThreadStatusError;
} }

View file

@ -11,6 +11,42 @@ import { PlannerGraphState } from "@open-swe/shared/open-swe/planner/types";
import { getActivePlanItems } from "@open-swe/shared/open-swe/tasks"; import { getActivePlanItems } from "@open-swe/shared/open-swe/tasks";
import { SessionCache, SessionCacheData } from "@/hooks/useThreadsStatus"; 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 { interface StatusResult {
graph: "manager" | "planner" | "programmer"; graph: "manager" | "planner" | "programmer";
runId: string; runId: string;
@ -134,6 +170,7 @@ export async function fetchThreadStatus(
sessionCache, sessionCache,
); );
} catch (error) { } catch (error) {
const errorFields = getErrorFields(error);
console.error(`Error fetching thread status for ${threadId}:`, error); console.error(`Error fetching thread status for ${threadId}:`, error);
const graph = lastPollingState?.graph || "manager"; const graph = lastPollingState?.graph || "manager";
@ -145,6 +182,7 @@ export async function fetchThreadStatus(
runId, runId,
threadId: errorThreadId, threadId: errorThreadId,
status: "error", status: "error",
error: errorFields,
}; };
} }
} }