Commit Graph

254 Commits

Author SHA1 Message Date
Paul Rastoin 6c40c7b91a Deterministic system field universal identifier (#22565)
# Introduction

Close twentyhq/core-team-issues#2641

Auto-provisioned field metadata used to get its `universalIdentifier`
from three unrelated sources: random `v4()` on the server when creating
custom objects, hardcoded values in `STANDARD_OBJECTS`, and an ad-hoc
`v5` derivation in the SDK manifest build. This PR unifies all of them
behind the shared `getFieldUniversalIdentifier` derivation:

```
universalIdentifier = f(applicationUniversalIdentifier, objectUniversalIdentifier, fieldName)
```

## Ownership model

The rollout is built on an explicit split of who owns a field's
universal identifier:

- **The 8 system fields** (`id`, `createdAt`, `updatedAt`, `deletedAt`,
`createdBy`, `updatedBy`, `position`, `searchVector`) are
**server-owned**. Their universal identifiers are always the
deterministic derivation, on **every** application (standard,
workspace-custom, installed). Clients cannot provide custom values: a
temporary check in `validateObjectMetadataSystemFieldsIntegrity` rejects
any non-derived system field identifier at migration build time. This
check stands in until system fields are generated exclusively server
side by the metadata side-effect engine and stripped from client inputs
— at which point it becomes structurally impossible to send one.
- **`name` is a default field, not a system field**: it is
auto-provisioned when absent (server side for custom objects, SDK side
for application objects) but authors can define their own. It is only
derived where it is guaranteed to be auto-provisioned. In particular,
standard objects keep their **historical hardcoded** `name` identifiers:
the standard app authors its `name` fields like any installed app would,
and moving those identifiers would break every installed application
referencing them (e.g. views on `opportunity.name`).
- **User-created and author-provided fields** keep random / explicit
identifiers, untouched.

## Server

- `validateObjectMetadataSystemFieldsIntegrity` now validates, on top of
the existing type/`isSystem` checks, that each system field's
`universalIdentifier` equals the deterministic derivation. Runs for
every object creation going through the migration orchestrator: app
sync, custom object creation, standard provisioning
- `build-default-flat-field-metadatas-for-custom-object.util.ts` derives
the system field identifiers (and the auto-provisioned `name`) with
`getFieldUniversalIdentifier` instead of `v4()`
-
`build-default-relation-flat-field-metadatas-for-custom-object.util.ts`
derives both the forward and the reverse default relation field
identifiers deterministically
- `generateMorphOrRelationFlatFieldMetadataPair` accepts optional
`sourceFieldUniversalIdentifier` / `targetFieldUniversalIdentifier` so
callers can inject deterministic values; user-created relations still
default to `v4()`

## twenty-shared

- `STANDARD_OBJECTS` system field identifiers (the 8) are now computed
at module load via `buildStandardObjectSystemFields`; `name` and every
other identifier keep their hardcoded values
- New snapshot test pinning **every** universal identifier of
`STANDARD_OBJECTS`: any identifier change now requires an explicit
snapshot update and should ship with a coordinated backfill

## SDK (breaking, pre-GA)

- `generateDefaultFieldUniversalIdentifier` delegates to
`getFieldUniversalIdentifier` and now requires
`applicationUniversalIdentifier`
- Reverse default relation field identifiers are derived from the
field's real coordinates (standard object UID + actual field name, e.g.
`targetRocket` on `attachment`) instead of the legacy custom-object UID
+ synthetic `${fieldName}Inverse` hash input. Field *names* are
unchanged
- The manifest build threads the application universal identifier
through default field injection (two-pass over object configs)
- `twenty dev:add` now resolves the application universal identifier
upfront and refuses to scaffold anything until `defineApplication`
declares one — no more `fill-later` placeholder for the app UID in
generated files

## Upgrade

A 2.19 **workspace command** backfills existing
`fieldMetadata.universalIdentifier` rows to the deterministic
derivation. Coverage follows the ownership model:

- **The 8 system fields**: taken over for **every application**,
whatever value they currently hold. This is both safe and required now
that sync rejects non-derived values — leaving a row unconverged would
make its application unsyncable
- **`name`**: workspace-custom app → always taken over
(server-generated, no author to clobber); installed applications → only
rows still carrying the legacy SDK derivation are recomputed,
author-provided identifiers are never touched; standard app → never
touched (hardcoded in `STANDARD_OBJECTS`)
- **Default relation fields**: workspace-custom app → forward fields on
custom objects and reverse fields on the standard relation objects;
installed applications → legacy-derivation probe only

All identifiers of a workspace are updated inside a single transaction,
then the command flushes the field-metadata-related workspace caches and
bumps the metadata version.

Stored `applicationRegistration.manifest` snapshots are intentionally
**not** rewritten: installs and upgrades always sync from the
`manifest.json` inside the resolved package (npm/tarball), the stored
column is only used for display/marketplace purposes.

## Breaking behavior for old packages (fail closed)

Packages built with an older SDK carry legacy system field identifiers
in their tarball `manifest.json`. Installing or upgrading such a package
now fails with an explicit `INVALID_SYSTEM_FIELD` validation error
("universal identifier is not deterministic") instead of silently
mismatching against the backfilled rows and triggering a destructive
delete+create. The remediation is to rebuild the package with the new
SDK; the backfill has already converged the installed rows, so the
rebuilt manifest syncs cleanly.

## Test plan

