fix: Threads pagination (#642)

* fix: Threads pagination

* cr
This commit is contained in:
Brace Sproul 2025-08-01 13:04:04 -07:00 • committed by GitHub
parent 46b435a543
commit c70da1b1c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 106 additions and 20 deletions

View file

@ -31,10 +31,22 @@ type FilterStatus =
function AllThreadsPageContent() { function AllThreadsPageContent() {
const router = useRouter(); const router = useRouter();
const limit = 25;
const [offset, setOffset] = useState(0);
const { currentInstallation, installationsLoading } = useGitHubAppProvider(); const { currentInstallation, installationsLoading } = useGitHubAppProvider();
const { threads, isLoading: threadsLoading } = useThreadsSWR({ const {
threads,
isLoading: threadsLoading,
hasMore,
} = useThreadsSWR({
assistantId: MANAGER_GRAPH_ID, assistantId: MANAGER_GRAPH_ID,
currentInstallation, currentInstallation,
pagination: {
limit,
offset,
sortBy: "updated_at",
sortOrder: "desc",
},
}); });
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [statusFilter, setStatusFilter] = useState<FilterStatus>("all"); const [statusFilter, setStatusFilter] = useState<FilterStatus>("all");
@ -96,6 +108,11 @@ function AllThreadsPageContent() {
(statusFilter === "all" (statusFilter === "all"
? Object.values(groupedThreads).flat().length === 0 ? Object.values(groupedThreads).flat().length === 0
: filteredThreads.length === 0); : filteredThreads.length === 0);
const showNoThreads =
filteredThreads.length === 0 &&
!threadsLoading &&
!statusLoading &&
!showThreadsLoading;
return ( return (
<div className="bg-background flex h-screen flex-col"> <div className="bg-background flex h-screen flex-col">
@ -233,22 +250,18 @@ function AllThreadsPageContent() {
</div> </div>
)} )}
{filteredThreads.length === 0 && {showNoThreads && (
!threadsLoading && <div className="py-12 text-center">
!statusLoading && ( <div className="text-muted-foreground mb-2">No threads found</div>
<div className="py-12 text-center"> <div className="text-muted-foreground/70 text-xs">
<div className="text-muted-foreground mb-2"> {!threads || threads.length === 0
No threads found ? "No threads have been created yet"
</div> : searchQuery
<div className="text-muted-foreground/70 text-xs"> ? "Try adjusting your search query"
{!threads || threads.length === 0 : "No threads match the selected filter"}
? "No threads have been created yet"
: searchQuery
? "Try adjusting your search query"
: "No threads match the selected filter"}
</div>
</div> </div>
)} </div>
)}
{showThreadsLoading && ( {showThreadsLoading && (
<div> <div>
@ -264,6 +277,16 @@ function AllThreadsPageContent() {
</div> </div>
</div> </div>
)} )}
{!showNoThreads && hasMore && (
<div className="flex items-center justify-center">
<Button
variant="outline"
onClick={() => setOffset((prev) => prev + limit)}
>
Load more
</Button>
</div>
)}
</div> </div>
</div> </div>
</div> </div>

View file

@ -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 { PlannerGraphState } from "@open-swe/shared/open-swe/planner/types";
import { ReviewerGraphState } from "@open-swe/shared/open-swe/reviewer/types"; import { ReviewerGraphState } from "@open-swe/shared/open-swe/reviewer/types";
import { GraphState } from "@open-swe/shared/open-swe/types"; import { GraphState } from "@open-swe/shared/open-swe/types";
import { useMemo } from "react"; import { useMemo, useState } from "react";
import type { Installation } from "./useGitHubInstallations"; 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 * Union type representing all possible graph states in the Open SWE system
*/ */
@ -25,6 +27,32 @@ interface UseThreadsSWROptions {
revalidateOnReconnect?: boolean; revalidateOnReconnect?: boolean;
currentInstallation?: Installation | null; currentInstallation?: Installation | null;
disableOrgFiltering?: boolean; 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, revalidateOnReconnect = THREAD_SWR_CONFIG.revalidateOnReconnect,
currentInstallation, currentInstallation,
disableOrgFiltering, disableOrgFiltering,
pagination,
} = options; } = 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 ?? ""; const apiUrl: string | undefined = process.env.NEXT_PUBLIC_API_URL ?? "";
// Create a unique key for SWR caching based on assistantId // Create a unique key for SWR caching based on assistantId and pagination parameters
const swrKey = assistantId ? ["threads", assistantId] : ["threads", "all"]; 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<Thread<TGraphState>[]> => { const fetcher = async (): Promise<Thread<TGraphState>[]> => {
if (!apiUrl) { if (!apiUrl) {
@ -63,8 +113,11 @@ export function useThreadsSWR<
metadata: { metadata: {
graph_id: assistantId, graph_id: assistantId,
}, },
...(paginationWithDefaults ? paginationWithDefaults : {}),
} }
: undefined; : paginationWithDefaults
? paginationWithDefaults
: undefined;
return await client.threads.search<TGraphState>(searchArgs); return await client.threads.search<TGraphState>(searchArgs);
}; };
@ -89,7 +142,12 @@ export function useThreadsSWR<
return allThreads; return allThreads;
} }
if (!allThreads.length) {
setHasMoreState(false);
}
if (!currentInstallation) { if (!currentInstallation) {
setHasMoreState(false);
return []; return [];
} }
@ -102,11 +160,16 @@ export function useThreadsSWR<
}); });
}, [data, currentInstallation, disableOrgFiltering]); }, [data, currentInstallation, disableOrgFiltering]);
const hasMore = useMemo(() => {
return hasMoreState && !!threads.length;
}, [threads, paginationWithDefaults]);
return { return {
threads, threads,
error, error,
isLoading, isLoading,
isValidating, isValidating,
mutate, mutate,
hasMore,
}; };
} }