import { rankByQuery } from '@/lib/search/fuzzySearch'; import React from 'react'; import type { Session } from '@opencode-ai/sdk/v2'; import { Icon } from '@/components/icon/Icon'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { toast } from '@/components/ui'; import { cn, formatDirectoryName } from '@/lib/utils'; import { useI18n } from '@/lib/i18n'; import { sessionEvents } from '@/lib/sessionEvents'; import { useUIStore } from '@/stores/useUIStore'; import { useDirectoryStore } from '@/stores/useDirectoryStore'; import { useSessionUIStore } from '@/sync/session-ui-store'; import { resolveGlobalSessionDirectory, useGlobalSessionsStore } from '@/stores/useGlobalSessionsStore'; import { formatSessionDateLabel, normalizePath } from '@/components/session/sidebar/utils'; import { useShallow } from 'zustand/react/shallow'; type DirectoryBucket = { directory: string; label: string; sessions: Session[]; }; // Bound the mounted DOM: archives grow into the hundreds; batch rendering // keeps the list responsive without a virtualizer. const PAGE_SIZE = 100; export function ArchiveView(): React.ReactNode { const { t } = useI18n(); const open = useUIStore((state) => state.isArchivePageOpen); const setOpen = useUIStore((state) => state.setArchivePageOpen); const setCurrentSession = useSessionUIStore((state) => state.setCurrentSession); const unarchiveSession = useSessionUIStore((state) => state.unarchiveSession); const homeDirectory = useDirectoryStore((state) => state.homeDirectory); const archivedSessions = useGlobalSessionsStore(useShallow((state) => open ? state.archivedSessions : [])); const [query, setQuery] = React.useState(''); const [selectedDirectory, setSelectedDirectory] = React.useState(null); const [visibleCount, setVisibleCount] = React.useState(PAGE_SIZE); const normalizedQuery = query.trim().toLowerCase(); const sortedSessions = React.useMemo(() => { if (!open) return []; return [...archivedSessions].sort((a, b) => (b.time?.archived ?? 0) - (a.time?.archived ?? 0)); }, [archivedSessions, open]); const buckets = React.useMemo(() => { const byDirectory = new Map(); for (const session of sortedSessions) { const directory = normalizePath(resolveGlobalSessionDirectory(session)) ?? ''; const existing = byDirectory.get(directory); if (existing) { existing.sessions.push(session); continue; } byDirectory.set(directory, { directory, label: directory ? (formatDirectoryName(directory, homeDirectory) || directory) : t('sessions.archivePage.otherProjects'), sessions: [session], }); } return [...byDirectory.values()].sort((a, b) => b.sessions.length - a.sessions.length); }, [homeDirectory, sortedSessions, t]); // Search spans every archived session; the directory filter applies only // while not searching. const filteredSessions = React.useMemo(() => { if (normalizedQuery) { return rankByQuery(sortedSessions, normalizedQuery, (session) => [session.title]); } if (selectedDirectory === null) return sortedSessions; return buckets.find((bucket) => bucket.directory === selectedDirectory)?.sessions ?? []; }, [buckets, normalizedQuery, selectedDirectory, sortedSessions]); const visibleSessions = filteredSessions.slice(0, visibleCount); const remainingCount = filteredSessions.length - visibleSessions.length; const totalCount = archivedSessions.length; const selectDirectory = React.useCallback((directory: string | null) => { setSelectedDirectory(directory); setVisibleCount(PAGE_SIZE); }, []); const openSession = React.useCallback((session: Session) => { const directory = normalizePath(resolveGlobalSessionDirectory(session)); setCurrentSession(session.id, directory ?? undefined); setOpen(false); }, [setCurrentSession, setOpen]); const restoreSession = React.useCallback((session: Session) => { void unarchiveSession(session.id).then((success) => { if (success) { toast.success(t('sessions.sidebar.session.restore.success')); } else { toast.error(t('sessions.sidebar.session.restore.error')); } }); }, [t, unarchiveSession]); if (!open) return null; const renderDirectoryItem = ( key: string, label: string, count: number, isSelected: boolean, onSelect: () => void, fullPath?: string, sessionsForDelete?: Session[], ) => (
{sessionsForDelete ? ( {t('sessions.archivePage.deleteProject')} ) : null}
); return (
{/* Directory filter panel */}
{renderDirectoryItem( '__all__', t('sessions.archivePage.allDirectories'), totalCount, selectedDirectory === null, () => selectDirectory(null), )} {buckets.map((bucket) => renderDirectoryItem( bucket.directory || '__none__', bucket.label, bucket.sessions.length, selectedDirectory === bucket.directory, () => selectDirectory(bucket.directory), bucket.directory || undefined, bucket.sessions, ))}
{/* Session list */}
{ setQuery(event.target.value); setVisibleCount(PAGE_SIZE); }} placeholder={t('sessions.archivePage.searchPlaceholder')} className="h-8 w-full rounded-md border border-border bg-transparent pl-8 pr-3 typography-ui-label text-foreground outline-none focus-visible:ring-2 focus-visible:ring-primary/50" />
{/* Pages have no close button: you leave via the sidebar. */} {filteredSessions.length === 1 ? t('sessions.archivePage.countSingle', { count: filteredSessions.length }) : t('sessions.archivePage.countPlural', { count: filteredSessions.length })}
{visibleSessions.length === 0 ? (

{normalizedQuery ? t('sessions.archivePage.empty.noMatches') : t('sessions.archivePage.empty.noArchived')}

) : visibleSessions.map((session) => { const sessionDirectory = normalizePath(resolveGlobalSessionDirectory(session)) ?? ''; const directoryLabel = sessionDirectory ? (formatDirectoryName(sessionDirectory, homeDirectory) || sessionDirectory) : null; return (
openSession(session)} role="button" tabIndex={0} onKeyDown={(event) => { if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); openSession(session); } }} > {session.title || t('sessions.sidebar.session.untitled')} {normalizedQuery && directoryLabel ? ( {directoryLabel} ) : null} {formatSessionDateLabel(session.time?.archived ?? session.time?.updated ?? session.time?.created ?? Date.now())}
); })} {remainingCount > 0 ? ( ) : null}
); }