mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
fix(web): respect manual session path collapse (#656)
This commit is contained in:
@@ -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 (
|
||||
<QueryClientProvider client={new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false },
|
||||
mutations: { retry: false },
|
||||
}
|
||||
})}>
|
||||
<I18nProvider>
|
||||
<SessionList
|
||||
sessions={sessions}
|
||||
selectedSessionId={selectedSessionId}
|
||||
onSelect={vi.fn()}
|
||||
onNewSession={vi.fn()}
|
||||
onRefresh={vi.fn()}
|
||||
isLoading={false}
|
||||
renderHeader={false}
|
||||
api={null}
|
||||
/>
|
||||
</I18nProvider>
|
||||
</QueryClientProvider>
|
||||
)
|
||||
}
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -739,6 +739,7 @@ export function SessionList(props: {
|
||||
const [collapseOverrides, setCollapseOverrides] = useState<Map<string, boolean>>(
|
||||
() => new Map()
|
||||
)
|
||||
const autoExpandedSelectedSessionKeyRef = useRef<string | null>(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
|
||||
|
||||
Reference in New Issue
Block a user