## 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
55 lines
1.9 KiB
TypeScript
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"
|
|
/>
|
|
);
|
|
};
|