refactor: remove loading sessions logic from sidebar

This commit is contained in:
Haileyesus
2026-04-25 19:56:50 +03:00
parent bb86236520
commit 3188ef5fee
10 changed files with 18 additions and 207 deletions

View File

@@ -19,7 +19,6 @@ import { getConnectableHost } from '../shared/networkHosts.js';
import { findAppRoot, getModuleDir } from './utils/runtime-paths.js';
import {
getSessionsById,
renameProjectById,
deleteSessionById,
deleteProjectById,
@@ -76,7 +75,7 @@ import pluginsRoutes from './routes/plugins.js';
import messagesRoutes from './routes/messages.js';
import providerRoutes from './modules/providers/provider.routes.js';
import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js';
import { initializeDatabase, sessionsDb, applyCustomSessionNames } from './modules/database/index.js';
import { initializeDatabase, sessionsDb } from './modules/database/index.js';
import { configureWebPush } from './services/vapid-keys.js';
import { validateApiKey, authenticateToken, authenticateWebSocket } from './middleware/auth.js';
import { IS_PLATFORM } from './constants/config.js';
@@ -303,18 +302,6 @@ app.post('/api/system/update', authenticateToken, async (req, res) => {
}
});
// Sessions for a project; `projectId` is resolved to a path via the DB.
app.get('/api/projects/:projectId/sessions', authenticateToken, async (req, res) => {
try {
const { limit = 5, offset = 0 } = req.query;
const result = await getSessionsById(req.params.projectId, parseInt(limit), parseInt(offset));
applyCustomSessionNames(result.sessions, 'claude');
res.json(result);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Rename project endpoint; stores the custom name on the DB row for `projectId`.
app.put('/api/projects/:projectId/rename', authenticateToken, async (req, res) => {
try {

View File

@@ -666,26 +666,6 @@ async function getSessionMessages(projectName, sessionId, limit = null, offset =
}
}
/**
* ID-based wrapper around `getSessions`.
*
* Resolves a `projectId` to the underlying Claude JSONL folder name (via the
* DB-backed project path) and defers to the legacy filesystem reader. Keeps
* the previous pagination shape so the sidebar's "Load more sessions" UI keeps
* working after the migration.
*/
async function getSessionsById(projectId, limit = 5, offset = 0) {
const projectPath = await getProjectPathById(projectId);
if (!projectPath) {
return { sessions: [], hasMore: false, total: 0 };
}
// Claude stores history under ~/.claude/projects/<encoded-path>/; derive the
// folder name from the absolute path the DB gave us.
const claudeFolderName = claudeFolderNameFromPath(projectPath);
return getSessions(claudeFolderName, limit, offset);
}
// Rename a project's display name
async function renameProject(projectName, newDisplayName) {
const config = await loadProjectConfig();
@@ -2053,7 +2033,6 @@ async function getGeminiCliSessionMessages(sessionId) {
// based helpers (`getSessions`, `renameProject`, `deleteSession`, etc.) are
// kept as internal implementation details of the id-based wrappers below.
export {
getSessionsById,
getSessionMessages,
renameProjectById,
deleteSessionById,

View File

@@ -4,9 +4,7 @@ import type { TFunction } from 'i18next';
import { api } from '../../../utils/api';
import type { Project, ProjectSession, LLMProvider } from '../../../types/app';
import type {
AdditionalSessionsByProject,
DeleteProjectConfirmation,
LoadingSessionsByProject,
ProjectSortOrder,
SessionDeleteConfirmation,
SessionWithProvider,
@@ -99,13 +97,10 @@ export function useSidebarController({
const [editingProject, setEditingProject] = useState<string | null>(null);
const [showNewProject, setShowNewProject] = useState(false);
const [editingName, setEditingName] = useState('');
const [loadingSessions, setLoadingSessions] = useState<LoadingSessionsByProject>({});
const [additionalSessions, setAdditionalSessions] = useState<AdditionalSessionsByProject>({});
const [initialSessionsLoaded, setInitialSessionsLoaded] = useState<Set<string>>(new Set());
const [currentTime, setCurrentTime] = useState(new Date());
const [projectSortOrder, setProjectSortOrder] = useState<ProjectSortOrder>('name');
const [isRefreshing, setIsRefreshing] = useState(false);
const [projectHasMoreOverrides, setProjectHasMoreOverrides] = useState<Record<string, boolean>>({});
const [editingSession, setEditingSession] = useState<string | null>(null);
const [editingSessionName, setEditingSessionName] = useState('');
const [searchFilter, setSearchFilter] = useState('');
@@ -133,9 +128,7 @@ export function useSidebarController({
}, []);
useEffect(() => {
setAdditionalSessions({});
setInitialSessionsLoaded(new Set());
setProjectHasMoreOverrides({});
}, [projects]);
useEffect(() => {
@@ -342,31 +335,11 @@ export function useSidebarController({
[starredProjects],
);
const getProjectSessions = useCallback(
(project: Project) => getAllSessions(project, additionalSessions),
[additionalSessions],
);
const projectsWithSessionMeta = useMemo(
() =>
projects.map((project) => {
// The `hasMore` override map is keyed by projectId (see loadMoreSessions).
const hasMoreOverride = projectHasMoreOverrides[project.projectId];
if (hasMoreOverride === undefined) {
return project;
}
return {
...project,
sessionMeta: { ...project.sessionMeta, hasMore: hasMoreOverride },
};
}),
[projectHasMoreOverrides, projects],
);
const getProjectSessions = useCallback((project: Project) => getAllSessions(project), []);
const sortedProjects = useMemo(
() => sortProjects(projectsWithSessionMeta, projectSortOrder, starredProjects, additionalSessions),
[additionalSessions, projectSortOrder, projectsWithSessionMeta, starredProjects],
() => sortProjects(projects, projectSortOrder, starredProjects),
[projectSortOrder, projects, starredProjects],
);
const filteredProjects = useMemo(
@@ -504,51 +477,6 @@ export function useSidebarController({
}
}, [deleteConfirmation, onProjectDelete, t]);
const loadMoreSessions = useCallback(
async (project: Project) => {
// Per-project bookkeeping (additionalSessions, loadingSessions,
// projectHasMoreOverrides) is indexed by the DB `projectId`.
const hasMoreOverride = projectHasMoreOverrides[project.projectId];
const canLoadMore =
hasMoreOverride !== undefined ? hasMoreOverride : project.sessionMeta?.hasMore === true;
if (!canLoadMore || loadingSessions[project.projectId]) {
return;
}
setLoadingSessions((prev) => ({ ...prev, [project.projectId]: true }));
try {
const currentSessionCount =
(project.sessions?.length || 0) + (additionalSessions[project.projectId]?.length || 0);
const response = await api.sessions(project.projectId, 5, currentSessionCount);
if (!response.ok) {
return;
}
const result = (await response.json()) as {
sessions?: ProjectSession[];
hasMore?: boolean;
};
setAdditionalSessions((prev) => ({
...prev,
[project.projectId]: [...(prev[project.projectId] || []), ...(result.sessions || [])],
}));
if (result.hasMore === false) {
// Keep hasMore state in local hook state instead of mutating the project prop object.
setProjectHasMoreOverrides((prev) => ({ ...prev, [project.projectId]: false }));
}
} catch (error) {
console.error('Error loading more sessions:', error);
} finally {
setLoadingSessions((prev) => ({ ...prev, [project.projectId]: false }));
}
},
[additionalSessions, loadingSessions, projectHasMoreOverrides],
);
const handleProjectSelect = useCallback(
(project: Project) => {
onProjectSelect(project);
@@ -609,8 +537,6 @@ export function useSidebarController({
editingProject,
showNewProject,
editingName,
loadingSessions,
additionalSessions,
initialSessionsLoaded,
currentTime,
projectSortOrder,
@@ -636,7 +562,6 @@ export function useSidebarController({
confirmDeleteSession,
requestProjectDelete,
confirmDeleteProject,
loadMoreSessions,
handleProjectSelect,
refreshProjects,
updateSessionSummary,

View File

@@ -6,9 +6,6 @@ export type SessionWithProvider = ProjectSession & {
__provider: LLMProvider;
};
export type AdditionalSessionsByProject = Record<string, ProjectSession[]>;
export type LoadingSessionsByProject = Record<string, boolean>;
export type DeleteProjectConfirmation = {
project: Project;
sessionCount: number;

View File

@@ -1,12 +1,6 @@
import type { TFunction } from 'i18next';
import type { Project } from '../../../types/app';
import type {
AdditionalSessionsByProject,
ProjectSortOrder,
SettingsProject,
SessionViewModel,
SessionWithProvider,
} from '../types/types';
import type { ProjectSortOrder, SettingsProject, SessionViewModel, SessionWithProvider } from '../types/types';
export const readProjectSortOrder = (): ProjectSortOrder => {
try {
@@ -98,16 +92,11 @@ export const createSessionViewModel = (
};
};
export const getAllSessions = (
project: Project,
additionalSessions: AdditionalSessionsByProject,
): SessionWithProvider[] => {
// `additionalSessions` is indexed by DB `projectId` now (the sidebar keys
// every per-project map by the same identifier).
const claudeSessions = [
...(project.sessions || []),
...(additionalSessions[project.projectId] || []),
].map((session) => ({ ...session, __provider: 'claude' as const }));
export const getAllSessions = (project: Project): SessionWithProvider[] => {
const claudeSessions = [...(project.sessions || [])].map((session) => ({
...session,
__provider: 'claude' as const,
}));
const cursorSessions = (project.cursorSessions || []).map((session) => ({
...session,
@@ -129,11 +118,8 @@ export const getAllSessions = (
);
};
export const getProjectLastActivity = (
project: Project,
additionalSessions: AdditionalSessionsByProject,
): Date => {
const sessions = getAllSessions(project, additionalSessions);
export const getProjectLastActivity = (project: Project): Date => {
const sessions = getAllSessions(project);
if (sessions.length === 0) {
return new Date(0);
}
@@ -148,7 +134,6 @@ export const sortProjects = (
projects: Project[],
projectSortOrder: ProjectSortOrder,
starredProjects: Set<string>,
additionalSessions: AdditionalSessionsByProject,
): Project[] => {
const byName = [...projects];
@@ -166,10 +151,7 @@ export const sortProjects = (
}
if (projectSortOrder === 'date') {
return (
getProjectLastActivity(projectB, additionalSessions).getTime() -
getProjectLastActivity(projectA, additionalSessions).getTime()
);
return getProjectLastActivity(projectB).getTime() - getProjectLastActivity(projectA).getTime();
}
return (projectA.displayName || projectA.projectId).localeCompare(projectB.displayName || projectB.projectId);

View File

@@ -53,7 +53,6 @@ function Sidebar({
editingProject,
showNewProject,
editingName,
loadingSessions,
initialSessionsLoaded,
currentTime,
isRefreshing,
@@ -83,7 +82,6 @@ function Sidebar({
confirmDeleteSession,
requestProjectDelete,
confirmDeleteProject,
loadMoreSessions,
handleProjectSelect,
refreshProjects,
updateSessionSummary,
@@ -142,7 +140,6 @@ function Sidebar({
expandedProjects,
editingProject,
editingName,
loadingSessions,
initialSessionsLoaded,
currentTime,
editingSession,
@@ -164,9 +161,6 @@ function Sidebar({
onDeleteProject: requestProjectDelete,
onSessionSelect: handleSessionClick,
onDeleteSession: showDeleteSessionConfirmation,
onLoadMoreSessions: (project) => {
void loadMoreSessions(project);
},
onNewSession,
onEditingSessionNameChange: setEditingSessionName,
onStartEditingSession: (sessionId, initialName) => {

View File

@@ -19,7 +19,6 @@ type SidebarProjectItemProps = {
editingName: string;
sessions: SessionWithProvider[];
initialSessionsLoaded: boolean;
isLoadingSessions: boolean;
currentTime: Date;
editingSession: string | null;
editingSessionName: string;
@@ -40,7 +39,6 @@ type SidebarProjectItemProps = {
sessionTitle: string,
provider: LLMProvider,
) => void;
onLoadMoreSessions: (project: Project) => void;
onNewSession: (project: Project) => void;
onEditingSessionNameChange: (value: string) => void;
onStartEditingSession: (sessionId: string, initialName: string) => void;
@@ -49,14 +47,7 @@ type SidebarProjectItemProps = {
t: TFunction;
};
const getSessionCountDisplay = (sessions: SessionWithProvider[], hasMoreSessions: boolean): string => {
const sessionCount = sessions.length;
if (hasMoreSessions && sessionCount >= 5) {
return `${sessionCount}+`;
}
return `${sessionCount}`;
};
const getSessionCountDisplay = (sessions: SessionWithProvider[]): string => String(sessions.length);
export default function SidebarProjectItem({
project,
@@ -69,7 +60,6 @@ export default function SidebarProjectItem({
editingName,
sessions,
initialSessionsLoaded,
isLoadingSessions,
currentTime,
editingSession,
editingSessionName,
@@ -85,7 +75,6 @@ export default function SidebarProjectItem({
onDeleteProject,
onSessionSelect,
onDeleteSession,
onLoadMoreSessions,
onNewSession,
onEditingSessionNameChange,
onStartEditingSession,
@@ -97,8 +86,7 @@ export default function SidebarProjectItem({
// after the projectName → projectId migration.
const isSelected = selectedProject?.projectId === project.projectId;
const isEditing = editingProject === project.projectId;
const hasMoreSessions = project.sessionMeta?.hasMore === true;
const sessionCountDisplay = getSessionCountDisplay(sessions, hasMoreSessions);
const sessionCountDisplay = getSessionCountDisplay(sessions);
const sessionCountLabel = `${sessionCountDisplay} session${sessions.length === 1 ? '' : 's'}`;
const taskStatus = getTaskIndicatorStatus(project, mcpServerStatus);
@@ -411,7 +399,6 @@ export default function SidebarProjectItem({
sessions={sessions}
selectedSession={selectedSession}
initialSessionsLoaded={initialSessionsLoaded}
isLoadingSessions={isLoadingSessions}
currentTime={currentTime}
editingSession={editingSession}
editingSessionName={editingSessionName}
@@ -422,7 +409,6 @@ export default function SidebarProjectItem({
onProjectSelect={onProjectSelect}
onSessionSelect={onSessionSelect}
onDeleteSession={onDeleteSession}
onLoadMoreSessions={onLoadMoreSessions}
onNewSession={onNewSession}
t={t}
/>

View File

@@ -1,11 +1,7 @@
import { useEffect } from 'react';
import type { TFunction } from 'i18next';
import type { LoadingProgress, Project, ProjectSession, LLMProvider } from '../../../../types/app';
import type {
LoadingSessionsByProject,
MCPServerStatus,
SessionWithProvider,
} from '../../types/types';
import type { MCPServerStatus, SessionWithProvider } from '../../types/types';
import SidebarProjectItem from './SidebarProjectItem';
import SidebarProjectsState from './SidebarProjectsState';
@@ -19,7 +15,6 @@ export type SidebarProjectListProps = {
expandedProjects: Set<string>;
editingProject: string | null;
editingName: string;
loadingSessions: LoadingSessionsByProject;
initialSessionsLoaded: Set<string>;
currentTime: Date;
editingSession: string | null;
@@ -44,7 +39,6 @@ export type SidebarProjectListProps = {
sessionTitle: string,
provider: LLMProvider,
) => void;
onLoadMoreSessions: (project: Project) => void;
onNewSession: (project: Project) => void;
onEditingSessionNameChange: (value: string) => void;
onStartEditingSession: (sessionId: string, initialName: string) => void;
@@ -63,7 +57,6 @@ export default function SidebarProjectList({
expandedProjects,
editingProject,
editingName,
loadingSessions,
initialSessionsLoaded,
currentTime,
editingSession,
@@ -83,7 +76,6 @@ export default function SidebarProjectList({
onDeleteProject,
onSessionSelect,
onDeleteSession,
onLoadMoreSessions,
onNewSession,
onEditingSessionNameChange,
onStartEditingSession,
@@ -131,7 +123,6 @@ export default function SidebarProjectList({
editingName={editingName}
sessions={getProjectSessions(project)}
initialSessionsLoaded={initialSessionsLoaded.has(project.projectId)}
isLoadingSessions={Boolean(loadingSessions[project.projectId])}
currentTime={currentTime}
editingSession={editingSession}
editingSessionName={editingSessionName}
@@ -147,7 +138,6 @@ export default function SidebarProjectList({
onDeleteProject={onDeleteProject}
onSessionSelect={onSessionSelect}
onDeleteSession={onDeleteSession}
onLoadMoreSessions={onLoadMoreSessions}
onNewSession={onNewSession}
onEditingSessionNameChange={onEditingSessionNameChange}
onStartEditingSession={onStartEditingSession}

View File

@@ -1,4 +1,4 @@
import { ChevronDown, Plus } from 'lucide-react';
import { Plus } from 'lucide-react';
import type { TFunction } from 'i18next';
import { Button } from '../../../../shared/view/ui';
import type { Project, ProjectSession, LLMProvider } from '../../../../types/app';
@@ -11,7 +11,6 @@ type SidebarProjectSessionsProps = {
sessions: SessionWithProvider[];
selectedSession: ProjectSession | null;
initialSessionsLoaded: boolean;
isLoadingSessions: boolean;
currentTime: Date;
editingSession: string | null;
editingSessionName: string;
@@ -27,7 +26,6 @@ type SidebarProjectSessionsProps = {
sessionTitle: string,
provider: LLMProvider,
) => void;
onLoadMoreSessions: (project: Project) => void;
onNewSession: (project: Project) => void;
t: TFunction;
};
@@ -56,7 +54,6 @@ export default function SidebarProjectSessions({
sessions,
selectedSession,
initialSessionsLoaded,
isLoadingSessions,
currentTime,
editingSession,
editingSessionName,
@@ -67,7 +64,6 @@ export default function SidebarProjectSessions({
onProjectSelect,
onSessionSelect,
onDeleteSession,
onLoadMoreSessions,
onNewSession,
t,
}: SidebarProjectSessionsProps) {
@@ -76,7 +72,6 @@ export default function SidebarProjectSessions({
}
const hasSessions = sessions.length > 0;
const hasMoreSessions = project.sessionMeta?.hasMore === true;
return (
<div className="ml-3 space-y-1 border-l border-border pl-3">
@@ -105,7 +100,7 @@ export default function SidebarProjectSessions({
{!initialSessionsLoaded ? (
<SessionListSkeleton />
) : !hasSessions && !isLoadingSessions ? (
) : !hasSessions ? (
<div className="px-3 py-2 text-left">
<p className="text-xs text-muted-foreground">{t('sessions.noSessions')}</p>
</div>
@@ -130,28 +125,6 @@ export default function SidebarProjectSessions({
/>
))
)}
{hasSessions && hasMoreSessions && (
<Button
variant="ghost"
size="sm"
className="mt-2 w-full justify-center gap-2 text-muted-foreground"
onClick={() => onLoadMoreSessions(project)}
disabled={isLoadingSessions}
>
{isLoadingSessions ? (
<>
<div className="h-3 w-3 animate-spin rounded-full border border-muted-foreground border-t-transparent" />
{t('sessions.loading')}
</>
) : (
<>
<ChevronDown className="h-3 w-3" />
{t('sessions.showMore')}
</>
)}
</Button>
)}
</div>
);
}

View File

@@ -56,8 +56,6 @@ export const api = {
projects: () => authenticatedFetch('/api/projects'),
projectTaskmaster: (projectId) =>
authenticatedFetch(`/api/projects/${encodeURIComponent(projectId)}/taskmaster`),
sessions: (projectId, limit = 5, offset = 0) =>
authenticatedFetch(`/api/projects/${projectId}/sessions?limit=${limit}&offset=${offset}`),
// Unified endpoint — all providers through one URL. The legacy `projectName`
// query parameter is preserved on the wire (routes/messages.js still reads
// it) but it now carries a projectId value supplied by the caller.