Files
twenty/packages/twenty-front/src/modules/ai/components/AiChatThreadDeleteConfirmationModal.tsx
T
nitinandGitHub e1828b6f41 [AI] Add thread actions, filters, and archive support (#20068)
## PR Description

### Summary
- Add AI chat thread actions: rename, archive (soft-delete via
`deletedAt`), and hard-delete with confirmation.
- Add chat thread filtering by status (active/archived/all), group-by
mode, and last activity.
- Rework drawer/side-panel thread lists to share thread sections, item
menus, archive icons, and empty-state behavior.
- Extend server chat thread model/API with `deletedAt`, mutations,
broadcasts, and archive-aware stream guards.

### Decisions
- Two-stage lifecycle: Archive sets `deletedAt` (soft); Delete is a
separate action on archived threads that hard-deletes the row. Aligns
with Twenty's soft-delete convention (Felix's suggestion).
- `lastMessageAt` is derived from `MAX(agentMessage.createdAt)` on read,
not stored. List query does inline aggregation for sort; `@ResolveField`
covers single-thread / mutation paths so the schema contract is honest
everywhere. Matches `timeline-messaging.service.ts` precedent and the
existing `totalInputCredits` / `totalOutputCredits` `@ResolveField`
pattern in the same resolver.
- Replaced auto-CRUD `chatThreads` (cursor-paginated Connection) with a
custom `[AgentChatThreadDTO!]` resolver. Frontend metadata-store treats
threads as a flat collection and filters/sorts client-side, so cursor
pagination was performative.
- Sending in an archived chat unarchives it optimistically on the client
and authoritatively on the server.
- Grouping and last-activity filtering use `lastMessageAt ?? updatedAt`
so archive/rename don't bump threads in the list.
- Kept metadata-store core API unchanged; AI chat uses the same local
cast pattern already used by other metadata-store partial updates.


https://github.com/user-attachments/assets/1b179b7b-1a2a-4a7a-aa0a-c88f6f051a87
2026-04-30 15:42:10 +00:00

55 lines
1.9 KiB
TypeScript

import { Trans, useLingui } from '@lingui/react/macro';
import { type AiChatThreadActionsSurface } from '@/ai/types/AiChatThreadActionsSurface';
import { useDeleteChatThread } from '@/ai/hooks/useDeleteChatThread';
import { aiChatThreadPendingDeleteFamilyState } from '@/ai/states/aiChatThreadPendingDeleteFamilyState';
import { getAiChatThreadDeleteModalId } from '@/ai/utils/getAiChatThreadDeleteModalId';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
import { useSetAtomFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomFamilyState';
type AiChatThreadDeleteConfirmationModalProps = {
surface: AiChatThreadActionsSurface;
};
export const AiChatThreadDeleteConfirmationModal = ({
surface,
}: AiChatThreadDeleteConfirmationModalProps) => {
const { t } = useLingui();
const { deleteChatThread } = useDeleteChatThread();
const aiChatThreadPendingDelete = useAtomFamilyStateValue(
aiChatThreadPendingDeleteFamilyState,
surface,
);
const setAiChatThreadPendingDelete = useSetAtomFamilyState(
aiChatThreadPendingDeleteFamilyState,
surface,
);
const modalInstanceId = getAiChatThreadDeleteModalId(surface);
const handleDelete = async () => {
if (aiChatThreadPendingDelete === null) return;
await deleteChatThread(aiChatThreadPendingDelete.threadId);
setAiChatThreadPendingDelete(null);
};
return (
<ConfirmationModal
modalInstanceId={modalInstanceId}
title={t`Delete chat`}
subtitle={
<Trans>
<strong>{aiChatThreadPendingDelete?.threadTitle ?? ''}</strong> and
all its messages will be removed.
</Trans>
}
onConfirmClick={handleDelete}
onClose={() => setAiChatThreadPendingDelete(null)}
confirmButtonText={t`Delete`}
confirmButtonAccent="danger"
/>
);
};