feat: bulk select sessions in sidebar

Select toggle in header enters selection mode. Click toggles, shift-click
ranges, parent auto-includes subtasks, Cmd/Ctrl+A selects visible, Esc
exits, Cmd/Ctrl+Backspace deletes. Bulk bar: move to folder (existing or
new inline), archive/delete via existing confirm + "Never ask" pref.
Cross-scope forbidden — new scope clears previous.

Also: web sidebar header folds toggle into action row (no separate chrome
row, same position on toggle); sidebar min width bumped 250→280 so icons
fit; minimal-mode hover reveal padding trimmed.
This commit is contained in:
Bohdan Triapitsyn
2026-04-20 23:32:32 +03:00
parent daa485c5d9
commit 08258908c2
8 changed files with 709 additions and 13 deletions
@@ -9,15 +9,18 @@ import {
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import {
RiCheckLine,
RiCheckboxMultipleLine,
RiChatNewLine,
RiEqualizer2Line,
RiFolderAddLine,
RiLayoutLeftLine,
RiSearchLine,
RiCloseLine,
RiContractUpDownLine,
RiExpandUpDownLine,
RiCalendarScheduleLine,
} from '@remixicon/react';
import { cn } from '@/lib/utils';
import { ArrowsMerge } from '@/components/icons/ArrowsMerge';
import { useSessionDisplayStore } from '@/stores/useSessionDisplayStore';
@@ -40,6 +43,10 @@ type Props = {
collapseAllProjects: () => void;
expandAllProjects: () => void;
openScheduledTasksDialog: () => void;
selectionModeEnabled: boolean;
onToggleSelectionMode: () => void;
showSidebarToggle?: boolean;
onToggleSidebar?: () => void;
};
export function SidebarHeader(props: Props): React.ReactNode {
@@ -62,6 +69,10 @@ export function SidebarHeader(props: Props): React.ReactNode {
collapseAllProjects,
expandAllProjects,
openScheduledTasksDialog,
selectionModeEnabled,
onToggleSelectionMode,
showSidebarToggle = false,
onToggleSidebar,
} = props;
const displayMode = useSessionDisplayStore((state) => state.displayMode);
@@ -72,11 +83,36 @@ export function SidebarHeader(props: Props): React.ReactNode {
}
return (
<div className="select-none flex-shrink-0 px-2.5 py-1">
<div
className={cn(
'select-none flex-shrink-0',
showSidebarToggle ? 'pl-3 pr-3' : 'px-2.5 py-1',
)}
>
{reserveHeaderActionsSpace ? (
<div className="flex h-auto min-h-8 flex-col gap-1">
<div
className={cn(
'flex h-auto flex-col gap-1',
showSidebarToggle ? 'min-h-[var(--oc-header-height,56px)] justify-center' : 'min-h-8',
)}
>
<div className="flex h-8 items-center justify-between gap-2">
<div className="flex items-center gap-1.5">
{showSidebarToggle && onToggleSidebar ? (
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={onToggleSidebar}
className="inline-flex h-8 w-8 items-center justify-center rounded-md typography-ui-label font-medium text-foreground transition-colors hover:bg-interactive-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary disabled:pointer-events-none disabled:opacity-50"
aria-label="Close sessions"
>
<RiLayoutLeftLine className="h-[18px] w-[18px]" />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}><p>Close sessions</p></TooltipContent>
</Tooltip>
) : null}
<Tooltip>
<TooltipTrigger asChild>
<button
@@ -150,6 +186,23 @@ export function SidebarHeader(props: Props): React.ReactNode {
<TooltipContent side="bottom" sideOffset={4}><p>Search sessions</p></TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
onClick={onToggleSelectionMode}
className={cn(headerActionButtonClass, selectionModeEnabled && 'bg-interactive-hover text-primary')}
aria-label={selectionModeEnabled ? 'Exit selection' : 'Select sessions'}
aria-pressed={selectionModeEnabled}
>
<RiCheckboxMultipleLine className={headerActionIconClass} />
</button>
</TooltipTrigger>
<TooltipContent side="bottom" sideOffset={4}>
<p>{selectionModeEnabled ? 'Exit selection' : 'Select sessions'}</p>
</TooltipContent>
</Tooltip>
<DropdownMenu>
<Tooltip>
<TooltipTrigger asChild>