fix(sidebar): keep permission badge and hover actions from overlapping (#2284)
Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
This commit is contained in:
co-authored by
Serhii Dziupin
parent
fe331c093b
commit
604da92fd9
@@ -32,7 +32,7 @@
|
||||
- `SidebarFooter.tsx`: Static footer with icon-only settings, shortcuts, and about actions.
|
||||
- `SidebarProjectsList.tsx`: Main scrollable renderer for project zones and their flat/archived groups plus empty/search states; owns project drag-to-reorder.
|
||||
- `SessionGroupSection.tsx`: Renders one flat (or archived) group: sessions first, then flat folder entries with path labels, show-more batching, and explicit loading/error/retry state for empty groups. Archived buckets (VS Code) virtualize past 50 rows.
|
||||
- `SessionNodeItem.tsx`: Renders one session row/tree node with a single-line layout, inline branch label, indicators, menu actions, and nested children. Pending-question counts stay per-session while expanded and roll up hidden descendants from their owning directory stores while collapsed. Rows do not initiate directory bootstrap on mount.
|
||||
- `SessionNodeItem.tsx`: Renders one session row/tree node with a single-line layout, inline branch label, indicators, menu actions, and nested children. Pending-question counts stay per-session while expanded and roll up hidden descendants from their owning directory stores while collapsed. Rows do not initiate directory bootstrap on mount. Pending-permission/question badges yield like the date/branch metadata whenever the hover actions are shown (hover, focus-within, or open row menu) on hover-reveal rows; rows with always-visible actions reserve permanent padding, so their badges stay visible (`selectRowBadgeVisibilityClass`).
|
||||
- `collapsedActivityIndicator.tsx`: Aggregate busy/unseen dot for collapsed groups and folders.
|
||||
- `ConfirmDialogs.tsx`: Shared confirm dialog wrappers for session delete and folder delete flows.
|
||||
- `sortableItems.tsx`: DnD sortable wrapper for project ordering plus the sticky zone-band project header and its action affordances.
|
||||
|
||||
@@ -26,7 +26,7 @@ import { buildSessionMessageRecordsSnapshot, useDirectoryStore, useGlobalSession
|
||||
import { useSync } from '@/sync/use-sync';
|
||||
import { useViewportStore, viewportSessionKey } from '@/sync/viewport-store';
|
||||
import { DraggableSessionRow } from './sessionFolderDnd';
|
||||
import { nodeContainsSessionId, nodeHasPinnedMembershipChange, selectQuestionBadgeSessionScopes } from './sessionNodeItemUtils';
|
||||
import { nodeContainsSessionId, nodeHasPinnedMembershipChange, selectQuestionBadgeSessionScopes, selectRowBadgeVisibilityClass } from './sessionNodeItemUtils';
|
||||
import type { SessionNodeChildRenderExtras, SessionNodeRenderExtras } from './sessionNodeItemUtils';
|
||||
import type { SessionNode } from './types';
|
||||
import { formatProjectLabel, formatSessionCompactDateLabel, formatSessionDateLabel, normalizePath, renderHighlightedText } from './utils';
|
||||
@@ -684,6 +684,14 @@ function SessionNodeItemComponent(props: Props): 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"
|
||||
@@ -1295,13 +1303,13 @@ function SessionNodeItemComponent(props: Props): React.ReactNode {
|
||||
</div>
|
||||
) : null}
|
||||
{pendingPermissionCount > 0 ? (
|
||||
<span className="inline-flex items-center gap-1 rounded bg-destructive/10 px-1 py-0.5 text-[0.7rem] text-destructive flex-shrink-0" title={t('sessions.sidebar.session.status.permissionRequired')} aria-label={t('sessions.sidebar.session.status.permissionRequired')}>
|
||||
<span className={cn('inline-flex items-center gap-1 rounded bg-destructive/10 px-1 py-0.5 text-[0.7rem] text-destructive flex-shrink-0', badgeVisibilityClass)} title={t('sessions.sidebar.session.status.permissionRequired')} aria-label={t('sessions.sidebar.session.status.permissionRequired')}>
|
||||
<Icon name="shield" className="h-3 w-3" />
|
||||
<span className="leading-none">{pendingPermissionCount}</span>
|
||||
</span>
|
||||
) : null}
|
||||
{pendingQuestionCount > 0 ? (
|
||||
<span className="inline-flex items-center gap-1 rounded bg-status-info/10 px-1 py-0.5 text-[0.7rem] text-status-info flex-shrink-0" title={pendingQuestionLabel} aria-label={pendingQuestionLabel}>
|
||||
<span className={cn('inline-flex items-center gap-1 rounded bg-status-info/10 px-1 py-0.5 text-[0.7rem] text-status-info flex-shrink-0', badgeVisibilityClass)} title={pendingQuestionLabel} aria-label={pendingQuestionLabel}>
|
||||
<Icon name="question" className="h-3 w-3" />
|
||||
<span className="leading-none">{pendingQuestionCount}</span>
|
||||
</span>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, test } from 'bun:test';
|
||||
import type { Session } from '@opencode-ai/sdk/v2';
|
||||
import { getRuntimeKey } from '@/lib/runtime-switch';
|
||||
import { getPinnedSessionKey } from '@/stores/useSessionPinnedStore';
|
||||
import { computeNodeStructureKey, nodeHasPinnedMembershipChange, selectFolderRootNodes, selectQuestionBadgeSessionScopes } from './sessionNodeItemUtils';
|
||||
import { computeNodeStructureKey, nodeHasPinnedMembershipChange, selectFolderRootNodes, selectQuestionBadgeSessionScopes, selectRowBadgeVisibilityClass } from './sessionNodeItemUtils';
|
||||
import type { SessionNode } from './types';
|
||||
|
||||
const session = (id: string, title: string): Session => ({
|
||||
@@ -158,3 +158,42 @@ describe('selectFolderRootNodes', () => {
|
||||
expect(selectFolderRootNodes(['missing-root', 'child'], new Map([['child', child]]))).toEqual([child]);
|
||||
});
|
||||
});
|
||||
|
||||
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('');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -196,6 +196,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.
|
||||
|
||||
Reference in New Issue
Block a user