mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-10-04 21:52:14 +00:00
fix: Initial thread loading bug (#650)
* fix: Initial thread loading bug * cr * cr
This commit is contained in:
parent
4c75eca3f9
commit
45817c42c2
3 changed files with 95 additions and 38 deletions
|
|
@ -6,13 +6,32 @@ 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";
|
||||||
import { useGitHubAppProvider } from "@/providers/GitHubApp";
|
|
||||||
import { MANAGER_GRAPH_ID } from "@open-swe/shared/constants";
|
import { MANAGER_GRAPH_ID } from "@open-swe/shared/constants";
|
||||||
import { ManagerGraphState } from "@open-swe/shared/open-swe/manager/types";
|
import { ManagerGraphState } from "@open-swe/shared/open-swe/manager/types";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { use, useMemo } from "react";
|
import { use, useEffect, useRef, useState } from "react";
|
||||||
import { threadsToMetadata } from "@/lib/thread-utils";
|
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 {
|
interface ThreadPageProps {
|
||||||
thread_id: string;
|
thread_id: string;
|
||||||
|
|
@ -23,6 +42,8 @@ export default function ThreadPage({
|
||||||
}: {
|
}: {
|
||||||
params: Promise<ThreadPageProps>;
|
params: Promise<ThreadPageProps>;
|
||||||
}) {
|
}) {
|
||||||
|
const [initialFetchedThread, setInitialFetchedThread] =
|
||||||
|
useState<Thread<ManagerGraphState> | null>(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { thread_id } = use(params);
|
const { thread_id } = use(params);
|
||||||
const stream = useStream<ManagerGraphState>({
|
const stream = useStream<ManagerGraphState>({
|
||||||
|
|
@ -38,19 +59,18 @@ export default function ThreadPage({
|
||||||
disableOrgFiltering: true,
|
disableOrgFiltering: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const threadsMetadata = useMemo(() => threadsToMetadata(threads), [threads]);
|
|
||||||
|
|
||||||
// Find the thread by ID
|
// Find the thread by ID
|
||||||
const thread = threads.find((t) => t.thread_id === thread_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
|
// We need a thread object for the hook, so use a dummy if not found
|
||||||
const dummyThread = thread || {
|
const dummyThread = thread ||
|
||||||
thread_id: thread_id,
|
initialFetchedThread || {
|
||||||
values: {},
|
thread_id,
|
||||||
status: "idle" as const,
|
values: {},
|
||||||
updated_at: new Date().toISOString(),
|
status: "idle" as const,
|
||||||
created_at: new Date().toISOString(),
|
updated_at: new Date().toISOString(),
|
||||||
};
|
created_at: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
const { metadata: currentDisplayThread, statusError } = useThreadMetadata(
|
const { metadata: currentDisplayThread, statusError } = useThreadMetadata(
|
||||||
dummyThread as any,
|
dummyThread as any,
|
||||||
|
|
@ -60,6 +80,19 @@ export default function ThreadPage({
|
||||||
router.push("/chat");
|
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) {
|
if (statusError && "message" in statusError && "type" in statusError) {
|
||||||
return (
|
return (
|
||||||
<ThreadErrorCard
|
<ThreadErrorCard
|
||||||
|
|
@ -69,7 +102,10 @@ export default function ThreadPage({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!thread || threadsLoading) {
|
if (
|
||||||
|
(!thread || threadsLoading) &&
|
||||||
|
(!initialFetchedThread || !initialThreadFetched.current)
|
||||||
|
) {
|
||||||
return <ThreadViewLoading onBackToHome={handleBackToHome} />;
|
return <ThreadViewLoading onBackToHome={handleBackToHome} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -78,7 +114,6 @@ export default function ThreadPage({
|
||||||
<ThreadView
|
<ThreadView
|
||||||
stream={stream}
|
stream={stream}
|
||||||
displayThread={currentDisplayThread}
|
displayThread={currentDisplayThread}
|
||||||
allDisplayThreads={threadsMetadata}
|
|
||||||
onBackToHome={handleBackToHome}
|
onBackToHome={handleBackToHome}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import {
|
import {
|
||||||
Sheet,
|
Sheet,
|
||||||
SheetContent,
|
SheetContent,
|
||||||
|
|
@ -11,32 +11,29 @@ import {
|
||||||
SheetTitle,
|
SheetTitle,
|
||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
} from "@/components/ui/sheet";
|
} from "@/components/ui/sheet";
|
||||||
import {
|
import { Layers3, Plus } from "lucide-react";
|
||||||
CheckCircle,
|
|
||||||
XCircle,
|
|
||||||
Loader2,
|
|
||||||
GitBranch,
|
|
||||||
Layers3,
|
|
||||||
Plus,
|
|
||||||
Bug,
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { ThreadMetadata } from "./types";
|
import { ThreadMetadata } from "./types";
|
||||||
import { ThreadCard } from "./thread-card";
|
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 {
|
interface ThreadSwitcherProps {
|
||||||
currentThread: ThreadMetadata;
|
currentThread: ThreadMetadata;
|
||||||
allThreads: ThreadMetadata[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ThreadSwitcher({
|
export function ThreadSwitcher({ currentThread }: ThreadSwitcherProps) {
|
||||||
currentThread,
|
|
||||||
allThreads,
|
|
||||||
}: ThreadSwitcherProps) {
|
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const router = useRouter();
|
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 (
|
return (
|
||||||
<Sheet
|
<Sheet
|
||||||
|
|
@ -86,7 +83,26 @@ export function ThreadSwitcher({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Other Threads */}
|
{/* 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">
|
<div className="h-full space-y-2">
|
||||||
<h3 className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
|
<h3 className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
|
||||||
Other Threads
|
Other Threads
|
||||||
|
|
@ -102,6 +118,17 @@ export function ThreadSwitcher({
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,6 @@ import { TokenUsage } from "./token-usage";
|
||||||
interface ThreadViewProps {
|
interface ThreadViewProps {
|
||||||
stream: ReturnType<typeof useStream<ManagerGraphState>>;
|
stream: ReturnType<typeof useStream<ManagerGraphState>>;
|
||||||
displayThread: ThreadMetadata;
|
displayThread: ThreadMetadata;
|
||||||
allDisplayThreads: ThreadMetadata[];
|
|
||||||
onBackToHome: () => void;
|
onBackToHome: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -87,7 +86,6 @@ const joinTokenData = (
|
||||||
export function ThreadView({
|
export function ThreadView({
|
||||||
stream,
|
stream,
|
||||||
displayThread,
|
displayThread,
|
||||||
allDisplayThreads,
|
|
||||||
onBackToHome,
|
onBackToHome,
|
||||||
}: ThreadViewProps) {
|
}: ThreadViewProps) {
|
||||||
const [chatInput, setChatInput] = useState("");
|
const [chatInput, setChatInput] = useState("");
|
||||||
|
|
@ -312,10 +310,7 @@ export function ThreadView({
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<ThreadSwitcher
|
<ThreadSwitcher currentThread={displayThread} />
|
||||||
currentThread={displayThread}
|
|
||||||
allThreads={allDisplayThreads}
|
|
||||||
/>
|
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue