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 82c8d30b..2d37cc59 100644 --- a/apps/web/src/app/(v2)/chat/[thread_id]/page.tsx +++ b/apps/web/src/app/(v2)/chat/[thread_id]/page.tsx @@ -6,13 +6,32 @@ 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"; -import { useGitHubAppProvider } from "@/providers/GitHubApp"; import { MANAGER_GRAPH_ID } from "@open-swe/shared/constants"; import { ManagerGraphState } from "@open-swe/shared/open-swe/manager/types"; import { useRouter } from "next/navigation"; import * as React from "react"; -import { use, useMemo } from "react"; -import { threadsToMetadata } from "@/lib/thread-utils"; +import { use, useEffect, useRef, useState } from "react"; +import { Client, Thread } from "@langchain/langgraph-sdk"; + +async function fetchInitialThread( + client: Client, + threadId: string, + reqCount = 0, +): Promise | null> { + try { + return await client.threads.get(threadId); + } catch (e) { + console.error("Failed to fetch thread", { + requestCount: reqCount, + error: e, + }); + // Retry a max of 5 times + if (reqCount < 5) { + return fetchInitialThread(client, threadId, reqCount + 1); + } + return null; + } +} interface ThreadPageProps { thread_id: string; @@ -23,6 +42,8 @@ export default function ThreadPage({ }: { params: Promise; }) { + const [initialFetchedThread, setInitialFetchedThread] = + useState | null>(null); const router = useRouter(); const { thread_id } = use(params); const stream = useStream({ @@ -38,19 +59,18 @@ export default function ThreadPage({ disableOrgFiltering: true, }); - const threadsMetadata = useMemo(() => threadsToMetadata(threads), [threads]); - // Find the thread by ID const thread = threads.find((t) => t.thread_id === thread_id); // We need a thread object for the hook, so use a dummy if not found - const dummyThread = thread || { - thread_id: thread_id, - values: {}, - status: "idle" as const, - updated_at: new Date().toISOString(), - created_at: new Date().toISOString(), - }; + const dummyThread = thread || + initialFetchedThread || { + thread_id, + values: {}, + status: "idle" as const, + updated_at: new Date().toISOString(), + created_at: new Date().toISOString(), + }; const { metadata: currentDisplayThread, statusError } = useThreadMetadata( dummyThread as any, @@ -60,6 +80,19 @@ export default function ThreadPage({ router.push("/chat"); }; + const initialThreadFetched = useRef(false); + useEffect(() => { + if (!thread && !initialFetchedThread && !initialThreadFetched.current) { + fetchInitialThread(stream.client as Client, thread_id) + .then(setInitialFetchedThread) + .finally(() => (initialThreadFetched.current = true)); + } + + if (initialThreadFetched.current && initialFetchedThread && thread) { + setInitialFetchedThread(null); + } + }, [thread_id, thread]); + if (statusError && "message" in statusError && "type" in statusError) { return ( ; } @@ -78,7 +114,6 @@ export default function ThreadPage({ diff --git a/apps/web/src/components/v2/thread-switcher.tsx b/apps/web/src/components/v2/thread-switcher.tsx index 88c34df5..6da25672 100644 --- a/apps/web/src/components/v2/thread-switcher.tsx +++ b/apps/web/src/components/v2/thread-switcher.tsx @@ -1,9 +1,9 @@ "use client"; -import { useState } from "react"; +import { useMemo, useState } from "react"; import { Button } from "@/components/ui/button"; -import { Badge } from "@/components/ui/badge"; import { ScrollArea } from "@/components/ui/scroll-area"; +import { Skeleton } from "@/components/ui/skeleton"; import { Sheet, SheetContent, @@ -11,32 +11,29 @@ import { SheetTitle, SheetTrigger, } from "@/components/ui/sheet"; -import { - CheckCircle, - XCircle, - Loader2, - GitBranch, - Layers3, - Plus, - Bug, -} from "lucide-react"; +import { Layers3, Plus } from "lucide-react"; import { useRouter } from "next/navigation"; import { ThreadMetadata } from "./types"; import { ThreadCard } from "./thread-card"; +import { useThreadsSWR } from "@/hooks/useThreadsSWR"; +import { MANAGER_GRAPH_ID } from "@open-swe/shared/constants"; +import { threadsToMetadata } from "@/lib/thread-utils"; interface ThreadSwitcherProps { currentThread: ThreadMetadata; - allThreads: ThreadMetadata[]; } -export function ThreadSwitcher({ - currentThread, - allThreads, -}: ThreadSwitcherProps) { +export function ThreadSwitcher({ currentThread }: ThreadSwitcherProps) { const [open, setOpen] = useState(false); const router = useRouter(); - const otherThreads = allThreads.filter((t) => t.id !== currentThread.id); + const { threads, isLoading: threadsLoading } = useThreadsSWR({ + assistantId: MANAGER_GRAPH_ID, + disableOrgFiltering: true, + }); + + const threadsMetadata = useMemo(() => threadsToMetadata(threads), [threads]); + const otherThreads = threadsMetadata.filter((t) => t.id !== currentThread.id); return ( {/* Other Threads */} - {otherThreads.length > 0 && ( + {threadsLoading ? ( +
+

+ Other Threads +

+ +
+ {Array.from({ length: 3 }).map((_, i) => ( +
+ + +
+ ))} +
+
+
+ ) : otherThreads.length > 0 ? (

Other Threads @@ -102,6 +118,17 @@ export function ThreadSwitcher({

+ ) : ( +
+

+ Other Threads +

+
+

+ No other threads +

+
+
)} diff --git a/apps/web/src/components/v2/thread-view.tsx b/apps/web/src/components/v2/thread-view.tsx index f47aa535..9a7a4ef1 100644 --- a/apps/web/src/components/v2/thread-view.tsx +++ b/apps/web/src/components/v2/thread-view.tsx @@ -47,7 +47,6 @@ import { TokenUsage } from "./token-usage"; interface ThreadViewProps { stream: ReturnType>; displayThread: ThreadMetadata; - allDisplayThreads: ThreadMetadata[]; onBackToHome: () => void; } @@ -87,7 +86,6 @@ const joinTokenData = ( export function ThreadView({ stream, displayThread, - allDisplayThreads, onBackToHome, }: ThreadViewProps) { const [chatInput, setChatInput] = useState(""); @@ -312,10 +310,7 @@ export function ThreadView({ )} - +