From cc7b41db0ea0b01971042a343d75565d9d3de0de Mon Sep 17 00:00:00 2001
From: Etienne <45695613+etiennejouan@users.noreply.github.com>
Date: Thu, 9 Jul 2026 09:02:56 +0200
Subject: [PATCH] feat(ai-chat): inject current date & per-message timestamps
into agent context (#22632)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
Gives the AI chat agent temporal awareness by injecting the current date
into
the system prompt and a per-message "sent at" timestamp into each user
message,
formatted in the member's timezone. Also hardens all timezone formatting
against
the `"system"` sentinel value, which was crashing the stream job.
## What changed
**Message timestamps (new)**
- Added `injectMessageTimestamps` util: prepends a
`Sent: …`
text part to each user message before it's sent to the model, so the
agent can
reason about "yesterday", "last week", etc.
- `loadMessagesFromDB` now stores the message time in the canonical
`metadata.createdAt` slot (ISO string, JSON-serializable for the BullMQ
job
payload) instead of a non-typed top-level `createdAt` field that nothing
read.
- Migrated the AI chat message pipeline from the generic `UIMessage` to
the
typed `ExtendedUIMessage` (`chat-execution.service`,
`extract-code-interpreter-files`,
`replace-unsupported-file-parts`, and related types), since
`metadata.createdAt`
is declared on `ExtendedUIMessage`.
**Current date in context**
- System prompt now includes `Current date: …` formatted in the member's
timezone
(`system-prompt-builder.service`).
- Settings › AI prompt preview mirrors the same `Current date` line.
**Timezone safety (bug fix)**
- Workspace members default `timeZone` to the `"system"` sentinel, which
is only
resolvable client-side. Passing it (or any invalid IANA zone) to
`Intl.DateTimeFormat` throws `RangeError: Invalid time zone specified:
system`,
which was failing the stream job.
- Added `getValidTimeZoneOrUndefined`, which returns a valid IANA zone
or
`undefined` (letting the runtime fall back to its default). Used in both
`injectMessageTimestamps` and `formatCurrentDate`. This mirrors the
existing
`isValidTimeZone` convention in the calendar module.
## Notes / follow-ups
- For members who never changed `timeZone` from `"system"`, timestamps
fall back
to the server's default zone (UTC). To honor their real local time, the
frontend would need to send the browser-detected zone with the chat
request
(the same way calendar/charts already pass a resolved zone). Not
included here.
## Test plan
- [x] `inject-message-timestamps.util.spec.ts` — covers timestamp
injection,
assistant messages untouched, invalid `createdAt`, and the `"system"`
timezone no longer throwing.
- [ ] Send a chat message and confirm the agent sees the correct
date/time.
- [ ] Verify a member with `timeZone = "system"` no longer crashes the
stream job.
---
.../pages/settings/ai/SettingsAiPrompts.tsx | 24 +++++-
.../system-prompt-builder.service.spec.ts | 36 ++++++++
.../services/agent-chat-streaming.service.ts | 2 +-
.../services/chat-execution.service.ts | 18 ++--
.../services/system-prompt-builder.service.ts | 23 ++++-
...ract-code-interpreter-files-result.type.ts | 4 +-
.../inject-message-timestamps.util.spec.ts | 85 +++++++++++++++++++
...eplace-unsupported-file-parts.util.spec.ts | 8 +-
.../extract-code-interpreter-files.util.ts | 5 +-
.../utils/inject-message-timestamps.util.ts | 52 ++++++++++++
.../replace-unsupported-file-parts.util.ts | 7 +-
.../getValidTimeZoneOrUndefined.test.ts | 21 +++++
.../utils/date/getValidTimeZoneOrUndefined.ts | 17 ++++
packages/twenty-shared/src/utils/index.ts | 1 +
14 files changed, 277 insertions(+), 26 deletions(-)
create mode 100644 packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/__tests__/system-prompt-builder.service.spec.ts
create mode 100644 packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/__tests__/inject-message-timestamps.util.spec.ts
create mode 100644 packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/inject-message-timestamps.util.ts
create mode 100644 packages/twenty-shared/src/utils/date/__tests__/getValidTimeZoneOrUndefined.test.ts
create mode 100644 packages/twenty-shared/src/utils/date/getValidTimeZoneOrUndefined.ts
diff --git a/packages/twenty-front/src/pages/settings/ai/SettingsAiPrompts.tsx b/packages/twenty-front/src/pages/settings/ai/SettingsAiPrompts.tsx
index 393ae8bcc9..2983fd5ffc 100644
--- a/packages/twenty-front/src/pages/settings/ai/SettingsAiPrompts.tsx
+++ b/packages/twenty-front/src/pages/settings/ai/SettingsAiPrompts.tsx
@@ -8,7 +8,11 @@ import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomState
import { useQuery } from '@apollo/client/react';
import { t } from '@lingui/core/macro';
import { SettingsPath } from 'twenty-shared/types';
-import { getSettingsPath, isDefined } from 'twenty-shared/utils';
+import {
+ getSettingsPath,
+ getValidTimeZoneOrUndefined,
+ isDefined,
+} from 'twenty-shared/utils';
import { H2Title, H3Title } from 'twenty-ui/typography';
import { Section } from 'twenty-ui/layout';
import { themeCssVariables } from 'twenty-ui/theme-constants';
@@ -48,10 +52,24 @@ export const SettingsAiPrompts = () => {
`**${t`Locale`}:** ${currentWorkspaceMember.locale ?? 'en'}`,
];
- if (isDefined(currentWorkspaceMember.timeZone)) {
- parts.push(`**${t`Timezone`}:** ${currentWorkspaceMember.timeZone}`);
+ const validTimeZone = getValidTimeZoneOrUndefined(
+ currentWorkspaceMember.timeZone,
+ );
+
+ if (isDefined(validTimeZone)) {
+ parts.push(`**${t`Timezone`}:** ${validTimeZone}`);
}
+ const currentDate = new Intl.DateTimeFormat('en-US', {
+ timeZone: validTimeZone,
+ weekday: 'long',
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ }).format(new Date());
+
+ parts.push(`**${t`Current date`}:** ${currentDate}`);
+
return parts.join('\n\n');
};
diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/__tests__/system-prompt-builder.service.spec.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/__tests__/system-prompt-builder.service.spec.ts
new file mode 100644
index 0000000000..c150480ac7
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/__tests__/system-prompt-builder.service.spec.ts
@@ -0,0 +1,36 @@
+import { SystemPromptBuilderService } from 'src/engine/metadata-modules/ai/ai-chat/services/system-prompt-builder.service';
+
+describe('SystemPromptBuilderService', () => {
+ const buildService = () =>
+ new SystemPromptBuilderService({} as never, {} as never, {} as never);
+
+ describe('buildUserContextSection', () => {
+ it('omits the timezone line when timezone is the "system" sentinel', () => {
+ const service = buildService();
+
+ const result = service.buildUserContextSection({
+ firstName: 'John',
+ lastName: 'Doe',
+ locale: 'en',
+ timezone: 'system',
+ });
+
+ expect(result).not.toContain('Timezone:');
+ expect(result).toContain('Current date:');
+ });
+
+ it('includes the timezone line for a valid IANA timezone', () => {
+ const service = buildService();
+
+ const result = service.buildUserContextSection({
+ firstName: 'John',
+ lastName: 'Doe',
+ locale: 'en',
+ timezone: 'America/New_York',
+ });
+
+ expect(result).toContain('Timezone: America/New_York');
+ expect(result).toContain('Current date:');
+ });
+ });
+});
diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat-streaming.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat-streaming.service.ts
index dbe29f5e76..fc4fd0708a 100644
--- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat-streaming.service.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/agent-chat-streaming.service.ts
@@ -603,7 +603,7 @@ export class AgentChatStreamingService {
return part;
}),
),
- createdAt: message.createdAt,
+ metadata: { createdAt: message.createdAt.toISOString() },
})),
);
}
diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-execution.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-execution.service.ts
index 90799abe8c..d28d7a4f02 100644
--- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-execution.service.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/chat-execution.service.ts
@@ -10,10 +10,8 @@ import {
streamText,
type SystemModelMessage,
type ToolSet,
- type UIDataTypes,
- type UIMessage,
- type UITools,
} from 'ai';
+import { type ExtendedUIMessage } from 'twenty-shared/ai';
import { type APP_LOCALES } from 'twenty-shared/translations';
import { AppPath } from 'twenty-shared/types';
import { getAppPath, isDefined } from 'twenty-shared/utils';
@@ -66,6 +64,7 @@ import { MessagePruningService } from 'src/engine/metadata-modules/ai/ai-chat/se
import { SystemPromptBuilderService } from 'src/engine/metadata-modules/ai/ai-chat/services/system-prompt-builder.service';
import { type ExtractedFile } from 'src/engine/metadata-modules/ai/ai-chat/types/extracted-file.type';
import { extractCodeInterpreterFiles } from 'src/engine/metadata-modules/ai/ai-chat/utils/extract-code-interpreter-files.util';
+import { injectMessageTimestamps } from 'src/engine/metadata-modules/ai/ai-chat/utils/inject-message-timestamps.util';
import {
getCacheProviderOptions,
getCallLevelProviderOptions,
@@ -83,7 +82,7 @@ export type ChatExecutionOptions = {
workspace: WorkspaceEntity;
userWorkspaceId: string;
threadId?: string;
- messages: UIMessage[];
+ messages: ExtendedUIMessage[];
browsingContext: BrowsingContextType | null;
onCodeExecutionUpdate?: CodeExecutionStreamEmitter;
onCompaction?: () => void;
@@ -236,7 +235,7 @@ export class ChatExecutionService {
const isCodeInterpreterEnabled = this.codeInterpreterService.isEnabled();
- let processedMessages: UIMessage[] = replaceUnsupportedFileParts(
+ let processedMessages: ExtendedUIMessage[] = replaceUnsupportedFileParts(
messages,
modelConfig.modalities,
isCodeInterpreterEnabled,
@@ -272,6 +271,11 @@ export class ChatExecutionService {
);
}
+ processedMessages = injectMessageTimestamps(
+ processedMessages,
+ userContext.timezone,
+ );
+
const systemPrompt = this.systemPromptBuilder.buildFullPrompt(
toolCatalog,
skillCatalog,
@@ -622,9 +626,9 @@ export class ChatExecutionService {
}
private injectBrowsingContextIntoLastUserMessage(
- messages: UIMessage[],
+ messages: ExtendedUIMessage[],
contextString: string,
- ): UIMessage[] {
+ ): ExtendedUIMessage[] {
const lastUserIndex = messages
.map((message) => message.role)
.lastIndexOf('user');
diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/system-prompt-builder.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/system-prompt-builder.service.ts
index 3ea9fe2a02..8c63c44e0a 100644
--- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/system-prompt-builder.service.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/services/system-prompt-builder.service.ts
@@ -1,6 +1,9 @@
import { Injectable } from '@nestjs/common';
-import { assertUnreachable } from 'twenty-shared/utils';
+import {
+ assertUnreachable,
+ getValidTimeZoneOrUndefined,
+} from 'twenty-shared/utils';
import { COMMON_PRELOAD_TOOLS } from 'src/engine/core-modules/tool-provider/constants/common-preload-tools.const';
import { ToolCategory } from 'twenty-shared/ai';
@@ -182,16 +185,30 @@ ${instructions}`;
`Locale: ${userContext.locale}`,
];
- if (userContext.timezone) {
- parts.push(`Timezone: ${userContext.timezone}`);
+ const resolvedTimeZone = getValidTimeZoneOrUndefined(userContext.timezone);
+
+ if (resolvedTimeZone) {
+ parts.push(`Timezone: ${resolvedTimeZone}`);
}
+ parts.push(`Current date: ${this.formatCurrentDate(userContext.timezone)}`);
+
return `
## User Context
${parts.join('\n')}`;
}
+ private formatCurrentDate(timezone: string | null): string {
+ return new Intl.DateTimeFormat('en-US', {
+ timeZone: getValidTimeZoneOrUndefined(timezone),
+ weekday: 'long',
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ }).format(new Date());
+ }
+
buildUploadedFilesSection(
storedFiles: Array<{ filename: string; fileId: string }>,
): string {
diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/types/extract-code-interpreter-files-result.type.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/types/extract-code-interpreter-files-result.type.ts
index d1500454d9..6451ebe556 100644
--- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/types/extract-code-interpreter-files-result.type.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/types/extract-code-interpreter-files-result.type.ts
@@ -1,8 +1,8 @@
-import { type UIMessage } from 'ai';
+import { type ExtendedUIMessage } from 'twenty-shared/ai';
import { type ExtractedFile } from 'src/engine/metadata-modules/ai/ai-chat/types/extracted-file.type';
export type ExtractCodeInterpreterFilesResult = {
- processedMessages: UIMessage[];
+ processedMessages: ExtendedUIMessage[];
extractedFiles: ExtractedFile[];
};
diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/__tests__/inject-message-timestamps.util.spec.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/__tests__/inject-message-timestamps.util.spec.ts
new file mode 100644
index 0000000000..4be1cbca45
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/__tests__/inject-message-timestamps.util.spec.ts
@@ -0,0 +1,85 @@
+import { type ExtendedUIMessage } from 'twenty-shared/ai';
+
+import { injectMessageTimestamps } from 'src/engine/metadata-modules/ai/ai-chat/utils/inject-message-timestamps.util';
+
+describe('injectMessageTimestamps', () => {
+ it('prefixes a user message with a timestamp part built from metadata.createdAt', () => {
+ const messages = [
+ {
+ id: '1',
+ role: 'user',
+ parts: [{ type: 'text', text: 'What happened yesterday?' }],
+ metadata: { createdAt: '2026-07-07T09:57:00.000Z' },
+ },
+ ] as unknown as ExtendedUIMessage[];
+
+ const [message] = injectMessageTimestamps(messages, 'UTC');
+
+ expect(message.parts).toHaveLength(2);
+ expect(message.parts[0]).toEqual({
+ type: 'text',
+ text: expect.stringContaining('Sent: '),
+ });
+ expect((message.parts[0] as { text: string }).text).toContain('UTC');
+ expect(message.parts[1]).toEqual({
+ type: 'text',
+ text: 'What happened yesterday?',
+ });
+ });
+
+ it('leaves assistant messages untouched', () => {
+ const messages = [
+ {
+ id: '1',
+ role: 'assistant',
+ parts: [{ type: 'text', text: 'response' }],
+ metadata: { createdAt: '2026-07-07T09:57:00.000Z' },
+ },
+ ] as unknown as ExtendedUIMessage[];
+
+ const [message] = injectMessageTimestamps(messages, 'UTC');
+
+ expect(message.parts).toHaveLength(1);
+ expect(message.parts[0]).toEqual({ type: 'text', text: 'response' });
+ });
+
+ it('does not throw and still injects a timestamp when the timezone is the "system" sentinel', () => {
+ const messages = [
+ {
+ id: '1',
+ role: 'user',
+ parts: [{ type: 'text', text: 'What happened yesterday?' }],
+ metadata: { createdAt: '2026-07-07T09:57:00.000Z' },
+ },
+ ] as unknown as ExtendedUIMessage[];
+
+ const [message] = injectMessageTimestamps(messages, 'system');
+
+ expect(message.parts).toHaveLength(2);
+ expect(message.parts[0]).toEqual({
+ type: 'text',
+ text: expect.stringContaining('Sent: '),
+ });
+ });
+
+ it('leaves user messages without a valid createdAt untouched', () => {
+ const messages = [
+ {
+ id: '1',
+ role: 'user',
+ parts: [{ type: 'text', text: 'hello' }],
+ },
+ {
+ id: '2',
+ role: 'user',
+ parts: [{ type: 'text', text: 'world' }],
+ metadata: { createdAt: 'not-a-date' },
+ },
+ ] as unknown as ExtendedUIMessage[];
+
+ const result = injectMessageTimestamps(messages, 'UTC');
+
+ expect(result[0].parts).toHaveLength(1);
+ expect(result[1].parts).toHaveLength(1);
+ });
+});
diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/__tests__/replace-unsupported-file-parts.util.spec.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/__tests__/replace-unsupported-file-parts.util.spec.ts
index e09ed57467..28e7cc0abe 100644
--- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/__tests__/replace-unsupported-file-parts.util.spec.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/__tests__/replace-unsupported-file-parts.util.spec.ts
@@ -1,4 +1,4 @@
-import { type UIMessage } from 'ai';
+import { type ExtendedUIMessage } from 'twenty-shared/ai';
import { replaceUnsupportedFileParts } from 'src/engine/metadata-modules/ai/ai-chat/utils/replace-unsupported-file-parts.util';
@@ -10,7 +10,9 @@ const buildFilePart = (mediaType: string, filename = 'file.bin') => ({
fileId: 'file-id',
});
-const buildUserMessage = (parts: UIMessage['parts']): UIMessage => ({
+const buildUserMessage = (
+ parts: ExtendedUIMessage['parts'],
+): ExtendedUIMessage => ({
id: 'message-id',
role: 'user',
parts,
@@ -65,7 +67,7 @@ describe('replaceUnsupportedFileParts', () => {
});
it('does not touch non-user messages', () => {
- const assistantMessage: UIMessage = {
+ const assistantMessage: ExtendedUIMessage = {
id: 'assistant-id',
role: 'assistant',
parts: [{ type: 'text', text: 'hello' }],
diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/extract-code-interpreter-files.util.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/extract-code-interpreter-files.util.ts
index 07e43fe13d..c8fcf7ed6e 100644
--- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/extract-code-interpreter-files.util.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/extract-code-interpreter-files.util.ts
@@ -1,12 +1,11 @@
-import { type UIMessage } from 'ai';
-import { isExtendedFileUIPart } from 'twenty-shared/ai';
+import { type ExtendedUIMessage, isExtendedFileUIPart } from 'twenty-shared/ai';
import { CODE_INTERPRETER_MIME_TYPES } from 'src/engine/metadata-modules/ai/ai-chat/constants/code-interpreter-mime-types.constant';
import { type ExtractCodeInterpreterFilesResult } from 'src/engine/metadata-modules/ai/ai-chat/types/extract-code-interpreter-files-result.type';
import { type ExtractedFile } from 'src/engine/metadata-modules/ai/ai-chat/types/extracted-file.type';
export const extractCodeInterpreterFiles = (
- messages: UIMessage[],
+ messages: ExtendedUIMessage[],
): ExtractCodeInterpreterFilesResult => {
const extractedFiles: ExtractedFile[] = [];
diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/inject-message-timestamps.util.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/inject-message-timestamps.util.ts
new file mode 100644
index 0000000000..9396a8fb82
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/inject-message-timestamps.util.ts
@@ -0,0 +1,52 @@
+import { type ExtendedUIMessage } from 'twenty-shared/ai';
+import { getValidTimeZoneOrUndefined, isDefined } from 'twenty-shared/utils';
+
+const formatMessageTimestamp = (date: Date, timezone: string | null): string =>
+ new Intl.DateTimeFormat('en-US', {
+ timeZone: getValidTimeZoneOrUndefined(timezone),
+ weekday: 'long',
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric',
+ hour: 'numeric',
+ minute: '2-digit',
+ timeZoneName: 'short',
+ }).format(date);
+
+const extractCreatedAt = (message: ExtendedUIMessage): Date | undefined => {
+ const rawCreatedAt = message.metadata?.createdAt;
+
+ if (!isDefined(rawCreatedAt)) {
+ return undefined;
+ }
+
+ const parsedCreatedAt = new Date(rawCreatedAt);
+
+ return isNaN(parsedCreatedAt.getTime()) ? undefined : parsedCreatedAt;
+};
+
+export const injectMessageTimestamps = (
+ messages: ExtendedUIMessage[],
+ timezone: string | null,
+): ExtendedUIMessage[] =>
+ messages.map((message) => {
+ if (message.role !== 'user') {
+ return message;
+ }
+
+ const createdAt = extractCreatedAt(message);
+
+ if (!isDefined(createdAt)) {
+ return message;
+ }
+
+ const timestampPart = {
+ type: 'text' as const,
+ text: `Sent: ${formatMessageTimestamp(createdAt, timezone)}`,
+ };
+
+ return {
+ ...message,
+ parts: [timestampPart, ...message.parts],
+ };
+ });
diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/replace-unsupported-file-parts.util.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/replace-unsupported-file-parts.util.ts
index ef43f6f542..edd3a5a365 100644
--- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/replace-unsupported-file-parts.util.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/utils/replace-unsupported-file-parts.util.ts
@@ -1,14 +1,13 @@
-import { type UIMessage } from 'ai';
-import { isExtendedFileUIPart } from 'twenty-shared/ai';
+import { type ExtendedUIMessage, isExtendedFileUIPart } from 'twenty-shared/ai';
import { CODE_INTERPRETER_MIME_TYPES } from 'src/engine/metadata-modules/ai/ai-chat/constants/code-interpreter-mime-types.constant';
import { getNativeMimeTypesForModalities } from 'src/engine/metadata-modules/ai/ai-chat/utils/get-native-mime-types-for-modalities.util';
export const replaceUnsupportedFileParts = (
- messages: UIMessage[],
+ messages: ExtendedUIMessage[],
modalities: string[] = [],
isCodeInterpreterEnabled: boolean,
-): UIMessage[] => {
+): ExtendedUIMessage[] => {
const nativeMimeTypes = getNativeMimeTypesForModalities(modalities);
return messages.map((message) => {
diff --git a/packages/twenty-shared/src/utils/date/__tests__/getValidTimeZoneOrUndefined.test.ts b/packages/twenty-shared/src/utils/date/__tests__/getValidTimeZoneOrUndefined.test.ts
new file mode 100644
index 0000000000..94e24f5f90
--- /dev/null
+++ b/packages/twenty-shared/src/utils/date/__tests__/getValidTimeZoneOrUndefined.test.ts
@@ -0,0 +1,21 @@
+import { getValidTimeZoneOrUndefined } from '@/utils/date/getValidTimeZoneOrUndefined';
+
+describe('getValidTimeZoneOrUndefined', () => {
+ it('should return undefined for null, undefined, empty string, and system', () => {
+ expect(getValidTimeZoneOrUndefined(null)).toBeUndefined();
+ expect(getValidTimeZoneOrUndefined(undefined)).toBeUndefined();
+ expect(getValidTimeZoneOrUndefined('')).toBeUndefined();
+ expect(getValidTimeZoneOrUndefined('system')).toBeUndefined();
+ });
+
+ it('should return undefined for invalid IANA time zones', () => {
+ expect(getValidTimeZoneOrUndefined('Not/A_Timezone')).toBeUndefined();
+ });
+
+ it('should return the time zone for valid IANA time zones', () => {
+ expect(getValidTimeZoneOrUndefined('America/New_York')).toBe(
+ 'America/New_York',
+ );
+ expect(getValidTimeZoneOrUndefined('UTC')).toBe('UTC');
+ });
+});
diff --git a/packages/twenty-shared/src/utils/date/getValidTimeZoneOrUndefined.ts b/packages/twenty-shared/src/utils/date/getValidTimeZoneOrUndefined.ts
new file mode 100644
index 0000000000..040b69340d
--- /dev/null
+++ b/packages/twenty-shared/src/utils/date/getValidTimeZoneOrUndefined.ts
@@ -0,0 +1,17 @@
+import { isNonEmptyString } from '@sniptt/guards';
+
+export const getValidTimeZoneOrUndefined = (
+ timeZone: string | null | undefined,
+): string | undefined => {
+ if (!isNonEmptyString(timeZone) || timeZone === 'system') {
+ return undefined;
+ }
+
+ try {
+ new Intl.DateTimeFormat('en-US', { timeZone });
+
+ return timeZone;
+ } catch {
+ return undefined;
+ }
+};
diff --git a/packages/twenty-shared/src/utils/index.ts b/packages/twenty-shared/src/utils/index.ts
index a06f426d46..9a85000204 100644
--- a/packages/twenty-shared/src/utils/index.ts
+++ b/packages/twenty-shared/src/utils/index.ts
@@ -34,6 +34,7 @@ export {
ACCEPTED_DATE_FORMATS,
ACCEPTED_DATE_TIME_FORMATS,
} from './date/dateInputFormats';
+export { getValidTimeZoneOrUndefined } from './date/getValidTimeZoneOrUndefined';
export { isDateWithoutTime } from './date/isDateWithoutTime';
export { isPlainDateAfter } from './date/isPlainDateAfter';
export { isPlainDateBefore } from './date/isPlainDateBefore';