diff --git a/apps/desktop/src/app/chat/sidebar/index.tsx b/apps/desktop/src/app/chat/sidebar/index.tsx index 6930c5343d..d36903a5ee 100644 --- a/apps/desktop/src/app/chat/sidebar/index.tsx +++ b/apps/desktop/src/app/chat/sidebar/index.tsx @@ -467,16 +467,19 @@ export function ChatSidebar({ // Profile scope = the "workspace switcher" context. Concrete scope shows only // that profile's sessions (clean rows, no per-row tags); ALL fans every - // profile in, grouped by profile below. Single-profile users land here with - // scope === their only profile, so nothing is filtered out. + // profile in. Grouped rendering stays gated on `showAllProfiles` (multi-profile + // + ALL) so a single-profile user is never stranded in a grouped view with no + // rail — but the *data* still has to fan in when the persisted scope is ALL + // (Grouping → Profile). Filtering that pool against the `__all__` sentinel + // matches nothing and empties recents + pins. // Archived rows are excluded from the sessions query, so Archived is a view of // its own set rather than a filter over this one — a flat list of archived // rows, no project tree, no date or status dividers. const scopedSessions = useMemo(() => { const pool = showArchived ? archivedSessions : sessions - return showAllProfiles ? pool : pool.filter(s => normalizeProfileKey(s.profile) === profileScope) - }, [sessions, archivedSessions, showArchived, showAllProfiles, profileScope]) + return filterSessionsByProfileScope(pool, profileScope) + }, [sessions, archivedSessions, showArchived, profileScope]) // One predicate for the status/project filters, so the flat list and the // project lanes narrow by the same rule. A project lane holds rows the loaded diff --git a/apps/desktop/src/app/chat/sidebar/profile-scope.test.ts b/apps/desktop/src/app/chat/sidebar/profile-scope.test.ts index 14e73edb61..68585af986 100644 --- a/apps/desktop/src/app/chat/sidebar/profile-scope.test.ts +++ b/apps/desktop/src/app/chat/sidebar/profile-scope.test.ts @@ -27,4 +27,12 @@ describe('filterSessionsByProfileScope', () => { expect(filterSessionsByProfileScope(rows, ALL_PROFILES)).toBe(rows) }) + + it('does not empty ALL scope when every row is one profile', () => { + // Grouping → Profile persists ALL even with one profile. Filtering + // against the `__all__` sentinel would empty recents and pins. + const rows = [row('a', 'default'), row('b', 'default'), row('c', 'default')] + + expect(filterSessionsByProfileScope(rows, ALL_PROFILES)).toBe(rows) + }) }) diff --git a/apps/desktop/src/app/chat/sidebar/profile-scope.ts b/apps/desktop/src/app/chat/sidebar/profile-scope.ts index 45686d8252..d585b14c9b 100644 --- a/apps/desktop/src/app/chat/sidebar/profile-scope.ts +++ b/apps/desktop/src/app/chat/sidebar/profile-scope.ts @@ -1,7 +1,13 @@ import { ALL_PROFILES, normalizeProfileKey } from '@/store/profile' import type { SessionInfo } from '@/types/hermes' -/** Return the sessions visible in one sidebar profile scope, or the original unified list for All profiles. */ +/** + * Sessions visible in one sidebar profile scope. + * + * ALL (`__all__`) returns the caller's list unchanged — including the + * single-profile case, where Grouping → Profile persists that sentinel + * while grouped rendering stays off. Never filter against `__all__`. + */ export function filterSessionsByProfileScope(sessions: SessionInfo[], profileScope: string): SessionInfo[] { if (profileScope === ALL_PROFILES) { return sessions