- [x] `twenty-sdk` unit tests (526 tests) and typecheck
- [x] `twenty-shared` unit tests (1635 tests) including the
`STANDARD_OBJECTS` snapshot; `name` identifiers verified byte-for-byte
identical to `main`
- [x] Lint and typecheck clean on all touched packages
- [x] Integration: create a custom object and verify system + default
relation field identifiers match the deterministic derivation
(`create-one-object-metadata-deterministic-field-universal-identifiers`,
13 assertions passing)
- [x] Integration: `failing-sync-application-object-system-fields`
extended with a non-derived system field identifier case; all
identifiers in the spec pinned deterministically so snapshots embedding
expected/actual values are stable across runs (verified with a double
run)
- [x] Integration: all application sync suites pass with the derived
system field identifiers now required by the
`buildDefaultObjectManifest` test helper (9 suites, 20 tests)
- [x] Full test-database reset: standard app provisioning and seeded
workspaces pass the new validation
- [x] SDK manifest build verified on the postcard example app: all
auto-generated default field identifiers match the derivation
- [ ] Run
`upgrade:2-19:backfill-deterministic-field-universal-identifiers`
(dry-run then real) on a seeded workspace and verify identifier
convergence with a rebuilt app manifest
2026-07-06 13:34:33 +00:00
nitin fe8ec0463a Call recorder: parse conference links from calendar event text (#22555) 2026-07-05 17:44:13 +02:00
Charles Bochet 74b3a4216e chore(apps): remove the twenty-for-twenty app (consolidated into twenty-eng) (#22549)
Removes the internal `twenty-for-twenty` app from this public repo. It
has been consolidated into the private `twenty-eng` monorepo, which now
also owns the Resend module (moved there in a companion PR).

- Removes `packages/twenty-apps/internal/twenty-for-twenty/**` (177
files).
- No build wiring referenced it (no nx project, not in `nx.json`/root
workspaces); the only mention elsewhere is a naming-convention comment
in `twenty-linear`.

## ⚠️ Sequencing
- This removes **source only** — it does **not** uninstall the app
currently deployed on the workspace. Merge only **after** the twenty-eng
app has taken over the Resend objects there, so the live integration
isn't left orphaned.
- Supersedes the migration-plan doc PR (#22546), which added a doc into
this now-removed directory; that doc now lives in the twenty-eng app.
#22546 can be closed.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22549?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-04 18:05:30 +02:00
Charles Bochet 6b2405c2e9 feat(twenty-for-twenty): bump to SDK 2.18 with local UI components (#22542)
## What & why

Bumps the **Twenty for Twenty** internal app to the current SDK line and
serves it on the matching `twenty-app-dev` image.

- `twenty-sdk` / `twenty-client-sdk`: `^2.14.0` → **`^2.18.0`** (npm
latest)
- Runs against `twentycrm/twenty-app-dev:v2.18.5` (same 2.18 line)
- App version `0.1.0` → **`0.2.0`**

### The `twenty-sdk/ui` break

twenty-sdk 2.18 **removed the `twenty-sdk/ui` subpath** (#22326, "Remove
twenty-ui reexport from the SDK"). The intended replacement —
`twenty-ui@1.0.0-alpha.1` subpaths — requires **React 19 + a
monaco-editor peer** that this React-18 app can't adopt, so the app no
longer builds against 2.18 as-is.

Instead of migrating to twenty-ui, this replaces the four
`twenty-sdk/ui` consumers with **self-contained local components** under
`src/ui/`, imported via a new `@ui` alias:

- `Callout`, `H2Title`, `Status`
- Tabler-style inline-SVG icons (`IconAlertCircle`, `IconInfoCircle`,
`IconMail`, `IconRefresh`, `IconHelp`)
- a `ThemeColor` type

They mirror the twenty-ui components 1:1 using the `--t-*` theme CSS
variables the front-component host injects (the same inline-style
pattern the app already used for theme tokens) — no new runtime deps, no
React 19 requirement.

## Test plan

- `twenty dev:build` ✓, `yarn typecheck` ✓, `yarn lint` ✓
- Synced into a local `twenty-app-dev:v2.18.5` container (`twenty dev
--once`) — objects/fields/views/app install applied cleanly
- Verified rendering live: Sync Status page (`H2Title` headings +
`Status` "Not synced" pills) and all three `Callout` variants
(error/info/neutral) with correct colors + icons

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22542?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-04 12:34:41 +02:00
nitin badaae17cd Call recorder: skeleton loading state + port front components to twenty-ui (#22473)
## What this does

Three changes to the call-recorder app's recording/transcript front
component:

### 1. Fix loading flicker with a skeleton loader (`4dc3a167`)

The widget's loading state rendered an empty `<video controls>` element
at 16:9, which was torn down and replaced once the recording query
resolved — so every open flashed a video player, even for calendar
events with no video at all.

- New `RecordingSkeletonLoader`: three transcript-shaped pulsing rows
(avatar circle + speaker bar + text bars) held at the same 240px
min-height as the empty state, so nothing jumps when content arrives.
Visuals follow the twenty-front skeleton standard
(react-loading-skeleton theming: `background.tertiary` base,
`background.transparent.lighter` highlight sweep, 4px radius), rebuilt
with emotion since the sandboxed bundle can't import the host's global
skeleton stylesheet.
- `RecordingVideoPlayer` now only renders with a real URL (`src:
string`).

### 2. Port front components to twenty-ui (`cea0d6ee`)

`twenty-ui@1.0.0-alpha.1` is now published and consumable by apps
(CSS-injection build support + the renderer's style bridge), so the
"remove once twenty-ui can be imported safely" duplications are
resolved:

- Deleted `recording-theme-css-variables.ts` → all components use
`themeCssVariables` from `twenty-ui/theme-constants` (only non-1:1
rename: `accent.primary` → `accent.accent9`).
- Deleted `TranscriptSpeakerAvatar` / `TranscriptSpeakerChip` → replaced
with twenty-ui `Avatar` (`size="md"`, `type="rounded"`) and `Chip`
(`ChipVariant.Transparent`, `isBold`, non-clickable) in
`TranscriptEntryListItem`.
- Added `import 'twenty-ui/style.css'` to the front-component entry,
plus a root `css.d.ts` declaration. The import is load-bearing: the SDK
inlines the CSS into the component bundle and the renderer's style
bridge injects it into the host document — the host's own twenty-ui CSS
can't be relied on, since scoped class hashes are content-derived and
drift between builds.
- Bumped `twenty-sdk` / `twenty-client-sdk` to `^2.18.0` (needed for the
CSS-injection build support) and added `twenty-ui@^1.0.0-alpha.1`.

### 3. Fix manifest build + typecheck after the port (`baa6eb2f`)

- `yarn twenty dev --once` failed at "Building manifest..." — the
manifest build loads front-component modules with `twenty-ui`
proxy-mocked, so the named `themeCssVariables` import resolves to
`undefined` and any static `${themeCssVariables.x}` interpolation throws
at module scope. All static interpolations are now lazy (`${() =>
themeCssVariables.x}`), deferring the access to render time in the
browser.
- Added `css.d.ts` to `tsconfig.spec.json`'s `include` — its
`["src/**"]` include replaced the parent config's default file set, so
the `*.css` module declaration was never loaded and `yarn typecheck`
failed with TS2882.

## Notes for review / testing

- Worth exercising manually: the skeleton during load, speaker
chips/avatars rendering via twenty-ui, and hovering a long (overflowing)
speaker name — Chip's tooltip uses a react-dom portal, which is the one
path not previously exercised inside the front-component sandbox.
- App version intentionally left at `1.0.4`.

https://claude.ai/code/session_01WuHQFWiRocn82ejxsnmz28

---
_Generated by [Claude
Code](https://claude.ai/code/session_01WuHQFWiRocn82ejxsnmz28)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22473?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-03 23:10:28 +05:30
nitin cbbb11b774 Add configurable call recorder summaries (#22405)
<img width="2560" height="1319" alt="CleanShot 2026-07-02 at 16 58 03"
src="https://github.com/user-attachments/assets/d968bff7-4b57-4816-be9c-02e5af32ae3d"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22405?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-03 21:48:48 +05:30
martmull 25fe66565c feat(applications): add type and options to application variables (#22157)
## Before
<img width="1452" height="709" alt="image"
src="https://github.com/user-attachments/assets/cd384ffa-cbe6-49d5-a807-ca8d580f55a9"
/>

<img width="1074" height="452" alt="image"
src="https://github.com/user-attachments/assets/720d38db-3495-4032-8831-17d24ec6a7e7"
/>

## After

<img width="1421" height="865" alt="image"
src="https://github.com/user-attachments/assets/2275c996-c895-4800-8324-2aa2ddfddd43"
/>

<img width="1348" height="870" alt="image"
src="https://github.com/user-attachments/assets/3e1a891d-6db0-4cbd-870a-2a5bbde4929d"
/>


## Summary

Adds typed application variables with optional select **options**. This
is the other half of #22059, split out from the custom-settings-tab
removal.

## Changes

- **Shared types**: `ApplicationVariable` / `ServerVariables` gain an
optional `type` (a `FieldMetadataType` subset — `TEXT`, `BOOLEAN`,
`NUMBER`, `DATE`, `SELECT`, `MULTI_SELECT`, `RAW_JSON`, `RICH_TEXT`,
`ARRAY`, …) and select `options`. New
`serializeApplicationVariableValue` /
`deserializeApplicationVariableValue` helpers convert typed values
to/from the encrypted string storage.
- **Server**: `type`/`options` columns on `applicationVariable` and
`applicationRegistrationVariable` (entities + DTOs), a fast `2-17`
instance command, manifest processing via the serialization helpers, and
a `QueryDeepPartialEntity` cast where the manifest JSON column is
persisted.
- **Frontend**: a polymorphic `SettingsApplicationVariableInput` that
renders the native `Form*` field component for each type (boolean,
number, date/date-time, select, multi-select, array, raw JSON, rich
text, text); fragment/query updates to fetch `type`/`options`.
- **SDK**: `defineApplication` validates that `SELECT`/`MULTI_SELECT`
variables declare non-empty `options` at build time (since `options` is
kept structurally optional for TypeORM/SDK compatibility).

Variables default to `TEXT` when no type is given, so existing manifests
are unaffected.

## Notes

The generated GraphQL artifacts (`type`/`options` on the variable types)
are regenerated by codegen; that change accompanies this PR.

https://claude.ai/code/session_013Z7UB35V2mvUozh55QHG23

---
_Generated by [Claude
Code](https://claude.ai/code/session_013Z7UB35V2mvUozh55QHG23)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22157?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-03 10:52:22 +02:00
nitin bc1a4cb3fb feat(call-recorder): cap media file size during ingestion to avoid OOM (#22463)
Media ingestion buffered whole Recall files in memory; long recordings
OOM'd the logic function.

- Caps the size of ingested media files, configurable via the
`CALL_RECORDER_MAX_MEDIA_FILE_SIZE_MB` app variable (default 80 MB).
- Downloads stream chunk by chunk and stop at the cap; a Content-Length
above the cap skips the download without reading the body.
- A skipped file is recorded as `video_file_too_large` /
`audio_file_too_large` in `callRecorderFailureReason`; the completion
gate treats a marked file as resolved, so the recording still completes
and bills with its remaining artifacts.
- A real failure reason always wins over the size markers when the
recording fails.

Deferred: the cap is a stopgap until core supports streaming uploads
(TODO in code).

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22463?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-02 20:55:47 +05:30
martmull f7f224aa7a Fix lint (#22416)
as title

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22416?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-01 20:56:54 +02:00
martmull 05ce08ddba Add twenty-app keyword (#22415)
as title, bump version

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22415?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-01 18:01:35 +00:00
martmull efd600c12b Update app name (#22410)
as title

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22410?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-01 16:33:32 +00:00
martmull 868ae4cbfd feat(last-contact): design for Last contact by + Last contact item (#22308)
To test, go to
https://twenty-applications.twenty.com/settings/applications/6aa2ca76-fdbe-456d-89ab-c622452ef055

## After

<img width="1512" height="697" alt="image"
src="https://github.com/user-attachments/assets/5b17f94e-6e0e-400a-8291-a39ad796e423"
/>


## What

Adds the design spec for the next version of the `twenty-last-contact`
public app, extending it from a single `lastContactAt` date column to
the three-column experience in the app's cover image on the All People
view:

1. **Last contact by** — the team member who last interacted with the
person (`ACTOR` field).
2. **Last contact** — the existing `lastContactAt` field, unchanged.
3. **Last contact item** — the email or meeting that was the last
contact, as a clickable record (`MORPH_RELATION` → message |
calendarEvent).

All three columns always describe the same single most-recent
interaction (atomic "newer wins" update).

This PR contains the **design doc only** —
`docs/superpowers/specs/2026-06-29-last-contact-by-and-item-design.md`.
Implementation follows.

## Why

The app today only answers *when* you last talked to someone. These
fields also answer *who* on your team and *through which* email/meeting,
matching the product vision in the cover.

## Key design decisions

- **`lastContactBy` is an ACTOR**, with the team member resolved from
the interaction's participants (`messageParticipant` /
`calendarEventParticipant` both carry `workspaceMemberId` +
`workspaceMember`).
- **No provider (Gmail/Outlook) logo.** That data lived on
`connectedAccount`, which v2.7
(`drop-connected-account-standard-object`) removed from the
app-queryable workspace schema. Confirmed acceptable; the actor still
shows the member + an email/calendar source.
- **`lastContactItem` is a MORPH_RELATION** following the SDK pattern
used by `attachment` / `noteTarget` / `taskTarget` (shared `morphId`,
one field per target, reverse relation on each target object).

## Reviewer notes

- **Load-bearing open risk** documented in the spec: how to *write* a
morph relation through the app's GraphQL API — no app in the repo writes
morph yet. The plan starts with a spike on this; if morph writes aren't
supported from an app, the fallback is two nullable `RELATION` fields
(`lastContactMessage` / `lastContactCalendarEvent`).
- No code/behavior change yet — safe to merge or hold as the design of
record.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22308?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-01 15:40:21 +02:00
Rashad Karanouh e0ebe0e029 v1.2.0 — Client brief intake (marketplace B2) (#22290)
## Summary

**Version:** 1.2.0
(`packages/twenty-apps/internal/twenty-partners/package.json`)

Adds marketplace B2 client brief intake to the partners app:

- `POST /s/client-briefs` logic function — validates payload, creates an
unlisted Opportunity (`isListed = false`) with `— client brief` name
suffix
- **Client briefs** ops view for review before listing
- Unit + integration tests

Merge this **before** the website PR (`rk-client-brief-web`).

## Test plan

- [ ] `yarn twenty dev --once` syncs schema
- [ ] `yarn test` passes in `twenty-partners`
- [ ] Smoke: `POST /s/client-briefs` with `x-application-secret` creates
Opportunity with `isListed = false`
- [ ] Client briefs view visible in nav

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22290?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-30 14:08:50 +02:00
Raphaël Bosi 0dc6272da5 Remove twenty-ui reexport from the SDK and use twenty-ui directly (#22326)
## What & why

Removes the `twenty-sdk/ui` reexport. Apps now use Twenty UI by
installing
[`twenty-ui@1.0.0-alpha.1`](https://www.npmjs.com/package/twenty-ui/v/1.0.0-alpha.1)
from npm and importing its subpaths directly. The reexport re-exported
types that didn't resolve, forcing typecheck workarounds.

## Changes

- **twenty-sdk**: delete `src/ui/index.ts`, drop the `./ui` export,
remove it from the browser vite build, and rewire the CLI manifest-mock
to `twenty-ui` (`.css` falls through to the empty-CSS loader).
`twenty-ui` stays a devDependency for the CLI fixture tests.
- **Renderer + create-twenty-app template**: import from `twenty-ui`
subpaths; the template pins `twenty-ui@1.0.0-alpha.1`.
- **Docs**: new "Using Twenty UI components" section (install + subpath
imports + `useTheme()` for theme tokens), codex references, and the
cross-doc-contract validator.

The `twenty-for-twenty` / `twenty-slack` example apps are intentionally
left on `twenty-sdk/ui`: they consume the published SDK (which still
ships `./ui`), and `twenty-ui@1.0.0-alpha.1` requires react 19 + a
`monaco-editor` peer the react-18 apps can't satisfy. They migrate once
the SDK is republished.
2026-06-30 11:17:48 +02:00
nitin 4045f9852e feat(call-recorder): use workspace logo for Recall bot image (#22302)
workspace logo - 

<img width="433" height="465" alt="CleanShot 2026-06-29 at 20 43 11"
src="https://github.com/user-attachments/assets/946782a5-7bab-461b-a613-ce056fec8bbc"
/>


how bot appears - 

<img width="2560" height="1315" alt="CleanShot 2026-06-29 at 20 47 01"
src="https://github.com/user-attachments/assets/91daa29c-83ef-4ac1-9d7c-ffa8bcf08135"
/>



<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22302?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-30 14:01:15 +05:30
nitin 996d3a7921 bump call recorder (#22318)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22318?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-30 13:29:43 +05:30
nitin f263edd122 Add call recorder oom diagnosis logs (#22315)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22315?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-29 17:14:06 +02:00
martmull 8a257e0de3 Update public app names (#22228)
- update names to comply with @twentyhq/ prefix standard
- update versions
- upgrade twenty sdks versions 

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22228?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-26 17:51:01 +02:00
martmull 1bf0d06e18 Add author to twenty apps (#22226)
as title

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22226?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-26 15:11:37 +00:00
martmull a22eb5a591 Bump call-recorder and people-data-lab apps (#22213)
as title

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22213?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-26 12:21:43 +02:00
martmull d6fca7a82e Bump call-recorder and people-data-lab apps (#22208)
as title

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22208?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-26 11:24:56 +02:00
martmull 1cf1c33899 Add command menu items to people data labs (#22180)
- Adds command menu items to enrich people and companies
- Improve readme

## After

<img width="1039" height="584" alt="image"
src="https://github.com/user-attachments/assets/9c6c3967-e905-4bf2-a3ef-307ae5bdca92"
/>


<img width="2560" height="332" alt="image"
src="https://github.com/user-attachments/assets/4ab4ffb8-fe52-4612-99c7-282d0ffa2c94"
/>
<img width="2560" height="478" alt="image"
src="https://github.com/user-attachments/assets/d496e21b-de18-4c52-acc7-6106afca0bb7"
/>
<img width="2560" height="452" alt="image"
src="https://github.com/user-attachments/assets/caea4de3-5bee-4de7-a347-c5d8540fd179"
/>
<img width="2560" height="335" alt="image"
src="https://github.com/user-attachments/assets/92bd17e3-f4b6-4fcb-84ae-d8ebc7ccce1e"
/>


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22180?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-25 15:12:30 +00:00
nitin aabffc427a Call recorder readme nitpick (#22185)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22185?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-25 17:05:41 +02:00
nitin f270fb25c8 call recorder readme billing update (#22184)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22184?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-25 20:25:58 +05:30
nitin 4f429565e1 call recorder polishes (#22170)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22170?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-25 18:27:43 +05:30
Raphaël Bosi 19bbd59b53 Support CSS imports in front-components via runtime style injection (#22150)
Front-components compile to a remote-dom worker, so a CSS import like
`import 'twenty-ui/style.css'` can't load a stylesheet and was breaking
the build at the manifest step.

This makes the build inline an imported CSS file as a runtime `<style>`
injection that flows through the existing style bridge into the host.
Because the CSS is bundled alongside that same build's hashed class
names, an app's styling matches its own twenty-ui version regardless of
which version the host ships — no server, manifest, or host changes
needed.

The manifest extractor keeps the no-op CSS loader (it executes the
bundle in Node, where `document` is undefined); the inject plugin runs
only in the real build and the dev watcher.
2026-06-25 13:31:00 +02:00
Rashad Karanouh 2af5370749 v1.1.16 — Restore Partner slug in side panel and Notes tab (#22165)
## Summary

**Package version:** `1.1.16`

- Adds **slug** to the Partner record-page `FIELDS_WIDGET` view so it
appears in the side panel for admins and partners (partners remain
update-locked on slug via `partner.role.ts`).
- Restores the **Notes** tab on the custom Partner `RECORD_PAGE` layout
— the marketplace v2 layout replaced the platform default but only
included Home + Timeline.

## Test plan

- [ ] `yarn lint` in `packages/twenty-apps/internal/twenty-partners` — 0
errors
- [ ] `yarn twenty dev --once` on a local partners workspace — sync
succeeds
- [ ] Admin: open a Partner record full page → slug visible under Name
in side panel; **Notes** tab present and can create a linked note
- [ ] Partner role (My Profile): slug visible, not editable; Notes tab
works
- [ ] After merge: `deploy` + `install` on prod partners workspace

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22165?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-25 13:20:53 +02:00
nitin db687c7407 chore(call-recorder): drop FAILED status schema bridge (#22134)
Cleanup of the call-recorder data layer + the SDK 2.16 bump and its
fallout, now that the `FAILED_UNKNOWN → FAILED` rename shipped in
`twenty/v2.16.0` (#22062).

- **Drop the schema bridge.** `executeCurrentSchemaMutation` and the
integration-test compatibility filter existed only to work around
servers exposing `FAILED_UNKNOWN`. Deleted the bridge;
`updateCallRecording` / `completeCallRecordingIngestion` call
`client.mutation(...)` directly. Integration test iterates all
`CallRecordingStatus` values.
- **Bump SDK** `twenty-sdk` / `twenty-client-sdk` to `2.16.0`.
- **One export per file.** Move `CallRecordingUpdateFields` to its own
type file; extract the duplicated media-file shape into
`CallRecordingMediaFile`.
- **Migrate the Recall webhook to `serverRouteTriggerSettings`** (2.16
dropped `serverWebhookTriggerSettings` + its declarative
`workspaceIdResolver`). The webhook is now a **resolver**
(`recall-webhook`) that verifies the Svix signature, reads
`twentyWorkspaceId` from the Recall bot metadata, and returns `{
workspaceId, targetLogicFunctionUniversalIdentifier, payload }`; the
platform dispatches to a new **target** function
(`process-recall-webhook`) in the resolved workspace, where
`CoreApiClient` is workspace-scoped. Resolver UID/route unchanged, so
the registered Recall endpoint URL stays valid. Failures now throw →
HTTP 500 (Svix retries) instead of returning 401/400.

Verified: typecheck, 213 unit tests, oxlint, oxfmt all green. **Not yet
verified end-to-end against a live server** — call-recorder is the first
app on `serverRouteTriggerSettings`, so a real Recall webhook should be
tested through the resolver→target path before relying on it.
2026-06-25 11:06:58 +02:00
Paul Rastoin f04db9751f fix(client-sdk): bundle metadata client into a single self-contained file (#22085)
## Problem

A front component that imports `MetadataApiClient` from
`twenty-client-sdk/metadata` crashes at render time:

```
FrontComponent error: Failed to resolve module specifier "./chunk-Dqa2HsxW.mjs".
Invalid relative url or base scheme isn't hierarchical.
```

(hash differs per build). The equivalent component using `CoreApiClient`
from `twenty-client-sdk/core` works fine.

## Root cause

The front-component renderer loads each SDK client as a **single
in-memory blob-URL module** and only rewrites the two bare specifiers it
knows (`twenty-client-sdk/core`, `twenty-client-sdk/metadata`). A
blob-URL module cannot resolve a **relative** `import … from
"./chunk-*.mjs"` (blob URLs aren't hierarchical), and that chunk isn't
served anyway.

Only two entrypoints are externalized by the front-component build
(`FRONT_COMPONENT_EXTERNAL_MODULES`) and thus served as blob modules:
`core` and `metadata`. Everything else (`rest`, `generate`) is bundled
into the component and is unaffected. Of those two:

| client | how `dist/*.mjs` is produced | self-contained? |
|---|---|---|
| **core** | esbuild single-file bundle (`compileGeneratedClient`),
re-run per workspace at server runtime by `replaceCoreClient` |  |
| **metadata** | the shared multi-entry Vite build, which hoists shared
code into a relative `chunk-*.mjs` |  |

The metadata client is built once at package-build time (it is not
workspace-specific) and was shipped straight from the multi-entry Vite
output, keeping the unresolvable relative chunk import.

## Regression trace

This was **not** broken on arrival — it regressed via a transitive
bundler swap:

| Date | Commit | Event |
|---|---|---|
| 2026-05-20 | `a26fe3bb65` | Metadata-client-in-front-components
shipped; `twenty-client-sdk` on **Vite 7 (Rollup)** |
| 2026-06-08 | `d2e7dc0e74` (#21309, *"security: bump vulnerable direct
dependencies"*) | Bumped **Vite 7 → 8**, introducing **Rolldown 1.0.3**
(no rolldown entries in the lockfile before this commit) |

Vite 7 is Rollup-based; Vite 8 uses Rolldown. The breaking artifact is
literally a `\0rolldown/runtime.js` shared chunk — a Rolldown construct
that could not have existed before the bump. So the metadata
front-component path worked from 2026-05-20 until the 2026-06-08
security dependency bump silently changed the bundler and split out the
shared runtime chunk.

## Fix

Build the metadata client as its **own single-entry Vite library**
(`vite.metadata.config.ts`) so its output is a single self-contained
file with no shared chunk. `core` / `rest` / `generate` stay in the main
multi-entry build (`vite.config.ts`); shared config (`isExternal`,
`entryFileNames`) is factored into `vite.shared.ts`. The build pipeline
runs `vite build && vite build -c vite.metadata.config.ts`.

The server picks this up automatically: `SdkClientGenerationService`
ships the pre-built package `dist/` and only regenerates the **core**
client; it never regenerates metadata. No server-side change required.

## Regression guard (e2e)

The postcard example's `card.front-component.tsx` previously used
`CoreApiClient` only, so this metadata-only regression had no e2e
coverage. It now loads and round-trips all three SDK clients (`Core`,
`Metadata`, `Rest`) via an SDK health panel, and the e2e asserts the
blob-served `core` + `metadata` probes reach `ok` — which only happens
if those bundles resolve and function. A future chunk-import regression
in either blob module would crash the component on load and fail the
test.

## Verification

- `npx nx build twenty-client-sdk` succeeds.
- `dist/metadata.mjs` / `dist/metadata.cjs`: **0** `chunk-*` imports,
**0** relative imports; both load and export `MetadataApiClient` +
`MetadataSchema`.
- `dist/metadata/index.d.ts` types still emitted.
- `npx nx typecheck` + `npx nx lint twenty-client-sdk` pass; postcard
app typecheck + lint pass.

## Notes

- `dist/` is not committed (CI builds it); a running server must rebuild
`twenty-client-sdk` for the fix to take effect.
- The e2e was validated statically (typecheck + lint); running it
end-to-end requires a live stack with a seeded postcard record.
2026-06-25 10:19:18 +02:00
nitin 811fecc119 chore(apps): move call-recorder to public, remove stale meeting-bot duplicate (#22132)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22132?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-25 00:13:13 +05:30
martmull 14e4656fe3 Update TypeScript moduleResolution to bundler (#22127)
## Summary
Updates the TypeScript `moduleResolution` configuration from `"node"` to
`"bundler"` across template and example tsconfig files. This aligns with
modern TypeScript best practices for bundler-based projects.

## Changes
- Updated `moduleResolution` setting in
`packages/create-twenty-app/src/constants/template/tsconfig.json`
- Updated `moduleResolution` setting in
`packages/twenty-apps/examples/hello-world/tsconfig.json`
- Updated `moduleResolution` setting in
`packages/twenty-apps/examples/postcard/tsconfig.json`

## Details
The `"bundler"` module resolution strategy is the recommended approach
for projects using modern bundlers (Vite, Webpack, etc.) as it provides
better compatibility with ESM and package.json exports field resolution.
This change ensures that new projects created from the template and
example applications follow current TypeScript best practices.

https://claude.ai/code/session_01XBdmaN1bpnE7DiH1XwBuX4
2026-06-24 20:00:59 +02:00
Parship Chowdhury 6ee5413951 chore(vite): replace vite-tsconfig-paths with resolve.tsconfigPaths (#22100)
### Summary
Migrates main monorepo packages from the `vite-tsconfig-paths` plugin to
vite’s built-in path resolution.

Vite 8 showing this warning when the plugin is detected:
> The plugin "vite-tsconfig-paths" is detected. Vite now supports
tsconfig paths resolution natively via the resolve.tsconfigPaths option.
You can remove the plugin and set resolve.tsconfigPaths: true in your
Vite config instead.

### References
- https://vite.dev/config/shared-options#resolve-tsconfigpaths
- https://vite.dev/guide/features#paths
- https://github.com/vitejs/vite/pull/21781

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22100?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

---------

Signed-off-by: Parship Chowdhury <parshipchowdhury@gmail.com>
2026-06-24 19:03:18 +02:00
martmull c71663946e chore(apps): move public apps to packages/twenty-apps/public and generalize CI workflow (#22096)
## What

Introduces a `packages/twenty-apps/public/` folder and moves the
publicly publishable apps into it, then generalizes the apps CI workflow
to cover both folders.

### Moves
The following apps were moved from `packages/twenty-apps/internal/` to
`packages/twenty-apps/public/` (via `git mv`, history preserved):
- `people-data-labs`
- `twenty-discord`
- `twenty-exa`
- `twenty-fireflies`
- `twenty-last-contact`
- `twenty-linear`
- `twenty-meeting-bot`
- `twenty-slack`

These remain in `internal/`: `self-hosting`, `twenty-for-twenty`,
`twenty-partners`.

### Workflow
- Renamed `.github/workflows/ci-internal-apps.yaml` →
`.github/workflows/ci-twenty-apps.yaml`.
- The discover job now scans **both** `packages/twenty-apps/internal`
and `packages/twenty-apps/public`:
  - the "no nested `.github`" guard checks both folders,
  - `changed-files` watches both globs,
- the matrix builder iterates over both roots (guarded with `existsSync`
so a missing folder is a no-op).
- Each matrix entry still carries its own `path`, so the `ci` job works
unchanged regardless of which folder an app lives in.

## Notes
- The apps are standalone packages (own `yarn.lock`, not part of the
root Nx workspaces), so no root `package.json` / `nx.json` / `tsconfig`
changes were needed.
- The companion publish workflow lives in `twentyhq/twenty-infra`
(`publish-internal-apps.yaml` → `publish-public-apps.yaml`) and is
updated in a paired PR.

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

---
_Generated by [Claude
Code](https://claude.ai/code/session_01Fmu3DWf1yTTkVW49eSkXwh)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22096?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-24 17:24:16 +02:00
Rashad Karanouh 53bfc6ab1c fix: update skill to fit requirements (#22097)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22097?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-24 17:02:43 +02:00
nitin 0bccbb5035 rename twenty meeting bot to call recorder (#22093)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22093?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: martmull <martmull@hotmail.fr>
2026-06-24 17:02:34 +02:00
nitin 411aee8b96 update readme file for meeting bot (#22069)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22069?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-24 19:34:30 +05:30
nitin 73e9374ef8 [BREAKING CHANGE] harden call recording failure handling (#22062)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22062?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-24 15:08:26 +02:00
Rashad Karanouh eb53dee3be fix(twenty-partners): opportunity stage constants + Partners per Stage table (v1.1.9) (#22052)
## Summary

Two related cleanup items for the partners workspace:

### 1. Opportunity `stage` field — shared constant + reference catalog

- Adds `src/constants/opportunity-stage-options.ts` exporting
`OPPORTUNITY_STAGE_FIELD_UNIVERSAL_IDENTIFIER` and an
`OPPORTUNITY_STAGE_OPTIONS` catalog (stock stages + **Done** /
**Dead**).
- **`deals-board.view.ts`** imports the shared field id instead of
inlining `STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS`.
- The catalog is **not** synced by the app manifest — it documents
planned option ids for scripts/reference only.

**Done / Dead on prod:** added manually in Settings → Objects →
Opportunity → Stage (not via app sync). `defineField` extension of the
standard `stage` field was attempted and rejected
(`FIELD_ALREADY_EXISTS`). Deals kanban groups in the manifest stay
NEW–CUSTOMER only; Done/Dead columns appear when the workspace has those
options.

**Prod data cleanup (outside this PR):** all opportunities moved to
**Done** except `ed95573c-cbe6-4dcc-a459-5369a7449636` (Aranya CRM
Migration, kept in **New**).

### 2. Partners per Stage view — TABLE with stage grouping

Replaces the old KANBAN board with a **grouped TABLE** (same pattern as
Partners per Country / Applications):

- **Type:** `TABLE` (was `KANBAN`)
- **Group by:** `validationStage` — Application, Potential, Validated,
Former, Rejected
- **Columns:** Name, Country, Categories (`partnerScope`), Tier
(`partnerTier`)
- **Nav icon:** `IconTable`

View / view-field / group universal ids were aligned to prod after
KANBAN→TABLE install recreated the view (Twenty cannot change view type
in place).

## Version

**`1.1.9`** in this branch. Pre-merge deploys to `partner-twenty-com`
went through **1.1.5 → 1.1.11** while iterating on the view; prod may be
ahead of this branch’s pinned view id — one more id-alignment install
after merge may be needed if nav doesn’t land on the grouped table.

## Files touched (twenty-partners only)

| File | Change |
|---|---|
| `src/constants/opportunity-stage-options.ts` | New — stage field id +
option catalog |
| `src/views/deals-board.view.ts` | Import shared stage field constant |
| `src/views/partners-per-stage.view.ts` | KANBAN → grouped TABLE +
columns |
| `src/navigation-menu-items/partners-per-stage.navigation-menu-item.ts`
| Icon → `IconTable` |
| `package.json` | **1.1.9** |

## Test plan

- [x] `yarn lint` in `packages/twenty-apps/internal/twenty-partners` —
0/0
- [x] Prod: Done/Dead stage options present; Deals kanban shows 7
columns, no duplicates
- [x] Prod: Partners per Stage — TABLE grouped by validation stage with
Name / Country / Categories / Tier
- [x] Prod: Opportunity cleanup — 20 → Done, 1 stays New (Aranya CRM
Migration)
- [ ] After merge + install on fresh workspace: Partners per Stage
grouping renders without manual view fixes
2026-06-24 11:52:23 +02:00
Rashad Karanouh 6ae6703e7d Map TFT useCase to partners need on opportunity import (#22054)
## Summary
- Accept optional `useCase` in the TFT → partners
`import-opportunity-from-tft` webhook payload
- Map it to `Opportunity.need` on create (TFT Use Case → partners Needs)
- Add unit tests for happy path and null `useCase` handling
- Bump twenty-partners to 1.1.4 (patch)

## Test plan
- [x] `yarn test:unit` (43 tests passing)
- [x] `yarn lint` (0 errors)
- [ ] Deploy to local/partners workspace with `yarn twenty dev --once`
- [ ] POST smoke test with `useCase` in body; confirm **Need** field
populated
- [ ] TFT workflow: add `"useCase": "{{record.useCase}}"` to HTTP body
(manual)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22054?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-24 11:43:46 +02:00
nitin 9a62cb5a67 add meeting bot app cover (#22049) 2026-06-23 23:29:39 +05:30
nitin 5e8932001c Simplify Recall webhook logic function config (#22026)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22026?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 20:22:44 +05:30
Rashad Karanouh ceecf33555 fix(twenty-partners): restore partner side panel field visibility (#22024)
## Summary

- Restores 17 partner profile fields in the `FIELDS_WIDGET` view backing
the Partner record page side panel (My Profile, admin partner views).
- Read-locks `validationStage` and `partnerTier` for the Partner role so
admins still see them on the record page but partners do not on My
Profile.
- Renames legacy `profilePicture` label to "Profile Picture (legacy)" to
distinguish from the new file field.
- Bumps `twenty-partners` to **1.1.3** (already deployed to prod).

## Test plan

- [ ] `yarn lint` in `packages/twenty-apps/internal/twenty-partners` — 0
errors
- [ ] `yarn twenty dev --once` on a local workspace — sync succeeds
- [ ] As admin: open a Partner record → side panel shows all profile
fields including validationStage and partnerTier
- [ ] As Partner role (My Profile): side panel shows profile fields but
**not** validationStage or partnerTier
- [ ] Upgrade path: install v1.1.3 on an existing workspace — view
fields update in place

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22024?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 16:49:38 +02:00
Abdullah. bab71afe54 fix(deps): bump vitest to 4 in twenty-meeting-bot (drops vulnerable esbuild) (#22025)
## Summary

Bumps **vitest** `^3.1.1 → ^4.1.9` in `twenty-meeting-bot`, which lets
**vite** resolve to **8.0.16** — and vite 8 dropped esbuild entirely
(moved to rolldown). That **removes the vulnerable transitive
`esbuild@0.27.7` outright**, resolving [Dependabot alert
#1470](https://github.com/twentyhq/twenty/security/dependabot/1470) —
GHSA-g7r4-m6w7-qqqr (esbuild dev-server arbitrary file read on Windows,
`>=0.27.3 <0.28.1`).

## Why a parent-bump, not a resolution

- The vulnerable esbuild came from `vite@7.3.5` (`esbuild ^0.27.0`, a
`0.x` caret capped at `<0.28` — so `yarn up` couldn't reach the fix).
- vite is gated by vitest's vite range: vitest **3.x** allows only
`^5||^6||^7` (caps vite at 7 → esbuild 0.27); vitest **4.x** allows
`^8`, and **vite 8 has no esbuild dependency at all**.
- So bumping vitest lets vite resolve to 8, which **eliminates the
vulnerable dependency entirely** — no `resolutions` entry to force or
maintain. (Matches the repo's stated preference: fix by upgrading the
parent, not by resolution.)

## Verification

- `yarn install` — vite resolves to `8.0.16`; all `@esbuild/*@0.27.7`
platform packages pruned; the only esbuild left is `0.28.1`
(already-fixed, from another consumer).
- `yarn typecheck` — passes.
- `yarn test:unit` — **202 tests / 30 files pass** under vitest 4.1.9,
no peer warnings; `vite-tsconfig-paths` still compatible with vite 8.
- `yarn install --immutable` — passes.
- (Integration `yarn test` is gated on a live Twenty server, so not run
here — that requirement is independent of this bump.)
- Separate yarn project — changes are confined to
`twenty-meeting-bot/{package.json,yarn.lock}`; no root impact.

## Note

vite 8 supports tsconfig-paths resolution natively
(`resolve.tsconfigPaths: true`), so `vite-tsconfig-paths` could be
dropped in a follow-up — left as-is to keep this change minimal.
2026-06-23 19:40:31 +05:00
nitin 9b2ca6f3f7 bump sdk and app version for call recording bot app (#22019)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22019?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 16:32:54 +02:00
nitin 99e7c2cde0 Improve meeting bot recording tab layout and transcript speakers ui (#22016) 2026-06-23 19:56:01 +05:30
Rashad Karanouh 4f1ffa0a96 fix(twenty-partners): coerce null fields in TFT opportunity import (#22017)
## What

The TFT `HTTP Request` action POSTs `null` for empty fields (e.g.
`amountMicros:null`, `closeDate:null`). The import schema typed those as
`z.number()/z.string().optional()`, which reject `null` (it is not
`undefined`), so the endpoint returned `ok:false / invalid_input` before
any API call.

## Fix

A `dropNulls` preprocessor on the request schema converts `null`
(top-level or nested) to "field absent" before validation. Null optional
fields are simply omitted from the created opportunity; required `name`
still fails correctly if null. No schema-shape or behaviour-contract
change.

## Tests

Added a case feeding the failing payload shape (`amountMicros:null`,
`closeDate:null`) → `created:true` with `amount`/`closeDate` omitted.
42/42 unit pass, lint clean.

Patch bump `1.1.1 → 1.1.2`.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22017?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 17:37:41 +04:00
Raphaël Bosi c52c983b90 Source app About description from README and improve internal app READMEs (#22012)
## What

- The SDK manifest build now sources an app's `aboutDescription` (the
long-form "About" tab content) from its `README.md`. An explicit
`aboutDescription` in the config still wins, matching the existing
marketplace CDN fallback.
- Removed the now-duplicated `aboutDescription` from internal app
configs and deleted the standalone `ABOUT_DESCRIPTION` constant files.
- Rewrote internal app READMEs to read as user-facing About content:
stripped developer/build/source-path noise, and expanded the thin ones.
`call-recording` and `self-hosting` (one-liners over substantial apps)
and `people-data-labs` were rewritten from a close reading of the code;
`twenty-exa` was verified for accuracy.
- Added a unit test (and a fixture README) covering README →
`aboutDescription` in the build.

## Why

The README and the About description were maintained separately and
drifted. Making the README the single source keeps the About tab
accurate and removes duplicated copy.

## Notes for reviewers

- Internal apps depend on the published `twenty-sdk`, so the build
change takes effect for them after an SDK release + dependency bump.
Until then, published apps still get README → `aboutDescription` via the
marketplace CDN sync.
- Standard/Custom app descriptions are unchanged (they are resolved in
the frontend, not via the manifest).

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22012?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 15:27:52 +02:00
Emmanuel Hernández Bazán e1c962acba feat(meeting-bot): configure Recall recording retention hours (#21978)
## Summary

- Sends an explicit Recall.ai recording retention policy when creating
or rescheduling meeting bots.
- Uses the optional server variable
`MEETING_BOT_RECORDING_RETENTION_HOURS` instead of a workspace/app
variable.
- Defaults to `166` hours (6 days and 22 hours), keeping Twenty-hosted
deployments below Recall.ai's 7-day free storage window while still
allowing self-hosters to configure a longer retention period.

## Why

Recall.ai accounts created after June 12, 2025 retain recording media
forever unless retention is configured. Twenty ingests the meeting
artifacts into its own storage, so Recall.ai media retention should be
bounded by default to avoid unnecessary third-party storage cost.

## Changes

- Replaces the days-based app variable with the server variable
`MEETING_BOT_RECORDING_RETENTION_HOURS`.
- Adds a default retention constant of `166` hours.
- Builds `recording_config.retention = { type: 'timed', hours }`
centrally through `getRecallBotRecordingConfig()`.
- Applies the same recording config to both bot creation and bot
rescheduling.
- Documents the server variable and warns that values above `168` hours
may incur Recall.ai storage charges.
- Updates Recall API tests to assert retention is sent and invalid
values fall back to the safe default.

## QA

- [x] `yarn test:unit`
- [x] `yarn lint`
- [x] `yarn exec tsc --noEmit -p tsconfig.spec.json`
- [x] `git diff --check`
- [x] Live Recall.ai bot payload includes `recording_config.retention =
{ type: 'timed', hours: 166 }`

---------

Co-authored-by: Emmanuel Hernandez <emmanuel.hernandez@clickbalance.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: ehconitin <nitinkoche03@gmail.com>
2026-06-23 15:07:08 +05:30
Rashad Karanouh 6f50b9d01e fix(twenty-partners): update Partner view in place on upgrade instead of deleting (#21995)
## Why

Upgrading the already-installed `twenty-partners` app in place (0.5.x →
1.x) via `yarn twenty app:install` aborts during the sync reconcile:

```
view:      INVALID_VIEW_DATA: Cannot delete the only view for this object (379b11d5-…)
viewField: INVALID_VIEW_DATA: Label identifier view field cannot be deleted (21afcc69-…)
```

The marketplace-v2 change deleted `all-partners.view.ts`. On an
installed workspace that view is the Partner object's primary view and
holds the **label-identifier** viewField (the `name` column). Twenty's
manifest sync refuses to delete an object's *only* view or a
label-identifier viewField, so the in-place upgrade fails. (Fresh
installs are unaffected; only upgrades from a version that had
`all-partners` hit this.)

## What

Repurpose the retired `all-partners` identity for `partners-validated`
so the sync performs an **update in place** instead of a delete:

- `partners-validated.view.ts` now uses the old view id `379b11d5-…`,
and its `name` column reuses the old label viewField id `21afcc69-…`.
- Remove the now-dangling `ALL_PARTNERS_VIEW_UNIVERSAL_IDENTIFIER`
constant (its file was already gone).
- Patch bump `1.1.0` → `1.1.1`.

The resulting view is the intended "Partners Validated"; the other
retired Partner view (`validated-partners`) deletes cleanly because the
object keeps other views.

## Revision

**Patch** — migration bugfix, no new behaviour.


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21995?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 11:33:11 +02:00
nitin c7ad1ff8ee Route Recall webhooks by workspace metadata (#21991)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21991?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-06-23 10:27:52 +02:00