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