import React from 'react'; import type { Session } from '@opencode-ai/sdk/v2'; import { RiLayoutLeftLine } from '@remixicon/react'; import { toast } from '@/components/ui'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { useI18n } from '@/lib/i18n'; import { isDesktopShell } from '@/lib/desktop'; import { isDesktopWindowFullscreen as getDesktopWindowFullscreen, onDesktopWindowResized, startDesktopWindowDrag } from '@/lib/desktopNative'; import { sessionEvents } from '@/lib/sessionEvents'; import { formatDirectoryName, cn } from '@/lib/utils'; import { useSessionUIStore } from '@/sync/session-ui-store'; import { useAllLiveSessions, useAllSessionStatuses } from '@/sync/sync-context'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { useSync } from '@/sync/use-sync'; import { useSessionPrefetch } from './sidebar/hooks/useSessionPrefetch'; import { useProjectsStore } from '@/stores/useProjectsStore'; import { useUIStore } from '@/stores/useUIStore'; import { getSafeStorage } from '@/stores/utils/safeStorage'; import { useGitStore, useGitAllBranches, useGitRepoStatusMap } from '@/stores/useGitStore'; import { isVSCodeRuntime } from '@/lib/desktop'; import { NewWorktreeDialog } from './NewWorktreeDialog'; import { ScheduledTasksDialog } from './ScheduledTasksDialog'; import { useSessionFoldersStore } from '@/stores/useSessionFoldersStore'; import { useDebouncedValue } from '@/hooks/useDebouncedValue'; import { useArchivedAutoFolders } from './sidebar/hooks/useArchivedAutoFolders'; import { useSessionSidebarSections } from './sidebar/hooks/useSessionSidebarSections'; import { useProjectSessionSelection } from './sidebar/hooks/useProjectSessionSelection'; import { useGroupOrdering } from './sidebar/hooks/useGroupOrdering'; import { useSessionGrouping } from './sidebar/hooks/useSessionGrouping'; import { useSessionSearchEffects } from './sidebar/hooks/useSessionSearchEffects'; import { useSessionActions } from './sidebar/hooks/useSessionActions'; import { useSidebarPersistence } from './sidebar/hooks/useSidebarPersistence'; import { useProjectRepoStatus } from './sidebar/hooks/useProjectRepoStatus'; import { useProjectSessionLists } from './sidebar/hooks/useProjectSessionLists'; import { useSessionFolderCleanup } from './sidebar/hooks/useSessionFolderCleanup'; import { useStickyProjectHeaders } from './sidebar/hooks/useStickyProjectHeaders'; import { getGitHubPrStatusKey, usePrVisualSummaryByKeys, useGitHubPrStatusStore } from '@/stores/useGitHubPrStatusStore'; import { ProjectEditDialog } from '@/components/layout/ProjectEditDialog'; import { UpdateDialog } from '@/components/ui/UpdateDialog'; import { SessionGroupSection } from './sidebar/SessionGroupSection'; import { SidebarHeader } from './sidebar/SidebarHeader'; import { SidebarActivitySections } from './sidebar/SidebarActivitySections'; import { SidebarFooter } from './sidebar/SidebarFooter'; import { SidebarProjectsList } from './sidebar/SidebarProjectsList'; import { SessionNodeItem } from './sidebar/SessionNodeItem'; import { useUpdateStore } from '@/stores/useUpdateStore'; import { useShallow } from 'zustand/react/shallow'; import { listProjectWorktrees } from '@/lib/worktrees/worktreeManager'; import type { WorktreeMetadata } from '@/types/worktree'; import type { SortableDragHandleProps } from './sidebar/sortableItems'; import { BulkSessionDeleteConfirmDialog, FolderDeleteConfirmDialog, SessionDeleteConfirmDialog, type BulkDeleteSessionsConfirmState, type DeleteFolderConfirmState, type DeleteSessionConfirmState, } from './sidebar/ConfirmDialogs'; import { BulkActionBar } from './sidebar/BulkActionBar'; import { useSessionMultiSelectStore } from '@/stores/useSessionMultiSelectStore'; import { type SessionGroup, type SessionNode } from './sidebar/types'; import { type ActiveNowEntry, addActiveNowSession, deriveActiveNowSessions, deriveLiveActiveNowSessions, persistActiveNowEntries, pruneActiveNowEntries, readActiveNowEntries, } from './sidebar/activitySections'; import { compareSessionsByPinnedAndTime, formatProjectLabel, normalizePath, } from './sidebar/utils'; import { refreshGlobalSessions, resolveGlobalSessionDirectory, useGlobalSessionsStore } from '@/stores/useGlobalSessionsStore'; import { useRuntimeAPIs } from '@/hooks/useRuntimeAPIs'; import { useGitHubAuthStore } from '@/stores/useGitHubAuthStore'; import { subscribeOpenchamberEvents } from '@/lib/openchamberEvents'; const PROJECT_COLLAPSE_STORAGE_KEY = 'oc.sessions.projectCollapse'; const GROUP_ORDER_STORAGE_KEY = 'oc.sessions.groupOrder'; const GROUP_COLLAPSE_STORAGE_KEY = 'oc.sessions.groupCollapse'; const PROJECT_ACTIVE_SESSION_STORAGE_KEY = 'oc.sessions.activeSessionByProject'; const SESSION_EXPANDED_STORAGE_KEY = 'oc.sessions.expandedParents'; const SESSION_PINNED_STORAGE_KEY = 'oc.sessions.pinned'; type PrVisualState = 'draft' | 'open' | 'blocked' | 'merged' | 'closed'; type PrIndicator = { visualState: PrVisualState; number: number; url: string | null; state: 'open' | 'closed' | 'merged'; draft: boolean; title: string | null; base: string | null; head: string | null; checks: { state: 'success' | 'failure' | 'pending' | 'unknown'; total: number; success: number; failure: number; pending: number; } | null; canMerge: boolean | null; mergeableState: string | null; repo: { owner: string; repo: string; } | null; }; const buildKnownSessionDirectories = ( projects: Array<{ path: string }>, availableWorktreesByProject: Map, ): Set => { const directories = new Set(); for (const project of projects) { const normalized = normalizePath(project.path)?.toLowerCase(); if (normalized) directories.add(normalized); } for (const worktrees of availableWorktreesByProject.values()) { for (const worktree of worktrees) { const normalized = normalizePath(worktree.path)?.toLowerCase(); if (normalized) directories.add(normalized); } } return directories; }; const isKnownActiveSessionDirectory = (session: Session, knownDirectories: Set): boolean => { if (session.time?.archived) return true; const directory = normalizePath(resolveGlobalSessionDirectory(session))?.toLowerCase(); if (!directory) return true; if (knownDirectories.size === 0) return true; return knownDirectories.has(directory); }; interface SessionSidebarProps { mobileVariant?: boolean; onSessionSelected?: (sessionId: string) => void; allowReselect?: boolean; hideDirectoryControls?: boolean; showOnlyMainWorkspace?: boolean; } export const SessionSidebar: React.FC = ({ mobileVariant = false, onSessionSelected, allowReselect = false, hideDirectoryControls = false, showOnlyMainWorkspace = false, }) => { const { t } = useI18n(); const [isSessionSearchOpen, setIsSessionSearchOpen] = React.useState(false); const [sessionSearchQuery, setSessionSearchQuery] = React.useState(''); const sessionSearchContainerRef = React.useRef(null); const sessionSearchInputRef = React.useRef(null); const [editingId, setEditingId] = React.useState(null); const [editTitle, setEditTitle] = React.useState(''); const [editingProjectDialogId, setEditingProjectDialogId] = React.useState(null); const [expandedParents, setExpandedParents] = React.useState>(new Set()); const [directoryStatus] = React.useState>( () => new Map(), ); const safeStorage = React.useMemo(() => getSafeStorage(), []); const [activeNowEntries, setActiveNowEntries] = React.useState(() => readActiveNowEntries(safeStorage)); const [collapsedProjects, setCollapsedProjects] = React.useState>(new Set()); const [projectRepoStatus, setProjectRepoStatus] = React.useState>(new Map()); const [expandedSessionGroups, setExpandedSessionGroups] = React.useState>(new Set()); const [newWorktreeDialogOpen, setNewWorktreeDialogOpen] = React.useState(false); const [updateDialogOpen, setUpdateDialogOpen] = React.useState(false); const [openSidebarMenuKey, setOpenSidebarMenuKey] = React.useState(null); const [renamingFolderId, setRenamingFolderId] = React.useState(null); const [renameFolderDraft, setRenameFolderDraft] = React.useState(''); const [deleteSessionConfirm, setDeleteSessionConfirm] = React.useState(null); const [deleteFolderConfirm, setDeleteFolderConfirm] = React.useState(null); const [bulkDeleteConfirm, setBulkDeleteConfirm] = React.useState(null); const [pinnedSessionIds, setPinnedSessionIds] = React.useState>(() => { try { const raw = getSafeStorage().getItem(SESSION_PINNED_STORAGE_KEY); if (!raw) { return new Set(); } const parsed = JSON.parse(raw) as string[]; return new Set(Array.isArray(parsed) ? parsed.filter((item) => typeof item === 'string') : []); } catch { return new Set(); } }); const [collapsedGroups, setCollapsedGroups] = React.useState>(() => { try { const raw = getSafeStorage().getItem(GROUP_COLLAPSE_STORAGE_KEY); if (!raw) { return new Set(); } const parsed = JSON.parse(raw) as string[]; return new Set(Array.isArray(parsed) ? parsed.filter((item) => typeof item === 'string') : []); } catch { return new Set(); } }); const [groupOrderByProject, setGroupOrderByProject] = React.useState>(() => { try { const raw = getSafeStorage().getItem(GROUP_ORDER_STORAGE_KEY); if (!raw) { return new Map(); } const parsed = JSON.parse(raw) as Record; const next = new Map(); Object.entries(parsed).forEach(([projectId, order]) => { if (Array.isArray(order)) { next.set(projectId, order.filter((item) => typeof item === 'string')); } }); return next; } catch { return new Map(); } }); const [activeSessionByProject, setActiveSessionByProject] = React.useState>(() => { try { const raw = getSafeStorage().getItem(PROJECT_ACTIVE_SESSION_STORAGE_KEY); if (!raw) { return new Map(); } const parsed = JSON.parse(raw) as Record; const next = new Map(); Object.entries(parsed).forEach(([projectId, sessionId]) => { if (typeof sessionId === 'string' && sessionId.length > 0) { next.set(projectId, sessionId); } }); return next; } catch { return new Map(); } }); const [projectRootBranches, setProjectRootBranches] = React.useState>(new Map()); const projectHeaderSentinelRefs = React.useRef>(new Map()); const ignoreIntersectionUntil = React.useRef(0); const homeDirectory = useDirectoryStore((state) => state.homeDirectory); const currentDirectory = useDirectoryStore((state) => state.currentDirectory); const setDirectory = useDirectoryStore((state) => state.setDirectory); const projects = useProjectsStore((state) => state.projects); const activeProjectId = useProjectsStore((state) => state.activeProjectId); const removeProject = useProjectsStore((state) => state.removeProject); const setActiveProjectIdOnly = useProjectsStore((state) => state.setActiveProjectIdOnly); const updateProjectMeta = useProjectsStore((state) => state.updateProjectMeta); const reorderProjects = useProjectsStore((state) => state.reorderProjects); const setActiveMainTab = useUIStore((state) => state.setActiveMainTab); const openContextPanelTab = useUIStore((state) => state.openContextPanelTab); const setSettingsDialogOpen = useUIStore((state) => state.setSettingsDialogOpen); const toggleHelpDialog = useUIStore((state) => state.toggleHelpDialog); const setAboutDialogOpen = useUIStore((state) => state.setAboutDialogOpen); const setSessionSwitcherOpen = useUIStore((state) => state.setSessionSwitcherOpen); const setScheduledTasksDialogOpen = useUIStore((state) => state.setScheduledTasksDialogOpen); const toggleSidebar = useUIStore((state) => state.toggleSidebar); const openMultiRunLauncher = useUIStore((state) => state.openMultiRunLauncher); const notifyOnSubtasks = useUIStore((state) => state.notifyOnSubtasks); const showDeletionDialog = useUIStore((state) => state.showDeletionDialog); const setShowDeletionDialog = useUIStore((state) => state.setShowDeletionDialog); const debouncedSessionSearchQuery = useDebouncedValue(sessionSearchQuery, 120); const normalizedSessionSearchQuery = React.useMemo( () => debouncedSessionSearchQuery.trim().toLowerCase(), [debouncedSessionSearchQuery], ); const hasSessionSearchQuery = normalizedSessionSearchQuery.length > 0; // Session Folders store const collapsedFolderIds = useSessionFoldersStore((state) => state.collapsedFolderIds); const foldersMap = useSessionFoldersStore((state) => state.foldersMap); const getFoldersForScope = useSessionFoldersStore((state) => state.getFoldersForScope); const createFolder = useSessionFoldersStore((state) => state.createFolder); const renameFolder = useSessionFoldersStore((state) => state.renameFolder); const deleteFolder = useSessionFoldersStore((state) => state.deleteFolder); const addSessionToFolder = useSessionFoldersStore((state) => state.addSessionToFolder); const addSessionsToFolder = useSessionFoldersStore((state) => state.addSessionsToFolder); const removeSessionFromFolder = useSessionFoldersStore((state) => state.removeSessionFromFolder); const removeSessionsFromFolders = useSessionFoldersStore((state) => state.removeSessionsFromFolders); const toggleFolderCollapse = useSessionFoldersStore((state) => state.toggleFolderCollapse); const cleanupSessions = useSessionFoldersStore((state) => state.cleanupSessions); const getSessionFolderId = useSessionFoldersStore((state) => state.getSessionFolderId); useSessionSearchEffects({ isSessionSearchOpen, setIsSessionSearchOpen, sessionSearchInputRef, sessionSearchContainerRef, }); const gitBranches = useGitAllBranches(); const sync = useSync(); const liveSessions = useAllLiveSessions(); const liveSessionStatuses = useAllSessionStatuses(); const hasLoadedGlobalSessions = useGlobalSessionsStore((state) => state.hasLoaded); const globalActiveSessions = useGlobalSessionsStore((state) => state.activeSessions); const archivedSessions = useGlobalSessionsStore((state) => state.archivedSessions); const currentSessionId = useSessionUIStore((state) => state.currentSessionId); const newSessionDraftOpen = useSessionUIStore((state) => Boolean(state.newSessionDraft?.open)); const setCurrentSession = useSessionUIStore((state) => state.setCurrentSession); const updateSessionTitle = useSessionUIStore((state) => state.updateSessionTitle); const shareSession = useSessionUIStore((state) => state.shareSession); const unshareSession = useSessionUIStore((state) => state.unshareSession); // sessionAttentionStates removed — now using notification-store directly in SessionNodeItem const worktreeMetadata = useSessionUIStore((state) => state.worktreeMetadata); const availableWorktreesByProject = useSessionUIStore((state) => state.availableWorktreesByProject); const openNewSessionDraft = useSessionUIStore((state) => state.openNewSessionDraft); const updateStore = useUpdateStore(useShallow((s) => ({ checkForUpdates: s.checkForUpdates, available: s.available, runtimeType: s.runtimeType, info: s.info, downloading: s.downloading, downloaded: s.downloaded, progress: s.progress, error: s.error, downloadUpdate: s.downloadUpdate, restartToUpdate: s.restartToUpdate, }))); const knownSessionDirectories = React.useMemo( () => buildKnownSessionDirectories(projects, availableWorktreesByProject), [availableWorktreesByProject, projects], ); const sessions = React.useMemo(() => { const liveById = new Map(liveSessions.map((session) => [session.id, session])); const merged = globalActiveSessions.map((session) => liveById.get(session.id) ?? session); const seenIds = new Set(merged.map((session) => session.id)); liveSessions.forEach((session) => { if (seenIds.has(session.id)) { return; } merged.push(session); }); return merged.filter((session) => isKnownActiveSessionDirectory(session, knownSessionDirectories)); }, [globalActiveSessions, knownSessionDirectories, liveSessions]); const syncSessionStructureSignature = React.useMemo( () => liveSessions .map((session) => { const directory = normalizePath((session as Session & { directory?: string | null }).directory ?? null) ?? ''; return `${session.id}:${session.title ?? ''}:${session.time?.archived ? 1 : 0}:${directory}`; }) .join('|'), [liveSessions], ); const syncSessionsSnapshotRef = React.useRef(liveSessions); React.useEffect(() => { syncSessionsSnapshotRef.current = liveSessions; }, [syncSessionStructureSignature, liveSessions]); React.useEffect(() => { let cancelled = false; const discoverWorktrees = async () => { const projectEntries = useProjectsStore.getState().projects; if (projectEntries.length === 0) return; const worktreesByProject = new Map(); const allWorktrees: WorktreeMetadata[] = []; await Promise.all( projectEntries.map(async (project) => { const projectPath = normalizePath(project.path); if (!projectPath) return; try { // Use store-cached isGitRepo when available; fall back to direct check for initial worktree discovery const cachedIsGitRepo = useGitStore.getState().directories.get(projectPath)?.isGitRepo; const isGitRepo = cachedIsGitRepo ?? await import('@/lib/gitApi').then(m => m.checkIsGitRepository(projectPath)); if (!isGitRepo) return; const worktrees = await listProjectWorktrees({ id: project.id, path: projectPath }); if (cancelled || worktrees.length === 0) return; worktreesByProject.set(projectPath, worktrees); allWorktrees.push(...worktrees); } catch { // ignore discovery errors } }), ); if (cancelled) return; useSessionUIStore.setState({ availableWorktrees: allWorktrees, availableWorktreesByProject: worktreesByProject, }); }; void refreshGlobalSessions(syncSessionsSnapshotRef.current); void discoverWorktrees(); return () => { cancelled = true; }; }, [currentDirectory, syncSessionStructureSignature, projects]); React.useEffect(() => { let refreshTimeout: ReturnType | null = null; const unsubscribe = subscribeOpenchamberEvents((event) => { if (event.type !== 'scheduled-task-ran') { return; } if (refreshTimeout) { clearTimeout(refreshTimeout); } refreshTimeout = setTimeout(() => { void refreshGlobalSessions(syncSessionsSnapshotRef.current); }, 500); }); return () => { if (refreshTimeout) { clearTimeout(refreshTimeout); } unsubscribe(); }; }, []); const isDesktopShellRuntime = React.useMemo(() => isDesktopShell(), []); const [isDesktopWindowFullscreen, setIsDesktopWindowFullscreen] = React.useState(false); const isVSCode = React.useMemo(() => isVSCodeRuntime(), []); const isMacPlatform = React.useMemo(() => { if (typeof navigator === 'undefined') { return false; } return /Macintosh|Mac OS X/.test(navigator.userAgent || ''); }, []); const isWebRuntime = !mobileVariant && !isVSCode && !isDesktopShellRuntime; const showDesktopSidebarChrome = !mobileVariant && !isVSCode && !isWebRuntime; const desktopSidebarTopPaddingClass = isDesktopShellRuntime && isMacPlatform && !isDesktopWindowFullscreen ? 'pl-[5.5rem]' : 'pl-3'; const desktopSidebarToggleButtonClass = 'app-region-no-drag inline-flex h-8 w-8 items-center justify-center rounded-md typography-ui-label font-medium text-foreground transition-colors hover:bg-interactive-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50'; React.useEffect(() => { if (!isDesktopShellRuntime || !isMacPlatform) { setIsDesktopWindowFullscreen(false); return; } let disposed = false; let unlistenResize: (() => void) | null = null; const syncFullscreenState = async () => { try { const fullscreen = await getDesktopWindowFullscreen(); if (!disposed) { setIsDesktopWindowFullscreen(fullscreen); } } catch { if (!disposed) { setIsDesktopWindowFullscreen(false); } } }; const attach = async () => { try { unlistenResize = onDesktopWindowResized(() => { void syncFullscreenState(); }); } catch { // Ignore listener setup failures; fallback state remains false. } }; void syncFullscreenState(); void attach(); return () => { disposed = true; if (unlistenResize) { unlistenResize(); } }; }, [isDesktopShellRuntime, isMacPlatform]); const handleDesktopSidebarDragStart = React.useCallback(async (event: React.MouseEvent) => { const target = event.target as HTMLElement; if (target.closest('.app-region-no-drag')) { return; } if (target.closest('button, a, input, select, textarea')) { return; } if (event.button !== 0) { return; } if (!isDesktopShellRuntime) { return; } await startDesktopWindowDrag(); }, [isDesktopShellRuntime]); const { buildGroupSearchText, filterSessionNodesForSearch, buildGroupedSessions, } = useSessionGrouping({ homeDirectory, worktreeMetadata, pinnedSessionIds, gitBranches, isVSCode, }); const { scheduleCollapsedProjectsPersist } = useSidebarPersistence({ isVSCode, hasLoadedGlobalSessions, safeStorage, keys: { sessionExpanded: SESSION_EXPANDED_STORAGE_KEY, projectCollapse: PROJECT_COLLAPSE_STORAGE_KEY, sessionPinned: SESSION_PINNED_STORAGE_KEY, groupOrder: GROUP_ORDER_STORAGE_KEY, projectActiveSession: PROJECT_ACTIVE_SESSION_STORAGE_KEY, groupCollapse: GROUP_COLLAPSE_STORAGE_KEY, }, sessions, pinnedSessionIds, setPinnedSessionIds, groupOrderByProject, activeSessionByProject, collapsedGroups, setExpandedParents, setCollapsedProjects, }); const togglePinnedSession = React.useCallback((sessionId: string) => { setPinnedSessionIds((prev) => { const next = new Set(prev); if (next.has(sessionId)) { next.delete(sessionId); } else { next.add(sessionId); } return next; }); }, []); const sortedSessions = React.useMemo(() => { return [...sessions].sort((a, b) => compareSessionsByPinnedAndTime(a, b, pinnedSessionIds)); }, [sessions, pinnedSessionIds]); const sessionOrderIndex = React.useMemo( () => new Map(sortedSessions.map((session, index) => [session.id, index])), [sortedSessions], ); const childrenMap = React.useMemo(() => { const map = new Map(); sortedSessions.forEach((session) => { const parentID = (session as Session & { parentID?: string | null }).parentID; if (!parentID) { return; } const collection = map.get(parentID) ?? []; collection.push(session); map.set(parentID, collection); }); map.forEach((list) => list.sort((a, b) => compareSessionsByPinnedAndTime(a, b, pinnedSessionIds))); return map; }, [sortedSessions, pinnedSessionIds]); const emptyState = (

{t('sessions.sidebar.empty.noSessions.title')}

{t('sessions.sidebar.empty.noSessions.description')}

); const editingProject = React.useMemo( () => projects.find((project) => project.id === editingProjectDialogId) ?? null, [projects, editingProjectDialogId], ); const handleSaveProjectEdit = React.useCallback((data: { label: string; icon: string | null; color: string | null; iconBackground: string | null }) => { if (!editingProjectDialogId) { return; } updateProjectMeta(editingProjectDialogId, data); setEditingProjectDialogId(null); }, [editingProjectDialogId, updateProjectMeta]); const openNewWorktreeDialog = React.useCallback(() => { setNewWorktreeDialogOpen(true); }, []); const handleOpenUpdateDialog = React.useCallback(() => { const current = useUpdateStore.getState(); if (current.available && current.info) { setUpdateDialogOpen(true); return; } void updateStore.checkForUpdates().then(() => { const { available, error } = useUpdateStore.getState(); if (error) { toast.error(t('sessions.sidebar.updateCheck.errorTitle'), { description: error }); return; } if (!available) { toast.success(t('sessions.sidebar.updateCheck.latestVersion')); return; } setUpdateDialogOpen(true); }); }, [t, updateStore]); const handleOpenSettings = React.useCallback(() => { if (mobileVariant) { setSessionSwitcherOpen(false); } setSettingsDialogOpen(true); }, [mobileVariant, setSessionSwitcherOpen, setSettingsDialogOpen]); const showSidebarUpdateButton = updateStore.available && (updateStore.runtimeType === 'desktop' || updateStore.runtimeType === 'web'); const deleteSession = useSessionUIStore((state) => state.deleteSession); const deleteSessions = useSessionUIStore((state) => state.deleteSessions); const archiveSession = useSessionUIStore((state) => state.archiveSession); const archiveSessions = useSessionUIStore((state) => state.archiveSessions); const { copiedSessionId, handleSessionSelect, handleSessionDoubleClick, handleSaveEdit, handleCancelEdit, handleShareSession, handleCopyShareUrl, handleUnshareSession, handleDeleteSession, confirmDeleteSession, } = useSessionActions({ activeProjectId, currentDirectory, currentSessionId, mobileVariant, allowReselect, onSessionSelected, isSessionSearchOpen, sessionSearchQuery, setSessionSearchQuery, setIsSessionSearchOpen, setActiveProjectIdOnly, setDirectory, setActiveMainTab, setSessionSwitcherOpen, setCurrentSession, updateSessionTitle, shareSession, unshareSession, deleteSession, deleteSessions, archiveSession, archiveSessions, childrenMap, showDeletionDialog, setDeleteSessionConfirm, deleteSessionConfirm, setEditingId, setEditTitle, editingId, editTitle, }); const confirmDeleteFolder = React.useCallback(() => { if (!deleteFolderConfirm) return; const { scopeKey, folderId } = deleteFolderConfirm; setDeleteFolderConfirm(null); deleteFolder(scopeKey, folderId); }, [deleteFolderConfirm, deleteFolder]); const handleOpenDirectoryDialog = React.useCallback(() => { sessionEvents.requestDirectoryDialog(); }, []); // Auto-expand parent session when navigating to a subagent (child) session React.useEffect(() => { if (!currentSessionId) return; const current = sessions.find((s) => s.id === currentSessionId); const parentID = (current as Session & { parentID?: string | null })?.parentID; if (!parentID) return; setExpandedParents((prev) => { if (prev.has(parentID)) return prev; const next = new Set(prev); next.add(parentID); try { safeStorage.setItem(SESSION_EXPANDED_STORAGE_KEY, JSON.stringify(Array.from(next))); } catch { /* ignored */ } return next; }); }, [currentSessionId, sessions, safeStorage]); const toggleParent = React.useCallback((sessionId: string) => { setExpandedParents((prev) => { const next = new Set(prev); if (next.has(sessionId)) { next.delete(sessionId); } else { next.add(sessionId); } try { safeStorage.setItem(SESSION_EXPANDED_STORAGE_KEY, JSON.stringify(Array.from(next))); } catch { /* ignored */ } return next; }); }, [safeStorage]); const createFolderAndStartRename = React.useCallback( (scopeKey: string, parentId?: string | null) => { if (!scopeKey) { return null; } if (parentId && collapsedFolderIds.has(parentId)) { toggleFolderCollapse(parentId); } const newFolder = createFolder(scopeKey, t('sessions.sidebar.folder.newFolderName'), parentId); setRenamingFolderId(newFolder.id); setRenameFolderDraft(newFolder.name); return newFolder; }, [collapsedFolderIds, toggleFolderCollapse, createFolder, t], ); const toggleGroupSessionLimit = React.useCallback((groupId: string) => { setExpandedSessionGroups((prev) => { const next = new Set(prev); if (next.has(groupId)) { next.delete(groupId); } else { next.add(groupId); } return next; }); }, []); const collapseAllProjects = React.useCallback(() => { ignoreIntersectionUntil.current = Date.now() + 150; setCollapsedProjects(() => { const allIds = new Set(projects.map((p) => p.id)); try { safeStorage.setItem(PROJECT_COLLAPSE_STORAGE_KEY, JSON.stringify(Array.from(allIds))); } catch { /* ignored */ } if (!isVSCode) { scheduleCollapsedProjectsPersist(allIds); } return allIds; }); }, [projects, isVSCode, safeStorage, scheduleCollapsedProjectsPersist]); const expandAllProjects = React.useCallback(() => { ignoreIntersectionUntil.current = Date.now() + 150; setCollapsedProjects(() => { const empty = new Set(); try { safeStorage.setItem(PROJECT_COLLAPSE_STORAGE_KEY, JSON.stringify([])); } catch { /* ignored */ } if (!isVSCode) { scheduleCollapsedProjectsPersist(empty); } return empty; }); }, [isVSCode, safeStorage, scheduleCollapsedProjectsPersist]); const toggleProject = React.useCallback((projectId: string) => { // Ignore intersection events for a short period after toggling ignoreIntersectionUntil.current = Date.now() + 150; setCollapsedProjects((prev) => { const next = new Set(prev); if (next.has(projectId)) { next.delete(projectId); } else { next.add(projectId); } try { safeStorage.setItem(PROJECT_COLLAPSE_STORAGE_KEY, JSON.stringify(Array.from(next))); } catch { /* ignored */ } // Persist collapse state to server settings (web + desktop local/remote). if (!isVSCode) { scheduleCollapsedProjectsPersist(next); } return next; }); }, [isVSCode, safeStorage, scheduleCollapsedProjectsPersist]); const normalizedProjects = React.useMemo(() => { return projects .map((project) => ({ ...project, normalizedPath: normalizePath(project.path), })) .filter((project) => Boolean(project.normalizedPath)) as Array<{ id: string; path: string; label?: string; normalizedPath: string; icon?: string; color?: string; iconImage?: { mime: string; updatedAt: number; source: 'custom' | 'auto' }; iconBackground?: string; }>; }, [projects]); const normalizedProjectPaths = React.useMemo( () => normalizedProjects.map((project) => project.normalizedPath), [normalizedProjects], ); const { github } = useRuntimeAPIs(); const githubAuthStatus = useGitHubAuthStore((state) => state.status); const githubAuthChecked = useGitHubAuthStore((state) => state.hasChecked); const gitRepoStatus = useGitRepoStatusMap(normalizedProjectPaths); const ensurePrStatusEntry = useGitHubPrStatusStore((state) => state.ensureEntry); const setPrStatusParams = useGitHubPrStatusStore((state) => state.setParams); const refreshPrStatusTargets = useGitHubPrStatusStore((state) => state.refreshTargets); useProjectRepoStatus({ normalizedProjects, gitRepoStatus, setProjectRepoStatus, setProjectRootBranches, }); const isSessionsLoading = useSessionUIStore((state) => state.isLoading); useSessionFolderCleanup({ isSessionsLoading, sessions, archivedSessions, normalizedProjects, isVSCode, availableWorktreesByProject, cleanupSessions, }); const { getSessionsForProject, getArchivedSessionsForProject } = useProjectSessionLists({ isVSCode, sessions, archivedSessions, availableWorktreesByProject, }); useArchivedAutoFolders({ normalizedProjects, sessions, archivedSessions, availableWorktreesByProject, isVSCode, isSessionsLoading, foldersMap, createFolder, addSessionToFolder, cleanupSessions, }); // Keep last-known repo status to avoid UI jiggling during project switch const lastRepoStatusRef = React.useRef(false); if (activeProjectId && projectRepoStatus.has(activeProjectId)) { lastRepoStatusRef.current = Boolean(projectRepoStatus.get(activeProjectId)); } const { projectSections, groupSearchDataByGroup, sectionsForRender, searchMatchCount, } = useSessionSidebarSections({ normalizedProjects, getSessionsForProject, getArchivedSessionsForProject, availableWorktreesByProject, projectRepoStatus, projectRootBranches, lastRepoStatus: lastRepoStatusRef.current, buildGroupedSessions, hasSessionSearchQuery, normalizedSessionSearchQuery, filterSessionNodesForSearch, buildGroupSearchText, foldersMap, }); const searchEmptyState = (

