diff --git a/web/src/components/Chat/SessionSidebar.tsx b/web/src/components/Chat/SessionSidebar.tsx index c629a64c..481a5533 100644 --- a/web/src/components/Chat/SessionSidebar.tsx +++ b/web/src/components/Chat/SessionSidebar.tsx @@ -32,6 +32,28 @@ function formatShortDate(dateStr: string): string { return date.toLocaleDateString([], { month: 'short', day: 'numeric' }); } +// Which session groups (Running / Starred / date buckets) the user has +// collapsed, persisted across reloads. Keyed by the group's visible label, +// mirroring the quota-safe write-through pattern in helpers/draftStorage.ts — +// if localStorage is full or disabled the collapse state stays in memory only. +const COLLAPSED_GROUPS_KEY = 'nerve_sidebar_collapsed_groups'; + +function loadCollapsedGroups(): Set { + try { + const raw = localStorage.getItem(COLLAPSED_GROUPS_KEY); + const arr = raw ? JSON.parse(raw) : []; + return new Set(Array.isArray(arr) ? arr.filter((x: unknown): x is string => typeof x === 'string') : []); + } catch { + return new Set(); + } +} + +function saveCollapsedGroups(groups: Set): void { + try { + localStorage.setItem(COLLAPSED_GROUPS_KEY, JSON.stringify([...groups])); + } catch { /* quota exceeded / disabled — keep the in-memory state only */ } +} + export function SessionSidebar({ sessions, activeSession, agentStatus, onCreate, onDelete, collapsed }: { sessions: Session[]; activeSession: string; @@ -41,6 +63,7 @@ export function SessionSidebar({ sessions, activeSession, agentStatus, onCreate, collapsed?: boolean; }) { const [systemExpanded, setSystemExpanded] = useState(false); + const [collapsedGroups, setCollapsedGroups] = useState>(loadCollapsedGroups); const [localQuery, setLocalQuery] = useState(''); const [searchHovered, setSearchHovered] = useState(false); const [searchFocused, setSearchFocused] = useState(false); @@ -208,6 +231,40 @@ export function SessionSidebar({ sessions, activeSession, agentStatus, onCreate, const groupedConversations = useMemo(() => groupByDate(restConversations), [restConversations]); + // Collapse/expand a session group (Running / Starred / a date bucket), + // persisting the new set so it survives a reload. + const toggleGroup = useCallback((label: string) => { + setCollapsedGroups(prev => { + const next = new Set(prev); + if (next.has(label)) next.delete(label); + else next.add(label); + saveCollapsedGroups(next); + return next; + }); + }, []); + + // Never leave the active session hidden inside a collapsed group: when the + // active session changes, expand whichever group holds it — once, so a later + // manual collapse of that same group still sticks. + const autoExpandedForRef = useRef(null); + useEffect(() => { + if (!activeSession || autoExpandedForRef.current === activeSession) return; + let label: string | null = null; + if (pinnedRunning.some(s => s.id === activeSession)) label = 'Running'; + else if (pinnedStarred.some(s => s.id === activeSession)) label = 'Starred'; + else label = groupedConversations.find(g => g.items.some(s => s.id === activeSession))?.group ?? null; + if (!label) return; // not located yet (sessions still loading) — retry on the next update + const found = label; + autoExpandedForRef.current = activeSession; + setCollapsedGroups(prev => { + if (!prev.has(found)) return prev; + const next = new Set(prev); + next.delete(found); + saveCollapsedGroups(next); + return next; + }); + }, [activeSession, pinnedRunning, pinnedStarred, groupedConversations]); + // Count running system sessions for the badge const runningSystemCount = useMemo( () => systemSessions.filter(s => s.is_running).length, @@ -293,7 +350,7 @@ export function SessionSidebar({ sessions, activeSession, agentStatus, onCreate, -
+
{/* Search results mode */} {isSearching ? (
@@ -361,10 +418,14 @@ export function SessionSidebar({ sessions, activeSession, agentStatus, onCreate, {/* Pinned running sessions */} {pinnedRunning.length > 0 && (
-
- Running -
- {pinnedRunning.map((s) => ( + toggleGroup('Running')} + /> + {!collapsedGroups.has('Running') && pinnedRunning.map((s) => ( 0 && (
-
- Starred -
- {pinnedStarred.map((s) => ( + toggleGroup('Starred')} + /> + {!collapsedGroups.has('Starred') && pinnedStarred.map((s) => ( (
-
- {group} -
- {items.map((s) => ( + toggleGroup(group)} + /> + {!collapsedGroups.has(group) && items.map((s) => ( void; +}) { + return ( + + ); +} + + /** Sidebar icon tint for review-loop observer sessions, by loop status. */ function reviewLoopIconTone(status: string): string { switch (status) {