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 { 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 (
<ThreadErrorCard
error={statusError}
onGoBack={handleBackToHome}
/>
);
}
if (!thread || threadsLoading) {
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 { 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<ManagerGraphState>): {
metadata: ThreadMetadata;
isStatusLoading: boolean;
statusError: Error | null;
statusError: Error | ThreadStatusError | null;
} {
const {
status,

View file

@ -56,7 +56,7 @@ export function useThreadStatus(
status: data?.status || "idle",
taskPlan: data?.taskPlan,
isLoading,
error,
error: data?.error ?? error,
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 {
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;
}

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 { 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,
};
}
}