Files
twenty/packages/twenty-front/src/modules/ai/hooks/useChatThreadArchiveActions.ts
T
nitin 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

56 lines
1.8 KiB
TypeScript

import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { useMutation } from '@apollo/client/react';
import { useApplyAgentChatThreadUpdate } from '@/ai/hooks/useApplyAgentChatThreadUpdate';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import {
ArchiveChatThreadDocument,
UnarchiveChatThreadDocument,
} from '~/generated-metadata/graphql';
export const useChatThreadArchiveActions = () => {
const { applyAgentChatThreadUpdate } = useApplyAgentChatThreadUpdate();
const { enqueueErrorSnackBar } = useSnackBar();
const [archiveMutation] = useMutation(ArchiveChatThreadDocument);
const [unarchiveMutation] = useMutation(UnarchiveChatThreadDocument);
const archiveChatThread = async (id: string) => {
try {
const { data } = await archiveMutation({ variables: { id } });
if (data?.archiveChatThread) {
applyAgentChatThreadUpdate({
id: data.archiveChatThread.id,
deletedAt: data.archiveChatThread.deletedAt ?? null,
updatedAt: data.archiveChatThread.updatedAt,
});
}
} catch (error) {
enqueueErrorSnackBar({
apolloError: CombinedGraphQLErrors.is(error) ? error : undefined,
});
}
};
const unarchiveChatThread = async (id: string) => {
try {
const { data } = await unarchiveMutation({ variables: { id } });
if (data?.unarchiveChatThread) {
applyAgentChatThreadUpdate({
id: data.unarchiveChatThread.id,
deletedAt: data.unarchiveChatThread.deletedAt ?? null,
updatedAt: data.unarchiveChatThread.updatedAt,
});
}
} catch (error) {
enqueueErrorSnackBar({
apolloError: CombinedGraphQLErrors.is(error) ? error : undefined,
});
}
};
return { archiveChatThread, unarchiveChatThread };
};