mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-04 20:05:38 +08:00
refactor: implement pagination for project sessions loading
This commit is contained in:
@@ -70,6 +70,7 @@ type UseSidebarControllerArgs = {
|
||||
onProjectSelect: (project: Project) => void;
|
||||
onSessionSelect: (session: ProjectSession) => void;
|
||||
onSessionDelete?: (sessionId: string) => void;
|
||||
onLoadMoreSessions?: (projectId: string) => Promise<void> | void;
|
||||
// `projectId` is the DB-assigned identifier; callbacks use that post-migration.
|
||||
onProjectDelete?: (projectId: string) => void;
|
||||
setCurrentProject: (project: Project) => void;
|
||||
@@ -88,6 +89,7 @@ export function useSidebarController({
|
||||
onProjectSelect,
|
||||
onSessionSelect,
|
||||
onSessionDelete,
|
||||
onLoadMoreSessions,
|
||||
onProjectDelete,
|
||||
setCurrentProject,
|
||||
setSidebarVisible,
|
||||
@@ -113,6 +115,7 @@ export function useSidebarController({
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [searchProgress, setSearchProgress] = useState<SearchProgress | null>(null);
|
||||
const [optimisticStarByProjectId, setOptimisticStarByProjectId] = useState<Map<string, boolean>>(new Map());
|
||||
const [loadingMoreProjects, setLoadingMoreProjects] = useState<Set<string>>(new Set());
|
||||
const searchTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const searchSeqRef = useRef(0);
|
||||
const eventSourceRef = useRef<EventSource | null>(null);
|
||||
@@ -436,6 +439,31 @@ export function useSidebarController({
|
||||
|
||||
const getProjectSessions = useCallback((project: Project) => getAllSessions(project), []);
|
||||
|
||||
const loadMoreSessionsForProject = useCallback(async (projectId: string) => {
|
||||
if (!onLoadMoreSessions || loadingMoreProjects.has(projectId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingMoreProjects((previous) => {
|
||||
const next = new Set(previous);
|
||||
next.add(projectId);
|
||||
return next;
|
||||
});
|
||||
|
||||
try {
|
||||
await onLoadMoreSessions(projectId);
|
||||
} catch (error) {
|
||||
console.error('[Sidebar] Failed to load more sessions:', error);
|
||||
alert(t('messages.refreshError'));
|
||||
} finally {
|
||||
setLoadingMoreProjects((previous) => {
|
||||
const next = new Set(previous);
|
||||
next.delete(projectId);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
}, [loadingMoreProjects, onLoadMoreSessions, t]);
|
||||
|
||||
const projectsWithResolvedStarState = useMemo(() => {
|
||||
if (optimisticStarByProjectId.size === 0) {
|
||||
return projects;
|
||||
@@ -661,6 +689,7 @@ export function useSidebarController({
|
||||
editingSessionName,
|
||||
searchFilter,
|
||||
deletingProjects,
|
||||
loadingMoreProjects,
|
||||
deleteConfirmation,
|
||||
sessionDeleteConfirmation,
|
||||
showVersionModal,
|
||||
@@ -670,6 +699,7 @@ export function useSidebarController({
|
||||
toggleStarProject,
|
||||
isProjectStarred,
|
||||
getProjectSessions,
|
||||
loadMoreSessionsForProject,
|
||||
startEditing,
|
||||
cancelEditing,
|
||||
saveProjectName,
|
||||
|
||||
@@ -28,6 +28,7 @@ export type SidebarProps = {
|
||||
onSessionSelect: (session: ProjectSession) => void;
|
||||
onNewSession: (project: Project) => void;
|
||||
onSessionDelete?: (sessionId: string) => void;
|
||||
onLoadMoreSessions?: (projectId: string) => Promise<void> | void;
|
||||
// `projectId` is the DB identifier; the sidebar hands it back to the parent
|
||||
// when the delete flow completes.
|
||||
onProjectDelete?: (projectId: string) => void;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { useDeviceSettings } from '../../../hooks/useDeviceSettings';
|
||||
import { useVersionCheck } from '../../../hooks/useVersionCheck';
|
||||
import { useUiPreferences } from '../../../hooks/useUiPreferences';
|
||||
@@ -8,6 +9,7 @@ import { useTaskMaster } from '../../../contexts/TaskMasterContext';
|
||||
import { useTasksSettings } from '../../../contexts/TasksSettingsContext';
|
||||
import type { Project, LLMProvider } from '../../../types/app';
|
||||
import type { MCPServerStatus, SidebarProps } from '../types/types';
|
||||
|
||||
import SidebarCollapsed from './subcomponents/SidebarCollapsed';
|
||||
import SidebarContent from './subcomponents/SidebarContent';
|
||||
import SidebarModals from './subcomponents/SidebarModals';
|
||||
@@ -26,6 +28,7 @@ function Sidebar({
|
||||
onSessionSelect,
|
||||
onNewSession,
|
||||
onSessionDelete,
|
||||
onLoadMoreSessions,
|
||||
onProjectDelete,
|
||||
isLoading,
|
||||
loadingProgress,
|
||||
@@ -75,6 +78,8 @@ function Sidebar({
|
||||
toggleStarProject,
|
||||
isProjectStarred,
|
||||
getProjectSessions,
|
||||
loadingMoreProjects,
|
||||
loadMoreSessionsForProject,
|
||||
startEditing,
|
||||
cancelEditing,
|
||||
saveProjectName,
|
||||
@@ -106,6 +111,7 @@ function Sidebar({
|
||||
onProjectSelect,
|
||||
onSessionSelect,
|
||||
onSessionDelete,
|
||||
onLoadMoreSessions,
|
||||
onProjectDelete,
|
||||
setCurrentProject,
|
||||
setSidebarVisible: (visible) => setPreference('sidebarVisible', visible),
|
||||
@@ -148,6 +154,7 @@ function Sidebar({
|
||||
tasksEnabled,
|
||||
mcpServerStatus,
|
||||
getProjectSessions,
|
||||
loadingMoreProjects,
|
||||
isProjectStarred,
|
||||
onEditingNameChange: setEditingName,
|
||||
onToggleProject: toggleProject,
|
||||
@@ -161,6 +168,7 @@ function Sidebar({
|
||||
onDeleteProject: requestProjectDelete,
|
||||
onSessionSelect: handleSessionClick,
|
||||
onDeleteSession: showDeleteSessionConfirmation,
|
||||
onLoadMoreSessions: loadMoreSessionsForProject,
|
||||
onNewSession,
|
||||
onEditingSessionNameChange: setEditingSessionName,
|
||||
onStartEditingSession: (sessionId, initialName) => {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Check, ChevronDown, ChevronRight, Edit3, Folder, FolderOpen, Star, Trash2, X } from 'lucide-react';
|
||||
import type { TFunction } from 'i18next';
|
||||
|
||||
import { Button } from '../../../../shared/view/ui';
|
||||
import { cn } from '../../../../lib/utils';
|
||||
import type { Project, ProjectSession, LLMProvider } from '../../../../types/app';
|
||||
import type { MCPServerStatus, SessionWithProvider } from '../../types/types';
|
||||
import { getTaskIndicatorStatus } from '../../utils/utils';
|
||||
|
||||
import TaskIndicator from './TaskIndicator';
|
||||
import SidebarProjectSessions from './SidebarProjectSessions';
|
||||
|
||||
@@ -19,6 +21,7 @@ type SidebarProjectItemProps = {
|
||||
editingName: string;
|
||||
sessions: SessionWithProvider[];
|
||||
initialSessionsLoaded: boolean;
|
||||
isLoadingMoreSessions: boolean;
|
||||
currentTime: Date;
|
||||
editingSession: string | null;
|
||||
editingSessionName: string;
|
||||
@@ -39,6 +42,7 @@ type SidebarProjectItemProps = {
|
||||
sessionTitle: string,
|
||||
provider: LLMProvider,
|
||||
) => void;
|
||||
onLoadMoreSessions: (projectId: string) => void;
|
||||
onNewSession: (project: Project) => void;
|
||||
onEditingSessionNameChange: (value: string) => void;
|
||||
onStartEditingSession: (sessionId: string, initialName: string) => void;
|
||||
@@ -47,7 +51,10 @@ type SidebarProjectItemProps = {
|
||||
t: TFunction;
|
||||
};
|
||||
|
||||
const getSessionCountDisplay = (sessions: SessionWithProvider[]): string => String(sessions.length);
|
||||
const getSessionCountDisplay = (project: Project, sessions: SessionWithProvider[]): string => {
|
||||
const total = Number(project.sessionMeta?.total ?? sessions.length);
|
||||
return String(total);
|
||||
};
|
||||
|
||||
export default function SidebarProjectItem({
|
||||
project,
|
||||
@@ -60,6 +67,7 @@ export default function SidebarProjectItem({
|
||||
editingName,
|
||||
sessions,
|
||||
initialSessionsLoaded,
|
||||
isLoadingMoreSessions,
|
||||
currentTime,
|
||||
editingSession,
|
||||
editingSessionName,
|
||||
@@ -75,6 +83,7 @@ export default function SidebarProjectItem({
|
||||
onDeleteProject,
|
||||
onSessionSelect,
|
||||
onDeleteSession,
|
||||
onLoadMoreSessions,
|
||||
onNewSession,
|
||||
onEditingSessionNameChange,
|
||||
onStartEditingSession,
|
||||
@@ -86,8 +95,9 @@ export default function SidebarProjectItem({
|
||||
// after the projectName → projectId migration.
|
||||
const isSelected = selectedProject?.projectId === project.projectId;
|
||||
const isEditing = editingProject === project.projectId;
|
||||
const sessionCountDisplay = getSessionCountDisplay(sessions);
|
||||
const sessionCountLabel = `${sessionCountDisplay} session${sessions.length === 1 ? '' : 's'}`;
|
||||
const totalSessionCount = Number(project.sessionMeta?.total ?? sessions.length);
|
||||
const sessionCountDisplay = getSessionCountDisplay(project, sessions);
|
||||
const sessionCountLabel = `${sessionCountDisplay} session${totalSessionCount === 1 ? '' : 's'}`;
|
||||
const taskStatus = getTaskIndicatorStatus(project, mcpServerStatus);
|
||||
|
||||
const toggleProject = () => onToggleProject(project.projectId);
|
||||
@@ -399,6 +409,8 @@ export default function SidebarProjectItem({
|
||||
sessions={sessions}
|
||||
selectedSession={selectedSession}
|
||||
initialSessionsLoaded={initialSessionsLoaded}
|
||||
hasMoreSessions={Boolean(project.sessionMeta?.hasMore)}
|
||||
isLoadingMoreSessions={isLoadingMoreSessions}
|
||||
currentTime={currentTime}
|
||||
editingSession={editingSession}
|
||||
editingSessionName={editingSessionName}
|
||||
@@ -409,6 +421,7 @@ export default function SidebarProjectItem({
|
||||
onProjectSelect={onProjectSelect}
|
||||
onSessionSelect={onSessionSelect}
|
||||
onDeleteSession={onDeleteSession}
|
||||
onLoadMoreSessions={onLoadMoreSessions}
|
||||
onNewSession={onNewSession}
|
||||
t={t}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { useEffect } from 'react';
|
||||
import type { TFunction } from 'i18next';
|
||||
|
||||
import type { LoadingProgress, Project, ProjectSession, LLMProvider } from '../../../../types/app';
|
||||
import type { MCPServerStatus, SessionWithProvider } from '../../types/types';
|
||||
|
||||
import SidebarProjectItem from './SidebarProjectItem';
|
||||
import SidebarProjectsState from './SidebarProjectsState';
|
||||
|
||||
@@ -23,6 +25,8 @@ export type SidebarProjectListProps = {
|
||||
tasksEnabled: boolean;
|
||||
mcpServerStatus: MCPServerStatus;
|
||||
getProjectSessions: (project: Project) => SessionWithProvider[];
|
||||
onLoadMoreSessions: (projectId: string) => void;
|
||||
loadingMoreProjects: Set<string>;
|
||||
isProjectStarred: (projectName: string) => boolean;
|
||||
onEditingNameChange: (value: string) => void;
|
||||
onToggleProject: (projectName: string) => void;
|
||||
@@ -65,6 +69,8 @@ export default function SidebarProjectList({
|
||||
tasksEnabled,
|
||||
mcpServerStatus,
|
||||
getProjectSessions,
|
||||
onLoadMoreSessions,
|
||||
loadingMoreProjects,
|
||||
isProjectStarred,
|
||||
onEditingNameChange,
|
||||
onToggleProject,
|
||||
@@ -123,6 +129,7 @@ export default function SidebarProjectList({
|
||||
editingName={editingName}
|
||||
sessions={getProjectSessions(project)}
|
||||
initialSessionsLoaded={initialSessionsLoaded.has(project.projectId)}
|
||||
isLoadingMoreSessions={loadingMoreProjects.has(project.projectId)}
|
||||
currentTime={currentTime}
|
||||
editingSession={editingSession}
|
||||
editingSessionName={editingSessionName}
|
||||
@@ -138,6 +145,7 @@ export default function SidebarProjectList({
|
||||
onDeleteProject={onDeleteProject}
|
||||
onSessionSelect={onSessionSelect}
|
||||
onDeleteSession={onDeleteSession}
|
||||
onLoadMoreSessions={onLoadMoreSessions}
|
||||
onNewSession={onNewSession}
|
||||
onEditingSessionNameChange={onEditingSessionNameChange}
|
||||
onStartEditingSession={onStartEditingSession}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Plus } from 'lucide-react';
|
||||
import type { TFunction } from 'i18next';
|
||||
|
||||
import { Button } from '../../../../shared/view/ui';
|
||||
import type { Project, ProjectSession, LLMProvider } from '../../../../types/app';
|
||||
import type { SessionWithProvider } from '../../types/types';
|
||||
|
||||
import SidebarSessionItem from './SidebarSessionItem';
|
||||
|
||||
type SidebarProjectSessionsProps = {
|
||||
@@ -11,6 +13,8 @@ type SidebarProjectSessionsProps = {
|
||||
sessions: SessionWithProvider[];
|
||||
selectedSession: ProjectSession | null;
|
||||
initialSessionsLoaded: boolean;
|
||||
hasMoreSessions: boolean;
|
||||
isLoadingMoreSessions: boolean;
|
||||
currentTime: Date;
|
||||
editingSession: string | null;
|
||||
editingSessionName: string;
|
||||
@@ -26,6 +30,7 @@ type SidebarProjectSessionsProps = {
|
||||
sessionTitle: string,
|
||||
provider: LLMProvider,
|
||||
) => void;
|
||||
onLoadMoreSessions: (projectId: string) => void;
|
||||
onNewSession: (project: Project) => void;
|
||||
t: TFunction;
|
||||
};
|
||||
@@ -54,6 +59,8 @@ export default function SidebarProjectSessions({
|
||||
sessions,
|
||||
selectedSession,
|
||||
initialSessionsLoaded,
|
||||
hasMoreSessions,
|
||||
isLoadingMoreSessions,
|
||||
currentTime,
|
||||
editingSession,
|
||||
editingSessionName,
|
||||
@@ -64,6 +71,7 @@ export default function SidebarProjectSessions({
|
||||
onProjectSelect,
|
||||
onSessionSelect,
|
||||
onDeleteSession,
|
||||
onLoadMoreSessions,
|
||||
onNewSession,
|
||||
t,
|
||||
}: SidebarProjectSessionsProps) {
|
||||
@@ -105,25 +113,39 @@ export default function SidebarProjectSessions({
|
||||
<p className="text-xs text-muted-foreground">{t('sessions.noSessions')}</p>
|
||||
</div>
|
||||
) : (
|
||||
sessions.map((session) => (
|
||||
<SidebarSessionItem
|
||||
key={session.id}
|
||||
project={project}
|
||||
session={session}
|
||||
selectedSession={selectedSession}
|
||||
currentTime={currentTime}
|
||||
editingSession={editingSession}
|
||||
editingSessionName={editingSessionName}
|
||||
onEditingSessionNameChange={onEditingSessionNameChange}
|
||||
onStartEditingSession={onStartEditingSession}
|
||||
onCancelEditingSession={onCancelEditingSession}
|
||||
onSaveEditingSession={onSaveEditingSession}
|
||||
onProjectSelect={onProjectSelect}
|
||||
onSessionSelect={onSessionSelect}
|
||||
onDeleteSession={onDeleteSession}
|
||||
t={t}
|
||||
/>
|
||||
))
|
||||
<>
|
||||
{sessions.map((session) => (
|
||||
<SidebarSessionItem
|
||||
key={session.id}
|
||||
project={project}
|
||||
session={session}
|
||||
selectedSession={selectedSession}
|
||||
currentTime={currentTime}
|
||||
editingSession={editingSession}
|
||||
editingSessionName={editingSessionName}
|
||||
onEditingSessionNameChange={onEditingSessionNameChange}
|
||||
onStartEditingSession={onStartEditingSession}
|
||||
onCancelEditingSession={onCancelEditingSession}
|
||||
onSaveEditingSession={onSaveEditingSession}
|
||||
onProjectSelect={onProjectSelect}
|
||||
onSessionSelect={onSessionSelect}
|
||||
onDeleteSession={onDeleteSession}
|
||||
t={t}
|
||||
/>
|
||||
))}
|
||||
|
||||
{hasMoreSessions && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-8 w-full justify-center text-xs text-muted-foreground hover:text-foreground"
|
||||
onClick={() => onLoadMoreSessions(project.projectId)}
|
||||
disabled={isLoadingMoreSessions}
|
||||
>
|
||||
{isLoadingMoreSessions ? t('sessions.loadingSessions') : 'Load more sessions'}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -100,6 +100,86 @@ const getProjectSessions = (project: Project): ProjectSession[] => {
|
||||
];
|
||||
};
|
||||
|
||||
const countLoadedProjectSessions = (project: Project): number => getProjectSessions(project).length;
|
||||
|
||||
const mergeSessionProviderLists = (baseSessions: ProjectSession[], additionalSessions: ProjectSession[]): ProjectSession[] => {
|
||||
const merged = [...baseSessions];
|
||||
const seenSessionIds = new Set(baseSessions.map((session) => String(session.id)));
|
||||
|
||||
for (const session of additionalSessions) {
|
||||
const sessionId = String(session.id);
|
||||
if (seenSessionIds.has(sessionId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
merged.push(session);
|
||||
seenSessionIds.add(sessionId);
|
||||
}
|
||||
|
||||
return merged;
|
||||
};
|
||||
|
||||
const mergeExpandedSessionPages = (previousProjects: Project[], incomingProjects: Project[]): Project[] => {
|
||||
if (previousProjects.length === 0) {
|
||||
return incomingProjects;
|
||||
}
|
||||
|
||||
const previousByProjectId = new Map(previousProjects.map((project) => [project.projectId, project]));
|
||||
|
||||
return incomingProjects.map((incomingProject) => {
|
||||
const previousProject = previousByProjectId.get(incomingProject.projectId);
|
||||
if (!previousProject) {
|
||||
return incomingProject;
|
||||
}
|
||||
|
||||
const previousLoadedCount = countLoadedProjectSessions(previousProject);
|
||||
const incomingLoadedCount = countLoadedProjectSessions(incomingProject);
|
||||
if (previousLoadedCount <= incomingLoadedCount) {
|
||||
return incomingProject;
|
||||
}
|
||||
|
||||
const mergedProject: Project = {
|
||||
...incomingProject,
|
||||
sessions: mergeSessionProviderLists(incomingProject.sessions ?? [], previousProject.sessions ?? []),
|
||||
cursorSessions: mergeSessionProviderLists(incomingProject.cursorSessions ?? [], previousProject.cursorSessions ?? []),
|
||||
codexSessions: mergeSessionProviderLists(incomingProject.codexSessions ?? [], previousProject.codexSessions ?? []),
|
||||
geminiSessions: mergeSessionProviderLists(incomingProject.geminiSessions ?? [], previousProject.geminiSessions ?? []),
|
||||
};
|
||||
|
||||
const totalSessions = Number(incomingProject.sessionMeta?.total ?? previousLoadedCount);
|
||||
mergedProject.sessionMeta = {
|
||||
...incomingProject.sessionMeta,
|
||||
total: totalSessions,
|
||||
hasMore: countLoadedProjectSessions(mergedProject) < totalSessions,
|
||||
};
|
||||
|
||||
return mergedProject;
|
||||
});
|
||||
};
|
||||
|
||||
const mergeProjectSessionPage = (
|
||||
existingProject: Project,
|
||||
sessionsPage: Pick<Project, 'sessions' | 'cursorSessions' | 'codexSessions' | 'geminiSessions' | 'sessionMeta'>,
|
||||
): Project => {
|
||||
const mergedProject: Project = {
|
||||
...existingProject,
|
||||
sessions: mergeSessionProviderLists(existingProject.sessions ?? [], sessionsPage.sessions ?? []),
|
||||
cursorSessions: mergeSessionProviderLists(existingProject.cursorSessions ?? [], sessionsPage.cursorSessions ?? []),
|
||||
codexSessions: mergeSessionProviderLists(existingProject.codexSessions ?? [], sessionsPage.codexSessions ?? []),
|
||||
geminiSessions: mergeSessionProviderLists(existingProject.geminiSessions ?? [], sessionsPage.geminiSessions ?? []),
|
||||
};
|
||||
|
||||
const totalSessions = Number(sessionsPage.sessionMeta?.total ?? existingProject.sessionMeta?.total ?? 0);
|
||||
mergedProject.sessionMeta = {
|
||||
...existingProject.sessionMeta,
|
||||
...sessionsPage.sessionMeta,
|
||||
total: totalSessions,
|
||||
hasMore: countLoadedProjectSessions(mergedProject) < totalSessions,
|
||||
};
|
||||
|
||||
return mergedProject;
|
||||
};
|
||||
|
||||
const isUpdateAdditive = (
|
||||
currentProjects: Project[],
|
||||
updatedProjects: Project[],
|
||||
@@ -194,7 +274,8 @@ export function useProjectsState({
|
||||
const projectData = (await response.json()) as Project[];
|
||||
|
||||
setProjects((prevProjects) => {
|
||||
const mergedProjects = mergeTaskMasterCache(projectData, prevProjects);
|
||||
const projectsWithTaskMaster = mergeTaskMasterCache(projectData, prevProjects);
|
||||
const mergedProjects = mergeExpandedSessionPages(prevProjects, projectsWithTaskMaster);
|
||||
|
||||
if (prevProjects.length === 0) {
|
||||
return mergedProjects;
|
||||
@@ -336,7 +417,8 @@ export function useProjectsState({
|
||||
(selectedSession && activeSessions.has(selectedSession.id)) ||
|
||||
(activeSessions.size > 0 && Array.from(activeSessions).some((id) => id.startsWith('new-session-')));
|
||||
|
||||
const updatedProjects = mergeTaskMasterCache(projectsMessage.projects, projects);
|
||||
const updatedProjectsWithTaskMaster = mergeTaskMasterCache(projectsMessage.projects, projects);
|
||||
const updatedProjects = mergeExpandedSessionPages(projects, updatedProjectsWithTaskMaster);
|
||||
|
||||
if (
|
||||
hasActiveSession &&
|
||||
@@ -522,14 +604,24 @@ export function useProjectsState({
|
||||
}
|
||||
|
||||
setProjects((prevProjects) =>
|
||||
prevProjects.map((project) => ({
|
||||
...project,
|
||||
sessions: project.sessions?.filter((session) => session.id !== sessionIdToDelete) ?? [],
|
||||
sessionMeta: {
|
||||
prevProjects.map((project) => {
|
||||
const updatedProject: Project = {
|
||||
...project,
|
||||
sessions: project.sessions?.filter((session) => session.id !== sessionIdToDelete) ?? [],
|
||||
cursorSessions: project.cursorSessions?.filter((session) => session.id !== sessionIdToDelete) ?? [],
|
||||
codexSessions: project.codexSessions?.filter((session) => session.id !== sessionIdToDelete) ?? [],
|
||||
geminiSessions: project.geminiSessions?.filter((session) => session.id !== sessionIdToDelete) ?? [],
|
||||
};
|
||||
|
||||
const totalSessions = Math.max(0, Number(project.sessionMeta?.total ?? 0) - 1);
|
||||
updatedProject.sessionMeta = {
|
||||
...project.sessionMeta,
|
||||
total: Math.max(0, (project.sessionMeta?.total as number | undefined ?? 0) - 1),
|
||||
},
|
||||
})),
|
||||
total: totalSessions,
|
||||
hasMore: countLoadedProjectSessions(updatedProject) < totalSessions,
|
||||
};
|
||||
|
||||
return updatedProject;
|
||||
}),
|
||||
);
|
||||
},
|
||||
[navigate, selectedSession?.id],
|
||||
@@ -539,7 +631,8 @@ export function useProjectsState({
|
||||
try {
|
||||
const response = await api.projects();
|
||||
const freshProjects = (await response.json()) as Project[];
|
||||
const mergedProjects = mergeTaskMasterCache(freshProjects, projects);
|
||||
const projectsWithTaskMaster = mergeTaskMasterCache(freshProjects, projects);
|
||||
const mergedProjects = mergeExpandedSessionPages(projects, projectsWithTaskMaster);
|
||||
|
||||
setProjects((prevProjects) =>
|
||||
projectsHaveChanges(prevProjects, mergedProjects, true) ? mergedProjects : prevProjects,
|
||||
@@ -582,6 +675,55 @@ export function useProjectsState({
|
||||
}
|
||||
}, [projects, selectedProject, selectedSession]);
|
||||
|
||||
const loadMoreProjectSessions = useCallback(async (projectId: string) => {
|
||||
const project = projects.find((candidate) => candidate.projectId === projectId);
|
||||
if (!project) {
|
||||
return;
|
||||
}
|
||||
|
||||
const loadedCount = countLoadedProjectSessions(project);
|
||||
const totalCount = Number(project.sessionMeta?.total ?? 0);
|
||||
if (totalCount > 0 && loadedCount >= totalCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await api.projectSessions(projectId, {
|
||||
limit: 20,
|
||||
offset: loadedCount,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const payload = (await response.json().catch(() => ({}))) as { error?: string | { message?: string } };
|
||||
const errorPayload = payload.error;
|
||||
const message =
|
||||
typeof errorPayload === 'string'
|
||||
? errorPayload
|
||||
: errorPayload && typeof errorPayload === 'object' && errorPayload.message
|
||||
? errorPayload.message
|
||||
: `Failed to load more sessions for project ${projectId}`;
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
const sessionsPage = (await response.json()) as Pick<Project, 'sessions' | 'cursorSessions' | 'codexSessions' | 'geminiSessions' | 'sessionMeta'>;
|
||||
|
||||
let mergedProjectForSelection: Project | null = null;
|
||||
setProjects((previousProjects) =>
|
||||
previousProjects.map((candidate) => {
|
||||
if (candidate.projectId !== projectId) {
|
||||
return candidate;
|
||||
}
|
||||
|
||||
const mergedProject = mergeProjectSessionPage(candidate, sessionsPage);
|
||||
mergedProjectForSelection = mergedProject;
|
||||
return mergedProject;
|
||||
}),
|
||||
);
|
||||
|
||||
if (selectedProject?.projectId === projectId && mergedProjectForSelection) {
|
||||
setSelectedProject(mergedProjectForSelection);
|
||||
}
|
||||
}, [projects, selectedProject?.projectId]);
|
||||
|
||||
// `projectId` is the DB identifier passed from the sidebar's delete flow
|
||||
// after the migration away from folder-derived project names.
|
||||
const handleProjectDelete = useCallback(
|
||||
@@ -606,6 +748,7 @@ export function useProjectsState({
|
||||
onSessionSelect: handleSessionSelect,
|
||||
onNewSession: handleNewSession,
|
||||
onSessionDelete: handleSessionDelete,
|
||||
onLoadMoreSessions: loadMoreProjectSessions,
|
||||
onProjectDelete: handleProjectDelete,
|
||||
isLoading: isLoadingProjects,
|
||||
loadingProgress,
|
||||
@@ -621,6 +764,7 @@ export function useProjectsState({
|
||||
handleProjectDelete,
|
||||
handleProjectSelect,
|
||||
handleSessionDelete,
|
||||
loadMoreProjectSessions,
|
||||
handleSessionSelect,
|
||||
handleSidebarRefresh,
|
||||
isLoadingProjects,
|
||||
@@ -658,6 +802,7 @@ export function useProjectsState({
|
||||
handleSessionSelect,
|
||||
handleNewSession,
|
||||
handleSessionDelete,
|
||||
loadMoreProjectSessions,
|
||||
handleProjectDelete,
|
||||
handleSidebarRefresh,
|
||||
};
|
||||
|
||||
@@ -54,6 +54,12 @@ export const api = {
|
||||
// After the projectName → projectId migration the path/query identifier is
|
||||
// the DB-assigned `projectId`; parameter names reflect that for clarity.
|
||||
projects: () => authenticatedFetch('/api/projects'),
|
||||
projectSessions: (projectId, { limit = 20, offset = 0 } = {}) => {
|
||||
const params = new URLSearchParams();
|
||||
params.set('limit', String(limit));
|
||||
params.set('offset', String(offset));
|
||||
return authenticatedFetch(`/api/projects/${encodeURIComponent(projectId)}/sessions?${params.toString()}`);
|
||||
},
|
||||
projectTaskmaster: (projectId) =>
|
||||
authenticatedFetch(`/api/projects/${encodeURIComponent(projectId)}/taskmaster`),
|
||||
// Unified endpoint for persisted session messages.
|
||||
|
||||
Reference in New Issue
Block a user