From a86597eae473c9fb79b180c3052fc3abcae61c69 Mon Sep 17 00:00:00 2001 From: Thomas Petersen Date: Thu, 7 May 2026 07:52:44 -0400 Subject: [PATCH 1/5] fix(desktop): remove left sidebar divider Co-authored-by: Cursor --- desktop/src/features/sidebar/ui/AppSidebar.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/desktop/src/features/sidebar/ui/AppSidebar.tsx b/desktop/src/features/sidebar/ui/AppSidebar.tsx index 8bbb15c7a..770a1a3d0 100644 --- a/desktop/src/features/sidebar/ui/AppSidebar.tsx +++ b/desktop/src/features/sidebar/ui/AppSidebar.tsx @@ -40,7 +40,6 @@ import { SidebarMenuButton, SidebarMenuItem, SidebarMenuSkeleton, - SidebarSeparator, } from "@/shared/ui/sidebar"; // --------------------------------------------------------------------------- @@ -438,8 +437,6 @@ export function AppSidebar({ - - {isLoading ? ( From b0d92f0f74a8c21033e7ca9a8a093634bfcfb0db Mon Sep 17 00:00:00 2001 From: Thomas Petersen Date: Thu, 7 May 2026 08:13:44 -0400 Subject: [PATCH 2/5] fix(desktop): tighten sidebar item spacing Co-authored-by: Cursor --- desktop/src/shared/ui/sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/desktop/src/shared/ui/sidebar.tsx b/desktop/src/shared/ui/sidebar.tsx index b362f466a..4d2bf71c6 100644 --- a/desktop/src/shared/ui/sidebar.tsx +++ b/desktop/src/shared/ui/sidebar.tsx @@ -500,7 +500,7 @@ const SidebarMenu = React.forwardRef<
    )); From 95f8e190a77bcd1437ad43a00281fba9916fb44e Mon Sep 17 00:00:00 2001 From: Thomas Petersen Date: Thu, 7 May 2026 10:13:47 -0400 Subject: [PATCH 3/5] fix(desktop): polish collapsible sidebar groups Co-authored-by: Cursor --- .../src/features/sidebar/ui/AppSidebar.tsx | 136 ++++++++++++++---- .../features/sidebar/ui/SidebarSection.tsx | 133 +++++++++++------ 2 files changed, 195 insertions(+), 74 deletions(-) diff --git a/desktop/src/features/sidebar/ui/AppSidebar.tsx b/desktop/src/features/sidebar/ui/AppSidebar.tsx index 770a1a3d0..f0ea86dc9 100644 --- a/desktop/src/features/sidebar/ui/AppSidebar.tsx +++ b/desktop/src/features/sidebar/ui/AppSidebar.tsx @@ -1,5 +1,14 @@ // biome-ignore format: keep compact to stay within file size limit -import { Activity, Bot, Home, PenSquare, Plus, Search, Zap } from "lucide-react"; +import { + Activity, + Bot, + ChevronDown, + Home, + PenSquare, + Plus, + Search, + Zap, +} from "lucide-react"; import * as React from "react"; import { useManagedAgentsQuery } from "@/features/agents/hooks"; @@ -25,6 +34,7 @@ import type { Profile, UserStatus, } from "@/shared/api/types"; +import { cn } from "@/shared/lib/cn"; import { Button } from "@/shared/ui/button"; import { Sidebar, @@ -48,6 +58,14 @@ import { const SECTION_ICON_BUTTON_CLASS = "flex h-5 w-5 items-center justify-center rounded-md text-sidebar-foreground/50 hover:bg-sidebar-accent/60 hover:text-sidebar-foreground"; +const SECTION_ACTION_VISIBILITY_CLASS = + "pointer-events-none opacity-0 transition-opacity group-hover/sidebar-section:pointer-events-auto group-hover/sidebar-section:opacity-100 group-focus-within/sidebar-section:pointer-events-auto group-focus-within/sidebar-section:opacity-100"; +const SECTION_LABEL_BUTTON_CLASS = + "group/section-label flex w-full cursor-pointer appearance-none items-center gap-1 pr-12 text-left transition-colors hover:text-sidebar-foreground focus-visible:text-sidebar-foreground"; +const SECTION_LABEL_CHEVRON_CLASS = + "h-2.5 w-2.5 shrink-0 opacity-0 text-sidebar-foreground/45 transition-[color,opacity,transform] group-hover/section-label:opacity-100 group-hover/section-label:text-sidebar-foreground group-focus-visible/section-label:opacity-100 group-focus-visible/section-label:text-sidebar-foreground"; + +type CollapsibleSidebarGroup = "channels" | "forums" | "directMessages"; // --------------------------------------------------------------------------- // Types @@ -121,18 +139,25 @@ type AppSidebarProps = { function SectionHeaderActions({ browseAriaLabel, browseTestId, + className, createAriaLabel, onBrowse, onCreateClick, }: { browseAriaLabel: string; browseTestId?: string; + className?: string; createAriaLabel: string; onBrowse: () => void; onCreateClick: () => void; }) { return ( -
    +
    + + +
    + {!isCollapsed ? ( + + {items.length > 0 ? ( + + {items.map((channel) => ( + + + + ))} + + ) : null} + + ) : null} ); } @@ -273,6 +328,23 @@ export function AppSidebar({ const [profilePopoverOpen, setProfilePopoverOpen] = React.useState(false); const [createDialogKind, setCreateDialogKind] = React.useState(null); + const [collapsedGroups, setCollapsedGroups] = React.useState< + Record + >({ + channels: false, + forums: false, + directMessages: false, + }); + + const toggleCollapsedGroup = React.useCallback( + (group: CollapsibleSidebarGroup) => { + setCollapsedGroups((current) => ({ + ...current, + [group]: !current[group], + })); + }, + [], + ); const streamChannels = React.useMemo( () => channels.filter((channel) => channel.channelType === "stream"), @@ -341,6 +413,7 @@ export function AppSidebar({ return ( setCreateDialogKind("stream")} onSelectChannel={onSelectChannel} + onToggleCollapsed={() => toggleCollapsedGroup("channels")} selectedChannelId={selectedChannelId} title="Channels" unreadChannelIds={unreadChannelIds} @@ -472,12 +547,14 @@ export function AppSidebar({ browseAriaLabel="Browse forums" browseTestId="browse-forums" createAriaLabel="Create a forum" + isCollapsed={collapsedGroups.forums} isActiveChannel={selectedView === "channel"} items={forumChannels} listTestId="forum-list" onBrowse={onOpenBrowseForums} onCreateClick={() => setCreateDialogKind("forum")} onSelectChannel={onSelectChannel} + onToggleCollapsed={() => toggleCollapsedGroup("forums")} selectedChannelId={selectedChannelId} title="Forums" unreadChannelIds={unreadChannelIds} @@ -487,7 +564,10 @@ export function AppSidebar({ { setIsNewDmOpen(true); @@ -498,11 +578,13 @@ export function AppSidebar({ } dmParticipantsByChannelId={dmParticipantsByChannelId} + isCollapsed={collapsedGroups.directMessages} isActiveChannel={selectedView === "channel"} items={directMessages} channelLabels={dmChannelLabels} onHideDm={onHideDm} onSelectChannel={onSelectChannel} + onToggleCollapsed={() => toggleCollapsedGroup("directMessages")} presenceByChannelId={dmPresenceByChannelId} selectedChannelId={selectedChannelId} testId="dm-list" diff --git a/desktop/src/features/sidebar/ui/SidebarSection.tsx b/desktop/src/features/sidebar/ui/SidebarSection.tsx index ce920195a..07fc861be 100644 --- a/desktop/src/features/sidebar/ui/SidebarSection.tsx +++ b/desktop/src/features/sidebar/ui/SidebarSection.tsx @@ -1,5 +1,5 @@ import type * as React from "react"; -import { CircleDot, FileText, Hash, Lock, X } from "lucide-react"; +import { ChevronDown, CircleDot, FileText, Hash, Lock, X } from "lucide-react"; import { getEphemeralChannelDisplay } from "@/features/channels/lib/ephemeralChannel"; import { EphemeralChannelBadge } from "@/features/channels/ui/EphemeralChannelBadge"; @@ -18,6 +18,11 @@ import { import { PresenceDot } from "@/features/presence/ui/PresenceBadge"; +const SECTION_LABEL_BUTTON_CLASS = + "group/section-label flex w-full cursor-pointer appearance-none items-center gap-1 pr-12 text-left transition-colors hover:text-sidebar-foreground focus-visible:text-sidebar-foreground"; +const SECTION_LABEL_CHEVRON_CLASS = + "h-2.5 w-2.5 shrink-0 opacity-0 text-sidebar-foreground/45 transition-[color,opacity,transform] group-hover/section-label:opacity-100 group-hover/section-label:text-sidebar-foreground group-focus-visible/section-label:opacity-100 group-focus-visible/section-label:text-sidebar-foreground"; + export type SidebarDmParticipant = { avatarUrl: string | null; label: string; @@ -187,6 +192,7 @@ export function SidebarSection({ emptyState, items, channelLabels, + isCollapsed, isActiveChannel, presenceByChannelId, selectedChannelId, @@ -195,12 +201,14 @@ export function SidebarSection({ unreadChannelIds, onHideDm, onSelectChannel, + onToggleCollapsed, }: { action?: React.ReactNode; dmParticipantsByChannelId?: Record; emptyState?: React.ReactNode; items: Channel[]; channelLabels?: Record; + isCollapsed?: boolean; isActiveChannel: boolean; presenceByChannelId?: Record; selectedChannelId: string | null; @@ -209,60 +217,91 @@ export function SidebarSection({ unreadChannelIds: Set; onHideDm?: (channelId: string) => void; onSelectChannel: (channelId: string) => void; + onToggleCollapsed?: () => void; }) { if (items.length === 0 && !action && !emptyState) { return null; } + const contentId = `sidebar-${testId}`; + const canToggle = Boolean(onToggleCollapsed); + return ( - {title} - {action} - - {items.length > 0 ? ( - - {items.map((channel) => ( - - - {channel.channelType === "dm" && - unreadChannelIds.has(channel.id) && - !(isActiveChannel && selectedChannelId === channel.id) ? ( -
    + {!isCollapsed ? ( + + {items.length > 0 ? ( + + {items.map((channel) => ( + + - ) : null} - {channel.channelType === "dm" && onHideDm ? ( - onHideDm(channel.id)} - showOnHover - > - - - ) : null} - - ))} - - ) : emptyState ? ( -
    - {emptyState} -
    - ) : null} -
    + {channel.channelType === "dm" && + unreadChannelIds.has(channel.id) && + !(isActiveChannel && selectedChannelId === channel.id) ? ( +