e1828b6f41
## 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
43 lines
1.3 KiB
TypeScript
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;
|
|
};
|