{t('sessions.sidebar.empty.noMatches.title')}

{t('sessions.sidebar.empty.noMatches.description')}

); const reserveHeaderActionsSpace = true; const { currentSessionDirectory } = useProjectSessionSelection({ projectSections, activeProjectId, activeSessionByProject, setActiveSessionByProject, currentSessionId, handleSessionSelect, newSessionDraftOpen, mobileVariant, openNewSessionDraft, setActiveMainTab, setSessionSwitcherOpen, sessions, worktreeMetadata, }); const { getOrderedGroups } = useGroupOrdering(groupOrderByProject); const hasInitializedArchivedCollapseRef = React.useRef(false); React.useEffect(() => { if (hasInitializedArchivedCollapseRef.current || projectSections.length === 0) { return; } const archivedGroupKeys = projectSections.flatMap((section) => section.groups .filter((group) => group.isArchivedBucket) .map((group) => `${section.project.id}:${group.id}`), ); if (archivedGroupKeys.length > 0) { setCollapsedGroups((prev) => new Set([...prev, ...archivedGroupKeys])); } hasInitializedArchivedCollapseRef.current = true; }, [projectSections]); const sessionSidebarMetaById = React.useMemo(() => { const meta = new Map(); const projectPathLengthBySessionId = new Map(); projectSections.forEach((section) => { const projectLabel = formatProjectLabel( section.project.label?.trim() || formatDirectoryName(section.project.normalizedPath, homeDirectory) || section.project.normalizedPath, ); section.groups.forEach((group) => { const secondaryMeta = group.branch && group.branch !== projectLabel ? { projectLabel, branchLabel: group.branch } : { projectLabel, branchLabel: null }; const visit = (nodes: SessionNode[]) => { nodes.forEach((node) => { const nextProjectPathLength = section.project.normalizedPath.length; const currentProjectPathLength = projectPathLengthBySessionId.get(node.session.id) ?? -1; if (nextProjectPathLength < currentProjectPathLength) { return; } meta.set(node.session.id, { node, projectId: section.project.id, groupDirectory: group.directory, secondaryMeta, }); projectPathLengthBySessionId.set(node.session.id, nextProjectPathLength); if (node.children.length > 0) { visit(node.children); } }); }; visit(group.sessions); }); }); return meta; }, [projectSections, homeDirectory]); const activeNowSessions = React.useMemo( () => deriveActiveNowSessions(activeNowEntries, new Map(sessions.map((session) => [session.id, session]))), [activeNowEntries, sessions], ); const liveActiveSessions = React.useMemo( () => deriveLiveActiveNowSessions(sessions, liveSessionStatuses), [liveSessionStatuses, sessions], ); React.useEffect(() => { if (liveActiveSessions.length === 0) { return; } setActiveNowEntries((prev) => { const next = liveActiveSessions.reduce((entries, session) => addActiveNowSession(entries, session.id), prev); if (next === prev) { return prev; } persistActiveNowEntries(safeStorage, next); return next; }); }, [liveActiveSessions, safeStorage]); React.useEffect(() => { const allKnownSessionsById = new Map(); [...sessions, ...archivedSessions].forEach((session) => { allKnownSessionsById.set(session.id, session); }); const pruned = pruneActiveNowEntries(activeNowEntries, allKnownSessionsById); if (pruned.length === activeNowEntries.length && pruned.every((entry, index) => entry.sessionId === activeNowEntries[index]?.sessionId)) { return; } setActiveNowEntries(pruned); persistActiveNowEntries(safeStorage, pruned); }, [activeNowEntries, archivedSessions, safeStorage, sessions]); // Prefetch is wired below, after recentSessionIds is computed. const activitySections = React.useMemo(() => { const toItem = (session: Session) => { const existing = sessionSidebarMetaById.get(session.id); const sessionDirectory = normalizePath((session as Session & { directory?: string | null }).directory ?? null); return { node: existing?.node ?? { session, children: [], worktree: null }, projectId: existing?.projectId ?? null, groupDirectory: existing?.groupDirectory ?? sessionDirectory, secondaryMeta: existing?.secondaryMeta ?? null, }; }; return [ { key: 'active-now' as const, title: t('sessions.sidebar.activity.recentTitle'), items: activeNowSessions.map(toItem) }, ]; }, [activeNowSessions, sessionSidebarMetaById, t]); const recentSessionIds = React.useMemo(() => { return new Set(activitySections.flatMap((section) => section.items.map((item) => item.node.session.id))); }, [activitySections]); const recentSessionIdsList = React.useMemo(() => [...recentSessionIds], [recentSessionIds]); useSessionPrefetch({ currentSessionId, sortedSessions, recentSessionIds: recentSessionIdsList, loadMessages: sync.syncSession, }); const sectionsForSidebarRender = React.useMemo(() => { if (!isVSCode || hasSessionSearchQuery || recentSessionIds.size === 0) { return sectionsForRender; } const filterNodes = (nodes: SessionNode[]): SessionNode[] => { return nodes.reduce((acc, node) => { if (recentSessionIds.has(node.session.id)) { return acc; } const filteredChildren = filterNodes(node.children); if (filteredChildren.length === node.children.length) { acc.push(node); return acc; } acc.push({ ...node, children: filteredChildren, }); return acc; }, []); }; return sectionsForRender.map((section) => ({ ...section, groups: section.groups.map((group) => ({ ...group, sessions: filterNodes(group.sessions), })), })); }, [isVSCode, hasSessionSearchQuery, recentSessionIds, sectionsForRender]); const prLookupKeys = React.useMemo(() => { const keys = new Set(); sectionsForSidebarRender.forEach((section) => { section.groups.forEach((group) => { const directory = normalizePath(group.directory ?? null); const branch = group.branch?.trim() || gitBranches.get(directory || '')?.trim(); if (!directory || !branch) { return; } keys.add(getGitHubPrStatusKey(directory, branch)); }); }); return [...keys]; }, [gitBranches, sectionsForSidebarRender]); const prVisualSummaryMap = usePrVisualSummaryByKeys(prLookupKeys); React.useEffect(() => { if (!githubAuthChecked || !githubAuthStatus?.connected || !github) { return; } const missingTargets: Array<{ directory: string; branch: string; remoteName?: string | null }> = []; sectionsForSidebarRender.forEach((section) => { if (collapsedProjects.has(section.project.id)) { return; } section.groups.forEach((group) => { const directory = normalizePath(group.directory ?? null); const branch = group.branch?.trim() || gitBranches.get(directory || '')?.trim(); if (!directory || !branch) { return; } const key = getGitHubPrStatusKey(directory, branch); const entry = useGitHubPrStatusStore.getState().entries[key]; if (!entry || !entry.isInitialStatusResolved) { missingTargets.push({ directory, branch }); } }); }); if (missingTargets.length === 0) { return; } const uniqueTargets = new Map(); missingTargets.forEach((target) => { const key = getGitHubPrStatusKey(target.directory, target.branch, target.remoteName ?? null); if (!uniqueTargets.has(key)) { uniqueTargets.set(key, target); } }); uniqueTargets.forEach((target, key) => { ensurePrStatusEntry(key); setPrStatusParams(key, { directory: target.directory, branch: target.branch, remoteName: target.remoteName ?? null, canShow: true, github, githubAuthChecked, githubConnected: githubAuthStatus.connected, }); }); void refreshPrStatusTargets([...uniqueTargets.values()], { force: true, silent: true, markInitialResolved: true, }); }, [ collapsedProjects, ensurePrStatusEntry, github, githubAuthChecked, githubAuthStatus?.connected, gitBranches, refreshPrStatusTargets, sectionsForSidebarRender, setPrStatusParams, ]); const desktopHeaderActionButtonClass = 'inline-flex h-6 w-6 cursor-pointer items-center justify-center rounded-md leading-none text-foreground hover:bg-interactive-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 disabled:cursor-not-allowed'; const mobileHeaderActionButtonClass = 'inline-flex h-6 w-6 cursor-pointer items-center justify-center rounded-md leading-none text-muted-foreground hover:text-foreground hover:bg-interactive-hover/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 disabled:cursor-not-allowed'; const headerActionButtonClass = mobileVariant ? mobileHeaderActionButtonClass : desktopHeaderActionButtonClass; const headerActionIconClass = 'h-4.5 w-4.5'; const stuckProjectHeaders = useStickyProjectHeaders({ isDesktopShellRuntime, projectSections, projectHeaderSentinelRefs, }); const renderSessionNode = React.useCallback( ( node: SessionNode, depth = 0, groupDirectory?: string | null, projectId?: string | null, archivedBucket = false, secondaryMeta?: { projectLabel?: string | null; branchLabel?: string | null } | null, renderContext: 'project' | 'recent' = 'project', ): React.ReactNode => ( ), [ directoryStatus, currentSessionId, pinnedSessionIds, expandedParents, hasSessionSearchQuery, normalizedSessionSearchQuery, notifyOnSubtasks, editingId, setEditingId, editTitle, setEditTitle, handleSaveEdit, handleCancelEdit, toggleParent, handleSessionSelect, handleSessionDoubleClick, togglePinnedSession, handleShareSession, copiedSessionId, handleCopyShareUrl, handleUnshareSession, openSidebarMenuKey, setOpenSidebarMenuKey, renamingFolderId, getFoldersForScope, getSessionFolderId, removeSessionFromFolder, addSessionToFolder, createFolderAndStartRename, openContextPanelTab, handleDeleteSession, mobileVariant, ], ); const toggleCollapsedGroup = React.useCallback((key: string) => { setCollapsedGroups((prev) => { const next = new Set(prev); if (next.has(key)) next.delete(key); else next.add(key); return next; }); }, []); const prVisualStateByDirectoryBranch = React.useMemo(() => { const result = new Map(); for (const [key, summary] of prVisualSummaryMap) { result.set(key, { visualState: summary.visualState as PrVisualState, number: summary.number, url: summary.url, state: summary.prState as 'open' | 'closed' | 'merged', draft: summary.draft, title: summary.title, base: summary.base, head: summary.head, checks: summary.checks as PrIndicator['checks'], canMerge: summary.canMerge, mergeableState: summary.mergeableState, repo: summary.repo, }); } return result; }, [prVisualSummaryMap]); const renderGroupSessions = React.useCallback( (group: SessionGroup, groupKey: string, projectId?: string | null, hideGroupLabel?: boolean, dragHandleProps?: SortableDragHandleProps | null, compactBodyPadding?: boolean) => ( ), [ hasSessionSearchQuery, normalizedSessionSearchQuery, groupSearchDataByGroup, expandedSessionGroups, collapsedGroups, hideDirectoryControls, collapsedFolderIds, toggleFolderCollapse, renameFolder, deleteFolder, showDeletionDialog, renderSessionNode, currentSessionDirectory, projectRepoStatus, toggleGroupSessionLimit, mobileVariant, activeProjectId, setActiveProjectIdOnly, setActiveMainTab, setSessionSwitcherOpen, openNewSessionDraft, addSessionToFolder, createFolderAndStartRename, renamingFolderId, renameFolderDraft, pinnedSessionIds, sessionOrderIndex, prVisualStateByDirectoryBranch, toggleCollapsedGroup, ], ); const topContent = !hasSessionSearchQuery ? ( ) : null; const isInlineEditing = Boolean(renamingFolderId || editingId || editingProjectDialogId); const selectionModeEnabled = useSessionMultiSelectStore((state) => state.enabled); const selectedIds = useSessionMultiSelectStore((state) => state.selectedIds); const selectionScopeKey = useSessionMultiSelectStore((state) => state.scopeKey); const multiSelectStoreApi = useSessionMultiSelectStore; const handleToggleSelectionMode = React.useCallback(() => { useSessionMultiSelectStore.getState().toggleMode(); }, []); const handleExitSelectionMode = React.useCallback(() => { useSessionMultiSelectStore.getState().disable(); }, []); const bulkScopeIsArchived = React.useMemo(() => { if (selectedIds.size === 0) return false; if (typeof document === 'undefined') return false; let sawActive = false; let sawArchived = false; for (const id of selectedIds) { const rows = document.querySelectorAll(`[data-session-row="${CSS.escape(id)}"]`); for (const row of rows) { if (row.getAttribute('data-session-archived') === '1') sawArchived = true; else sawActive = true; } } return sawArchived && !sawActive; }, [selectedIds]); const derivedSelectionScope = React.useMemo(() => { if (selectionScopeKey) return selectionScopeKey; if (selectedIds.size === 0) return null; if (typeof document === 'undefined') return null; for (const id of selectedIds) { const row = document.querySelector(`[data-session-row="${CSS.escape(id)}"]`); const scope = row?.getAttribute('data-session-scope'); if (scope && scope.length > 0) return scope; } return null; }, [selectedIds, selectionScopeKey]); const bulkScopeFolders = React.useMemo(() => { if (!derivedSelectionScope) return []; return foldersMap[derivedSelectionScope] ?? []; }, [foldersMap, derivedSelectionScope]); const bulkCanRemoveFromFolder = React.useMemo(() => { if (!derivedSelectionScope || selectedIds.size === 0) return false; const scopeFolders = foldersMap[derivedSelectionScope] ?? []; for (const folder of scopeFolders) { for (const id of folder.sessionIds) { if (selectedIds.has(id)) return true; } } return false; }, [foldersMap, derivedSelectionScope, selectedIds]); const handleBulkMoveToFolder = React.useCallback((folderId: string) => { if (!derivedSelectionScope || selectedIds.size === 0) return; addSessionsToFolder(derivedSelectionScope, folderId, Array.from(selectedIds)); }, [addSessionsToFolder, selectedIds, derivedSelectionScope]); const handleBulkCreateFolderAndMove = React.useCallback(() => { if (!derivedSelectionScope || selectedIds.size === 0) return; const newFolder = createFolderAndStartRename(derivedSelectionScope); if (!newFolder) return; addSessionsToFolder(derivedSelectionScope, newFolder.id, Array.from(selectedIds)); }, [addSessionsToFolder, createFolderAndStartRename, selectedIds, derivedSelectionScope]); const handleBulkRemoveFromFolder = React.useCallback(() => { if (!derivedSelectionScope || selectedIds.size === 0) return; removeSessionsFromFolders(derivedSelectionScope, Array.from(selectedIds)); }, [removeSessionsFromFolders, selectedIds, derivedSelectionScope]); const executeBulkDelete = React.useCallback(async () => { const ids = Array.from(selectedIds); if (ids.length === 0) return; if (bulkScopeIsArchived) { const { deletedIds, failedIds } = await deleteSessions(ids); if (deletedIds.length > 0) { toast.success(deletedIds.length === 1 ? t('sessions.sidebar.bulkActions.deletedSingle', { count: deletedIds.length }) : t('sessions.sidebar.bulkActions.deletedPlural', { count: deletedIds.length })); } if (failedIds.length > 0) { toast.error(failedIds.length === 1 ? t('sessions.sidebar.bulkActions.failedDeleteSingle', { count: failedIds.length }) : t('sessions.sidebar.bulkActions.failedDeletePlural', { count: failedIds.length })); } } else { const { archivedIds, failedIds } = await archiveSessions(ids); if (archivedIds.length > 0) { toast.success(archivedIds.length === 1 ? t('sessions.sidebar.bulkActions.archivedSingle', { count: archivedIds.length }) : t('sessions.sidebar.bulkActions.archivedPlural', { count: archivedIds.length })); } if (failedIds.length > 0) { toast.error(failedIds.length === 1 ? t('sessions.sidebar.bulkActions.failedArchiveSingle', { count: failedIds.length }) : t('sessions.sidebar.bulkActions.failedArchivePlural', { count: failedIds.length })); } } useSessionMultiSelectStore.getState().clear(); }, [archiveSessions, bulkScopeIsArchived, deleteSessions, selectedIds, t]); const handleBulkDelete = React.useCallback(() => { const count = selectedIds.size; if (count === 0) return; if (!showDeletionDialog) { void executeBulkDelete(); return; } setBulkDeleteConfirm({ sessionCount: count, archivedBucket: bulkScopeIsArchived }); }, [bulkScopeIsArchived, executeBulkDelete, selectedIds, showDeletionDialog]); const confirmBulkDelete = React.useCallback(async () => { setBulkDeleteConfirm(null); await executeBulkDelete(); }, [executeBulkDelete]); React.useEffect(() => { if (!selectionModeEnabled) return; const isMac = typeof navigator !== 'undefined' && /Macintosh|Mac OS X/.test(navigator.userAgent || ''); const listener = (event: KeyboardEvent) => { if (isInlineEditing) return; const target = event.target as HTMLElement | null; if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)) { return; } const modifier = isMac ? event.metaKey : event.ctrlKey; if (event.key === 'Escape') { event.preventDefault(); useSessionMultiSelectStore.getState().disable(); return; } if (modifier && event.key === 'Backspace') { event.preventDefault(); handleBulkDelete(); return; } if (modifier && (event.key === 'a' || event.key === 'A')) { const rows = typeof document !== 'undefined' ? Array.from(document.querySelectorAll('[data-session-row]')) : []; if (rows.length === 0) return; event.preventDefault(); const currentScope = multiSelectStoreApi.getState().scopeKey; const targetScope = currentScope ?? rows[0]?.getAttribute('data-session-scope') ?? null; const scopeFilter = (el: HTMLElement): boolean => { if (!targetScope) return true; return el.getAttribute('data-session-scope') === targetScope; }; const ids = rows .filter(scopeFilter) .map((el) => el.getAttribute('data-session-row')) .filter((id): id is string => typeof id === 'string' && id.length > 0); if (ids.length === 0) return; multiSelectStoreApi.getState().replaceAll(ids, targetScope || null); } }; window.addEventListener('keydown', listener); return () => window.removeEventListener('keydown', listener); }, [handleBulkDelete, isInlineEditing, multiSelectStoreApi, selectionModeEnabled]); const handleSidebarNewSession = React.useCallback(() => { setActiveMainTab('chat'); if (mobileVariant) { setSessionSwitcherOpen(false); } openNewSessionDraft(); }, [mobileVariant, openNewSessionDraft, setActiveMainTab, setSessionSwitcherOpen]); const handleOpenMultiRunFromHeader = React.useCallback(() => { setActiveMainTab('chat'); if (mobileVariant) { setSessionSwitcherOpen(false); } openMultiRunLauncher(); }, [mobileVariant, openMultiRunLauncher, setActiveMainTab, setSessionSwitcherOpen]); return (
{showDesktopSidebarChrome ? (

{t('sessions.sidebar.header.actions.closeSessions')}

) : null} 0} openMultiRunLauncher={handleOpenMultiRunFromHeader} headerActionIconClass={headerActionIconClass} reserveHeaderActionsSpace={reserveHeaderActionsSpace} headerActionButtonClass={headerActionButtonClass} isSessionSearchOpen={isSessionSearchOpen} setIsSessionSearchOpen={setIsSessionSearchOpen} sessionSearchInputRef={sessionSearchInputRef} sessionSearchQuery={sessionSearchQuery} setSessionSearchQuery={setSessionSearchQuery} hasSessionSearchQuery={hasSessionSearchQuery} searchMatchCount={searchMatchCount} collapseAllProjects={collapseAllProjects} expandAllProjects={expandAllProjects} openScheduledTasksDialog={() => setScheduledTasksDialogOpen(true)} selectionModeEnabled={selectionModeEnabled} onToggleSelectionMode={handleToggleSelectionMode} showSidebarToggle={isWebRuntime} onToggleSidebar={toggleSidebar} /> {selectionModeEnabled && selectedIds.size > 0 ? ( ) : null} setAboutDialogOpen(true)} onOpenUpdate={handleOpenUpdateDialog} showRuntimeButtons={!isVSCode} showUpdateButton={showSidebarUpdateButton} /> {editingProject ? ( { if (!open) { setEditingProjectDialogId(null); } }} projectId={editingProject.id} projectName={editingProject.label || formatDirectoryName(editingProject.path, homeDirectory)} projectPath={editingProject.path} initialIcon={editingProject.icon} initialColor={editingProject.color} initialIconBackground={editingProject.iconBackground} onSave={handleSaveProjectEdit} /> ) : null} { setActiveMainTab('chat'); if (mobileVariant) { setSessionSwitcherOpen(false); } if (options?.sessionId) { setCurrentSession(options.sessionId); return; } openNewSessionDraft({ directoryOverride: worktreePath }); }} />
); };