fix(desktop): keep session refresh usable

Fixes #893.

Reproduced the stuck session-list refresh by keeping the automatic fetch promise pending, then clicking the manual refresh button. Also covered the existing-sessions loading state so a background refresh does not leave the control disabled or spinning forever.

Tested: cd desktop && bun run test -- src/components/layout/Sidebar.test.tsx --run

Tested: bun run check:desktop

Confidence: high

Scope-risk: narrow
This commit is contained in:
程序员阿江(Relakkes) 2026-06-23 00:17:18 +08:00
parent 90cbe9f62c
commit 1f7b1e68c4
2 changed files with 32 additions and 3 deletions

View File

@ -293,6 +293,35 @@ describe('Sidebar', () => {
expect(screen.getByRole('button', { name: 'Collapse display' })).toBeInTheDocument()
})
it('lets a manual session refresh supersede a stuck automatic refresh', async () => {
fetchSessions.mockReturnValue(new Promise(() => {}))
render(<Sidebar />)
await waitFor(() => expect(fetchSessions).toHaveBeenCalledTimes(1))
fireEvent.click(screen.getByRole('button', { name: 'Refresh sessions' }))
await waitFor(() => expect(fetchSessions).toHaveBeenCalledTimes(2))
})
it('keeps the session refresh control usable when a background refresh is still loading existing sessions', async () => {
useSessionStore.setState({
sessions: [
makeSession('session-loaded', 'Loaded session', '/workspace/alpha', '2026-05-15T10:00:00.000Z'),
],
isLoading: true,
})
render(<Sidebar />)
const refreshButton = screen.getByRole('button', { name: 'Refresh sessions' })
expect(refreshButton).not.toBeDisabled()
expect(refreshButton.querySelector('svg')).not.toHaveClass('animate-spin')
fireEvent.click(refreshButton)
await waitFor(() => expect(fetchSessions).toHaveBeenCalled())
})
it('reorders project groups by dragging project headers while preserving expanded state', async () => {
const base = new Date('2026-05-15T10:00:00.000Z').getTime()
useSessionStore.setState({

View File

@ -127,6 +127,7 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) {
))
}, [hiddenProjectKeys, orderedProjectGroups])
const showInitialLoading = isLoading && sessions.length === 0
const showRefreshLoading = showInitialLoading
const filteredSessionIds = useMemo(() => filteredSessions.map((session) => session.id), [filteredSessions])
const selectedCount = selectedSessionIds.size
const sessionsById = useMemo(
@ -709,12 +710,11 @@ export function Sidebar({ isMobile = false, onRequestClose }: SidebarProps) {
<button
type="button"
onClick={() => void refreshSessionsNow()}
disabled={isLoading}
className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-[12px] border border-[var(--color-sidebar-search-border)] bg-[var(--color-sidebar-search-bg)] text-[var(--color-text-secondary)] transition-colors hover:bg-[var(--color-sidebar-item-hover)] hover:text-[var(--color-text-primary)] disabled:cursor-default disabled:opacity-65 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-border-focus)]"
aria-label={t('sidebar.refreshSessions')}
title={t('sidebar.refreshSessions')}
>
<RefreshCw className={`h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} strokeWidth={1.9} aria-hidden="true" />
<RefreshCw className={`h-4 w-4 ${showRefreshLoading ? 'animate-spin' : ''}`} strokeWidth={1.9} aria-hidden="true" />
</button>
<button
type="button"
@ -1213,7 +1213,7 @@ function useSessionListAutoRefresh(fetchSessions: () => Promise<void>): () => Pr
const lastStartedAtRef = useRef(0)
const refreshSessions = useCallback((force = false) => {
if (inFlightRef.current) return inFlightRef.current
if (inFlightRef.current && !force) return inFlightRef.current
const now = Date.now()
if (!force && now - lastStartedAtRef.current < SESSION_LIST_FOCUS_REFRESH_MIN_MS) {