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
65 lines
2.4 KiB
TypeScript
65 lines
2.4 KiB
TypeScript
import { type MessageDescriptor } from '@lingui/core';
|
|
import { msg } from '@lingui/core/macro';
|
|
import { assertUnreachable } from 'twenty-shared/utils';
|
|
|
|
import { CustomException } from 'src/utils/custom-exception';
|
|
|
|
export enum AiExceptionCode {
|
|
AGENT_NOT_FOUND = 'AGENT_NOT_FOUND',
|
|
AGENT_ALREADY_EXISTS = 'AGENT_ALREADY_EXISTS',
|
|
AGENT_IS_STANDARD = 'AGENT_IS_STANDARD',
|
|
AGENT_EXECUTION_FAILED = 'AGENT_EXECUTION_FAILED',
|
|
INVALID_AGENT_INPUT = 'INVALID_AGENT_INPUT',
|
|
THREAD_NOT_FOUND = 'THREAD_NOT_FOUND',
|
|
INVALID_CHAT_THREAD_TITLE = 'INVALID_CHAT_THREAD_TITLE',
|
|
MESSAGE_NOT_FOUND = 'MESSAGE_NOT_FOUND',
|
|
API_KEY_NOT_CONFIGURED = 'API_KEY_NOT_CONFIGURED',
|
|
USER_WORKSPACE_ID_NOT_FOUND = 'USER_WORKSPACE_ID_NOT_FOUND',
|
|
ROLE_NOT_FOUND = 'ROLE_NOT_FOUND',
|
|
ROLE_CANNOT_BE_ASSIGNED_TO_AGENTS = 'ROLE_CANNOT_BE_ASSIGNED_TO_AGENTS',
|
|
}
|
|
|
|
const getAiExceptionUserFriendlyMessage = (code: AiExceptionCode) => {
|
|
switch (code) {
|
|
case AiExceptionCode.AGENT_NOT_FOUND:
|
|
return msg`Agent not found.`;
|
|
case AiExceptionCode.AGENT_ALREADY_EXISTS:
|
|
return msg`An agent with this name already exists.`;
|
|
case AiExceptionCode.AGENT_IS_STANDARD:
|
|
return msg`Standard agents cannot be modified.`;
|
|
case AiExceptionCode.AGENT_EXECUTION_FAILED:
|
|
return msg`Agent execution failed.`;
|
|
case AiExceptionCode.INVALID_AGENT_INPUT:
|
|
return msg`Invalid agent input.`;
|
|
case AiExceptionCode.THREAD_NOT_FOUND:
|
|
return msg`Chat thread not found.`;
|
|
case AiExceptionCode.INVALID_CHAT_THREAD_TITLE:
|
|
return msg`Chat thread title cannot be empty.`;
|
|
case AiExceptionCode.MESSAGE_NOT_FOUND:
|
|
return msg`Chat message not found.`;
|
|
case AiExceptionCode.API_KEY_NOT_CONFIGURED:
|
|
return msg`API key is not configured.`;
|
|
case AiExceptionCode.USER_WORKSPACE_ID_NOT_FOUND:
|
|
return msg`User workspace not found.`;
|
|
case AiExceptionCode.ROLE_NOT_FOUND:
|
|
return msg`Role not found.`;
|
|
case AiExceptionCode.ROLE_CANNOT_BE_ASSIGNED_TO_AGENTS:
|
|
return msg`This role cannot be assigned to agents.`;
|
|
default:
|
|
assertUnreachable(code);
|
|
}
|
|
};
|
|
|
|
export class AiException extends CustomException<AiExceptionCode> {
|
|
constructor(
|
|
message: string,
|
|
code: AiExceptionCode,
|
|
{ userFriendlyMessage }: { userFriendlyMessage?: MessageDescriptor } = {},
|
|
) {
|
|
super(message, code, {
|
|
userFriendlyMessage:
|
|
userFriendlyMessage ?? getAiExceptionUserFriendlyMessage(code),
|
|
});
|
|
}
|
|
}
|