From 60a4110d7f48423a92a2f032a789bde9461dc9e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Ng=C3=B4=20Th=C6=B0=E1=BB=A3ng?= <83950837+nguyenngothuong@users.noreply.github.com> Date: Fri, 20 Mar 2026 17:35:06 +0700 Subject: [PATCH] fix(sidebar): show sessions in both Recent and Project sections (#715) * fix(sidebar): show sessions in both Recent and Project sections Previously, sessions displayed in the Recent section were filtered out of their Project section (dedup logic). This meant users could only see a session in one place, making it hard to find sessions within their project context. Now sessions appear in both: - Recent section: with project name tag for quick identification - Project section: in their normal grouped position The project tag (secondaryMeta.projectLabel) was already implemented in SessionNodeItem but never visible because the dedup logic removed sessions from project sections before metadata could be populated. Co-investigated-by: @huylamnguyen * feat(sidebar): add collapse/expand all projects toggle When many projects are open, collapsing them one by one is tedious. Add 'Collapse all' and 'Expand all' options to the sidebar display mode dropdown menu (gear icon). - Add collapseAllProjects/expandAllProjects callbacks in SessionSidebar - Pass to SidebarHeader and render in the existing dropdown menu - Persist collapsed state to localStorage and server settings - Uses RiContractUpDownLine/RiExpandUpDownLine icons from Remixicon Co-investigated-by: @huylamnguyen * refactor: simplify session sidebar section rendering Remove redundant filtered section aliases in SessionSidebar Pass project and render sections directly for clearer code --------- Co-authored-by: Bohdan Triapitsyn --- .../src/components/session/SessionSidebar.tsx | 81 ++++++++----------- .../session/sidebar/SidebarHeader.tsx | 16 ++++ 2 files changed, 48 insertions(+), 49 deletions(-) diff --git a/packages/ui/src/components/session/SessionSidebar.tsx b/packages/ui/src/components/session/SessionSidebar.tsx index 2dde7c94..726d7db2 100644 --- a/packages/ui/src/components/session/SessionSidebar.tsx +++ b/packages/ui/src/components/session/SessionSidebar.tsx @@ -743,6 +743,34 @@ export const SessionSidebar: React.FC = ({ }); }, []); + const collapseAllProjects = React.useCallback(() => { + ignoreIntersectionUntil.current = Date.now() + 150; + setCollapsedProjects(() => { + const allIds = new Set(projects.map((p) => p.id)); + try { + safeStorage.setItem(PROJECT_COLLAPSE_STORAGE_KEY, JSON.stringify(Array.from(allIds))); + } catch { /* ignored */ } + if (!isVSCode) { + scheduleCollapsedProjectsPersist(allIds); + } + return allIds; + }); + }, [projects, isVSCode, safeStorage, scheduleCollapsedProjectsPersist]); + + const expandAllProjects = React.useCallback(() => { + ignoreIntersectionUntil.current = Date.now() + 150; + setCollapsedProjects(() => { + const empty = new Set(); + try { + safeStorage.setItem(PROJECT_COLLAPSE_STORAGE_KEY, JSON.stringify([])); + } catch { /* ignored */ } + if (!isVSCode) { + scheduleCollapsedProjectsPersist(empty); + } + return empty; + }); + }, [isVSCode, safeStorage, scheduleCollapsedProjectsPersist]); + const toggleProject = React.useCallback((projectId: string) => { // Ignore intersection events for a short period after toggling ignoreIntersectionUntil.current = Date.now() + 150; @@ -996,53 +1024,6 @@ export const SessionSidebar: React.FC = ({ ]; }, [activeNowSessions, sessionSidebarMetaById]); - const activitySessionIds = React.useMemo(() => { - const next = new Set(); - activitySections.forEach((section) => { - section.items.forEach((item) => { - next.add(item.node.session.id); - }); - }); - return next; - }, [activitySections]); - - const filteredProjectSections = React.useMemo(() => { - if (hasSessionSearchQuery || activitySessionIds.size === 0) { - return projectSections; - } - - const filterNodes = (nodes: SessionNode[]): SessionNode[] => { - return nodes.flatMap((node) => { - if (activitySessionIds.has(node.session.id)) { - return []; - } - return [{ - ...node, - children: filterNodes(node.children), - }]; - }); - }; - - return projectSections.map((section) => ({ - ...section, - groups: section.groups.map((group) => ({ - ...group, - sessions: filterNodes(group.sessions), - })), - })); - }, [hasSessionSearchQuery, activitySessionIds, projectSections]); - - const filteredSectionsForRender = React.useMemo(() => { - if (hasSessionSearchQuery || activitySessionIds.size === 0) { - return sectionsForRender; - } - - const sectionsByProjectId = new Map(filteredProjectSections.map((section) => [section.project.id, section])); - return sectionsForRender - .map((section) => sectionsByProjectId.get(section.project.id) ?? section) - .filter(Boolean); - }, [hasSessionSearchQuery, activitySessionIds, filteredProjectSections, sectionsForRender]); - const desktopHeaderActionButtonClass = 'inline-flex h-6 w-6 cursor-pointer items-center justify-center rounded-md leading-none text-foreground hover:bg-interactive-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 disabled:cursor-not-allowed'; const mobileHeaderActionButtonClass = @@ -1352,12 +1333,14 @@ export const SessionSidebar: React.FC = ({ setSessionSearchQuery={setSessionSearchQuery} hasSessionSearchQuery={hasSessionSearchQuery} searchMatchCount={searchMatchCount} + collapseAllProjects={collapseAllProjects} + expandAllProjects={expandAllProjects} /> void; hasSessionSearchQuery: boolean; searchMatchCount: number; + collapseAllProjects: () => void; + expandAllProjects: () => void; }; export function SidebarHeader(props: Props): React.ReactNode { @@ -47,6 +52,8 @@ export function SidebarHeader(props: Props): React.ReactNode { setSessionSearchQuery, hasSessionSearchQuery, searchMatchCount, + collapseAllProjects, + expandAllProjects, } = props; const displayMode = useSessionDisplayStore((state) => state.displayMode); @@ -136,6 +143,15 @@ export function SidebarHeader(props: Props): React.ReactNode { Minimal {displayMode === 'minimal' ? : null} + + + + Collapse all + + + + Expand all +