mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-05-16 01:12:46 +00:00
338 lines
9.7 KiB
TypeScript
338 lines
9.7 KiB
TypeScript
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import { useLocation, useNavigate } from 'react-router-dom';
|
|
import {
|
|
deleteSessionById,
|
|
deleteWorkspaceById,
|
|
getWorkspaceSessions,
|
|
updateSessionCustomName,
|
|
updateWorkspaceCustomName,
|
|
updateWorkspaceStar,
|
|
} from '@/components/refactored/sidebar/data/workspacesApi';
|
|
import type {
|
|
SearchMode,
|
|
SessionDeleteTarget,
|
|
WorkspaceDeleteTarget,
|
|
WorkspaceRecord,
|
|
WorkspaceSession,
|
|
} from '@/components/refactored/sidebar/types';
|
|
import { filterWorkspacesBySearch } from '@/components/refactored/sidebar/utils/search';
|
|
import {
|
|
getSessionDisplayName,
|
|
getWorkspaceDisplayName,
|
|
sortWorkspacesByLastActivity,
|
|
splitWorkspacesByStarred,
|
|
} from '@/components/refactored/sidebar/utils/workspaceTransforms';
|
|
|
|
const SESSION_ROUTE_PATTERN = /^\/session\/([^/]+)$/;
|
|
const LEGACY_SESSION_ROUTE_PATTERN = /^\/sessions\/([^/]+)$/;
|
|
|
|
const extractSessionIdFromPathname = (pathname: string): string | null => {
|
|
const sessionMatch = pathname.match(SESSION_ROUTE_PATTERN);
|
|
if (sessionMatch?.[1]) {
|
|
return decodeURIComponent(sessionMatch[1]);
|
|
}
|
|
|
|
const legacySessionMatch = pathname.match(LEGACY_SESSION_ROUTE_PATTERN);
|
|
if (legacySessionMatch?.[1]) {
|
|
return decodeURIComponent(legacySessionMatch[1]);
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
/**
|
|
* Hook layer (The Manager)
|
|
* Owns sidebar workspace/session state and coordinates UI actions.
|
|
*/
|
|
export const useWorkspaces = () => {
|
|
const navigate = useNavigate();
|
|
const location = useLocation();
|
|
|
|
const [workspaces, setWorkspaces] = useState<WorkspaceRecord[]>([]);
|
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
|
const [searchMode, setSearchMode] = useState<SearchMode>('projects');
|
|
const [searchFilter, setSearchFilter] = useState('');
|
|
const [expandedWorkspaces, setExpandedWorkspaces] = useState<Set<string>>(new Set());
|
|
const [editingWorkspacePath, setEditingWorkspacePath] = useState<string | null>(null);
|
|
const [editingWorkspaceName, setEditingWorkspaceName] = useState('');
|
|
const [workspaceDeleteTarget, setWorkspaceDeleteTarget] = useState<WorkspaceDeleteTarget | null>(null);
|
|
const [sessionDeleteTarget, setSessionDeleteTarget] = useState<SessionDeleteTarget | null>(null);
|
|
const [isSavingWorkspaceName, setIsSavingWorkspaceName] = useState(false);
|
|
const [editingSessionId, setEditingSessionId] = useState<string | null>(null);
|
|
const [editingSessionName, setEditingSessionName] = useState('');
|
|
const [isSavingSessionName, setIsSavingSessionName] = useState(false);
|
|
|
|
const selectedSessionId = useMemo(
|
|
() => extractSessionIdFromPathname(location.pathname),
|
|
[location.pathname],
|
|
);
|
|
|
|
const refreshWorkspaces = useCallback(async () => {
|
|
setIsRefreshing(true);
|
|
try {
|
|
const fetchedWorkspaces = await getWorkspaceSessions();
|
|
setWorkspaces(sortWorkspacesByLastActivity(fetchedWorkspaces));
|
|
} catch (error) {
|
|
console.error('Failed to refresh workspaces:', error);
|
|
setWorkspaces([]);
|
|
} finally {
|
|
setIsRefreshing(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
void refreshWorkspaces();
|
|
}, [refreshWorkspaces]);
|
|
|
|
const filteredWorkspaces = useMemo(
|
|
() => filterWorkspacesBySearch(workspaces, searchMode, searchFilter),
|
|
[searchFilter, searchMode, workspaces],
|
|
);
|
|
|
|
const workspaceGroups = useMemo(
|
|
() => splitWorkspacesByStarred(filteredWorkspaces),
|
|
[filteredWorkspaces],
|
|
);
|
|
|
|
const toggleWorkspace = useCallback((workspaceId: string, workspacePath: string) => {
|
|
setExpandedWorkspaces((previousSet) => {
|
|
const nextSet = new Set(previousSet);
|
|
|
|
if (nextSet.has(workspacePath)) {
|
|
nextSet.delete(workspacePath);
|
|
} else {
|
|
nextSet.add(workspacePath);
|
|
}
|
|
|
|
return nextSet;
|
|
});
|
|
navigate(`/workspaces/${encodeURIComponent(workspaceId)}`);
|
|
}, [navigate]);
|
|
|
|
const openSession = useCallback(
|
|
(workspacePath: string, sessionId: string) => {
|
|
setExpandedWorkspaces((previousSet) => {
|
|
const nextSet = new Set(previousSet);
|
|
nextSet.add(workspacePath);
|
|
return nextSet;
|
|
});
|
|
navigate(`/session/${encodeURIComponent(sessionId)}`);
|
|
},
|
|
[navigate],
|
|
);
|
|
|
|
const openNewSession = useCallback(() => {
|
|
navigate('/');
|
|
}, [navigate]);
|
|
|
|
const toggleWorkspaceStar = useCallback(async (workspaceId: string) => {
|
|
try {
|
|
await updateWorkspaceStar(workspaceId);
|
|
await refreshWorkspaces();
|
|
} catch (error) {
|
|
console.error('Failed to update workspace star:', error);
|
|
}
|
|
}, [refreshWorkspaces]);
|
|
|
|
const startWorkspaceRename = useCallback((workspace: WorkspaceRecord) => {
|
|
setEditingWorkspacePath(workspace.workspaceOriginalPath);
|
|
setEditingWorkspaceName(workspace.workspaceCustomName || '');
|
|
}, []);
|
|
|
|
const cancelWorkspaceRename = useCallback(() => {
|
|
setEditingWorkspacePath(null);
|
|
setEditingWorkspaceName('');
|
|
}, []);
|
|
|
|
const saveWorkspaceRename = useCallback(async () => {
|
|
if (!editingWorkspacePath) {
|
|
return;
|
|
}
|
|
|
|
const editingWorkspace = workspaces.find(
|
|
(workspace) => workspace.workspaceOriginalPath === editingWorkspacePath,
|
|
);
|
|
if (!editingWorkspace) {
|
|
return;
|
|
}
|
|
|
|
setIsSavingWorkspaceName(true);
|
|
try {
|
|
const trimmedName = editingWorkspaceName.trim();
|
|
await updateWorkspaceCustomName(editingWorkspace.workspaceId, trimmedName || null);
|
|
await refreshWorkspaces();
|
|
cancelWorkspaceRename();
|
|
} catch (error) {
|
|
console.error('Failed to update workspace name:', error);
|
|
} finally {
|
|
setIsSavingWorkspaceName(false);
|
|
}
|
|
}, [
|
|
cancelWorkspaceRename,
|
|
editingWorkspaceName,
|
|
editingWorkspacePath,
|
|
refreshWorkspaces,
|
|
workspaces,
|
|
]);
|
|
|
|
const requestWorkspaceDelete = useCallback((workspace: WorkspaceRecord) => {
|
|
setWorkspaceDeleteTarget({
|
|
workspaceId: workspace.workspaceId,
|
|
workspacePath: workspace.workspaceOriginalPath,
|
|
workspaceName: getWorkspaceDisplayName(workspace),
|
|
sessionCount: workspace.sessions.length,
|
|
});
|
|
}, []);
|
|
|
|
const cancelWorkspaceDelete = useCallback(() => {
|
|
setWorkspaceDeleteTarget(null);
|
|
}, []);
|
|
|
|
const confirmWorkspaceDelete = useCallback(async () => {
|
|
if (!workspaceDeleteTarget) {
|
|
return;
|
|
}
|
|
|
|
const deletingWorkspaceId = workspaceDeleteTarget.workspaceId;
|
|
const deletingWorkspacePath = workspaceDeleteTarget.workspacePath;
|
|
setWorkspaceDeleteTarget(null);
|
|
try {
|
|
await deleteWorkspaceById(deletingWorkspaceId);
|
|
|
|
// If the current session belonged to the deleted workspace, reset to root.
|
|
const hadSelectedSession = workspaces.some(
|
|
(workspace) =>
|
|
workspace.workspaceOriginalPath === deletingWorkspacePath &&
|
|
workspace.sessions.some((session) => session.sessionId === selectedSessionId),
|
|
);
|
|
if (hadSelectedSession) {
|
|
navigate('/');
|
|
}
|
|
|
|
await refreshWorkspaces();
|
|
} catch (error) {
|
|
console.error('Failed to delete workspace:', error);
|
|
}
|
|
}, [
|
|
navigate,
|
|
refreshWorkspaces,
|
|
selectedSessionId,
|
|
workspaceDeleteTarget,
|
|
workspaces,
|
|
]);
|
|
|
|
const requestSessionDelete = useCallback(
|
|
(workspacePath: string, session: WorkspaceSession) => {
|
|
setSessionDeleteTarget({
|
|
sessionId: session.sessionId,
|
|
sessionName: getSessionDisplayName(session),
|
|
workspacePath,
|
|
});
|
|
},
|
|
[],
|
|
);
|
|
|
|
const cancelSessionDelete = useCallback(() => {
|
|
setSessionDeleteTarget(null);
|
|
}, []);
|
|
|
|
const startSessionRename = useCallback((session: WorkspaceSession) => {
|
|
setEditingSessionId(session.sessionId);
|
|
setEditingSessionName(getSessionDisplayName(session));
|
|
}, []);
|
|
|
|
const cancelSessionRename = useCallback(() => {
|
|
setEditingSessionId(null);
|
|
setEditingSessionName('');
|
|
}, []);
|
|
|
|
const saveSessionRename = useCallback(async () => {
|
|
if (!editingSessionId) {
|
|
return;
|
|
}
|
|
|
|
const trimmedName = editingSessionName.trim();
|
|
if (!trimmedName) {
|
|
cancelSessionRename();
|
|
return;
|
|
}
|
|
|
|
setIsSavingSessionName(true);
|
|
try {
|
|
await updateSessionCustomName(editingSessionId, trimmedName);
|
|
await refreshWorkspaces();
|
|
cancelSessionRename();
|
|
} catch (error) {
|
|
console.error('Failed to rename session:', error);
|
|
} finally {
|
|
setIsSavingSessionName(false);
|
|
}
|
|
}, [
|
|
cancelSessionRename,
|
|
editingSessionId,
|
|
editingSessionName,
|
|
refreshWorkspaces,
|
|
]);
|
|
|
|
const confirmSessionDelete = useCallback(async () => {
|
|
if (!sessionDeleteTarget) {
|
|
return;
|
|
}
|
|
|
|
const deletingSessionId = sessionDeleteTarget.sessionId;
|
|
setSessionDeleteTarget(null);
|
|
|
|
try {
|
|
await deleteSessionById(deletingSessionId);
|
|
|
|
if (selectedSessionId === deletingSessionId) {
|
|
navigate('/');
|
|
}
|
|
|
|
await refreshWorkspaces();
|
|
} catch (error) {
|
|
console.error('Failed to delete session:', error);
|
|
}
|
|
}, [navigate, refreshWorkspaces, selectedSessionId, sessionDeleteTarget]);
|
|
|
|
return {
|
|
workspaces,
|
|
starredWorkspaces: workspaceGroups.starred,
|
|
unstarredWorkspaces: workspaceGroups.unstarred,
|
|
isRefreshing,
|
|
refreshWorkspaces,
|
|
searchMode,
|
|
setSearchMode,
|
|
searchFilter,
|
|
setSearchFilter,
|
|
selectedSessionId,
|
|
expandedWorkspaces,
|
|
toggleWorkspace,
|
|
openSession,
|
|
openNewSession,
|
|
editingWorkspacePath,
|
|
editingWorkspaceName,
|
|
isSavingWorkspaceName,
|
|
editingSessionId,
|
|
editingSessionName,
|
|
isSavingSessionName,
|
|
setEditingWorkspaceName,
|
|
setEditingSessionName,
|
|
startWorkspaceRename,
|
|
cancelWorkspaceRename,
|
|
saveWorkspaceRename,
|
|
startSessionRename,
|
|
cancelSessionRename,
|
|
saveSessionRename,
|
|
toggleWorkspaceStar,
|
|
workspaceDeleteTarget,
|
|
sessionDeleteTarget,
|
|
requestWorkspaceDelete,
|
|
cancelWorkspaceDelete,
|
|
confirmWorkspaceDelete,
|
|
requestSessionDelete,
|
|
cancelSessionDelete,
|
|
confirmSessionDelete,
|
|
};
|
|
};
|