diff --git a/packages/twenty-front/src/modules/ai/components/__stories__/ChatReferenceChip.stories.tsx b/packages/twenty-front/src/modules/ai/components/__stories__/ChatReferenceChip.stories.tsx
index d8541b169a..63d878b4a6 100644
--- a/packages/twenty-front/src/modules/ai/components/__stories__/ChatReferenceChip.stories.tsx
+++ b/packages/twenty-front/src/modules/ai/components/__stories__/ChatReferenceChip.stories.tsx
@@ -113,22 +113,6 @@ export const ExistingMetadata: Story = {
},
};
-export const SurplusClosingBrackets: Story = {
- args: {
- text: `I created the ${formatChatReference({
- kind: 'object',
- objectNameSingular: 'company',
- displayName: 'Companies',
- })}] object.`,
- },
- play: async ({ canvasElement }) => {
- const canvas = within(canvasElement);
-
- expect(await canvas.findByText('Companies')).toBeVisible();
- expect(canvasElement).toHaveTextContent('I created the Companies object.');
- },
-};
-
export const ProposedObject: Story = {
args: {
text: `As a Head of Partnerships, you seem to work across partner companies, key contacts, and commercial follow-ups, so I suggest creating a ${formatChatReference(
diff --git a/packages/twenty-front/src/modules/ai/components/__tests__/TextWithChatReferences.test.tsx b/packages/twenty-front/src/modules/ai/components/__tests__/TextWithChatReferences.test.tsx
index 5eed72153f..96fb0be745 100644
--- a/packages/twenty-front/src/modules/ai/components/__tests__/TextWithChatReferences.test.tsx
+++ b/packages/twenty-front/src/modules/ai/components/__tests__/TextWithChatReferences.test.tsx
@@ -70,9 +70,9 @@ describe('TextWithChatReferences', () => {
expect(screen.queryByTestId('record-link')).not.toBeInTheDocument();
});
- it('should replace tagged record references with RecordLink chips', () => {
+ it('should replace record references with RecordLink chips', () => {
render(
- ,
+ ,
);
expect(screen.getByTestId('record-link')).toHaveTextContent('Acme');
@@ -80,18 +80,9 @@ describe('TextWithChatReferences', () => {
expect(screen.queryByText(/\[\[record:company:/)).not.toBeInTheDocument();
});
- it('should still replace legacy ]] record references with RecordLink chips', () => {
- render(
- ,
- );
-
- expect(screen.getByTestId('record-link')).toHaveTextContent('Acme');
- expect(screen.getByText(/Contact/)).toHaveTextContent('Contact Acme next');
- });
-
it('should replace multiple record references in option-style labels', () => {
render(
- ,
+ ,
);
const recordLinks = screen.getAllByTestId('record-link');
@@ -102,49 +93,33 @@ describe('TextWithChatReferences', () => {
expect(screen.queryByText(/\[\[/)).not.toBeInTheDocument();
});
- it('should chip tagged labels that contain backticks, brackets, colons, and ]]', () => {
+ it('should chip labels that contain backticks and colons', () => {
render(
- ,
+ ,
);
const recordLinks = screen.getAllByTestId('record-link');
- expect(recordLinks).toHaveLength(3);
+ expect(recordLinks).toHaveLength(2);
expect(recordLinks[0]).toHaveTextContent('Workflow `UPDATE_RECORD` step');
- expect(recordLinks[1]).toHaveTextContent('[test] ]] [test] [test] ###');
- expect(recordLinks[2]).toHaveTextContent('Doe: Jane');
+ expect(recordLinks[1]).toHaveTextContent('Doe: Jane');
expect(screen.queryByText(/\[\[record:/)).not.toBeInTheDocument();
});
- it('should not leave a surplus bracket after a chip', () => {
+ it('should render a reference using a retired closing tag as plain text', () => {
render(
- ,
+ ,
);
- expect(screen.getByTestId('object-link')).toHaveTextContent(
- 'Opportunities',
- );
- expect(screen.getByText(/Created/)).toHaveTextContent(
- 'Created Opportunities.',
- );
- });
-
- it('should not leave the extra brackets of an over-wrapped reference', () => {
- render(
- ,
- );
-
- expect(screen.getByTestId('object-link')).toHaveTextContent(
- 'Opportunities',
- );
- expect(screen.getByText(/Created/)).toHaveTextContent(
- 'Created Opportunities now',
+ expect(screen.queryByTestId('record-link')).not.toBeInTheDocument();
+ expect(screen.getByText(/Contact/)).toHaveTextContent(
+ 'Contact [[record:company:a1b2c3d4-e5f6-7890-abcd-ef1234567890:Acme[[/record]] next',
);
});
it('should route each reference kind to its own chip', () => {
render(
- ,
+ ,
);
expect(screen.getByTestId('view-link')).toHaveAttribute(
diff --git a/packages/twenty-front/src/modules/ai/constants/AnyChatReferenceCloseTagRegex.ts b/packages/twenty-front/src/modules/ai/constants/AnyChatReferenceCloseTagRegex.ts
deleted file mode 100644
index a3dbaba49f..0000000000
--- a/packages/twenty-front/src/modules/ai/constants/AnyChatReferenceCloseTagRegex.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { CHAT_REFERENCE_KINDS } from '@/ai/constants/ChatReferenceKinds';
-
-export const ANY_CHAT_REFERENCE_CLOSE_TAG_REGEX = new RegExp(
- `\\[\\[/(?:${CHAT_REFERENCE_KINDS.join('|')})\\]\\]`,
-);
diff --git a/packages/twenty-front/src/modules/ai/constants/ChatReferenceFieldPattern.ts b/packages/twenty-front/src/modules/ai/constants/ChatReferenceFieldPattern.ts
new file mode 100644
index 0000000000..7040fbe2d4
--- /dev/null
+++ b/packages/twenty-front/src/modules/ai/constants/ChatReferenceFieldPattern.ts
@@ -0,0 +1,4 @@
+import { CHAT_REFERENCE_LABEL_PATTERN } from '@/ai/constants/ChatReferenceLabelPattern';
+import { CHAT_REFERENCE_UUID_PATTERN } from '@/ai/constants/ChatReferenceUuidPattern';
+
+export const CHAT_REFERENCE_FIELD_PATTERN = `\\[\\[field:(?${CHAT_REFERENCE_UUID_PATTERN}):(?${CHAT_REFERENCE_LABEL_PATTERN})\\]\\]`;
diff --git a/packages/twenty-front/src/modules/ai/constants/ChatReferenceKinds.ts b/packages/twenty-front/src/modules/ai/constants/ChatReferenceKinds.ts
deleted file mode 100644
index 82334264ec..0000000000
--- a/packages/twenty-front/src/modules/ai/constants/ChatReferenceKinds.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export const CHAT_REFERENCE_KINDS = [
- 'record',
- 'object',
- 'field',
- 'view',
-] as const;
diff --git a/packages/twenty-front/src/modules/ai/constants/ChatReferenceLabelPattern.ts b/packages/twenty-front/src/modules/ai/constants/ChatReferenceLabelPattern.ts
new file mode 100644
index 0000000000..fb08cedfb4
--- /dev/null
+++ b/packages/twenty-front/src/modules/ai/constants/ChatReferenceLabelPattern.ts
@@ -0,0 +1,2 @@
+// Excluding brackets and line breaks stops an unclosed reference from swallowing the text and markers after it.
+export const CHAT_REFERENCE_LABEL_PATTERN = '[^\\[\\]\\n]*';
diff --git a/packages/twenty-front/src/modules/ai/constants/ChatReferenceObjectPattern.ts b/packages/twenty-front/src/modules/ai/constants/ChatReferenceObjectPattern.ts
new file mode 100644
index 0000000000..6e2b6a62a9
--- /dev/null
+++ b/packages/twenty-front/src/modules/ai/constants/ChatReferenceObjectPattern.ts
@@ -0,0 +1,4 @@
+import { CHAT_REFERENCE_LABEL_PATTERN } from '@/ai/constants/ChatReferenceLabelPattern';
+import { CHAT_REFERENCE_METADATA_NAME_PATTERN } from '@/ai/constants/ChatReferenceMetadataNamePattern';
+
+export const CHAT_REFERENCE_OBJECT_PATTERN = `\\[\\[object:(?${CHAT_REFERENCE_METADATA_NAME_PATTERN}):(?${CHAT_REFERENCE_LABEL_PATTERN})\\]\\]`;
diff --git a/packages/twenty-front/src/modules/ai/constants/ChatReferenceOpenPattern.ts b/packages/twenty-front/src/modules/ai/constants/ChatReferenceOpenPattern.ts
deleted file mode 100644
index 3edde75f9c..0000000000
--- a/packages/twenty-front/src/modules/ai/constants/ChatReferenceOpenPattern.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-// The lookbehind anchors the match to the start of a bracket run. Without it,
-// every position inside a long run is a candidate start and the greedy + makes
-// the scan quadratic in the run length; a run start always yields the same
-// match, so no valid marker is lost.
-export const CHAT_REFERENCE_OPEN_PATTERN = '(?${CHAT_REFERENCE_METADATA_NAME_PATTERN}):(?${CHAT_REFERENCE_UUID_PATTERN}):(?${CHAT_REFERENCE_LABEL_PATTERN})\\]\\]`;
diff --git a/packages/twenty-front/src/modules/ai/constants/ChatReferenceRegex.ts b/packages/twenty-front/src/modules/ai/constants/ChatReferenceRegex.ts
new file mode 100644
index 0000000000..1166c13826
--- /dev/null
+++ b/packages/twenty-front/src/modules/ai/constants/ChatReferenceRegex.ts
@@ -0,0 +1,16 @@
+import { CHAT_REFERENCE_FIELD_PATTERN } from '@/ai/constants/ChatReferenceFieldPattern';
+import { CHAT_REFERENCE_OBJECT_PATTERN } from '@/ai/constants/ChatReferenceObjectPattern';
+import { CHAT_REFERENCE_RECORD_PATTERN } from '@/ai/constants/ChatReferenceRecordPattern';
+import { CHAT_REFERENCE_VIEW_PATTERN } from '@/ai/constants/ChatReferenceViewPattern';
+
+// The record pattern must stay last: its `record:` prefix is optional, so it
+// matches the metadata markers too and would swallow them if tried first.
+export const CHAT_REFERENCE_REGEX = new RegExp(
+ [
+ CHAT_REFERENCE_OBJECT_PATTERN,
+ CHAT_REFERENCE_FIELD_PATTERN,
+ CHAT_REFERENCE_VIEW_PATTERN,
+ CHAT_REFERENCE_RECORD_PATTERN,
+ ].join('|'),
+ 'g',
+);
diff --git a/packages/twenty-front/src/modules/ai/constants/ChatReferenceStartRegex.ts b/packages/twenty-front/src/modules/ai/constants/ChatReferenceStartRegex.ts
deleted file mode 100644
index 52a98c7187..0000000000
--- a/packages/twenty-front/src/modules/ai/constants/ChatReferenceStartRegex.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { CHAT_REFERENCE_METADATA_NAME_PATTERN } from '@/ai/constants/ChatReferenceMetadataNamePattern';
-import { CHAT_REFERENCE_OPEN_PATTERN } from '@/ai/constants/ChatReferenceOpenPattern';
-import { CHAT_REFERENCE_UUID_PATTERN } from '@/ai/constants/ChatReferenceUuidPattern';
-
-// The record alternative must stay last: its `record:` prefix is optional, so it
-// matches the metadata markers too and would swallow them if tried first.
-export const CHAT_REFERENCE_START_REGEX = new RegExp(
- [
- `${CHAT_REFERENCE_OPEN_PATTERN}object:(?${CHAT_REFERENCE_METADATA_NAME_PATTERN}):`,
- `${CHAT_REFERENCE_OPEN_PATTERN}field:(?${CHAT_REFERENCE_UUID_PATTERN}):`,
- `${CHAT_REFERENCE_OPEN_PATTERN}view:(?${CHAT_REFERENCE_UUID_PATTERN}):`,
- `${CHAT_REFERENCE_OPEN_PATTERN}(?:record:)?(?${CHAT_REFERENCE_METADATA_NAME_PATTERN}):(?${CHAT_REFERENCE_UUID_PATTERN}):`,
- ].join('|'),
- 'g',
-);
diff --git a/packages/twenty-front/src/modules/ai/constants/ChatReferenceViewPattern.ts b/packages/twenty-front/src/modules/ai/constants/ChatReferenceViewPattern.ts
new file mode 100644
index 0000000000..2087aead5b
--- /dev/null
+++ b/packages/twenty-front/src/modules/ai/constants/ChatReferenceViewPattern.ts
@@ -0,0 +1,4 @@
+import { CHAT_REFERENCE_LABEL_PATTERN } from '@/ai/constants/ChatReferenceLabelPattern';
+import { CHAT_REFERENCE_UUID_PATTERN } from '@/ai/constants/ChatReferenceUuidPattern';
+
+export const CHAT_REFERENCE_VIEW_PATTERN = `\\[\\[view:(?${CHAT_REFERENCE_UUID_PATTERN}):(?${CHAT_REFERENCE_LABEL_PATTERN})\\]\\]`;
diff --git a/packages/twenty-front/src/modules/ai/types/ChatReferenceClosing.ts b/packages/twenty-front/src/modules/ai/types/ChatReferenceClosing.ts
deleted file mode 100644
index a4f422612f..0000000000
--- a/packages/twenty-front/src/modules/ai/types/ChatReferenceClosing.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export type ChatReferenceClosing = {
- index: number;
- length: number;
-};
diff --git a/packages/twenty-front/src/modules/ai/types/ChatReferenceKind.ts b/packages/twenty-front/src/modules/ai/types/ChatReferenceKind.ts
deleted file mode 100644
index c6ddeab25c..0000000000
--- a/packages/twenty-front/src/modules/ai/types/ChatReferenceKind.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { type CHAT_REFERENCE_KINDS } from '@/ai/constants/ChatReferenceKinds';
-
-export type ChatReferenceKind = (typeof CHAT_REFERENCE_KINDS)[number];
diff --git a/packages/twenty-front/src/modules/ai/types/ChatReferenceStart.ts b/packages/twenty-front/src/modules/ai/types/ChatReferenceStart.ts
deleted file mode 100644
index b59b2c1eaa..0000000000
--- a/packages/twenty-front/src/modules/ai/types/ChatReferenceStart.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { type ChatReferenceIdentity } from '@/ai/types/ChatReferenceIdentity';
-
-export type ChatReferenceStart = {
- index: number;
- prefixLength: number;
- openBracketLength: number;
- identity: ChatReferenceIdentity;
-};
diff --git a/packages/twenty-front/src/modules/ai/utils/__tests__/findChatReferences.test.ts b/packages/twenty-front/src/modules/ai/utils/__tests__/findChatReferences.test.ts
index 44b1894495..b163e88e14 100644
--- a/packages/twenty-front/src/modules/ai/utils/__tests__/findChatReferences.test.ts
+++ b/packages/twenty-front/src/modules/ai/utils/__tests__/findChatReferences.test.ts
@@ -5,43 +5,7 @@ describe('findChatReferences', () => {
expect(findChatReferences('Which company should we contact?')).toEqual([]);
});
- it('should find a tagged record reference', () => {
- expect(
- findChatReferences(
- 'Contact [[record:company:a1b2c3d4-e5f6-7890-abcd-ef1234567890:Acme[[/record]] next',
- ),
- ).toEqual([
- {
- kind: 'record',
- fullMatch:
- '[[record:company:a1b2c3d4-e5f6-7890-abcd-ef1234567890:Acme[[/record]]',
- index: 8,
- objectNameSingular: 'company',
- recordId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
- displayName: 'Acme',
- },
- ]);
- });
-
- it('should include ]] inside a tagged display name', () => {
- expect(
- findChatReferences(
- 'The company is [[record:company:a1b2c3d4-e5f6-7890-abcd-ef1234567890:[test] ]] [test] [test] ###[[/record]], created on July 21',
- ),
- ).toEqual([
- {
- kind: 'record',
- fullMatch:
- '[[record:company:a1b2c3d4-e5f6-7890-abcd-ef1234567890:[test] ]] [test] [test] ###[[/record]]',
- index: 15,
- objectNameSingular: 'company',
- recordId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
- displayName: '[test] ]] [test] [test] ###',
- },
- ]);
- });
-
- it('should still support legacy ]] terminators', () => {
+ it('should find a record reference', () => {
expect(
findChatReferences(
'Contact [[record:company:a1b2c3d4-e5f6-7890-abcd-ef1234567890:Acme]] next',
@@ -59,34 +23,15 @@ describe('findChatReferences', () => {
]);
});
- it('should still support legacy ]] inside display names', () => {
+ it('should find a record reference written without the record prefix', () => {
expect(
findChatReferences(
- 'The company is [[record:company:a1b2c3d4-e5f6-7890-abcd-ef1234567890:[test] ]] [test] [test] ###]], created on July 21',
+ 'Merge [[person:11111111-1111-1111-1111-111111111111:Alice]] into [[record:person:22222222-2222-2222-2222-222222222222:Bob]]',
),
).toEqual([
{
kind: 'record',
- fullMatch:
- '[[record:company:a1b2c3d4-e5f6-7890-abcd-ef1234567890:[test] ]] [test] [test] ###]]',
- index: 15,
- objectNameSingular: 'company',
- recordId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
- displayName: '[test] ]] [test] [test] ###',
- },
- ]);
- });
-
- it('should find multiple tagged references without consuming into the next one', () => {
- expect(
- findChatReferences(
- 'Merge [[person:11111111-1111-1111-1111-111111111111:Alice[[/record]] into [[record:person:22222222-2222-2222-2222-222222222222:Bob[[/record]]',
- ),
- ).toEqual([
- {
- kind: 'record',
- fullMatch:
- '[[person:11111111-1111-1111-1111-111111111111:Alice[[/record]]',
+ fullMatch: '[[person:11111111-1111-1111-1111-111111111111:Alice]]',
index: 6,
objectNameSingular: 'person',
recordId: '11111111-1111-1111-1111-111111111111',
@@ -94,9 +39,8 @@ describe('findChatReferences', () => {
},
{
kind: 'record',
- fullMatch:
- '[[record:person:22222222-2222-2222-2222-222222222222:Bob[[/record]]',
- index: 74,
+ fullMatch: '[[record:person:22222222-2222-2222-2222-222222222222:Bob]]',
+ index: 65,
objectNameSingular: 'person',
recordId: '22222222-2222-2222-2222-222222222222',
displayName: 'Bob',
@@ -106,11 +50,11 @@ describe('findChatReferences', () => {
it('should find an object reference', () => {
expect(
- findChatReferences('Open [[object:partner:Partners[[/object]] to start'),
+ findChatReferences('Open [[object:partner:Partners]] to start'),
).toEqual([
{
kind: 'object',
- fullMatch: '[[object:partner:Partners[[/object]]',
+ fullMatch: '[[object:partner:Partners]]',
index: 5,
objectNameSingular: 'partner',
displayName: 'Partners',
@@ -121,13 +65,12 @@ describe('findChatReferences', () => {
it('should find a field reference instead of reading it as a record', () => {
expect(
findChatReferences(
- 'The [[field:33333333-3333-3333-3333-333333333333:Stage[[/field]] field',
+ 'The [[field:33333333-3333-3333-3333-333333333333:Stage]] field',
),
).toEqual([
{
kind: 'field',
- fullMatch:
- '[[field:33333333-3333-3333-3333-333333333333:Stage[[/field]]',
+ fullMatch: '[[field:33333333-3333-3333-3333-333333333333:Stage]]',
index: 4,
fieldMetadataItemId: '33333333-3333-3333-3333-333333333333',
displayName: 'Stage',
@@ -138,13 +81,13 @@ describe('findChatReferences', () => {
it('should find a view reference instead of reading it as a record', () => {
expect(
findChatReferences(
- 'See [[view:44444444-4444-4444-4444-444444444444:All Companies[[/view]]',
+ 'See [[view:44444444-4444-4444-4444-444444444444:All Companies]]',
),
).toEqual([
{
kind: 'view',
fullMatch:
- '[[view:44444444-4444-4444-4444-444444444444:All Companies[[/view]]',
+ '[[view:44444444-4444-4444-4444-444444444444:All Companies]]',
index: 4,
viewId: '44444444-4444-4444-4444-444444444444',
displayName: 'All Companies',
@@ -155,13 +98,13 @@ describe('findChatReferences', () => {
it('should read an explicit record prefix as a record even when the object is named view', () => {
expect(
findChatReferences(
- 'Open [[record:view:44444444-4444-4444-4444-444444444444:Quarterly[[/record]]',
+ 'Open [[record:view:44444444-4444-4444-4444-444444444444:Quarterly]]',
),
).toEqual([
{
kind: 'record',
fullMatch:
- '[[record:view:44444444-4444-4444-4444-444444444444:Quarterly[[/record]]',
+ '[[record:view:44444444-4444-4444-4444-444444444444:Quarterly]]',
index: 5,
objectNameSingular: 'view',
recordId: '44444444-4444-4444-4444-444444444444',
@@ -170,48 +113,16 @@ describe('findChatReferences', () => {
]);
});
- it('should drop a metadata reference closed by a foreign tag', () => {
- expect(
- findChatReferences(
- 'See [[view:44444444-4444-4444-4444-444444444444:All Companies[[/record]]',
- ),
- ).toEqual([]);
- });
-
- it('should drop a metadata reference closed by a bare legacy terminator', () => {
- expect(
- findChatReferences('Open [[object:partner:Partners]] to start'),
- ).toEqual([]);
- });
-
- it('should not let a legacy record swallow a foreign close tag', () => {
- expect(
- findChatReferences(
- '[[record:company:a1b2c3d4-e5f6-7890-abcd-ef1234567890:Acme]] blah [[/object]]',
- ),
- ).toEqual([
- {
- kind: 'record',
- fullMatch:
- '[[record:company:a1b2c3d4-e5f6-7890-abcd-ef1234567890:Acme]]',
- index: 0,
- objectNameSingular: 'company',
- recordId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
- displayName: 'Acme',
- },
- ]);
- });
-
it('should match object names containing digits', () => {
expect(
findChatReferences(
- '[[record:company2:a1b2c3d4-e5f6-7890-abcd-ef1234567890:Acme[[/record]] and [[object:company2:Companies 2[[/object]]',
+ '[[record:company2:a1b2c3d4-e5f6-7890-abcd-ef1234567890:Acme]] and [[object:company2:Companies 2]]',
),
).toEqual([
{
kind: 'record',
fullMatch:
- '[[record:company2:a1b2c3d4-e5f6-7890-abcd-ef1234567890:Acme[[/record]]',
+ '[[record:company2:a1b2c3d4-e5f6-7890-abcd-ef1234567890:Acme]]',
index: 0,
objectNameSingular: 'company2',
recordId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
@@ -219,8 +130,8 @@ describe('findChatReferences', () => {
},
{
kind: 'object',
- fullMatch: '[[object:company2:Companies 2[[/object]]',
- index: 75,
+ fullMatch: '[[object:company2:Companies 2]]',
+ index: 66,
objectNameSingular: 'company2',
displayName: 'Companies 2',
},
@@ -229,7 +140,7 @@ describe('findChatReferences', () => {
it('should find every kind in a single string', () => {
const references = findChatReferences(
- 'The [[view:44444444-4444-4444-4444-444444444444:Pipeline[[/view]] view of [[object:partner:Partners[[/object]] groups [[record:person:11111111-1111-1111-1111-111111111111:Alice[[/record]] by [[field:33333333-3333-3333-3333-333333333333:Stage[[/field]]',
+ 'The [[view:44444444-4444-4444-4444-444444444444:Pipeline]] view of [[object:partner:Partners]] groups [[record:person:11111111-1111-1111-1111-111111111111:Alice]] by [[field:33333333-3333-3333-3333-333333333333:Stage]]',
);
expect(references.map((reference) => reference.kind)).toEqual([
@@ -248,28 +159,80 @@ describe('findChatReferences', () => {
it('should find adjacent references without a separator', () => {
const references = findChatReferences(
- '[[object:partner:Partners[[/object]][[object:company:Companies[[/object]]',
+ '[[object:partner:Partners]][[object:company:Companies]]',
);
expect(references).toHaveLength(2);
expect(references[0].displayName).toBe('Partners');
expect(references[1].displayName).toBe('Companies');
- expect(references[1].index).toBe(36);
+ expect(references[1].index).toBe(27);
+ });
+
+ it('should find a reference with an empty display name', () => {
+ expect(
+ findChatReferences(
+ '[[record:person:11111111-1111-1111-1111-111111111111:]]',
+ ),
+ ).toEqual([
+ {
+ kind: 'record',
+ fullMatch: '[[record:person:11111111-1111-1111-1111-111111111111:]]',
+ index: 0,
+ objectNameSingular: 'person',
+ recordId: '11111111-1111-1111-1111-111111111111',
+ displayName: '',
+ },
+ ]);
});
it('should drop an unclosed reference', () => {
expect(findChatReferences('Open [[object:partner:Partners')).toEqual([]);
});
- it('should consume a surplus bracket added after the closing tag', () => {
+ it('should not treat a single bracket as a terminator', () => {
+ expect(
+ findChatReferences('Open [[object:partner:Partners] to start'),
+ ).toEqual([]);
+ });
+
+ it('should not let an unclosed reference swallow the next one', () => {
+ const references = findChatReferences(
+ 'Open [[object:partner:Partners [[object:company:Companies]]',
+ );
+
+ expect(references).toHaveLength(1);
+ expect(references[0].displayName).toBe('Companies');
+ });
+
+ it('should not close a reference across a line break', () => {
+ expect(
+ findChatReferences('Open [[object:partner:Partners\nand others]]'),
+ ).toEqual([]);
+ });
+
+ it('should drop a reference whose display name contains brackets', () => {
expect(
findChatReferences(
- 'Created [[object:opportunity:Opportunities[[/object]]].',
+ 'Open [[record:company:a1b2c3d4-e5f6-7890-abcd-ef1234567890:[test] name]]',
),
+ ).toEqual([]);
+ });
+
+ it('should drop a reference closed by a retired closing tag', () => {
+ expect(
+ findChatReferences(
+ 'Contact [[record:company:a1b2c3d4-e5f6-7890-abcd-ef1234567890:Acme[[/record]] and [[object:partner:Partners[[/object]] and [[field:33333333-3333-3333-3333-333333333333:Stage[[/field]] and [[view:44444444-4444-4444-4444-444444444444:Pipeline[[/view]]',
+ ),
+ ).toEqual([]);
+ });
+
+ it('should leave a surplus bracket added after a reference out of the match', () => {
+ expect(
+ findChatReferences('Created [[object:opportunity:Opportunities]]].'),
).toEqual([
{
kind: 'object',
- fullMatch: '[[object:opportunity:Opportunities[[/object]]]',
+ fullMatch: '[[object:opportunity:Opportunities]]',
index: 8,
objectNameSingular: 'opportunity',
displayName: 'Opportunities',
@@ -277,30 +240,38 @@ describe('findChatReferences', () => {
]);
});
- it('should consume the extra brackets of an over-wrapped reference', () => {
+ it('should find a reference whose display name contains markdown characters', () => {
expect(
- findChatReferences('Created [[[object:partner:Partners[[/object]]]] now'),
+ findChatReferences(
+ 'See [[record:workflow:a1b2c3d4-e5f6-7890-abcd-ef1234567890:Workflow `UPDATE_RECORD` step]]',
+ ),
).toEqual([
{
- kind: 'object',
- fullMatch: '[[[object:partner:Partners[[/object]]]]',
- index: 8,
- objectNameSingular: 'partner',
- displayName: 'Partners',
+ kind: 'record',
+ fullMatch:
+ '[[record:workflow:a1b2c3d4-e5f6-7890-abcd-ef1234567890:Workflow `UPDATE_RECORD` step]]',
+ index: 4,
+ objectNameSingular: 'workflow',
+ recordId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
+ displayName: 'Workflow `UPDATE_RECORD` step',
},
]);
});
- it('should not consume a bracket separated from the closing tag', () => {
+ it('should find a reference whose display name contains a colon', () => {
expect(
- findChatReferences('Created [[object:partner:Partners[[/object]] ] now'),
+ findChatReferences(
+ 'Ping [[record:person:a1b2c3d4-e5f6-7890-abcd-ef1234567890:Doe: Jane]]',
+ ),
).toEqual([
{
- kind: 'object',
- fullMatch: '[[object:partner:Partners[[/object]]',
- index: 8,
- objectNameSingular: 'partner',
- displayName: 'Partners',
+ kind: 'record',
+ fullMatch:
+ '[[record:person:a1b2c3d4-e5f6-7890-abcd-ef1234567890:Doe: Jane]]',
+ index: 5,
+ objectNameSingular: 'person',
+ recordId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
+ displayName: 'Doe: Jane',
},
]);
});
diff --git a/packages/twenty-front/src/modules/ai/utils/__tests__/formatRecordReference.test.ts b/packages/twenty-front/src/modules/ai/utils/__tests__/formatRecordReference.test.ts
index 1caae8b38d..2ab63543de 100644
--- a/packages/twenty-front/src/modules/ai/utils/__tests__/formatRecordReference.test.ts
+++ b/packages/twenty-front/src/modules/ai/utils/__tests__/formatRecordReference.test.ts
@@ -1,15 +1,13 @@
import { formatRecordReference } from '@/ai/utils/formatRecordReference';
describe('formatRecordReference', () => {
- it('should use the [[/record]] close tag', () => {
+ it('should close the reference with ]]', () => {
expect(
formatRecordReference({
objectNameSingular: 'company',
recordId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
- displayName: '[test] ]] [test]',
+ displayName: 'Acme',
}),
- ).toBe(
- '[[record:company:a1b2c3d4-e5f6-7890-abcd-ef1234567890:[test] ]] [test][[/record]]',
- );
+ ).toBe('[[record:company:a1b2c3d4-e5f6-7890-abcd-ef1234567890:Acme]]');
});
});
diff --git a/packages/twenty-front/src/modules/ai/utils/__tests__/getMarkdownBlocksIncrementally.test.ts b/packages/twenty-front/src/modules/ai/utils/__tests__/getMarkdownBlocksIncrementally.test.ts
index a6928ab13e..f4c75b9190 100644
--- a/packages/twenty-front/src/modules/ai/utils/__tests__/getMarkdownBlocksIncrementally.test.ts
+++ b/packages/twenty-front/src/modules/ai/utils/__tests__/getMarkdownBlocksIncrementally.test.ts
@@ -38,7 +38,7 @@ const FIXTURES: Record = {
'# H1\n\ntext\n\n## H2\n\n- list\n- items\n\n> quote\n> more quote\n\nend',
blockquotes: '> a\n\n> b\n\ntext',
chatReferences:
- 'Check [[field:12345678-1234-5678-abcd-123456789012:Annual Revenue[[/field]] and\n\n- [[view:12345678-1234-5678-abcd-123456789012:All[[/view]]\n\n| [[object:company:Companies[[/object]] | x |\n|---|---|\n| a | b |',
+ 'Check [[field:12345678-1234-5678-abcd-123456789012:Annual Revenue]] and\n\n- [[view:12345678-1234-5678-abcd-123456789012:All]]\n\n| [[object:company:Companies]] | x |\n|---|---|\n| a | b |',
windowsLineEndings: 'line one\r\n\r\n- a\r\n\r\n- b\r\n\r\nend',
};
diff --git a/packages/twenty-front/src/modules/ai/utils/__tests__/getSurplusCloseBracketLength.test.ts b/packages/twenty-front/src/modules/ai/utils/__tests__/getSurplusCloseBracketLength.test.ts
deleted file mode 100644
index 641d73f0d6..0000000000
--- a/packages/twenty-front/src/modules/ai/utils/__tests__/getSurplusCloseBracketLength.test.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import { getSurplusCloseBracketLength } from '@/ai/utils/getSurplusCloseBracketLength';
-
-describe('getSurplusCloseBracketLength', () => {
- it('should return zero when the closing tag is not followed by a bracket', () => {
- expect(
- getSurplusCloseBracketLength({
- textAfterClosing: ' next',
- openBracketLength: 2,
- }),
- ).toBe(0);
- });
-
- it('should return zero when a bracket is separated from the closing tag', () => {
- expect(
- getSurplusCloseBracketLength({
- textAfterClosing: ' ] next',
- openBracketLength: 2,
- }),
- ).toBe(0);
- });
-
- it('should count the brackets following the closing tag', () => {
- expect(
- getSurplusCloseBracketLength({
- textAfterClosing: ']. next',
- openBracketLength: 2,
- }),
- ).toBe(1);
- });
-
- it('should count no more brackets than the reference opened with', () => {
- expect(
- getSurplusCloseBracketLength({
- textAfterClosing: ']]] next',
- openBracketLength: 2,
- }),
- ).toBe(2);
-
- expect(
- getSurplusCloseBracketLength({
- textAfterClosing: ']]] next',
- openBracketLength: 3,
- }),
- ).toBe(3);
- });
-});
diff --git a/packages/twenty-front/src/modules/ai/utils/__tests__/protectChatReferencesForMarkdown.test.ts b/packages/twenty-front/src/modules/ai/utils/__tests__/protectChatReferencesForMarkdown.test.ts
index cb521374fc..ea18219872 100644
--- a/packages/twenty-front/src/modules/ai/utils/__tests__/protectChatReferencesForMarkdown.test.ts
+++ b/packages/twenty-front/src/modules/ai/utils/__tests__/protectChatReferencesForMarkdown.test.ts
@@ -7,97 +7,107 @@ describe('protectChatReferencesForMarkdown', () => {
).toBe('Which company should we contact?');
});
- it('should rewrite legacy refs to the tagged format', () => {
+ it('should leave a reference with a plain label unchanged', () => {
expect(
protectChatReferencesForMarkdown(
'Contact [[record:company:a1b2c3d4-e5f6-7890-abcd-ef1234567890:Acme]] next',
),
).toBe(
- 'Contact [[record:company:a1b2c3d4-e5f6-7890-abcd-ef1234567890:Acme[[/record]] next',
+ 'Contact [[record:company:a1b2c3d4-e5f6-7890-abcd-ef1234567890:Acme]] next',
);
});
- it('should escape backticks in labels and close with the tag', () => {
+ it('should add the record prefix to a reference written without it', () => {
expect(
protectChatReferencesForMarkdown(
- 'See [[record:workflow:a1b2c3d4-e5f6-7890-abcd-ef1234567890:Workflow `UPDATE_RECORD` step[[/record]]',
+ 'Contact [[company:a1b2c3d4-e5f6-7890-abcd-ef1234567890:Acme]] next',
),
).toBe(
- 'See [[record:workflow:a1b2c3d4-e5f6-7890-abcd-ef1234567890:Workflow \\`UPDATE\\_RECORD\\` step[[/record]]',
+ 'Contact [[record:company:a1b2c3d4-e5f6-7890-abcd-ef1234567890:Acme]] next',
);
});
- it('should escape square brackets and ]] inside labels', () => {
+ it('should escape backticks in labels', () => {
expect(
protectChatReferencesForMarkdown(
- 'Open [[record:company:a1b2c3d4-e5f6-7890-abcd-ef1234567890:[test] ]] [test] [test] ###[[/record]]',
+ 'See [[record:workflow:a1b2c3d4-e5f6-7890-abcd-ef1234567890:Workflow `UPDATE_RECORD` step]]',
),
).toBe(
- 'Open [[record:company:a1b2c3d4-e5f6-7890-abcd-ef1234567890:\\[test\\] \\]\\] \\[test\\] \\[test\\] \\#\\#\\#[[/record]]',
+ 'See [[record:workflow:a1b2c3d4-e5f6-7890-abcd-ef1234567890:Workflow \\`UPDATE\\_RECORD\\` step]]',
);
});
it('should leave colons in labels unchanged', () => {
expect(
protectChatReferencesForMarkdown(
- 'Ping [[record:person:a1b2c3d4-e5f6-7890-abcd-ef1234567890:Doe: Jane[[/record]]',
+ 'Ping [[record:person:a1b2c3d4-e5f6-7890-abcd-ef1234567890:Doe: Jane]]',
),
).toBe(
- 'Ping [[record:person:a1b2c3d4-e5f6-7890-abcd-ef1234567890:Doe: Jane[[/record]]',
+ 'Ping [[record:person:a1b2c3d4-e5f6-7890-abcd-ef1234567890:Doe: Jane]]',
);
});
it('should escape an object label without touching its name', () => {
expect(
protectChatReferencesForMarkdown(
- 'Open [[object:partner:Partners (EMEA)[[/object]]',
+ 'Open [[object:partner:Partners (EMEA)]]',
),
- ).toBe('Open [[object:partner:Partners \\(EMEA\\)[[/object]]');
+ ).toBe('Open [[object:partner:Partners \\(EMEA\\)]]');
});
it('should escape a field label without touching its id', () => {
expect(
protectChatReferencesForMarkdown(
- 'The [[field:33333333-3333-3333-3333-333333333333:Next step[[/field]] field',
+ 'The [[field:33333333-3333-3333-3333-333333333333:Next step]] field',
),
).toBe(
- 'The [[field:33333333-3333-3333-3333-333333333333:Next step[[/field]] field',
+ 'The [[field:33333333-3333-3333-3333-333333333333:Next step]] field',
);
});
it('should escape a view label without touching its id', () => {
expect(
protectChatReferencesForMarkdown(
- 'See [[view:44444444-4444-4444-4444-444444444444:Q1 - pipeline[[/view]]',
+ 'See [[view:44444444-4444-4444-4444-444444444444:Q1 - pipeline]]',
+ ),
+ ).toBe('See [[view:44444444-4444-4444-4444-444444444444:Q1 \\- pipeline]]');
+ });
+
+ it('should leave a marker whose label contains brackets unchanged', () => {
+ expect(
+ protectChatReferencesForMarkdown(
+ 'Open [[record:company:a1b2c3d4-e5f6-7890-abcd-ef1234567890:[test] name]]',
),
).toBe(
- 'See [[view:44444444-4444-4444-4444-444444444444:Q1 \\- pipeline[[/view]]',
+ 'Open [[record:company:a1b2c3d4-e5f6-7890-abcd-ef1234567890:[test] name]]',
);
});
- it('should drop a surplus bracket added after the closing tag', () => {
+ it('should leave a marker using a retired closing tag unchanged', () => {
expect(
protectChatReferencesForMarkdown(
- 'Created [[object:opportunity:Opportunities[[/object]]].',
+ 'The [[field:33333333-3333-3333-3333-333333333333:Stage[[/field]] field',
),
- ).toBe('Created [[object:opportunity:Opportunities[[/object]].');
+ ).toBe(
+ 'The [[field:33333333-3333-3333-3333-333333333333:Stage[[/field]] field',
+ );
});
- it('should drop the extra brackets of an over-wrapped reference', () => {
+ it('should keep a surplus bracket added after a reference', () => {
expect(
protectChatReferencesForMarkdown(
- 'Created [[[object:opportunity:Opportunities[[/object]]]] now',
+ 'Created [[object:opportunity:Opportunities]]].',
),
- ).toBe('Created [[object:opportunity:Opportunities[[/object]] now');
+ ).toBe('Created [[object:opportunity:Opportunities]]].');
});
it('should rewrite every kind in a mixed string', () => {
expect(
protectChatReferencesForMarkdown(
- 'The [[view:44444444-4444-4444-4444-444444444444:Pipeline[[/view]] view of [[object:partner:Partners[[/object]] groups [[record:person:11111111-1111-1111-1111-111111111111:Alice]] by [[field:33333333-3333-3333-3333-333333333333:Stage[[/field]]',
+ 'The [[view:44444444-4444-4444-4444-444444444444:Pipeline]] view of [[object:partner:Partners]] groups [[person:11111111-1111-1111-1111-111111111111:Alice]] by [[field:33333333-3333-3333-3333-333333333333:Stage]]',
),
).toBe(
- 'The [[view:44444444-4444-4444-4444-444444444444:Pipeline[[/view]] view of [[object:partner:Partners[[/object]] groups [[record:person:11111111-1111-1111-1111-111111111111:Alice[[/record]] by [[field:33333333-3333-3333-3333-333333333333:Stage[[/field]]',
+ 'The [[view:44444444-4444-4444-4444-444444444444:Pipeline]] view of [[object:partner:Partners]] groups [[record:person:11111111-1111-1111-1111-111111111111:Alice]] by [[field:33333333-3333-3333-3333-333333333333:Stage]]',
);
});
});
diff --git a/packages/twenty-front/src/modules/ai/utils/findChatReferenceClosing.ts b/packages/twenty-front/src/modules/ai/utils/findChatReferenceClosing.ts
deleted file mode 100644
index 25e7ee4650..0000000000
--- a/packages/twenty-front/src/modules/ai/utils/findChatReferenceClosing.ts
+++ /dev/null
@@ -1,44 +0,0 @@
-import { ANY_CHAT_REFERENCE_CLOSE_TAG_REGEX } from '@/ai/constants/AnyChatReferenceCloseTagRegex';
-import { type ChatReferenceClosing } from '@/ai/types/ChatReferenceClosing';
-import { type ChatReferenceKind } from '@/ai/types/ChatReferenceKind';
-import { getChatReferenceCloseTag } from '@/ai/utils/getChatReferenceCloseTag';
-import { isDefined } from 'twenty-shared/utils';
-
-const LEGACY_RECORD_REFERENCE_CLOSE_TAG = ']]';
-
-export const findChatReferenceClosing = ({
- displayNameWindow,
- kind,
-}: {
- displayNameWindow: string;
- kind: ChatReferenceKind;
-}): ChatReferenceClosing | undefined => {
- const closeTag = getChatReferenceCloseTag(kind);
- const closeTagIndex = displayNameWindow.indexOf(closeTag);
-
- if (closeTagIndex !== -1) {
- return { index: closeTagIndex, length: closeTag.length };
- }
-
- if (kind !== 'record') {
- return undefined;
- }
-
- const foreignCloseTagMatch =
- ANY_CHAT_REFERENCE_CLOSE_TAG_REGEX.exec(displayNameWindow);
- const legacySearchSpace = isDefined(foreignCloseTagMatch)
- ? displayNameWindow.slice(0, foreignCloseTagMatch.index)
- : displayNameWindow;
- const legacyCloseIndex = legacySearchSpace.lastIndexOf(
- LEGACY_RECORD_REFERENCE_CLOSE_TAG,
- );
-
- if (legacyCloseIndex === -1) {
- return undefined;
- }
-
- return {
- index: legacyCloseIndex,
- length: LEGACY_RECORD_REFERENCE_CLOSE_TAG.length,
- };
-};
diff --git a/packages/twenty-front/src/modules/ai/utils/findChatReferences.ts b/packages/twenty-front/src/modules/ai/utils/findChatReferences.ts
index 6deb3f0969..34cdb9f973 100644
--- a/packages/twenty-front/src/modules/ai/utils/findChatReferences.ts
+++ b/packages/twenty-front/src/modules/ai/utils/findChatReferences.ts
@@ -1,59 +1,13 @@
-import { CHAT_REFERENCE_START_REGEX } from '@/ai/constants/ChatReferenceStartRegex';
+import { CHAT_REFERENCE_REGEX } from '@/ai/constants/ChatReferenceRegex';
import { type ChatReferenceMatch } from '@/ai/types/ChatReferenceMatch';
-import { type ChatReferenceStart } from '@/ai/types/ChatReferenceStart';
-import { findChatReferenceClosing } from '@/ai/utils/findChatReferenceClosing';
-import { getChatReferenceStartFromMatch } from '@/ai/utils/getChatReferenceStartFromMatch';
-import { getSurplusCloseBracketLength } from '@/ai/utils/getSurplusCloseBracketLength';
-import { isDefined } from 'twenty-shared/utils';
+import { getChatReferenceMatchFromRegexMatch } from '@/ai/utils/getChatReferenceMatchFromRegexMatch';
export const findChatReferences = (text: string): ChatReferenceMatch[] => {
if (!text.includes('[[')) {
return [];
}
- const starts: ChatReferenceStart[] = [];
-
- CHAT_REFERENCE_START_REGEX.lastIndex = 0;
-
- let startMatch;
-
- while ((startMatch = CHAT_REFERENCE_START_REGEX.exec(text)) !== null) {
- starts.push(getChatReferenceStartFromMatch(startMatch));
- }
-
- return starts.flatMap((start, startIndex) => {
- const displayNameStart = start.index + start.prefixLength;
- const windowEnd =
- startIndex + 1 < starts.length
- ? starts[startIndex + 1].index
- : text.length;
- const displayNameWindow = text.slice(displayNameStart, windowEnd);
-
- const closing = findChatReferenceClosing({
- displayNameWindow,
- kind: start.identity.kind,
- });
-
- if (!isDefined(closing)) {
- return [];
- }
-
- const closingEnd = closing.index + closing.length;
- const surplusCloseBracketLength = getSurplusCloseBracketLength({
- textAfterClosing: displayNameWindow.slice(closingEnd),
- openBracketLength: start.openBracketLength,
- });
-
- return [
- {
- ...start.identity,
- fullMatch: text.slice(
- start.index,
- displayNameStart + closingEnd + surplusCloseBracketLength,
- ),
- index: start.index,
- displayName: displayNameWindow.slice(0, closing.index),
- },
- ];
- });
+ return [...text.matchAll(CHAT_REFERENCE_REGEX)].map((match) =>
+ getChatReferenceMatchFromRegexMatch(match),
+ );
};
diff --git a/packages/twenty-front/src/modules/ai/utils/formatChatReference.ts b/packages/twenty-front/src/modules/ai/utils/formatChatReference.ts
index debbb46bc6..9ce2613f79 100644
--- a/packages/twenty-front/src/modules/ai/utils/formatChatReference.ts
+++ b/packages/twenty-front/src/modules/ai/utils/formatChatReference.ts
@@ -1,5 +1,4 @@
import { type ChatReferenceIdentity } from '@/ai/types/ChatReferenceIdentity';
-import { getChatReferenceCloseTag } from '@/ai/utils/getChatReferenceCloseTag';
import { getChatReferenceIdentitySegment } from '@/ai/utils/getChatReferenceIdentitySegment';
import { formatRecordReference } from 'twenty-shared/ai';
@@ -10,5 +9,5 @@ export const formatChatReference = (
return formatRecordReference(reference);
}
- return `[[${reference.kind}:${getChatReferenceIdentitySegment(reference)}:${reference.displayName}${getChatReferenceCloseTag(reference.kind)}`;
+ return `[[${reference.kind}:${getChatReferenceIdentitySegment(reference)}:${reference.displayName}]]`;
};
diff --git a/packages/twenty-front/src/modules/ai/utils/getChatReferenceCloseTag.ts b/packages/twenty-front/src/modules/ai/utils/getChatReferenceCloseTag.ts
deleted file mode 100644
index 3eea347fac..0000000000
--- a/packages/twenty-front/src/modules/ai/utils/getChatReferenceCloseTag.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-import { type ChatReferenceKind } from '@/ai/types/ChatReferenceKind';
-
-export const getChatReferenceCloseTag = (kind: ChatReferenceKind): string =>
- `[[/${kind}]]`;
diff --git a/packages/twenty-front/src/modules/ai/utils/getChatReferenceMatchFromRegexMatch.ts b/packages/twenty-front/src/modules/ai/utils/getChatReferenceMatchFromRegexMatch.ts
new file mode 100644
index 0000000000..7b0c7fa804
--- /dev/null
+++ b/packages/twenty-front/src/modules/ai/utils/getChatReferenceMatchFromRegexMatch.ts
@@ -0,0 +1,55 @@
+import { type ChatReferenceMatch } from '@/ai/types/ChatReferenceMatch';
+import { isDefined } from 'twenty-shared/utils';
+
+export const getChatReferenceMatchFromRegexMatch = (
+ match: RegExpExecArray,
+): ChatReferenceMatch => {
+ const {
+ objectNameSingular,
+ objectLabel,
+ fieldMetadataItemId,
+ fieldLabel,
+ viewId,
+ viewLabel,
+ recordObjectNameSingular,
+ recordId,
+ recordLabel,
+ } = match.groups ?? {};
+
+ const position = { fullMatch: match[0], index: match.index };
+
+ if (isDefined(objectNameSingular)) {
+ return {
+ ...position,
+ kind: 'object',
+ objectNameSingular,
+ displayName: objectLabel,
+ };
+ }
+
+ if (isDefined(fieldMetadataItemId)) {
+ return {
+ ...position,
+ kind: 'field',
+ fieldMetadataItemId,
+ displayName: fieldLabel,
+ };
+ }
+
+ if (isDefined(viewId)) {
+ return {
+ ...position,
+ kind: 'view',
+ viewId,
+ displayName: viewLabel,
+ };
+ }
+
+ return {
+ ...position,
+ kind: 'record',
+ objectNameSingular: recordObjectNameSingular,
+ recordId,
+ displayName: recordLabel,
+ };
+};
diff --git a/packages/twenty-front/src/modules/ai/utils/getChatReferenceStartFromMatch.ts b/packages/twenty-front/src/modules/ai/utils/getChatReferenceStartFromMatch.ts
deleted file mode 100644
index ce0f475314..0000000000
--- a/packages/twenty-front/src/modules/ai/utils/getChatReferenceStartFromMatch.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { type ChatReferenceStart } from '@/ai/types/ChatReferenceStart';
-import { isDefined } from 'twenty-shared/utils';
-
-const OPEN_BRACKETS_REGEX = /^\[+/;
-
-export const getChatReferenceStartFromMatch = (
- match: RegExpExecArray,
-): ChatReferenceStart => {
- const {
- objectNameSingular,
- fieldMetadataItemId,
- viewId,
- recordObjectNameSingular,
- recordId,
- } = match.groups ?? {};
-
- const openBracketsMatch = OPEN_BRACKETS_REGEX.exec(match[0]);
-
- const position = {
- index: match.index,
- prefixLength: match[0].length,
- openBracketLength: isDefined(openBracketsMatch)
- ? openBracketsMatch[0].length
- : 0,
- };
-
- if (isDefined(objectNameSingular)) {
- return { ...position, identity: { kind: 'object', objectNameSingular } };
- }
-
- if (isDefined(fieldMetadataItemId)) {
- return { ...position, identity: { kind: 'field', fieldMetadataItemId } };
- }
-
- if (isDefined(viewId)) {
- return { ...position, identity: { kind: 'view', viewId } };
- }
-
- return {
- ...position,
- identity: {
- kind: 'record',
- objectNameSingular: recordObjectNameSingular,
- recordId,
- },
- };
-};
diff --git a/packages/twenty-front/src/modules/ai/utils/getSurplusCloseBracketLength.ts b/packages/twenty-front/src/modules/ai/utils/getSurplusCloseBracketLength.ts
deleted file mode 100644
index 3baa361d27..0000000000
--- a/packages/twenty-front/src/modules/ai/utils/getSurplusCloseBracketLength.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { isDefined } from 'twenty-shared/utils';
-
-const SURPLUS_CLOSE_BRACKETS_REGEX = /^\]+/;
-
-export const getSurplusCloseBracketLength = ({
- textAfterClosing,
- openBracketLength,
-}: {
- textAfterClosing: string;
- openBracketLength: number;
-}): number => {
- const surplusMatch = SURPLUS_CLOSE_BRACKETS_REGEX.exec(textAfterClosing);
-
- if (!isDefined(surplusMatch)) {
- return 0;
- }
-
- return Math.min(surplusMatch[0].length, openBracketLength);
-};
diff --git a/packages/twenty-front/src/modules/mention/extensions/__tests__/MentionSuggestion.test.ts b/packages/twenty-front/src/modules/mention/extensions/__tests__/MentionSuggestion.test.ts
index af973af4aa..25afe3fc8b 100644
--- a/packages/twenty-front/src/modules/mention/extensions/__tests__/MentionSuggestion.test.ts
+++ b/packages/twenty-front/src/modules/mention/extensions/__tests__/MentionSuggestion.test.ts
@@ -82,7 +82,7 @@ describe('MentionSuggestion', () => {
const text = editor.getText();
- expect(text).toContain('[[record:company:test-id:Acme[[/record]]');
+ expect(text).toContain('[[record:company:test-id:Acme]]');
});
it('should accept @ character in editor content', () => {
diff --git a/packages/twenty-front/src/modules/mention/extensions/__tests__/MentionTag.test.ts b/packages/twenty-front/src/modules/mention/extensions/__tests__/MentionTag.test.ts
index 5214eb1cce..6ab95dffbb 100644
--- a/packages/twenty-front/src/modules/mention/extensions/__tests__/MentionTag.test.ts
+++ b/packages/twenty-front/src/modules/mention/extensions/__tests__/MentionTag.test.ts
@@ -67,9 +67,7 @@ describe('MentionTag', () => {
const text = editor.getText();
- expect(text).toBe(
- 'Hello [[record:company:abc-123:Acme Corp[[/record]] world',
- );
+ expect(text).toBe('Hello [[record:company:abc-123:Acme Corp]] world');
});
it('should handle mentions with empty label', () => {
@@ -95,7 +93,7 @@ describe('MentionTag', () => {
const text = editor.getText();
- expect(text).toBe('[[record:person:id-456:[[/record]]');
+ expect(text).toBe('[[record:person:id-456:]]');
});
it('should serialize multiple mentions in the same paragraph', () => {
@@ -132,7 +130,7 @@ describe('MentionTag', () => {
const text = editor.getText();
expect(text).toBe(
- '[[record:person:r1:Alice[[/record]] and [[record:company:r2:Beta Inc[[/record]]',
+ '[[record:person:r1:Alice]] and [[record:company:r2:Beta Inc]]',
);
});
});
@@ -158,9 +156,7 @@ describe('MentionTag', () => {
const text = editor.getText();
- expect(text).toContain(
- '[[record:opportunity:test-id:Big Deal[[/record]]',
- );
+ expect(text).toContain('[[record:opportunity:test-id:Big Deal]]');
});
});
diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/constants/chat-system-prompts.const.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/constants/chat-system-prompts.const.ts
index b92d0e0c2d..cc166db496 100644
--- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/constants/chat-system-prompts.const.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-chat/constants/chat-system-prompts.const.ts
@@ -79,8 +79,8 @@ Format responses with markdown for clarity (headings, lists, code blocks, tables
Record References - IMPORTANT:
- Tool responses include a "recordReferences" array with clickable links
- ONLY use record references that are returned by tools - NEVER make up IDs
-- Copy the exact format from the tool response: [[record:objectName:recordId:displayName[[/record]]
-- Example: [[record:company:abc12345-1234-5678-abcd-123456789012:Acme Corp[[/record]]
+- Copy the exact format from the tool response: [[record:objectName:recordId:displayName]]
+- Example: [[record:company:abc12345-1234-5678-abcd-123456789012:Acme Corp]]
- Use record references only in paragraphs, lists, or markdown tables (\`| ... |\`); never in headings, code, links, or raw HTML
- The recordId MUST be a real UUID (like "abc12345-1234-5678-abcd-123456789012")
- DO NOT create record references before calling the tool
@@ -90,20 +90,20 @@ Record References - IMPORTANT:
Metadata References:
Whenever you name an object, a field, or a view in your prose, write it as a metadata reference instead of plain text. Each one becomes a chip the user can click.
-- Object: [[object:objectNameSingular:displayName[[/object]]
- - Example: [[object:company:Companies[[/object]]
+- Object: [[object:objectNameSingular:displayName]]
+ - Example: [[object:company:Companies]]
- Use the \`nameSingular\` from \`get_object_metadata\` or \`create_object_metadata\` (NOT the label, NOT the plural, NOT the id)
- This is the only reference you may write for something that does not exist yet: when you propose creating an object, reference it with the \`nameSingular\` you intend to use and it renders as a chip without a link
-- Field: [[field:fieldMetadataId:displayName[[/field]]
- - Example: [[field:abc12345-1234-5678-abcd-123456789012:Annual Recurring Revenue[[/field]]
+- Field: [[field:fieldMetadataId:displayName]]
+ - Example: [[field:abc12345-1234-5678-abcd-123456789012:Annual Recurring Revenue]]
- Use the \`id\` returned by \`get_field_metadata\`, \`create_field_metadata\`, or the \`fields\` array of \`get_object_metadata\`
-- View: [[view:viewId:displayName[[/view]]
- - Example: [[view:abc12345-1234-5678-abcd-123456789012:All Companies[[/view]]
+- View: [[view:viewId:displayName]]
+ - Example: [[view:abc12345-1234-5678-abcd-123456789012:All Companies]]
- Use the \`id\` returned by \`get_views\`, \`create_view\`, or \`upsert_complete_view\`
- The displayName is what the user reads, so use the human-readable label ("Annual Recurring Revenue"), not the technical name
+- The displayName must stay on a single line and must not contain \`[\` or \`]\` - leave those characters out if a name includes them
- Field and view ids MUST be real UUIDs copied from a tool response - never invent one, and never reference a field or view before the tool that returns it has run
-- Always close a reference with its own tag: \`[[/object]]\`, \`[[/field]]\`, \`[[/view]]\`. A mismatched closing tag drops the chip
-- A reference is complete as written: never wrap it in extra square brackets, and never add \`]\` or \`]]\` after its closing tag
+- A reference ends with the \`]]\` right after the displayName: never wrap it in extra square brackets, and never add \`]\` or \`]]\` after it
- Use metadata references only in paragraphs, lists, or markdown tables (\`| ... |\`); never in headings, code, links, or raw HTML`,
};
diff --git a/packages/twenty-shared/src/ai/utils/__tests__/format-record-reference.util.spec.ts b/packages/twenty-shared/src/ai/utils/__tests__/format-record-reference.util.spec.ts
index ca99a3c31d..3dddcbcb22 100644
--- a/packages/twenty-shared/src/ai/utils/__tests__/format-record-reference.util.spec.ts
+++ b/packages/twenty-shared/src/ai/utils/__tests__/format-record-reference.util.spec.ts
@@ -6,8 +6,8 @@ describe('formatRecordReference', () => {
formatRecordReference({
objectNameSingular: 'company',
recordId: 'record-id',
- displayName: '[test] ]] [test]',
+ displayName: 'Acme Corp',
}),
- ).toBe('[[record:company:record-id:[test] ]] [test][[/record]]');
+ ).toBe('[[record:company:record-id:Acme Corp]]');
});
});
diff --git a/packages/twenty-shared/src/ai/utils/format-record-reference.util.ts b/packages/twenty-shared/src/ai/utils/format-record-reference.util.ts
index 84adc538a7..710ffe00e8 100644
--- a/packages/twenty-shared/src/ai/utils/format-record-reference.util.ts
+++ b/packages/twenty-shared/src/ai/utils/format-record-reference.util.ts
@@ -6,5 +6,4 @@ export const formatRecordReference = ({
objectNameSingular: string;
recordId: string;
displayName: string;
-}): string =>
- `[[record:${objectNameSingular}:${recordId}:${displayName}[[/record]]`;
+}): string => `[[record:${objectNameSingular}:${recordId}:${displayName}]]`;
diff --git a/packages/twenty-shared/src/utils/tiptap/__tests__/tiptap-document.test.ts b/packages/twenty-shared/src/utils/tiptap/__tests__/tiptap-document.test.ts
index c031c32462..ba534ea77e 100644
--- a/packages/twenty-shared/src/utils/tiptap/__tests__/tiptap-document.test.ts
+++ b/packages/twenty-shared/src/utils/tiptap/__tests__/tiptap-document.test.ts
@@ -112,7 +112,7 @@ describe('TipTap document primitives', () => {
[
'## Overview',
'',
- 'Hello [**Ada**](https://example.com) [[record:person:person-id:Ada Lovelace[[/record]]',
+ 'Hello [**Ada**](https://example.com) [[record:person:person-id:Ada Lovelace]]',
'',
'- First',
'- Second',