mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-01 10:18:37 +00:00
* refactor(ui): replace in-repo Command primitive with cmdk wrapper * feat(command-palette): add global Cmd+K palette with v1 actions * feat(command-palette): add session, file, and commit search sources * refactor: add provider names to model constants * feat(command-palette): add settings, navigation, message search, and ⌘K hints * feat(command-palette): add git fetch/pull/push and branch switch actions * refactor(command-palette): consolidate fetch source hooks behind useApiSource * refactor(command-palette): extract useCommandKey and SETTINGS_MAIN_TABS metadata * refactor(command-palette): extract groups into declarative registry * refactor(command-palette): wire openFile through PaletteOpsContext * refactor: migrate openSettings and refreshProjects from window.* to PaletteOpsContext * refactor(command-palette): inline groups and delete registry indirection * refactor(command-palette): return items array directly from source hooks * refactor(palette-ops): flatten Handle wrapper into ref-based registry * refactor: inline useCommandKey as MOD_KEY constant in two call sites * feat: introduce pages and fix bug on branch switching * fix: small labels * fix: coderabbit issues * fix: coderabbit comments * Update src/components/chat/view/subcomponents/ProviderSelectionEmptyState.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
758 lines
23 KiB
TypeScript
758 lines
23 KiB
TypeScript
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
import type { TFunction } from 'i18next';
|
|
|
|
import { api } from '../../../utils/api';
|
|
import { usePaletteOps } from '../../../contexts/PaletteOpsContext';
|
|
import type { Project, ProjectSession, LLMProvider } from '../../../types/app';
|
|
import type {
|
|
DeleteProjectConfirmation,
|
|
ProjectSortOrder,
|
|
SessionDeleteConfirmation,
|
|
SessionWithProvider,
|
|
} from '../types/types';
|
|
import {
|
|
clearLegacyStarredProjectIds,
|
|
filterProjects,
|
|
getAllSessions,
|
|
readLegacyStarredProjectIds,
|
|
readProjectSortOrder,
|
|
sortProjects,
|
|
} from '../utils/utils';
|
|
|
|
type SnippetHighlight = {
|
|
start: number;
|
|
end: number;
|
|
};
|
|
|
|
type ConversationMatch = {
|
|
role: string;
|
|
snippet: string;
|
|
highlights: SnippetHighlight[];
|
|
timestamp: string | null;
|
|
provider?: string;
|
|
messageUuid?: string | null;
|
|
};
|
|
|
|
type ConversationSession = {
|
|
sessionId: string;
|
|
sessionSummary: string;
|
|
provider?: string;
|
|
matches: ConversationMatch[];
|
|
};
|
|
|
|
type ConversationProjectResult = {
|
|
// Emitted by the provider search service so the sidebar can map a
|
|
// match back to the Project in its current state by projectId.
|
|
projectId: string | null;
|
|
projectName: string;
|
|
projectDisplayName: string;
|
|
sessions: ConversationSession[];
|
|
};
|
|
|
|
export type ConversationSearchResults = {
|
|
results: ConversationProjectResult[];
|
|
totalMatches: number;
|
|
query: string;
|
|
};
|
|
|
|
export type SearchProgress = {
|
|
scannedProjects: number;
|
|
totalProjects: number;
|
|
};
|
|
|
|
type UseSidebarControllerArgs = {
|
|
projects: Project[];
|
|
selectedProject: Project | null;
|
|
selectedSession: ProjectSession | null;
|
|
isLoading: boolean;
|
|
isMobile: boolean;
|
|
t: TFunction;
|
|
onRefresh: () => Promise<void> | void;
|
|
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;
|
|
setSidebarVisible: (visible: boolean) => void;
|
|
sidebarVisible: boolean;
|
|
};
|
|
|
|
export function useSidebarController({
|
|
projects,
|
|
selectedProject,
|
|
selectedSession: _selectedSession,
|
|
isLoading,
|
|
isMobile,
|
|
t,
|
|
onRefresh,
|
|
onProjectSelect,
|
|
onSessionSelect,
|
|
onSessionDelete,
|
|
onLoadMoreSessions,
|
|
onProjectDelete,
|
|
setCurrentProject,
|
|
setSidebarVisible,
|
|
sidebarVisible,
|
|
}: UseSidebarControllerArgs) {
|
|
const paletteOps = usePaletteOps();
|
|
const [expandedProjects, setExpandedProjects] = useState<Set<string>>(new Set());
|
|
const [editingProject, setEditingProject] = useState<string | null>(null);
|
|
const [showNewProject, setShowNewProject] = useState(false);
|
|
const [editingName, setEditingName] = useState('');
|
|
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 [editingSession, setEditingSession] = useState<string | null>(null);
|
|
const [editingSessionName, setEditingSessionName] = useState('');
|
|
const [searchFilter, setSearchFilter] = useState('');
|
|
const [deletingProjects, setDeletingProjects] = useState<Set<string>>(new Set());
|
|
const [deleteConfirmation, setDeleteConfirmation] = useState<DeleteProjectConfirmation | null>(null);
|
|
const [sessionDeleteConfirmation, setSessionDeleteConfirmation] = useState<SessionDeleteConfirmation | null>(null);
|
|
const [showVersionModal, setShowVersionModal] = useState(false);
|
|
const [searchMode, setSearchMode] = useState<'projects' | 'conversations'>('projects');
|
|
const [conversationResults, setConversationResults] = useState<ConversationSearchResults | null>(null);
|
|
const [isSearching, setIsSearching] = useState(false);
|
|
const [searchProgress, setSearchProgress] = useState<SearchProgress | null>(null);
|
|
const [debouncedSearchQuery, setDebouncedSearchQuery] = useState('');
|
|
const [optimisticStarByProjectId, setOptimisticStarByProjectId] = useState<Map<string, boolean>>(new Map());
|
|
const [loadingMoreProjects, setLoadingMoreProjects] = useState<Set<string>>(new Set());
|
|
const searchSeqRef = useRef(0);
|
|
const eventSourceRef = useRef<EventSource | null>(null);
|
|
const starToggleSequenceByProjectRef = useRef<Map<string, number>>(new Map());
|
|
const migrationStartedRef = useRef(false);
|
|
const onRefreshRef = useRef(onRefresh);
|
|
|
|
const isSidebarCollapsed = !isMobile && !sidebarVisible;
|
|
|
|
useEffect(() => {
|
|
const timer = setInterval(() => {
|
|
setCurrentTime(new Date());
|
|
}, 60000);
|
|
|
|
return () => clearInterval(timer);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
setInitialSessionsLoaded(new Set());
|
|
}, [projects]);
|
|
|
|
useEffect(() => {
|
|
// Auto-expand only when the selected project identity changes.
|
|
// Depending on the full `selectedProject` object (or `selectedSession`) causes
|
|
// websocket-driven list refreshes to re-open projects users manually collapsed.
|
|
const selectedProjectId = selectedProject?.projectId;
|
|
if (!selectedProjectId) {
|
|
return;
|
|
}
|
|
|
|
setExpandedProjects((prev) => {
|
|
if (prev.has(selectedProjectId)) {
|
|
return prev;
|
|
}
|
|
const next = new Set(prev);
|
|
next.add(selectedProjectId);
|
|
return next;
|
|
});
|
|
}, [selectedProject?.projectId]);
|
|
|
|
useEffect(() => {
|
|
if (projects.length > 0 && !isLoading) {
|
|
const loadedProjects = new Set<string>();
|
|
projects.forEach((project) => {
|
|
if (project.sessions && project.sessions.length >= 0) {
|
|
loadedProjects.add(project.projectId);
|
|
}
|
|
});
|
|
setInitialSessionsLoaded(loadedProjects);
|
|
}
|
|
}, [projects, isLoading]);
|
|
|
|
useEffect(() => {
|
|
const loadSortOrder = () => {
|
|
setProjectSortOrder(readProjectSortOrder());
|
|
};
|
|
|
|
loadSortOrder();
|
|
|
|
const handleStorageChange = (event: StorageEvent) => {
|
|
if (event.key === 'claude-settings') {
|
|
loadSortOrder();
|
|
}
|
|
};
|
|
|
|
window.addEventListener('storage', handleStorageChange);
|
|
|
|
const interval = setInterval(() => {
|
|
if (document.hasFocus()) {
|
|
loadSortOrder();
|
|
}
|
|
}, 1000);
|
|
|
|
return () => {
|
|
window.removeEventListener('storage', handleStorageChange);
|
|
clearInterval(interval);
|
|
};
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
onRefreshRef.current = onRefresh;
|
|
}, [onRefresh]);
|
|
|
|
useEffect(() => {
|
|
if (migrationStartedRef.current) {
|
|
return;
|
|
}
|
|
|
|
const legacyStarredProjectIds = readLegacyStarredProjectIds();
|
|
if (legacyStarredProjectIds.length === 0) {
|
|
return;
|
|
}
|
|
|
|
migrationStartedRef.current = true;
|
|
|
|
const migrateLegacyStars = async () => {
|
|
try {
|
|
await api.migrateLegacyProjectStars(legacyStarredProjectIds);
|
|
await onRefreshRef.current();
|
|
} catch (error) {
|
|
console.error('[Sidebar] Failed to migrate legacy starred projects:', error);
|
|
} finally {
|
|
clearLegacyStarredProjectIds();
|
|
}
|
|
};
|
|
|
|
void migrateLegacyStars();
|
|
}, [onRefresh]);
|
|
|
|
useEffect(() => {
|
|
setOptimisticStarByProjectId((previous) => {
|
|
if (previous.size === 0) {
|
|
return previous;
|
|
}
|
|
|
|
const next = new Map(previous);
|
|
let changed = false;
|
|
|
|
for (const [projectId, optimisticValue] of previous.entries()) {
|
|
const project = projects.find((candidate) => candidate.projectId === projectId);
|
|
if (!project) {
|
|
next.delete(projectId);
|
|
changed = true;
|
|
continue;
|
|
}
|
|
|
|
if (Boolean(project.isStarred) === optimisticValue) {
|
|
next.delete(projectId);
|
|
changed = true;
|
|
}
|
|
}
|
|
|
|
return changed ? next : previous;
|
|
});
|
|
}, [projects]);
|
|
|
|
// Debounce search text updates so both project filtering and conversation
|
|
// SSE requests avoid running on every keypress.
|
|
useEffect(() => {
|
|
const timeout = setTimeout(() => {
|
|
setDebouncedSearchQuery(searchFilter.trim());
|
|
}, 300);
|
|
|
|
return () => {
|
|
clearTimeout(timeout);
|
|
};
|
|
}, [searchFilter]);
|
|
|
|
// Debounced conversation search with SSE streaming
|
|
useEffect(() => {
|
|
if (eventSourceRef.current) {
|
|
eventSourceRef.current.close();
|
|
eventSourceRef.current = null;
|
|
}
|
|
|
|
const query = debouncedSearchQuery;
|
|
if (searchMode !== 'conversations' || query.length < 2) {
|
|
searchSeqRef.current += 1;
|
|
setConversationResults(null);
|
|
setSearchProgress(null);
|
|
setIsSearching(false);
|
|
return;
|
|
}
|
|
|
|
setIsSearching(true);
|
|
const seq = ++searchSeqRef.current;
|
|
|
|
if (seq !== searchSeqRef.current) {
|
|
return;
|
|
}
|
|
|
|
const url = api.searchConversationsUrl(query);
|
|
const es = new EventSource(url);
|
|
eventSourceRef.current = es;
|
|
|
|
const accumulated: ConversationProjectResult[] = [];
|
|
let totalMatches = 0;
|
|
|
|
es.addEventListener('result', (evt) => {
|
|
if (seq !== searchSeqRef.current) { es.close(); return; }
|
|
try {
|
|
const data = JSON.parse(evt.data) as {
|
|
projectResult: ConversationProjectResult;
|
|
totalMatches: number;
|
|
scannedProjects: number;
|
|
totalProjects: number;
|
|
};
|
|
accumulated.push(data.projectResult);
|
|
totalMatches = data.totalMatches;
|
|
setConversationResults({ results: [...accumulated], totalMatches, query });
|
|
setSearchProgress({ scannedProjects: data.scannedProjects, totalProjects: data.totalProjects });
|
|
} catch {
|
|
// Ignore malformed SSE data
|
|
}
|
|
});
|
|
|
|
es.addEventListener('progress', (evt) => {
|
|
if (seq !== searchSeqRef.current) { es.close(); return; }
|
|
try {
|
|
const data = JSON.parse(evt.data) as { totalMatches: number; scannedProjects: number; totalProjects: number };
|
|
totalMatches = data.totalMatches;
|
|
setSearchProgress({ scannedProjects: data.scannedProjects, totalProjects: data.totalProjects });
|
|
} catch {
|
|
// Ignore malformed SSE data
|
|
}
|
|
});
|
|
|
|
es.addEventListener('done', () => {
|
|
if (seq !== searchSeqRef.current) { es.close(); return; }
|
|
es.close();
|
|
eventSourceRef.current = null;
|
|
setIsSearching(false);
|
|
setSearchProgress(null);
|
|
if (accumulated.length === 0) {
|
|
setConversationResults({ results: [], totalMatches: 0, query });
|
|
}
|
|
});
|
|
|
|
es.addEventListener('error', () => {
|
|
if (seq !== searchSeqRef.current) { es.close(); return; }
|
|
es.close();
|
|
eventSourceRef.current = null;
|
|
setIsSearching(false);
|
|
setSearchProgress(null);
|
|
if (accumulated.length === 0) {
|
|
setConversationResults({ results: [], totalMatches: 0, query });
|
|
}
|
|
});
|
|
|
|
return () => {
|
|
if (eventSourceRef.current) {
|
|
eventSourceRef.current.close();
|
|
eventSourceRef.current = null;
|
|
}
|
|
};
|
|
}, [debouncedSearchQuery, searchMode]);
|
|
|
|
// All sidebar state keys (expanded, starred, loading, etc.) use the DB
|
|
// `projectId` as their identifier after the migration.
|
|
const toggleProject = useCallback((projectId: string) => {
|
|
setExpandedProjects((prev) => {
|
|
const next = new Set<string>();
|
|
if (!prev.has(projectId)) {
|
|
next.add(projectId);
|
|
}
|
|
return next;
|
|
});
|
|
}, []);
|
|
|
|
const handleSessionClick = useCallback(
|
|
(session: SessionWithProvider, projectId: string) => {
|
|
// Tag the session with its owning projectId so downstream handlers
|
|
// can correlate it with the selectedProject in the app state.
|
|
onSessionSelect({ ...session, __projectId: projectId });
|
|
},
|
|
[onSessionSelect],
|
|
);
|
|
|
|
const resolveProjectStarState = useCallback(
|
|
(projectId: string): boolean => {
|
|
if (optimisticStarByProjectId.has(projectId)) {
|
|
return Boolean(optimisticStarByProjectId.get(projectId));
|
|
}
|
|
|
|
return projects.some((project) => project.projectId === projectId && Boolean(project.isStarred));
|
|
},
|
|
[optimisticStarByProjectId, projects],
|
|
);
|
|
|
|
const toggleStarProject = useCallback((projectId: string) => {
|
|
const previousStarState = resolveProjectStarState(projectId);
|
|
const optimisticStarState = !previousStarState;
|
|
const latestSequence = (starToggleSequenceByProjectRef.current.get(projectId) ?? 0) + 1;
|
|
starToggleSequenceByProjectRef.current.set(projectId, latestSequence);
|
|
|
|
setOptimisticStarByProjectId((previous) => {
|
|
const next = new Map(previous);
|
|
next.set(projectId, optimisticStarState);
|
|
return next;
|
|
});
|
|
|
|
const updateStar = async () => {
|
|
try {
|
|
const response = await api.toggleProjectStar(projectId);
|
|
if (!response.ok) {
|
|
const payload = (await response.json()) as { error?: string | { message?: string } };
|
|
const errorPayload = payload.error;
|
|
const message =
|
|
typeof errorPayload === 'string'
|
|
? errorPayload
|
|
: errorPayload && typeof errorPayload === 'object' && errorPayload.message
|
|
? errorPayload.message
|
|
: t('messages.updateProjectError');
|
|
throw new Error(message);
|
|
}
|
|
|
|
const payload = (await response.json()) as { isStarred?: boolean };
|
|
const isLatestSequence = starToggleSequenceByProjectRef.current.get(projectId) === latestSequence;
|
|
if (!isLatestSequence) {
|
|
return;
|
|
}
|
|
|
|
setOptimisticStarByProjectId((previous) => {
|
|
const next = new Map(previous);
|
|
next.set(projectId, Boolean(payload.isStarred));
|
|
return next;
|
|
});
|
|
} catch (error) {
|
|
const isLatestSequence = starToggleSequenceByProjectRef.current.get(projectId) === latestSequence;
|
|
if (!isLatestSequence) {
|
|
return;
|
|
}
|
|
|
|
setOptimisticStarByProjectId((previous) => {
|
|
const next = new Map(previous);
|
|
next.set(projectId, previousStarState);
|
|
return next;
|
|
});
|
|
console.error('[Sidebar] Failed to toggle project star:', error);
|
|
alert(t('messages.updateProjectError'));
|
|
}
|
|
};
|
|
|
|
void updateStar();
|
|
}, [resolveProjectStarState, t]);
|
|
|
|
const isProjectStarred = useCallback(
|
|
(projectId: string) => resolveProjectStarState(projectId),
|
|
[resolveProjectStarState],
|
|
);
|
|
|
|
const getProjectSessions = useCallback((project: Project) => getAllSessions(project), []);
|
|
|
|
const loadMoreSessionsForProject = useCallback(async (projectId: string) => {
|
|
if (!onLoadMoreSessions) {
|
|
return;
|
|
}
|
|
|
|
let shouldLoad = false;
|
|
setLoadingMoreProjects((previous) => {
|
|
if (previous.has(projectId)) {
|
|
return previous;
|
|
}
|
|
|
|
shouldLoad = true;
|
|
const next = new Set(previous);
|
|
next.add(projectId);
|
|
return next;
|
|
});
|
|
|
|
if (!shouldLoad) {
|
|
return;
|
|
}
|
|
|
|
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;
|
|
});
|
|
}
|
|
}, [onLoadMoreSessions, t]);
|
|
|
|
const projectsWithResolvedStarState = useMemo(() => {
|
|
if (optimisticStarByProjectId.size === 0) {
|
|
return projects;
|
|
}
|
|
|
|
return projects.map((project) => {
|
|
const optimisticStarState = optimisticStarByProjectId.get(project.projectId);
|
|
if (optimisticStarState === undefined) {
|
|
return project;
|
|
}
|
|
|
|
const currentStarState = Boolean(project.isStarred);
|
|
if (currentStarState === optimisticStarState) {
|
|
return project;
|
|
}
|
|
|
|
return {
|
|
...project,
|
|
isStarred: optimisticStarState,
|
|
};
|
|
});
|
|
}, [optimisticStarByProjectId, projects]);
|
|
|
|
const sortedProjects = useMemo(
|
|
() => sortProjects(projectsWithResolvedStarState, projectSortOrder),
|
|
[projectSortOrder, projectsWithResolvedStarState],
|
|
);
|
|
|
|
const filteredProjects = useMemo(
|
|
() => filterProjects(sortedProjects, debouncedSearchQuery),
|
|
[debouncedSearchQuery, sortedProjects],
|
|
);
|
|
|
|
const startEditing = useCallback((project: Project) => {
|
|
// `editingProject` is keyed by projectId so it stays stable across
|
|
// display-name mutations that happen while the input is open.
|
|
setEditingProject(project.projectId);
|
|
setEditingName(project.displayName);
|
|
}, []);
|
|
|
|
const cancelEditing = useCallback(() => {
|
|
setEditingProject(null);
|
|
setEditingName('');
|
|
}, []);
|
|
|
|
const saveProjectName = useCallback(
|
|
// `projectId` is the DB primary key; the rename API resolves the path
|
|
// through the `projects` table before writing the new display name.
|
|
async (projectId: string) => {
|
|
try {
|
|
const response = await api.renameProject(projectId, editingName);
|
|
if (response.ok) {
|
|
await paletteOps.refreshProjects();
|
|
} else {
|
|
console.error('Failed to rename project');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error renaming project:', error);
|
|
} finally {
|
|
setEditingProject(null);
|
|
setEditingName('');
|
|
}
|
|
},
|
|
[editingName, paletteOps],
|
|
);
|
|
|
|
const showDeleteSessionConfirmation = useCallback(
|
|
// Kept with project/provider arguments for component wiring compatibility;
|
|
// deletion now uses only `sessionId` via /api/providers/sessions/:sessionId.
|
|
(
|
|
projectId: string,
|
|
sessionId: string,
|
|
sessionTitle: string,
|
|
provider: SessionDeleteConfirmation['provider'] = 'claude',
|
|
) => {
|
|
setSessionDeleteConfirmation({ projectId, sessionId, sessionTitle, provider });
|
|
},
|
|
[],
|
|
);
|
|
|
|
const confirmDeleteSession = useCallback(async () => {
|
|
if (!sessionDeleteConfirmation) {
|
|
return;
|
|
}
|
|
|
|
const { sessionId } = sessionDeleteConfirmation;
|
|
setSessionDeleteConfirmation(null);
|
|
|
|
try {
|
|
const response = await api.deleteSession(sessionId);
|
|
|
|
if (response.ok) {
|
|
onSessionDelete?.(sessionId);
|
|
} else {
|
|
const errorText = await response.text();
|
|
console.error('[Sidebar] Failed to delete session:', {
|
|
status: response.status,
|
|
error: errorText,
|
|
});
|
|
alert(t('messages.deleteSessionFailed'));
|
|
}
|
|
} catch (error) {
|
|
console.error('[Sidebar] Error deleting session:', error);
|
|
alert(t('messages.deleteSessionError'));
|
|
}
|
|
}, [onSessionDelete, sessionDeleteConfirmation, t]);
|
|
|
|
const requestProjectDelete = useCallback(
|
|
(project: Project) => {
|
|
setDeleteConfirmation({
|
|
project,
|
|
sessionCount: getProjectSessions(project).length,
|
|
});
|
|
},
|
|
[getProjectSessions],
|
|
);
|
|
|
|
const confirmDeleteProject = useCallback(async (deleteData = false) => {
|
|
if (!deleteConfirmation) {
|
|
return;
|
|
}
|
|
|
|
const { project } = deleteConfirmation;
|
|
|
|
setDeleteConfirmation(null);
|
|
// Track in-flight deletes by projectId so the UI can disable actions
|
|
// even if the project object is rebuilt while the request is flying.
|
|
setDeletingProjects((prev) => new Set([...prev, project.projectId]));
|
|
|
|
try {
|
|
const response = await api.deleteProject(project.projectId, deleteData);
|
|
|
|
if (response.ok) {
|
|
onProjectDelete?.(project.projectId);
|
|
} else {
|
|
const data = (await response.json()) as { error?: string | { message?: string } };
|
|
const err = data.error;
|
|
const message =
|
|
typeof err === 'string' ? err : err && typeof err === 'object' && err.message ? err.message : t('messages.deleteProjectFailed');
|
|
alert(message);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error deleting project:', error);
|
|
alert(t('messages.deleteProjectError'));
|
|
} finally {
|
|
setDeletingProjects((prev) => {
|
|
const next = new Set(prev);
|
|
next.delete(project.projectId);
|
|
return next;
|
|
});
|
|
}
|
|
}, [deleteConfirmation, onProjectDelete, t]);
|
|
|
|
const handleProjectSelect = useCallback(
|
|
(project: Project) => {
|
|
onProjectSelect(project);
|
|
setCurrentProject(project);
|
|
},
|
|
[onProjectSelect, setCurrentProject],
|
|
);
|
|
|
|
const refreshProjects = useCallback(async () => {
|
|
setIsRefreshing(true);
|
|
try {
|
|
await onRefresh();
|
|
} finally {
|
|
setIsRefreshing(false);
|
|
}
|
|
}, [onRefresh]);
|
|
|
|
const updateSessionSummary = useCallback(
|
|
// `_projectId` and `_provider` are preserved for compatibility with
|
|
// existing sidebar callback signatures; backend rename only needs sessionId.
|
|
async (_projectId: string, sessionId: string, summary: string, _provider: LLMProvider) => {
|
|
const trimmed = summary.trim();
|
|
if (!trimmed) {
|
|
setEditingSession(null);
|
|
setEditingSessionName('');
|
|
return;
|
|
}
|
|
try {
|
|
const response = await api.renameSession(sessionId, trimmed);
|
|
if (response.ok) {
|
|
await onRefresh();
|
|
} else {
|
|
console.error('[Sidebar] Failed to rename session:', response.status);
|
|
alert(t('messages.renameSessionFailed'));
|
|
}
|
|
} catch (error) {
|
|
console.error('[Sidebar] Error renaming session:', error);
|
|
alert(t('messages.renameSessionError'));
|
|
} finally {
|
|
setEditingSession(null);
|
|
setEditingSessionName('');
|
|
}
|
|
},
|
|
[onRefresh, t],
|
|
);
|
|
|
|
const collapseSidebar = useCallback(() => {
|
|
setSidebarVisible(false);
|
|
}, [setSidebarVisible]);
|
|
|
|
const expandSidebar = useCallback(() => {
|
|
setSidebarVisible(true);
|
|
}, [setSidebarVisible]);
|
|
|
|
return {
|
|
isSidebarCollapsed,
|
|
expandedProjects,
|
|
editingProject,
|
|
showNewProject,
|
|
editingName,
|
|
initialSessionsLoaded,
|
|
currentTime,
|
|
projectSortOrder,
|
|
isRefreshing,
|
|
editingSession,
|
|
editingSessionName,
|
|
searchFilter,
|
|
deletingProjects,
|
|
loadingMoreProjects,
|
|
deleteConfirmation,
|
|
sessionDeleteConfirmation,
|
|
showVersionModal,
|
|
filteredProjects,
|
|
toggleProject,
|
|
handleSessionClick,
|
|
toggleStarProject,
|
|
isProjectStarred,
|
|
getProjectSessions,
|
|
loadMoreSessionsForProject,
|
|
startEditing,
|
|
cancelEditing,
|
|
saveProjectName,
|
|
showDeleteSessionConfirmation,
|
|
confirmDeleteSession,
|
|
requestProjectDelete,
|
|
confirmDeleteProject,
|
|
handleProjectSelect,
|
|
refreshProjects,
|
|
updateSessionSummary,
|
|
collapseSidebar,
|
|
expandSidebar,
|
|
setShowNewProject,
|
|
setEditingName,
|
|
setEditingSession,
|
|
setEditingSessionName,
|
|
searchMode,
|
|
setSearchMode,
|
|
conversationResults,
|
|
isSearching,
|
|
searchProgress,
|
|
clearConversationResults: useCallback(() => {
|
|
searchSeqRef.current += 1;
|
|
if (eventSourceRef.current) {
|
|
eventSourceRef.current.close();
|
|
eventSourceRef.current = null;
|
|
}
|
|
setIsSearching(false);
|
|
setSearchProgress(null);
|
|
setConversationResults(null);
|
|
}, []),
|
|
setSearchFilter,
|
|
setDeleteConfirmation,
|
|
setSessionDeleteConfirmation,
|
|
setShowVersionModal,
|
|
};
|
|
}
|