fix(sessions): align archive-cascade count with executed list (#1298)
* fix(sessions): align archive-cascade count with executed list When archiving a parent session, the dialog showed the total descendant count but the actual archive request skipped already-archived descendants — so the toast/result didn't match what the user was told. Collect descendants once, then filter per-action: hard-delete cascades to every descendant; archive skips already-archived ones. Dialog count and the ID list passed to archiveSessions/deleteSessions now agree. * fix(sessions): snapshot descendant IDs at dialog open Address Greptile review: handleDeleteSession populated the dialog count from a fresh collectDescendants() call, then confirmDeleteSession re-collected at confirm time — so a background sync that mutated childrenMap while the dialog was open could re-introduce the same count-vs-executed mismatch this PR fixed. Store the computed descendant IDs in deleteSessionConfirm and pass them through to executeDeleteSession, so the executed list is always the snapshot the user was shown. The no-dialog direct-execute path also routes through the snapshot for consistency. --------- Co-authored-by: vhqtvn <8930337+vhqtvn@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
co-authored by
vhqtvn
parent
86f039d3fd
commit
cbe6335ed9
@@ -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;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useI18n } from '@/lib/i18n';
|
||||
type DeleteSessionConfirmSetter = React.Dispatch<React.SetStateAction<{
|
||||
session: Session;
|
||||
descendantCount: number;
|
||||
descendantIds: string[];
|
||||
archivedBucket: boolean;
|
||||
} | null>>;
|
||||
|
||||
@@ -36,7 +37,7 @@ type Args = {
|
||||
childrenMap: Map<string, Session[]>;
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user