Files
aryx/src/renderer/hooks/useSessionSelection.ts
T
David KayaandCopilot 575aca360a feat: batch session archive and delete with multi-select UI
Add multi-select mode to the sidebar session list, enabling users to
archive, restore, or delete multiple sessions at once.

UX:
- Ctrl+Click (Cmd+Click on Mac) any session to enter multi-select mode
- Animated checkboxes stagger-reveal across all visible sessions
- Click to toggle, Shift+Click for range selection
- Running sessions are excluded from selection (safety guard)
- Floating action bar slides up with count pill, Archive, Delete, Cancel
- Batch delete shows a confirmation dialog with hold-to-confirm for 3+
  sessions and a scrollable list of session titles
- Batch archive shows an undo toast with 5-second auto-dismiss
- Escape key or Cancel button exits multi-select mode

Backend:
- batchSetSessionsArchived IPC: single load/mutate/persist cycle for N
  sessions instead of N sequential calls
- batchDeleteSessions IPC: parallel cleanup of scratchpad dirs and SDK
  sessions, then single workspace persist

Accessibility:
- Action bar: role=toolbar, aria-label
- Checkboxes: role=checkbox, aria-checked, keyboard-activatable
- Selection count: aria-live=polite
- Delete dialog: role=alertdialog, aria-modal, focus trap

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-15 07:12:45 +02:00

105 lines
3.1 KiB
TypeScript

import { useCallback, useRef, useState } from 'react';
export interface SessionSelectionState {
/** Currently selected session IDs */
selectedIds: Set<string>;
/** Whether multi-select mode is active */
isSelecting: boolean;
/** Toggle a single session's selection */
toggle: (sessionId: string) => void;
/** Shift+click range selection: selects everything between the last anchor and the target */
rangeSelect: (sessionId: string, allVisibleIds: string[]) => void;
/** Select all provided session IDs */
selectAll: (sessionIds: string[]) => void;
/** Deselect all */
deselectAll: () => void;
/** Enter multi-select mode with an initial selection */
enterSelectionMode: (initialId: string) => void;
/** Exit multi-select mode and clear selection */
exitSelectionMode: () => void;
/** Check if a session is selected */
isSelected: (sessionId: string) => boolean;
}
export function useSessionSelection(): SessionSelectionState {
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [isSelecting, setIsSelecting] = useState(false);
// Tracks the last explicitly toggled session for shift+click range behaviour
const anchorRef = useRef<string | null>(null);
const enterSelectionMode = useCallback((initialId: string) => {
setIsSelecting(true);
setSelectedIds(new Set([initialId]));
anchorRef.current = initialId;
}, []);
const exitSelectionMode = useCallback(() => {
setIsSelecting(false);
setSelectedIds(new Set());
anchorRef.current = null;
}, []);
const toggle = useCallback((sessionId: string) => {
setSelectedIds((prev) => {
const next = new Set(prev);
if (next.has(sessionId)) {
next.delete(sessionId);
} else {
next.add(sessionId);
}
anchorRef.current = sessionId;
return next;
});
}, []);
const rangeSelect = useCallback((sessionId: string, allVisibleIds: string[]) => {
const anchor = anchorRef.current;
if (!anchor) {
// No anchor yet — treat as a simple toggle
setSelectedIds(new Set([sessionId]));
anchorRef.current = sessionId;
return;
}
const anchorIdx = allVisibleIds.indexOf(anchor);
const targetIdx = allVisibleIds.indexOf(sessionId);
if (anchorIdx === -1 || targetIdx === -1) return;
const start = Math.min(anchorIdx, targetIdx);
const end = Math.max(anchorIdx, targetIdx);
const rangeIds = allVisibleIds.slice(start, end + 1);
setSelectedIds((prev) => {
const next = new Set(prev);
for (const id of rangeIds) {
next.add(id);
}
return next;
});
// Anchor stays the same for contiguous range extension
}, []);
const selectAll = useCallback((sessionIds: string[]) => {
setSelectedIds(new Set(sessionIds));
}, []);
const deselectAll = useCallback(() => {
setSelectedIds(new Set());
}, []);
const isSelected = useCallback((sessionId: string) => selectedIds.has(sessionId), [selectedIds]);
return {
selectedIds,
isSelecting,
toggle,
rangeSelect,
selectAll,
deselectAll,
enterSelectionMode,
exitSelectionMode,
isSelected,
};
}