fix: Initial thread loading bug (#650)

* fix: Initial thread loading bug

* cr

* cr
This commit is contained in:
Brace Sproul 2025-08-03 11:41:51 -07:00 • committed by GitHub
parent 4c75eca3f9
commit 45817c42c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 95 additions and 38 deletions

View file

@ -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<ManagerGraphState>,
threadId: string,
reqCount = 0,
): Promise<Thread<ManagerGraphState> | 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<ThreadPageProps>;
}) {
const [initialFetchedThread, setInitialFetchedThread] =
useState<Thread<ManagerGraphState> | null>(null);
const router = useRouter();
const { thread_id } = use(params);
const stream = useStream<ManagerGraphState>({
@ -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<ManagerGraphState>, 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 (
<ThreadErrorCard
@ -69,7 +102,10 @@ export default function ThreadPage({
);
}
if (!thread || threadsLoading) {
if (
(!thread || threadsLoading) &&
(!initialFetchedThread || !initialThreadFetched.current)
) {
return <ThreadViewLoading onBackToHome={handleBackToHome} />;
}
@ -78,7 +114,6 @@ export default function ThreadPage({
<ThreadView
stream={stream}
displayThread={currentDisplayThread}
allDisplayThreads={threadsMetadata}
onBackToHome={handleBackToHome}
/>
</div>

View file

@ -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 (
<Sheet
@ -86,7 +83,26 @@ export function ThreadSwitcher({
</div>
{/* Other Threads */}
{otherThreads.length > 0 && (
{threadsLoading ? (
<div className="h-full space-y-2">
<h3 className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
Other Threads
</h3>
<ScrollArea className="h-full">
<div className="space-y-1">
{Array.from({ length: 3 }).map((_, i) => (
<div
key={i}
className="border-border space-y-2 rounded-lg border p-3"
>
<Skeleton className="h-4 w-3/4" />
<Skeleton className="h-3 w-1/2" />
</div>
))}
</div>
</ScrollArea>
</div>
) : otherThreads.length > 0 ? (
<div className="h-full space-y-2">
<h3 className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
Other Threads
@ -102,6 +118,17 @@ export function ThreadSwitcher({
</div>
</ScrollArea>
</div>
) : (
<div className="h-full space-y-2">
<h3 className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
Other Threads
</h3>
<div className="flex h-32 items-center justify-center">
<p className="text-muted-foreground text-sm">
No other threads
</p>
</div>
</div>
)}
</div>
</SheetContent>

View file

@ -47,7 +47,6 @@ import { TokenUsage } from "./token-usage";
interface ThreadViewProps {
stream: ReturnType<typeof useStream<ManagerGraphState>>;
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({
</>
)}
</div>
<ThreadSwitcher
currentThread={displayThread}
allThreads={allDisplayThreads}
/>
<ThreadSwitcher currentThread={displayThread} />
<ThemeToggle />
</div>
</div>