Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 25 additions & 6 deletions apps/desktop/src/app/session/hooks/use-session-list-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,56 +76,75 @@ 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, {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This fetch is now profile-specific, but it has no generation/current-scope guard. refreshSessions can invoke an old closure after awaiting its own request (use-session-list-actions.ts:191-193), so a late previous-profile response can overwrite the new profile's messaging rows. Please discard stale responses here and in the analogous cron/pagination paths.

excludeSources: MESSAGING_EXCLUDED_SOURCES
})

// Drop any non-messaging source the broad exclude didn't catch (custom
// 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".
setMessagingTruncated(result.sessions.length >= MESSAGING_SECTION_LIMIT)
} 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)

Expand All @@ -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
Expand Down
Loading