diff --git a/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx b/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx index f90e9d31..377c37e5 100644 --- a/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx +++ b/packages/ui/src/components/session/sidebar/SessionNodeItem.tsx @@ -130,6 +130,30 @@ const treeContainsSessionId = (node: SessionNode, sessionId: string | null): boo return false; }; +const treeContainsMenuKey = ( + node: SessionNode, + menuKey: string | null, + renderContext: 'project' | 'recent', + archivedBucket: boolean, +): boolean => { + if (!menuKey) { + return false; + } + + const nodeMenuKey = `${renderContext}:${archivedBucket ? 'archived' : 'active'}:${node.session.id}`; + if (nodeMenuKey === menuKey) { + return true; + } + + for (const child of node.children) { + if (treeContainsMenuKey(child, menuKey, renderContext, archivedBucket)) { + return true; + } + } + + return false; +}; + const areEqual = (prev: Props, next: Props): boolean => { const prevSession = prev.node.session; const nextSession = next.node.session; @@ -159,9 +183,9 @@ const areEqual = (prev: Props, next: Props): boolean => { if (prev.editTitle !== next.editTitle && ((prev.editingId === prevSessionId) || (next.editingId === nextSessionId))) return false; if ((prev.copiedSessionId === prevSessionId) !== (next.copiedSessionId === nextSessionId)) return false; - const prevMenuKey = `${prev.renderContext ?? 'project'}:${prev.archivedBucket ? 'archived' : 'active'}:${prevSessionId}`; - const nextMenuKey = `${next.renderContext ?? 'project'}:${next.archivedBucket ? 'archived' : 'active'}:${nextSessionId}`; - if ((prev.openSidebarMenuKey === prevMenuKey) !== (next.openSidebarMenuKey === nextMenuKey)) return false; + const prevMenuInTree = treeContainsMenuKey(prev.node, prev.openSidebarMenuKey, prev.renderContext ?? 'project', prev.archivedBucket ?? false); + const nextMenuInTree = treeContainsMenuKey(next.node, next.openSidebarMenuKey, next.renderContext ?? 'project', next.archivedBucket ?? false); + if (prevMenuInTree !== nextMenuInTree) return false; const prevDirectory = normalizePath((prevSession as Session & { directory?: string | null }).directory ?? null) ?? normalizePath(prev.groupDirectory ?? null); @@ -426,6 +450,18 @@ function SessionNodeItemComponent(props: Props): React.ReactNode { }; const handleMenuTriggerClick = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + setOpenSidebarMenuKey(isMenuOpen ? null : menuInstanceKey); + }; + + const handleMenuTriggerPointerDown = (event: React.PointerEvent) => { + event.preventDefault(); + event.stopPropagation(); + }; + + const handleMenuTriggerMouseDown = (event: React.MouseEvent) => { + event.preventDefault(); event.stopPropagation(); }; @@ -568,7 +604,7 @@ function SessionNodeItemComponent(props: Props): React.ReactNode { 'flex min-w-0 flex-1 cursor-pointer flex-col gap-0 overflow-hidden rounded-sm text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/50 text-foreground select-none disabled:cursor-not-allowed transition-[padding]', mobileVariant ? (isVSCode ? revealPaddingClass : 'pr-7') - : '', + : revealPaddingClass, )} >
@@ -585,25 +621,6 @@ function SessionNodeItemComponent(props: Props): React.ReactNode { )}> {sessionCompactUpdatedLabel} - - - - - {sessionMenuContent} -
) : null} {pendingPermissionCount > 0 ? ( @@ -683,41 +700,38 @@ function SessionNodeItemComponent(props: Props): React.ReactNode { ) : null} - {!isMinimalMode || mobileVariant ? ( -
- - - - - {sessionMenuContent} - -
- ) : null} +
+ + + + + {sessionMenuContent} + +
{hasChildren && isExpanded diff --git a/packages/ui/src/components/session/sidebar/sortableItems.tsx b/packages/ui/src/components/session/sidebar/sortableItems.tsx index 46f6c383..d4fff7eb 100644 --- a/packages/ui/src/components/session/sidebar/sortableItems.tsx +++ b/packages/ui/src/components/session/sidebar/sortableItems.tsx @@ -118,6 +118,14 @@ export const SortableProjectItem: React.FC = ({ event.stopPropagation(); }, []); + const handleMenuTriggerPointerDown = React.useCallback((event: React.PointerEvent) => { + event.stopPropagation(); + }, []); + + const handleMenuTriggerMouseDown = React.useCallback((event: React.MouseEvent) => { + event.stopPropagation(); + }, []); + const handleToggleMouseDown = React.useCallback((event: React.MouseEvent) => { if (event.button === 2 || (event.button === 0 && event.ctrlKey)) { suppressNextToggleRef.current = true; @@ -250,6 +258,8 @@ export const SortableProjectItem: React.FC = ({ : 'opacity-0 pointer-events-none group-hover/project:opacity-100 group-hover/project:pointer-events-auto group-focus-within/project:opacity-100 group-focus-within/project:pointer-events-auto', )} aria-label="Project menu" + onPointerDown={handleMenuTriggerPointerDown} + onMouseDown={handleMenuTriggerMouseDown} onClick={handleMenuTriggerClick} >