From c70da1b1c27d7b12aa55e5f2c7b027ac02415dcb Mon Sep 17 00:00:00 2001 From: Brace Sproul Date: Fri, 1 Aug 2025 13:04:04 -0700 Subject: [PATCH] fix: Threads pagination (#642) * fix: Threads pagination * cr --- apps/web/src/app/(v2)/chat/threads/page.tsx | 55 +++++++++++----- apps/web/src/hooks/useThreadsSWR.ts | 71 +++++++++++++++++++-- 2 files changed, 106 insertions(+), 20 deletions(-) diff --git a/apps/web/src/app/(v2)/chat/threads/page.tsx b/apps/web/src/app/(v2)/chat/threads/page.tsx index 4079194b..ebc35891 100644 --- a/apps/web/src/app/(v2)/chat/threads/page.tsx +++ b/apps/web/src/app/(v2)/chat/threads/page.tsx @@ -31,10 +31,22 @@ type FilterStatus = function AllThreadsPageContent() { const router = useRouter(); + const limit = 25; + const [offset, setOffset] = useState(0); const { currentInstallation, installationsLoading } = useGitHubAppProvider(); - const { threads, isLoading: threadsLoading } = useThreadsSWR({ + const { + threads, + isLoading: threadsLoading, + hasMore, + } = useThreadsSWR({ assistantId: MANAGER_GRAPH_ID, currentInstallation, + pagination: { + limit, + offset, + sortBy: "updated_at", + sortOrder: "desc", + }, }); const [searchQuery, setSearchQuery] = useState(""); const [statusFilter, setStatusFilter] = useState("all"); @@ -96,6 +108,11 @@ function AllThreadsPageContent() { (statusFilter === "all" ? Object.values(groupedThreads).flat().length === 0 : filteredThreads.length === 0); + const showNoThreads = + filteredThreads.length === 0 && + !threadsLoading && + !statusLoading && + !showThreadsLoading; return (
@@ -233,22 +250,18 @@ function AllThreadsPageContent() {
)} - {filteredThreads.length === 0 && - !threadsLoading && - !statusLoading && ( -
-
- No threads found -
-
- {!threads || threads.length === 0 - ? "No threads have been created yet" - : searchQuery - ? "Try adjusting your search query" - : "No threads match the selected filter"} -
+ {showNoThreads && ( +
+
No threads found
+
+ {!threads || threads.length === 0 + ? "No threads have been created yet" + : searchQuery + ? "Try adjusting your search query" + : "No threads match the selected filter"}
- )} +
+ )} {showThreadsLoading && (
@@ -264,6 +277,16 @@ function AllThreadsPageContent() {
)} + {!showNoThreads && hasMore && ( +
+ +
+ )} diff --git a/apps/web/src/hooks/useThreadsSWR.ts b/apps/web/src/hooks/useThreadsSWR.ts index 3c723ee7..0b7fe2a0 100644 --- a/apps/web/src/hooks/useThreadsSWR.ts +++ b/apps/web/src/hooks/useThreadsSWR.ts @@ -6,9 +6,11 @@ import { ManagerGraphState } from "@open-swe/shared/open-swe/manager/types"; import { PlannerGraphState } from "@open-swe/shared/open-swe/planner/types"; import { ReviewerGraphState } from "@open-swe/shared/open-swe/reviewer/types"; import { GraphState } from "@open-swe/shared/open-swe/types"; -import { useMemo } from "react"; +import { useMemo, useState } from "react"; import type { Installation } from "./useGitHubInstallations"; +type ThreadSortBy = "thread_id" | "status" | "created_at" | "updated_at"; +type SortOrder = "asc" | "desc"; /** * Union type representing all possible graph states in the Open SWE system */ @@ -25,6 +27,32 @@ interface UseThreadsSWROptions { revalidateOnReconnect?: boolean; currentInstallation?: Installation | null; disableOrgFiltering?: boolean; + /** + * Pagination options + */ + pagination?: { + /** + * Maximum number of threads to return. + * @default 25 + */ + limit?: number; + /** + * Offset to start from. + * @default 0 + */ + offset?: number; + /** + * Sort by. + * @default "updated_at" + */ + sortBy?: ThreadSortBy; + /** + * Sort order. + * Must be one of 'asc' or 'desc'. + * @default "desc" + */ + sortOrder?: SortOrder; + }; } /** @@ -45,12 +73,34 @@ export function useThreadsSWR< revalidateOnReconnect = THREAD_SWR_CONFIG.revalidateOnReconnect, currentInstallation, disableOrgFiltering, + pagination, } = options; + const [hasMoreState, setHasMoreState] = useState(true); + + const paginationWithDefaults = { + limit: 25, + offset: 0, + sortBy: "updated_at" as ThreadSortBy, + sortOrder: "desc" as SortOrder, + ...pagination, + }; 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"]; + // Create a unique key for SWR caching based on assistantId and pagination parameters + const swrKey = useMemo(() => { + const baseKey = assistantId ? ["threads", assistantId] : ["threads", "all"]; + if (pagination) { + return [ + ...baseKey, + paginationWithDefaults.limit, + paginationWithDefaults.offset, + paginationWithDefaults.sortBy, + paginationWithDefaults.sortOrder, + ]; + } + return baseKey; + }, [assistantId, paginationWithDefaults]); const fetcher = async (): Promise[]> => { if (!apiUrl) { @@ -63,8 +113,11 @@ export function useThreadsSWR< metadata: { graph_id: assistantId, }, + ...(paginationWithDefaults ? paginationWithDefaults : {}), } - : undefined; + : paginationWithDefaults + ? paginationWithDefaults + : undefined; return await client.threads.search(searchArgs); }; @@ -89,7 +142,12 @@ export function useThreadsSWR< return allThreads; } + if (!allThreads.length) { + setHasMoreState(false); + } + if (!currentInstallation) { + setHasMoreState(false); return []; } @@ -102,11 +160,16 @@ export function useThreadsSWR< }); }, [data, currentInstallation, disableOrgFiltering]); + const hasMore = useMemo(() => { + return hasMoreState && !!threads.length; + }, [threads, paginationWithDefaults]); + return { threads, error, isLoading, isValidating, mutate, + hasMore, }; }