4aaf171d63d4beff0a3505d9db4e6bac4f69ebe9
13326 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4aaf171d63 |
feat(ai): add ask_questions interactive clarifying-question tool (#22346)
## What & why Adds an `ask_questions` tool that lets the in-app **Ask AI** assistant **pause a turn to ask the user one or more multiple-choice questions** (per the [Figma design](https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=105959-117153)) and resume once answered — instead of guessing on ambiguous/consequential decisions. The tool is **harness-only**: an interactive question UI is meaningless without a user to answer it, so it must be absent from MCP and from head-less workflow agents. ## Design — true tool-result resume (not a synthetic user message) The user's answer is a **structured tool result bound to the `toolCallId`**, and the **same agent turn resumes** — exactly how Anthropic (`tool_result` by `tool_use_id`) and OpenAI (`function_call_output`) model human-in-the-loop. The naive form of this (leave the tool call in `input-available` to mean "pending") is **impossible** here: `finalizeDanglingToolParts` rewrites `input-available` → `output-error` ("Tool execution was interrupted") on both the persist path (`addMessage`) and the model-reload path (`chat-execution.service.ts`). That util is a load-bearing safety net, so weakening it is the wrong move. Instead: - `ask_questions` is an **inline, chat-only tool with an `execute` that returns a `status: 'pending'` result immediately**, so the tool part is always `output-available` and **immune to `finalizeDanglingToolParts`**. `stopWhen(hasToolCall('ask_questions'))` halts the turn right after the call (the model never sees the placeholder). - A nullable **`thread.pendingQuestionMessageId`** marker records that a turn is awaiting an answer. - The new **`answerAgentChatQuestion`** mutation atomically *claims* the question (clears the marker, marks the thread streaming), **writes the answer onto the same tool part** (`status: 'answered'`), and **re-enqueues the turn via the existing `existingTurnId` plumbing** (`isResume` bypasses the per-turn dedup guard). On resume `finalizeDanglingToolParts` leaves the `output-available` part untouched and `convertToModelMessages` emits `assistant(tool_use)` + `tool_result(answers)`, so the model continues. This achieves the platform-aligned semantics **without** weakening the finalize safety net or inventing a fragile new part state. ### Meets the two requirements - **Survives refresh, scoped per-thread** — the pending state is a normal persisted `output-available` part + the thread marker; the frontend card is derived per-thread from the loaded messages, so it re-appears on reload and only on its own thread. - **Takes priority over the queue** — a unified `isBlocked = activeStreamId || pendingQuestionMessageId` gate is applied in both `sendChatMessage` (new messages queue) and `flushNextQueuedMessage` (the drain). The queue cannot unpile until the question is answered and the resumed turn completes. ### Harness-only by construction `ask_questions` is added **only** to the chat's inline `activeTools` (like `learn_tools`/`execute_tool`/`load_skills`). It never enters the tool registry/catalog, so it is invisible to MCP and to workflow agents — no `MCP_EXCLUDED_TOOL_NAMES` entry needed. ## UX While a question is pending, the **composer is replaced by the question card** (matching the Figma): question title + pager (`1/2`), numbered option rows (`IconSquareNumber*`) with per-option info-icon descriptions and a "Recommended" badge, and the normal composer as the free-text fallback ("Type anything to do differently."). The transcript shows a compact "Asking questions…" status line that becomes an answered summary. ## Changes **twenty-shared** - `ai/types/AskQuestionsToolTypes.ts` — `AskQuestionItem/Option/Answer/Result`, `ASK_QUESTIONS_TOOL_NAME`. **twenty-server** - `ai-chat/tools/ask-questions.tool.ts` — inline tool factory (pending-result `execute`, zod schema, 1–4 questions × 2–4 options). - `chat-execution.service.ts` — add to `activeTools` + `preloadedToolNames`; `hasToolCall` in `stopWhen`. - `chat-system-prompts.const.ts` — when-to-use guidance. - `entities/agent-chat-thread.entity.ts` — `pendingQuestionMessageId` column. - `stream-agent-chat.job.ts` — set the marker on a question pause; bypass the dedup guard on resume; suppress the no-text warning for question pauses. - `agent-chat-streaming.service.ts` — gate `flushNextQueuedMessage`; `enqueueResumeStream`. - `agent-chat.resolver.ts` — gate `sendChatMessage`; `answerAgentChatQuestion` mutation. - `agent-chat.service.ts` — `resolvePendingQuestion` (atomic claim + write answer). - `dtos/agent-chat-question-answer.input.ts`, `ai.exception.ts` (`QUESTION_NOT_PENDING`), `utils/find-pending-question-part.util.ts`. **twenty-front** - `components/AiChatQuestionCard.tsx` — the interactive card (matches Figma tokens) + `__stories__/AiChatQuestionCard.stories.tsx`. - `components/AiChatEditorSection.tsx` — swap the composer for the card while pending. - `components/AiChatQuestionStatusRenderer.tsx` + branch in `AiChatAssistantMessageRenderer.tsx`. - `states/selectors/agentChatPendingQuestionComponentSelector.ts`, `types/AgentChatPendingQuestion.ts`. - `hooks/useSubmitQuestionAnswer.ts` + `utils/markQuestionAnswered.ts` (optimistic) + `graphql/mutations/answerAgentChatQuestion.ts`. A design doc lives at `packages/twenty-server/docs/ASK_USER_QUESTION_TOOL_PLAN.md`. ## Migration Adds a nullable `pendingQuestionMessageId` (uuid) column to `core.agentChatThread`. Needs a generated **fast instance command** (`database:migrate:generate --name addThreadPendingQuestion --type fast`) — see "Verification status". ## Tests - Server: `ask-questions.tool.spec.ts` (pending echo + schema bounds), `find-pending-question-part.util.spec.ts`. - Front: `markQuestionAnswered.test.ts`, plus the Storybook story. ## Verification status (please read) This branch was authored in an environment where the monorepo `yarn install` repeatedly failed on transient TLS resets from the package registry, so I could **not** locally run the mechanical gates. The logic was reviewed by hand and the `ai@6.0.97` exports used (`hasToolCall`, `stepCountIs`, `generateId`) were confirmed against the package's type defs. Still **TODO** (will rely on CI / a follow-up once deps install): - [ ] `nx run twenty-shared:generateBarrels` (the `ai/index.ts` export was added by hand; regen to reconcile) - [ ] `nx run twenty-front:graphql:generate` (new mutation + input type) - [ ] generate the fast instance command (migration) for the new column - [ ] `typecheck` + `lint:diff-with-main` (front + server) — expect minor import-ordering autofixes - [ ] run the unit tests **Screenshots:** reproducing the live flow needs an AI provider API key (to get the model to actually call `ask_questions`), which isn't available here. The card can be screenshotted from its **Storybook story** (`AiChatQuestionCard.stories.tsx`) with no API key — I'll add that image once deps install, or a reviewer can run `nx storybook twenty-front`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01AArS8H3y3Z1Qwm763xhPLB --- _Generated by [Claude Code](https://claude.ai/code/session_01AArS8H3y3Z1Qwm763xhPLB)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22346?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. --> |
||
|
|
198f1e4916 |
feat(emailing): forward SES events to Tatami Monitor (#22407)
Add AwsSesObservabilityService, which adds an SNS event destination to each workspace's SES configuration set (gated on TATAMI_SNS_TOPIC_ARN) so deliverability events reach Tatami. Tag sends with tenant_id for per-workspace breakdowns. Requires to be merged https://github.com/twentyhq/twenty-infra/pull/765 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22407?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. --> |
||
|
|
ff6a0c6e69 |
Fix front component crash on unknown elements (#22455)
## What Front components are third-party React components rendered on the host via remote-dom against an allow-list of elements. Today the host renderer throws on any element tag it has no component for (e.g. a raw tag produced by `innerHTML`), and there is no error boundary, so a single unknown element crashes the whole widget. This wraps the component registry with a fallback: - a raw tag that has an allow-listed `html-*` equivalent is routed to that safe wrapper (so a raw `iframe` renders through the existing sandbox-forcing renderer instead of being dropped), - tags with no safe renderer (`script`, `object`, `embed`, `link`, `meta`, `base`, `noscript`, `style`) render nothing, - any other unknown tag renders children only. `RemoteRootRenderer` is also wrapped in an error boundary that fails closed to the existing error panel, so a render error can no longer take down the host. ## Notes The host allow-list remains the single rendering gate. This is the first hardening step of a broader effort to widen the DOM/Web API surface available to front components; it is self-contained and does not change behavior for components that only use allow-listed elements. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22455?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. --> |
||
|
|
11b2990dd6 |
fix(twenty-sdk): stop dev-mode OOM by caching compiled manifest modules (#22435)
## Context Closes twentyhq/core-team-issues#2601 `twenty dev` crashed with a Node.js heap OOM (`FATAL ERROR: Reached heap limit — JavaScript heap out of memory`) after a while of editing. ## Root cause `loadModule()` in `manifest-extract-config-from-file.ts` compiled every manifest-defining file with **`vm.compileFunction`** on every manifest rebuild. V8 pins every function compiled through the `vm` module and never releases it ([nodejs/node#35375](https://github.com/nodejs/node/issues/35375)). In the dev loop this is on the hottest path and heavily amplified: - `runSyncPipeline` → `buildManifest` re-globs **all** `.ts/.tsx` files and recompiles every entity file on **every** sync — not just the edited one. - A single save triggers 2+ full rebuilds (the manifest watcher change → `scheduleSync`, then the esbuild watcher's `handleFileBuilt` → `scheduleSync` again). - Each compiled unit is the full esbuild bundle — hundreds of KB, up to MBs for front components (React/JSX inlined). So over an hour of editing, thousands of `vm.compileFunction` calls × large source, all permanently retained → multi-GB heap → crash. This matches the reported profile exactly. Investigation ruled out (with evidence): chokidar watchers (disposed on restart), ts-morph/`createProgram` (dead code, not in the dev loop — typecheck runs in child `tsc` processes), the event log (hard-capped at 200), Ink timers/subscriptions (all cleaned up), and graphql-sse (only used by `logs`). ## Change Keep only the **latest build per file**, keyed by file path. Each cache entry stores the file's last bundled-output hash and its compiled wrapper: - Rebuild with **unchanged** output → reuse the existing wrapper (no recompile). - Output **changed** → overwrite the entry, so the file's previous build is dropped instead of accumulating. This bounds the cache to one entry per file rather than one per rebuild, so old builds no longer pile up in the heap. The wrapper is still executed fresh into a new module shim on every call, so extraction behavior is unchanged. One file changed: `packages/twenty-sdk/src/cli/utilities/build/manifest/manifest-extract-config-from-file.ts`. ## Test - Behavior of `extractManifestFromFile` is unchanged (fresh execution per call); only redundant recompilation is eliminated and stale builds are dropped. - Note: `yarn install` could not complete in the authoring sandbox (a git-based transitive dep of `twenty-desktop` is blocked by the proxy), so lint/typecheck/tests were not run locally — relying on CI. ## Follow-ups (not in this PR) - Redundant **double-sync per save** (`start-watchers-orchestrator-step.ts`) triggers two full rebuilds per edit. - Latent **event-log display bug**: new events stop appearing once the 200-event cap is reached. |
||
|
|
d709467902 |
feat(ai): surface AI chat stream failures through one typed error channel (#22434)
## Context Investigating a report where the AI chat showed only a `...` spinner while the network response clearly contained `No AI models are available`. Root cause: terminal stream failures reach the client on **two mismatched channels**. | Representation | Persisted (survives reload) | Rendered by client | |---|---|---| | AI-SDK `error` chunk (inside `stream-chunk`) | ✅ RPUSH'd to Redis | ❌ dropped by `readUIMessageStream` (no message part, no error state) | | typed `stream-error` event | ❌ never persisted | ✅ sets the error atom | Live, the `stream-error` event renders. But on reload, `chatStreamCatchupChunks` replays only the persisted **error chunk** — which the reducer discards — and the streaming indicator never clears. ## Change Collapse to a single typed error contract: - **Suppress the opaque `error` chunk** in the stream job; every failure is surfaced through the typed `stream-error` event. Errors are mapped via `mapErrorToStreamError` so an `AiException` keeps its `AiExceptionCode` (e.g. `API_KEY_NOT_CONFIGURED` → the existing "AI not configured" banner) instead of leaking a raw string. - **Persist the terminal error** next to the accumulated chunks and expose it as an explicit `error { code message }` field on `ChatStreamCatchupChunks`, so a client catching up after a reload recovers it — no dependency on the AI SDK's internal chunk shape. - **Reset per-thread stream state at job start**, so a failed turn's leftover chunks/error never replay on the next stream. - **Client replays the catchup error** as a terminal `stream-error` event, which clears the streaming indicator and renders the error (fixes the infinite spinner on a stream that ended in error). ## Notes - `ChatStreamError` is a new metadata GraphQL type; generated types (twenty-front metadata + client-sdk) were hand-updated to keep the tree consistent and will be reconciled by CI's `graphql:generate` check if anything differs. - Server unit test added for the error mapping. No schema/DB migration. ## Test plan - [ ] With no AI provider configured, send a chat message → error renders immediately (not a spinner). - [ ] Reload the thread → the error still renders (recovered from catchup), indicator not spinning. - [ ] Configure a provider and send again → normal streaming; no stale error from the previous failed turn. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22434?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. --> |
||
|
|
47689e676b |
feat(server): improve traceability of flat-entity map mutation errors (#22396)
## Context
cc @rashad
Twenty applies metadata changes optimistically to in-memory *flat entity
maps* before persisting them. The utils that mutate these maps throw
`FlatEntityMapsException` on invariant violations, which surface in
Sentry (e.g. during `InstallApplication`) as a **hardcoded, generic
message with no identifying data**:
```
GraphQLError: addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow: flat entity to add already exists
```
There was no way to know *which* entity collided — making triage
impossible.
## What this does (two layers)
**Layer 1 — leaf utils emit identifiers**
- `FlatEntityMapsException` gains an optional structured `context`
(`universalIdentifier` / `id` / `applicationId` / `metadataName` /
`relatedMetadataName` / `operation`), read by the Sentry driver's
existing `'context' in exception` → `setExtra` channel.
- All **9 leaf throw sites** append their in-scope identifiers to the
message **and** populate `context`.
**Propagation — context survives the re-wraps**
- On the install path the collision throws in the (unwrapped)
`compute()` step, so the raw exception + context reaches app-sync
intact.
- For the run/build-phase paths, the migration runner and
build-orchestrator re-wraps copy only `.message`; they now also
**forward `context`** so structured data survives there too.
**Layer 2 — human installation error**
- `synchronizeFromManifest` catches flat-entity failures, resolves the
offending `universalIdentifier` to a manifest **object/field label**,
and rethrows `ApplicationException(APPLICATION_INSTALLATION_FAILED)`
with a safe, human `userFriendlyMessage`.
- The leaf `userFriendlyMessage` stays `STANDARD_ERROR_MESSAGE` — the
detailed message never leaks to end users.
- `APPLICATION_INSTALLATION_FAILED` surfaces with the dedicated
`ErrorCode.APPLICATION_INSTALLATION_FAILED` GraphQL code (mirroring the
workspace-migration runner formatter), not `INTERNAL_SERVER_ERROR`.
### Result — client-facing GraphQL error envelope
```json
{
"extensions": {
"code": "APPLICATION_INSTALLATION_FAILED",
"subCode": "APPLICATION_INSTALLATION_FAILED",
"userFriendlyMessage": "We couldn't install \"Test Application\". Its Invoice could not be applied to your workspace."
},
"message": "Installing application 'Test Application' failed [object: Invoice]: addUniversalFlatEntityToUniversalFlatEntityMapsThroughMutationOrThrow: flat entity to add already exists (universalIdentifier: ...)",
"name": "GraphQLError"
}
```
## Where the identifier shows up (not just Sentry)
The offending `universalIdentifier` reaches every consumer, not only
Sentry:
- **Sentry (server):** structured `context` extras + the enriched
message (fingerprinted by `code`, so no issue fragmentation).
- **GraphQL response `message`:** un-masked (no `useMaskedErrors`; the
error-handler hook passes `BaseGraphQLError` through as-is), so it
travels over the wire.
- **App-author SDK/CLI terminal:** `twenty-sdk` captures
`errors[0].message`; for this error `formatManifestValidationErrors`
returns `null` (no `extensions.errors`/`summary`), so the orchestrator
falls back to printing the full message, e.g.:
```
✗ Sync failed with error: Installing application 'X' failed [object:
Invoice]: … already exists (universalIdentifier: b1b2c3d4-…)
ℹ Hint: a metadata conflict was detected. Preview the plan with `yarn
twenty dev --once --dry-run`; …
```
The `already exists` / `universalidentifier` substrings also trigger
`getSyncErrorRecoveryHint`, so the author gets an actionable next step.
- **End-user (CRM UI):** only the safe rendered `userFriendlyMessage`
(no UUIDs).
## Design note
`userFriendlyMessage` behaviour of the leaf exceptions is intentionally
unchanged (guardrail). Layer 2 resolves labels for **objects and
fields** (the bulk of metadata); other manifest entity kinds fall back
to an app-name-only human message to avoid brittle manifest-walking —
easy to extend. A future first-class option would be structured
`extensions` (like `METADATA_VALIDATION_FAILED`) + a dedicated SDK
formatter; deferred since the message path already surfaces the detail
in the terminal.
## Tests
- **Unit:** existing through-mutation + runner-exception specs still
pass (they assert on exception **code**, not message). Added a spec for
the enrichment util.
- **Response-format snapshot (verified, green):**
`application-exception-filter.spec.ts` runs the exception filter and
snapshots the exact client-facing GraphQL error envelope shown above.
- **Integration:**
`failing-sync-application-flat-entity-map-conflict.integration-spec.ts`
syncs a manifest whose two objects share a `universalIdentifier`
(collision during manifest map build, before validation) and snapshots
the GraphQL error response via
`expectOneNotInternalServerErrorSnapshot`.
- ⚠️ The integration `.snap` was authored from the identical
deterministic path (verified by the filter unit snapshot) because the
integration suite couldn't be executed in the authoring sandbox. Please
regenerate/confirm with `nx test:integration:with-db-reset` (or `-u`) in
a seeded env.
## Status
Draft — opening for review.
|
||
|
|
632114e5e2 |
fix(front): hide sub-item tree connector in navigation drag preview (#22442)
## Context Dragging a navigation menu sub-item cloned the whole row as the floating drag preview, which included the vertical tree-connector bar on the left. Hide that connector inside the moving clone (marked by dnd-kit with [data-dnd-dragging]) so the preview shows only the icon and label. The static placeholder left in the list and the other items keep their connectors, so the list layout is unchanged. ## Before https://github.com/user-attachments/assets/8fc04a28-e1d2-49e0-88c1-ef03f89475c2 ## After https://github.com/user-attachments/assets/dbcd3cc5-8a51-40a3-98de-a8ea3d440774 |
||
|
|
63a0b0ab96 |
fix(front): render pinned command-menu buttons inline in page header (#22446)
## Context Pinned command-menu items (isPinned: true) stopped appearing as inline buttons next to the command-menu/burger control and only showed up in the side panel's "Pinned" list. Root cause: PR #21308 replaced the flex-based PageHeader with the grid-based PageCardHeader. The old header sized the title with `flex: 0 1 auto` (content width) and the action container with `flex: 1 1 0` (grows to fill), so the pinned-buttons wrapper — itself a `flex: 1 1 0` element that measures its own available width to decide how many buttons fit inline — had room to expand. PageCardHeader inverted this: it put the title in the flexible `minmax(0, 1fr)` track and the action area in the content-sized `auto` track. With the pinned wrapper empty on first paint, the `auto` track collapsed to zero, the measured container width was 0, and the "wait until measured" guard kept the visible inline count pinned at 0 forever — a deadlock where nothing ever rendered inline and every pinned item fell through to overflow. Fix: give the non-centered header the same intent as the old flex layout — title track content-sized/shrinkable (`minmax(0, auto)`), action track flexible (`minmax(0, 1fr)`). The centered variant already placed the action area in a `1fr` track, so it is unchanged. Both tracks keep a 0 minimum, so long titles still clip without causing horizontal overflow. ## Before <img width="1299" height="140" alt="Screenshot 2026-07-02 at 13 05 20" src="https://github.com/user-attachments/assets/948f5ded-1a9f-4329-825e-313924a829fe" /> ## After <img width="1296" height="238" alt="Screenshot 2026-07-02 at 13 05 11" src="https://github.com/user-attachments/assets/64edd5f2-b307-4119-9158-813e39f813aa" /> |
||
|
|
1cba0cdf49 |
fix(server): apply row-level security predicates to API key and application principals (#22456)
## What Row-level security predicates were only resolved for **user** principals. For API key and application principals, object-level and field-level permissions were resolved (via `resolveRolePermissionConfig`), but the row-level predicate role was left `undefined`, so: - on the read path, `buildRowLevelPermissionRecordFilter` returned `null` and no `WHERE` clause was added; and - on the write path, `validateRLSPredicatesForRecords` returned early and skipped post-write validation. The result was that a role carrying row-level predicates constrained users as intended, but the same role applied to an API key or installed application was subject only to its object/field permissions — not its row filters. ## Changes - Add `resolveRoleIdFromAuthContext`, a single helper that resolves the effective role id for user, API key, and application principals. - Use it in `applyRowLevelPermissionPredicates` (read) and `validateRLSPredicatesForRecords` (write) so row-level predicates are enforced for all principal types. - Thread `apiKeyRoleMap` through `WorkspaceInternalContext` (it was already available on the ORM workspace context). - Refactor `resolveRolePermissionConfig` to reuse the same helper, so object-, field-, and row-level checks all resolve the role identically. `workspaceMember`-relative predicate values are still only bound for user contexts (API keys/applications have no workspace member), matching existing behaviour. ## Notes - Enterprise-gated RLS code paths only. - Could not run `nx typecheck`/lint in this environment (dependencies not installed); changes reviewed manually. CI will validate. https://claude.ai/code/session_01N2RkG8aMwgfFU2jBghMgCQ --- _Generated by [Claude Code](https://claude.ai/code/session_01N2RkG8aMwgfFU2jBghMgCQ)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22456?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. --> |
||
|
|
049a98a36a |
fix(website): restore footer CTA buttons on the light card inside the dark stage (#22447)
## What The footer's **Talk to us** / **Get started** CTAs regressed: filled rendered white-on-white (only the label showed) and outlined vanished entirely. Resolves [this](https://github.com/twentyhq/core-team-issues/issues/2634) issue. ## Why #22241 marked the footer root as a dark menu-surface (`data-scheme="dark"`) so the sticky menu adapts over the dark footer stage. But the footer's content sits on a **white Card inside that root**, and the button's dark override is a *descendant* selector (`[data-scheme='dark'] &`) — so it leaked into the card. Filled → white fill + black label (invisible fill on white); outlined → white stroke + white label (fully invisible). The card's text was fine because it uses the light default semantic vars; only the buttons key off the raw attribute. ## Fix - Mark the white `Card` as `data-scheme="light"` — it *is* a light surface. The root keeps `data-menu-surface`/`data-scheme="dark"`, so **menu adaptation is unchanged**. - Add a button override scoped to `[data-scheme='dark'] [data-scheme='light'] &` — a light surface *nested inside* a dark one. It's higher specificity than the dark rule and matches **only** this footer case, so a dark card nested in a *light* section (e.g. `HelpedCard`) is never affected. No other button changes. Result: filled = black fill + white label, outlined = black stroke + black label — matching the design. ## Testing - `nx typecheck twenty-website` ✓ · `nx lint twenty-website` ✓ (check-conventions + oxlint + oxfmt) - Reviewable on the PR preview (footer CTAs, plus menu/FAQ/hero/signoff buttons unaffected). |
||
|
|
38fbff465f |
chore(server): ship the 2.20 standardOverrides drop as a dormant command (#22448)
Follow-up to #22417, per [this thread](https://github.com/twentyhq/twenty/pull/22417#discussion_r3512187719): migrate the `2-20/README.md` placeholder into a real command using the `TWENTY_NEXT_VERSIONS` mechanism. ### What - Add `DropMetadataStandardOverridesColumnFastInstanceCommand`, registered against `2.20.0`. It boots (`2.20.0` is in `TWENTY_ALL_VERSIONS`) but stays **dormant** — the upgrade sequence only runs `TWENTY_CROSS_UPGRADE_SUPPORTED_VERSIONS` (previous + current), so it never executes during the 2.19 deploy and activates automatically when `nx version:bump` promotes 2.20 to current. - Name constant + unit test (SQL parity, registration against `2.20.0`, name-constant parity). - Register it in `instance-commands.constant.ts`. - Update the `standardOverrides` `@deprecated` comments on object/field metadata to point at the shipped command. - Delete `2-20/README.md`. - Document the "ship a command for a future version" flow in `docs/UPGRADE_COMMANDS.md` and `.cursor/rules/server-migrations.mdc` (the mechanism was previously undocumented). ### Note / correction to the README's plan The old README implied both the command **and** `@WasRemovedInUpgrade` could be added at 2.20 time. Only the command can ship now: the decorator's validator runs against the active sequence, so referencing a still-dormant 2.20 step fails boot with `unknown-step-name`. So the entity keeps its `WasRemovedInUpgrade<T>` type wrapper for now; the decorator gets wired (one line, via the name constant) once 2.20 is current — same deferred-drop shape as `isUIReadOnly`. ### Verification Could not run `jest`/`typecheck`/`lint` in this environment: `yarn install` is blocked by egress policy on a git-based transitive dep (`github.com/electron/node-gyp.git`). Verified by review against the sibling 2-19 add-column and 2-12 drop commands. **Please let CI run before merge.** https://claude.ai/code/session_01KMArJvdEmsX3eAmJLbS1b6 --- _Generated by [Claude Code](https://claude.ai/code/session_01KMArJvdEmsX3eAmJLbS1b6)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22448?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. --> |
||
|
|
7a5896ff5d |
feat(ai) - add delete_workflow tool (#22432)
## Summary - Add a new `delete_workflow` agent tool that soft-deletes a workflow and cleans up its sub-entities (versions, runs, triggers) via `WorkflowCommonWorkspaceService.handleWorkflowSubEntities` - Update the workflow skill system prompt to document the new capability and instruct the agent to always confirm with the user before deleting - Wire `WorkflowCommonModule` / `WorkflowCommonWorkspaceService` into the workflow-tools dependency graph ## Test plan - [x] Unit tests added (`delete-workflow.tool.spec.ts`) covering successful deletion and error handling - [ ] Verify the agent can resolve a workflow by name via `list_workflows` then delete it with `delete_workflow` - [ ] Confirm the agent asks for user confirmation before executing the deletion - [ ] Confirm sub-entities (versions, runs, triggers) are removed after deletion <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22432?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. --> |
||
|
|
080014c970 |
fix(ai) - Fix date stripped from agent tool output (#22443)
https://discord.com/channels/1130383047699738754/1522138489238458569 **Summary** Fix a bug where Date objects (returned by TypeORM for createdAt, updatedAt, deletedAt columns) were silently dropped from AI agent tool responses stripEmptyValues treated Date instances as empty objects because Object.entries(new Date()) returns [], causing the function to discard them Add instanceof Date guard before the generic object branch so Date values pass through unchanged **Root cause** TypeORM marks createdAt/updatedAt/deletedAt as special columns (createDate/updateDate/deleteDate) and returns them as JavaScript Date objects rather than strings. The stripEmptyValues utility checked typeof value === 'object' (true for Date), then called Object.entries() on it -- which yields an empty array since Date has no own enumerable properties -- and concluded the value was "empty". The existing tests used string dates ('2024-01-01') instead of actual Date objects, so the bug was never caught. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22443?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. --> |
||
|
|
5a4ebca226 |
refactor(server): unify the two metadata override mechanisms into one (#22417)
## Unify the two metadata override mechanisms into one Twenty had **two** override mechanisms: - **`standardOverrides`** — a bespoke JSONB column on `objectMetadata`/`fieldMetadata` with typed DTOs and a per-locale `translations` map, resolved by two i18n-aware resolvers. - **`OverridableEntity.overrides`** — a flat, registry-driven JSONB blob on view / view-field / view-field-group / command-menu-item / page-layout-tab / page-layout-widget, resolved by a plain spread. This PR collapses them into **one** concept: a single `overrides` blob, one registry-driven overridable set, one i18n-aware read path, and one write path (`computeMetadataOverridesBlob`, extracted in #22404). Object/field **stay on `SyncableEntity`** (not reparented to `OverridableEntity`) so their `isActive` default stays **FALSE** — this sidesteps the `isActive` default conflict entirely. ### GraphQL breaking change (accepted) The `standardOverrides` field is **removed** with no deprecation alias — `overrides` (a `JSON` scalar) is exposed instead on `Object` and `Field`. Product confirmed negligible external usage; the front-end has no hand-written consumer (only generated types), which are regenerated here. ### Commit structure (reviewable commit-by-commit) 1. **Unified resolver + parity harness** — `resolveEffectiveEntityProperty` is a strict superset of the three legacy resolvers; a corpus parity spec compares it against a *frozen reference* of the old logic across every locale, `isStandardApp` branch and override shape. 2. **Registry-driven** — object/field presentation props tagged `isOverridable` + `translatable`; the overridable/translatable sets are derived from the registry (a test asserts they equal the legacy hardcoded lists). 3. **Rename + swap + delete** — `standardOverrides` → `overrides` across entities, DTOs, flat/universal types, producers, the ~12 resolve/write/create/sync call sites, mocks and specs; the reconciler's two compare entries collapse to one; the three legacy resolvers, both DTOs and the hardcoded constants/types are deleted. 4. **Migration (zero-downtime, two-phase)** — split across two releases so a rolling deploy never drops a column a previous-release pod still `SELECT`s: - **2.19 fast** — add the `overrides` column (schema only). - **2.19 slow** — backfill `overrides` from `standardOverrides` in `runDataMigration` (kept out of the schema transaction so the bulk write doesn't hold the ACCESS EXCLUSIVE lock; skipped on fresh installs, which have no data to copy). - **2.20 fast** — drop the legacy `standardOverrides` column (gated by `TWENTY_NEXT_VERSIONS`, so it stays dormant until the instance reaches 2.20). 5. **Front/client-SDK regen** — regenerated metadata GraphQL types. 6. **Integration specs + i18n** — updated the standard object/field update integration specs + snapshots, and the reworded validator message catalog entry. ### Rolling-deploy safety `standardOverrides` is retained through 2.19 and only dropped in 2.20, mirroring the codebase's deferred-drop convention (`isUIReadOnly`/`isCustom`). During the 2.19 rollout both columns exist, so old and new pods coexist without "column does not exist" errors. The backfill lives in a slow `runDataMigration` (per the `no-data-mutation-in-fast-instance-command` rule) so it doesn't stall reads. ### `isActive` guard The migration never reads or writes `isActive`; the backfill asserts the active-row count is unchanged and aborts otherwise. Verified on a real DB: apply + revert preserves the blob **and** the nested `translations` map, with `isActive` counts identical before/after. ### Verification (local) - `nx typecheck twenty-server` + `nx typecheck twenty-front` — green - `nx lint:diff-with-main twenty-server` (oxlint `--type-aware` + oxfmt) — green - `nx test twenty-server` — green (unit + parity + registry + migration tests) - `nx run twenty-server:test:integration:with-db-reset` — green - `database:reset` applies the 2.19 phases and leaves **both** columns present (2.20 drop stays dormant); backfill + revert round-trip verified on a real DB - Metadata integration suites (standard object/field update, application sync) pass end-to-end against the two-column schema - Metadata GraphQL types regenerated against a booted server; zero `standardOverrides` references remain in application code (only the migration commands + the legacy schema baseline) --------- Co-authored-by: prastoin <paul@twenty.com> |
||
|
|
63d092a31b |
fix(website): center the User Guide nav preview image (#22445)
## What The **User Guide** item in the Resources dropdown rendered its preview image off-center (anchored top-left with a gap below the halftone). ## Fix Set `imagePosition: 'center'` on the User Guide preview so the halftone book is centered and fills to the bottom of the frame, matching the others. ## Testing - `nx lint twenty-website` ✓ (check-conventions + oxlint + oxfmt) - `nx typecheck twenty-website` ✓ |
||
|
|
3bbc08d41f |
refactor(schema): reorganize IndexField and related types (#22439)
## Summary
Querying `indexMetadatas { indexFieldMetadatas { ... } }` on the
`/metadata` GraphQL endpoint fails with a 500:
> Nest could not find IndexFieldMetadataDTOAuthorizer element (this
provider does not
> exist in the current context)
The `@CursorConnection('indexFieldMetadatas', ...)` decorator on
`IndexMetadataDTO` makes nestjs-query auto-generate a relation resolver
that injects an authorizer for `IndexFieldMetadataDTO`. That authorizer
is never provided, because the DTO was never registered as a resolver in
`IndexMetadataModule` — so the field has been broken since it was
introduced in #7162.
Since the working, DataLoader-backed `indexFieldMetadataList` field
already exposes the same data (and is what the frontend uses), this PR
removes the dead connection instead of wiring up the authorizer.
## Changes
- Remove `@CursorConnection('indexFieldMetadatas', ...)` from
`IndexMetadataDTO`
- Regenerate frontend metadata GraphQL types
(`twenty-front/src/generated-metadata`)
- Regenerate client SDK metadata schema/types
(`twenty-client-sdk/src/metadata/generated`)
## Notes
- Not a breaking change in practice: the removed field always threw, so
no consumer can have been relying on it. Callers now get a standard
GraphQL validation error suggesting `indexFieldMetadataList` instead of
an internal server error.
- Verified locally: the failing query now returns `Cannot query field
"indexFieldMetadatas" on type "Index". Did you mean
"indexFieldMetadataList"?` and `indexFieldMetadataList` continues to
work.
Fixes [sonarly issue #54098](https://sonarly.com/issue/54098?type=bug)
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22439?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. -->
|
||
|
|
a99431902d |
fix: bump json-2-csv 5.5.10 -> 5.5.11 (Dependabot) (#22438)
## Summary Bumps **json-2-csv 5.5.10 → 5.5.11** to clear **1 medium Dependabot alert** ([alert 1575](https://github.com/twentyhq/twenty/security/dependabot/1575) — GHSA-g27c-q7cp-mhx6 / CVE-2026-9673, CSV Injection via the `preventCsvInjection` option, vulnerable `>= 3.15.0, < 5.5.11`). json-2-csv is a **direct dependency** of `twenty-front` (`"json-2-csv": "^5.4.0"`). The caret already permits 5.5.11, so this is a **lockfile-only** bump — no `package.json` change (matches Dependabot's `versioning-strategy: lockfile-only`). ## Verification - `yarn install --immutable` passes (CI parity). - Diff is `yarn.lock`-only; json-2-csv resolves to 5.5.11. - 5.5.11 is the latest and cleared twenty's 3-day npm age gate (published 2026-05-26). |
||
|
|
717b297bd1 |
Add Last contact app to onboarding v2 installable apps (#22433)
Adds the Last contact app to the list of installable apps shown in the onboarding v2 install-apps step, alongside Call recorder and Enrichment. Wired in both the frontend list (label + description) and the backend reward/install allow-list so it can be selected, installed server-side, and credited. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22433?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. --> |
||
|
|
8b6bd34a17 |
i18n - website translations (#22436)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22436?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: github-actions <github-actions@twenty.com> |
||
|
|
3474d75b56 |
feat(website): add Product to menu + footer nav, move Why into Resources dropdown (#22429)
## What Restores the **Product** link to the site nav (removed in #21794), reversing that commit's nav-structure change: - **Menu** — Product is the first top-level item (in place of Why); **Why** moves back into the **Resources** dropdown with its `why.webp` preview (`IconBulb`, "Why teams choose Twenty"). - **Footer** — Product added to the **Sitemap** group (after Home). The `/product` and `/why-twenty` routes already exist and are in the sitemap; only the nav data changed. The rest of #21794 (dropdown frame height, preview assets, current-page highlight) is untouched. ## Notes - New `msg` strings (`Product`, and Why's restored strings) are left to CI / the i18n bot to extract + translate — no catalog changes here. ## Testing - `nx lint twenty-website` ✓ (check-conventions + oxlint + oxfmt) - `nx typecheck twenty-website` ✓ |
||
|
|
1b06532cb1 |
fix(website): tighten product-feature spotlight height and align bento spacing (#22428)
## What Design polish on the product page's `ProductFeature` bento, from designer review: - **Spotlight height** — the first (spotlight) card's visual was `min-height: 420px` on desktop vs the grid cards' `340px` (80px taller), so it towered over the rest. Now **340px**, matching the grid cards. - **Spacing consistency** — the spotlight visual used a uniform `margin` (bottom margin included), unlike the other cards' `CardVisualFrame` (`… 0` bottom). Removed it so the visual→content gap is consistent across every card. - **Gap** — bumped the visual→content gap to `spacing(6)` (**24px**) on desktop for all cards. ## Testing - `nx lint twenty-website` ✓ (oxlint + oxfmt + check-conventions) - `nx typecheck twenty-website` ✓ |
||
|
|
8182b2a07d |
fix(billing) - invalidate activationStatus after billing event (#22414)
Issue with workspaces still blocked after being re-activated <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22414?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. --> |
||
|
|
3c13dc1ab8 |
feat(twenty-sdk): include app readme in published package (#22431)
## Context When running `yarn twenty app:publish`, no README was included in the npm-published app package. Closes twentyhq/core-team-issues#2632 ## What changed - Added `copy-readme-to-output.ts`, which finds the app's root readme file (matched case-insensitively, preferring the markdown variant, mirroring how npm ranks README candidates) and copies it into the build output directory (`.twenty/output/`). - Wired `copyReadmeToOutput` into `buildApplication` — the shared build path used by `publish`, `build`, and `dev` — so the readme is present when `npm publish`/`npm pack` runs from the output directory. npm only ships a README when the file lives in the package root, which for published apps is `.twenty/output/`. The readme is not tracked in the manifest checksums; it is a pure npm packaging artifact, so it is only copied into the output directory and does not affect app installation/validation. ## Tests - Added unit tests for `findReadmeFileName` (case-insensitivity, markdown preference, ignoring unrelated files) and `copyReadmeToOutput` (copies the readme into the output dir; no-ops when the app has no readme). https://claude.ai/code/session_01Qje6VemuMk8nunn6yVJNtL --- _Generated by [Claude Code](https://claude.ai/code/session_01Qje6VemuMk8nunn6yVJNtL)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22431?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. --> |
||
|
|
7b682bced9 |
feat(shared): require defaultValue on non-nullable field manifests (#22419)
## Context Follow-up to #22362, which made `isNullable` manifest changes actually apply (including a nullable → non-nullable backfill). This models the `isNullable` / `defaultValue` relationship directly in the `FieldManifest` type. ## Rule - A **non-nullable** field (`isNullable: false`) must declare a `defaultValue`, so the column always has a value to fall back on (e.g. for the backfill on the nullable → non-nullable transition). - A **nullable** or **unspecified** field may omit `defaultValue`. ## Changes - Split `RegularFieldManifest` into a base shape plus a discriminated nullability union. The union keeps `isNullable` free once a `defaultValue` is supplied, so helpers that always provide one can still pass a dynamic `boolean` `isNullable`. - `defaultValue` keeps its rich per-type `FieldMetadataDefaultValue<T>` (POSITION → number, ACTOR → composite) rather than a bare `string`. - `RelationFieldManifest` is rebased on the shared base and keeps `isNullable` / `defaultValue` optional, since relation join columns are always nullable by design. - Narrowed `buildEstimateFieldManifest` in the manifest-update integration test to satisfy the stricter type. ## Verification Environment couldn't install the monorepo deps (registry connections aborting), so `nx typecheck` wasn't run here. Validated the union structure with standalone `tsc` synthetic tests mirroring every construction pattern in the codebase: - ✅ nullable/no-default, no-`isNullable`, non-nullable with string/number/composite defaults, dynamic-boolean-with-default, and the `DistributiveOmit` path into `ObjectFieldManifest` - ✅ non-nullable **without** a default is correctly rejected with a clear "defaultValue is missing but required" error Recommend a full `nx typecheck twenty-shared twenty-sdk twenty-server` in CI to confirm against full project resolution. https://claude.ai/code/session_01VnbrgBB3kNGP876qaKPYDL --- _Generated by [Claude Code](https://claude.ai/code/session_01VnbrgBB3kNGP876qaKPYDL)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22419?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. --> |
||
|
|
7b5d313dd1 |
test(server): fix DPA Annex C test broken by sub-processor sync action (#22422)
## Fix flaky DPA Annex C test broken by the sub-processor sync action `resolveDpa`'s Annex C test hard-coded Amazon Web Services' processing locations: ``` Amazon Web Services (https://aws.amazon.com) — Processing location(s): United States, Germany, France. ``` But `subprocessors.json` is overwritten by the **trust-center sync GitHub action** (#22403). AWS is now listed with `processingLocations: ["DE"]`, so the DPA renders `Processing location(s): Germany.` and the hard-coded assertion fails on `main` (`twenty-server:test:ci`). This makes the test derive its expectations from `subprocessors.json` — asserting that every synced sub-processor renders an Annex C entry (`<name> (<vendorUrl>) — Processing location(s):`) and that Annex C is tied to §6.1 — instead of hard-coding vendor locations the sync action controls. The sibling `expands the sub-processor sentinel into exactly the synced entries` test already follows this data-derived pattern. No production code changes — test only. ### Verification - `resolve-dpa.util.spec.ts` — 18/18 pass (was 1 failing on `main`) - oxlint + oxfmt clean <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22422?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. --> |
||
|
|
128abcc433 |
i18n - docs translations (#22420)
Created by Github action Co-authored-by: github-actions <github-actions@twenty.com> |
||
|
|
795785653d |
chore: sync DPA sub-processors from trust center (#22403)
Automated weekly sync of `subprocessors.json` from Twenty's Trust Center (OneLeet). This keeps the DPA's Annex C (the SCC Annex III list of Sub-Processors) in lockstep with the canonical list at https://trust.twenty.com — the Trust Center is the single source of truth; this file is generated from it. **Please review before merging** — confirm the added/removed Sub-Processors are expected, and that customers were notified per Section 6.2 where required. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22403?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: FelixMalfait <6399865+FelixMalfait@users.noreply.github.com> |
||
|
|
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. --> |
||
|
|
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. --> |
||
|
|
15c0c3b773 |
i18n - docs translations (#22413)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22413?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: github-actions <github-actions@twenty.com> |
||
|
|
d8cc81cb91 |
Improve billing settings UI (#22377)
## Summary - Refresh the settings billing subscription and credits cards with clearer status, usage, and action states. - Add the credit-package picker flow and route past-due/cancellation actions to billing management instead of credit modals. - Clean up related billing UI helpers and formatting. ## Screens ### regular <img width="1007" height="781" alt="image" src="https://github.com/user-attachments/assets/dc9c0c59-8cda-422a-a0d3-56292421a744" /> ### downgrading <img width="1049" height="818" alt="image" src="https://github.com/user-attachments/assets/0dc3dea8-5b55-4cf2-bc92-997cf6e9bebc" /> <img width="1048" height="901" alt="image" src="https://github.com/user-attachments/assets/fba805ad-f523-444b-93a0-2011b6b43443" /> ### Trialing without card <img width="1008" height="903" alt="image" src="https://github.com/user-attachments/assets/d5dd11b6-4c92-4102-ac22-ad1ad4e9fbfb" /> with card <img width="1008" height="806" alt="image" src="https://github.com/user-attachments/assets/0dbeb00e-890e-4448-94fe-0cc0ef411e8d" /> ### Past due & Unpaid <img width="1052" height="860" alt="Past due" src="https://github.com/user-attachments/assets/25ba53ef-6e74-4b4c-bfe1-d6c74e165917" /> <img width="1138" height="860" alt="Unpaid" src="https://github.com/user-attachments/assets/bb16fe65-84e4-40a7-8951-90b01030aded" /> --------- Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com> |
||
|
|
55ed4b7adb |
feat(sdk): translate front-component strings with t()/Trans/useTranslate (#22301)
## What
Lets app **front components** localize the strings they render,
extending the
existing application-translation pipeline (which today only covers
manifest
labels) to component source. App authors mark strings with a small,
familiar
API; the build extracts and bakes them; the runtime resolves them for
the
user's locale.
```tsx
import { Trans, t, msg, useTranslate } from 'twenty-sdk/front-component';
<Trans>Loading postcard…</Trans>
<Trans context="card-title">Untitled</Trans> // disambiguation
const empty = t('No content yet…'); // works outside JSX
<p>{t('Saved {count} cards', { count })}</p> // interpolation
const STATUSES = [{ id: 'draft', label: msg('Draft') }]; // lazy descriptor
```
## How
- **Runtime** (`twenty-sdk/front-component`): `t()` (eager, usable
anywhere —
event handlers, helpers, module scope), `msg()` (lazy descriptor),
`<Trans>`
(reactive JSX), `useTranslate()` / `useLocale()`. Source-string
fallback,
`{name}` interpolation, and `context` disambiguation. No build-time
macro —
these are plain runtime functions.
- **Extraction**: a `ts-morph` scan collects `t()`/`msg()`/`<Trans>`
strings
from component source into the same `locales/*.json` catalogs the
manifest
pipeline already writes (`twenty dev:translations-extract`).
- **Delivery**: `twenty dev:build` bakes the compiled per-locale catalog
into
each front-component bundle via an esbuild banner, so the runtime
resolves
with **no server or renderer changes**. Locale comes from the execution
context that already flows to the worker.
The catalog key and `generateMessageId` hashing are shared between the
node
extractor and the browser runtime; `<Trans>` text whitespace is
normalized
identically on both sides so multi-line elements resolve.
## Design notes
- Reuses the existing `extract → compile → manifest.translations`
contract and
`generateMessageId`, so component strings flow through the same
machinery as
manifest labels.
- Self-contained in `twenty-sdk` + a shared pure helper; the server is
untouched.
## Scope / follow-ups
- `twenty dev` (watch) does not bake catalogs yet — preview shows source
strings; use `twenty dev:build` (documented). Wiring the watcher is a
follow-up.
- Usage is documented in twenty-docs under **Apps → Translations**
(`developers/extend/apps/translations`).
## Tests
Unit tests for the catalog-key/interpolation helpers, the runtime
resolver
(hit/miss/context/fallback/interpolation), and the ts-morph extractor
(static `t`/`msg`/`<Trans>`, dynamic-skip, dedup, multi-line
whitespace), plus a
compile test for context→messageId. Verified with an adversarial review
pass.
https://claude.ai/code/session_01NiE7o3cd3zCLZarVkJa6UA
---
_Generated by [Claude
Code](https://claude.ai/code/session_01NiE7o3cd3zCLZarVkJa6UA)_
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22301?utm_source=github"
rel="nofollow noreferrer noopener" target="_blank">``<img alt="Review
in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg">``</a>
---------
Co-authored-by: github-actions <github-actions@twenty.com>
|
||
|
|
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. --> |
||
|
|
ad31227f7c |
Fix BlockNote placeholder alignment + paddings (#22409)
## Before <img width="660" height="958" alt="image" src="https://github.com/user-attachments/assets/017e73c3-cd55-47e3-9470-3f6c31152663" /> ## After <img width="1347" height="995" alt="file-c55b9d7bc3bd212f18f8c77a0318eaef" src="https://github.com/user-attachments/assets/3f96af62-6509-4b58-a740-cd87856422cd" /> |
||
|
|
2e7441380f |
refactor(server): unify metadata override-blob computation (step 1 of override unification) (#22404)
## Context
Twenty currently has **two override mechanisms** for metadata:
- `standardOverrides` (a bespoke JSONB column on `objectMetadata` /
`fieldMetadata`) — i18n-aware, typed DTO with a per-locale
`translations` map, resolved via
`resolve-object/field-metadata-standard-override.util.ts`.
- `OverridableEntity.overrides` (base class: `view`, `view-field`,
`view-field-group`, `command-menu-item`, `page-layout-tab`,
`page-layout-widget`) — a flat, i18n-free `{...entity, ...overrides}`
spread, registry-driven via `isOverridable`.
"One concept, two code paths → drift & confusion; reconciliation has to
special-case." This PR is **step 1** of collapsing them.
## What this PR does (small, behavior-preserving)
The add / remove / null-collapse **override-blob write logic was
triplicated** across:
- `sanitizeOverridableEntityInput` (`overrides`)
- `sanitizeRawUpdateObjectInput` (object `standardOverrides`)
- `sanitizeRawUpdateFieldInput` (field `standardOverrides`)
This extracts it into a single `computeMetadataOverridesBlob` helper
that all three now call. This is genuine **cross-mechanism convergence
of the write path** — the first concrete reduction of the "two code
paths".
- Behavior-preserving: same diff semantics. The object/field paths used
strict `===` on their string standard-override props; `isEqual` subsumes
that for strings, and the overridable path already used `isEqual`.
- Type-casts are contained **inside** the one helper; the three call
sites stay clean and type-preserving.
- 4 files: 1 new util + 3 refactors.
## ⚠️ Draft — verification status
I could **not** run `typecheck` / `lint` / tests locally: the sandbox
this was authored in cannot complete `yarn install` (network aborts
mid-install, no `node_modules`). The change is small and reasoned, but
**please let CI validate it** — that's why this is a draft. If CI flags
a type/lint nit in the contained casts, it's isolated to
`compute-metadata-overrides-blob.util.ts`.
Per request: no code comments were added; the design/tradeoff discussion
lives here.
## The full unification plan (this PR is step 1)
The remaining steps are deliberately **not** in this PR because they
need a live DB (migration) and the front-end codegen pipeline to verify
— neither is available in the authoring sandbox. Documented here for
review before we proceed:
| Step | Change | Why staged |
|------|--------|-----------|
| **(this PR)** | Unify the write-path blob logic | Safe,
behavior-preserving, no DB/FE |
| Read path | One i18n-aware `resolveEffectiveEntity` (superset of the
flat spread + the two i18n resolvers) | The i18n resolvers are entangled
with typed translation-key narrowing; merging cleanly needs the
storage/i18n generalization below |
| Registry | Make object/field presentation props registry-driven
(`facet` + `translatable`), like the overridable set already is |
Depends on the facet annotation |
| Storage | Object/field extend `OverridableEntity`; `standardOverrides`
→ `overrides` (translations preserved); **one data migration** | Needs
DB verification; changes schema |
| GraphQL + FE | Remove the `standardOverrides` field, expose
`overrides`; regen `twenty-front` / client-SDK types; update the
Settings → Data-Model rename UI | Needs codegen; see tradeoff below |
## Key tradeoffs / decisions to confirm
1. **GraphQL break on `standardOverrides` — accepted.** Per product
call, external usage is negligible, so the later step will **remove**
the field outright (no deprecated alias). The one real consumer is the
Settings → Data-Model rename-label UI, updated in the same step. This
drops the most complex part of the original plan (a virtual-alias
resolver + deprecation window).
2. **`isActive` default.** `OverridableEntity` defaults `isActive` to
`true`; object/field default it to `false`. The storage step must
**explicitly override the default** and assert in the migration that no
existing row's `isActive` changes.
3. **Overrides stay anonymous single-slot blobs** (no per-app
attribution / multi-contributor 3-way merge). That limitation is
unchanged here and is only worth revisiting if a concrete use case needs
owner-tagged layering (real schema work, sized separately).
4. **Parity harness is the safety net for the storage step.** Because
the read-path/storage merge touches the hot object/field resolve path
and i18n precedence, that PR should land a golden-corpus parity gate
(all locales, `isStandardApp`, empty/partial/full overrides) proving the
unified resolver reproduces today's output byte-for-byte, before any
switch.
## Not included (per request)
- No service tests added.
- No code comments added (rationale/tradeoffs are here, in the PR).
## Test plan
- CI: `typecheck` + `lint` + the existing
`sanitize-overridable-entity-input.util.spec.ts` (which exercises the
shared logic through `sanitizeOverridableEntityInput`).
- The object/field write paths have no dedicated unit spec; they're
covered by the metadata integration suites
(`successful-update-one-standard-object/field-metadata`).
https://claude.ai/code/session_01E1pGBDLC3gEBs1w45G2W5Z
---
_Generated by [Claude
Code](https://claude.ai/code/session_01E1pGBDLC3gEBs1w45G2W5Z)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22404?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. -->
|
||
|
|
27dea0ed0b |
Add installed workspaces view to application registration (#22359)
## After <img width="895" height="344" alt="image" src="https://github.com/user-attachments/assets/33591753-f248-45ce-b32d-cc1112f50579" /> <img width="889" height="425" alt="image" src="https://github.com/user-attachments/assets/469ee228-9abb-486f-b2ec-9efb490bb2c8" /> <img width="766" height="343" alt="image" src="https://github.com/user-attachments/assets/2d88444a-6d98-4f97-8e5d-109197cfad27" /> ## Summary Add a new "Installed workspaces" section to the application registration settings page that displays all workspaces that have installed a given application, with pagination support. ## Key Changes - **Backend Service**: Added `getInstalledWorkspaces()` method to `ApplicationRegistrationService` that queries installed applications across workspaces with pagination support - **Backend DTO**: Created `ApplicationRegistrationInstalledWorkspacesDTO` and `InstalledWorkspaceDTO` to structure the response with workspace details (id, displayName, logo, version), total count, and hasMore flag - **GraphQL Resolver**: Added `findApplicationRegistrationInstalledWorkspaces` query resolver with pagination (page parameter, default page size of 10) and proper authorization guards - **Frontend Component**: Created `SettingsApplicationRegistrationInstalledWorkspaces` component that: - Displays installed workspaces in a table with workspace logo, name, and version - Shows initial 3 workspaces with "Show all" button to expand - Implements pagination with "Show more" button to load additional pages - Handles empty state (returns null if no workspaces installed) - **GraphQL Query**: Added `FindApplicationRegistrationInstalledWorkspaces` query document for frontend data fetching - **Integration**: Integrated the new component into `SettingsApplicationRegistrationGeneralTab` ## Implementation Details - Pagination uses offset-based approach with configurable page size (10 workspaces per page) - Query results are ordered by workspace displayName and id for consistent ordering - Soft-deleted applications and workspaces are excluded from the list and counts - Apollo Client's `fetchMore` with `updateQuery` merges paginated results into the cache - Component respects existing authorization (API_KEYS_AND_WEBHOOKS permission required) - Uses existing UI components (Table, Card, Avatar, Button) from twenty-ui library - Supports internationalization with Lingui ## Screenshots The new "Installed workspaces" section on the app registration General tab (admin app detail page), captured against a local instance with a demo app installed in 14 workspaces. The three PNGs are committed under `.github/assets/screenshots/installed-workspaces/` and render inline in the **Files changed** tab of this PR: - `1-first-3-show-all.png` — Collapsed: the first 3 installed workspaces (avatar + name + installed version) with a "Show all" button. - `2-expanded-show-more.png` — "Show all": the first page of 10 workspaces, with a "Show more" button (more remain). - `3-all-paginated.png` — "Show more": all 14 workspaces loaded, button gone. Review in cubic: https://cubic.dev/pr/twentyhq/twenty/pull/22359?utm_source=github https://claude.ai/code/session_012nWtviSBdfFeHEASTtwvJ7 |
||
|
|
b8a3399230 |
fix(billing): link billing emails to the workspace subdomain (#22401)
## Problem Billing and workspace-suspension emails hardcoded a `BILLING_SETTINGS_URL` constant pointing at `https://app.twenty.com/settings/billing`. A user in `myworkspace.twenty.com` therefore received a CTA that bounced through the central `app` domain instead of landing on their own workspace. Those cross-subdomain redirects are unreliable, so it's better to link straight to the workspace. The invite, password-reset and email-verification emails already do this correctly by building a workspace-specific URL server-side with `WorkspaceDomainsService.buildWorkspaceURL(...)`; the billing/suspension senders had the `workspace` entity in scope but never used it. ## Fix Build the billing settings URL server-side and pass it into the templates as a `link` prop, mirroring the existing pattern: - **Templates** now take a `link` prop instead of the hardcoded constant: `billing-trial-ending`, `billing-trial-converting`, `billing-subscription-renewing`, `warn-suspended-workspace`. - **`BillingReminderService`** and **`CleanerWorkspaceService`** build `buildWorkspaceURL({ workspace, pathname: getSettingsPath(SettingsPath.Billing) })` and thread it through. - Wired `WorkspaceDomainsModule` into both NestJS modules; deleted the now-unused `billing-settings-url.constant.ts`; updated the reminder unit test. This also fixes **self-hosted** deployments, which previously got the same wrong hardcoded `app.twenty.com` link. ### Intentionally unchanged - `clean-suspended-workspace` keeps its central-domain "start a new workspace" CTA — that workspace is already deleted, so its subdomain no longer resolves. - `password-update-notify` (not a billing email) still uses `getBaseUrl()`; the workspace entity isn't readily loaded there. Can be a follow-up. ## Testing Extended `billing-reminder.service.spec.ts` to assert the workspace-specific `link` is threaded into the email. Note: local `typecheck`/tests could not be run because the sandbox proxy repeatedly dropped `yarn install` mid-fetch; the diff was reviewed line-by-line and import paths verified against the actual `twenty-shared` exports and module wiring. CI will provide the authoritative check. https://claude.ai/code/session_01QsgNd4SWdcRkPFyrnCgj2b --- _Generated by [Claude Code](https://claude.ai/code/session_01QsgNd4SWdcRkPFyrnCgj2b)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22401?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. --> |
||
|
|
72bcc78e36 |
Let the sign-in screen scroll when its content overflows the viewport (#22397)
On the sign-in screen, when a step's content is taller than the viewport (e.g. many workspaces to choose from), it grew past the fixed-height background and overflowed the page. Make the shared background scroll instead, so every sign-in step scrolls when its content overflows and stays centered when it fits. ## Before https://github.com/user-attachments/assets/0226daee-0cd9-454c-9f4b-257cfab61bfb ## After https://github.com/user-attachments/assets/7a535099-f0f0-431b-86ad-9fa8121638db <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22397?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. --> |
||
|
|
1b9ba48e34 |
Update onboarding v2 credit reward amounts (#22399)
Adjusts the free-credit rewards shown and granted across onboarding v2: - Import contacts: 2 → 1 credit - Install app: 1 → 0.5 credit per app - Invite user: 0.5 credit per user (unchanged) - Upgrade free trial: 5 → 1 credit All values live as defaults in `config-variables.ts` (micro-credits) and reach the frontend via ClientConfig, so nothing else needed changing. Note: the upgrade reward maps to `BILLING_FREE_WORKFLOW_CREDITS_FOR_TRIAL_PERIOD_WITH_CREDIT_CARD`, which is also the actual with-credit-card trial grant, so that grant drops from 5 → 1 credit too (intentionally the same number the onboarding advertises). <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22399?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. --> |
||
|
|
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. --> |
||
|
|
f2e7009baa |
i18n - docs translations (#22400)
Created by Github action <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22400?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: github-actions <github-actions@twenty.com> |
||
|
|
4d96ec489b |
Add smooth page transitions to onboarding v2 (#22392)
## Before https://github.com/user-attachments/assets/d2fcd5ce-7e34-4f07-9a52-cac8acdc37cd ## After https://github.com/user-attachments/assets/5c245949-cff9-41b2-802d-3deeb562efa6 On a full-page load of a v2 onboarding URL (the post-signup workspace-subdomain redirect), Lingui's `I18nProvider` renders `null` until the locale chunk async-activates, so the app is blank for ~2s before the verify step appears. Steps also hard-cut and flashed a loader between each other. - Show a pulsing-logo loader until the locale activates (a gate above `I18nProvider`), scoped to onboarding v2 paths so every other page is unchanged. - Cross-fade between steps and preload their chunks on entry, so navigating never flashes the loader. Frontend-only; i18n loading itself is untouched. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22392?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. --> |
||
|
|
2b1417770e |
SearchVector derivation via migration-scoped index (alt to #2622 __warmedUpCache) (#22389)
## What this is close https://github.com/twentyhq/core-team-issues/issues/2622 A **POC / discussion branch** implementing the runner-scoped alternative to the `__warmedUpCache` design in [core-team-issues#2622](https://github.com/twentyhq/core-team-issues/issues/2622). Not for merge as-is — meant to diff against that plan. ## Problem `deriveSearchVectorAsExpressionForTsVectorField` scans the entire `flatSearchFieldMetadataMaps` (`Object.values(...).filter(...)`) once per object created in a migration. On install that's `O(objectsCreated × totalSearchFields)` — the quadratic #2622 targets. Only **one** of the three call sites is actually hot: - `create-object` (runner) — global maps, called per created object → the quadratic - export DDL — maps already built **per object** (O(k)) - `update-field` rebuild — one field, gated on `rebuildSearchVector` ## Approach Instead of a private `__warmedUpCache` side-channel on `FlatEntityMaps<T>` + drain-on-hydration, this keeps the index in the **consumer**: 1. `derive` now takes `targetSearchFieldMetadatas` (already scoped to the tsVector field) instead of scanning the map itself. 2. The runner builds a `Map<tsVectorFieldMetadataId, searchFields[]>` **once per migration**, lazily, and threads it through the action context. Safe because `searchFieldMetadata` creates are ordered before `objectMetadata` creates (`computeOrderedMigrationActions`), so the map is complete on first use. → `O(totalSearchFields)`. 3. `getTargetSearchFieldMetadatasForTsVectorField` (O(total) filter) stays as the fallback for the one-off callers (export, field-update) and when the accessor isn't provided. ## Why this over `__warmedUpCache` - **No `FlatEntityMaps<T>` type widening**, no convention-only privacy, no id/universalIdentifier drain to keep in sync. - **No referential-integrity obligation.** The index only ever contains entities present in the map, so the "search field created-then-deleted before its object hydrates" case (deferred as an edge in #2622) can't put a stale id into an aggregator and crash `derive` via the `-orThrow` lookup. - **One `derive` path**, not "aggregator + direct-filter fallback for export". - Blast radius: ~220 lines, mostly a new util + test. ## Benchmark (micro, isolated function) Median of 7 trials, 10 search fields per object, running the real shipped utils — old = `getTargetSearchFieldMetadatasForTsVectorField` once per object (identical to the old inline scan), new = `buildSearchFieldMetadatasByTsVectorFieldId` once + N lookups (both assert they resolve the same fields): | objects | total search fields | old (scan/obj) | new (index once) | speedup | |--------:|--------------------:|---------------:|-----------------:|--------:| | 50 | 500 | 2.08 ms | 0.06 ms | 33× | | 100 | 1,000 | 9.26 ms | 0.12 ms | 77× | | 200 | 2,000 | 36.2 ms | 0.23 ms | 160× | | 400 | 4,000 | 151 ms | 0.40 ms | 379× | | 800 | 8,000 | 701 ms | 0.92 ms | 766× | Confirms the old path is quadratic (~4× per doubling of object count) and the new path is linear (sub-ms throughout). **Caveats — read these before trusting the speedup:** - This is the **isolated derivation function**, no DB / DDL / inserts. In a real `create-object` action the derive is a small fraction of per-action cost, so the end-to-end win is far smaller than the ratios above. - A default workspace has ~20–30 objects, where the **old** code already costs only ~1–2 ms total across the whole install. The quadratic only becomes material (>50 ms, the runner's slow-action threshold) around **200–400 objects**. - The measurement that should actually gate this — `[install-perf] create:objectMetadata` on a real install against a real DB with a few hundred objects — has **not** been run yet. The micro-benchmark bounds the upside and locates the knee of the curve; it does not prove end-to-end payoff. ## Not done on purpose - **No end-to-end benchmark yet** — step 0 should still be measuring `[install-perf] create:objectMetadata` on a real large install to confirm the quadratic is worth removing at all. - Relies on the ordering invariant (commented at the build site). The fully self-contained variant is to put the object's search fields on `FlatCreateObjectAction` (builder change) — deliberately left out to keep this runner-scoped. ## Checks `nx typecheck twenty-server`, `nx lint:diff-with-main twenty-server`, new util spec + existing `generate-workspace-schema-ddl` spec all green. |
||
|
|
1a475d0edd |
feat(twenty-sdk): terraform-style plan/apply for app metadata sync (#22372)
## What & why Syncing a Twenty app's metadata is destructive (removing a field/object drops the backing column/table), but the only preview was `dev --once --dry-run`, which collapsed every change into one line per entity — no before/after, no color, no destructive warning, and no confirmation before a real sync. This introduces a `terraform plan`-style flow. The server's `syncApplication(manifest, dryRun)` already returns a complete `SyncAction[]` (create/update/delete with per-attribute `before`/`after`), so this is a CLI-only change — **no server changes**. ## Command surface `plan` previews, `apply` applies; `dev` is the watch wrapper over the same engine. | Command | Behavior | | --- | --- | | `twenty plan [appPath]` | Render the full plan, read-only | | `twenty apply [appPath]` | Plan → confirm on destructive → apply | | `twenty dev --once` | **Deprecated** alias of `twenty apply` (still works, warns) | | `twenty dev --once --dry-run` | **Deprecated** alias of `twenty plan` (still works, warns) | | `twenty dev` (watch) | Compact summary; inline `[y/N]` confirm on destructive saves | | `-f, --force` | Skip the destructive gate (on `apply` and `dev`) | ## Plan output ``` Twenty will perform the following actions: # objectMetadata "rocket" will be created + nameSingular = "rocket" + labelSingular = "Rocket" # fieldMetadata "name" will be updated in-place ~ label = "Name" -> "Launch name" ~ isNullable = true -> false # fieldMetadata "legacyCode" will be destroyed - name = "legacyCode" Plan: 1 to add, 1 to change, 1 to destroy. Warning: 1 destructive change(s) will permanently delete data. - fieldMetadata "legacyCode" — drops the column and its data Destroys are irreversible. Review carefully before applying. ``` Grouped by metadata type, ordered create → update → destroy, `=` aligned per block. Internal keys (`id`, `workspaceId`, `*Id`, timestamps, nulls) are filtered; updates show only changed keys via the server `diff`. ## Destructive safety gate The server applies the manifest diff atomically, so every apply path computes the plan read-only first, then decides whether to apply: - **`twenty apply` / `dev --once`** — interactive `y/N` prompt when the plan deletes metadata; `--force` skips; **fails closed** (exit 1) in CI / non-TTY. - **`dev` (watch)** — creates/updates auto-apply with the compact summary; a save that deletes metadata shows an inline `y/N` prompt in the Ink UI. **Declining cleanly stops the watch** (exit 1) rather than leaving the session in a nagging/blocked state — since the atomic apply would otherwise also block the additive changes on every subsequent save until resolved. `dev --force` applies deletions without asking. ## Notes - `twenty apply` / `dev --once` now do one extra **read-only** dry-run before applying (to compute the plan + gate). `--force` skips it. - The watch sync step now skips API-client regeneration on any non-synced outcome (error or decline), avoiding a partial client write during shutdown. - The Ink watch UI keeps its existing compact summary; the full plan renders only on the plain-console surfaces — `dev` watch output is unchanged in the common case. ## Test plan - `npx nx typecheck twenty-sdk` ✓ - `npx nx lint twenty-sdk` ✓ - Unit tests (vitest): renderer (`format-sync-actions-plan.spec.ts`) + confirm gate (`confirm-destructive-apply.spec.ts`); existing summary / sync-step specs still green. - Manual against `simple-app` + a local server: `plan`, `apply` (destructive prompt + `--force` + non-TTY fail-closed), and the `dev` watch inline confirm (incl. decline → stop). |
||
|
|
2e6077383b |
Add install your first apps onboarding V2 step (#22347)
https://github.com/user-attachments/assets/5326d48f-1842-4db1-bc7c-94852145c035 <img width="838" height="754" alt="CleanShot 2026-06-30 at 16 25 05@2x" src="https://github.com/user-attachments/assets/5c7d53d7-4d65-4e35-aed1-edf0c104e140" /> Adds an "Install your first apps" step to the V2 onboarding, shown right after import-contacts. It lets users opt into installing marketplace apps (Call recorder and People Data Labs for now) during onboarding. - New backend `OnboardingStatus.APPS_INSTALLATION` (between SYNC_EMAIL and PROFILE_CREATION); V1 auto-skips it. - The primary button sends the selected app ids to the server via `triggerInstallAppsOnboardingStep`, which enqueues a dedicated job that installs them asynchronously so onboarding isn't blocked. Skip continues without installing. - The workspace is credited per app on successful installation. Credits are env-driven via `ONBOARDING_INSTALL_APPS_CREDITS_REWARD_PER_APP`, shown as "Earn +N free credits (1 per tool)". <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22347?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. --> |
||
|
|
93b8f66794 |
fix(dpa): correct US processor entity to Twenty.com PBC (#22393)
As title. |
||
|
|
fab0358df5 |
Handle field isNullable update (#22362)
## Context Setting isNullable on a field via the app SDK manifest was silently ignored when re-syncing an existing field. The first sync that creates a field honored isNullable correctly, but any later manifest change to isNullable had no effect, neither on the field metadata nor on the underlying Postgres column. Two compounding gaps caused this: The diff never detected the change. isNullable was configured with toCompare: false, so compareTwoFlatEntity excluded it from the diff and no update action was ever generated. There was no DDL to apply it. Even if detected, the update field action handler only altered name, options, defaultValue, and settings. The column manager had no way to alter a column's NOT NULL constraint. ## Fix - Set isNullable.toCompare: true so manifest changes are detected and persisted to the field metadata (via the existing executeForMetadata path). - Add WorkspaceSchemaColumnManagerService.alterColumnNullable(): emits SET NOT NULL / DROP NOT NULL, with an optional pre-serialized backfill (UPDATE … WHERE col IS NULL) applied only on the nullable → non-nullable transition. - Add handleFieldNullableUpdate() to the update field action handler, dispatched after the defaultValue block so the default is in place before NOT NULL is enforced. It is composite-aware (mirrors the per-sub-column parentIsNullable || !property.isRequired rule used at column creation) and skips relation/morph join columns and TS_VECTOR, which are always nullable by design. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22362?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. --> |
||
|
|
49a80c72d2 |
feat: show group-by context in record show breadcrumb (#22247)
Fixes [#837](https://github.com/twentyhq/core-team-issues/issues/837) On the record show page, extend breadcrumb pagination when the current view is grouped: (`rank/total in {viewName} -> {groupValue}`) Example: `Tasks / Schedule follow-up call (1/1,800 in By Status -> To do)` https://github.com/user-attachments/assets/7038d1f5-57e5-4e85-a3e6-09ac46c5b824 https://github.com/user-attachments/assets/88134fa4-e038-4520-a970-ce058a4444b0 <img width="1427" height="173" alt="Screenshot 2026-06-27 202807" src="https://github.com/user-attachments/assets/842d911e-b4bc-4443-afcd-4c67ed007ae0" /> <img width="1426" height="183" alt="Screenshot 2026-06-27 202851" src="https://github.com/user-attachments/assets/f8d9ee31-b1cb-46fa-9e7c-8876d4b099ff" /> <img width="1427" height="178" alt="Screenshot 2026-06-27 203423" src="https://github.com/user-attachments/assets/196ed694-0edb-4b52-934c-0938d3fd4da2" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22247?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> Co-authored-by: bosiraphael <raphael.bosi@gmail.com> |
||
|
|
4fef02394f |
Backfill webhook subscriptions for existing connected accounts (#22314)
Add command iterating workspaces, enqueuing staggered per-channel jobs for Google/Microsoft channels still on polling <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22314?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. --> |
||
|
|
59d708e324 |
fix(workflow): stop relative date filter from crashing on empty/invalid date values (#22384)
## Problem
A workflow **Filter** step on a `DATE`/`DATE_TIME` field using
`IS_RELATIVE` crashes with a `RangeError` when the referenced step
output is empty or invalid. The empty value is coerced into an `Invalid
Date` (`new Date("undefined")`),
whose `.getTime()` is `NaN`, and
`Temporal.Instant.fromEpochMilliseconds(NaN)` throws, failing the
affected workflow runs.
This is a latent regression from the Date → Temporal migration (#16544):
the previous `date-fns` implementation silently returned `false` on an
invalid date, but Temporal is strict and throws. The guard was never
carried over.
## Fix
Validate the coerced date once at the boundary in `evaluateDateFilter` —
the single place arbitrary/empty step output is turned into a `Date`. An
unparseable date now resolves to "does not match" for every comparison
operand (`IS`, `IS_IN_PAST`, `IS_IN_FUTURE`, `IS_TODAY`, `IS_BEFORE`,
`IS_AFTER`, `IS_RELATIVE`), restoring the pre-migration contract.
`IS_EMPTY` / `IS_NOT_EMPTY` are intentionally excluded so emptiness is
still evaluated on the raw operand.
## Tests
Added a parameterized regression test covering every date comparison
operand with empty and missing step output, asserting no throw and a
`false` result.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22384?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. -->
|