Files
twenty/packages/twenty-server/src/engine/metadata-modules/ai/utils/ai-graphql-api-exception-handler.util.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

43 lines
1.3 KiB
TypeScript

import { assertUnreachable } from 'twenty-shared/utils';
import {
ConflictError,
ForbiddenError,
InternalServerError,
NotFoundError,
UserInputError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import {
AiException,
AiExceptionCode,
} from 'src/engine/metadata-modules/ai/ai.exception';
export const aiGraphqlApiExceptionHandler = (error: Error) => {
if (error instanceof AiException) {
switch (error.code) {
case AiExceptionCode.AGENT_NOT_FOUND:
case AiExceptionCode.THREAD_NOT_FOUND:
case AiExceptionCode.MESSAGE_NOT_FOUND:
case AiExceptionCode.ROLE_NOT_FOUND:
throw new NotFoundError(error);
case AiExceptionCode.INVALID_AGENT_INPUT:
case AiExceptionCode.INVALID_CHAT_THREAD_TITLE:
throw new UserInputError(error);
case AiExceptionCode.AGENT_ALREADY_EXISTS:
throw new ConflictError(error);
case AiExceptionCode.AGENT_IS_STANDARD:
case AiExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_AGENTS:
throw new ForbiddenError(error);
case AiExceptionCode.AGENT_EXECUTION_FAILED:
case AiExceptionCode.API_KEY_NOT_CONFIGURED:
case AiExceptionCode.USER_WORKSPACE_ID_NOT_FOUND:
throw new InternalServerError(error);
default: {
return assertUnreachable(error.code);
}
}
}
throw error;
};