Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac75ca7f72 | ||
|
|
8f24cda586 |
@@ -11,6 +11,7 @@ import { AGENT_CHAT_REFETCH_MESSAGES_EVENT_NAME } from '@/ai/constants/AgentChat
|
||||
import { AGENT_CHAT_RESTORE_EDITOR_CONTENT_EVENT_NAME } from '@/ai/constants/AgentChatRestoreEditorContentEventName';
|
||||
import { AGENT_CHAT_SEND_MESSAGE_EVENT_NAME } from '@/ai/constants/AgentChatSendMessageEventName';
|
||||
import { AGENT_CHAT_STOP_EVENT_NAME } from '@/ai/constants/AgentChatStopEventName';
|
||||
import { DELETE_CHAT_THREAD } from '@/ai/graphql/mutations/deleteChatThread';
|
||||
import { SEND_CHAT_MESSAGE } from '@/ai/graphql/mutations/sendChatMessage';
|
||||
import { STOP_AGENT_CHAT_STREAM } from '@/ai/graphql/mutations/stopAgentChatStream';
|
||||
import { useAgentChatModelId } from '@/ai/hooks/useAgentChatModelId';
|
||||
@@ -29,9 +30,13 @@ import { agentChatUploadedFilesState } from '@/ai/states/agentChatUploadedFilesS
|
||||
import { currentAiChatThreadState } from '@/ai/states/currentAiChatThreadState';
|
||||
import { useListenToBrowserEvent } from '@/browser-event/hooks/useListenToBrowserEvent';
|
||||
import { dispatchBrowserEvent } from '@/browser-event/utils/dispatchBrowserEvent';
|
||||
import { useUpdateMetadataStoreDraft } from '@/metadata-store/hooks/useUpdateMetadataStoreDraft';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { isGraphqlErrorOfType } from '~/utils/is-graphql-error-of-type.util';
|
||||
|
||||
const AI_THREAD_NOT_FOUND_ERROR_CODE = 'THREAD_NOT_FOUND';
|
||||
|
||||
export const useAgentChat = (
|
||||
ensureThreadIdForSend: () => Promise<string | null>,
|
||||
@@ -42,6 +47,7 @@ export const useAgentChat = (
|
||||
const apolloClient = useApolloClient();
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const setCurrentAiChatThread = useSetAtomState(currentAiChatThreadState);
|
||||
const { removeFromDraft, applyChanges } = useUpdateMetadataStoreDraft();
|
||||
const store = useStore();
|
||||
|
||||
const [, setPendingThreadIdAfterFirstSend] = useState<string | null>(null);
|
||||
@@ -193,8 +199,20 @@ export const useAgentChat = (
|
||||
return null;
|
||||
});
|
||||
} catch (error) {
|
||||
const restoredDraftKey =
|
||||
draftKey === AGENT_CHAT_NEW_THREAD_DRAFT_KEY ? threadId : draftKey;
|
||||
const isFirstSendForNewThread =
|
||||
draftKey === AGENT_CHAT_NEW_THREAD_DRAFT_KEY;
|
||||
const isThreadMissing = isGraphqlErrorOfType(
|
||||
error,
|
||||
AI_THREAD_NOT_FOUND_ERROR_CODE,
|
||||
);
|
||||
const shouldResetToNewThread = isFirstSendForNewThread || isThreadMissing;
|
||||
|
||||
const restoredDraftKey = shouldResetToNewThread
|
||||
? AGENT_CHAT_NEW_THREAD_DRAFT_KEY
|
||||
: draftKey;
|
||||
const errorTargetThreadId = shouldResetToNewThread
|
||||
? AGENT_CHAT_NEW_THREAD_DRAFT_KEY
|
||||
: threadId;
|
||||
|
||||
rollbackOptimisticUnarchive?.();
|
||||
|
||||
@@ -202,9 +220,6 @@ export const useAgentChat = (
|
||||
setAgentChatDraftsByThreadId((prev) => ({
|
||||
...prev,
|
||||
[restoredDraftKey]: contentToSend,
|
||||
...(draftKey === AGENT_CHAT_NEW_THREAD_DRAFT_KEY
|
||||
? { [AGENT_CHAT_NEW_THREAD_DRAFT_KEY]: '' }
|
||||
: {}),
|
||||
}));
|
||||
setAgentChatUploadedFiles(uploadedFilesSnapshot);
|
||||
|
||||
@@ -215,19 +230,38 @@ export const useAgentChat = (
|
||||
latestMessages.filter((message) => message.id !== messageId),
|
||||
);
|
||||
|
||||
store.set(
|
||||
errorAtom,
|
||||
const normalizedError =
|
||||
CombinedGraphQLErrors.is(error) || error instanceof Error
|
||||
? error
|
||||
: new Error('An unexpected error occurred'),
|
||||
: new Error('An unexpected error occurred');
|
||||
|
||||
store.set(
|
||||
agentChatErrorComponentFamilyState.atomFamily({
|
||||
instanceId: AGENT_CHAT_INSTANCE_ID,
|
||||
familyKey: { threadId: errorTargetThreadId },
|
||||
}),
|
||||
normalizedError,
|
||||
);
|
||||
|
||||
dispatchBrowserEvent(AGENT_CHAT_RESTORE_EDITOR_CONTENT_EVENT_NAME, {
|
||||
content: contentToSend,
|
||||
});
|
||||
|
||||
if (draftKey === AGENT_CHAT_NEW_THREAD_DRAFT_KEY) {
|
||||
setCurrentAiChatThread(threadId);
|
||||
if (shouldResetToNewThread) {
|
||||
removeFromDraft({ key: 'agentChatThreads', itemIds: [threadId] });
|
||||
applyChanges();
|
||||
setCurrentAiChatThread(AGENT_CHAT_NEW_THREAD_DRAFT_KEY);
|
||||
|
||||
if (isFirstSendForNewThread) {
|
||||
// Best-effort orphan cleanup; safe to ignore failure because
|
||||
// currentAiChatThreadState is already reset.
|
||||
apolloClient
|
||||
.mutate({
|
||||
mutation: DELETE_CHAT_THREAD,
|
||||
variables: { id: threadId },
|
||||
})
|
||||
.catch(() => undefined);
|
||||
}
|
||||
}
|
||||
|
||||
setPendingThreadIdAfterFirstSend(null);
|
||||
|
||||
+13
-33
@@ -10,7 +10,7 @@ import {
|
||||
ServerParseError,
|
||||
UnconventionalError,
|
||||
} from '@apollo/client/errors';
|
||||
import { isDefined, isNonEmptyString, type CustomError } from 'twenty-shared/utils';
|
||||
import { isDefined, type CustomError } from 'twenty-shared/utils';
|
||||
|
||||
const isApolloError = (error: unknown): boolean =>
|
||||
CombinedGraphQLErrors.is(error) ||
|
||||
@@ -44,45 +44,25 @@ export const PromiseRejectionEffect = () => {
|
||||
error?.networkError?.name === 'AbortError' ||
|
||||
error?.name === 'AbortError';
|
||||
|
||||
if (isAbortError) {
|
||||
return;
|
||||
if (!isAbortError) {
|
||||
enqueueErrorSnackBar(
|
||||
error instanceof Error ? { message: error.message } : {},
|
||||
);
|
||||
}
|
||||
|
||||
enqueueErrorSnackBar(
|
||||
error instanceof Error ? { message: error.message } : {},
|
||||
);
|
||||
|
||||
try {
|
||||
const { captureException, captureMessage } = await import('@sentry/react');
|
||||
const { captureException } = await import('@sentry/react');
|
||||
captureException(error, (scope) => {
|
||||
scope.setExtras({ mechanism: 'onUnhandle' });
|
||||
|
||||
if (error instanceof Error) {
|
||||
captureException(error, (scope) => {
|
||||
scope.setExtras({ mechanism: 'onUnhandledRejection' });
|
||||
|
||||
const fingerprint = hasErrorCode(error)
|
||||
? error.code
|
||||
: isNonEmptyString(error.message)
|
||||
? error.message
|
||||
: 'unknown-unhandled-rejection';
|
||||
|
||||
scope.setFingerprint([fingerprint]);
|
||||
|
||||
return scope;
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
captureMessage('Unhandled promise rejection with non-error reason', {
|
||||
level: 'warning',
|
||||
extra: {
|
||||
mechanism: 'onUnhandledRejection',
|
||||
reasonType: typeof error,
|
||||
},
|
||||
const fingerprint = hasErrorCode(error) ? error.code : error.message;
|
||||
scope.setFingerprint([fingerprint]);
|
||||
error.name = error.message;
|
||||
return scope;
|
||||
});
|
||||
} catch (sentryError) {
|
||||
// oxlint-disable-next-line no-console
|
||||
console.warn('Failed to capture exception with Sentry:', sentryError);
|
||||
console.error('Failed to capture exception with Sentry:', sentryError);
|
||||
}
|
||||
},
|
||||
[enqueueErrorSnackBar],
|
||||
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
|
||||
import { type FieldDateMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { computeDraftValueFromString } from '@/object-record/record-field/ui/utils/computeDraftValueFromString';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
describe('computeDraftValueFromString', () => {
|
||||
it('should return typed value for DATE field', () => {
|
||||
const fieldDefinition: Pick<FieldDefinition<FieldDateMetadata>, 'type'> = {
|
||||
type: FieldMetadataType.DATE,
|
||||
};
|
||||
|
||||
expect(
|
||||
computeDraftValueFromString<string>({
|
||||
fieldDefinition,
|
||||
value: '2026-05-22',
|
||||
}),
|
||||
).toBe('2026-05-22');
|
||||
});
|
||||
});
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
import { type FieldDefinition } from '@/object-record/record-field/ui/types/FieldDefinition';
|
||||
import { type FieldDateMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { computeEmptyDraftValue } from '@/object-record/record-field/ui/utils/computeEmptyDraftValue';
|
||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||
|
||||
describe('computeEmptyDraftValue', () => {
|
||||
it('should return empty draft value for DATE field', () => {
|
||||
const fieldDefinition: Pick<FieldDefinition<FieldDateMetadata>, 'type'> = {
|
||||
type: FieldMetadataType.DATE,
|
||||
};
|
||||
|
||||
expect(
|
||||
computeEmptyDraftValue<string>({
|
||||
fieldDefinition,
|
||||
}),
|
||||
).toBe('');
|
||||
});
|
||||
});
|
||||
+1
-3
@@ -3,7 +3,6 @@ import { type FieldInputDraftValue } from '@/object-record/record-field/ui/types
|
||||
import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { isFieldAddress } from '@/object-record/record-field/ui/types/guards/isFieldAddress';
|
||||
import { isFieldCurrency } from '@/object-record/record-field/ui/types/guards/isFieldCurrency';
|
||||
import { isFieldDate } from '@/object-record/record-field/ui/types/guards/isFieldDate';
|
||||
import { isFieldDateTime } from '@/object-record/record-field/ui/types/guards/isFieldDateTime';
|
||||
import { isFieldEmails } from '@/object-record/record-field/ui/types/guards/isFieldEmails';
|
||||
import { isFieldFullName } from '@/object-record/record-field/ui/types/guards/isFieldFullName';
|
||||
@@ -31,7 +30,6 @@ export const computeDraftValueFromString = <FieldValue>({
|
||||
if (
|
||||
isFieldUuid(fieldDefinition) ||
|
||||
isFieldText(fieldDefinition) ||
|
||||
isFieldDate(fieldDefinition) ||
|
||||
isFieldDateTime(fieldDefinition) ||
|
||||
isFieldNumber(fieldDefinition) ||
|
||||
isFieldRelation(fieldDefinition)
|
||||
@@ -85,7 +83,7 @@ export const computeDraftValueFromString = <FieldValue>({
|
||||
}
|
||||
|
||||
throw new CustomError(
|
||||
`Record field type not supported : ${fieldDefinition.type}`,
|
||||
`Record field type not supported : ${fieldDefinition.type}}`,
|
||||
'RECORD_FIELD_TYPE_NOT_SUPPORTED',
|
||||
);
|
||||
};
|
||||
|
||||
+1
-3
@@ -3,7 +3,6 @@ import { type FieldInputDraftValue } from '@/object-record/record-field/ui/types
|
||||
import { type FieldMetadata } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
import { isFieldAddress } from '@/object-record/record-field/ui/types/guards/isFieldAddress';
|
||||
import { isFieldCurrency } from '@/object-record/record-field/ui/types/guards/isFieldCurrency';
|
||||
import { isFieldDate } from '@/object-record/record-field/ui/types/guards/isFieldDate';
|
||||
import { isFieldDateTime } from '@/object-record/record-field/ui/types/guards/isFieldDateTime';
|
||||
import { isFieldFullName } from '@/object-record/record-field/ui/types/guards/isFieldFullName';
|
||||
import { isFieldLinks } from '@/object-record/record-field/ui/types/guards/isFieldLinks';
|
||||
@@ -26,7 +25,6 @@ export const computeEmptyDraftValue = <FieldValue>({
|
||||
if (
|
||||
isFieldUuid(fieldDefinition) ||
|
||||
isFieldText(fieldDefinition) ||
|
||||
isFieldDate(fieldDefinition) ||
|
||||
isFieldDateTime(fieldDefinition) ||
|
||||
isFieldNumber(fieldDefinition) ||
|
||||
isFieldRelation(fieldDefinition) ||
|
||||
@@ -70,7 +68,7 @@ export const computeEmptyDraftValue = <FieldValue>({
|
||||
}
|
||||
|
||||
throw new CustomError(
|
||||
`Record field type not supported : ${fieldDefinition.type}`,
|
||||
`Record field type not supported : ${fieldDefinition.type}}`,
|
||||
'RECORD_FIELD_TYPE_NOT_SUPPORTED',
|
||||
);
|
||||
};
|
||||
|
||||
-1
@@ -258,7 +258,6 @@ exports[`ALL_UNIVERSAL_FLAT_ENTITY_PROPERTIES_TO_COMPARE_AND_STRINGIFY should ma
|
||||
"propertiesToCompare": [
|
||||
"permissionFlagUniversalIdentifier",
|
||||
"roleUniversalIdentifier",
|
||||
"flag",
|
||||
],
|
||||
"propertiesToStringify": [],
|
||||
},
|
||||
|
||||
+1
-1
@@ -1198,7 +1198,7 @@ export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
|
||||
universalProperty: 'roleUniversalIdentifier',
|
||||
},
|
||||
flag: {
|
||||
toCompare: true,
|
||||
toCompare: false,
|
||||
toStringify: false,
|
||||
universalProperty: undefined,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user