fix(threads): preserve selectedThreadId on cold-boot identity hydration (#1196)

This commit is contained in:
Steven Enamakel
2026-05-04 15:20:16 -07:00
committed by GitHub
parent 229e374025
commit 477bb63ef3
3 changed files with 39 additions and 4 deletions
+7 -2
View File
@@ -28,7 +28,7 @@ import {
import { socketService } from '../services/socketService';
import { store } from '../store';
import { resetUserScopedState } from '../store/resetActions';
import { clearAllThreads, loadThreads } from '../store/threadSlice';
import { loadThreads, resetThreadCachesPreservingSelection } from '../store/threadSlice';
import { getActiveUserId, setActiveUserId } from '../store/userScopedStorage';
import {
openhumanUpdateAnalyticsSettings,
@@ -257,7 +257,12 @@ export default function CoreStateProvider({ children }: { children: ReactNode })
!isLogout
) {
const threadReloadRequestId = requestId;
store.dispatch(clearAllThreads());
// Reset the in-memory thread caches (rows from a pre-auth bucket — see
// #1157) but preserve the redux-persisted `selectedThreadId` so a
// reload of an already-authed user resumes the user's last-viewed
// thread (#1168). The Conversations mount effect falls back to "most
// recent" if the persisted id is no longer in the reloaded list.
store.dispatch(resetThreadCachesPreservingSelection());
void store
.dispatch(loadThreads())
.unwrap()
@@ -153,9 +153,13 @@ describe('CoreStateProvider — identity flip cleanup (#900)', () => {
setActiveSpy.mockRestore();
});
it('seed matches next user (no restart) but identity hydrates null→B: clears threads and reloads (#1157)', async () => {
it('seed matches next user (no restart) but identity hydrates null→B: resets thread caches (preserving selection) and reloads (#1157, #1168)', async () => {
userScopedStorage.setActiveUserId('B');
fetchSnapshot.mockResolvedValue(makeSnapshot({ userId: 'B', sessionToken: 'tokB' }));
// Seed a persisted selection that should survive the boot-time reset so
// the user resumes their last-viewed thread instead of falling through
// to "most recent".
store.dispatch(threadSlice.setSelectedThread('thr-persisted'));
const dispatchSpy = vi.spyOn(store, 'dispatch');
const loadThreadsSpy = vi.spyOn(threadSlice, 'loadThreads');
@@ -173,10 +177,22 @@ describe('CoreStateProvider — identity flip cleanup (#900)', () => {
expect(
dispatchSpy.mock.calls.some(([action]) => {
if (!action || typeof action !== 'object' || !('type' in action)) return false;
return (action as { type: string }).type === threadSlice.clearAllThreads().type;
return (
(action as { type: string }).type ===
threadSlice.resetThreadCachesPreservingSelection().type
);
})
).toBe(true);
// The legacy `clearAllThreads` (which nulls selectedThreadId) must NOT
// be dispatched on this boot path — that was the #1168 regression.
expect(
dispatchSpy.mock.calls.some(([action]) => {
if (!action || typeof action !== 'object' || !('type' in action)) return false;
return (action as { type: string }).type === threadSlice.clearAllThreads().type;
})
).toBe(false);
expect(loadThreadsSpy).toHaveBeenCalled();
expect(store.getState().thread.selectedThreadId).toBe('thr-persisted');
dispatchSpy.mockRestore();
loadThreadsSpy.mockRestore();
+14
View File
@@ -279,6 +279,19 @@ const threadSlice = createSlice({
state.activeThreadId = null;
state.welcomeThreadId = null;
},
// Like `clearAllThreads` but keeps `selectedThreadId`. Used on the
// post-bootstrap identity-observation path (#1168 + #1157): we need to
// drop pre-auth in-memory thread rows but the persisted last-viewed
// thread id is still valid for the reloaded user, so preserving it lets
// the Conversations effect resume that thread instead of falling
// through to "most recent".
resetThreadCachesPreservingSelection: state => {
state.threads = [];
state.messagesByThreadId = {};
state.messages = [];
state.activeThreadId = null;
state.welcomeThreadId = null;
},
// [#1123] True no-op — welcome-agent onboarding replaced by Joyride walkthrough.
// Kept to avoid breaking existing imports; state.welcomeThreadId is never
// mutated because the welcome-agent flow no longer runs.
@@ -350,6 +363,7 @@ export const {
clearSelectedThread,
setActiveThread,
clearAllThreads,
resetThreadCachesPreservingSelection,
setWelcomeThreadId,
} = threadSlice.actions;