perf: added pagination for session in order to improve large project's performance issue

This commit is contained in:
d-kimsuon
2025-10-15 02:25:26 +09:00
parent 0259e71b44
commit d322db543c
20 changed files with 316 additions and 192 deletions

View File

@@ -25,24 +25,30 @@ import {
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { projectDetailQuery } from "../../../../lib/api/queries";
import { useConfig } from "../../../hooks/useConfig";
import { useProject } from "../hooks/useProject";
import { firstCommandToTitle } from "../services/firstCommandToTitle";
import { NewChatModal } from "./newChat/NewChatModal";
export const ProjectPageContent = ({ projectId }: { projectId: string }) => {
const {
data: { project, sessions },
} = useProject(projectId);
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
useProject(projectId);
const { config } = useConfig();
const queryClient = useQueryClient();
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
// Flatten all pages to get project and sessions
const project = data.pages.at(0)?.project;
const sessions = data.pages.flatMap((page) => page.sessions);
if (!project) {
throw new Error("Unreachable: Project must be defined.");
}
// biome-ignore lint/correctness/useExhaustiveDependencies: invalidate when config changed
useEffect(() => {
void queryClient.invalidateQueries({
queryKey: projectDetailQuery(projectId).queryKey,
queryKey: ["projects", projectId],
});
}, [config.hideNoUserMessageSession, config.unifySameTitleSession]);
@@ -170,10 +176,8 @@ export const ProjectPageContent = ({ projectId }: { projectId: string }) => {
</p>
<p className="text-sm text-muted-foreground">
Last modified:{" "}
{session.meta.lastModifiedAt
? new Date(
session.meta.lastModifiedAt,
).toLocaleDateString()
{session.lastModifiedAt
? new Date(session.lastModifiedAt).toLocaleDateString()
: ""}
</p>
<p className="text-xs text-muted-foreground font-mono">
@@ -195,6 +199,21 @@ export const ProjectPageContent = ({ projectId }: { projectId: string }) => {
))}
</div>
)}
{/* Load More Button */}
{sessions.length > 0 && hasNextPage && (
<div className="mt-6 flex justify-center">
<Button
onClick={() => fetchNextPage()}
disabled={isFetchingNextPage}
variant="outline"
size="lg"
className="min-w-[200px]"
>
{isFetchingNextPage ? "Loading..." : "Load More"}
</Button>
</div>
)}
</section>
</main>
</div>

View File

