diff --git a/packages/ui/src/components/session/sidebar/DOCUMENTATION.md b/packages/ui/src/components/session/sidebar/DOCUMENTATION.md index 5daf7f68..9a93f7ea 100644 --- a/packages/ui/src/components/session/sidebar/DOCUMENTATION.md +++ b/packages/ui/src/components/session/sidebar/DOCUMENTATION.md @@ -74,3 +74,4 @@ make every row observe unrelated streaming updates. - Empty successful lists, unresolved loads, and failed loads are separate UI states. Failed groups expose Retry and retain prior data. - Directory permission failures remain visible even when stale sessions are retained. Flat groups inspect every represented root/worktree directory; local Desktop may open the native picker for the exact failed directory, while other runtimes keep the ordinary Retry action. - Pins and folder assignments are not pruned from the first startup snapshot or from optimistic mutations. Confirmed local deletion and routed external deletion clean immediately; a later authoritative omission after an established baseline covers missed external delete events. +- Pending-permission/question row badges fade with the same hover/menu-open rule as the date label, except on always-visible-actions rows, which reserve permanent padding and keep the badges shown (`selectRowBadgeVisibilityClass` in `sessions/sessionNodeItemUtils.ts`). diff --git a/packages/ui/src/components/session/sidebar/sessions/SessionNodeItem.tsx b/packages/ui/src/components/session/sidebar/sessions/SessionNodeItem.tsx index f4fabef8..761a7cba 100644 --- a/packages/ui/src/components/session/sidebar/sessions/SessionNodeItem.tsx +++ b/packages/ui/src/components/session/sidebar/sessions/SessionNodeItem.tsx @@ -26,7 +26,7 @@ import { useGlobalSessionStatus, useSessionPermissions, useSessionQuestionCount import { useSessionMessageRecordsForExport } from '@/sync/use-sync'; import { useViewportStore, viewportSessionKey } from '@/sync/viewport-store'; import { DraggableSessionRow } from '../folders/sessionFolderDnd'; -import { canShowSessionWorktreeMenu, getSessionWorktreeMenuDisabled, nodeContainsSessionId, nodeHasPinnedMembershipChange, selectQuestionBadgeSessionScopes } from './sessionNodeItemUtils'; +import { canShowSessionWorktreeMenu, getSessionWorktreeMenuDisabled, nodeContainsSessionId, nodeHasPinnedMembershipChange, selectQuestionBadgeSessionScopes, selectRowBadgeVisibilityClass } from './sessionNodeItemUtils'; import type { SessionNode } from '../types'; import { formatProjectLabel, formatSessionCompactDateLabel, formatSessionDateLabel, normalizePath, renderHighlightedText } from '../utils'; import { useProjectsStore } from '@/stores/useProjectsStore'; @@ -685,6 +685,14 @@ function SessionNodeItemComponent(props: SessionNodeItemProps): React.ReactNode const pendingQuestionLabel = pendingQuestionCount === 1 ? t('sessions.sidebar.session.status.questionPendingSingle') : t('sessions.sidebar.session.status.questionPendingMany', { count: pendingQuestionCount }); + // Actions are permanently visible (with matching permanent padding) only in + // the non-VSCode alwaysShowActions layout; every other layout hover-reveals + // them over the row's right edge, where the badges live (#2284). + const badgeVisibilityClass = selectRowBadgeVisibilityClass({ + actionsAlwaysVisible: alwaysShowActions && !isVSCode, + menuOpen: isSessionMenuOpen, + hideOnHoverClass, + }); const showUnreadStatus = !isMovingToWorktree && !isStreaming && needsAttention && !isActive; const showStatusMarker = isStreaming || showUnreadStatus; // Both states are the same static dot; only the color separates "running" @@ -1432,13 +1440,13 @@ function SessionNodeItemComponent(props: SessionNodeItemProps): React.ReactNode ) : null} {pendingPermissionCount > 0 ? ( - + {pendingPermissionCount} ) : null} {pendingQuestionCount > 0 ? ( - + {pendingQuestionCount} diff --git a/packages/ui/src/components/session/sidebar/sessions/sessionNodeItemUtils.test.ts b/packages/ui/src/components/session/sidebar/sessions/sessionNodeItemUtils.test.ts index 93128ec0..9c775b35 100644 --- a/packages/ui/src/components/session/sidebar/sessions/sessionNodeItemUtils.test.ts +++ b/packages/ui/src/components/session/sidebar/sessions/sessionNodeItemUtils.test.ts @@ -9,6 +9,7 @@ import { nodeHasPinnedMembershipChange, selectFolderRootNodes, selectQuestionBadgeSessionScopes, + selectRowBadgeVisibilityClass, } from './sessionNodeItemUtils'; import type { SessionNode } from '../types'; @@ -166,6 +167,45 @@ describe('selectFolderRootNodes', () => { }); }); +describe('selectRowBadgeVisibilityClass', () => { + const hideOnHoverClass = 'group-hover:opacity-0 group-focus-within:opacity-0'; + + test('hides the badge while hover-revealed actions are shown, like the date label (#2284)', () => { + const className = selectRowBadgeVisibilityClass({ + actionsAlwaysVisible: false, + menuOpen: false, + hideOnHoverClass, + }); + + expect(className).toContain(hideOnHoverClass); + expect(className).toContain('transition-opacity'); + }); + + test('hides the badge while the row menu keeps the actions visible without hover', () => { + const className = selectRowBadgeVisibilityClass({ + actionsAlwaysVisible: false, + menuOpen: true, + hideOnHoverClass, + }); + + expect(className).toContain('opacity-0'); + expect(className).not.toContain('group-hover'); + }); + + test('keeps the badge always visible when actions have reserved permanent padding', () => { + expect(selectRowBadgeVisibilityClass({ + actionsAlwaysVisible: true, + menuOpen: false, + hideOnHoverClass, + })).toBe(''); + expect(selectRowBadgeVisibilityClass({ + actionsAlwaysVisible: true, + menuOpen: true, + hideOnHoverClass, + })).toBe(''); + }); +}); + describe('getSessionWorktreeMenuDisabled', () => { test('shares the parent trigger disabled contract with the new worktree action', () => { expect(getSessionWorktreeMenuDisabled({ diff --git a/packages/ui/src/components/session/sidebar/sessions/sessionNodeItemUtils.ts b/packages/ui/src/components/session/sidebar/sessions/sessionNodeItemUtils.ts index 171c36cb..71caff37 100644 --- a/packages/ui/src/components/session/sidebar/sessions/sessionNodeItemUtils.ts +++ b/packages/ui/src/components/session/sidebar/sessions/sessionNodeItemUtils.ts @@ -333,6 +333,25 @@ export const nodeHasPinnedMembershipChange = ( return visit(prevNode, nextNode); }; +/** + * Visibility classes for the row's right-edge badges (pending permissions / + * questions). The hover actions paint over the row's right edge, and they are + * also forced visible while the row menu is open — without hover, so the + * hover reveal padding does not apply and the actions would cover the badges. + * The badges therefore yield exactly like the date/branch metadata label: + * hidden while the actions are hover-revealed or the menu is open. Rows with + * always-visible actions reserve permanent padding instead, so their badges + * never conflict and must stay visible. + */ +export const selectRowBadgeVisibilityClass = (input: { + actionsAlwaysVisible: boolean; + menuOpen: boolean; + hideOnHoverClass: string; +}): string => { + if (input.actionsAlwaysVisible) return ''; + return `transition-opacity duration-150 ${input.menuOpen ? 'opacity-0' : input.hideOnHoverClass}`; +}; + /** * Resolve the session id whose sidebar menu is open, or null if no * menu is open. Only one row can have its menu open at a time.