diff --git a/packages/ui/src/components/session/sidebar/ConfirmDialogs.tsx b/packages/ui/src/components/session/sidebar/ConfirmDialogs.tsx index fb6ee1e8..50bfc0da 100644 --- a/packages/ui/src/components/session/sidebar/ConfirmDialogs.tsx +++ b/packages/ui/src/components/session/sidebar/ConfirmDialogs.tsx @@ -7,6 +7,10 @@ import { useI18n } from '@/lib/i18n'; export type DeleteSessionConfirmState = { session: Session; descendantCount: number; + // Snapshot of the descendant IDs computed when the dialog opened, so the + // executed list matches the count shown to the user even if childrenMap + // changes while the dialog is open. + descendantIds: string[]; archivedBucket: boolean; } | null; diff --git a/packages/ui/src/components/session/sidebar/hooks/useSessionActions.ts b/packages/ui/src/components/session/sidebar/hooks/useSessionActions.ts index 1f85c325..6bfc9de8 100644 --- a/packages/ui/src/components/session/sidebar/hooks/useSessionActions.ts +++ b/packages/ui/src/components/session/sidebar/hooks/useSessionActions.ts @@ -7,6 +7,7 @@ import { useI18n } from '@/lib/i18n'; type DeleteSessionConfirmSetter = React.Dispatch>; @@ -36,7 +37,7 @@ type Args = { childrenMap: Map; showDeletionDialog: boolean; setDeleteSessionConfirm: DeleteSessionConfirmSetter; - deleteSessionConfirm: { session: Session; descendantCount: number; archivedBucket: boolean } | null; + deleteSessionConfirm: { session: Session; descendantCount: number; descendantIds: string[]; archivedBucket: boolean } | null; setEditingId: (id: string | null) => void; setEditTitle: (value: string) => void; editingId: string | null; @@ -168,11 +169,30 @@ export const useSessionActions = (args: Args) => { return collected; }, [args.childrenMap]); + // Archive cascades to subagents that aren't already archived; hard-delete + // cascades to every descendant unconditionally. We collect once and filter + // per-action so the dialog count and the executed ID list always agree. + const filterDescendantsForAction = React.useCallback( + (descendants: Session[], shouldHardDelete: boolean): Session[] => { + if (shouldHardDelete) return descendants; + return descendants.filter((s) => !s.time?.archived); + }, + [], + ); + const executeDeleteSession = React.useCallback( - async (session: Session, source?: { archivedBucket?: boolean }) => { - const descendants = collectDescendants(session.id); + async ( + session: Session, + source?: { archivedBucket?: boolean }, + precomputed?: { descendantIds: string[] }, + ) => { const shouldHardDelete = source?.archivedBucket === true; - if (descendants.length === 0) { + // Use the snapshot taken when the dialog opened (if any) so the + // executed list matches what the user was told. Fall back to a fresh + // collection for direct-execute (no-dialog) callers. + const descendantIds = precomputed?.descendantIds + ?? filterDescendantsForAction(collectDescendants(session.id), shouldHardDelete).map((s) => s.id); + if (descendantIds.length === 0) { const success = shouldHardDelete ? await args.deleteSession(session.id) : await args.archiveSession(session.id); @@ -188,7 +208,7 @@ export const useSessionActions = (args: Args) => { return; } - const ids = [session.id, ...descendants.map((s) => s.id)]; + const ids = [session.id, ...descendantIds]; if (shouldHardDelete) { const { deletedIds, failedIds } = await args.deleteSessions(ids); if (deletedIds.length > 0) { @@ -216,26 +236,35 @@ export const useSessionActions = (args: Args) => { : t('sessions.sidebar.bulkActions.failedArchivePlural', { count: failedIds.length })); } }, - [args, collectDescendants, t], + [args, collectDescendants, filterDescendantsForAction, t], ); const handleDeleteSession = React.useCallback( (session: Session, source?: { archivedBucket?: boolean }) => { - const descendants = collectDescendants(session.id); + const shouldHardDelete = source?.archivedBucket === true; + const effectiveDescendantIds = filterDescendantsForAction( + collectDescendants(session.id), + shouldHardDelete, + ).map((s) => s.id); if (!args.showDeletionDialog) { - void executeDeleteSession(session, source); + void executeDeleteSession(session, source, { descendantIds: effectiveDescendantIds }); return; } - args.setDeleteSessionConfirm({ session, descendantCount: descendants.length, archivedBucket: source?.archivedBucket === true }); + args.setDeleteSessionConfirm({ + session, + descendantCount: effectiveDescendantIds.length, + descendantIds: effectiveDescendantIds, + archivedBucket: shouldHardDelete, + }); }, - [args, collectDescendants, executeDeleteSession], + [args, collectDescendants, executeDeleteSession, filterDescendantsForAction], ); const confirmDeleteSession = React.useCallback(async () => { if (!args.deleteSessionConfirm) return; - const { session, archivedBucket } = args.deleteSessionConfirm; + const { session, archivedBucket, descendantIds } = args.deleteSessionConfirm; args.setDeleteSessionConfirm(null); - await executeDeleteSession(session, { archivedBucket }); + await executeDeleteSession(session, { archivedBucket }, { descendantIds }); }, [args, executeDeleteSession]); return {