Commit Graph

1078 Commits

Author SHA1 Message Date
Thomas Trompette 00208dbf1f Build lambda error - catch user code compilation errors (#19516)
Fixes
https://twenty-v7.sentry.io/issues/7335177730/events/latest/?environment=prod&project=4507072499810304&query=is%3Aunresolved&referrer=latest-event

- Provide a more explicit error for user
- Remove from sentry while keeping others due to infra issues
2026-04-09 16:28:27 +00:00
Charles Bochet 07745947ee Fix rolesPermissions cache query cartesian product (62k → 162 rows) (#19511)
## Summary
- **Split the `rolesPermissions` cache query into parallel queries** to
eliminate a 5-way LEFT JOIN cartesian product. A workspace with a role
having 5 objectPermissions × 4 permissionFlags × 124 fieldPermissions ×
5 RLP predicates × 5 RLP groups was returning 62,000 rows from just ~162
distinct records, causing 10s+ query timeouts on view updates.
- **Added missing `roleId` index on `permissionFlag` table** — the only
permission table without one, which was causing sequential scans on the
JOIN.
2026-04-09 16:13:33 +00:00
nitin 50c5a1a8de [AI] new model tab design (#19384)
closes
https://discord.com/channels/1130383047699738754/1480979892610007121

<img width="1839" height="1316" alt="CleanShot 2026-04-07 at 15 22 38"
src="https://github.com/user-attachments/assets/d8f6048c-4e0f-425f-b7ea-4913d116020e"
/>
2026-04-09 14:15:43 +00:00
Baptiste Devessier 16e145b036 Fix moving a widget to another tab (#19450)
https://github.com/user-attachments/assets/aac81e79-7f2f-4a34-bf68-c76061086821

---------

Co-authored-by: Weiko <corentin@twenty.com>
2026-04-09 15:59:04 +02:00
neo773 1e908e5f0f fix: SendEmail workflow ConnectedAccount query (#19484) 2026-04-09 15:58:12 +02:00
Raphaël Bosi 19f11b1216 [COMMAND MENU ITEMS] Add dynamic label and icon to command menu navigation items (#19452) 2026-04-09 12:00:49 +00:00
Raphaël Bosi 9080180156 [COMMAND MENU ITEMS] Sync object metadata with navigation command menu items (#19456)
When an object is created or enabled we create a navigation command menu
item to that object. And when the object is disabled or deleted, we
remove this command menu item.
2026-04-09 08:18:49 +00:00
github-actions[bot] c27e3cd1a0 chore: sync AI model catalog from models.dev (#19478)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

This PR updates pricing, context windows, and model availability based
on the latest data.
New models meeting inclusion criteria (tool calling, pricing data,
context limits) are added automatically.
Deprecated models are detected based on cost-efficiency within the same
model family.

**Please review before merging** — verify no critical models were
incorrectly deprecated.

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-04-09 08:28:33 +02:00
Félix Malfait ffb6029c2d fix(ai-models): use AWS provider chain for Bedrock IRSA auth (#19470)
## Summary
- `buildBedrockProvider` ignored `authType: "role"` and only honored
static `accessKeyId`/`secretAccessKey` pairs, so deployments relying on
IRSA (e.g. EKS pods with `AWS_WEB_IDENTITY_TOKEN_FILE` + `AWS_ROLE_ARN`)
had no way to authenticate — `@ai-sdk/amazon-bedrock` does not walk the
AWS default credential chain on its own.
- When `authType === 'role'`, wire `fromNodeProviderChain()` from
`@aws-sdk/credential-providers` (already a server dependency, used by
the S3 and logic-function drivers) into `createAmazonBedrock` so IRSA
and other ambient AWS credentials resolve correctly.
- The static-credentials path is unchanged for `authType !== 'role'`.

## Test plan
- [ ] Deploy a server pod with IRSA + an `ai-models-config` entry using
`"authType": "role"` and verify Bedrock calls succeed (no `Could not
load credentials from any providers` error).
- [ ] Verify static-credentials providers (`accessKeyId` +
`secretAccessKey`) still work as before.
- [ ] `npx nx lint:diff-with-main twenty-server` passes.
- [ ] `npx nx typecheck twenty-server` passes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 22:54:35 +02:00
Félix Malfait 6073bb6706 Fix AI model registry staleness on self-hosted instances (#19427)
## Summary

Fixes #19422.

Self-hosted users hitting "No AI models are available" after configuring
API keys via the admin panel were victims of a stale
`AiModelRegistryService` cache. Only the four `addAiProvider` /
`removeAiProvider` / `addModelToProvider` / `removeModelFromProvider`
mutations called `refreshRegistry()` — setting an API key through
`set/update/deleteDatabaseConfigVariable` left the registry pointing at
the pre-mutation provider state.

Rather than patch each mutation site (and re-introduce the same class of
bug on the next one), the registry now invalidates lazily based on the
LLM config-group hash, mirroring the pattern `WebSearchDriverFactory`
already uses via `DriverFactoryBase`. Any mutation to an LLM-tagged
config variable is picked up automatically on the next read — callers
never have to remember to refresh.

- Extracted `getConfigGroupHash` into a shared util reused by both
`DriverFactoryBase` and `AiModelRegistryService` (and switched the hash
to `JSON.stringify` so object-typed config vars like `AI_PROVIDERS`
actually contribute meaningfully).
- `AiModelRegistryService` gates all internal `Map` access behind
private getters that call `ensureFresh()`, so future read paths can't
accidentally observe stale state. Build path uses underscored backing
fields directly to avoid recursing.
- Dropped the now-redundant `refreshRegistry()` public method and its
four call sites in `admin-panel.resolver.ts`.

## Test plan

- [x] `nx typecheck twenty-server` passes
- [x] Existing admin-panel + ai-models specs pass (79 tests)
- [ ] Manual: on a self-hosted instance, set `OPENAI_API_KEY` (or
another provider key) via the admin panel `setDatabaseConfigVariable`
mutation and confirm AI features work without a server restart
- [ ] Manual: existing flows (`addAiProvider`, `addModelToProvider`,
etc.) still pick up new providers on the next read

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 22:32:05 +02:00
Weiko 238018dc7b Reset Tab Page Layout (#19453)
## Context
- Add "Reset to default" for page layout tabs and widgets — backend
mutation resets overrides, reactivates deactivated children, and deletes
custom children
- Fix override write bug where mutating an overridable property (e.g.
widget title) on a standard-app entity incorrectly overwrote the base
column instead of writing to the overrides JSONB —
PageLayoutUpdateService now uses resolveFlatEntityOverridableProperties
for accurate diffing and routes properties through
sanitizeOverridableEntityInput
- Deprecate isOverridden - We need more time to think about this
feature. Currently this adds too much complexity for a very small
benefit



https://github.com/user-attachments/assets/a84546c8-1e15-4d9e-a489-0825cf8b8ed2
2026-04-08 18:43:57 +02:00
Félix Malfait 7aed9291cd fix(ai-chat): preload web_search action tool when driver is enabled (#19461)
## Summary
- When `WEB_SEARCH_DRIVER` is set to a non-native driver (e.g. `EXA`),
the `web_search` action tool was only listed in the tool catalog and had
to be discovered via `learn_tools` before use. Worse,
`system-prompt-builder` explicitly instructed the model **not** to call
`web_search` whenever it wasn't in the preloaded set, so even setting
`EXA_API_KEY` correctly resulted in the model never calling Exa.
- Fix: preload `web_search` from the registry alongside the other action
tools when `shouldUseNativeSearch()` is false, mirroring how native
provider search is already injected into `directTools`. The system
prompt builder picks this up automatically and advertises `web_search`
as a ready-to-use tool.
- Unrelated: pass `isSystemBuild: true` to the messageThread upgrade
command's migration build.

## Test plan
- [ ] With `WEB_SEARCH_DRIVER=EXA` and `EXA_API_KEY` set, ask the chat
agent for current/news information and verify it calls `web_search`
directly (not via `learn_tools` / `execute_tool`) and that Exa is hit.
- [ ] With `WEB_SEARCH_PREFER_NATIVE=true`, verify native provider
search is still used and the action tool is not preloaded.
- [ ] With `WEB_SEARCH_DRIVER=DISABLED`, verify behavior is unchanged
(`shouldUseNativeSearch()` returns true → no Exa preload).
- [ ] Run the 1-21 fix-message-thread-view-and-label-identifier upgrade
command on a workspace and confirm the migration builds/runs as a system
build.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 18:22:27 +02:00
Raphaël Bosi d07c27a907 [COMMAND MENU ITEMS] Create union type for command menu item payload (#19432)
Replace generic JSON scalar with a typed GraphQL union
CommandMenuItemPayload (PathNavigationPayload |
ObjectMetadataNavigationPayload) for the CommandMenuItem.payload field
2026-04-08 12:47:34 +00:00
BugIsGod 8a84e32cf6 fix(ai): use @ai-sdk/openai-compatible for third-party providers (#19438)
## Summary:

I found a bug: 404 call when I use third-party providers (I used
deepseek). I found the final request url is:
`https://api.deepseek.com/v1/responses' `if we use createOpenAI. But the
correct one should be `https://api.provider.com/v1/chat/completions` for
the thirty provider. So I replace createOpenAI with
createOpenAICompatible in the openai-compatible provider path.

**Reproduction**: Add DeepSeek as a new AI provider (deepseek-chat,
deepseek-reasoner)

Also check the source code in Open Code project, they also use
createOpenAICompatible for the @ai-sdk/openai-compatible scenario
<img width="703" height="369" alt="image"
src="https://github.com/user-attachments/assets/90c4f924-6f1a-4fe7-821b-f13ee86a7a39"
/>


official docs: https://ai-sdk.dev/providers/openai-compatible-providers
https://ai-sdk.dev/providers/ai-sdk-providers/openai

<img width="860" height="293" alt="image"
src="https://github.com/user-attachments/assets/283b9b91-f1d6-4b1c-bb91-16ddccdff8b4"
/>



## Before
<img width="473" height="750" alt="deepseek before"
src="https://github.com/user-attachments/assets/f6b89294-1fa7-4ddc-a6a8-d396070caaca"
/>

## After

<img width="468" height="753" alt="deepseek after"
src="https://github.com/user-attachments/assets/0d170b70-e829-4a4c-abad-38879427c2df"
/>

## Backend error log

```json
[1] [Nest] 50907  - 07/04/2026, 23:48:57     LOG [ChatExecutionService] Starting chat execution with model deepseek/deepseek-chat, 4 active tools
[1] APICallError [AI_APICallError]: Not Found
[1]     at /Users/abel/Documents/Code/twenty/node_modules/@ai-sdk/provider-utils/dist/index.js:2512:14
[1]     at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
[1]     at async postToApi (/Users/abel/Documents/Code/twenty/node_modules/@ai-sdk/provider-utils/dist/index.js:2373:28)
[1]     at async OpenAIResponsesLanguageModel.doStream (/Users/abel/Documents/Code/twenty/node_modules/@ai-sdk/openai/dist/index.js:4925:50)
[1]     at async fn (/Users/abel/Documents/Code/twenty/node_modules/ai/dist/index.js:7106:27)
[1]     at async /Users/abel/Documents/Code/twenty/node_modules/ai/dist/index.js:2340:24
[1]     at async _retryWithExponentialBackoff (/Users/abel/Documents/Code/twenty/node_modules/ai/dist/index.js:2599:12)
[1]     at async streamStep (/Users/abel/Documents/Code/twenty/node_modules/ai/dist/index.js:7063:17)
[1]     at async fn (/Users/abel/Documents/Code/twenty/node_modules/ai/dist/index.js:7455:9)
[1]     at async /Users/abel/Documents/Code/twenty/node_modules/ai/dist/index.js:2340:24 {
[1]   cause: undefined,
[1]   url: 'https://api.deepseek.com/v1/responses',
[1]   requestBodyValues: {
[1]     model: 'deepseek-chat',
[1]     input: [ [Object], [Object] ],
[1]     temperature: undefined,
[1]     top_p: undefined,
[1]     max_output_tokens: undefined,
[1]     conversation: undefined,
[1]     max_tool_calls: undefined,
[1]     metadata: undefined,
[1]     parallel_tool_calls: undefined,
[1]     previous_response_id: undefined,
[1]     store: undefined,
[1]     user: undefined,
[1]     instructions: undefined,
[1]     service_tier: undefined,
[1]     include: undefined,
[1]     prompt_cache_key: undefined,
[1]     prompt_cache_retention: undefined,
[1]     safety_identifier: undefined,
[1]     top_logprobs: undefined,
[1]     truncation: undefined,
[1]     tools: [ [Object], [Object], [Object], [Object] ],
[1]     tool_choice: 'auto',
[1]     stream: true
[1]   },
[1]   statusCode: 404,
[1]   responseHeaders: {
[1]     'access-control-allow-credentials': 'true',
[1]     connection: 'keep-alive',
[1]     'content-length': '0',
[1]     date: 'Tue, 07 Apr 2026 22:48:57 GMT',
[1]     server: 'elb',
[1]     'strict-transport-security': 'max-age=31536000; includeSubDomains; preload',
[1]     vary: 'origin, access-control-request-method, access-control-request-headers',
[1]     via: '1.1 83867089cd39052cd05f9e04909bedde.cloudfront.net (CloudFront)',
[1]     'x-amz-cf-id': 'O4b0VTi9Q1VVmTmq6czGlEWst7IPnAQl544hB7uIvfnSphBvUKbZTw==',
[1]     'x-amz-cf-pop': 'DUB56-P3',
[1]     'x-cache': 'Error from cloudfront',
[1]     'x-content-type-options': 'nosniff',
[1]     'x-ds-trace-id': 'a90e91809d285170338ef077f67ae2be'
[1]   },
[1]   responseBody: '',
[1]   isRetryable: false,
[1]   data: undefined,
[1]   Symbol(vercel.ai.error): true,
[1]   Symbol(vercel.ai.error.AI_APICallError): true
[1] }
[1] Exception Captured
[1]   undefined
[1]   [
[1]     NoOutputGeneratedError [AI_NoOutputGeneratedError]: No output generated. Check the stream for errors.
[1]         at Object.flush (/Users/abel/Documents/Code/twenty/node_modules/ai/dist/index.js:6656:103)
[1]         at invokePromiseCallback (node:internal/webstreams/util:172:10)
[1]         at Object.<anonymous> (node:internal/webstreams/util:177:23)
[1]         at transformStreamDefaultSinkCloseAlgorithm (node:internal/webstreams/transformstream:621:43)
[1]         at node:internal/webstreams/transformstream:379:11
[1]         at writableStreamDefaultControllerProcessClose (node:internal/webstreams/writablestream:1162:28)
[1]         at writableStreamDefaultControllerAdvanceQueueIfNeeded (node:internal/webstreams/writablestream:1253:5)
[1]         at writableStreamDefaultControllerClose (node:internal/webstreams/writablestream:1220:3)
[1]         at writableStreamClose (node:internal/webstreams/writablestream:722:3)
[1]         at writableStreamDefaultWriterClose (node:internal/webstreams/writablestream:1091:10) {
[1]       cause: undefined,
[1]       Symbol(vercel.ai.error): true,
[1]       Symbol(vercel.ai.error.AI_NoOutputGeneratedError): true
[1]     }
[1]   ]
[1] [Nest] 50907  - 07/04/2026, 23:48:59     LOG [BullMQDriver] Job 24 with name StreamAgentChatJob processed on queue ai-stream-queue in 2756.63ms
2026-04-08 12:01:49 +00:00
Weiko 8aa208fc93 Fix overridable entities logic for SSE (#19433)
## Context
SSE metadata events for overridable entities (viewField, viewFieldGroup,
pageLayoutWidget, pageLayoutTab) were sending raw flat entity data
without resolving overrides into base properties or filtering isActive:
false entities. This caused the frontend to display stale/incorrect
values (e.g., a hidden viewField still appearing visible).

## Implementation
Add a sanitization step in
MetadataEventPublisher.enrichMetadataEventBatch that resolves overrides
and converts isActive transitions into the appropriate event types
(deactivated -> delete, reactivated -> create), matching what GraphQL
resolvers already do
2026-04-08 11:53:55 +00:00
github-actions[bot] 8026451220 chore: sync AI model catalog from models.dev (#19426)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

This PR updates pricing, context windows, and model availability based
on the latest data.
New models meeting inclusion criteria (tool calling, pricing data,
context limits) are added automatically.
Deprecated models are detected based on cost-efficiency within the same
model family.

**Please review before merging** — verify no critical models were
incorrectly deprecated.

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-04-08 08:27:14 +02:00
Raphaël Bosi d341d0d624 Refactor navigation commands to use NAVIGATION engine key with payload (#19303)
- Adds a payload JSON column to `CommandMenuItem` and introduces a
unified `NAVIGATION` engine component key that replaces all individual
GO_TO_* keys
- Navigation commands now use the payload to determine their target
(either an objectMetadataItemId or a path), making navigation commands
dynamic and eliminating the need for a hardcoded engine key per object
- Includes a 1.21 upgrade command (refactor-navigation-commands) that
migrates existing GO_TO_* items to NAVIGATION items with the appropriate
payload, and applies a CHECK constraint enforcing payload coherence



https://github.com/user-attachments/assets/4d305ba2-ae0b-4556-bb0e-e9d899777350


TODO: In a second PR, create the sync between object metadata items and
the navigation command menu items
- Object metadata item created or enabled -> Create navigation command
- Object metadata item deleted or disabled -> Delete associated
navigation command

In another PR:
- Allow `label`, `shortLabel` and `icon` to resolve the
`navigateToObjectMetadataItem` dynamically in their interpolation
instead of being hardcoded in the command menu item
- Make the icon dynamic in the command menu items as the label so that
we can resolve ${navigateToObjectMetadataItem.icon} at runTime -> This
way we won't need to keep update the command menu item icon when we
update the objectMetadataItem icon
2026-04-07 15:00:04 +00:00
Weiko 6e23ca35e6 Pagelayout backfill command standard app (#19380)
## Context
Due to the chosen strategy for "Reset to default" feature for page
layouts. Those overridable entities need to be associated to the
Standard app to work properly (there is no "Default" state for custom
entities). Until we find a better implementation, I'm changing the
backfill command to reflect that
2026-04-07 12:49:35 +00:00
Weiko a2188cb0eb Reset Fields widget implementation (#19283)
## Context
This PR implements the first steps for overridable entities resets.

<img width="1277" height="568" alt="Screenshot 2026-04-02 at 18 58 04"
src="https://github.com/user-attachments/assets/4c7f93b1-c453-4905-a919-cd6af11e0e16"
/>
2026-04-07 09:34:44 +00:00
neo773 b23d2b4e73 messaging post migration cleanup (#19365)
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-07 10:56:49 +02:00
Félix Malfait 83d30f8b76 feat: Send email from UI — inline reply composer & SendEmail mutation (#19363)
## Summary

- **Inline email reply**: Replace external email client redirects
(Gmail/Outlook deeplinks) with an in-app email composer. Users can reply
to email threads directly from the email thread widget or via the
command menu.
- **SendEmail GraphQL mutation**: New backend mutation that reuses
`EmailComposerService` for body sanitization, recipient validation, and
SMTP dispatch via the existing outbound messaging infrastructure.
- **Side panel compose page**: Command menu "Reply" action now opens a
side-panel compose email page with pre-filled To, Subject, and
In-Reply-To fields.

### Backend
- `SendEmailResolver` with `SendEmailInput` / `SendEmailOutputDTO`
- `SendEmailModule` wired into `CoreEngineModule`
- Reuses `EmailComposerService` + `MessagingMessageOutboundService`

### Frontend
- `EmailComposer` / `EmailComposerFields` components
- `useSendEmail`, `useReplyContext`, `useEmailComposerState` hooks
- `useOpenComposeEmailInSidePanel` + `SidePanelComposeEmailPage`
- `EmailThreadWidget` inline Reply bar with toggle composer
- `ReplyToEmailThreadCommand` now opens side-panel instead of external
links

### Seeds
- Added `handle` field to message participant seeds for realistic email
addresses
- Seed `connectedAccount` and `messageChannel` in correct batch order

## Test plan

- [ ] Open an email thread on a person/company record → verify
"Reply..." bar appears below the last message
- [ ] Click "Reply..." → composer opens inline with pre-filled To and
Subject
- [ ] Type a message and click Send → email is sent via SMTP, composer
closes
- [ ] Use command menu Reply action → side panel opens with compose
email page
- [ ] Verify Send/Cancel buttons work correctly in side panel
- [ ] Test with Cc/Bcc toggle in composer fields
- [ ] Verify error handling: invalid recipients, missing connected
account


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-07 08:43:48 +02:00
Félix Malfait ea572975d8 feat: generic web search driver abstraction with Exa support and billing (#19341)
## Summary

- Introduces a pluggable `WebSearchDriver` abstraction (interface,
factory, service, module) so web search is no longer tied to native
provider tools (Anthropic/OpenAI)
- **Exa** is the first driver implementation with support for
category-filtered search (company, people, news, research paper, etc.) —
particularly useful for CRM workflows
- Per-query billing for both Exa ($0.007/query) and native provider
surcharges ($0.01/query for Anthropic/OpenAI) via the existing
`USAGE_RECORDED` pipeline
- New config variables: `WEB_SEARCH_DRIVER` (EXA/DISABLED),
`EXA_API_KEY`, `WEB_SEARCH_PREFER_NATIVE` (default false — prefers Exa
over native when both available)
- `WEB_SEARCH` operation type added for usage tracking and Stripe
metering

### Architecture

```
WebSearchDriver (interface)
├── ExaDriver          — Exa neural search with category support
└── DisabledDriver     — throws when search is disabled

WebSearchDriverFactory (extends DriverFactoryBase)
└── creates driver based on WEB_SEARCH_DRIVER config

WebSearchService (facade)
├── search(query, options?, billingContext?)
├── isEnabled()
└── emits USAGE_RECORDED events per query

WebSearchTool (Tool implementation)
└── registered in ActionToolProvider, available via tool catalog
```

### Native search billing gap fixed

Anthropic and OpenAI both charge $0.01/search on top of token costs. The
token costs were already billed, but the per-call surcharge was not.
Added `countNativeWebSearchCallsFromSteps` utility +
`billNativeWebSearchUsage` to `AiBillingService`, wired into both chat
and workflow agent paths.

## Test plan

- [ ] Set `WEB_SEARCH_DRIVER=EXA` + `EXA_API_KEY=...` and verify AI chat
can search the web
- [ ] Verify category parameter works (ask about a specific
company/person)
- [ ] Set `WEB_SEARCH_DRIVER=DISABLED` and verify search tool is not
exposed
- [ ] Set `WEB_SEARCH_PREFER_NATIVE=true` with Anthropic model and
verify native search is used
- [ ] Verify usage events are emitted in ClickHouse for both Exa and
native search paths
- [ ] Verify existing billing tests pass (`npx jest
ai-billing.service.spec.ts`)


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-04-06 12:02:46 +02:00
Félix Malfait 8acfacc69c Add email thread widget and message thread record page layout (#19351)
## Summary
- Move email thread display from side panel to a dedicated record page
with a new `EMAIL_THREAD` widget type
- Add message thread as a standard object with page layout, subject
field, and backfill command
- Add reply-to-email command menu item for message thread records
- Remove old side panel message thread components in favor of the new
widget-based approach

## Type fixes
- Add `EMAIL_THREAD` to `WidgetConfigurationType`, `WidgetType`, and all
configuration/validator maps
- Create `EmailThreadConfigurationDTO` and shared
`EmailThreadConfiguration` type
- Register EMAIL_THREAD in widget type validators, configuration
resolvers, and standard widget mappings

## Test plan
- [ ] Verify message thread record pages render with the email thread
widget
- [ ] Verify email thread preview navigates to the record page instead
of opening side panel
- [ ] Verify reply-to-email command appears for message thread records
- [ ] Verify typecheck passes for both twenty-front and twenty-server
- [ ] Run existing test suites to check for regressions

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 12:02:04 +02:00
neo773 d3f0162cf5 Remove connected account feature flag (#19286)
Co-authored-by: martmull <martmull@hotmail.fr>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@twenty.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
2026-04-06 08:13:41 +00:00
yukuotec 265b2582ee Fix zh-CN sidebar translations in minimalMetadata API (#19342)
The minimalMetadata query was returning untranslated English labels for
standard objects (Companies, People, Opportunities, etc.) when users
requested zh-CN locale. This fix adds translation logic to
MinimalMetadataService using the existing I18nService.

Changes:
- Add translateLabel() method to translate standard object labels
- Pass locale from GraphQL context through resolver to service
- Only translate non-custom objects (custom objects use user-defined
labels)

Co-authored-by: Kevin Yu <kevin.yu@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-05 20:14:20 +02:00
github-actions[bot] ed912ec548 chore: sync AI model catalog from models.dev (#19339)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

This PR updates pricing, context windows, and model availability based
on the latest data.
New models meeting inclusion criteria (tool calling, pricing data,
context limits) are added automatically.
Deprecated models are detected based on cost-efficiency within the same
model family.

**Please review before merging** — verify no critical models were
incorrectly deprecated.

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-04-05 08:22:58 +02:00
Félix Malfait 563e27831b Clean up tool output architecture: remove wrappers, enforce ToolOutput everywhere (#19321)
## Summary

- **Remove `ExecuteToolResult` wrapper** — `execute_tool` is now a
transparent dispatcher that returns the raw `ToolOutput` from underlying
tools. No more `{ toolName, result }` envelope.
- **Type the entire execution chain as `Promise<ToolOutput>`** — from
`ToolExecutorService.dispatch()` through `resolveAndExecute()` to
`execute_tool.execute()`. Zero `Promise<unknown>` remaining in the tool
layer.
- **Use `Extract<ToolExecutionRef, ...>`** for dispatch methods,
enabling exhaustive switch checking and removing `as never` casts.
- **Relax `ToolOutput.result` to accept `null`** — removes `??
undefined` hacks at the boundary with logic function results.
- **Enforce 1-export-per-file** across tool type/interface files (split
`tool-descriptor.type.ts`, `tool-provider.interface.ts`, `tool.type.ts`,
`tool-output.type.ts`, `tool-executor.service.ts`).
- **Simplify error handling** — `wrapWithErrorHandler` and all
meta-errors (tool not found, tool excluded) now return consistent
`ToolOutput` shape with `error` as a plain string.
- **Frontend reads output directly** — removed `unwrapToolOutput`
utility; `ToolStepRenderer` and `ThinkingStepsDisplay` extract
`message`/`error` from the raw output with simple type guards.
- **Add permission error detection** for email tools via
`isInsufficientPermissionsError`, guiding the AI model to suggest
account reconnection instead of hallucinating about visibility settings.

## Test plan

- [ ] AI chat tool calls return visible output (not "null") in the UI
- [ ] Tool errors display correctly in the JSON tree
- [ ] Email draft/send tools return actionable permission errors
- [ ] Code interpreter output renders correctly
- [ ] Thinking steps display tool outputs properly


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-04 14:05:42 +02:00
neo773 0b44c9e4e5 Remove connected account upgrade command (#19316) 2026-04-03 18:01:21 +02:00
Félix Malfait bb3d556799 Refined demo workspace creation skill (rebased, review fixes) (#19274)
## Summary

Rebased version of #19051 with all review comments addressed. Clean
branch on latest main, lint/typecheck/tests passing.

### Changes from original PR
- AI can now create, update, and delete **view filters**
(`ViewFilterToolsFactory`) and **view sorts** (`ViewSortToolsFactory`)
- `create_view` now accepts `calendarFieldName`, `calendarLayout`, and
`fieldNames` to configure views at creation time
- Three new standard skills: `view-building`, `view-filters-and-sorts`,
`custom-objects-cleanup`
- `workspace-demo-seeding` skill reworked to keep standard objects and
enrich them with custom fields
- Cache invalidation for nav menu items when object `isActive` changes
- Dashboard tool descriptions improved (RECORD_TABLE widget workflow)

### Review comments addressed (all 10 from #19051)
1. **Sentry + Cubic**: Calendar field DATE/DATE_TIME validation — added
`resolveCalendarFieldMetadataId` using `isFieldMetadataDateKind`
2. **Cubic**: "navigate tool" → "navigate_app tool" in skill metadata
(all 7 occurrences)
3. **Copilot**: KANBAN views now require `mainGroupByFieldName` — throws
clear error if missing
4. **Copilot**: CALENDAR views now require both `calendarFieldName` and
`calendarLayout` — validated before DB call
5. **Copilot**: Mock field fixtures include `type` property (DATE_TIME,
TEXT, SELECT)
6. **Copilot**: `ViewFilterValue` type assertion instead of unsafe `as
string` casts (3 locations)
7. **FelixMalfait**: Removed
`NavigationMenuItemObjectDeactivationListener` — replaced with cache
invalidation
8. **FelixMalfait**: Consolidated `ViewFilterToolProvider` and
`ViewSortToolProvider` into single `ViewToolProvider`
9. Removed `VIEW_FILTER` and `VIEW_SORT` from `ToolCategory` enum
(merged into `VIEW`)
10. Removed stale `existingFeatureFlagsMap` param incompatible with
current main

## Test plan
- [x] `npx nx lint:diff-with-main twenty-server` — passes
- [x] `npx nx typecheck twenty-server` — passes
- [x] `view-tools.factory.spec.ts` — all 20 tests pass (including 3 new
validation tests)

Supersedes #19051

https://claude.ai/code/session_01QPV74NU6vzmJb32e4i899E

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-03 09:19:46 +02:00
github-actions[bot] 12b031b67d chore: sync AI model catalog from models.dev (#19298)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

This PR updates pricing, context windows, and model availability based
on the latest data.
New models meeting inclusion criteria (tool calling, pricing data,
context limits) are added automatically.
Deprecated models are detected based on cost-efficiency within the same
model family.

**Please review before merging** — verify no critical models were
incorrectly deprecated.

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-04-03 08:21:46 +02:00
Marie 2d6c8be7df [Apps] Fix - app-synced object should be searchable (#19206)
## Summary

- **Make app-synced objects searchable**: `isSearchable` was hardcoded
to `false` and the `searchVector` field was missing the `GENERATED
ALWAYS AS (...)` expression, causing all records to have a `NULL` search
vector and be excluded from search results. Fixed by defaulting
`isSearchable` to `true` (configurable via the object manifest),
computing the `asExpression` from the label identifier field, and
allowing the update-field-action-handler to handle the `null` → defined
`asExpression` transition.
- **Make `isSearchable` updatable on an object**: The property had
`toCompare: false` in the entity properties configuration, so updates
via the API were silently ignored and never persisted. Fixed by setting
`toCompare: true`.
2026-04-02 17:14:37 +00:00
Abdul Rahman 7f2b853ae1 feat: add message compaction for AI chats (#19205) 2026-04-02 15:13:11 +00:00
Weiko c1e4756f9c Add is active to overridable entities and deactivation logic for page layouts (#19200)
- Replace soft-deletion (deletedAt) with isActive boolean for
overridable entities (tabs, widgets, viewFieldGroups, viewFields).
Standard entities are deactivated (isActive: false) when removed from
update payloads, while custom entities are hard-deleted.
- When a viewFieldGroup is deactivated/deleted, its viewFields are
reassigned to the next section by position (or null if none remain).
- Add isActive: true filters to viewFieldGroup and viewField API queries
so deactivated entities are excluded from responses.

Next: 
- fields-widget-upsert.service.ts should be refactored a bit
- Add restore logic
2026-04-02 14:50:50 +00:00
nitin 3c067f072c [AI] Improve tools tab (#19221) 2026-04-02 13:36:33 +00:00
Weiko 6ae5900ac9 Fix field permission validation rejecting undefined optional fields (#19243)
The backend validation for field permissions was using !== null to check
canReadFieldValue and canUpdateFieldValue, but these optional GraphQL
fields can also be undefined when omitted from the input. This caused
the validation to incorrectly reject legitimate requests (e.g.,
restricting only "update" without specifying "read") with the error
"Field permissions can only be used to restrict access, not to grant
additional permissions."

Replaced !== null checks with isDefined() so that both null and
undefined are treated as "no opinion" on that permission.

To reproduce:
- Create a single FieldPermission on an object with canEdit: false
without any other rule on that same object
2026-04-02 13:15:14 +00:00
Charles Bochet b8e7179a85 Add missing index on viewField.viewFieldGroupId (#19253)
## Summary

- Adds an index on `viewField.viewFieldGroupId` to fix a ~28s `DELETE
FROM core.viewFieldGroup WHERE workspaceId = $1` query observed in
production
- The slow query is triggered during workspace hard deletion
(`deleteWorkspaceSyncableMetadataEntities` in `workspace.service.ts`)
- Root cause: the FK constraint `viewField.viewFieldGroupId →
viewFieldGroup.id` with `ON DELETE SET NULL` forces PostgreSQL to
sequentially scan the entire `viewField` table for every deleted
`viewFieldGroup` row, because no index exists on the referencing column
- Uses `CREATE INDEX CONCURRENTLY` in the migration to avoid locking the
table on production
2026-04-02 11:57:27 +00:00
Baptiste Devessier 885ab8b444 Seed Front Components (#19220)
https://github.com/user-attachments/assets/6b0887e8-8ba4-49c9-9371-e3db3e9fdde8
2026-04-02 11:20:08 +00:00
nitin 223943550c [AI] Unify code-interpreter streaming rendering and fix assistant width jitter (#19235)
closes
https://discord.com/channels/1130383047699738754/1480991390782455838

- Use data-code-execution as the streaming source of truth and hide
duplicate code-interpreter tool parts (including tool-execute_tool
wrappers).
- Ensure wrapped execute_tool code-interpreter outputs still render
correctly after refetch.
- Gate code-interpreter server behavior by enablement state and keep
assistant messages full-width to avoid streaming vs completed width
shifts.

Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
2026-04-02 10:58:14 +00:00
Raphaël Bosi f688db30a9 Command to deduplicate command menu items (#19202)
- Deduplicate standard record command menu items by merging
single-record and multi-record delete, restore, destroy, and export
actions into shared record commands.
- Update frontend command registrations and engine component mappings to
use the new unified keys, while keeping deprecated engine keys
temporarily mapped for backward compatibility.
- Add a 1-21 upgrade command that removes legacy command menu items from
workspaces and creates the new unified standard items.
2026-04-02 09:51:15 +00:00
Félix Malfait fd7387928c feat: queue messages + replace AI SDK with GraphQL SSE subscription (#19203)
## Summary

- **Queue messages while streaming**: Messages sent during active AI
streaming are queued server-side and auto-flushed when the current
stream completes. Frontend renders queued messages optimistically in a
dedicated queue UI.
- **Drop `@ai-sdk/react` + `resumable-stream`**: Replace the dual HTTP
SSE + AI SDK client architecture with a single GraphQL SSE subscription
per thread. All events (token chunks, message persistence, queue
updates, errors) flow through Redis PubSub → GraphQL subscription.
- **Server-driven architecture**: The server decides whether to queue or
stream (via `POST /:threadId/message`). The frontend mirrors this
decision for optimistic rendering but defers to the server response.
- **Reuse AI SDK accumulation logic**: `readUIMessageStream` from the
`ai` package handles chunk-to-message accumulation on the frontend,
avoiding a custom 780-line accumulator.

## Key files

**Backend:**
- `agent-chat-event-publisher.service.ts` — publishes events to Redis
PubSub
- `agent-chat-subscription.resolver.ts` — GraphQL subscription resolver
- `stream-agent-chat.job.ts` — publishes chunks via PubSub instead of
resumable-stream
- `agent-chat.controller.ts` — unified `POST /:threadId/message`
endpoint

**Frontend:**
- `useAgentChatSubscription.ts` — subscribes to `onAgentChatEvent`,
bridges to `readUIMessageStream`
- `useAgentChat.ts` — send/stop/optimistic rendering (no more AI SDK)
- `AgentChatStreamSubscriptionEffect.tsx` — replaces
`AgentChatAiSdkStreamEffect.tsx`

## Test plan

- [ ] Send message on new thread → optimistic render, streaming response
appears
- [ ] Send message while streaming → queued instantly (no flash in main
thread)
- [ ] Queued message auto-flushes after current stream completes
- [ ] Remove queued message via queue UI
- [ ] Stop streaming mid-response
- [ ] Leave chat idle for several minutes → streaming still works after
(SSE client recycling)
- [ ] Token refresh during session → requests succeed (authenticated
fetch)
- [ ] Switch threads while streaming → clean subscription handoff


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-02 10:10:13 +02:00
Raphaël Bosi 9f95c4763c [COMMAND MENU ITEMS] Remove deprecated code (#19199)
This PR is the first one of a cleanup after upgrading command menu items
to V2.
2026-04-01 15:56:52 +00:00
Baptiste Devessier c11e4ece39 Fallback to field metadata (#19131)
Rely on the field metadata items to always display all object's fields
in the fields widget configuration editor. If fields are missing in the
returned view fields, we add the missing fields through object metadata.


https://github.com/user-attachments/assets/3c4d45e8-05d0-4943-be4b-bcf1e310155c
2026-03-31 19:00:16 +00:00
Félix Malfait 8fa3962e1c feat: add resumable stream support for agent chat (#19107)
## Overview
Add resumable stream support for agent chat to allow clients to
reconnect and resume streaming responses if the connection is
interrupted (e.g., during page refresh).

## Changes

### Backend (Twenty Server)
- Add `activeStreamId` column to `AgentChatThreadEntity` to track
ongoing streams
- Create `AgentChatResumableStreamService` to manage Redis-backed
resumable streams using the `resumable-stream` library with ioredis
- Extend `AgentChatController` with:
  - `GET /:threadId/stream` endpoint to resume an existing stream
  - `DELETE /:threadId/stream` endpoint to stop an active stream
- Update `AgentChatStreamingService` to store streams in Redis and track
active stream IDs
- Add `resumable-stream@^2.2.12` dependency to package.json

### Frontend (Twenty Front)
- Update `useAgentChat` hook to:
- Use a persistent transport with `prepareReconnectToStreamRequest` for
resumable streams
  - Export `resumeStream` function from useChat
  - Add `handleStop` callback to clear active stream on DELETE endpoint
- Use thread ID as stable message ID instead of including message count
- Add stream resumption logic in `AgentChatAiSdkStreamEffect` component
to automatically call `resumeStream()` when switching threads

## Database Migration
New migration `1774003611071-add-active-stream-id-to-agent-chat-thread`
adds the `activeStreamId` column to store the current resumable stream
identifier.

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 18:19:25 +02:00
Etienne ecf8161d0e Fix - Remove signFileUrl method (#19121)
`signFileUrl` is the old way to resolve file url. Replaced by
`signFileByIdUrl` which needs `FileFolder` and `fileId`.

Fix also avatar for person and workspaceMember. To be continued with
imageIdentifier refactor.


Fix : 
- https://discord.com/channels/1130383047699738754/1486723854910099576
- https://discord.com/channels/1130383047699738754/1484566445584285870
- https://discord.com/channels/1130383047699738754/1487124612889313420

---------

Co-authored-by: Charles Bochet <charlesBochet@users.noreply.github.com>
2026-03-30 15:14:54 +00:00
neo773 2fccd29ec6 messaging cleanup (#19124) 2026-03-30 16:21:00 +02:00
Charles Bochet 1109b89cd7 fix: use Redis metadata version for GraphQL response cache key (#19111)
## Summary

- Fix stale `ObjectMetadataItems` GraphQL response cache after field
creation by using `request.workspaceMetadataVersion` (sourced from
Redis) instead of `workspace.metadataVersion` (from the potentially
stale CoreEntityCacheService)
- Make the E2E kanban view test selector more robust with a regex match

## Root Cause

The `useCachedMetadata` GraphQL plugin keys cached responses using
`workspace.metadataVersion` from the `CoreEntityCacheService`. When a
field is created:

1. The migration runner increments `metadataVersion` in DB and Redis
2. But the `CoreEntityCacheService` for `WorkspaceEntity` is **not**
invalidated
3. So `request.workspace.metadataVersion` still has the old version
4. The cache key resolves to the old cached response
5. The frontend gets stale metadata without the newly created field

This breaks E2E tests (and likely affects users) - after creating a
custom field, the metadata isn't visible until the workspace entity
cache refreshes.

## Fix

Use `request.workspaceMetadataVersion` (populated from Redis by the
middleware, always up-to-date) as the primary version for cache keys,
falling back to the entity cache version.

## Test plan

- [ ] E2E `create-kanban-view` tests should pass (creating a Select
field and immediately using it in a Kanban view)
- [ ] Verify `ObjectMetadataItems` returns fresh data after field
creation (no stale cache)


Made with [Cursor](https://cursor.com)
2026-03-30 15:01:54 +02:00
neo773 ef8789fad6 fix: convert empty parentFolderId to null in messageFolder core dual-write (#19110) 2026-03-30 13:11:38 +02:00
github-actions[bot] 58189e1c05 chore: sync AI model catalog from models.dev (#19100)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

This PR updates pricing, context windows, and model availability based
on the latest data.
New models meeting inclusion criteria (tool calling, pricing data,
context limits) are added automatically.
Deprecated models are detected based on cost-efficiency within the same
model family.

**Please review before merging** — verify no critical models were
incorrectly deprecated.

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-03-30 08:31:25 +02:00
Charles Bochet e8cb086b64 chore: remove completed migration feature flags and upgrade commands <= 1.18 (#19074)
## Summary

- Remove 3 completed migration feature flags: `IS_ATTACHMENT_MIGRATED`,
`IS_NOTE_TARGET_MIGRATED`, `IS_TASK_TARGET_MIGRATED` — these were
already enabled by default for all new workspaces via
`DEFAULT_FEATURE_FLAGS`
- Delete all upgrade command directories for versions <= 1.18 (`1-16/`,
`1-17/`, `1-18/`) along with their module registrations, removing ~6,600
lines of dead migration code
- Simplify frontend utility functions
(`getActivityTargetObjectFieldIdName`, `getActivityTargetsFilter`,
`getActivityTargetFieldNameForObject`,
`generateActivityTargetMorphFieldKeys`,
`findActivitiesOperationSignatureFactory`) by removing the
`isMorphRelation` parameter and always using the morph relation path
- Remove feature flag checks from 7 frontend hooks/components that were
gating attachment and activity target behavior behind the removed flags
- Simplify `buildDefaultRelationFlatFieldMetadatasForCustomObject`
server util to always treat attachment, noteTarget, and taskTarget as
morph relations without checking feature flags
2026-03-29 09:15:26 +02:00
github-actions[bot] f651413297 chore: sync AI model catalog from models.dev (#19078)
Automated daily sync of `ai-providers.json` from
[models.dev](https://models.dev).

This PR updates pricing, context windows, and model availability based
on the latest data.
New models meeting inclusion criteria (tool calling, pricing data,
context limits) are added automatically.
Deprecated models are detected based on cost-efficiency within the same
model family.

**Please review before merging** — verify no critical models were
incorrectly deprecated.

Co-authored-by: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com>
2026-03-29 08:20:40 +02:00