From 352518732114353490328cbe72ff8b1da986049e Mon Sep 17 00:00:00 2001
From: Etienne <45695613+etiennejouan@users.noreply.github.com>
Date: Fri, 26 Jun 2026 19:53:29 +0200
Subject: [PATCH] fix(ai) - fixes (#22227)
- ai chat author fix (before : "workflow", after : "user")
- https://discord.com/channels/1130383047699738754/1496872385687584768
---
.../ai/components/AiChatEditorSection.tsx | 18 ++++++-
.../__stories__/AiChatBanner.stories.tsx | 39 ++++++++++++++
.../components/internal/SendMessageButton.tsx | 8 ++-
.../src/modules/ai/hooks/useAgentChat.ts | 13 +++++
.../ai/components/SettingsAiModelsTab.tsx | 2 +-
...created-by-from-full-name-metadata.util.ts | 5 +-
.../services/tool-registry.service.ts | 1 +
.../services/agent-actor-context.service.ts | 10 ++--
.../services/ai-model-registry.service.ts | 21 +++++---
.../workspace-has-enabled-models.util.spec.ts | 52 +++++++++++++++++++
.../ai-models/utils/is-model-allowed.util.ts | 6 +--
.../workspace-has-enabled-models.util.ts | 12 +++++
12 files changed, 165 insertions(+), 22 deletions(-)
create mode 100644 packages/twenty-front/src/modules/ai/components/__stories__/AiChatBanner.stories.tsx
create mode 100644 packages/twenty-server/src/engine/metadata-modules/ai/ai-models/utils/__tests__/workspace-has-enabled-models.util.spec.ts
create mode 100644 packages/twenty-server/src/engine/metadata-modules/ai/ai-models/utils/workspace-has-enabled-models.util.ts
diff --git a/packages/twenty-front/src/modules/ai/components/AiChatEditorSection.tsx b/packages/twenty-front/src/modules/ai/components/AiChatEditorSection.tsx
index 11d6408ce4..64777935ca 100644
--- a/packages/twenty-front/src/modules/ai/components/AiChatEditorSection.tsx
+++ b/packages/twenty-front/src/modules/ai/components/AiChatEditorSection.tsx
@@ -1,7 +1,9 @@
import { styled } from '@linaria/react';
import { EditorContent } from '@tiptap/react';
+import { useLingui } from '@lingui/react/macro';
import { themeCssVariables } from 'twenty-ui/theme-constants';
+import { AiChatBanner } from '@/ai/components/AiChatBanner';
import { AiChatEmptyState } from '@/ai/components/AiChatEmptyState';
import { AIChatNoMoreBillingCreditsBanner } from '@/ai/components/AIChatNoMoreBillingCreditsBanner';
import { AiChatStandaloneError } from '@/ai/components/AiChatStandaloneError';
@@ -14,6 +16,7 @@ import { SendMessageButton } from '@/ai/components/internal/SendMessageButton';
import { useAgentChatModelId } from '@/ai/hooks/useAgentChatModelId';
import { useAiChatEditor } from '@/ai/hooks/useAiChatEditor';
import { useAiModelOptions } from '@/ai/hooks/useAiModelOptions';
+import { useWorkspaceAiModelAvailability } from '@/ai/hooks/useWorkspaceAiModelAvailability';
import { agentChatUserSelectedModelState } from '@/ai/states/agentChatUserSelectedModelState';
import { Select } from '@/ui/input/components/Select';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
@@ -108,10 +111,13 @@ const StyledRightButtonsContainer = styled.div`
`;
export const AiChatEditorSection = () => {
+ const { t } = useLingui();
const isMobile = useIsMobile();
const hasReachedCurrentBillingPeriodCap = useAtomStateValue(
hasReachedCurrentBillingPeriodCapSelector,
);
+ const { enabledModels } = useWorkspaceAiModelAvailability();
+ const hasNoEnabledModels = enabledModels.length === 0;
const { options, pinnedOption } = useAiModelOptions({
variant: 'pinned-default',
});
@@ -140,6 +146,12 @@ export const AiChatEditorSection = () => {
+ {hasNoEnabledModels && (
+
+ )}
{hasReachedCurrentBillingPeriodCap && (
)}
@@ -159,12 +171,16 @@ export const AiChatEditorSection = () => {
onChange={setAgentChatUserSelectedModel}
options={smartModelOptions}
pinnedOption={defaultPinnedOption}
+ disabled={hasNoEnabledModels}
selectSizeVariant="small"
showContextualTextInControl={false}
withSearchInput
dropdownOffset={{ x: 0, y: 8 }}
/>
-
+
diff --git a/packages/twenty-front/src/modules/ai/components/__stories__/AiChatBanner.stories.tsx b/packages/twenty-front/src/modules/ai/components/__stories__/AiChatBanner.stories.tsx
new file mode 100644
index 0000000000..41681c540e
--- /dev/null
+++ b/packages/twenty-front/src/modules/ai/components/__stories__/AiChatBanner.stories.tsx
@@ -0,0 +1,39 @@
+import { type Meta, type StoryObj } from '@storybook/react-vite';
+import { ComponentDecorator } from 'twenty-ui/testing';
+
+import { AiChatBanner } from '@/ai/components/AiChatBanner';
+
+const meta: Meta = {
+ title: 'Modules/AI/AiChatBanner',
+ component: AiChatBanner,
+ decorators: [ComponentDecorator],
+ parameters: {
+ container: { width: 400 },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const NoEnabledModels: Story = {
+ args: {
+ message: 'No AI models are enabled in this workspace.',
+ variant: 'warning',
+ },
+};
+
+export const Default: Story = {
+ args: {
+ message: 'This is an informational message.',
+ variant: 'default',
+ },
+};
+
+export const WithButton: Story = {
+ args: {
+ message: "You've hit your usage limit.",
+ variant: 'warning',
+ buttonTitle: 'Upgrade',
+ buttonOnClick: () => {},
+ },
+};
diff --git a/packages/twenty-front/src/modules/ai/components/internal/SendMessageButton.tsx b/packages/twenty-front/src/modules/ai/components/internal/SendMessageButton.tsx
index 31f4d016e0..73de0f9900 100644
--- a/packages/twenty-front/src/modules/ai/components/internal/SendMessageButton.tsx
+++ b/packages/twenty-front/src/modules/ai/components/internal/SendMessageButton.tsx
@@ -11,9 +11,13 @@ import { RoundedIconButton } from 'twenty-ui/input';
type SendMessageButtonProps = {
onSend: () => void;
+ isDisabled?: boolean;
};
-export const SendMessageButton = ({ onSend }: SendMessageButtonProps) => {
+export const SendMessageButton = ({
+ onSend,
+ isDisabled = false,
+}: SendMessageButtonProps) => {
const agentChatInputIsEmpty = useAtomStateValue(
agentChatInputIsEmptySelector,
);
@@ -45,7 +49,7 @@ export const SendMessageButton = ({ onSend }: SendMessageButtonProps) => {
Icon={IconArrowUp}
size="medium"
onClick={onSend}
- disabled={agentChatInputIsEmpty || agentChatIsLoading}
+ disabled={isDisabled || agentChatInputIsEmpty || agentChatIsLoading}
/>
);
};
diff --git a/packages/twenty-front/src/modules/ai/hooks/useAgentChat.ts b/packages/twenty-front/src/modules/ai/hooks/useAgentChat.ts
index e381c5c13b..0249933116 100644
--- a/packages/twenty-front/src/modules/ai/hooks/useAgentChat.ts
+++ b/packages/twenty-front/src/modules/ai/hooks/useAgentChat.ts
@@ -1,5 +1,6 @@
import { CombinedGraphQLErrors } from '@apollo/client/errors';
import { useApolloClient } from '@apollo/client/react';
+import { t } from '@lingui/core/macro';
import { useStore } from 'jotai';
import { useCallback } from 'react';
import { type ExtendedUIMessage } from 'twenty-shared/ai';
@@ -16,6 +17,7 @@ import { STOP_AGENT_CHAT_STREAM } from '@/ai/graphql/mutations/stopAgentChatStre
import { useAgentChatModelId } from '@/ai/hooks/useAgentChatModelId';
import { useGetBrowsingContext } from '@/ai/hooks/useBrowsingContext';
import { useOptimisticallyUnarchiveOnSend } from '@/ai/hooks/useOptimisticallyUnarchiveOnSend';
+import { useWorkspaceAiModelAvailability } from '@/ai/hooks/useWorkspaceAiModelAvailability';
import {
AGENT_CHAT_NEW_THREAD_DRAFT_KEY,
agentChatDraftsByThreadIdState,
@@ -37,6 +39,7 @@ export const useAgentChat = (
ensureThreadIdForSend: () => Promise,
) => {
const { modelIdForRequest } = useAgentChatModelId();
+ const { enabledModels } = useWorkspaceAiModelAvailability();
const { getBrowsingContext } = useGetBrowsingContext();
const { applyOptimisticUnarchive } = useOptimisticallyUnarchiveOnSend();
const apolloClient = useApolloClient();
@@ -70,6 +73,14 @@ export const useAgentChat = (
return;
}
+ if (enabledModels.length === 0) {
+ enqueueErrorSnackBar({
+ message: t`No AI models are enabled in this workspace.`,
+ });
+
+ return;
+ }
+
const agentChatSelectedFiles = store.get(agentChatSelectedFilesState.atom);
if (agentChatSelectedFiles.length > 0) {
@@ -225,6 +236,8 @@ export const useAgentChat = (
setAgentChatUploadedFiles,
setAgentChatDraftsByThreadId,
modelIdForRequest,
+ enabledModels,
+ enqueueErrorSnackBar,
setCurrentAiChatThread,
apolloClient,
applyOptimisticUnarchive,
diff --git a/packages/twenty-front/src/pages/settings/ai/components/SettingsAiModelsTab.tsx b/packages/twenty-front/src/pages/settings/ai/components/SettingsAiModelsTab.tsx
index 6fe7b9763f..f72b40645a 100644
--- a/packages/twenty-front/src/pages/settings/ai/components/SettingsAiModelsTab.tsx
+++ b/packages/twenty-front/src/pages/settings/ai/components/SettingsAiModelsTab.tsx
@@ -155,7 +155,7 @@ export const SettingsAiModelsTab = () => {
({
workspaceMemberId,
- source: FieldActorSource.MANUAL,
+ source,
name: `${fullNameMetadata.firstName} ${fullNameMetadata.lastName}`,
context: {},
});
diff --git a/packages/twenty-server/src/engine/core-modules/tool-provider/services/tool-registry.service.ts b/packages/twenty-server/src/engine/core-modules/tool-provider/services/tool-registry.service.ts
index a569212605..cc0d781574 100644
--- a/packages/twenty-server/src/engine/core-modules/tool-provider/services/tool-registry.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/tool-provider/services/tool-registry.service.ts
@@ -404,6 +404,7 @@ export class ToolRegistryService {
roleId: context.roleId,
rolePermissionConfig,
authContext: context.authContext,
+ actorContext: context.actorContext,
userId: context.userId,
userWorkspaceId: context.userWorkspaceId,
threadId: context.threadId,
diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-actor-context.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-actor-context.service.ts
index 7d5169eef9..6ea9a0faa1 100644
--- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-actor-context.service.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-agent-execution/services/agent-actor-context.service.ts
@@ -90,13 +90,11 @@ export class AgentActorContextService {
);
}
- const actorContext: ActorMetadata = {
- ...buildCreatedByFromFullNameMetadata({
- fullNameMetadata: workspaceMember.name,
- workspaceMemberId: workspaceMember.id,
- }),
+ const actorContext = buildCreatedByFromFullNameMetadata({
+ fullNameMetadata: workspaceMember.name,
+ workspaceMemberId: workspaceMember.id,
source: FieldActorSource.AGENT,
- };
+ });
const userContext: UserContext = {
firstName: workspaceMember.name?.firstName ?? '',
diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service.ts
index 62e44f2592..f556f8c59c 100644
--- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service.ts
@@ -33,6 +33,7 @@ import {
isModelAllowedByWorkspace,
type WorkspaceModelAvailabilitySettings,
} from 'src/engine/metadata-modules/ai/ai-models/utils/is-model-allowed.util';
+import { workspaceHasEnabledModels } from 'src/engine/metadata-modules/ai/ai-models/utils/workspace-has-enabled-models.util';
export interface RegisteredAiModel {
modelId: string;
@@ -293,7 +294,7 @@ export class AiModelRegistryService {
validateModelAvailability(
modelId: string,
- workspace: WorkspaceModelAvailabilitySettings,
+ availabilitySettings: WorkspaceModelAvailabilitySettings,
): void {
if (!this.isModelAdminAllowed(modelId)) {
throw new AiException(
@@ -302,13 +303,17 @@ export class AiModelRegistryService {
);
}
- if (
- !isModelAllowedByWorkspace(
- modelId,
- workspace,
- this.getRecommendedModelIds(),
- )
- ) {
+ const recommendedModelIds = this.getRecommendedModelIds();
+
+ const isAvailable = isAutoSelectModelId(modelId)
+ ? workspaceHasEnabledModels(availabilitySettings, recommendedModelIds)
+ : isModelAllowedByWorkspace(
+ modelId,
+ availabilitySettings,
+ recommendedModelIds,
+ );
+
+ if (!isAvailable) {
throw new AiException(
'The selected model is not available in this workspace.',
AiExceptionCode.AGENT_EXECUTION_FAILED,
diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/utils/__tests__/workspace-has-enabled-models.util.spec.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/utils/__tests__/workspace-has-enabled-models.util.spec.ts
new file mode 100644
index 0000000000..247fd48ea8
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/utils/__tests__/workspace-has-enabled-models.util.spec.ts
@@ -0,0 +1,52 @@
+import { workspaceHasEnabledModels } from 'src/engine/metadata-modules/ai/ai-models/utils/workspace-has-enabled-models.util';
+
+describe('workspaceHasEnabledModels', () => {
+ describe('when the workspace uses recommended models', () => {
+ it('returns true when at least one model is recommended', () => {
+ expect(
+ workspaceHasEnabledModels(
+ { useRecommendedModels: true, enabledAiModelIds: [] },
+ new Set(['openai/gpt-5.2']),
+ ),
+ ).toBe(true);
+ });
+
+ it('returns false when no model is recommended', () => {
+ expect(
+ workspaceHasEnabledModels(
+ { useRecommendedModels: true, enabledAiModelIds: [] },
+ new Set(),
+ ),
+ ).toBe(false);
+ });
+
+ it('returns false when no recommended ids are provided', () => {
+ expect(
+ workspaceHasEnabledModels({
+ useRecommendedModels: true,
+ enabledAiModelIds: [],
+ }),
+ ).toBe(false);
+ });
+ });
+
+ describe('when the workspace selects models manually', () => {
+ it('returns true when at least one model is enabled', () => {
+ expect(
+ workspaceHasEnabledModels({
+ useRecommendedModels: false,
+ enabledAiModelIds: ['openai/gpt-5.2'],
+ }),
+ ).toBe(true);
+ });
+
+ it('returns false when every model is disabled', () => {
+ expect(
+ workspaceHasEnabledModels({
+ useRecommendedModels: false,
+ enabledAiModelIds: [],
+ }),
+ ).toBe(false);
+ });
+ });
+});
diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/utils/is-model-allowed.util.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/utils/is-model-allowed.util.ts
index 3350106786..fec2a326e4 100644
--- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/utils/is-model-allowed.util.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/utils/is-model-allowed.util.ts
@@ -7,16 +7,16 @@ export type WorkspaceModelAvailabilitySettings = {
export const isModelAllowedByWorkspace = (
modelId: string,
- workspace: WorkspaceModelAvailabilitySettings,
+ availabilitySettings: WorkspaceModelAvailabilitySettings,
recommendedModelIds?: Set,
): boolean => {
if (isAutoSelectModelId(modelId)) {
return true;
}
- if (workspace.useRecommendedModels) {
+ if (availabilitySettings.useRecommendedModels) {
return recommendedModelIds?.has(modelId) ?? false;
}
- return workspace.enabledAiModelIds.includes(modelId);
+ return availabilitySettings.enabledAiModelIds.includes(modelId);
};
diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/utils/workspace-has-enabled-models.util.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/utils/workspace-has-enabled-models.util.ts
new file mode 100644
index 0000000000..9ea6687aaa
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/utils/workspace-has-enabled-models.util.ts
@@ -0,0 +1,12 @@
+import { type WorkspaceModelAvailabilitySettings } from 'src/engine/metadata-modules/ai/ai-models/utils/is-model-allowed.util';
+
+export const workspaceHasEnabledModels = (
+ availabilitySettings: WorkspaceModelAvailabilitySettings,
+ recommendedModelIds?: Set,
+): boolean => {
+ if (availabilitySettings.useRecommendedModels) {
+ return (recommendedModelIds?.size ?? 0) > 0;
+ }
+
+ return availabilitySettings.enabledAiModelIds.length > 0;
+};