diff --git a/src/hooks/useProjectsState.restore.dom.bun.test.tsx b/src/hooks/useProjectsState.restore.dom.bun.test.tsx index 0563c67..98cc97e 100644 --- a/src/hooks/useProjectsState.restore.dom.bun.test.tsx +++ b/src/hooks/useProjectsState.restore.dom.bun.test.tsx @@ -3,7 +3,7 @@ import { afterEach, beforeEach, test } from 'node:test'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { act, cleanup, render, waitFor } from '@testing-library/react'; -import { createElement } from 'react'; +import { createElement, useState } from 'react'; import type { Project } from '../types/app'; import { resetAppShellStore } from '../stores/useAppShellStore'; @@ -37,10 +37,13 @@ type HookState = ReturnType; /** One page load: a fresh query cache and shell store, the same localStorage. */ const mountApp = (route: { sessionId?: string | null } = {}) => { let state: HookState | null = null; + let setRouteSessionId: ((sessionId: string | null) => void) | null = null; const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, retry: false } } }); const Harness = () => { + const [sessionId, setSessionId] = useState(route.sessionId ?? null); + setRouteSessionId = setSessionId; state = useProjectsState({ - sessionId: route.sessionId ?? null, + sessionId, navigate: (() => undefined) as never, subscribe: () => () => undefined, isMobile: false, @@ -54,6 +57,10 @@ const mountApp = (route: { sessionId?: string | null } = {}) => { assert.ok(state, 'hook state is available after rendering'); return state; }, + setRouteSessionId: (sessionId: string | null) => { + assert.ok(setRouteSessionId, 'route state is available after rendering'); + setRouteSessionId(sessionId); + }, reload: () => { view.unmount(); resetAppShellStore(); @@ -142,3 +149,25 @@ test('a session route restores from the URL and leaves the remembered project al restore(); } }); + +test('returning to the root route clears a session rehydrated before navigation settled', async () => { + const session = { id: 'session-1', summary: 'Existing work' }; + const projectWithSession = { + ...project('project-1', 'Project one'), + sessions: [session], + sessionMeta: { hasMore: false, total: 1 }, + }; + const restore = serveProjects([projectWithSession, twoProjects[1]]); + try { + const app = mountApp({ sessionId: session.id }); + await waitFor(() => assert.equal(app.getState().selectedSession?.id, session.id)); + + act(() => { app.getState().handleNewSession(projectWithSession); }); + await waitFor(() => assert.equal(app.getState().selectedSession?.id, session.id)); + + act(() => { app.setRouteSessionId(null); }); + await waitFor(() => assert.equal(app.getState().selectedSession, null)); + } finally { + restore(); + } +}); diff --git a/src/hooks/useProjectsState.ts b/src/hooks/useProjectsState.ts index 043c608..624fec1 100644 --- a/src/hooks/useProjectsState.ts +++ b/src/hooks/useProjectsState.ts @@ -228,20 +228,31 @@ export function useProjectsState({ sessionId, navigate, subscribe, isMobile, act useEffect(() => () => { if (timer.current) clearTimeout(timer.current); }, []); + const restoredFromUrlRef = useRef(null); useEffect(() => { - if (!sessionId || !projects.length) return; + if (!sessionId) { + // Only a session this effect restored from the URL is stale once the + // route settles at '/'; an optimistically registered new-chat session + // must survive there until its own /session/:id navigation lands. + if (selectedSession && restoredFromUrlRef.current === selectedSession.id) setSelectedSession(null); + restoredFromUrlRef.current = null; + return; + } + if (!projects.length) return; for (const project of projects) { const session = rowsOf(project).find((candidate) => candidate.id === sessionId); if (!session) continue; const normalized = withProvider(session); if (selectedProject?.projectId !== project.projectId) setSelectedProject(project); if (selectedSession?.id !== sessionId || selectedSession.__provider !== normalized.__provider) setSelectedSession(normalized); + restoredFromUrlRef.current = sessionId; return; } if (selectedSession?.id !== sessionId && selectedProject) { setSelectedSession({ id: sessionId, __provider: fallbackProvider, __projectId: selectedProject.projectId, summary: '' }); + restoredFromUrlRef.current = sessionId; } - }, [projects, selectedProject, selectedSession?.__provider, selectedSession?.id, sessionId, setSelectedProject, setSelectedSession]); + }, [projects, selectedProject, selectedSession, sessionId, setSelectedProject, setSelectedSession]); const handleProjectSelect = useCallback((project: Project) => { setSelectedProject(project);