diff --git a/web/src/components/SessionList.directory-action.test.tsx b/web/src/components/SessionList.directory-action.test.tsx index 2b76e43201..f5243d266f 100644 --- a/web/src/components/SessionList.directory-action.test.tsx +++ b/web/src/components/SessionList.directory-action.test.tsx @@ -1,4 +1,4 @@ -import { cleanup, fireEvent, render, screen } from '@testing-library/react' +import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { afterEach, describe, expect, it, vi } from 'vitest' import type { ReactNode } from 'react' @@ -95,3 +95,102 @@ describe('SessionList directory action', () => { expect(screen.queryByRole('button', { name: 'New session in this directory' })).toBeNull() }) }) + +describe('SessionList collapse behavior', () => { + function renderSessionList(sessions: SessionSummary[], selectedSessionId = 'session-running') { + return ( + + + + + + ) + } + + function getProjectPanel(): Element { + const header = screen.getByTitle('/work/hapi') + const panel = header.nextElementSibling + if (!panel) { + throw new Error('Expected project collapse panel') + } + return panel + } + + it('keeps a selected running path collapsed across live session-list refreshes', async () => { + const baseSessions = [ + makeSession({ + id: 'session-running', + active: true, + thinking: true, + pendingRequestsCount: 1, + updatedAt: 100, + metadata: { path: '/work/hapi', name: 'Running task', flavor: 'codex' }, + }), + makeSession({ + id: 'session-old', + updatedAt: 50, + metadata: { path: '/work/hapi', name: 'Older task', flavor: 'codex' }, + }) + ] + const { rerender } = render(renderSessionList(baseSessions)) + + expect(getProjectPanel().getAttribute('data-open')).toBe('true') + + fireEvent.click(screen.getByTitle('/work/hapi')) + expect(getProjectPanel().getAttribute('data-open')).toBeNull() + + rerender(renderSessionList([ + { + ...baseSessions[0]!, + pendingRequestsCount: 2, + updatedAt: 200, + }, + baseSessions[1]! + ])) + + await waitFor(() => { + expect(getProjectPanel().getAttribute('data-open')).toBeNull() + }) + }) + + it('auto-expands the path again when the selected session changes', async () => { + const sessions = [ + makeSession({ + id: 'session-running', + active: true, + thinking: true, + updatedAt: 100, + metadata: { path: '/work/hapi', name: 'Running task', flavor: 'codex' }, + }), + makeSession({ + id: 'session-next', + updatedAt: 90, + metadata: { path: '/work/hapi', name: 'Next task', flavor: 'codex' }, + }) + ] + const { rerender } = render(renderSessionList(sessions)) + + fireEvent.click(screen.getByTitle('/work/hapi')) + expect(getProjectPanel().getAttribute('data-open')).toBeNull() + + rerender(renderSessionList(sessions, 'session-next')) + + await waitFor(() => { + expect(getProjectPanel().getAttribute('data-open')).toBe('true') + }) + }) +}) diff --git a/web/src/components/SessionList.tsx b/web/src/components/SessionList.tsx index 294fcc364b..8b48a86c51 100644 --- a/web/src/components/SessionList.tsx +++ b/web/src/components/SessionList.tsx @@ -739,6 +739,7 @@ export function SessionList(props: { const [collapseOverrides, setCollapseOverrides] = useState>( () => new Map() ) + const autoExpandedSelectedSessionKeyRef = useRef(null) const isGroupCollapsed = (group: SessionGroup): boolean => { if (isSearching) return false const override = collapseOverrides.get(group.key) @@ -812,16 +813,26 @@ export function SessionList(props: { }) } - // Auto-expand group (and machine) containing selected session + // Auto-expand group (and machine) containing the selected session only when + // the selected-session/group pair changes. Without this guard, every live + // session-list refresh (for example tool-call updates from a running selected + // session) reopens a path the user just collapsed. useEffect(() => { - if (!selectedSessionId) return - setCollapseOverrides(prev => { - const group = allGroups.find(g => - g.sessions.some(s => s.id === selectedSessionId) - ) - if (!group) return prev - return expandSelectedSessionCollapseOverrides(prev, group) - }) + if (!selectedSessionId) { + autoExpandedSelectedSessionKeyRef.current = null + return + } + + const group = allGroups.find(g => + g.sessions.some(s => s.id === selectedSessionId) + ) + if (!group) return + + const autoExpandKey = `${selectedSessionId}::${group.key}` + if (autoExpandedSelectedSessionKeyRef.current === autoExpandKey) return + autoExpandedSelectedSessionKeyRef.current = autoExpandKey + + setCollapseOverrides(prev => expandSelectedSessionCollapseOverrides(prev, group)) }, [selectedSessionId, allGroups]) // Clean up stale collapse overrides