From 6f7c5e1111d4a85eff2a8fffd6d8eb95c0a38f14 Mon Sep 17 00:00:00 2001 From: Dylan Boudro <121908331+starmorph@users.noreply.github.com> Date: Tue, 8 Jul 2025 08:08:42 -0700 Subject: [PATCH] Feat: Polling /chat/threads (#337) * Thread polling on /chat/page (recent) * polling chat/threads using SWR polling hook * reinstall swr in web/ package.json --------- Co-authored-by: Brace Sproul --- apps/web/package.json | 1 + apps/web/src/app/(v2)/chat/page.tsx | 10 +++- apps/web/src/app/(v2)/chat/threads/page.tsx | 7 ++- apps/web/src/hooks/useThreadsSWR.ts | 63 +++++++++++++++++++++ yarn.lock | 17 +++++- 5 files changed, 92 insertions(+), 6 deletions(-) create mode 100644 apps/web/src/hooks/useThreadsSWR.ts diff --git a/apps/web/package.json b/apps/web/package.json index 8d052f5b..d4b839ab 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -62,6 +62,7 @@ "remark-gfm": "^4.0.1", "remark-math": "^6.0.0", "sonner": "^2.0.1", + "swr": "^2.3.4", "tailwind-merge": "^3.0.2", "tailwindcss-animate": "^1.0.7", "use-stick-to-bottom": "^1.0.46", diff --git a/apps/web/src/app/(v2)/chat/page.tsx b/apps/web/src/app/(v2)/chat/page.tsx index c7b6fe85..80a77b89 100644 --- a/apps/web/src/app/(v2)/chat/page.tsx +++ b/apps/web/src/app/(v2)/chat/page.tsx @@ -2,7 +2,7 @@ import { DefaultView } from "@/components/v2/default-view"; import { ThreadDisplayInfo, threadToDisplayInfo } from "@/components/v2/types"; -import { useThreads } from "@/hooks/useThreads"; +import { useThreadsSWR } from "@/hooks/useThreadsSWR"; import { GitHubAppProvider } from "@/providers/GitHubApp"; import { GraphState } from "@open-swe/shared/open-swe/types"; import { Toaster } from "@/components/ui/sonner"; @@ -10,7 +10,13 @@ import { Suspense } from "react"; import { MANAGER_GRAPH_ID } from "@open-swe/shared/constants"; export default function ChatPage() { - const { threads, threadsLoading } = useThreads(MANAGER_GRAPH_ID); + const { threads, isLoading: threadsLoading } = useThreadsSWR({ + assistantId: MANAGER_GRAPH_ID, + refreshInterval: 15000, // Poll every 15 seconds + }); + if (!threads) { + return
No threads
; + } // Convert Thread objects to ThreadDisplayInfo for UI const displayThreads: ThreadDisplayInfo[] = threads.map(threadToDisplayInfo); diff --git a/apps/web/src/app/(v2)/chat/threads/page.tsx b/apps/web/src/app/(v2)/chat/threads/page.tsx index fef96ea0..8fa03340 100644 --- a/apps/web/src/app/(v2)/chat/threads/page.tsx +++ b/apps/web/src/app/(v2)/chat/threads/page.tsx @@ -8,7 +8,7 @@ import { Input } from "@/components/ui/input"; import { ArrowLeft, Search, Filter } from "lucide-react"; import { useRouter } from "next/navigation"; import { ThreadDisplayInfo, threadToDisplayInfo } from "@/components/v2/types"; -import { useThreads } from "@/hooks/useThreads"; +import { useThreadsSWR } from "@/hooks/useThreadsSWR"; import { GraphState } from "@open-swe/shared/open-swe/types"; import { ThreadCard, ThreadCardLoading } from "@/components/v2/thread-card"; import { ThemeToggle } from "@/components/theme-toggle"; @@ -18,7 +18,10 @@ type FilterStatus = "all" | "running" | "completed" | "failed" | "pending"; export default function AllThreadsPage() { const router = useRouter(); - const { threads, threadsLoading } = useThreads(MANAGER_GRAPH_ID); + const { threads, isLoading: threadsLoading } = useThreadsSWR({ + assistantId: MANAGER_GRAPH_ID, + refreshInterval: 15000, // Poll every 15 seconds + }); const [searchQuery, setSearchQuery] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); diff --git a/apps/web/src/hooks/useThreadsSWR.ts b/apps/web/src/hooks/useThreadsSWR.ts new file mode 100644 index 00000000..66fdadea --- /dev/null +++ b/apps/web/src/hooks/useThreadsSWR.ts @@ -0,0 +1,63 @@ +import { createClient } from "@/providers/client"; +import { Thread } from "@langchain/langgraph-sdk"; +import useSWR from "swr"; + +export interface UseThreadsSWROptions { + assistantId?: string; + refreshInterval?: number; + revalidateOnFocus?: boolean; + revalidateOnReconnect?: boolean; +} + +export function useThreadsSWR>( + options: UseThreadsSWROptions = {}, +) { + const { + assistantId, + refreshInterval = 0, // Default to no polling, can be overridden + revalidateOnFocus = true, + revalidateOnReconnect = true, + } = options; + + const apiUrl: string | undefined = process.env.NEXT_PUBLIC_API_URL ?? ""; + + // Create a unique key for SWR caching based on assistantId + const swrKey = assistantId ? ["threads", assistantId] : ["threads", "all"]; + + const fetcher = async (): Promise[]> => { + if (!apiUrl) { + throw new Error("API URL is not configured"); + } + + const client = createClient(apiUrl); + const searchArgs = assistantId + ? { + metadata: { + graph_id: assistantId, + }, + } + : undefined; + + return await client.threads.search(searchArgs); + }; + + const { data, error, isLoading, mutate, isValidating } = useSWR( + swrKey, + fetcher, + { + refreshInterval, + revalidateOnFocus, + revalidateOnReconnect, + errorRetryCount: 3, + errorRetryInterval: 5000, + }, + ); + + return { + threads: data ?? [], + error, + isLoading, + isValidating, + mutate, // For manual revalidation + }; +} diff --git a/yarn.lock b/yarn.lock index 8fa13fb1..94508673 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2539,6 +2539,7 @@ __metadata: remark-math: ^6.0.0 shadcn: ^2.6.1 sonner: ^2.0.1 + swr: ^2.3.4 tailwind-merge: ^3.0.2 tailwind-scrollbar: ^4.0.1 tailwindcss: ^4.0.13 @@ -6097,7 +6098,7 @@ __metadata: languageName: node linkType: hard -"dequal@npm:^2.0.0": +"dequal@npm:^2.0.0, dequal@npm:^2.0.3": version: 2.0.3 resolution: "dequal@npm:2.0.3" checksum: 8679b850e1a3d0ebbc46ee780d5df7b478c23f335887464023a631d1b9af051ad4a6595a44220f9ff8ff95a8ddccf019b5ad778a976fd7bbf77383d36f412f90 @@ -13437,6 +13438,18 @@ __metadata: languageName: node linkType: hard +"swr@npm:^2.3.4": + version: 2.3.4 + resolution: "swr@npm:2.3.4" + dependencies: + dequal: ^2.0.3 + use-sync-external-store: ^1.4.0 + peerDependencies: + react: ^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 18172b2215d8fa883b4845a094637e5aafb847b3ab96fb950866f0e50a9ada59450c8bcf9dc930e30e6fa40b986dbee45684c96337685b6d6f4d68006bd26b2a + languageName: node + linkType: hard + "tailwind-merge@npm:^3.0.2": version: 3.3.0 resolution: "tailwind-merge@npm:3.3.0" @@ -14246,7 +14259,7 @@ __metadata: languageName: node linkType: hard -"use-sync-external-store@npm:^1.5.0": +"use-sync-external-store@npm:^1.4.0, use-sync-external-store@npm:^1.5.0": version: 1.5.0 resolution: "use-sync-external-store@npm:1.5.0" peerDependencies: