632114e5e226e891a6798e4cfa6eaefb2bc7a4dc
179 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
13f80f0d95 |
fix: ignore IME composition Enter in chat-thread and attachment rename inputs (#22270)
## What's this PR doing? Two inline rename inputs run their action on `Enter` without ignoring the `Enter` that confirms an IME composition: - `AiChatThreadListItem` (renaming an AI chat thread). It also calls `preventDefault()`, so the composition-commit `Enter` is swallowed and the half-typed title gets saved. - `AttachmentRow` (renaming an attachment). The same `Enter` saves the unfinished name. When you type with an IME (Japanese, Chinese, Korean), the first `Enter` after typing confirms the candidate text rather than submitting, so these handlers fire with text the user hasn't finished entering. ## Why The codebase already guards this where keyboard handling goes through `useHotkeysOnFocusedElement` (`if (keyboardEvent.isComposing || keyboardEvent.keyCode === 229) return`), and the inline inputs that don't use that hook add the same check themselves: see `SettingsAccountsBlocklistInput`, `SettingsDevelopersApiKeysNew`, and the sign-up workspace forms. These two rename inputs were just missing it. ## How Add the same `isComposing || keyCode === 229` guard before the `Enter` branch. For input without an IME, `isComposing` is `false` and `keyCode` is `13`, so the rename-on-Enter behavior stays the same. This only skips the action on the composition-commit key. I checked the change against the repo's Prettier config locally. I couldn't add a unit test because jsdom doesn't dispatch real composition events (`isComposing` stays `false`), so it can't reproduce the keystroke. Happy to add an e2e test if that's preferred. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22270?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. --> |
||
|
|
3525187321 |
fix(ai) - fixes (#22227)
- ai chat author fix (before : "workflow", after : "user") - https://discord.com/channels/1130383047699738754/1496872385687584768 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22227?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. --> |
||
|
|
da6a2ee300 |
fix(ai-chat): keep streams alive on silent SSE death + make the stream job idempotent (#22201)
## Problem In production, an AI-chat assistant response sometimes freezes mid-stream (partial text, looks hung), then "picks up again on its own" later without the user resending and without a known worker restart. Root cause: the **agent-chat SSE subscription has no keepalive and no silent-death detection**. - Delivery is fire-and-forget Redis pub/sub (`SubscriptionService.publishToAgentChat`) and the resolver returns the **raw** iterator — unlike `EventStreamResolver`, which heartbeats every 30s via `wrapAsyncIteratorWithLifecycle`. - During a quiet model/tool gap the connection sends no bytes, so a proxy/LB/NAT can silently drop it mid-stream. `graphql-sse` neither surfaces an error nor resumes with `Last-Event-ID`, and **nothing re-pulls the existing Redis chunk catch-up on reconnect** (it only runs on thread (re)mount / `message-persisted` refetch). - So the live view freezes; recovery only happens when the terminal `message-persisted` fires a full refetch from the DB — the observed "self-recovery". This is the **same silent-SSE-death class fixed for the DB event stream in #21061**, which was never applied to the agent-chat path. The symptom also matches #21096 (worker logs the job finishing, client never updates, reload shows the message). It is **not** queue prioritization, and it is **not** addressed by #22193 (which only stabilizes the assistant message id and removes end-of-stream flicker). A secondary, independent self-recovery path also existed: BullMQ stalled-job re-run (default 30s `lockDuration`, no idempotency guard) re-streaming the whole turn → duplicate assistant messages / double billing. ## Changes ### Commit 1 — keepalive + silent-death recovery (ports the #21061 pattern to agent chat) - **Shared:** new `keepalive` variant on `AgentChatSubscriptionEvent`. - **Server:** wrap the agent-chat subscription iterator with `wrapAsyncIteratorWithLifecycle` — emit a `keepalive` on connect and every `APPLICATION_KEEPALIVE_INTERVAL_MS` (30s) so the connection keeps flushing bytes and a dead connection becomes detectable. - **Client:** track the last received event timestamp (refreshed on every chunk/keepalive in the SSE `next` sink); new `AgentChatStreamKeepAliveEffect` forces a resubscribe + messages refetch after 90s of silence, so the durable Redis chunk list backfills the gap (`firstLiveSeq` is reset on resubscribe). ### Commit 2 — stream-job idempotency + lockDuration - Thread a `lockDuration` option through `MessageQueueWorkerOptions` + the BullMQ driver; set `aiStreamQueue` to 10 min so long streams aren't falsely stalled. - Guard `StreamAgentChatJob.handle` with a `streamId`-scoped Redis lock (`SET NX PX` + compare-and-delete release) so a stalled re-run is skipped instead of double-processing. ## Verification ⚠️ I could **not run typecheck/lint locally** — `yarn install` could not complete in this environment (transient registry network aborts before the link step, so `node_modules` never populated). **Please rely on CI for type/lint verification.** The changes are written to match existing conventions; the points most worth a reviewer's eye are the resolver's iterator typing and the ioredis `set(..., 'PX', ttl, 'NX')` overload. How to confirm the root cause in prod: a frozen client with the worker logging `StreamAgentChatJob processed in …ms` and no `[AI_CHAT_NO_TEXT]` is the silent-death signature (check reverse-proxy idle/buffering). For the secondary path, watch `aiStreamQueue` `stalled`/re-processed metrics and duplicate turns around worker restarts. ## Notes / trade-offs - The 10-min `lockDuration` means a genuinely crashed worker's job isn't reclaimed for up to 10 min; the client-side keepalive/catch-up recovers the view independently, and the idempotency lock prevents duplicates. Faster dead-worker recovery could be a follow-up. - Touches `useAgentChatSubscription.ts` / `AgentChatRuntimeEffects.tsx` / `stream-agent-chat.job.ts`, which #22193 also touches — trivial rebase expected. Opened as **draft** pending CI. https://claude.ai/code/session_018dF82A1VcsuWMxPLmdY3dm --- _Generated by [Claude Code](https://claude.ai/code/session_018dF82A1VcsuWMxPLmdY3dm)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22201?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. --> |
||
|
|
b625bd1995 |
fix(ai-chat) - improvements (#22193)
- remove flickering at assistant message streamed end - add copy code - leave chat history when navigating to settings <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22193?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. --> |
||
|
|
87329c8810 |
fix(ask-ai): resolve stream subscription race condition on new thread… (#21916)
## Description Resolves a race condition in the Ask AI feature where the first assistant reply in a newly created thread does not stream into the UI and only appears after sending a second message. ### What's Changed - **Immediate Thread Subscription:** Updated `useAgentChat.ts` to immediately set `currentAiChatThread` to the newly generated `threadId` instead of deferring it until after the `SEND_CHAT_MESSAGE` mutation finishes. - **The Bug:** Previously, the backend worker processed the AI chat job so quickly that the stream completed and fired the `message-persisted` event *before* the frontend established the SSE subscription. - **The Fix:** By setting the thread ID immediately, the `useAgentChatSubscription` hook now properly connects and listens to the SSE stream before the backend begins emitting chunks, guaranteeing the first message streams seamlessly. ### How to Test 1. Open the Ask AI panel and start a completely new thread. 2. Send an initial message (e.g., "Hello!"). 3. Observe that the AI's response successfully streams into the chat without needing a workaround or page refresh. Closes #21694 --------- Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com> |
||
|
|
94dbcc27a9 |
feat(billing): embed credit card form in the add-card trial-end modal (#22125)
## Context
When a trialing workspace (trial without a credit card) clicks **Add
Credit Card** from the "End trial period" banner or the AI-chat
usage-limit banner, the modal currently redirects the browser to
Stripe's hosted billing portal to collect the card. Since we already
embed the Stripe Payment Element in onboarding, this brings the same
in-app experience to the trial-end modal so the whole flow stays inside
Twenty.
## Why the onboarding flow couldn't be reused as-is
The onboarding embed (`createSubscriptionPaymentIntent` /
`SubscriptionPaymentForm`) **creates a new subscription** with
`payment_behavior: 'default_incomplete'`. In the trial-end case the
customer **already has a trialing subscription**, so that path throws
`BILLING_SUBSCRIPTION_INVALID`. The correct primitive here is a
**SetupIntent** against the existing customer: collect + save the card,
then end the trial.
A standalone SetupIntent attaches the card to the customer but does
**not** make it the default (the Stripe portal used to do that for us),
so the trial-end invoice would have no payment method. The backend now
backfills the customer default before charging.
## Changes
**Backend**
- `StripeCustomerService`: `createSetupIntent()` for an existing
customer, and `ensureDefaultPaymentMethod()` which sets the customer
default only when none is already set (won't clobber a portal-chosen
default).
- `BillingPortalWorkspaceService.createPaymentMethodSetupIntent()`:
returns a SetupIntent client secret for the current non-canceled
subscription's customer.
- `BillingSubscriptionService.endTrialPeriod()`: ensures a default
payment method before `trial_end: 'now'`.
- New `createBillingPaymentMethodSetupIntent` mutation +
`BillingSetupIntent` DTO; SDK schema snapshot synced.
**Frontend**
- `AddPaymentMethodForm`: Stripe Elements (`mode: 'setup'`), confirms
with `redirect: 'if_required'` so the common card case stays in-app; 3DS
still redirects and is finished by the existing
`EndTrialAfterPaymentMethodEffect`.
- `AddCreditCardModal`: hosts the embedded form.
- Both trial-end banners (`InformationBannerEndTrialPeriod`,
`AIChatNoMoreBillingCreditsBanner`) open the embedded modal instead of
redirecting when no card is on file; the AI-chat path preserves its
thread context in the 3DS return URL.
## Flow
1. User clicks **Add Credit Card** → embedded modal opens.
2. Card entered → `createBillingPaymentMethodSetupIntent` →
`confirmSetup({ redirect: 'if_required' })`.
3. Non-3DS: confirms inline → `endSubscriptionTrialPeriod` →
subscription active, no redirect.
4. 3DS: redirects to `?startSubscriptionAfterPaymentMethod=true` →
existing effect finishes activation.
5. Self-hosted instances without a Stripe publishable key fall back to
the existing portal redirect (the form renders an unavailable state).
## Notes for reviewers
- The metadata GraphQL types were regenerated by hand (codegen needs a
live `/metadata` server, which wasn't available in the authoring
environment); a `graphql:generate --configuration=metadata` run against
a live backend should be a no-op.
- Local `typecheck`/`lint` could not be run in the authoring environment
(dependency install was blocked); relying on CI to validate.
- Scope is intentionally limited to the two trial-end banner modals. The
Settings → Billing "update payment method" link still uses the Stripe
portal.
https://claude.ai/code/session_01VU7SfrSgaYWr2AhVL8DMfu
---
_Generated by [Claude
Code](https://claude.ai/code/session_01VU7SfrSgaYWr2AhVL8DMfu)_
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22125?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. -->
|
||
|
|
ad3c82bd15 |
fix(front): prevent lingui extract crash in buildCrudToolStatusMessage (#22080)
## Problem
The `build-front / s3-build` CD job fails during the `Build frontend`
step, in the `twenty-front:lingui:extract` target (`lingui extract
--overwrite --clean`):
```
Cannot process file .../build-crud-tool-status-message.util.ts:
Cannot read properties of undefined (reading 'name')
at @lingui/babel-plugin-extract-messages/dist/index.cjs:88:22
at extractFromObjectExpression (...index.cjs:87:18)
at extractFromMessageDescriptor (...index.cjs:121:19)
at PluginPass.CallExpression (...index.cjs:189:11)
```
## Root cause
`buildCrudToolStatusMessage` called `i18n._()` with an inline object
literal containing a spread:
```ts
i18n._({ ...verbs.loading, values: { objectLabel } })
```
Lingui's `extract-messages` babel plugin fires on every `i18n._(...)`
call. When the first argument is an `ObjectExpression`, it runs
`extractFromObjectExpression`, which reads `key.name` for **every**
property. The spread element `...verbs.loading` has no `key`, so
`key.name` throws `Cannot read properties of undefined (reading
'name')`, crashing `lingui extract` and failing the whole S3 publish
job.
## Fix
Hoist the descriptors into variables so `i18n._()` receives an
identifier rather than an inline object expression. The plugin then
skips extraction (no statically-extractable id), so no crash. Runtime
behavior is unchanged — the translatable strings are still extracted
from the `msg` macros in `CRUD_TOOL_OPERATION_VERBS`.
## Testing
- Reproduced the **exact** CI crash locally on `main` by running `lingui
extract --overwrite --clean` (same file, message, and stack frames).
- After the fix, `lingui extract --overwrite --clean` runs clean (exit
0).
- `build-crud-tool-status-message.util.test.ts` passes (2/2).
- `nx lint:diff-with-main twenty-front` passes (0 warnings, 0 errors,
formatting clean).
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22080?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. -->
|
||
|
|
5ca41d55fb |
feat(ai): humanize tool-call (#21976)
# Humanize tool-call labels cc: https://github.com/twentyhq/twenty/pull/21462 ## Preview <img width="459" height="156" alt="Screenshot 2026-06-22 at 19 13 11" src="https://github.com/user-attachments/assets/e7a2f5f5-cd09-4ec6-920b-5eb16b98285c" /> <img width="461" height="156" alt="Screenshot 2026-06-22 at 19 14 54" src="https://github.com/user-attachments/assets/c2114d2e-2aa8-499a-9801-68e3bb7c45f8" /> <img width="461" height="505" alt="Screenshot 2026-06-22 at 19 15 01" src="https://github.com/user-attachments/assets/ee9ca5d0-8e79-4c63-a2ff-ed5e359a9a9c" /> ## Why In the AI chat, tool steps were displayed using raw tool identifiers (`find_many_companies`, `create_one_task`, `send_email`...) and labels were partially reconstructed/humanized on the frontend. This was hard to localize and inconsistent across tool categories. This PR makes the **backend the single source of truth for human-readable, localized tool labels**, exposes them through `getToolIndex`, and reduces the frontend to a thin resolver that picks the right label for the current status (in-progress / completed). ## What changed ### Backend - `ToolIndexEntry` (and the `getToolIndex` GraphQL DTO) now carry `label`, `inProgressLabel?`, `completedLabel?`. - New `getCrudToolLabels(operation, objectLabel, i18nService, locale)` builds CRUD labels from a verb table (Search / Find / Group / Create / Update / Upsert / Delete × imperative / in-progress / completed) + the (translated, lowercased) object label. - New `translate-tool-label.util.ts` translates a source label via `I18nService` (`generateMessageId` → fallback to source when no translation exists). - Action tools: labels extracted to the `ACTION_TOOL_LABELS` constant (`msg` + `i18nLabel`) and translated in `ActionToolProvider.buildDescriptor`. - Logic-function tools use the function name as label; `toolSetToDescriptors` (workflow / view / metadata / dashboard) accepts an optional `labels` map and falls back to a humanized tool name. - Labels are localized server-side using the request locale (`@RequestLocale` → `buildToolIndex` → `context.locale`, threaded through `ToolContext` / `ToolProviderContext`). - `code_interpreter` schema now asks the model for `loadingMessage` (present tense) and `completedMessage` (past tense), so its status text is model-generated. - Removed the old generic `loadingMessage` injection mechanism (`wrap-tool-for-execution.util.ts` deleted; `wrapJsonSchemaForExecution` / `stripLoadingMessage` no longer wrap every tool). ### Frontend - New `useToolLabelMap()` hook builds a `Map<name, { label, inProgressLabel, completedLabel }>` from `getToolIndex`. - `getToolDisplayMessage` → `resolveToolDisplayMessage({ input, toolName, isFinished, labelMap, output })`: a small resolver registry keyed by tool name (`execute_tool`, `web_search`, `learn_tools`, `load_skills`, `code_interpreter`, default). - Default resolver prefers backend `completedLabel` / `inProgressLabel`, falling back to `Ran X` / `Running X`. - `learn_tools` / `load_skills` resolve their inner tool/skill names to labels (label map → tool output labels via `getToolOutputLabelEntries` → raw name). - `code_interpreter` step is now expandable to show the code even while running. ## How tool labelling flows (BE → FE) ```text BACKEND ┌───────────────────────────────────────────────────────────────────────────┐ │ Tool providers (per category) → ToolIndexEntry │ │ │ │ DatabaseToolProvider │ │ getCrudToolLabels(operation, object.labelPlural/Singular, i18n, locale) │ │ verb table (Search/Create/Update/Delete…) + translateToolLabel(object) │ │ → { label, inProgressLabel, completedLabel } │ │ │ │ ActionToolProvider │ │ ACTION_TOOL_LABELS[toolId] (msg) → translateToolLabel(…, locale) │ │ → { label, inProgressLabel?, completedLabel? } │ │ │ │ LogicFunctionToolProvider → label = logicFunction.name │ │ toolSetToDescriptors → label = labels[name] ?? humanize(name) │ │ (workflow / view / metadata / dashboard) │ └───────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌───────────────────────────────────────────────────────────────────────────┐ │ GraphQL Query getToolIndex : [ToolIndexEntry] │ │ { name, label, inProgressLabel, completedLabel, description, │ │ category, objectName, icon } │ └───────────────────────────────────────────────────────────────────────────┘ │ ▼ FRONTEND ─ resolve the right label for the current status ┌───────────────────────────────────────────────────────────────────────────┐ │ useGetToolIndex() → useToolLabelMap() │ │ Map<name, { label, inProgressLabel?, completedLabel? }> │ └───────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌───────────────────────────────────────────────────────────────────────────┐ │ resolveToolDisplayMessage({ input, toolName, isFinished, labelMap, output })│ │ │ │ TOOL_LABEL_RESOLVERS[toolName] ?? defaultResolver │ │ ├─ execute_tool → unwrap { toolName, arguments } then re-resolve │ │ ├─ web_search → "Searching/Searched the web for <query>" │ │ ├─ learn_tools → "Learning/Learned <labels>" │ │ ├─ load_skills → "Loading/Loaded <labels>" │ │ │ inner names resolved via: labelMap → output labels → raw name │ │ ├─ code_interpreter → model's loadingMessage / completedMessage │ │ └─ default → isFinished │ │ ? completedLabel ?? "Ran <label>" │ │ : inProgressLabel ?? "Running <label>" │ └───────────────────────────────────────────────────────────────────────────┘ │ ▼ Rendered by ThinkingStepsDisplay / ToolStepRenderer ``` ## Localization notes - Standard object labels and action/CRUD verbs are translated server-side via `I18nService` using the requester's locale. - Custom object labels are not translated unless a workspace custom translation exists (matched by `generateMessageId`); otherwise the source label is used as-is. ## Tests - **FE:** `resolveToolDisplayMessage` / `getToolOutputLabelEntries` (status selection, inner-name resolution, `code_interpreter` model labels, fallbacks). - **BE:** `toolSetToDescriptors` (label map + humanized fallback) and `database-tool.provider` label generation. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21976?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. --> |
||
|
|
d2083e7a1b |
Set OpenAI Responses store false for AI chat and agents (#20888)
## Summary This PR sets `openai.store = false` for Twenty's `@ai-sdk/openai` AI calls. This follows the approach discussed in #20877: instead of adding a new Twenty-specific Zero Data Retention config variable, OpenAI Responses calls no longer rely on OpenAI-stored response/item references. This should help Zero Data Retention organizations and may also avoid stale persisted-item replay errors for non-ZDR OpenAI users. Changes included: - Adds a shared OpenAI provider-options helper that merges `openai.store = false` for `@ai-sdk/openai` models. - Applies the helper to AI chat `streamText` calls. - Applies the helper to workflow/agent `generateText` calls. - Preserves OpenAI encrypted reasoning metadata through DB/UI message mappers so reasoning context can be replayed without stored OpenAI item references. - Does not add a new env/config variable. Related to issue #20877. ## Behavior / Tradeoffs This changes OpenAI Responses behavior for all Twenty OpenAI users, not only ZDR users. The intended benefit is that Twenty no longer depends on OpenAI-stored response/item references. The main tradeoff is reduced provider-side item-reference reuse for non-ZDR OpenAI users. To reduce the impact for reasoning models, this PR preserves `providerMetadata.openai.reasoningEncryptedContent` through message persistence/replay so reasoning context can still be provided without stored OpenAI item references. ## Tests - Focused server Jest tests for OpenAI provider-options merging and reasoning metadata mapping. - Focused frontend Jest test for reasoning metadata mapping. - `oxlint` and `oxfmt --check` on changed files. - `git diff --check`. --------- Co-authored-by: Charles Bochet <charles@twenty.com> Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com> |
||
|
|
8034c7725f |
Reorganize twenty-ui into best-practice component domains and per-component folders (#21745)
Reorganizes `twenty-ui`'s component organization to follow how the best
UI libraries (MUI, Mantine, Base UI, Polaris) structure their source,
now that the package has stabilized.
**Taxonomy** — dissolves the meaningless `components/` junk-drawer and
the 107-file `display/` mega-category. New domains/subpaths:
`data-display`, `typography`, `icon`, `surfaces`; `feedback` and
`layout` absorb the rest (banners/callout/info + placeholders →
feedback; modal/card → surfaces; motion + separators → layout).
**Per-component layout** — every component is now
`<domain>/<ComponentName>/<ComponentName>.tsx` with colocated
styles/stories/types, `internal/` for private helpers and `parts/` for
re-exported compound sub-parts. The redundant inner `/components/` is
gone. `icon` and `json-visualizer` are kept as cohesive subsystems.
**Also:** adds a tree-shakeable root barrel (`import { Button } from
'twenty-ui'`), the generator now owns `individual-entry.ts`, and a real
barrel-leak bug is fixed (private `internals/` parts were leaking into
the public API).
Consumer imports (~1.2k files) and the `twenty-sdk` UI aggregator were
updated by codemod. The change is **export-neutral** except 16
intentionally-removed private internals symbols (all verified
unconsumed). Gates green: typecheck, lint, build, size-limit, storybook.
<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21745?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. -->
|
||
|
|
d99e479be8 |
feat(billing) - facilitate top up in ai chat (#21645)
Today, when a trialing user hits their AI usage cap inside the Ask AI chat, ending the trial bounces them to the Stripe billing portal (and, for card-less users, loses their place in the conversation). This PR makes activating a paid plan / topping up credits feel seamless from within the chat: Trial users with a card on file activate their subscription in place, without leaving the app. Trial users without a card are sent to the Stripe payment-method portal and, on return, the trial is ended automatically and they're dropped back into the exact Ask AI thread they came from. Credit-exhaustion and trial banners now reflect whether a payment method exists (Add Credit Card vs Subscribe Now / End Trial Period) and upgrade inline via a confirmation modal instead of redirecting to Settings. Uploading Screen Recording 2026-06-16 at 07.51.12.mov… https://github.com/user-attachments/assets/4ea77273-da63-4b32-b6f1-5ac9e9560651 <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21645?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. --> |
||
|
|
02a3a3c47c |
fix(ai): handle dynamic-tool message parts in chat persistence (#21740)
## Summary Fixes #20558. AI chat streams crashed with `Unsupported part type: dynamic-tool` whenever the model emitted a *dynamic* tool call (a tool that isn't part of the bound schema). The assistant message never persisted, so the user saw a hard failure mid-stream. ## Root cause The AI SDK v6 emits two flavors of tool parts: - **Static** — `type: "tool-<toolName>"` (e.g. `tool-execute_tool`) - **Dynamic** — `type: "dynamic-tool"`, with the name on `part.toolName` `mapUIMessagePartsToDBParts` recognised tool parts with a homegrown check: ```ts part.type.includes('tool-') && 'toolCallId' in part ``` That returns `false` for `'dynamic-tool'` (it contains `-tool`, not `tool-`), so dynamic parts fell through to `throw new Error(\`Unsupported part type: ${part.type}\`)` during the `handleStreamFinish` persistence step. Stack trace from the issue matches exactly. The same broken heuristic was duplicated in: - `packages/twenty-server/.../mapDBPartToUIMessagePart.ts` (reverse mapper) - `packages/twenty-front/.../utils/mapDBPartToUIMessagePart.ts` (frontend mirror — would also throw on a `dynamic-tool` row reloaded from history) Meanwhile, two other call sites in the codebase (`finalize-dangling-tool-parts.util.ts`, `isThinkingStepPart.ts`) already correctly use the SDK's `isToolUIPart`, which natively recognises both flavors. ## What this PR does 1. **Switches all three mappers to the SDK's canonical check** (`isToolUIPart` on the forward path; explicit `dynamic-tool` + `tool-` startsWith on the reverse paths, where the input is an entity/DTO, not a UI part). 2. **Persists `toolName`** — the column already existed on the entity, DTO and GraphQL fragment but nothing wrote it. For static parts the name is recoverable from `type`; for dynamic parts it's the only place the name lives, so without it the round-trip is impossible. The shared denormalisation also helps existing per-tool analytics (`count-native-web-search-calls-from-steps.util.ts`). 3. **Reconstructs `dynamic-tool` parts on read** (with `toolName`) so they survive a DB round-trip both on the server and on the frontend history view. 4. **Adds a round-trip unit test** covering both `dynamic-tool` and a static tool part to lock the behavior in. ## Architecture notes (called out for review) - `mapDBPartToUIMessagePart` is duplicated frontend + backend because the input shape differs (TypeORM entity vs. GraphQL DTO). Out of scope to consolidate here, but they're drifting — this PR is what that drift looked like in production. Worth a follow-up to express the shared logic once over a unified row type. - I left the existing renderer guard `part.type !== 'dynamic-tool'` in `AiChatAssistantMessageRenderer.tsx` alone — it's a reasonable UI-side decision to not attempt to render an unknown dynamic tool generically. Persistence and history reload now work; rendering of dynamic tool calls is a separate UX decision. - No DB migration needed — the `toolName` column already exists. Old static rows have `toolName: null`; the reverse mapper recovers their name from the `type` column as before. Old dynamic-tool rows don't exist (they all threw on write). ## Test plan - [x] `yarn workspace twenty-server jest map-message-parts.dynamic-tool` — 5 passed - [x] `yarn workspace twenty-server jest finalize-dangling-tool-parts.roundtrip` — still 4 passed (no regression) - [x] `yarn nx typecheck twenty-server` — clean - [x] `yarn nx typecheck twenty-front` — clean - [x] `yarn nx lint:diff-with-main twenty-server` — clean - [x] `yarn nx lint:diff-with-main twenty-front` — clean - [ ] Manual: trigger an AI chat that exercises a dynamic tool (e.g. via an MCP server returning a tool not in the bound schema) and confirm the stream finishes and the message persists. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_013EE11eVWtyxmdcbEHVJKoc --- _Generated by [Claude Code](https://claude.ai/code/session_013EE11eVWtyxmdcbEHVJKoc)_ <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21740?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: Claude <noreply@anthropic.com> |
||
|
|
9c9c34fccf |
Remove twenty-ui-deprecated and migrate frontend to twenty-ui (#21596)
Migrates `twenty-front`, `twenty-sdk`, and `twenty-front-component-renderer` from `twenty-ui-deprecated` to `twenty-ui` (mechanical import swap — the packages have API parity) and deletes the deprecated package along with its workspace/CI/config wiring. Also adds `@linaria/react`/`@linaria/core` as direct deps of `twenty-front` (it used them transitively via the deprecated package). Note: move the required status check from `ci-ui-status-check` to `ci-new-ui-status-check`. Argos: the Storybook box-model/button-reset baseline shift (the bulk of the visual diffs) is isolated in #21665 — Storybook now loads twenty-ui's global `reset.scss`, which the production app already ships. Once #21665 merges and this branch is rebased, the remaining Argos diffs are component-level visual-parity items only. |
||
|
|
ceb7698689 |
fix(ai) - workflow tool outputs optim + display fix (#21500)
<!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21500?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. --> |
||
|
|
ecc7b38b75 |
Remove randomness from flaky twenty-front Storybook stories (#21594)
Several twenty-front Storybook stories were flagged flaky by Argos because they render different pixels across runs. This removes the non-determinism behind them. **What changed** - **Images** — replaced random `picsum.photos` URLs in the Logo and TabList stories with the existing `AVATAR_URL_MOCK`, and added global MSW handlers in `.storybook/preview.tsx` that serve a deterministic image for every remote host (picsum, twenty-icons.com, twentyhq.github.io, etc.) so no story depends on a network image load. - **Numbers** — the line-chart story built its data with `Math.random()`; now uses a deterministic formula. - **Dates** — the terminal "long output" story stamped its lines with `new Date()`; now uses a fixed base timestamp. The calendar-channel date/time format previews and example event used render-time `Date.now()`/`new Date()` in shared components; they now use a fixed reference date (`DateTimeSettingsPreviewDate`). - **Lazy-load timing** — the date-picker story now waits for the lazily-loaded calendar before the snapshot. <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/21594?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. --> |
||
|
|
b14195428a |
Fix navigation and settings UI polish (#21523)
## Summary This PR groups the requested UI polish pass across navigation, settings, AI settings, data model, community/lab, and dashboard chips. ### Navigation and drawer polish - Smooths app/settings route switching with a 300ms content transition. - Smooths app menu/settings menu drawer swaps with a fade transition while preserving the existing drawer dimensions. - Keeps navigation and AI chat history panes mounted to avoid flicker when switching tabs. - Aligns the first app/settings section with the AI chat history section title. - Removes the main app navigation "Other" section. - Aligns the settings exit header with the workspace switcher header. - Sets the settings exit icon gap to 8px, uses the small icon stroke token, and keeps the icon color on text-secondary. - Makes the workspace switcher button 28px high inside its 32px container, moves the dropdown up so the workspace name does not jump, and keeps a 2px gap between header icon buttons. - Moves Settings below Support in the workspace switcher menu. - Pins the Advanced toggle to the bottom of the settings drawer and aligns its right padding with nav items. ### Settings surface polish - Fixes vertically cropped dropdown menu headers. - Makes wizard parent titles tertiary when a nested wizard title is visible. - Places danger-zone buttons side by side. - Uses a 32px Visualize button. - Restores 8px top/bottom padding on settings tables. - Separates AI overview counts into three columns like layout overview. - Adds separators inside setting cards, including the Smart Model / Fast Model card. - Groups lab/early-access toggles into one card with separators and full-width row backgrounds. - Replaces deprecated Enterprise adornment tags with the Organization adornment on gated AI/security/settings surfaces. - Uses the Discord brand icon in Community while keeping the standard icon component rendering pattern. - Tightens Skills/Tools search-to-table spacing so switching tabs does not shake the table top. - Fixes the small gap in nested navigation breadcrumbs between Emails and Calendars. ### Dashboard/data table polish - Fixes vertically cropped "Not shared" chips on dashboards. - Keeps table row/header spacing stable after the settings table padding restoration. ## Validation - `npx nx lint:diff-with-main twenty-front` - `npx tsc -p packages/twenty-front/tsconfig.json --noEmit` - Manual Chrome pass on `apple.localhost:3001` for profile, app navigation, workspace switcher, AI overview/models/skills/tools/usage, community/lab, data model, new-field wizard, and dashboards. ## Visual QA ### Settings surface fixes <img width="1324" height="2668" alt="Settings surface fixes before/after board" src="https://github.com/user-attachments/assets/312131fa-3922-4724-9460-46fd373754d1" /> ### Navigation drawer fixes <img width="1324" height="1816" alt="Navigation drawer fixes before/after board" src="https://github.com/user-attachments/assets/ca0b29e5-e393-46c0-81f7-e6bed2557b59" /> ### Tables, wizards, gates and chips <img width="1324" height="2242" alt="Tables, wizards, gates and chips before/after board" src="https://github.com/user-attachments/assets/5f358240-c31c-4c15-8ca8-04ed761fbf85" /> |
||
|
|
c4453923f0 |
Update CI: Argos visual regression for twenty-front storybook (#21454)
## What Adds Argos visual regression for `twenty-front`, reusing the storybook CI already builds and the existing sharded test matrix. Stories in the `modules` and `pages` scopes are captured as PNGs during `front-sb-test`, merged into one artifact, and pixel-diffed against `main` on the self-hosted Argos with results posted as a PR comment — same pipeline as `twenty-ui` (#21210 / #21262). ## How - **Capture**: `@argos-ci/storybook` vitest plugin, same setup as `twenty-ui`. Skipped for `performance` stories (nondeterministic profiling reports). Freezes framer-motion to avoid flaky diffs (#21412). - **Sharding**: each modules/pages shard uploads a partial artifact; a new `front-sb-screenshots` job merges them into `argos-screenshots-twenty-front` (`overwrite: true` so re-runs work). - **Baselines**: `CI Front` now runs on `push: main` — Argos resolves base builds by exact merge-base commit, so every main commit needs a build (#21217/#21222 pattern). Main pushes get a per-SHA concurrency group so back-to-back merges can't cancel queued runs and leave baseline gaps; the `performance` scope is dropped on push. - **Dispatch**: `visual-regression-dispatch.yaml` watches `CI Front` → `project=twenty-front`. ## Rollout - ✅ Prod Argos project `twenty-front` created (id 68) + `ARGOS_TOKEN_FRONT` secret set - ⬜ Merge the twentyhq/ci-privileged companion PR **before** this one - First PR builds show as *orphan* until the first main push creates a baseline (expected, same as the twenty-ui rollout) |
||
|
|
184c4948d6 |
security: strip Node dev headers from images + lingui 5.9.5 (drops vulnerable esbuild) (#21448)
## Context
AWS Inspector flags the `prod-twenty` image (built from current main)
with 16 findings, and Dependabot alert 174 flags esbuild. This PR fixes
the OpenSSL scanner findings and the esbuild CVE. The typeorm bump
(CVE-2025-60542) was **pulled out of this PR** — see "typeorm status"
below.
## Changes
### Strip `/usr/local/include/node` from runtime stages
(`twenty-server`, `twenty-app-dev`)
15 OpenSSL CVEs (June 9 advisory, incl. CRITICAL CVE-2026-34182) are all
detected via **Node's bundled OpenSSL dev headers**: 3 GENERIC
`openssl/openssl` 3.5.6 detections per CVE at
`/usr/local/include/node/openssl/archs/linux-x86_64/{asm,asm_avx2,no-asm}/include/openssl/opensslv.h`.
The headers are only needed by node-gyp and native addons are compiled
in the build stages — nothing compiles at runtime. Dropping them clears
all 45 detection instances and permanently ends this class of finding
(third occurrence: 3.5.5 → 3.5.6 → 3.5.7). None of these CVEs are
reachable through Node (no CMS/PKCS#7 API, `pfx` is operator-supplied,
Node's QUIC uses ngtcp2, ASN.1 issues need ~2GB inputs).
**Follow-up (~June 17, 2026):** the `node` binary itself still
statically links OpenSSL 3.5.6 — invisible to the scanner after this PR
and unreachable in practice, but the real fix is bumping the pinned
`node:24-alpine` digest once the [announced June 17 Node.js security
releases](https://nodejs.org/en/blog/vulnerability/june-2026-security-releases)
ship a 24.x linking OpenSSL ≥ 3.5.7 (verify via
`deps/openssl/openssl/VERSION.dat` on the release tag — 24.16.0 is still
on 3.5.6). A dated TODO sits next to the cleanup in the Dockerfile.
### esbuild dev-server CORS CVE (Dependabot alert 174,
GHSA-67mh-4wv8-2f99)
`@lingui/cli@5.1.2` (pins `esbuild ^0.21.5`) was the last parent
resolving a vulnerable esbuild (≤ 0.24.2 lets any website send requests
to the dev server and read responses). Instead of a resolution override,
this bumps the lockstepped **lingui suite 5.1.2 → 5.9.5** (within-major;
lingui adopted `esbuild ^0.25.1` in 5.4.1), which:
- removes `esbuild@0.21.5` and all its platform packages from the
lockfile with no forced ranges;
- drops the `@lingui/core` lockstep resolution (its comment marked it
droppable on the next coordinated lingui bump — the tree now resolves a
single `@lingui/core@5.9.5`);
- `@lingui/swc-plugin` stays at `^5.11.0` (peers on `@lingui/core: 5`;
its 6.x line targets lingui 6).
**lingui 5.9.5 behavioral fallout handled here:**
- Translation functions now **throw without an active locale** (5.1.2
fell back silently). The global `i18n` singleton that backs server-side
`` t`…` `` calls only had a messages compiler set, never an activated
locale → activate the source locale in `I18nService.loadTranslations()`,
mirrored in the server jest setup (unit tests bypass Nest bootstrap).
- `msg`/`t` placeholders are now strictly typed (reject
`null`/`undefined`/`unknown`) → one server call site and 16 twenty-front
files adapted with minimal nullish-coalescing fixes that preserve
rendering.
- `.po`/compiled-catalog churn from the new extractor/compiler
(reference reordering, sorted keys — verified content-identical on
unchanged `.po` inputs) is intentionally not committed: the scheduled
i18n workflows regenerate those.
## typeorm status (pulled out)
typeorm 0.3.20 → 0.3.26 was originally in this PR but **made workspace
metadata sync intermittently lossy**: `example-app-postcard` failed
twice with a *different* field missing from the synced PostCard object
each run, and one integration shard's `DataSeedWorkspaceCommand` died
with "Could not find flat entity with universal identifier …" — versus
zero such failures on recent main. Local runs (db reset + seed, group-by
integration suite 19/19) pass, so it is a nondeterministic
CI-load-sensitive regression that needs dedicated debugging (typeorm
changed LIMIT/OFFSET 0 semantics, lazy count for `getManyAndCount`,
upsert WHERE construction, and topological-sort internals in that
range). The resolutions comment documents this as the blocker;
CVE-2025-60542 is MySQL-driver-only (`sqlstring`), so Postgres-only
Twenty is not exposed in the meantime.
## Verification
- `npx nx typecheck twenty-server` / `twenty-front` — clean (no cache)
- `npx nx test twenty-server` — full suite green
- `lingui:extract` + `lingui:compile` — clean for twenty-server /
twenty-emails / twenty-front
- `oxfmt --check` — clean for both packages
- Lockfile diff: lingui 5.9.5 entries, `esbuild@0.21.5` +
`@esbuild/*@0.21.5` platform packages removed, no typeorm changes
|
||
|
|
303c415dd1 |
fix(ai) - add logs + remove dashboard building (#21440)
- add logs for thread finishing without agent message - add logs to monitor toolCall token usage - remove dashboard building via AI (before fixing it) - fix Anthropic compute |
||
|
|
6c65ae8257 |
perf(twenty-front): stop Sentry Replay from re-serializing record-table mutations on navigation (#21381)
## Problem
Navigating between record-index pages (e.g. People ↔ Companies) blocks
the main thread for seconds, on every navigation, for ~every user.
Profiling pointed at **Sentry Session Replay(rrweb)**, not app code.
## Root cause
Swapping one record table for another produces a large DOM mutation
batch. rrweb serializes that batch **synchronously on the main thread**
(`_isParentRemoved` / mutation processing).
The built-in `mutationLimit` safety valve doesn't help: it's a *count*
threshold (default 10000), but our cost is *per-mutation serialization*
on a wide/deep table DOM — the batch is expensive, not numerous, so it
slips under the limit.
## Fix
```ts
replayIntegration({
_experiments: {
ignoreMutations: ['[id^="row-virtual-index-"]'],
},
}),
```
- ignoreMutations tells rrweb to drop mutation batches originating from
the virtualized row containers (StyledVirtualizedRowContainer, ids
row-virtual-index-N) — the source of the
table-swap churn. The table still appears in replays (initial snapshot;
text is already masked by default), its live row updates just aren't
re-serialized.
## Test
Measured locally
```
┌────────────────────────────────────────────────────────────┬───────────┬───────────────┐
│ │ Baseline │ With fix │
├────────────────────────────────────────────────────────────┼───────────┼───────────────┤
│ Replay/rrweb total │ 4,112 ms │ 188 ms (−95%) │
├────────────────────────────────────────────────────────────┼───────────┼───────────────┤
│ _isParentRemoved │ 2,195 ms │ 6 ms │
├────────────────────────────────────────────────────────────┼───────────┼───────────────┤
```
## Tradeoff
ignoreMutations tells rrweb to skip mutation batches coming from the
virtualized record-table rows, so session replays won't reflect live
changes inside the table — rows scrolling, cells updating, inline edits
will appear "frozen" at the last full snapshot. The table still shows in
the replay (initial render), and **its text is masked by default anyway,
so in practice we lose little**: the surrounding UI, navigation, clicks,
and interactions are all still recorded. The cost we're removing
(multi-second main-thread freeze on every navigation, for ~all users)
**far outweighs not seeing table row churn in replays** imho.
(@FelixMalfait @charlesBochet)
Two caveats worth noting: _experiments.ignoreMutations is an
experimental Sentry API, and it's batch-coarse, if a mutation batch
contains any matching element, the whole batch is dropped, so an
unrelated change occasionally batched with table mutations could be
missed. During navigation these batches are almost entirely table
mutations, so collateral is minimal.
If it ever proves insufficient, the reliable fallback is
`data-sentry-block` on the record-table body (which turns the table into
a placeholder box in replays).
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
|
||
|
|
55cbd3bfbf |
perf(ai): lazy-load agent chat runtime so it doesn't fetch/diff threads until opened (#21331)
## Problem On workspaces with a sizeable AI chat history, the whole app was freezing during navigation, including Settings (one navigation click measured ~6.5s). ## Root cause `AgentChatProvider` is mounted app-wide in `AppRouterProviders`, so its effects run on every page. On every render it would: 1. auto-select the most recently active thread (`AgentChatThreadInitializationEffect`), 2. fetch that thread's **full message history** (`AgentChatMessagesFetchEffect`), 3. run `AgentChatStreamingPartsDiffSyncEffect` → `updateStreamingPartsWithDiff`, which loops over every message doing `isDeeplyEqual(existing, incoming)` + `structuredClone`. A large thread would produce multi-second freeze on every interaction, app-wide. (Confirmed via a Chrome CPU profile) ## Fix Don't run the agent-chat **message runtime** until the chat is actually opened. ## Note There is still room for improvement, opening AI chats would still be very slow. |
||
|
|
c596a5e342 |
Rename twenty-ui to twenty-ui-deprecated and twenty-new-ui to twenty-ui to prepare package release (#21315)
## Description Promotes the next-gen UI library (formerly `twenty-new-ui`) to the name **`twenty-ui`** (v0.1.0, publishable) and renames the old package to **`twenty-ui-deprecated`**. Rewrites ~1,730 `twenty-ui` imports → `twenty-ui-deprecated`, updates all configs/CI/Docker/deps, and migrates twenty-front's `Toggle` to the new package (first consumer) as a drop-in. ## Next steps - Wire the `ui/v*` publish dispatch (`cd-deploy-tag.yaml` + `.yarnrc.yml`), then tag `ui/v0.1.0` to publish. - Continue migrating components from `twenty-ui-deprecated` → `twenty-ui`. |
||
|
|
15eaabdbc1 |
fix(ai) - optimize crud tools (#21133)
- **Add delete many**, `delete_many_{object}` added alongside the
existing `delete_one_{object}`.
- **Uniformize naming**, crud module, type names, and MCP helper
constants renamed for consistency.
- **Optimize tool schema (learn phase)**
- `find_many(_companies)`: **7 158 → 2 700 tokens**
- `find_one(_company)`: **280 → 126 tokens**
- ....
- Main mechanism: `reused: 'ref'` (line 7 of
`to-tool-json-schema.util.ts`). Zod walks the schema tree, tracks which
Zod schema instances appear more than once, and emits each reused
instance exactly once in `$defs`, replacing all subsequent occurrences
with a `$ref`. Works because filter and value schemas are now extracted
as shared objects.
- **Optimize system prompt (tool catalog)**, DATABASE_CRUD section
restructured to list operation patterns (`find_many_{object}`, …) once +
objects once, instead of the full N×M cross-product of tool names.
- **Optimize execute_tool**, shared record-properties schema (same
`$defs` deduplication applies at call time); introduced `upsert_many`;
added `selectedFields` to `find_*` so the agent only fetches the fields
it needs.
|
||
|
|
4b15b949f3 |
Provide additional logsobservability to workflow runs (per node) (#21142)
Surfaces per-step "Logs" tabs in the workflow run side panel so users can see what each step actually did (model + tokens + tool calls for AI, console output for serverless functions, request/response for HTTP, recipients/body for Email). <img width="546" height="501" alt="ai_agent_without_websearch" src="https://github.com/user-attachments/assets/c6ca3518-9489-4484-a570-3d0569ff3b03" /> ## Storage - New `stepLogs` JSONB column on the `workflowRun` workspace entity, typed as `Record<string, WorkflowRunStepLog>` (keyed by step id). - Schema lives in `twenty-shared`: `workflowRunStepLogSchema` with a discriminated `details.type` union for `AI_AGENT | CODE | HTTP_REQUEST | EMAIL` — frontends and backends consume the same Zod-inferred type. - Field is added to existing workspaces via a workspace upgrade command (`2-9 add-workflow-run-step-logs-field`); the standard-object metadata declares it for new workspaces. - Writes happen atomically per step in `WorkflowRunStepLogWorkspaceService.setStepLog` using `jsonb_set`. That lets concurrent steps in the same run write their own keys without contending with the existing lock around `workflowRun.state`. - Per-step payload is hard-capped at 256 KB; anything larger is dropped with a `logger.warn`, so a pathological tool call can never bloat a row. See below for more information. ## How logs are produced **Aalmost everything was already being collected; this PR mostly persists and renders it.** - **AI agent** — `AgentAsyncExecutorService` already tracked token usage, model id, native web-search count, and the AI SDK's `steps[]`. We map those into the log via `mapAiStepsToToolCallLogs` (`searchVector` stripped from record outputs, per-call input/output capped at 32/64 KB, max 200 tool calls per step). The only new measurement is a wall-clock `durationMs` taken around `executeAgent`, and we now fold native web-search cost into the displayed `totalCostInDollars` (it was already billed, just not shown). - **Code / serverless function** — reuses the `console.log` output the function runner already returns (`logsByLevel`); `build-code-step-log.util` only repackages it. - **HTTP request** — built from the action's existing input/output via `build-http-request-step-log.util`. No new signals collected. - **Email (send / draft)** — added `sanitizedHtmlBody` + `plainTextBody` to the existing tool outputs (a small additive change), then `build-email-step-log.util` consumes them. No additional AI inference or external calls are made for logging — the cost is a small CPU overhead per step plus the JSONB write. ## Security The log surface intentionally shows whatever the workflow touched, which made redaction and sanitization the main design concern. - **HTTP — secrets in headers**: existing `SENSITIVE_HEADER_NAMES` set (Authorization, Cookie, …) replaced with `[redacted]` in both request and response. - **HTTP — secrets in URLs**: `SENSITIVE_URL_PARAM_NAMES` (e.g. `api_key`, `token`, `access_token`) replaced in the query string via `URL`-based parsing. - **HTTP — secrets in bodies**: `SENSITIVE_BODY_KEY_REGEX` deep-walks JSON request/response bodies (object input or stringified JSON) and redacts matching keys. Applied to the `error` field too, since transport-layer errors sometimes embed structured payloads. - **Email — XSS risk in body preview**: tool outputs now expose a server-side `sanitizedHtmlBody`; the log builder prefers it over the raw user-authored `input.body`, with `plainTextBody` as a second fallback. The original raw body is only used if sanitization didn't happen (e.g. tool failed before composing). - **AI — internal/noisy data**: `searchVector` (Postgres tsvector strings) is stripped from record outputs returned by Twenty tools to avoid leaking internal full-text-search payloads. - **DB bloat / runaway agents**: 256 KB per-step cap + 32 KB / 64 KB per-tool-call input/output cap + 200 tool calls per step. <img width="547" height="307" alt="logic_function" src="https://github.com/user-attachments/assets/dd4a3d16-67f2-434b-95b3-bdcaf9ed053d" /> ## More details on Log size & truncation Logs are stored in `workflowRun.stepLogs` (JSONB), keyed by `stepId`. ### Per-step cap Each step's log is hard-capped at **256 KB** (`MAX_STEP_LOG_BYTES` in `WorkflowRunStepLogWorkspaceService.setStepLog`). For ~99% of workflows this is roomy — typical real-world sizes: - Code / serverless function: 1–20 KB - HTTP request: 5–70 KB - Email: 5–30 KB - AI agent (a handful of tool calls): 5–50 KB ### Two layers of bounding 1. **Per-field truncation** in each builder (before writing): - **Code**: ≤ 500 entries, ≤ 4 KB per message, ≤ 8 KB stack trace - **HTTP**: ≤ 32 KB per body (request + response), UTF-8 byte-aware - **Email**: ≤ 8 KB body preview, UTF-8 byte-aware - **AI agent**: ≤ 32 KB tool input, ≤ 64 KB tool output, ≤ 200 tool calls/step 2. **Global per-step safety net** at write time: if the assembled `stepLog` still exceeds 256 KB, the write is **dropped entirely** with a `logger.warn`. The workflow itself keeps running unaffected. ### What this means in practice - **Safe**: workflow execution, step results, downstream steps — never blocked by log size. - **Safe**: iterators (each iteration overwrites the previous log for that `stepId`, so they can't accumulate). - **Safe**: step retries (same `stepId` is overwritten, not appended). - **Possible**: an AI agent step with many large tool outputs (e.g., 50+ heavy `web_search` calls) can exceed 256 KB → the **entire** step's log is dropped, side panel shows "No logs were recorded for this step". The user has no explicit signal that the log was dropped due to size (only server-side warn). - **Possible** (theoretical): a workflow with hundreds of distinct steps could push the row toward Postgres's internal ~256 MB jsonb limit. Beyond that, individual `jsonb_set` writes would error and be swallowed by the action's try/catch — workflow still completes. ### Possible future hardening (not in this PR) - Replace "drop entire log" with a stub that preserves the summary card (cost, duration, status) and marks `truncated.reason = 'size_cap'`. - Surface size-drops in the UI (similar to the existing `<StyledTruncatedNotice>`). - Emit a metric so dropped logs are observable in dashboards. |
||
|
|
b338a7a1d2 |
feat(settings): discovery hero rollout + ephemeral playground token (#21072)
## Summary
Two intertwined streams of work:
### UI — discovery hero pattern, settings shell, AI/API redesign
- **Generalize `SettingsDiscoveryHeroCard`** and use it on Layout, Data
Model, Apps, AI, API/Webhooks, Members. Drops 4 per-page wrapper files
(`SettingsObjectCoverImage`, `SettingsLayoutCoverImage`,
`SettingsLayoutCustomizeVideoModal`,
`SettingsDataModelVisualizeVideoModal`). Each page now supplies cover
src, modal id, and tab list.
- **Modal**: swap `<video>` placeholder for the Vimeo iframe pattern
from `twenty-docs`, per-tab `vimeoId`. Drop the parallel border-bottom
on the header (TabList draws its own baseline) and the grey background
behind the video. Note: Vimeo's embed allowlist applies — the iframes
load with the correct URL on `localhost` but the player itself requires
the video owner to allow the dev/staging domains in Vimeo settings.
- **AI page** rebuilt into a Cockpit pattern (Overview / Models / Skills
/ Tools / Usage). New `SettingsAiOverviewTab` with default Smart/Fast
pickers, at-a-glance stats, and an MCP signpost that deep-links to
`/settings/api-webhooks#mcp`. System Prompt link moved under Models.
Advanced tab removed.
- **API & Webhooks** now has 4 tabs (Playground / MCP / API Keys /
Webhooks). Hero card above tabs. Playground tab inverted to "Core API" /
"Metadata API" sections, each containing REST + GraphQL cards — schema
is the meaningful axis, protocol is secondary. Hash deep-link sync
delegated to the shared `TabListFromUrlOptionalEffect`.
- **Settings shell**: unified drawer outer padding (kill `isSettings`
branch), extract `CollapsibleNavigationDrawerSection`, add `iconColor`
on settings nav items, fix Exit Settings button alignment, 880px content
cap.
### Backend — strategy C: ephemeral playground token
The legacy paste-your-API-key flow is replaced by an on-demand
short-lived token scoped to the calling user's permissions. No shared
"Playground" API key to manage or revoke.
- New `JwtTokenTypeEnum.PLAYGROUND`. `PlaygroundTokenJwtPayload =
Omit<AccessTokenJwtPayload, 'type' | impersonation fields>` so any
future ACCESS claim flows through automatically.
- `AccessTokenService.generatePlaygroundToken` signs an access-shaped
JWT with `type: PLAYGROUND` and a configurable short TTL. A shared
private `resolveTokenSubject` helper parallelizes the user / workspace /
userWorkspace lookups for both generators.
- `JwtAuthStrategy.validateAccessToken` widened to accept
`AccessTokenJwtPayload | PlaygroundTokenJwtPayload`; impersonation gated
on `payload.type === ACCESS` so the union narrows without `as unknown
as` casts. The two branches in `validate()` collapse into one.
- New `PLAYGROUND_TOKEN_EXPIRES_IN` config var (default `2h`).
- New `generatePlaygroundToken` mutation (`WorkspaceAuthGuard`, no args,
returns `AuthToken`).
- Frontend `useOpenPlayground` hook centralizes mint → atom write →
navigate, with Apollo `onError` snackbar and a "use cached PLAYGROUND
token if still fresh" short-circuit (decodes via `jwt-decode`, checks
both `type` AND `exp`). Old API_KEY tokens left in localStorage from the
prior paste-form flow are rejected on `type` alone and force a re-mint —
this is what was causing the "This API Key is revoked" symptom on stale
browsers.
### Drive-by cleanups
- `PlaygroundToken` DTO removed (identical shape to `AuthToken` already
in use).
- 5 `customize-sidebar.webm` imports and the dead placeholder pipeline
removed.
## Test plan
### Discovery hero
- [ ] `/settings/layout`, `/settings/data-model`,
`/settings/applications`, `/settings/ai`, `/settings/api-webhooks`,
`/settings/members` each render the discovery hero card with its
illustration + play button + tabbed modal
- [ ] Modal tabs show the correct Vimeo embed URL per tab; aspect ratio
stays at 1440/900; no parallel border-bottom jog at the tab baseline
- [ ] AI Overview tab shows Smart/Fast model pickers + stats grid + MCP
signpost card; the MCP card lands on `/settings/api-webhooks#mcp` with
the MCP tab active
### API playground (ephemeral token)
- [ ] With an empty `playgroundApiKeyState` in localStorage, clicking
REST or GraphQL playground card opens the playground and the cached
token has `type: "PLAYGROUND"` with ~2h exp
- [ ] Clicking the card again within the freshness window does **not**
re-mint (`iat` / fingerprint stable across visits)
- [ ] Planting a fake API_KEY-shaped JWT in localStorage and clicking
the card forces a fresh mint (old token rejected on `type`)
- [ ] `GET /rest/companies?limit=1` with the cached token returns 200 +
real data
- [ ] `POST /graphql { __typename }` returns 200
### Settings shell
- [ ] Settings nav matches main app drawer padding; sections collapse;
Exit Settings button aligns with the workspace links above
- [ ] Active nav items have a right-gap (cleaner active state)
- [ ] Content area capped at 880px
### Verify
- [ ] `npx nx typecheck twenty-front` passes
- [ ] `npx nx typecheck twenty-server` passes
- [ ] `npx nx lint:diff-with-main twenty-front` passes
- [ ] `npx nx lint:diff-with-main twenty-server` passes
|
||
|
|
996cdaf3ff |
refactor(agents): split tool resolution into native and action rails (#20331)
## Summary
Splits AI agent tool resolution into two independent rails:
- **Native tools** — capabilities baked into the model SDK
(Anthropic/OpenAI `web_search`, xAI `web`/`x` provider options). Bound
by `NativeToolBinderService`, controlled by per-agent
`modelConfiguration` toggles. Opaque to Twenty — executed on the model
provider's servers.
- **Action tools** — registry-scoped tools from `ToolRegistryService`
(code interpreter, send email, record CRUD, etc.). Permission-gated via
the agent's role. Executed on Twenty's server.
Both rails merge into a single `ToolSet` at call time. When both
surfaces expose a search tool the model picks at runtime — coexistence
is intentional (relevant once Exa returns as an action, see below).
## Notable changes worth calling out
**Contract change: `AgentAsyncExecutorService.executeAgent` no longer
accepts `rolePermissionConfig`.** Workflow agents now scope exclusively
by the agent's own permission-tab role (`unionOf: [agentRoleId]`). The
previous role-merging path (caller role intersected with agent role) is
removed. No agent role → no registry tools (fail-closed by design).
**`NativeToolBinderService` relocated** from
`core-modules/tool-provider/native/` →
`metadata-modules/ai/ai-models/services/`. The binder needs SDK-package
knowledge, which lives in `ai-models`. Old location created a backwards
module dependency.
**`NATIVE_MODEL_TOOLS_BY_SDK_PACKAGE` is exhaustive over
`AiSdkPackage`** (`Record<>`, not `Partial<Record<>>`). Adding a new SDK
without thinking about native tools now fails the build. SDKs without
native tools (Bedrock, Google, Mistral, Azure, OpenAI-compatible) get
explicit `{}` entries.
**Discriminated union `kind: 'sdk-tool' | 'provider-option'`** lets one
registry describe both function tools (Anthropic/OpenAI) and runtime
sources (xAI). Follows the local `tool-provider` convention from #19321.
## Deferred to follow-ups
- **Exa web search is dropped from this PR** (along with its
`WEB_SEARCH_TOOL` permission flag and the Exa-specific gating). Exa
comes back as an **action/app tool** once apps can define permission
flags through the SDK — ongoing work in #20481.
- **xAI native search currently errors.** xAI deprecated its Live Search
API (the `web`/`x` provider-option sources this rail maps to), so xAI
returns `410` when native search is actually exercised. The code path
itself is clear — it's only hit if you test xAI native tools. Fixed
separately alongside the broader xAI model fixes.
## Conscious non-decisions
- **No "twenty-native" category.** `native` is reserved for
model/provider SDK features; everything Twenty-owned is just a
tool/action.
- **Coexistence over precedence.** No rule forcing an action search tool
to override native search (or vice-versa) — when both exist, it's the
user's choice in workflow agents and the model's choice in chat.
---------
Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
Co-authored-by: Félix Malfait <felix@twenty.com>
|
||
|
|
865ca697ca |
Fix AI permission gating: use Ask AI for chat UI, AI Settings for admin endpoints (#21030)
## Summary Closes #20662. Two AI permission flags exist: - **`AI`** (label "Ask AI") — user-facing: chat with AI agents, use AI features - **`AI_SETTINGS`** (label "AI") — admin: create and configure AI agents After auditing every use of these flags I found: ### Frontend — chat UI gated by the admin permission (user-facing bug from the issue) A user granted only `Ask AI` could not see chat tabs, the "new chat" button (desktop & mobile), or the chat content pane; thread initialization was also skipped, leaving the chat in a half-initialized state and producing intermittent `THREAD_NOT_FOUND` errors. Switched these to `AI`: - `MainNavigationDrawerTabsRow.tsx` - `MainNavigationDrawer.tsx` - `MobileNavigationBar.tsx` - `AgentChatThreadInitializationEffect.tsx` ### Backend — admin-only resolvers gated by the user permission (privilege escalation) Two resolvers had a class-level guard of `AI`, letting any user with the user-facing flag reach admin endpoints (skill CRUD, eval runs). Switched the class-level guards to `AI_SETTINGS`: - `SkillResolver` — create/update/delete/activate/deactivate skills - `AgentTurnResolver` — read turns, run/grade evaluations ### Left as-is (already correct) - `AgentResolver` — class-level `AI` for reads (workflow editors and admin pages both need them), mutation-level `AI_SETTINGS` overrides for writes - `AgentChatResolver` & `AgentChatSubscriptionResolver` — already `AI` - `AiGenerateTextController` — already `AI` - Workspace AI config fields in `workspace.service.ts` — already `AI_SETTINGS` ## Test plan - [ ] As a user with `Ask AI` only (no `AI_SETTINGS`): chat tabs, "new chat" button, and chat history pane are visible on desktop + mobile; sending a message works; no `THREAD_NOT_FOUND` errors - [ ] As a user with `AI_SETTINGS` but no `Ask AI`: chat UI is hidden - [ ] As a user with `Ask AI` only: calling `skills` / `createSkill` / `agentTurns` / `runEvaluationInput` via GraphQL returns permission denied - [ ] As an admin (`AI_SETTINGS`): skill settings and agent eval pages still work |
||
|
|
2fcf3e3c2b |
fix(ai-chat) - fix browser context injection (#20809)
Move the browsing context out of the system prompt and injecting it directly into the last user message instead. Previously (before this PR), browsing context change, update system prompt then break whole conversation history ... and caching. Now, browsing context is sent with last message only if changed. "Benchmark" this PR vs main : - same conv with 3-4 turns - 60% -> 85% cache ratio || 0.31 credits -> 0.13 |
||
|
|
89579f5225 |
fix(ai-chat) - upload files (#20681)
closes https://github.com/twentyhq/twenty/issues/20437 bonus : persist file filename for UI display |
||
|
|
fcd2d586ee |
chore(billing) - remove feature flag (#20531)
- remove feature flag - remove old enforce cap usage logic |
||
|
|
aecfe699f4 |
feat(ai-chat) - Stop ai thinking if credits exhausted (#20526)
Billing is now decremented per-step, not per-turn. The onStepFinish callback in chat-execution.service.ts calls a new decrementAndCheckAvailableCredits method on each model step, so Redis is debited incrementally as the agent runs rather than all at once at the end. Credit exhaustion stops the stream mid-run. When a step depletes the remaining credits, a hasNoMoreAvailableCredits flag is set and passed into the stopWhen predicate of streamText, causing the agent to halt before starting the next step. A new credits-exhausted event is introduced. After the stream drains and the response is persisted, if credits ran out the job publishes a dedicated credits-exhausted event to the frontend instead of the normal message-persisted event. The frontend handles this new event. useAgentChatSubscription has a new credits-exhausted case that sets a BILLING_CREDITS_EXHAUSTED-coded error on the atom, closes the writer, and stops the streaming state — triggering the existing AiChatCreditsExhaustedMessage UI. |
||
|
|
5c1fe45760 |
fix: update broken AI documentation link (#20401)
## Summary Updated the broken AI documentation link in `AiChatApiKeyNotConfiguredMessage.tsx`. ## Changes * Replaced outdated self-hosting AI docs URL * Updated link to a valid self-hosting documentation page Fixes #20071 --------- Co-authored-by: Charles Bochet <charles@twenty.com> |
||
|
|
524e5d8cf7 |
fix: scroll AI chat to bottom on side panel reopen (#20413)
## Summary - Fix: when reopening the AI chat side panel, users landed at the top of the conversation and had to scroll down to find the latest messages - Root cause: the side panel fully unmounts on close ([SidePanelForDesktop.tsx](packages/twenty-front/src/modules/side-panel/components/SidePanelForDesktop.tsx) clears `shouldShowContent` after the close transition), so on reopen the scroll wrapper is recreated with `scrollTop = 0`. The existing initial-scroll-to-bottom only fires on thread change, but the displayed-thread atom outlives the unmount, so no thread change is detected on a remount and the scroll-to-bottom never runs - Fix: add a tiny `AgentChatScrollToBottomOnMountLayoutEffect` rendered inside the message list that calls `scrollAiChatToBottom()` directly in `useLayoutEffect`. Because the parent returns `null` when there are no messages, the mount only fires when there is content to scroll past ## Why direct scroll, not the existing flag `agentChatIsInitialScrollPendingOnThreadChangeState` is paired with a `MutationObserver` settle that only clears the flag once the subtree is quiet for 150 ms. During a live stream the message subtree mutates on every token, the settle resets indefinitely and `visibility: hidden` never lifts. The thread-change handler avoids this because it is gated on `!agentChatIsStreaming` ([AgentChatStreamSubscriptionEffect.tsx:78-79](packages/twenty-front/src/modules/ai/components/AgentChatStreamSubscriptionEffect.tsx)), but a mount can happen at any time, including mid-stream. Scrolling the DOM directly in `useLayoutEffect` runs synchronously between commit and paint, so the user sees the bottom on the first paint with no flash and no settle dependency. ## Tradeoff A user who scrolled up to read history and then closes/reopens the panel will land back at the bottom instead of where they were. Standard chat UX (Slack, ChatGPT, iMessage); preserving per-thread scroll position would need a new atom and is left out of scope. ## Test plan - [ ] Open AI chat with messages, close the side panel, reopen → lands at the bottom (latest messages visible) - [ ] Reopen the side panel **mid-stream** → lands at the bottom and continues to follow new tokens (chat is not hidden) - [ ] Switch between threads → existing thread-change scroll still works (no double-scroll, no regression) - [ ] Open AI chat with no messages → no flash, no errors - [ ] Rapidly close/reopen the panel a few times → each reopen lands at the bottom --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
9fc5be1c4c |
Billing - Migrate from Stripe metering (#20298)
**Overall strategy** **1. Introduce “Billing V2” behind a workspace flag** Gate the new model with FeatureFlagKey.IS_BILLING_V2_ENABLED so existing workspaces stay on the old behavior until they’re migrated or explicitly on V2. **2. Replace workflow metered SKUs with a resource-credit product** Conceptually, billable “workflow execution” usage is not the primary subscription line item anymore. Add a RESOURCE_CREDIT product (and keep WORKFLOW_NODE_EXECUTION as deprecated for the transition). Usage and limits are expressed through credit buckets (e.g. price metadata like credit_amount), so one product can represent pooled credits instead of a narrow workflow-only meter. **3. Migrate subscriptions in two layers** Schema/catalog: persist extra price metadata (instance upgrade) so the server knows credit amounts and can match Stripe prices to the new model. Per workspace: the registered workspace command upgrade:2-2:migrate-to-billing-v2 finds subscriptions that still have WORKFLOW_NODE_EXECUTION, swaps those items to the right RESOURCE_CREDIT prices (using existing Stripe schedule + BillingSubscriptionUpdateService stack), then treats the workspace as V2 (flag). Workspaces without that legacy item or without a subscription are skipped. **4. Unify subscription lifecycle + usage on the server** **5. Refresh the product surface in Settings** Test : - [x] Subscribe v1 + Update subscribe + Migrate - [x] Subscribe v2 + Update subscribe |
||
|
|
ff65b5001d | fix: show AI chat filter button only on hover in navigation drawer (#20274) | ||
|
|
e1828b6f41 |
[AI] Add thread actions, filters, and archive support (#20068)
## PR Description ### Summary - Add AI chat thread actions: rename, archive (soft-delete via `deletedAt`), and hard-delete with confirmation. - Add chat thread filtering by status (active/archived/all), group-by mode, and last activity. - Rework drawer/side-panel thread lists to share thread sections, item menus, archive icons, and empty-state behavior. - Extend server chat thread model/API with `deletedAt`, mutations, broadcasts, and archive-aware stream guards. ### Decisions - Two-stage lifecycle: Archive sets `deletedAt` (soft); Delete is a separate action on archived threads that hard-deletes the row. Aligns with Twenty's soft-delete convention (Felix's suggestion). - `lastMessageAt` is derived from `MAX(agentMessage.createdAt)` on read, not stored. List query does inline aggregation for sort; `@ResolveField` covers single-thread / mutation paths so the schema contract is honest everywhere. Matches `timeline-messaging.service.ts` precedent and the existing `totalInputCredits` / `totalOutputCredits` `@ResolveField` pattern in the same resolver. - Replaced auto-CRUD `chatThreads` (cursor-paginated Connection) with a custom `[AgentChatThreadDTO!]` resolver. Frontend metadata-store treats threads as a flat collection and filters/sorts client-side, so cursor pagination was performative. - Sending in an archived chat unarchives it optimistically on the client and authoritatively on the server. - Grouping and last-activity filtering use `lastMessageAt ?? updatedAt` so archive/rename don't bump threads in the list. - Kept metadata-store core API unchanged; AI chat uses the same local cast pattern already used by other metadata-store partial updates. https://github.com/user-attachments/assets/1b179b7b-1a2a-4a7a-aa0a-c88f6f051a87 |
||
|
|
480e5796ec |
fix(ai): render record links inside markdown headings in AI chat (#20074)
## Summary Adds `h1`–`h6` component overrides to `LazyMarkdownRenderer` so that `[[record:...]]` references placed inside markdown headings in the AI chat are parsed by `processChildrenForRecordLinks` and rendered as clickable `RecordLink` chips, matching the behavior already in place for `p`, `li`, `td`, `th`, and `a`. Fixes #20072 ## Test plan - [ ] In AI chat, ask a question whose answer places a record reference inside a markdown heading (e.g. `## Found [[person:uuid:John Doe]]`) and confirm a clickable `RecordLink` chip renders instead of the raw `[[...]]` text. - [ ] Verify heading styling (sizes, weights, margins) is unchanged. Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: nitin <ehconitin@users.noreply.github.com> |
||
|
|
251f5deab6 |
[breaking, deploy server first] fix(ai-chat): persist providerExecuted flag on tool parts (#20030)
## Summary Fixes Sentry errors of the form: > \`messages.3: \`tool_use\` ids were found without \`tool_result\` blocks immediately after: srvtoolu_…. Each \`tool_use\` block must have a corresponding \`tool_result\` block in the next message.\` ### Root cause When the model invokes a **provider-hosted tool** (e.g. Anthropic's native \`web_search\` — note the \`srvtoolu_\` ID prefix), the AI SDK marks the resulting \`UIMessagePart\` with \`providerExecuted: true\`. \`convertToModelMessages\` uses that flag to emit the tool_use/tool_result pair *inside the same assistant message* — the format Anthropic requires for server-side tools. Our \`AgentMessagePart\` persistence was dropping \`providerExecuted\` on the way to the DB (and re-hydration didn't know to set it). On the next turn, \`convertToModelMessages\` treated the rehydrated part as a client-side tool call, splitting it into \`assistant(tool_use)\` + \`user(tool_result)\` — which Anthropic then rejects with the error above. ### Fix - Add nullable \`providerExecuted BOOLEAN\` column on \`core.agentMessagePart\` via a fast instance command. - Surface the field on \`AgentMessagePartDTO\` (GraphQL). - Preserve it through \`mapUIMessagePartsToDBParts\` (server) and both \`mapDBPartToUIMessagePart\` mappers (server + frontend). - Include it in \`GET_CHAT_MESSAGES\` and \`GET_AGENT_TURNS\` selections. - Regenerate \`generated-metadata/graphql.ts\`. ### Backwards compatibility Existing rows have \`NULL providerExecuted\` and round-trip as the omitted flag — which is exactly the pre-fix behaviour for tool parts that were never provider-executed. Only *new* assistant messages using \`web_search\` (or other provider-hosted tools) will write \`true\`, and those are the only ones that were breaking. ## Test plan - [x] \`npx tsgo\` typecheck — server + front clean - [x] \`oxlint\` + \`prettier --check\` on all touched files — clean - [x] \`npx nx run twenty-server:database:migrate:prod\` runs the new instance command locally; \`providerExecuted\` column present on \`core.agentMessagePart\` - [x] Regenerated \`generated-metadata/graphql.ts\` — \`providerExecuted\` wired into both queries and \`AgentMessagePart\` type - [ ] Manual: start a chat with Anthropic web_search enabled, invoke the tool in turn 1, reply in turn 2 — should not throw the srvtoolu error 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4f938aa097 |
feat(app): infrastructure for pre-installed apps (#19973)
**PR 1 of 2.** Follow-up PR ships the Exa app, sets it as a default
pre-installed app, and removes the current `WebSearchTool` /
`WebSearchService` / `ExaDriver`. This PR adds the plumbing; no
user-visible change yet.
## Summary
- Server admins can declare a list of npm app packages to auto-install
on every new workspace and backfill onto existing workspaces via CLI.
- Server-level secrets (like Exa's API key) live on the
`ApplicationRegistration` (one row per server, encrypted) and are
injected into logic function execution env at runtime. No more
per-workspace storage of global secrets.
- A generic `POST /app/billing/charge` endpoint lets app logic functions
emit workspace usage events for metered features. Exa uses it in PR 2;
future apps (call recorder, etc.) reuse it.
- `LogicFunctionToolProvider` tool name prefix changes `logic_function_`
→ `app_`. Shorter, accurate (they come from installed apps).
## What's in this PR
**Logic function executor — server-level variables**
- `LogicFunctionExecutorService.getExecutionEnvVariables` now resolves
env vars in the order: hardcoded defaults →
`ApplicationRegistrationVariable[]` (server-level) →
`ApplicationVariable[]` (workspace-level override). The manifest
`serverVariables` schema has existed; this closes the loop.
**Config**
- `PRE_INSTALLED_APPS` — comma-separated list of npm packages. Default:
empty.
**\`PreInstalledAppsService\`** (new module)
- \`onApplicationBootstrap()\` — fetches each package's manifest from
the app registry CDN, upserts an \`ApplicationRegistration\`, and seeds
declared \`serverVariables\` from matching env vars (e.g.
\`EXA_API_KEY\` env → encrypted registration variable).
- \`installOnWorkspace(workspaceId)\` — installs all pre-installed apps
on a single workspace. Tolerates per-app failures.
**Auto-install on new workspace activation**
- \`WorkspaceService.prefillCreatedWorkspaceRecords\` invokes
\`installOnWorkspace\` after prefilling standard records. Non-blocking
on failure.
**Backfill CLI command**
- \`install-pre-installed-apps\` — iterates active and suspended
workspaces, installs pre-installed apps that aren't yet installed.
Idempotent. Run after changing \`PRE_INSTALLED_APPS\`.
**App billing endpoint**
- \`POST /app/billing/charge\`. Authenticated via \`APPLICATION_ACCESS\`
token (already injected into logic function execution env as
\`DEFAULT_APP_ACCESS_TOKEN\`). Body: \`{ creditsUsedMicro, quantity,
unit, operationType, resourceContext? }\`. Emits \`USAGE_RECORDED\` with
\`applicationId\` as \`resourceId\`. Generic — reusable by any app.
**Tool name prefix**
- \`LogicFunctionToolProvider.buildLogicFunctionToolName\` now produces
\`app_<name>\` instead of \`logic_function_<name>\`. Only affects tools
sourced from logic functions; other tool providers unchanged.
## Stats
- 16 files, +501 / −2
- 7 new files (1 command, 1 service × 2, 1 controller, 1 DTO, 2 modules)
- Typecheck: 7 pre-existing errors, zero new
- Prettier clean
## Behavior deltas
- **\`PRE_INSTALLED_APPS\` default = empty**: existing servers see no
change on merge.
- **\`ApplicationRegistrationVariable\` is now read by the executor**:
apps that were using manifest \`serverVariables\` but expecting them to
be ignored by the executor will now see them injected. No apps ship with
\`isTool: true\` logic functions today, so this is latent — first
consumer is Exa in PR 2.
- **Tool prefix**: currently no logic-function tools are named
\`logic_function_*\` in any production flow. The prefix change affects
only future tools emitted by \`LogicFunctionToolProvider\`.
## Risks
- **CDN unavailability at startup**: if the app registry CDN is down,
\`ensureRegistrationsExist\` logs warnings but doesn't block server
start. Installation on new workspaces during this window will find no
registrations and log a non-blocking error. Backfill command can retry
after CDN recovers.
- **Cold-start overhead**: \`ensureRegistrationsExist\` is called once
per process on bootstrap. Current configurable default is empty, so zero
overhead. When an admin sets \`PRE_INSTALLED_APPS\`, they accept one
HTTP call per package at boot.
- **Server-level variables flow**:
\`ApplicationRegistrationVariable.encryptedValue\` is shared by all
workspaces of a server. Appropriate for a single-tenant Exa key. Not
appropriate for per-tenant keys — those go in workspace-level
\`ApplicationVariable\` and override.
## Test plan
- [ ] \`npx nx typecheck twenty-server\` passes (verified: 7
pre-existing unrelated errors, zero new)
- [ ] Set \`PRE_INSTALLED_APPS=@twenty-apps/hello-world\` (or any real
npm-published app), \`HELLO_WORLD_API_KEY=xxx\`, restart server:
\`ApplicationRegistration\` row is upserted,
\`ApplicationRegistrationVariable\` for HELLO_WORLD_API_KEY is populated
(encrypted).
- [ ] Create a new workspace: the app is auto-installed,
\`ApplicationEntity\` row created, \`LogicFunctionEntity\` rows created.
- [ ] Existing workspace: run \`yarn nx run twenty-server:command
install-pre-installed-apps\`: apps install across all workspaces,
idempotent on re-run.
- [ ] Trigger a logic function that reads
\`process.env.HELLO_WORLD_API_KEY\`: value resolves from the
server-level \`ApplicationRegistrationVariable\`.
- [ ] Log a charge from the handler: \`POST /app/billing/charge\` with
\`Authorization: Bearer \$DEFAULT_APP_ACCESS_TOKEN\` body
\`{creditsUsedMicro: 1000, quantity: 1, unit: "INVOCATION",
operationType: "WEB_SEARCH"}\` → returns \`{success: true}\`,
\`USAGE_RECORDED\` event emitted with correct
\`resourceId=applicationId\`.
- [ ] Tool name generated by \`LogicFunctionToolProvider\` starts with
\`app_\`.
## What's NOT in this PR (PR 2 scope)
- The Exa app itself (\`packages/twenty-apps/...\` directory)
- Removing \`WebSearchTool\`, \`WebSearchService\`, \`ExaDriver\`,
\`web-search\` module
- Removing \`WEB_SEARCH_DRIVER\` config var
- Removing the current \`exa_web_search\` entry in
\`ActionToolProvider\`
- Chat preload list updated to \`app_exa_web_search\`
- Frontend \`getToolDisplayMessage\` branch for \`app_exa_web_search\`
- Setting \`PRE_INSTALLED_APPS\` default to include \`@twenty-apps/exa\`
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
0c929e7903 |
refactor(tool-provider): rename web_search to exa_web_search, drop XOR toggle (#19969)
## Summary
- Today `WEB_SEARCH_PREFER_NATIVE` forces a **mutual exclusion**: either
the custom Exa tool preloads as `web_search` or the SDK-native
`web_search` binds. Same name, different backends.
- This PR lets them **coexist**. Custom Exa becomes `exa_web_search`;
native keeps `web_search`. The model picks based on tool descriptions.
- `WEB_SEARCH_PREFER_NATIVE` and `shouldUseNativeSearch()` are deleted.
Exa enablement follows `WEB_SEARCH_DRIVER` (existing). Native enablement
follows the agent's `modelConfiguration.webSearch.enabled` (existing).
## Key changes
**Config / service**
- Deleted `WEB_SEARCH_PREFER_NATIVE` (config-variables.ts)
- Deleted `WebSearchService.shouldUseNativeSearch()`
- `WebSearchService.isEnabled()` unchanged — still gates Exa
availability
**Custom tool rename**
- `ActionToolProvider.toolMap`: `'web_search'` → `'exa_web_search'`
- Descriptor name matches
- `WebSearchTool.description` rewritten to position Exa as
structured/entity-aware, complementary to native
**Native tool binder**
- `NativeToolBinder.bind()` drops the `shouldUseNativeSearch` gate.
Per-agent `modelConfiguration.webSearch.enabled` (inside
`getNativeModelTools`) stays authoritative.
**Chat**
- Preload list now always includes `exa_web_search` —
`ActionToolProvider` silently skips the descriptor when Exa is disabled,
so `getToolsByName` degrades gracefully
- Native tools always attempted; returns empty ToolSet when the model
doesn't support them
- `directTools = { ...preloadedTools, ...nativeSearchTools }` — both
present when both enabled
- `billNativeWebSearchUsage` called unconditionally (the function
already short-circuits on count ≤ 0)
**Workflow agent**
- Same unconditional billing pattern
- `WebSearchService` dependency removed
**System prompt**
- Dropped the special-cased `web_search` branch. Preloaded tools list
uniformly now.
**Frontend**
- `exa_web_search` reuses the same "Searching the web for X" display as
native
- Test coverage added
## Billing isolation (verified)
- `countNativeWebSearchCallsFromSteps` counts `toolName ===
'web_search'` only. After the rename, only native calls match. Exa calls
(`exa_web_search`) are billed separately via
`WebSearchService.emitUsageEvent` inside `search()`.
- No double-billing path.
## Behavior deltas (intended)
| Scenario | Before | After |
|---|---|---|
| Anthropic model + Exa enabled + PREFER_NATIVE=true | native only |
**both** |
| Anthropic + Exa enabled + PREFER_NATIVE=false | Exa only (as
`web_search`) | **both** |
| Non-native model + Exa enabled | Exa as `web_search` | Exa as
`exa_web_search` |
| Any model + Exa disabled + native supported | native only | native
only |
| Workflow agent with `webSearch.enabled=true` + Anthropic + Exa enabled
| native only | **both** |
## Known regression (accepted)
Customers who set `WEB_SEARCH_PREFER_NATIVE=false` to force Exa-only
will now **also** see native `web_search` if the model supports it.
There's no chat-level kill switch after this PR. Per discussion, this is
accepted — future model-level capability gating (in the model JSON) will
be the right place for that control.
## Stats
- 10 files, +63 / −73 (net deletion)
- Typecheck clean (server: 7 pre-existing unrelated, front: 13
pre-existing unrelated — zero new either side)
- Prettier clean
## Test plan
- [ ] `npx nx typecheck twenty-server` and `npx nx typecheck
twenty-front` pass
- [ ] With Anthropic + Exa enabled: chat shows both `web_search` and
`exa_web_search` in preloaded list; model can call either
- [ ] With Anthropic + Exa disabled: chat shows only native `web_search`
- [ ] With non-native model + Exa enabled: chat shows only
`exa_web_search`
- [ ] Workflow agent with `modelConfiguration.webSearch.enabled=true` +
Exa enabled: both available
- [ ] Billing: native calls billed via `billNativeWebSearchUsage`; Exa
calls billed via `WebSearchService.emitUsageEvent`; no double-billing
- [ ] Frontend: `exa_web_search` renders "Searching the web for X" the
same as `web_search`
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
30b8663a74 |
chore: remove IS_AI_ENABLED feature flag (#19916)
## Summary - AI is now GA, so the public/lab `IS_AI_ENABLED` flag is removed from `FeatureFlagKey`, the public flag catalog, and the dev seeder. - Drops every backend `@RequireFeatureFlag(IS_AI_ENABLED)` guard (agent, agent chat, chat subscription, role-to-agent assignment, workflow AI step creation) and the now-unused `FeatureFlagModule`/`FeatureFlagGuard` wiring in the AI and workflow modules. - Removes frontend gating from settings nav, role permissions/assignment/applicability, command menu hotkeys, side panel, mobile/drawer nav, and the agent chat provider so AI UI is always on. Tests and generated GraphQL/SDK schemas updated accordingly. ## Test plan - [x] `npx nx typecheck twenty-shared` - [x] `npx nx typecheck twenty-server` - [x] `npx nx typecheck twenty-front` - [x] `npx nx lint:diff-with-main twenty-server` - [x] `npx nx lint:diff-with-main twenty-front` - [x] `npx jest --config=packages/twenty-server/jest.config.mjs feature-flag` - [x] `npx jest --config=packages/twenty-server/jest.config.mjs workspace-entity-manager` - [ ] Manual smoke test: AI features still accessible without any flag row in `featureFlag` 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
13c4a71594 |
fix(ui): make CardPicker hover cover the whole card and align content left (#19884)
## Summary Two small visual issues with the shared `CardPicker` (used in the Enterprise plan modal and the onboarding plan picker): - Labels like \`Monthly\` / \`Yearly\` were center-aligned inside their cards while the subtitle (\`\$25 / seat / month\`) stayed left-aligned, because the underlying \`<button>\` element's default \`text-align: center\` was leaking into the children. - The hover background was painted on the same element that owned the inner padding, so the hover surface didn't visually feel like the whole card. This PR: - Moves the content padding into a new \`StyledCardInner\` so the outer \`<button>\` is just the card chrome (border + radius + background + hover). - Adds \`text-align: left\` so titles align with their subtitles. - Hoists \`cursor: pointer\` out of \`:hover\` (it should be on by default for the card). Affects: - \`EnterprisePlanModal\` (Settings → Enterprise) - \`ChooseYourPlanContent\` (onboarding trial picker) |
||
|
|
e68842c268 |
Billing - fixes (#19867)
- Uniformize credit formating : In UI, 1$=1credit. In BE 1 UI credit = 1_000_000 BE "crédits" - Add crédit rollover information + Link to documentation + Documentation update <img width="291" height="317" alt="Screenshot 2026-04-17 at 18 22 59" src="https://github.com/user-attachments/assets/2519fb9f-159d-4c85-95f4-a6e005a8a1a3" /> <img width="848" height="763" alt="Screenshot 2026-04-17 at 14 12 20" src="https://github.com/user-attachments/assets/a3cc0874-f275-49ea-819f-305ec314bdfe" /> <img width="797" height="757" alt="Screenshot 2026-04-17 at 14 12 13" src="https://github.com/user-attachments/assets/9048409b-d5a2-435a-b735-70370705e668" /> - Enable direct top-up (or subscription if in trial) from AI chat <img width="333" height="215" alt="Screenshot 2026-04-17 at 22 52 00" src="https://github.com/user-attachments/assets/7a20c627-2806-4bcf-a037-b45752232be9" /> <img width="457" height="769" alt="Screenshot 2026-04-17 at 22 51 41" src="https://github.com/user-attachments/assets/d2a90c1b-271f-4fe9-8891-baeb2fabb86d" /> - Inform users if credit limit is reached - Banner <img width="1130" height="127" alt="Screenshot 2026-04-17 at 19 15 11" src="https://github.com/user-attachments/assets/30723e5e-c07e-462f-8eb8-e08f52bbab1c" /> |
||
|
|
6117a1d6c0 |
refactor: standardize AI acronym to Ai (PascalCase) across internal identifiers (#19837)
## Summary
The "AI" acronym was rendered inconsistently across the codebase. The
backend AI module had settled on PascalCase `Ai` (`AiAgentModule`,
`AiBillingService`, `AiChatModule`, `AiModelRegistryService`, etc.),
while frontend components, several DTOs, a few types, and shared
identifiers still used all-caps `AI` (`AIChatTab`,
`AISystemPromptPreviewDTO`, `SettingsPath.AIPrompts`, ...). CLAUDE.md
specifies PascalCase for classes; this PR normalizes everything internal
to `Ai`.
**This is a pure internal rename.** The GraphQL schema is untouched —
`@ObjectType` decorator string arguments, resolver method names (which
become Query/Mutation field names), gql template contents, and the
`generated-metadata/graphql.ts` file are preserved verbatim. The only
visible change is TypeScript identifiers and file names.
## Also folded in (adjacent cleanups)
- **`AgentModelConfigService` → `AiModelConfigService`**. Lives in
`ai-models/` and is used by multiple AI code paths, not just the Agent
entity. The "Agent" prefix was misleading.
- **`generate-text-input.dto.ts` → `generate-text.input.ts`**. The
`ai-agent/dtos/` folder already uses `<entity>.input.ts` convention for
Input classes (`create-agent.input.ts` etc.); the old path mixed
`.dto.ts` file extension with a class that has no DTO suffix. File
rename only; class stays `GenerateTextInput`.
- **Removed stale TODO** in `ai-model-config.type.ts` that asked for the
`AiModelConfig` rename that this PR performs.
## Rename methodology
Bulk rename via perl with anchored regex
`(?<!['"])(?<![A-Z.])AI([A-Z])(?=[a-z])/Ai$1/g`:
- **Lookbehind for non-uppercase** skips adjacent acronyms (`MOSAIC`,
`OIDCSSO`) and leaves `AIRBNB_ID` alone.
- **Lookbehind for non-quote** protects most string literals.
- **Lookahead for lowercase** restricts matches to PascalCase
identifiers (`AIChatTab`), leaving SCREAMING_SNAKE constants untouched.
Strict file-scope exclusions: `generated-metadata/**`, `generated/**`,
`locales/**`, `migrations/**`, `illustrations/**`, `halftone/**`, and
the two gql template files (`queries/getAISystemPromptPreview.ts`,
`mutations/uploadAIChatFile.ts`).
Post-rename reverts for identifiers where the regex was too eager:
- Backend resolver method names kept: `getAISystemPromptPreview`,
`uploadAIChatFile` (they are GraphQL field names).
- `@ObjectType('AdminAIModels')` / `('AISystemPromptPreview')` /
`('AISystemPromptSection')` kept as-is.
- Backend classes `ClientAIModelConfig` / `AdminAIModelConfig` kept
as-is (they use `@ObjectType()` with no argument, so the class name IS
the schema name).
- External-library symbols restored: `OpenAIProvider`,
`createOpenAICompatible`, `vercelAIIntegration`.
File renames use a two-step rename to work on macOS case-insensitive
filesystems: `git mv X.tsx X.tsx.tmp && git mv X.tsx.tmp renamed.tsx`.
## Diff audit
- 0 changes to migrations
- 0 changes to locale `.po` / `.ts` files
- 0 changes to `generated-metadata/graphql.ts`
- 0 changes to website illustration files (base64 blobs preserved)
- 0 renames inside user-facing translation strings (`t\`…\``,
`msg\`…\``, `<Trans>…</Trans>`)
## Test plan
- [x] `npx nx typecheck twenty-server` — PASS
- [x] `npx nx typecheck twenty-front` — PASS
- [x] `npx jest ai-model admin agent-role` — 79/79 PASS
- [x] `npx oxlint --type-aware` on 118 changed files — 0 errors
- [x] `npx prettier --check` on 118 changed files — clean
- [ ] CI
|
||
|
|
a93f23a150 |
Fix AI chat dropzone persisting when dragging file out without dropping (#19794)
Issue link: https://discord.com/channels/1130383047699738754/1494248499351519232 ### Before https://github.com/user-attachments/assets/ec4ba8cc-b9e7-4b77-8b8f-b254e11edb19 ### After https://github.com/user-attachments/assets/2905536b-6a31-41e8-9f25-8768fd224b6a |
||
|
|
307b6c94de |
Align GraphQL error handling for billing and AI chat (#19690)
## What changed This refactor fixes AI chat error surfacing by aligning both the backend and frontend with the existing GraphQL error architecture instead of adding AI-local error translation. On the backend: - add a dedicated GraphQL billing exception path - register billing GraphQL handling globally for GraphQL requests - reuse the existing AI GraphQL interceptor path for agent/chat exceptions - keep billing status classification shared between REST and GraphQL - remove the earlier attempt to preserve `CustomException` metadata in the global GraphQL fallback On the frontend: - keep the original Apollo GraphQL error object in AI chat state - reuse shared Apollo/GraphQL helpers for user-facing messages and error-type checks - delete AI-specific error extraction helpers that duplicated generic GraphQL parsing - replace a few direct `extensions.subCode` call sites with a shared predicate ## Why it changed The original bug was that `BillingException` and AI exceptions thrown from chat were not being translated into GraphQL errors with the expected `extensions.subCode` and `extensions.userFriendlyMessage`, so the AI chat UI had nothing structured to inspect. An intermediate fix worked mechanically but pushed `CustomException` handling into the global GraphQL fallback, which blurred the intended layering. This PR moves the behavior back to explicit GraphQL edges. ## Root cause `AgentChatResolver` could throw `BillingException` and `AgentException`, but: - billing had a REST exception filter and no shared GraphQL equivalent - AI chat was not consistently using the same GraphQL exception translation path as the sibling AI resolver - the frontend chat UI had drifted into AI-specific error parsing instead of consuming the same structured Apollo errors as the rest of the app ## Impact - `BILLING_CREDITS_EXHAUSTED` is now preserved through GraphQL and can render the existing credits-exhausted UI in chat - `API_KEY_NOT_CONFIGURED` is preserved through the AI GraphQL path - AI chat now follows the same general GraphQL error consumption pattern as the rest of the frontend - billing GraphQL handling is less dependent on individual resolver authors remembering to add a filter ## Validation - `yarn jest --config packages/twenty-server/jest.config.mjs packages/twenty-server/src/engine/core-modules/billing/utils/__tests__/billing-graphql-api-exception-handler.util.spec.ts packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/utils/__tests__/agent-graphql-api-exception-handler.util.spec.ts` - `yarn jest --config packages/twenty-front/jest.config.mjs packages/twenty-front/src/utils/__tests__/is-graphql-error-of-type.util.test.ts` - `npx oxlint --type-aware ...` on touched backend/frontend files - `npx prettier --check ...` on touched backend/frontend files ## Follow-up ideas - consolidate frontend GraphQL error helpers further so more existing direct `extensions.subCode` checks move to shared utilities - consider whether common GraphQL exception filter registration should live in a more explicit GraphQL-specific module instead of `CoreEngineModule` - add an end-to-end test for a real `sendChatMessage` GraphQL failure path in AI chat --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
a88d1f4442 |
Introduce standalone page (#19675)
Add support for standalone pages: a new `PageLayout` type (`STANDALONE_PAGE`) that can be rendered independently at `/page/:pageLayoutId`, not tied to any record or object context. - New `STANDALONE_PAGE` page layout type - New `PAGE_LAYOUT` navigation menu item type: adds a `pageLayoutId` foreign key to `NavigationMenuItemEntity`, allowing sidebar items to link directly to standalone pages - New `GLOBAL_OBJECT_CONTEXT` command menu availability type: separates object-context-dependent commands (Create Record, Import, Export, See Deleted, Create View, Hide Deleted) from truly global ones, so standalone pages only show relevant commands - Frontend routing & rendering: adds a `/page/:pageLayoutId` route with its own page component, header, and command menu - Widget rendering refactor - Instance commands: two fast 1.22 migrations: `pageLayoutId` column + `STANDALONE_PAGE` enum, and `GLOBAL_OBJECT_CONTEXT` availability type enum - Workspace command: backfills existing command menu items from `GLOBAL` to `GLOBAL_OBJECT_CONTEXT` where appropriate - Dev seeds: adds a sample "Star History" standalone page with an iframe widget for local development |
||
|
|
55b1624210 |
Convert AI chat state atoms to component family states (#19585)
## Summary
Refactored AI chat state management to use component family states
instead of global atoms. This change enables proper state isolation per
chat thread, preventing state leakage between different chat instances.
## Key Changes
- **Converted global atoms to component family states:**
- `agentChatErrorState` → `agentChatErrorComponentFamilyState`
- `agentChatIsStreamingState` →
`agentChatIsStreamingComponentFamilyState`
- `agentChatFirstLiveSeqState` →
`agentChatFirstLiveSeqComponentFamilyState`
- `agentChatHandleEventCallbackState` →
`agentChatHandleEventCallbackComponentFamilyState`
- `agentChatUsageState` → `agentChatUsageComponentFamilyState`
- `currentAIChatThreadTitleState` →
`currentAIChatThreadTitleComponentFamilyState`
- **Updated state access patterns:**
- Introduced `useAtomComponentFamilyStateCallbackState` hook to get
family state callbacks
- Changed from direct atom access to family key-based access using `{
threadId }` as the family key
- Updated all components and hooks to use the new family state patterns
- **Removed instance ID dependency:**
- Eliminated `AGENT_CHAT_INSTANCE_ID` constant usage
- Simplified state access by using thread ID directly as the family key
- **Updated affected components and hooks:**
- `useAgentChatSubscription`: Now creates family state atoms per thread
- `AgentChatMessagesFetchEffect`: Uses family state callbacks for event
handling
- `AgentChatThreadInitializationEffect`: Sets family state values per
thread
- `useAIChatThreadClick`: Updates family states when switching threads
- `AIChatErrorUnderMessageList`, `AIChatLastMessageWithStreamingState`,
`AIChatEmptyState`, `AIChatStandaloneError`, `SendMessageButton`,
`AIChatContextUsageButton`, `SidePanelAskAIInfo`: Updated to use family
state values with thread ID
## Implementation Details
- All new component family states use
`AgentChatComponentInstanceContext` for proper component-level isolation
- Family key structure: `{ threadId: string | null }`
- Removed `disposed` flag logic as it's no longer needed with proper
state isolation
- Updated dependency arrays in hooks to include family callback
functions
https://claude.ai/code/session_01D8syiJtCXu5bEHSr5KYkCt
---------
Co-authored-by: Claude <noreply@anthropic.com>
|
||
|
|
8c4a6cd663 |
Replace AGENT_CHAT_UNKNOWN_THREAD_ID with null for thread state (#19552)
## Summary This PR refactors the AI chat thread state management to use `null` instead of a sentinel string value (`AGENT_CHAT_UNKNOWN_THREAD_ID`) to represent an uninitialized or new chat thread. This improves type safety and makes the code more idiomatic by using `null` to represent the absence of a value. ## Key Changes - **Removed sentinel constant**: Deleted `AGENT_CHAT_UNKNOWN_THREAD_ID` constant and replaced all usages with `null` - **Updated state types**: Changed `currentAIChatThreadState`, `agentChatLastDiffSyncedThreadState`, and `agentChatDisplayedThreadState` to use `string | null` type with `null` as default value - **Updated component family states**: Modified message-related state families to accept `threadId: string | null` instead of `threadId: string` - **Refined null checks**: Added explicit `null` checks in: - `AgentChatMessagesFetchEffect`: Updated `isNewThread` logic to check for `null` first - `useAIChatThreadClick` and `useSwitchToNewAIChat`: Added guards to only save drafts when `currentAIChatThread !== null` - `AgentChatThreadInitializationEffect`: Added null check before UUID validation - `useEnsureAgentChatThreadIdForSend`: Added null check before comparing with draft key - **Updated fallback logic**: Used nullish coalescing operator (`??`) in `AIChatTab` and `useAIChatEditor` to default to `AGENT_CHAT_NEW_THREAD_DRAFT_KEY` when thread is null - **Enhanced refetch safety**: Added early return in `handleRefetchMessages` to prevent refetching when in new thread state ## Implementation Details - The change maintains backward compatibility by treating `null` the same way the code previously treated `AGENT_CHAT_UNKNOWN_THREAD_ID` - All draft saving operations now safely check for null before attempting to store drafts - The nullish coalescing pattern (`currentAIChatThread ?? AGENT_CHAT_NEW_THREAD_DRAFT_KEY`) ensures proper fallback behavior when accessing draft storage https://claude.ai/code/session_01Pz8KCygSNgBPYsndbMq8f7 --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
2bb939b4b5 |
Add file attachment support to agent chat messaging (#19517)
## Summary This PR adds support for attaching files to agent chat messages. Users can now upload files when sending messages to the AI agent, and these files are properly processed, stored, and made available to the agent with signed URLs. ## Key Changes - **File attachment input**: Added `fileIds` parameter to the `sendChatMessage` GraphQL mutation to accept file IDs from the client - **File processing**: Implemented `buildFilePartsFromIds()` method to convert file IDs into file UI parts with signed URLs - **Message composition**: Updated user messages to include both text and file parts when files are attached - **File URL signing**: Integrated `FileUrlService` to generate signed URLs for files in the AgentChat folder, ensuring secure access - **Message persistence**: Files are now included in the message parts stored in the database and retrieved when loading conversation history - **File metadata mapping**: Enhanced `mapDBPartToUIMessagePart()` to properly extract MIME types from file entities and include file IDs ## Implementation Details - Files are fetched from the database using the provided file IDs and workspace context - Each file is converted to an `ExtendedFileUIPart` with proper metadata (filename, MIME type, signed URL, and file ID) - When loading messages from the database, file parts are enhanced with signed URLs to ensure they remain accessible - The `loadMessagesFromDB()` method now requires the workspace ID to properly sign file URLs - File attachments are seamlessly integrated into the existing message part system alongside text content https://claude.ai/code/session_01TAdN1gBzeiYELX4XDrrYY1 --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> |