From 3d0d64e90ead80dbfa04dd7f2461fd12fad771a2 Mon Sep 17 00:00:00 2001 From: snowykr Date: Fri, 4 Sep 2026 22:50:29 +0900 Subject: [PATCH 1/2] fix: clear session on root navigation --- .../useProjectsState.restore.dom.bun.test.tsx | 33 +++++++++++++++++-- src/hooks/useProjectsState.ts | 8 +++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/hooks/useProjectsState.restore.dom.bun.test.tsx b/src/hooks/useProjectsState.restore.dom.bun.test.tsx index 0563c67f..98cc97ee 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 043c608d..dd808a3a 100644 --- a/src/hooks/useProjectsState.ts +++ b/src/hooks/useProjectsState.ts @@ -229,7 +229,11 @@ export function useProjectsState({ sessionId, navigate, subscribe, isMobile, act useEffect(() => () => { if (timer.current) clearTimeout(timer.current); }, []); useEffect(() => { - if (!sessionId || !projects.length) return; + if (!sessionId) { + if (selectedSession) setSelectedSession(null); + return; + } + if (!projects.length) return; for (const project of projects) { const session = rowsOf(project).find((candidate) => candidate.id === sessionId); if (!session) continue; @@ -241,7 +245,7 @@ export function useProjectsState({ sessionId, navigate, subscribe, isMobile, act if (selectedSession?.id !== sessionId && selectedProject) { setSelectedSession({ id: sessionId, __provider: fallbackProvider, __projectId: selectedProject.projectId, summary: '' }); } - }, [projects, selectedProject, selectedSession?.__provider, selectedSession?.id, sessionId, setSelectedProject, setSelectedSession]); + }, [projects, selectedProject, selectedSession, sessionId, setSelectedProject, setSelectedSession]); const handleProjectSelect = useCallback((project: Project) => { setSelectedProject(project); From 562693ee3d55a3209c4e8356bbd7dfaa21aa3519 Mon Sep 17 00:00:00 2001 From: devswha <25837994+devswha@users.noreply.github.com> Date: Fri, 4 Sep 2026 23:01:46 +0900 Subject: [PATCH 2/2] fix: keep optimistic new-chat sessions alive at the root route Clearing every selected session when the route has no session id also wiped a just-registered optimistic session before its /session/:id navigation landed (the header rename flow the query dom suite covers). Only a session the URL-restoration effect itself restored is stale at '/', so track that id and clear just that one. --- src/hooks/useProjectsState.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/hooks/useProjectsState.ts b/src/hooks/useProjectsState.ts index dd808a3a..624fec1d 100644 --- a/src/hooks/useProjectsState.ts +++ b/src/hooks/useProjectsState.ts @@ -228,9 +228,14 @@ export function useProjectsState({ sessionId, navigate, subscribe, isMobile, act useEffect(() => () => { if (timer.current) clearTimeout(timer.current); }, []); + const restoredFromUrlRef = useRef(null); useEffect(() => { if (!sessionId) { - if (selectedSession) setSelectedSession(null); + // 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; @@ -240,10 +245,12 @@ export function useProjectsState({ sessionId, navigate, subscribe, isMobile, act 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, sessionId, setSelectedProject, setSelectedSession]);