Commit Graph
11775 Commits
Author SHA1 Message Date
Félix MalfaitandClaude Opus 4.7 85effd6750 fix(billing): gate AI credit-cap at entry points instead of workflow executor
Restores the credit-cap enforcement that was lifted in #19904 (which had
correctly identified that gating per-workflow-step was too coarse), but
moves it to the AI entry points where the actual cost lives.

- agent-async-executor.service: gate executeAgent.
- ai-generate-text.controller: gate the REST handler.
- agent-title-generation.service: gate generateThreadTitle.
- workflow-executor.workspace-service: replace #19904's TODO with an
  absolute-behavior comment documenting why the gate is not here.

Chat resolver was already gated; no change there. Repair-tool-call util
is a sub-call inside an already-gated flow and is left ungated.

Net effect: a workspace that exhausts credits via chat or AI agent stops
making AI calls, but its non-AI workflows (DB CRUD, branching, action
steps) continue running normally.

Addresses the root cause of the 2026-04-26 token-usage incident.

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
v2.1.1
2026-04-29 18:52:00 +02:00
Charles BochetandGitHub 80c0e5603d Move isPreInstalled applicationRegistration instance command to 2.1 (#20037)
## Summary

The fast instance command adding `isPreInstalled` to
`core.applicationRegistration` was added after 2.0 was released, so it
must run as part of the 2.1 upgrade rather than 2.0.

- Renamed
`2-0/2-0-instance-command-fast-1776886452831-add-is-pre-installed-to-application-registration.ts`
to
`2-1/2-1-instance-command-fast-1776886452831-add-is-pre-installed-to-application-registration.ts`
- Updated `@RegisteredInstanceCommand` version from `'2.0.0'` to
`'2.1.0'`
- Updated the import path in `instance-commands.constant.ts`

The original timestamp (`1776886452831`) was kept; it is smaller than
the existing 2.1 fast command timestamp, so this command will simply run
first within the 2.1.0 batch.
v2.1.0
2026-04-24 17:52:58 +00:00
Abdullah.andGitHub 47710908a8 [Website] Architecture, hardening, and perf pass. (#20020)
This PR is the result of a critical architectural review of
`twenty-website-new` and the
follow-on cleanup work. It does not touch any other package. Scope is
everything except
the enterprise/billing routes (deferred to a separate PR) and CI wiring
(also deferred).

### Why

The package had grown organically: ad-hoc scroll/motion/halftone code
per section,
WebGL renderers instantiated raw, sections without a shared shape
contract, route-scoped
files leaking across layers, public API routes without rate limiting /
timeouts /
schema validation, unbounded module-level caches in visual code,
drag/resize handlers
re-rendering React 60x/sec, no security headers, and a Lottie
scroll-mapping that would
silently drift if the asset got re-exported. We needed contracts and
primitives in
place before further work (i18n, decomposition of the giant visual
files, MDX migration
of customer/legal pages) is safe to do.

### What changed

**Layering and contracts (now enforced, not just documented).**
- New layering rule: `app → sections → lib → design-system / theme /
icons`.
Cross-section reuse goes through `lib/`. Flipped the design-system
import rule
  from warn to error.
- Extracted shared primitives into `lib/`: `scroll`, `motion`,
`halftone`, `customers`,
`partner-application`, `api`, `seo`, `semver`, `community`,
`visual-runtime`.
- Lifted route-scoped data/types out of `app/` into `lib/` +
`sections/`.
- Section shape contract: every section exposes a single compound export
from
`components/index.ts(x)`; non-leaf sections own the outer `<section>`
from
`Root.tsx`; named slots are matched by `displayName` (no
`Children.toArray`
  positional indexing). Enforced by `scripts/check-section-shape.mjs`.
- WebGL boundary: `new THREE.WebGLRenderer(...)` is forbidden outside
`src/lib/visual-runtime/`. Everything goes through
`createSiteWebGlRenderer`,
which enforces the site-wide context cap, the
`NEXT_PUBLIC_DISABLE_HEAVY_VISUALS`
  kill switch, and GPU/power-preference defaults. Enforced by
`scripts/check-boundaries.mjs` with per-line
`boundary-allow-next-line:<rule-id>`
  escape hatches and stale-directive detection.

**Design system grew to cover real cases.**
- Added Modal, Form, and Layout primitives (Stack / Inline / Grid) so
sections stop
reinventing them. Built on `@base-ui/react` for accessibility + focus
management.

**Public API hardening (non-enterprise).**
- New `lib/api/` primitives: `createRateLimiter` (in-memory token
bucket),
`fetchWithTimeout`, `readJsonBody` (Zod-validated). Applied to
newsletter,
community, and partner-application routes. `/api/partner-application`
specifically
  got a per-IP rate limit, body cap, and timeout.

**Performance.**
- `DraggableTerminal` and `DraggableAppWindow`: `pointermove` now
mutates `transform`
(via `translate3d`) and `width`/`height` directly on the DOM ref. React
state
commits only on `pointerup`. Eliminates per-frame re-renders during
interaction.
- `createBoundedFailureCache` (FIFO, 256 entries) replaces unbounded
module-level
failure caches in four visual components. Bounds memory growth from bad
asset URLs.

**Lottie frame-map guard.**
- `dotlottie-react`'s `player.totalFrames` returns a raw float (`op -
ip`), not an
integer. The HomeStepper scroll → frame map is keyed to the authored
timeline,
  so silent drift would desync every step boundary.
- Reads now `Math.floor(player.totalFrames)` consistently.
- `scripts/check-lottie-frames.mjs` extracts `op - ip` from
`public/lottie/stepper/stepper.lottie` at build time and asserts it
against
`HOME_STEPPER_LOTTIE_EXPECTED_TOTAL_FRAMES`. If anyone re-exports the
Lottie,
the build fails until both that constant and every `STEP_*_END` are
updated together.

**Security headers (`next.config.ts`).**
- HSTS, `X-Content-Type-Options: nosniff`, `Referrer-Policy:
strict-origin-when-cross-origin`,
`Permissions-Policy` (camera/mic/geolocation/payment off),
`X-Frame-Options: DENY`,
  `Content-Security-Policy: frame-ancestors 'none'`.
- Full CSP intentionally deferred until we enumerate all third-party
origins
  (Cal.com, Stripe, GitHub avatars, twenty-icons.com, etc.).

**Build / config quirks documented in code.**
- `tsconfig.json` is standalone (does NOT extend the monorepo base) —
Next.js +
  React Compiler require options that conflict with the base config.
- `sharp` moved from `devDependencies` to `dependencies` so production
image
  optimization works.

### What's deliberately NOT in this PR

- **Enterprise / billing routes** — open redirect on
`/api/enterprise/checkout`,
indefinite-bearer JWT, non-idempotent Stripe seat updates, unpinned
Stripe
`apiVersion`, missing webhook reconciliation, inconsistent error
envelope.
  Going out as a separate, security-focused PR.
- **CI workflow for `twenty-website-new`** (`lint` / `typecheck` /
`test` / `build`
  targets) — separate follow-up PR.
- **i18n via Lingui** — decision made (we need internationalization and
we already
use Lingui in `twenty-front` / `twenty-emails`); 4-phase migration plan
exists
  but does not land here.
- **Decomposition of giant visual files** (HomeVisual, ThreeCards
visuals) — blocked
  on the i18n landing first; otherwise we'd rebase the world twice.
- **Customer / legal pages → MDX** — same reason.
- **Selective memoization pass** — needs browser profiling, not blind
`useMemo`.
- **Pre-existing lint errors / typecheck noise** (~44 errors, ~41
warnings, plus
generated Next.js types and `@ts-nocheck` files) are unchanged. The
cleanup
  did not introduce new ones.

### Test plan

- [ ] `yarn install`
- [ ] `yarn nx run twenty-website-new:dev` — homepage, customers,
partner,
enterprise activate, blog, why-twenty, plans/pricing, legal pages
render.
- [ ] HomeStepper: scroll through, confirm the Lottie animation lines up
with
every step boundary. Console must NOT log a `totalFrames` mismatch.
- [ ] HomeVisual: drag and resize the terminal + app window; verify
smoothness
(no per-frame React re-renders) and that final position/size persists on
release.
- [ ] Public API endpoints: hit `/api/newsletter`, `/api/community`,
`/api/partner-application` with bad payloads → expect 4xx with Zod
errors,
not 500s. Hammer `/api/partner-application` past the per-IP limit → 429.
- [ ] Response headers on any page include HSTS, nosniff, referrer
policy,
permissions policy, X-Frame-Options, and `frame-ancestors 'none'` CSP.
- [ ] `yarn nx run twenty-website-new:lint` — error/warning count must
not exceed
      the pre-existing baseline.
- [ ] `yarn nx run twenty-website-new:typecheck` — same baseline rule.
- [ ] `node packages/twenty-website-new/scripts/check-boundaries.mjs` —
passes,
      no stale directives.
- [ ] `node packages/twenty-website-new/scripts/check-section-shape.mjs`
— passes.
- [ ] `node packages/twenty-website-new/scripts/check-lottie-frames.mjs`
— passes.
- [ ] `yarn nx run twenty-website-new:build` — green, including the
three checks
      above if wired into the build target.
2026-04-24 16:40:09 +00:00
Charles BochetandGitHub 0bb3660844 chore(twenty-sdk): shrink logic-function bundles via stubbing (#20033)
## Summary

Logic-function bundles produced by the SDK CLI were ~1.2 MB each (source
maps ~3.1 MB) because esbuild was inlining `twenty-sdk/define` and its
transitive dependencies (zod + locales, twenty-shared, etc.). Those
`define*` factories are pure build-time metadata used only by the
manifest extractor — the Lambda runtime only ever invokes
`default.config.handler`, so the factories are dead weight at runtime.

This PR shrinks the bundles to ~9.5 KB each (~99% reduction) without
changing runtime behaviour.

## What changes

- **Stub `twenty-sdk/define` at user-app build time.** New esbuild
plugin
(`packages/twenty-sdk/src/cli/utilities/build/common/plugins/stub-twenty-sdk-define.plugin.ts`)
intercepts every import of `twenty-sdk/define` during user-app builds
and replaces it with a tiny virtual module:
- Factory functions (`defineLogicFunction`,
`definePostInstallLogicFunction`, …) become `(config) => ({ success:
true, config, errors: [] })`.
  - Enums and helpers become `Proxy`-based no-ops.
- Wired into both the one-shot build (`build-application.ts`) and the
watcher (`esbuild-watcher.ts`), for logic functions and front
components.
- **New runtime barrel `twenty-sdk/logic-function`.** Re-exports only
the types logic-function authors need (`InstallPayload`, `RoutePayload`,
`CronPayload`, `DatabaseEventPayload`, `LogicFunctionConfig`,
`InputJsonSchema`, …). Compiled `.mjs` is 36 bytes. Wired into Vite,
Rollup `.d.ts` bundling, `package.json#exports`, and `typesVersions`.
- **Lint enforcement.** Added an oxlint `no-restricted-imports` rule
that forbids `twenty-shared` / `twenty-shared/*` imports from
`**/*.logic-function.ts` and `**/logic-functions/**/*.ts`, with a help
message pointing at the new barrel. Applied to the `create-twenty-app`
template and to `github-connector`, `hello-world`, `postcard`.
- **Migrated existing sources.** All logic-function files across
`community/{github-connector, apollo-enrich}`, `examples/{hello-world,
postcard}`, and `internal/{twenty-for-twenty, self-hosting, exa}` now
import types from `twenty-sdk/logic-function` instead of
`twenty-sdk/define` or `twenty-shared/*`. Renamed leftover
`InstallLogicFunctionPayload` references to `InstallPayload`.

## Why this is safe

- `define*` exports from `twenty-sdk/define` are metadata factories
whose call expressions are statically inspected by the manifest
extractor (`manifest-extract-config.ts`). They're never evaluated at
runtime — the Lambda executor only walks `default.config.handler`
(`logic-function-drivers/constants/executor/index.mjs`).
- The stub keeps the same call shape (`{ success, config, errors }`), so
any logic-function module that re-exports
`defineX(config).config.handler` still resolves to the user's handler at
runtime.
- Front-component bundles are unaffected by the stub because the
pre-existing JSX transform plugin
(`jsx-transform-to-remote-dom-worker-format-plugin.ts`) unwraps
`defineFrontComponent(...)` earlier in the pipeline. That's intentional
— front-component bloat is React/Preact, not in scope here.

## Measurements (github-connector)

| Asset | Before | After |
|---|---|---|
| `*.logic-function.mjs` | ~1.2 MB | ~9.5 KB |
| `*.logic-function.mjs.map` | ~3.1 MB | ~22 KB |
2026-04-24 17:51:35 +02:00
WeikoandGitHub 2ccc293f99 Gate export/import command menu items by permission flag (#19991)
## Summary
- Hides the `exportRecords`, `exportView`, and `importRecords` command
menu actions from
users whose role does not hold the matching `EXPORT_CSV` / `IMPORT_CSV`
permission flag.
- Exposes the current user's role permission flags to
`conditionalAvailabilityExpression`
by adding `permissionFlags: Record<string, boolean>` to
`CommandMenuContextApi`, mirroring
 how `featureFlags` is already accessible.
- Adds a `2.1.0` workspace upgrade command that rewrites the three
existing rows on every
 active/suspended workspace.

## Before
<img width="1294" height="287" alt="Screenshot 2026-04-22 at 19 37 40"
src="https://github.com/user-attachments/assets/11ca8635-14d7-40a0-9ca0-76329c54e3c6"
/>

## After
<img width="1283" height="285" alt="Screenshot 2026-04-22 at 19 32 25"
src="https://github.com/user-attachments/assets/5e49fa8a-4541-42ee-96da-4c1de7d00aae"
/>
2026-04-24 15:22:44 +00:00
nitinGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>Félix MalfaitFélix Malfait
6c1c0737b0 Clarify registry tools vs native model tool binding (#20022)
## Intent

This is a small foundation cleanup for the tool architecture.

The main decision is: registry tools and native SDK/model tools are
different things.

- Registry tools have descriptors, schemas, catalog entries, and execute
through `ToolExecutorService`
- Native model tools are opaque AI SDK objects, bound directly into the
model `ToolSet`
- Surfaces still own their policy: chat, MCP, and workflow agents decide
what they expose

## What changed

- Removed `NATIVE_MODEL` from `ToolCategory`
- Kept `ToolRegistryService` focused on registry-backed tools only
- Moved native model tool binding through `NativeToolBinderService`
- Reused native binding from chat instead of duplicating
provider-specific web-search logic
- Kept MCP local execution exclusions in a dedicated constant
- Moved surface-specific constants into dedicated constant files

## What comes next

- Move hardcoded chat app preloads, like Exa web search, into
app/manifest metadata
- Decide a clearer policy for local runtime tools like code interpreter
and HTTP request
- Gradually document the three tool shapes: registry tools, native model
tools, and local runtime tools

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Félix Malfait <[email protected]>
Co-authored-by: Félix Malfait <[email protected]>
2026-04-24 17:18:19 +02:00
251f5deab6 [breaking, deploy server first] fix(ai-chat): persist providerExecuted flag on tool parts (#20030)
## Summary
Fixes Sentry errors of the form:

> \`messages.3: \`tool_use\` ids were found without \`tool_result\`
blocks immediately after: srvtoolu_…. Each \`tool_use\` block must have
a corresponding \`tool_result\` block in the next message.\`

### Root cause
When the model invokes a **provider-hosted tool** (e.g. Anthropic's
native \`web_search\` — note the \`srvtoolu_\` ID prefix), the AI SDK
marks the resulting \`UIMessagePart\` with \`providerExecuted: true\`.
\`convertToModelMessages\` uses that flag to emit the
tool_use/tool_result pair *inside the same assistant message* — the
format Anthropic requires for server-side tools.

Our \`AgentMessagePart\` persistence was dropping \`providerExecuted\`
on the way to the DB (and re-hydration didn't know to set it). On the
next turn, \`convertToModelMessages\` treated the rehydrated part as a
client-side tool call, splitting it into \`assistant(tool_use)\` +
\`user(tool_result)\` — which Anthropic then rejects with the error
above.

### Fix
- Add nullable \`providerExecuted BOOLEAN\` column on
\`core.agentMessagePart\` via a fast instance command.
- Surface the field on \`AgentMessagePartDTO\` (GraphQL).
- Preserve it through \`mapUIMessagePartsToDBParts\` (server) and both
\`mapDBPartToUIMessagePart\` mappers (server + frontend).
- Include it in \`GET_CHAT_MESSAGES\` and \`GET_AGENT_TURNS\`
selections.
- Regenerate \`generated-metadata/graphql.ts\`.

### Backwards compatibility
Existing rows have \`NULL providerExecuted\` and round-trip as the
omitted flag — which is exactly the pre-fix behaviour for tool parts
that were never provider-executed. Only *new* assistant messages using
\`web_search\` (or other provider-hosted tools) will write \`true\`, and
those are the only ones that were breaking.

## Test plan
- [x] \`npx tsgo\` typecheck — server + front clean
- [x] \`oxlint\` + \`prettier --check\` on all touched files — clean
- [x] \`npx nx run twenty-server:database:migrate:prod\` runs the new
instance command locally; \`providerExecuted\` column present on
\`core.agentMessagePart\`
- [x] Regenerated \`generated-metadata/graphql.ts\` —
\`providerExecuted\` wired into both queries and \`AgentMessagePart\`
type
- [ ] Manual: start a chat with Anthropic web_search enabled, invoke the
tool in turn 1, reply in turn 2 — should not throw the srvtoolu error

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-04-24 16:58:26 +02:00
WeikoandGitHub 5f604a503a backfill widget position from gridPosition (phase 1 of gridPosition removal) (#20032)
## Context
Phase 1 of removing the legacy `gridPosition` field from
`PageLayoutWidget` in favor of the new `position` discriminated union
(`grid` / `vertical-list` / `canvas`). This PR is purely additive —
`gridPosition` is still required and read everywhere; we just guarantee
that every widget now also has a non-null `position` so a follow-up PR
can drop `gridPosition` cleanly.

## Changes
- **Slow instance command**
`BackfillPageLayoutWidgetPositionSlowInstanceCommand` (2.1.0):
for every `core.pageLayoutWidget` row where `position IS NULL`, copies
`gridPosition`
into `position` with `layoutMode: 'GRID'`. Historically only grid
widgets used
`gridPosition`, so a single SQL update covers every existing row.
- **`PageLayoutDuplicationService`**: when duplicating a widget, also
forwards
`originalWidget.position` (was previously only forwarding
`gridPosition`).
- **Frontend default layouts** (10 `Default*PageLayout.ts` files): added
a `position`
sibling to every widget, matching the parent tab's `layoutMode` —
`VERTICAL_LIST` widgets
get `{ layoutMode, index }`, `CANVAS` widgets get `{ layoutMode }`

<img width="338" height="226" alt="Screenshot 2026-04-24 at 16 23 17"
src="https://github.com/user-attachments/assets/c3319318-f1b8-4271-96b4-196b209a1f5e"
/>
2026-04-24 14:35:56 +00:00
Paul RastoinandGitHub 9771725cc4 Dockerfile twenty-server target (#20028)
# Introduction
Creating a target funnel that allow bypassing front build and injection
in the server

## New targets
- `twenty-server` only ships server
- `twenty` ships both front and back in the same image
- `twenty-server-aws` only ships server and `aws-cli`
- `twenty-aws` ships both front and back in the same image with the
`aws-image`
2026-04-24 14:24:32 +00:00
WeikoandGitHub 2a3c759928 Removing community apps in favor of npm-distributed apps (#20029)
## Context
With the registry pattern around `Twenty Applications` and the upcoming
marketplace, app
distribution can be decoupled from the core repo. Pulling apps from npm
instead of
shipping them in-tree means:

- Contributors don't clone / index / build code for apps they'll never
touch
- Each app can version, release, and iterate independently of the core
cadence
- We stop carrying apps that are effectively unmaintained in the
monorepo

## How
This PR removes the 11 community apps from
`packages/twenty-apps/community/`

## Note
- **Marketplace apps** must be published on npm and pulled from there —
no app code living in this repo just to be distributed
- **Showcase / example apps** stay as living examples of what the SDK
can do under `example/`
- **Officially-maintained marketplace apps** (like the new
`github-connector`?) stay in the repo under `internal/`. Some may be
pre-installed on new workspaces alongside the Standard app
2026-04-24 13:50:08 +00:00
a36c67a500 i18n - translations (#20027)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-04-24 12:33:40 +02:00
nitinandGitHub 097432d3a2 [Command Menu] Refactor layout customization conditional availability [Warning] (#19974)
closes
https://discord.com/channels/1130383047699738754/1494312529286004837
2026-04-24 10:18:02 +00:00
WeikoandGitHub 3deb467845 Fix layout edition mode dark mode text color - 2 (#20024)
Followup https://github.com/twentyhq/twenty/pull/19992

Added button { --tw-button-color: ${GRAY_SCALE_LIGHT.gray1}; } to
StyledContainer,
mirroring the existing color override. This forces the loader's color
(which reads
var(--tw-button-color) in Loader.tsx) to stay white on the blue bar
regardless of theme —
in dark mode, the button's inline --tw-button-color previously resolved
to a dark
font.color.inverted, making the spinner nearly invisible.

## Before
<img width="137" height="37" alt="Screenshot 2026-04-24 at 11 01 51"
src="https://github.com/user-attachments/assets/a870f1a3-83bd-4094-bc78-f9416fce1e0e"
/>

## After
<img width="139" height="31" alt="Screenshot 2026-04-24 at 11 00 14"
src="https://github.com/user-attachments/assets/1bff4411-aee2-4ad4-bae3-8118fa117368"
/>
2026-04-24 09:44:16 +00:00
WeikoandGitHub 9a9daf77ca Keep fallback record page layouts read-only in edition mode (#20023)
When a record has no associated `pageLayoutId`, the FE falls back to a
hardcoded default page layout (`DEFAULT_*_RECORD_PAGE_LAYOUT`). These
mocks use non-UUID ids for the layout, tabs, and widgets
(e.g. `default-person-page-layout`).

Today nothing distinguishes these fallbacks from real layouts at edit
time. Clicking "Edit layout" flips the global customization mode,
`PageLayoutRecordPageCustomizationSessionRegistrationEffect` registers
the mock id, and on Save `useSaveLayoutCustomization` fires
`UpdatePageLayoutWithTabsAndWidgets` with those mock ids — the BE
rejects them and we end up in a partial-save state (nav / command menu
commit while the page layout fails).

This PR keeps fallback record page layouts in read mode even when
global layout-edition mode is on, and defends the save loop so mock
ids can never be sent to the BE.

## Non editable "base" record page layout (=mock)
<img width="1508" height="616" alt="Screenshot 2026-04-24 at 10 35 02"
src="https://github.com/user-attachments/assets/34b093d1-f4bb-4076-ab8c-ce97ecb945a4"
/>

## Editable record page layout
<img width="1512" height="597" alt="Screenshot 2026-04-24 at 10 35 20"
src="https://github.com/user-attachments/assets/5ff6f3ff-79a5-4e6f-aa1c-01c7e09273b3"
/>
2026-04-24 08:54:37 +00:00
e8f58fd23a chore: sync AI model catalog from models.dev (#20018)
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 <[email protected]>
2026-04-24 08:34:13 +02:00
d51d982a5f Fix: Database query on opportunity table (#20017)
## Automated fix for [bug
30463](https://sonarly.com/issue/30463?type=bug)

**Severity:** `critical`

### Summary
When createMany is called with upsert:true and records lack pre-assigned
IDs, findExistingRecords() executes a SELECT with no WHERE clause,
scanning the entire table. This caused an 11-second transaction on the
opportunity table (2.9s query + 7.7s JS processing).

### Root Cause
1. WHY was the transaction 11 seconds? Because a SELECT on the
opportunity table took 2.9s and its result processing took 7.7s.
2. WHY did the SELECT take 2.9s? Because it was a full table scan with
NO WHERE clause, fetching every record (including soft-deleted).
3. WHY was there no WHERE clause? Because findExistingRecords() in
common-create-many-query-runner.service.ts calls buildWhereConditions()
which returned an empty array, so no .orWhere() was applied to the query
builder before .getMany() was called.
4. WHY did buildWhereConditions return empty? Because the input records
had no pre-assigned IDs and the opportunity object has no other unique
fields, so there were no conflicting field values to build conditions
from.
5. WHY wasn't the empty-conditions case guarded? The
findExistingRecords() method was written without an early-return check
for empty whereConditions — it always executes the query regardless.

**Introduced by:** etiennejouan on 2025-10-15 in commit
[`4ae2999`](https://github.com/twentyhq/twenty/commit/4ae299973bcea3470252c4c68540d33a4df59cc7)
> Common Api - createOne/Many (#15083)

### Suggested Fix
Added an early return in findExistingRecords() when
buildWhereConditions() returns an empty array. When no WHERE conditions
exist (because input records lack values for any unique/conflicting
field), the method now returns an empty array immediately instead of
executing a SELECT with no WHERE clause that scans the entire table.
This prevents the O(n) full table scan + O(n) JS result processing that
caused the 11-second transaction. The downstream categorizeRecords()
correctly handles empty existingRecords by classifying all input records
as recordsToInsert.

---
*Generated by [Sonarly](https://sonarly.com)*

Co-authored-by: Sonarly Claude Code <[email protected]>
2026-04-24 06:18:57 +00:00
dbc350fafe i18n - translations (#20016)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-04-23 22:18:46 +02:00
085c0b9b7f feat(admin-panel): add read-only Billing tab and workspace logos (#20012)
## Summary
- Adds a **Billing** tab on the admin-panel workspace detail page that
surfaces Stripe customer + active subscription details (status, plan,
interval, current period, trial, cancellation, line items, credit
balance). Tab is gated on `IS_BILLING_ENABLED` both in the backend
service and in the frontend tab list — completely hidden on instances
where billing is disabled.
- Renders a **workspace avatar next to the name** in the admin Top
Workspaces list by plumbing the workspace `logo` field through the admin
DTO, statistics SQL query, and generated admin GraphQL types.
- **Read-only** by design: no Stripe API calls, no mutations — data
comes from the existing \`BillingCustomerEntity\` /
\`BillingSubscriptionEntity\` / \`BillingPriceEntity\` tables via
\`BillingSubscriptionService.getCurrentBillingSubscription\`.

### What the tab shows
- **Customer** container — Stripe customer ID (with link to the Stripe
dashboard, monospaced), credit balance (formatted, from
\`creditBalanceMicro\`).
- **Subscription** container — status tag (color-coded), plan tag,
billing interval, current period range, trial range (if trialing),
\`cancelAtPeriodEnd\` / \`cancelAt\` / \`canceledAt\` (only when set),
Stripe subscription ID (external link).
- **Line items** — one card per subscription item with product name,
product key tag, seats (if quantity), credits per period (for metered),
unit price (formatted with currency).

### Design choices
- Styling matches the user-facing billing page
(\`SubscriptionInfoContainer\` + \`Tag\` + \`H2Title\` + \`Section\`) —
no new UI primitives.
- Currency is rendered inline with amounts via \`Intl.NumberFormat\`
(e.g. \`\$19.00\`) instead of as a separate row.
- Uses the generated admin GraphQL types
(\`WorkspaceBillingAdminPanelQuery\`, \`SubscriptionStatus\`,
\`SubscriptionInterval\`) — no hand-typed response shapes.

## Test plan
- [x] \`npx nx typecheck twenty-server\` — passes
- [x] \`npx nx typecheck twenty-front\` — passes
- [x] oxlint + prettier on all touched files — clean
- [x] \`graphql:generate --configuration=admin\` — regenerated; new
\`workspaceBillingAdminPanel\` query + \`logo\` field on
\`AdminPanelTopWorkspace\` appear in \`generated-admin/graphql.ts\`
- [x] Backend GraphQL schema introspection shows
\`workspaceBillingAdminPanel\` query on \`/admin-panel\`
- [x] Direct GraphQL call with seeded \`BillingCustomer\` +
\`BillingSubscription\` + \`BillingPrice\` rows returns the expected
shape (\`status: "Trialing"\`, plan \`PRO\`, items with
quantity/unitAmount/includedCredits, trial period dates)
- [x] With \`IS_BILLING_ENABLED=false\` (default) the Billing tab is
hidden — verified in the admin panel UI
- [x] Top Workspaces list renders workspace avatars next to names —
verified in the admin panel UI
- [ ] Smoke test the Billing tab render in a real instance that has
\`IS_BILLING_ENABLED=true\` + live Stripe data (skipped locally due to
dev-env auth friction after toggling billing/multi-workspace; recommend
a reviewer check)

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-04-23 22:12:05 +02:00
9ce9e2bc12 i18n - translations (#20015)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-04-23 22:05:44 +02:00
4f938aa097 feat(app): infrastructure for pre-installed apps (#19973)
**PR 1 of 2.** Follow-up PR ships the Exa app, sets it as a default
pre-installed app, and removes the current `WebSearchTool` /
`WebSearchService` / `ExaDriver`. This PR adds the plumbing; no
user-visible change yet.

## Summary

- Server admins can declare a list of npm app packages to auto-install
on every new workspace and backfill onto existing workspaces via CLI.
- Server-level secrets (like Exa's API key) live on the
`ApplicationRegistration` (one row per server, encrypted) and are
injected into logic function execution env at runtime. No more
per-workspace storage of global secrets.
- A generic `POST /app/billing/charge` endpoint lets app logic functions
emit workspace usage events for metered features. Exa uses it in PR 2;
future apps (call recorder, etc.) reuse it.
- `LogicFunctionToolProvider` tool name prefix changes `logic_function_`
→ `app_`. Shorter, accurate (they come from installed apps).

## What's in this PR

**Logic function executor — server-level variables**
- `LogicFunctionExecutorService.getExecutionEnvVariables` now resolves
env vars in the order: hardcoded defaults →
`ApplicationRegistrationVariable[]` (server-level) →
`ApplicationVariable[]` (workspace-level override). The manifest
`serverVariables` schema has existed; this closes the loop.

**Config**
- `PRE_INSTALLED_APPS` — comma-separated list of npm packages. Default:
empty.

**\`PreInstalledAppsService\`** (new module)
- \`onApplicationBootstrap()\` — fetches each package's manifest from
the app registry CDN, upserts an \`ApplicationRegistration\`, and seeds
declared \`serverVariables\` from matching env vars (e.g.
\`EXA_API_KEY\` env → encrypted registration variable).
- \`installOnWorkspace(workspaceId)\` — installs all pre-installed apps
on a single workspace. Tolerates per-app failures.

**Auto-install on new workspace activation**
- \`WorkspaceService.prefillCreatedWorkspaceRecords\` invokes
\`installOnWorkspace\` after prefilling standard records. Non-blocking
on failure.

**Backfill CLI command**
- \`install-pre-installed-apps\` — iterates active and suspended
workspaces, installs pre-installed apps that aren't yet installed.
Idempotent. Run after changing \`PRE_INSTALLED_APPS\`.

**App billing endpoint**
- \`POST /app/billing/charge\`. Authenticated via \`APPLICATION_ACCESS\`
token (already injected into logic function execution env as
\`DEFAULT_APP_ACCESS_TOKEN\`). Body: \`{ creditsUsedMicro, quantity,
unit, operationType, resourceContext? }\`. Emits \`USAGE_RECORDED\` with
\`applicationId\` as \`resourceId\`. Generic — reusable by any app.

**Tool name prefix**
- \`LogicFunctionToolProvider.buildLogicFunctionToolName\` now produces
\`app_<name>\` instead of \`logic_function_<name>\`. Only affects tools
sourced from logic functions; other tool providers unchanged.

## Stats

- 16 files, +501 / −2
- 7 new files (1 command, 1 service × 2, 1 controller, 1 DTO, 2 modules)
- Typecheck: 7 pre-existing errors, zero new
- Prettier clean

## Behavior deltas

- **\`PRE_INSTALLED_APPS\` default = empty**: existing servers see no
change on merge.
- **\`ApplicationRegistrationVariable\` is now read by the executor**:
apps that were using manifest \`serverVariables\` but expecting them to
be ignored by the executor will now see them injected. No apps ship with
\`isTool: true\` logic functions today, so this is latent — first
consumer is Exa in PR 2.
- **Tool prefix**: currently no logic-function tools are named
\`logic_function_*\` in any production flow. The prefix change affects
only future tools emitted by \`LogicFunctionToolProvider\`.

## Risks

- **CDN unavailability at startup**: if the app registry CDN is down,
\`ensureRegistrationsExist\` logs warnings but doesn't block server
start. Installation on new workspaces during this window will find no
registrations and log a non-blocking error. Backfill command can retry
after CDN recovers.
- **Cold-start overhead**: \`ensureRegistrationsExist\` is called once
per process on bootstrap. Current configurable default is empty, so zero
overhead. When an admin sets \`PRE_INSTALLED_APPS\`, they accept one
HTTP call per package at boot.
- **Server-level variables flow**:
\`ApplicationRegistrationVariable.encryptedValue\` is shared by all
workspaces of a server. Appropriate for a single-tenant Exa key. Not
appropriate for per-tenant keys — those go in workspace-level
\`ApplicationVariable\` and override.

## Test plan

- [ ] \`npx nx typecheck twenty-server\` passes (verified: 7
pre-existing unrelated errors, zero new)
- [ ] Set \`PRE_INSTALLED_APPS=@twenty-apps/hello-world\` (or any real
npm-published app), \`HELLO_WORLD_API_KEY=xxx\`, restart server:
\`ApplicationRegistration\` row is upserted,
\`ApplicationRegistrationVariable\` for HELLO_WORLD_API_KEY is populated
(encrypted).
- [ ] Create a new workspace: the app is auto-installed,
\`ApplicationEntity\` row created, \`LogicFunctionEntity\` rows created.
- [ ] Existing workspace: run \`yarn nx run twenty-server:command
install-pre-installed-apps\`: apps install across all workspaces,
idempotent on re-run.
- [ ] Trigger a logic function that reads
\`process.env.HELLO_WORLD_API_KEY\`: value resolves from the
server-level \`ApplicationRegistrationVariable\`.
- [ ] Log a charge from the handler: \`POST /app/billing/charge\` with
\`Authorization: Bearer \$DEFAULT_APP_ACCESS_TOKEN\` body
\`{creditsUsedMicro: 1000, quantity: 1, unit: "INVOCATION",
operationType: "WEB_SEARCH"}\` → returns \`{success: true}\`,
\`USAGE_RECORDED\` event emitted with correct
\`resourceId=applicationId\`.
- [ ] Tool name generated by \`LogicFunctionToolProvider\` starts with
\`app_\`.

## What's NOT in this PR (PR 2 scope)

- The Exa app itself (\`packages/twenty-apps/...\` directory)
- Removing \`WebSearchTool\`, \`WebSearchService\`, \`ExaDriver\`,
\`web-search\` module
- Removing \`WEB_SEARCH_DRIVER\` config var
- Removing the current \`exa_web_search\` entry in
\`ActionToolProvider\`
- Chat preload list updated to \`app_exa_web_search\`
- Frontend \`getToolDisplayMessage\` branch for \`app_exa_web_search\`
- Setting \`PRE_INSTALLED_APPS\` default to include \`@twenty-apps/exa\`

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-04-23 21:59:00 +02:00
ec98130def fix(admin-panel): inline skeleton loaders for table sections (#20014)
## Summary
\`SettingsSkeletonLoader\` wraps its content in \`PageHeader\` +
\`PageBody\`, which is right for **full-page replacement** (user detail,
config variable detail, etc.) but renders as a large empty stub with one
tiny floating bar when placed **inline inside a section that's already
scaffolded**. That's what showed up in Recent Users, Top Workspaces, and
the Chats tab in the admin panel — a jarring white page flash where a
few row placeholders should be.

This PR adds \`SettingsAdminSectionSkeletonLoader\` — a small,
configurable-row-count skeleton that uses the project's standard
\`SkeletonTheme\` pattern (\`theme.background.tertiary\` /
\`theme.background.transparent.lighter\` + \`borderRadius: 4\`, matching
\`PageContentSkeletonLoader\` and \`SettingsAdminTabSkeletonLoader\`)
and renders row-height bars that match \`TableRow\` spacing. Swaps it
into the three inline call sites.

Full-page usages of \`SettingsSkeletonLoader\` are unchanged.

### Changed
- **New**:
\`packages/twenty-front/src/modules/settings/admin-panel/components/SettingsAdminSectionSkeletonLoader.tsx\`
- **Swapped** (3 inline loading states): \`SettingsAdminGeneral.tsx\`
(Recent Users + Top Workspaces), \`SettingsAdminWorkspaceDetail.tsx\`
(Chats tab)

## Test plan
- [x] typecheck (\`npx nx typecheck twenty-front --skip-nx-cache\`) —
clean
- [x] oxlint — 0 warnings
- [x] prettier — clean
- [ ] Visual: navigate to admin panel, observe Recent Users / Top
Workspaces / Chats tab loading — should see a tight stack of row-height
placeholder bars instead of a big empty page stub

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-04-23 21:58:33 +02:00
Paul RastoinandGitHub b1838b3090 Retrieve ai catalog at bootstrap (#20005)
# Introduction
Migrating from build injection to a runtime bootstrap injection of the
ai catalog that are dynamic to the env we're deploying to
2026-04-23 17:00:35 +00:00
Thomas TrompetteandGitHub fa3d0cd4a6 Force uuids in AI workflow tools (#20010)
- Add .uuid() Zod validation to all AI workflow tool schemas
(workflowVersionId, workflowId, stepId, edge target) so the AI model is
constrained to use valid UUIDs instead of human-readable strings like
step-find-stale-leads
- Fixes Sentry noise from Invalid UUID errors triggered when workflows
created with non-UUID step IDs are later edited via GraphQL mutations
that enforce UUID scalars

Will fix
https://twenty-v7.sentry.io/issues/7240446584/events/05c7782655a34f4a8f5fc889164026ab/?project=4507072499810304&referrer=previous-event
2026-04-23 16:27:53 +00:00
652930e0ac i18n - docs translations (#20009)
Created by Github action

Co-authored-by: github-actions <[email protected]>
2026-04-23 17:07:23 +02:00
Thomas TrompetteandGitHub d887fdc532 Stop throwing for event stream does not exists (#20008)
Fixes
https://twenty-v7.sentry.io/issues/7351816489/?environment=prod&environment=prod-eu&project=4507072499810304&query=is%3Aunresolved%20%21issue.type%3A%5Bperformance_consecutive_db_queries%2Cperformance_consecutive_http%2Cperformance_file_io_main_thread%2Cperformance_db_main_thread%2Cperformance_n_plus_one_db_queries%2Cperformance_n_plus_one_api_calls%2Cperformance_p95_endpoint_regression%2Cperformance_slow_db_query%2Cperformance_render_blocking_asset_span%2Cperformance_uncompressed_assets%2Cperformance_http_overhead%2Cperformance_large_http_payload%5D%20timesSeen%3A%3E10&referrer=issue-stream&sort=date

- When an SSE event stream already exists in Redis during a
reconnection, the server now checks if the caller is the rightful owner
(via isAuthorized) and destroys the stale stream before creating a fresh
one, instead of throwing an EVENT_STREAM_ALREADY_EXISTS error
- Unauthorized callers still receive the error, preserving the security
guard against hijacking
2026-04-23 13:22:09 +00:00
992a7ca12f i18n - docs translations (#20007)
Created by Github action

Co-authored-by: github-actions <[email protected]>
2026-04-23 14:45:01 +02:00
Charles BochetandGitHub a445f4a6fa feat(sdk): add definePageLayoutTab for extending existing page layouts (#20004)
## Summary

Introduces `definePageLayoutTab` so apps can attach a single tab (with
optional widgets) to an **existing** `pageLayout` referenced by
`pageLayoutUniversalIdentifier`. The parent layout can be standard, from
the same app, or from another app — mirroring how `defineField`
references an object via `objectUniversalIdentifier`.

This complements `definePageLayout`: use `definePageLayout` when you own
the entire layout, use `definePageLayoutTab` when you only want to add
to one.

```ts
import { definePageLayoutTab, PageLayoutTabLayoutMode } from 'twenty-sdk/define';

export default definePageLayoutTab({
  universalIdentifier: 'b1b2b3b4-b5b6-4000-8000-000000000001',
  pageLayoutUniversalIdentifier: 'STANDARD-OR-OTHER-APP-PAGE-LAYOUT-UUID',
  title: 'Hello World',
  position: 1000,
  icon: 'IconWorld',
  layoutMode: PageLayoutTabLayoutMode.CANVAS,
  widgets: [/* ... */],
});
```

## Changes

- **twenty-shared**: new top-level `pageLayoutTabs:
PageLayoutTabManifest[]` on `Manifest`, optional
`pageLayoutUniversalIdentifier` on `PageLayoutTabManifest`, new
`SyncableEntity.PageLayoutTab`.
- **twenty-sdk**:
  - new `definePageLayoutTab` + `PageLayoutTabConfig` exports;
- manifest extraction wiring (`TargetFunction.DefinePageLayoutTab`,
`ManifestEntityKey.PageLayoutTabs`);
  - dev-mode label/state for the new entity;
- CLI scaffold (`getPageLayoutTabBaseFile`) + unit tests for `npx
twenty-cli add`.
- **twenty-server**: convert top-level `pageLayoutTabs` (and their
widgets) into universal flat entities in
`computeApplicationManifestAllUniversalFlatEntityMaps`. Cross-app FK
validation on `pageLayoutUniversalIdentifier` is already handled by the
existing `FlatPageLayoutTab` validator.
- **docs**: new `definePageLayoutTab` accordion in `apps/layout.mdx`
with usage example and guidance vs `definePageLayout`.
- **CI / rich-app fixture**: `extra-tab.page-layout-tab.ts` exercises
the new flow with a front-component widget; `expected-manifest.ts` and
`manifest.tests.ts` updated.
2026-04-23 12:00:10 +00:00
neo773andGitHub 20f7ba82d7 optimize workspace export command (#20000)
- Use `COPY` instead of `INSERT` for workspace tables, 40x faster
imports
- Multi value statements, 2x smaller file size
- Bump Batch size to 10K , remove COUNT query
- Add `formatPgCopyField` utility with unit tests
2026-04-23 09:42:49 +00:00
WeikoandGitHub aa4aea0f9b Fix layout edition mode dark mode text color (#19992)
Summary

Fixed the dark-mode regression in the layout customization banner at
packages/twenty-front/src/modules/layout-customization/components/LayoutCustomizationBar.tsx:

- The banner's background is themeCssVariables.color.blue
(theme-independent), so its text color must be theme-independent too.
- Replaced color: ${themeCssVariables.font.color.inverted} (which
resolves to near-black in dark mode) with color:
${GRAY_SCALE_LIGHT.gray1} (always white), matching the convention used
 in AnimatedButton.tsx for text on colored backgrounds.

## Before
<img width="1508" height="185" alt="Screenshot 2026-04-22 at 19 50 03"
src="https://github.com/user-attachments/assets/b0a95fc2-05d5-4207-9d72-64a83daf94ae"
/>

## After
<img width="1511" height="190" alt="Screenshot 2026-04-22 at 19 49 39"
src="https://github.com/user-attachments/assets/9ce33353-412f-4db2-9322-a6f293f6f1b4"
/>
2026-04-23 08:36:12 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
77f5b9d7c8 chore(deps): bump @ai-sdk/google from 3.0.31 to 3.0.64 (#19998)
Bumps [@ai-sdk/google](https://github.com/vercel/ai) from 3.0.31 to
3.0.64.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/vercel/ai/commit/e2f82795704d7badbd90f9f4a798d8bf2d40b2df"><code>e2f8279</code></a>
Version Packages (<a
href="https://redirect.github.com/vercel/ai/issues/14527">#14527</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/71c52e0c053571dd6893be6fa56d275ec2d9e216"><code>71c52e0</code></a>
Backport: chore(provider/google): update available models (<a
href="https://redirect.github.com/vercel/ai/issues/14524">#14524</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/e662efda9bb35838d5c2a03a6320197d1192db4c"><code>e662efd</code></a>
Version Packages (<a
href="https://redirect.github.com/vercel/ai/issues/14510">#14510</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/83434a97d6856c34d440e22038ac083cd5134017"><code>83434a9</code></a>
Backport: feat (provider/gateway): add provider routing sort options (<a
href="https://redirect.github.com/vercel/ai/issues/14508">#14508</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/3aa3a68cfec59cae4dccb1ad1446d30fbe93f17c"><code>3aa3a68</code></a>
Version Packages (<a
href="https://redirect.github.com/vercel/ai/issues/14507">#14507</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/a27a631939dedc310b13fd48de144760cc2a3145"><code>a27a631</code></a>
Backport: feat (provider/gateway): make model list resilient to unknown
model...</li>
<li><a
href="https://github.com/vercel/ai/commit/a2d619a76be80c4fdd60ce047d21fdf95879e08f"><code>a2d619a</code></a>
Version Packages (<a
href="https://redirect.github.com/vercel/ai/issues/14502">#14502</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/b937f3e7fa0c943c6ce8a379b57dd5bb7abf07b4"><code>b937f3e</code></a>
Backport: fix(xai): support encrypted reasoning round-trip for ZDR (<a
href="https://redirect.github.com/vercel/ai/issues/14497">#14497</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/b0e5ab3a75052fbc0469066b19196ec419e49c20"><code>b0e5ab3</code></a>
Version Packages (<a
href="https://redirect.github.com/vercel/ai/issues/14494">#14494</a>)</li>
<li><a
href="https://github.com/vercel/ai/commit/8c4abafaaa28f82f031a498ff335c9f394a33d48"><code>8c4abaf</code></a>
Backport: chore(provider/gateway): update gateway model settings files
v6 (<a
href="https://redirect.github.com/vercel/ai/issues/1">#1</a>...</li>
<li>Additional commits viewable in <a
href="https://github.com/vercel/ai/compare/@ai-sdk/[email protected]...@ai-sdk/[email protected]">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@ai-sdk/google&package-manager=npm_and_yarn&previous-version=3.0.31&new-version=3.0.64)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-23 07:37:58 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Abdullah
80e8f6d516 chore(deps): bump @blocknote/server-util from 0.47.1 to 0.47.3 (#19997)
Bumps
[@blocknote/server-util](https://github.com/TypeCellOS/BlockNote/tree/HEAD/packages/server-util)
from 0.47.1 to 0.47.3.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/TypeCellOS/BlockNote/releases"><code>@​blocknote/server-util</code>'s
releases</a>.</em></p>
<blockquote>
<h2>v0.47.3</h2>
<h2>0.47.3 (2026-03-25)</h2>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>core:</strong> preserve whitespace edge cases but collapse
html formatting newlines (BLO-1065) (<a
href="https://redirect.github.com/TypeCellOS/BlockNote/pull/2551">#2551</a>,
<a
href="https://redirect.github.com/TypeCellOS/BlockNote/issues/2230">#2230</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Yousef</li>
</ul>
<h2>v0.47.2</h2>
<h2>0.47.2 (2026-03-20)</h2>
<h3>🩹 Fixes</h3>
<ul>
<li>use <code>&lt;details&gt;</code> &amp; <code>&lt;summary&gt;</code>
for toggle block HTML export (<a
href="https://redirect.github.com/TypeCellOS/BlockNote/pull/2524">#2524</a>)</li>
<li>remove <code>@hocuspocus/provider</code> peer dependency by inlining
tiptap comment types BLO-1064 (<a
href="https://redirect.github.com/TypeCellOS/BlockNote/pull/2564">#2564</a>)</li>
<li><strong>core:</strong> slash menu fails in custom blocks after space
BLO-1036 (<a
href="https://redirect.github.com/TypeCellOS/BlockNote/pull/2553">#2553</a>)</li>
<li><strong>i18n:</strong> fix typo in russian translation (<a
href="https://redirect.github.com/TypeCellOS/BlockNote/pull/2560">#2560</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Drone</li>
<li>Yousef</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/TypeCellOS/BlockNote/blob/main/CHANGELOG.md"><code>@​blocknote/server-util</code>'s
changelog</a>.</em></p>
<blockquote>
<h2>0.47.3 (2026-03-25)</h2>
<h3>🩹 Fixes</h3>
<ul>
<li><strong>core:</strong> preserve whitespace edge cases but collapse
html formatting newlines (BLO-1065) (<a
href="https://redirect.github.com/TypeCellOS/BlockNote/pull/2551">#2551</a>,
<a
href="https://redirect.github.com/TypeCellOS/BlockNote/issues/2230">#2230</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Yousef</li>
</ul>
<h2>0.47.2 (2026-03-20)</h2>
<h3>🩹 Fixes</h3>
<ul>
<li>use <!-- raw HTML omitted -->/<!-- raw HTML omitted --> for toggle
block HTML export (<a
href="https://redirect.github.com/TypeCellOS/BlockNote/pull/2524">#2524</a>)</li>
<li>remove <code>@​hocuspocus/provider</code> peer dependency by
inlining tiptap comment types BLO-1064 (<a
href="https://redirect.github.com/TypeCellOS/BlockNote/pull/2564">#2564</a>)</li>
<li><strong>core:</strong> slash menu fails in custom blocks after space
BLO-1036 (<a
href="https://redirect.github.com/TypeCellOS/BlockNote/pull/2553">#2553</a>)</li>
<li><strong>i18n:</strong> fix typo in russian translation (<a
href="https://redirect.github.com/TypeCellOS/BlockNote/pull/2560">#2560</a>)</li>
</ul>
<h3>❤️ Thank You</h3>
<ul>
<li>Claude Opus 4.6</li>
<li>Drone</li>
<li>Yousef</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/TypeCellOS/BlockNote/commit/cd92dc21be49397b658fef4e308e01ce8f5c04ad"><code>cd92dc2</code></a>
chore(release): publish 0.47.3</li>
<li><a
href="https://github.com/TypeCellOS/BlockNote/commit/b63b4096daa575f821980ef897fd90f4c76d9e42"><code>b63b409</code></a>
chore(release): publish 0.47.2</li>
<li><a
href="https://github.com/TypeCellOS/BlockNote/commit/d76fd68e016da698f3896f9d349a935a26a52d5f"><code>d76fd68</code></a>
test: get snapshots working again (<a
href="https://github.com/TypeCellOS/BlockNote/tree/HEAD/packages/server-util/issues/2554">#2554</a>)</li>
<li>See full diff in <a
href="https://github.com/TypeCellOS/BlockNote/commits/v0.47.3/packages/server-util">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@blocknote/server-util&package-manager=npm_and_yarn&previous-version=0.47.1&new-version=0.47.3)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

---------

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Abdullah <[email protected]>
2026-04-23 07:29:45 +00:00
39831338f7 i18n - translations (#19988)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-04-22 18:46:31 +02:00
WeikoandGitHub 876214bc1d scaffold record page layout + fields view when adding an object (#19977)
## Summary

Extends `yarn twenty add` → **Object** so it scaffolds a complete record
page out
of the box:

- A **record-page-fields view** (`<name>-record-page-fields.ts`,
FIELDS_WIDGET)
pre-populated with the `name` field plus the auto-generated default
fields (`createdAt`,
`updatedAt`, `createdBy`, `updatedBy`) — the default-field entries are
emitted as
`generateDefaultFieldUniversalIdentifier({ objectUniversalIdentifier,
fieldName: '...' })`
calls rather than pre-computed UUIDs, so the generated file
double-serves as
documentation for the public util.
- A **record page layout** (`<name>-record-page-layout.ts`) with a Home
tab whose Fields
widget points at the new view (via `viewUniversalIdentifier`), plus a
Timeline tab.
- The companion prompt now covers all three artefacts (was view + nav
menu item).

Fix: Server-side, renames `viewId` → `viewUniversalIdentifier` on the
universal-flat FIELDS
widget configuration so it is consistent with other universal-flat
references. The DB-side
DTO keeps `viewId` (now typed as `SerializedRelation`), and the
conversion utils map
between the two.

<img width="337" height="349" alt="Screenshot 2026-04-22 at 15 40 22"
src="https://github.com/user-attachments/assets/59e36540-1761-46b0-808d-648c68604268"
/>
2026-04-22 16:30:45 +00:00
Raphaël BosiandGitHub 0d996a5629 Resend app improvements (#19986)
## Summary
Major overhaul of the `twenty-for-twenty` Resend app to make sync more
reliable, observable, and feature-complete.
### SDK upgrade
- Bumps `twenty-sdk` to `2.0.0` and `twenty-client-sdk` to
`1.23.0-canary.1`
- Pins React back to `^18.2.0` to match the SDK
### Sync engine rewrite
- Splits the single `sync-resend-data` job into 4 staggered cron-driven
logic functions: **Emails**, **Contacts**, **Broadcasts (+ segments +
dependencies)**, **Templates** — each running every 5 minutes on a
different minute offset with per-slot timeouts
- Adds a new `ResendSyncCursor` object + `with-sync-cursor`
orchestration so each step persists its progress, last run timestamp,
and last run status
- Introduces an `INITIAL_SYNC_MODE` app variable +
`resend-initial-sync-mode-monitor` that flips to intermediate sync once
every cursor is empty (intermediate sync only refetches the last 7 days
of emails)
- Stops auto-creating People from Resend contacts; instead backfills
`personId` on Resend contacts/emails by matching existing People by
email
- Renames `on-*-deleted` handlers to `on-*-destroyed` and removes from
Resend on destroy (not soft delete)
- Adds rate-limit retry, paginated `for-each-page`, typed-client, and
existing-IDs lookup helpers

### New objects & fields
- New `ResendTopic` object with relation to `ResendBroadcast` (+
navigation menu item, view, page layout)
- New `ResendSyncCursor` object (step / cursor / last run at / last run
status)
- Adds `html` and `text` fields on `ResendBroadcast`; removes raw
`htmlBody`/`textBody`/`tags` from `ResendEmail`

### New UI
- **Sync Status standalone page** (`ResendSyncStatus` front component +
nav item) showing live cursor / last run state per step
- **Person Resend Email Stats** front component: deliverability rate +
per-status breakdown with progress bars
- **Email Broadcast HTML viewer** front component renders an individual
email against its parent broadcast's HTML; new dedicated **Broadcast
HTML viewer**
- Adds Resend Broadcast record page layout (Home / Preview / Timeline /
Tasks / Notes / Files tabs)
### Tests
- ~25 new unit / integration test files covering sync utilities, cursor
lifecycle, webhook handler, email-stats computation, sync-status page
resolution, and rate-limit retry
- Replaces legacy `fetch-all-paginated` tests with `for-each-page` tests
2026-04-22 18:17:27 +02:00
Charles BochetandGitHub 3ebeb3a3e8 feat(community): add github-connector example app (#19961)
## Summary

Adds a new community app at
`packages/twenty-apps/community/github-connector` that demonstrates a
complete, production-style GitHub integration built on the Twenty SDK.

It is extracted (and decoupled) from the internal `twenty-eng` workspace
so external developers can use it as a reference for their own
connectors.

What it ships:

- **Six synced objects**: `pullRequest`, `pullRequestReview`,
`pullRequestReviewEvent`, `issue`, `projectItem`, `engineer`
- **Logic functions** for periodic backfills (PRs, reviews, issues,
project items, contributors) and a single signed-webhook route trigger
(`POST /github/webhook`) that performs idempotent upserts for
`pull_request`, `pull_request_review`, `issues`, and `projects_v2_item`
events
- **Views, navigation menu items and a GitHub folder** so the data is
discoverable in the UI out of the box
- **Configurable repos / project numbers** via `GITHUB_REPOS` and
`GITHUB_PROJECT_NUMBERS` application variables — no hardcoded org

## Authentication

Two interchangeable modes (PAT preferred for quick setup, GitHub App
recommended for production):

1. **Personal Access Token** — set `GITHUB_TOKEN`. Used as-is for both
REST and GraphQL.
2. **GitHub App** — set `GITHUB_APP_ID`, `GITHUB_APP_PRIVATE_KEY`,
`GITHUB_APP_INSTALLATION_ID`. Issues a signed JWT, exchanges it for a
short-lived installation token, and caches the token until expiry.

Webhook signature verification (`X-Hub-Signature-256`) is enforced when
`GITHUB_WEBHOOK_SECRET` is set.

## Notes

- Built on `[email protected]` / `[email protected]`
- Decoupled from internal modules (`quality/bug`, `discord`, `release`,
`code-build`, `project-management`) — `mustBeQa` is inlined and a local
`github` nav folder replaces shared ones
- `npx twenty typecheck`, `yarn lint`, and `npx twenty build` all run
cleanly
- Includes a comprehensive README with setup, env vars, webhook
configuration, and the auth resolution flow
2026-04-22 18:17:08 +02:00
f30ef2432f i18n - translations (#19987)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-04-22 17:55:26 +02:00
Paul RastoinandGitHub 921a0f01c8 Forbid permissions update cross app role retarget (#19982)
closes https://github.com/twentyhq/twenty/issues/19807
2026-04-22 15:39:26 +00:00
WeikoandGitHub 0696290af4 fix(page-layout): hide deactivated fields from FIELDS widget and layout editor (#19984)
## Context

- The FIELDS widget resolved every viewField against
objectMetadataItem.fields, which
includes deactivated field metadata — so fields deactivated after being
added to a view
kept rendering on records.
- Same leak existed in the layout editor: deactivated fields appeared as
toggleable hidden
viewFields, and newly-deactivated object fields were auto-proposed via
the "missing
fields" flow.

## Fix
- pre-filter objectMetadataItem.fields to field.isActive at each entry
point
(useFieldsWidgetGroups, useFieldsWidgetEditorGroupsData,
useFieldsWidgetHiddenFields) and
inside buildDefaultFieldsWidgetGroups for the no-view fallback.
2026-04-22 15:28:29 +00:00
EtienneGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>Etienne
32e0425a65 Docs - Update getting started (#19976)
- Add product tour video
- Add new graphic design assets

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
Co-authored-by: Etienne <[email protected]>
2026-04-22 14:52:52 +00:00
f34ba6ac12 i18n - docs translations (#19983)
Created by Github action

Co-authored-by: github-actions <[email protected]>
2026-04-22 16:50:26 +02:00
Paul RastoinandGitHub f0a625c3f8 Cleanup application and app registration test util (#19981)
## Introduction
Centralizing integ test app cleanup
Role will be deleted by the app uninstall if exists
2026-04-22 14:39:58 +00:00
Félix MalfaitGitHubclaude[bot] <41898282+claude[bot]@users.noreply.github.com>
b010599000 fix(server): preserve kanban/calendar fields in view manifest sync (#19946)
## Summary

The `fromViewManifestToUniversalFlatView` converter hardcoded five view
fields to `null` instead of reading them from the manifest:
- `mainGroupByFieldMetadataUniversalIdentifier`
- `kanbanAggregateOperation`
- `kanbanAggregateOperationFieldMetadataUniversalIdentifier`
- `calendarLayout`
- `calendarFieldMetadataUniversalIdentifier`

As a result **any** Kanban view in an app manifest is rejected by
`validateFlatViewCreation` with `"Kanban view must have a main group by
field"`, and any Calendar view would trip the `view.entity.ts` check
constraint requiring `calendarLayout` + `calendarFieldMetadataId` to be
non-null. Discovered while trying to install
[`twenty-crm-meeting-baas`](https://github.com/Meeting-BaaS/twenty-crm-meeting-baas)
which ships a Kanban view.

## Changes

- **Server converter**: read all five fields from the manifest (with `??
null` fallback).
- **`ViewManifest` type** (`twenty-shared`): add the five fields so SDK
users can set them type-safely.
- **Move `ViewCalendarLayout`** from `twenty-server` to `twenty-shared`
so the manifest type can reference it. Seven import sites updated; the
front-end imports via generated GraphQL types and is unaffected.
- **Unit tests**: extend
`from-view-manifest-to-universal-flat-view.util.spec.ts` with
preservation + null-default cases for both Kanban and Calendar (5 tests
total).
- **Regression coverage**: add a Kanban view
(`post-cards-by-status.view.ts`) to the `rich-app` fixture grouped by
the existing `status` SELECT field. The existing
`applications-install-delete-reinstall` e2e test now exercises the
Kanban path end-to-end — a future regression here would fail CI.

Note: `expected-manifest.ts` and the `views.length` assertion in
`manifest.tests.ts` were updated to reflect the new fixture view.

## Test plan

- [x] `nx test twenty-server --
from-view-manifest-to-universal-flat-view` → 5/5 pass
- [x] `nx typecheck twenty-shared` / `twenty-sdk` / `twenty-server` → no
new errors (one pre-existing unrelated error in
`admin-panel.module-factory.ts`)
- [x] `nx lint twenty-shared` / `twenty-sdk` → clean
- [x] Manual install of the Meeting BaaS app on a dev workspace succeeds
with the Kanban view after this fix
- [ ] CI: SDK e2e `applications-install-delete-reinstall` passes against
the new fixture view
- [ ] CI: integration test `calendar-field-deactivation-deletes-views`
still passes after the enum move

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

---------

Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com>
2026-04-22 16:31:00 +02:00
Félix MalfaitandGitHub c2cf3eac50 feat(sdk): confirm authentication method on remote add (#19947)
## Summary

`yarn twenty remote add` only prints `✓ Default remote set to X.` after
authenticating. When using the OAuth path, the browser flow happens
silently — there's no line that says _"you authenticated"_ — so users
(including me this morning while installing a Twenty app) are left
wondering whether auth actually completed and which method was used.

This PR adds explicit confirmation of the auth step:

**New remote via OAuth**
```
✓ Remote "myremote" added (https://app.twenty.com) via OAuth.
✓ Default remote set to "myremote".
```

**New remote via API key**
```
✓ Remote "myremote" added (https://app.twenty.com) via API key.
✓ Default remote set to "myremote".
```

**Re-authenticating an existing remote**
```
✓ Re-authenticated "myremote" via OAuth.
✓ Default remote set to "myremote".
```

## Implementation

- `authenticate()` now returns the method actually used (`'OAuth' | 'API
key'`) instead of `void`. This correctly surfaces OAuth → API-key
fallback: if OAuth fails and the user drops into the API-key prompt, the
success line reflects that.
- New-remote and re-auth paths print distinct messages so the user can
tell which path they took.
- No new API calls — method name comes from which branch of
`authenticate()` succeeded.

## Test plan

- [x] `nx typecheck twenty-sdk` — clean
- [x] `nx lint twenty-sdk` — clean
- [ ] Manual smoke test: `yarn twenty remote add --as test --api-url
...` via OAuth, API key, and re-auth

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-04-22 13:18:25 +00:00
789f8aba5d i18n - translations (#19975)
Created by Github action

---------

Co-authored-by: github-actions <[email protected]>
2026-04-22 15:23:41 +02:00
68d509e98d Update settings application illustrations and app metadata previews (#19964)
## Summary
- Refresh the settings application visuals with new light/dark PNG
covers for the data model card
- Replace the custom and standard application carousel assets with the
new provided illustrations
- Align app chips, type tags, and application detail previews with the
updated icon and description treatment
- Keep the data model cover container and overlay button behavior intact
while swapping the underlying imagery

## Testing
- Not run (not requested)
- Existing frontend typecheck and formatting checks were exercised
during implementation

---------

Co-authored-by: Charles Bochet <[email protected]>
2026-04-22 13:07:41 +00:00
0c929e7903 refactor(tool-provider): rename web_search to exa_web_search, drop XOR toggle (#19969)
## Summary

- Today `WEB_SEARCH_PREFER_NATIVE` forces a **mutual exclusion**: either
the custom Exa tool preloads as `web_search` or the SDK-native
`web_search` binds. Same name, different backends.
- This PR lets them **coexist**. Custom Exa becomes `exa_web_search`;
native keeps `web_search`. The model picks based on tool descriptions.
- `WEB_SEARCH_PREFER_NATIVE` and `shouldUseNativeSearch()` are deleted.
Exa enablement follows `WEB_SEARCH_DRIVER` (existing). Native enablement
follows the agent's `modelConfiguration.webSearch.enabled` (existing).

## Key changes

**Config / service**
- Deleted `WEB_SEARCH_PREFER_NATIVE` (config-variables.ts)
- Deleted `WebSearchService.shouldUseNativeSearch()`
- `WebSearchService.isEnabled()` unchanged — still gates Exa
availability

**Custom tool rename**
- `ActionToolProvider.toolMap`: `'web_search'` → `'exa_web_search'`
- Descriptor name matches
- `WebSearchTool.description` rewritten to position Exa as
structured/entity-aware, complementary to native

**Native tool binder**
- `NativeToolBinder.bind()` drops the `shouldUseNativeSearch` gate.
Per-agent `modelConfiguration.webSearch.enabled` (inside
`getNativeModelTools`) stays authoritative.

**Chat**
- Preload list now always includes `exa_web_search` —
`ActionToolProvider` silently skips the descriptor when Exa is disabled,
so `getToolsByName` degrades gracefully
- Native tools always attempted; returns empty ToolSet when the model
doesn't support them
- `directTools = { ...preloadedTools, ...nativeSearchTools }` — both
present when both enabled
- `billNativeWebSearchUsage` called unconditionally (the function
already short-circuits on count ≤ 0)

**Workflow agent**
- Same unconditional billing pattern
- `WebSearchService` dependency removed

**System prompt**
- Dropped the special-cased `web_search` branch. Preloaded tools list
uniformly now.

**Frontend**
- `exa_web_search` reuses the same "Searching the web for X" display as
native
- Test coverage added

## Billing isolation (verified)

- `countNativeWebSearchCallsFromSteps` counts `toolName ===
'web_search'` only. After the rename, only native calls match. Exa calls
(`exa_web_search`) are billed separately via
`WebSearchService.emitUsageEvent` inside `search()`.
- No double-billing path.

## Behavior deltas (intended)

| Scenario | Before | After |
|---|---|---|
| Anthropic model + Exa enabled + PREFER_NATIVE=true | native only |
**both** |
| Anthropic + Exa enabled + PREFER_NATIVE=false | Exa only (as
`web_search`) | **both** |
| Non-native model + Exa enabled | Exa as `web_search` | Exa as
`exa_web_search` |
| Any model + Exa disabled + native supported | native only | native
only |
| Workflow agent with `webSearch.enabled=true` + Anthropic + Exa enabled
| native only | **both** |

## Known regression (accepted)

Customers who set `WEB_SEARCH_PREFER_NATIVE=false` to force Exa-only
will now **also** see native `web_search` if the model supports it.
There's no chat-level kill switch after this PR. Per discussion, this is
accepted — future model-level capability gating (in the model JSON) will
be the right place for that control.

## Stats

- 10 files, +63 / −73 (net deletion)
- Typecheck clean (server: 7 pre-existing unrelated, front: 13
pre-existing unrelated — zero new either side)
- Prettier clean

## Test plan

- [ ] `npx nx typecheck twenty-server` and `npx nx typecheck
twenty-front` pass
- [ ] With Anthropic + Exa enabled: chat shows both `web_search` and
`exa_web_search` in preloaded list; model can call either
- [ ] With Anthropic + Exa disabled: chat shows only native `web_search`
- [ ] With non-native model + Exa enabled: chat shows only
`exa_web_search`
- [ ] Workflow agent with `modelConfiguration.webSearch.enabled=true` +
Exa enabled: both available
- [ ] Billing: native calls billed via `billNativeWebSearchUsage`; Exa
calls billed via `WebSearchService.emitUsageEvent`; no double-billing
- [ ] Frontend: `exa_web_search` renders "Searching the web for X" the
same as `web_search`

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-04-22 14:57:44 +02:00
f018f17133 i18n - docs translations (#19970)
Created by Github action

Co-authored-by: github-actions <[email protected]>
2026-04-22 14:44:57 +02:00
Yash RajandGitHub a486ead39d fix: restore Try Twenty button text visibility on docs navbar (#19968)
# [Docs]: Fix blank Try Twenty button text in dark mode

## 🐛 Problem
The "Try Twenty" CTA button in the docs navbar appears blank/invisible
when users enable dark mode, making it impossible to click through to
the sign-up page.

**Issue:** #19965

##  Root Cause
CSS specificity conflict in `packages/twenty-docs/custom.css`:
- The dark mode CSS rule targets only the `<a>` element
- Mintlify renders button text in a nested `<span>` with class
`text-white` (white color)
- The `text-white` Tailwind utility directly applies `color: rgb(255 255
255)` to the span
- This overrides the inherited dark color from the parent `<a>` element
- **Result:** White text on white background = invisible button

##  Solution
Update the CSS selector in `packages/twenty-docs/custom.css` to target
both the `<a>` element AND the nested `<span>`:

**Before:**
```css
:is(.dark, [data-theme="dark"]) #topbar-cta-button a {
  background-color: #ffffff !important;
  color: #141414 !important;
}
2026-04-22 13:59:42 +02:00
44309a6fd9 refactor(tool-provider): rename NativeModelToolProvider to NativeToolBinderService (#19966)
**Stacked on top of #19962.**

## Summary

- `NativeModelToolProvider` lived under `providers/` and had the
`*-tool.provider.ts` suffix, but it never implemented `ToolProvider`,
wasn't in `TOOL_PROVIDERS`, had no descriptors, and wasn't executed by
`ToolExecutorService`. The shape misled readers.
- It's actually a **parallel concept**: a binder that produces
SDK-native tool objects (Anthropic `webSearch`, OpenAI `webSearch`,
etc.) which the AI SDK passes straight to the model. Opaque, not
serializable, never in the catalog, never dispatched by the executor.
- This PR renames + moves it to reflect that.

## Renames

| Before | After |
|---|---|
| `NativeModelToolProvider` (class) | `NativeToolBinderService` |
| `NativeToolProvider` (interface) | `NativeToolBinder` |
| `generateTools(context)` (method) | `bind(context)` |
| `providers/native-model-tool.provider.ts` |
`native/native-tool-binder.service.ts` |
| `interfaces/native-tool-provider.interface.ts` |
`native/native-tool-binder.interface.ts` |

## What doesn't change

- `ToolCategory.NATIVE_MODEL` enum stays (still used by
`getToolsByCategories`).
- `isAvailable()` signature unchanged.
- `WebSearchService.shouldUseNativeSearch()` toggle untouched — that's
product-level and belongs to a separate PR that handles the Exa
coexistence story.
- No behavior change. Pure rename + move.

## Why this matters for the broader architecture

This rename makes the native/binder concept **visible in the type system
and directory structure**. That's what later enables coexisting native +
custom tools (e.g., `web_search` native alongside `exa_web_search`
custom) without the current naming collision, because native tools are
no longer masquerading as a registry provider.

## Stats

- 5 files, +30 / −28.
- Blast radius: 4 files modified, 1 file renamed (git tracks as rename).
- Typecheck clean (7 pre-existing unrelated errors, zero new).
- Prettier clean.

## Test plan

- [ ] `npx nx typecheck twenty-server` passes
- [ ] AI chat: native `web_search` still works end-to-end when enabled
- [ ] Workflow AI agent: `ToolCategory.NATIVE_MODEL` still works (goes
through `bind()` now)
- [ ] MCP: unaffected (doesn't use native tools)

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-04-22 13:43:30 +02:00
2a5d5b36db refactor(tool-provider): kill execute_tool's dual dispatch (#19962)
**Stacked on top of #19960.**

## Summary

- `execute_tool` used to check `directTools[toolName]` first, falling
back to the registry. Same tool name, different wrapping: preloaded went
through `wrapToolsWithOutputSerialization`, fallback didn't. Silent
divergence — a model calling a CRUD tool via
\`learn_tools\`/\`execute_tool\` got raw output, while calling it as a
preloaded direct tool got compacted output.
- Now: `execute_tool` always routes through
`toolRegistry.resolveAndExecute`. One path, no fast-path.
- Output serialization (`compactToolOutput`) moves into the registry,
gated by a new `serializeOutput` flag on `hydrateToolSet` /
`resolveAndExecute` / `getToolsByName` / `getToolsByCategories` /
`ToolRetrievalOptions`. Chat passes `true`, MCP and workflow pass
`false`.

## Key changes

**Registry (`tool-registry.service.ts`)**
- `hydrateToolSet` options gain `serializeOutput?: boolean`; when true
the execute closure wraps dispatch result with `compactToolOutput`.
- `resolveAndExecute` signature: replaces unused \`_options:
ToolExecutionOptions\` with `{ serializeOutput?: boolean }`.
- `getToolsByName` and `getToolsByCategories` thread `serializeOutput`
through to `hydrateToolSet`.

**Meta-tool (`execute-tool.tool.ts`)**
- API changes from positional `(toolRegistry, context, directTools?,
excludeTools?)` to `(toolRegistry, context, options?: { excludeTools?,
serializeOutput? })`.
- `directTools` fallback removed. All invocations go to the registry.

**Chat (`chat-execution.service.ts`)**
- Passes `serializeOutput: true` to `getToolsByName` — preloaded tools
get compacted output from the hydrator, no external wrap needed.
- Drops the external `wrapToolsWithOutputSerialization(preloadedTools)`
call.
- `createExecuteToolTool` call now passes `{ serializeOutput: true }`.
Direct-tool and `execute_tool` paths produce identical output shape.

**MCP (`mcp-protocol.service.ts`)**
- `createExecuteToolTool` call updated to new options shape with `{
excludeTools: MCP_EXCLUDED_TOOLS }`. No `serializeOutput` flag → raw
output as today.

**Deletes**
- `output-serialization/wrap-tools-with-output-serialization.util.ts` —
sole caller removed.

## Behavior changes

- **Chat, `execute_tool` fallback path**: now produces compacted output
(matches direct path). Net effect: fewer tokens for CRUD results reached
via discovery. Intended improvement.
- **Chat, `execute_tool({toolName: 'web_search'})` edge**: today
silently hits the native tool via `directTools`; now returns \"tool not
found, use get_tool_catalog\". Self-correcting, rare — native tools are
always directly available to the model.
- **MCP**: no change. No `serializeOutput` flag → identical raw output.
- **Workflow agent**: no change. Doesn't use `execute_tool`.

## Test plan

- [ ] `npx nx typecheck twenty-server` passes (verified: 7 pre-existing
unrelated errors, zero new)
- [ ] \`npx jest
packages/twenty-server/src/engine/api/mcp/services/__tests__/mcp-protocol.service.spec.ts\`
passes in CI
- [ ] AI chat: call a preloaded tool (e.g. \`search_help_center\`)
directly → compacted output
- [ ] AI chat: call a non-preloaded CRUD tool via
\`learn_tools\`/\`execute_tool\` → compacted output (this is the
behavior change)
- [ ] AI chat: native \`web_search\` still works when model calls it
directly
- [ ] MCP: \`tools/call\` on a registry tool → raw output (nulls
preserved)
- [ ] Workflow AI agent: tool dispatch unchanged

Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
2026-04-22 12:49:35 +02:00