diff --git a/apps/desktop/src/app/session/hooks/use-session-list-actions.ts b/apps/desktop/src/app/session/hooks/use-session-list-actions.ts index 6f971c3165be..4dfa3e0590a8 100644 --- a/apps/desktop/src/app/session/hooks/use-session-list-actions.ts +++ b/apps/desktop/src/app/session/hooks/use-session-list-actions.ts @@ -76,30 +76,44 @@ interface UseSessionListActionsArgs { * wires into the sidebar and refresh effects. */ export function useSessionListActions({ profileScope }: UseSessionListActionsArgs) { const refreshSessionsRequestRef = useRef(0) + // Per-callback request refs guard against late responses overriding + // the active profile/closure pair when a profile switch happens + // mid-flight. Mirrors the same race in ``refreshSessions`` (#60678 + // review: stale-response guard). + const cronSessionsRequestRef = useRef(0) + const messagingSessionsRequestRef = useRef(0) + const messagingPageRequestRef = useRef(0) // Cron-job sessions as their own list (latest N). Independent of the recents // page so the two never compete for slots. Cheap + bounded. Kept (even though // the sidebar now lists cron *jobs*, not run sessions) so a pinned cron run // still resolves into the Pinned section via sessionByAnyId. const refreshCronSessions = useCallback(async () => { + const requestId = cronSessionsRequestRef.current + 1 + cronSessionsRequestRef.current = requestId try { - const { sessions } = await listAllProfileSessions(CRON_SECTION_LIMIT, 1, 'exclude', 'recent', 'all', { + const sessionProfile = profileScope === ALL_PROFILES ? 'all' : profileScope + const { sessions } = await listAllProfileSessions(CRON_SECTION_LIMIT, 1, 'exclude', 'recent', sessionProfile, { source: 'cron' }) + if (cronSessionsRequestRef.current !== requestId) return setCronSessions(prev => (sameCronSignature(prev, sessions) ? prev : sessions)) } catch { // Non-fatal: the cron section just stays empty/stale. } - }, []) + }, [profileScope]) // Messaging-platform sessions as their own slice, fetched separately from // local recents so each platform renders a self-managed section and never // competes with local chats for the recents page budget. One combined fetch // seeds every platform; the sidebar splits the rows per source. const refreshMessagingSessions = useCallback(async () => { + const requestId = messagingSessionsRequestRef.current + 1 + messagingSessionsRequestRef.current = requestId try { - const result = await listAllProfileSessions(MESSAGING_SECTION_LIMIT, 1, 'exclude', 'recent', 'all', { + const sessionProfile = profileScope === ALL_PROFILES ? 'all' : profileScope + const result = await listAllProfileSessions(MESSAGING_SECTION_LIMIT, 1, 'exclude', 'recent', sessionProfile, { excludeSources: MESSAGING_EXCLUDED_SOURCES }) @@ -107,6 +121,7 @@ export function useSessionListActions({ profileScope }: UseSessionListActionsArg // sources) — those stay in local recents, not a platform section. const rows = result.sessions.filter(s => isMessagingSource(s.source)) + if (messagingSessionsRequestRef.current !== requestId) return setMessagingSessions(prev => (sameCronSignature(prev, rows) ? prev : rows)) // Hit the cap → at least one platform may have more on disk than loaded, // so platform sections offer their own per-platform "load more". @@ -114,18 +129,22 @@ export function useSessionListActions({ profileScope }: UseSessionListActionsArg } catch { // Non-fatal: the messaging sections just stay empty/stale. } - }, []) + }, [profileScope]) // Page a single platform's section independently (mirrors the per-profile // pager): fetch that source's next window and merge it back in place, leaving // every other platform's rows untouched. Resolves the platform's exact total. const loadMoreMessagingForPlatform = useCallback(async (platform: string) => { + const requestId = messagingPageRequestRef.current + 1 + messagingPageRequestRef.current = requestId const inPlatform = (s: SessionInfo) => normalizeSessionSource(s.source) === platform const loaded = $messagingSessions.get().filter(inPlatform).length - const result = await listAllProfileSessions(loaded + SIDEBAR_SESSIONS_PAGE_SIZE, 1, 'exclude', 'recent', 'all', { + const sessionProfile = profileScope === ALL_PROFILES ? 'all' : profileScope + const result = await listAllProfileSessions(loaded + SIDEBAR_SESSIONS_PAGE_SIZE, 1, 'exclude', 'recent', sessionProfile, { source: platform }) + if (messagingPageRequestRef.current !== requestId) return const incoming = result.sessions.filter(s => normalizeSessionSource(s.source) === platform) @@ -136,7 +155,7 @@ export function useSessionListActions({ profileScope }: UseSessionListActionsArg const total = result.total ?? incoming.length setMessagingPlatformTotals(prev => ({ ...prev, [platform]: Math.max(total, incoming.length) })) - }, []) + }, [profileScope]) // Cron *jobs* drive the sidebar "Cron jobs" section. Jobs are created // synchronously (agent tool call or the cron UI), so refreshing here right