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 <braceasproul@gmail.com>
This commit is contained in:
Dylan Boudro 2025-07-08 08:08:42 -07:00 • committed by GitHub
parent 4793ee3d3a
commit 6f7c5e1111
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 92 additions and 6 deletions

View file

@ -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",

View file

@ -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<GraphState>(MANAGER_GRAPH_ID);
const { threads, isLoading: threadsLoading } = useThreadsSWR<GraphState>({
assistantId: MANAGER_GRAPH_ID,
refreshInterval: 15000, // Poll every 15 seconds
});
if (!threads) {
return <div>No threads</div>;
}
// Convert Thread objects to ThreadDisplayInfo for UI
const displayThreads: ThreadDisplayInfo[] = threads.map(threadToDisplayInfo);

View file

@ -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<GraphState>(MANAGER_GRAPH_ID);
const { threads, isLoading: threadsLoading } = useThreadsSWR<GraphState>({
assistantId: MANAGER_GRAPH_ID,
refreshInterval: 15000, // Poll every 15 seconds
});
const [searchQuery, setSearchQuery] = useState("");
const [statusFilter, setStatusFilter] = useState<FilterStatus>("all");

View file

@ -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<State extends Record<string, any>>(
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<Thread<State>[]> => {
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<State>(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
};
}

View file

@ -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: