mirror of
https://github.com/davidkaya/aryx.git
synced 2026-07-29 07:58:47 +02:00
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>
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user