@@ -1,10 +1,14 @@
import { useSuspenseQuery } from "@tanstack/react-query";
import { useSuspenseInfiniteQuery } from "@tanstack/react-query";
import { projectDetailQuery } from "../../../../lib/api/queries";
export const useProject = (projectId: string) => {
return useSuspenseQuery({
queryKey: projectDetailQuery(projectId).queryKey,
queryFn: projectDetailQuery(projectId).queryFn,
return useSuspenseInfiniteQuery({
queryKey: ["projects", projectId],
queryFn: async ({ pageParam }) => {
return await projectDetailQuery(projectId, pageParam).queryFn();
},
initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor,
refetchOnReconnect: true,
});
};

View File

@@ -15,9 +15,12 @@ export default async function ProjectPage({ params }: ProjectPageProps) {
const queryClient = new QueryClient();
await queryClient.prefetchQuery({
queryKey: projectDetailQuery(projectId).queryKey,
queryFn: projectDetailQuery(projectId).queryFn,
await queryClient.prefetchInfiniteQuery({
queryKey: ["projects", projectId],
queryFn: async ({ pageParam }) => {
return await projectDetailQuery(projectId, pageParam).queryFn();
},
initialPageParam: undefined as string | undefined,
});
return (

View File

@@ -35,7 +35,9 @@ export const SessionPageContent: FC<{
projectId,
sessionId,
);
const { data: project } = useProject(projectId);
const { data: projectData } = useProject(projectId);
// biome-ignore lint/style/noNonNullAssertion: useSuspenseInfiniteQuery guarantees at least one page
const project = projectData.pages[0]!.project;
const abortTask = useMutation({
mutationFn: async (sessionId: string) => {
@@ -111,7 +113,7 @@ export const SessionPageContent: FC<{
</div>
<div className="px-1 sm:px-5 flex flex-wrap items-center gap-1 sm:gap-2">
{project?.project.claudeProjectPath && (
{project?.claudeProjectPath && (
<Link
href={`/projects/${projectId}`}
target="_blank"
@@ -122,8 +124,7 @@ export const SessionPageContent: FC<{
className="h-6 sm:h-8 text-xs sm:text-sm flex items-center hover:bg-blue-50/60 hover:border-blue-300/60 hover:shadow-sm transition-all duration-200 cursor-pointer"
>
<ExternalLinkIcon className="w-3 h-3 sm:w-4 sm:h-4 mr-1" />
{project.project.meta.projectPath ??
project.project.claudeProjectPath}
{project.meta.projectPath ?? project.claudeProjectPath}
</Badge>
</Link>
)}

View File

@@ -24,8 +24,12 @@ export const MobileSidebar: FC<MobileSidebarProps> = ({
onClose,
}) => {
const {
data: { sessions },
data: projectData,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useProject(projectId);
const sessions = projectData.pages.flatMap((page) => page.sessions);
const [activeTab, setActiveTab] = useState<"sessions" | "mcp" | "settings">(
"sessions",
);
@@ -71,9 +75,15 @@ export const MobileSidebar: FC<MobileSidebarProps> = ({
case "sessions":
return (
<SessionsTab
sessions={sessions}
sessions={sessions.map((session) => ({
...session,
lastModifiedAt: new Date(session.lastModifiedAt),
}))}
currentSessionId={currentSessionId}
projectId={projectId}
hasNextPage={hasNextPage}
isFetchingNextPage={isFetchingNextPage}
onLoadMore={() => fetchNextPage()}
/>
);
case "mcp":

View File

@@ -30,8 +30,12 @@ export const SessionSidebar: FC<{
}) => {
const router = useRouter();
const {
data: { sessions },
data: projectData,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useProject(projectId);
const sessions = projectData.pages.flatMap((page) => page.sessions);
const [activeTab, setActiveTab] = useState<"sessions" | "mcp" | "settings">(
"sessions",
);
@@ -53,9 +57,15 @@ export const SessionSidebar: FC<{
case "sessions":
return (
<SessionsTab
sessions={sessions}
sessions={sessions.map((session) => ({
...session,
lastModifiedAt: new Date(session.lastModifiedAt),
}))}
currentSessionId={currentSessionId}
projectId={projectId}
hasNextPage={hasNextPage}
isFetchingNextPage={isFetchingNextPage}
onLoadMore={() => fetchNextPage()}
/>
);
case "mcp":

View File

@@ -16,7 +16,17 @@ export const SessionsTab: FC<{
sessions: Session[];
currentSessionId: string;
projectId: string;
}> = ({ sessions, currentSessionId, projectId }) => {
hasNextPage?: boolean;
isFetchingNextPage?: boolean;
onLoadMore?: () => void;
}> = ({
sessions,
currentSessionId,
projectId,
hasNextPage,
isFetchingNextPage,
onLoadMore,
}) => {
const aliveTasks = useAtomValue(aliveTasksAtom);
// Sort sessions: Running > Paused > Others, then by lastModifiedAt (newest first)
@@ -43,12 +53,8 @@ export const SessionsTab: FC<{
}
// Then sort by lastModifiedAt (newest first)
const aTime = a.meta.lastModifiedAt
? new Date(a.meta.lastModifiedAt).getTime()
: 0;
const bTime = b.meta.lastModifiedAt
? new Date(b.meta.lastModifiedAt).getTime()
: 0;
const aTime = a.lastModifiedAt ? a.lastModifiedAt.getTime() : 0;
const bTime = b.lastModifiedAt ? b.lastModifiedAt.getTime() : 0;
return bTime - aTime;
});
@@ -121,9 +127,9 @@ export const SessionsTab: FC<{
<MessageSquareIcon className="w-3 h-3" />
<span>{session.meta.messageCount}</span>
</div>
{session.meta.lastModifiedAt && (
{session.lastModifiedAt && (
<span className="text-xs text-sidebar-foreground/60">
{new Date(session.meta.lastModifiedAt).toLocaleDateString(
{new Date(session.lastModifiedAt).toLocaleDateString(
"en-US",
{
month: "short",
@@ -137,6 +143,21 @@ export const SessionsTab: FC<{
</Link>
);
})}
{/* Load More Button */}
{hasNextPage && onLoadMore && (
<div className="p-2">
<Button
onClick={onLoadMore}
disabled={isFetchingNextPage}
variant="outline"
size="sm"
className="w-full"
>
{isFetchingNextPage ? "Loading..." : "Load More"}
</Button>
</div>
)}
</div>
</div>
);

View File

@@ -49,8 +49,8 @@ export const ProjectList: FC = () => {
<CardContent className="space-y-2">
<p className="text-sm text-muted-foreground">
Last modified:{" "}
{project.meta.lastModifiedAt
? new Date(project.meta.lastModifiedAt).toLocaleDateString()
{project.lastModifiedAt
? new Date(project.lastModifiedAt).toLocaleDateString()
: ""}
</p>
<p className="text-xs text-muted-foreground">