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>
This commit is contained in:
Félix Malfait
2026-04-21 09:49:46 +02:00
committed by GitHub
parent 69868a0ab6
commit 30b8663a74
32 changed files with 73 additions and 240 deletions
@@ -1,17 +1,9 @@
import { AgentChatProviderContent } from '@/ai/components/AgentChatProviderContent';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const AgentChatProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
return (
<AgentChatProviderContent isAiEnabled={isAiEnabled}>
{children}
</AgentChatProviderContent>
);
return <AgentChatProviderContent>{children}</AgentChatProviderContent>;
};
@@ -10,26 +10,20 @@ import { Suspense } from 'react';
export const AgentChatProviderContent = ({
children,
isAiEnabled,
}: {
children: React.ReactNode;
isAiEnabled: boolean;
}) => {
return (
<Suspense fallback={null}>
<AgentChatComponentInstanceContext.Provider
value={{ instanceId: 'agentChatComponentInstance' }}
>
{isAiEnabled && (
<>
<AgentChatThreadInitializationEffect />
<AgentChatMessagesFetchEffect />
<AgentChatStreamSubscriptionEffect />
<AgentChatStreamingPartsDiffSyncEffect />
<AgentChatSessionStartTimeEffect />
<AgentChatStreamingAutoScrollEffect />
</>
)}
<AgentChatThreadInitializationEffect />
<AgentChatMessagesFetchEffect />
<AgentChatStreamSubscriptionEffect />
<AgentChatStreamingPartsDiffSyncEffect />
<AgentChatSessionStartTimeEffect />
<AgentChatStreamingAutoScrollEffect />
{children}
</AgentChatComponentInstanceContext.Provider>
</Suspense>
@@ -9,11 +9,9 @@ import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
import { useGlobalHotkeys } from '@/ui/utilities/hotkey/hooks/useGlobalHotkeys';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { isNonEmptyString } from '@sniptt/guards';
import { Key } from 'ts-key-enum';
import { SidePanelPages } from 'twenty-shared/types';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
export const useCommandMenuHotKeys = () => {
const { toggleSidePanelMenu } = useSidePanelMenu();
@@ -31,8 +29,6 @@ export const useCommandMenuHotKeys = () => {
const sidePanelPage = useAtomStateValue(sidePanelPageState);
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
useGlobalHotkeys({
keys: ['ctrl+k', 'meta+k'],
callback: () => {
@@ -58,12 +54,10 @@ export const useCommandMenuHotKeys = () => {
useGlobalHotkeys({
keys: ['@'],
callback: () => {
if (isAiEnabled) {
openAskAiPage({ resetNavigationStack: true });
}
openAskAiPage({ resetNavigationStack: true });
},
containsModifier: false,
dependencies: [openAskAiPage, isAiEnabled],
dependencies: [openAskAiPage],
options: {
ignoreModifiers: true,
},
@@ -22,8 +22,6 @@ import {
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
const StyledRow = styled.div<{ isExpanded: boolean }>`
align-items: center;
@@ -141,15 +139,10 @@ export const MainNavigationDrawerTabsRow = () => {
const [navigationDrawerActiveTab, setNavigationDrawerActiveTab] =
useAtomState(navigationDrawerActiveTabState);
const { switchToNewChat } = useSwitchToNewAiChat();
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
const setIsNavigationDrawerExpanded = useSetAtomState(
isNavigationDrawerExpandedState,
);
if (!isAiEnabled) {
return null;
}
const isExpanded = isNavigationDrawerExpanded || isMobile;
const handleTabClick = (tab: NavigationDrawerActiveTab) => () => {
@@ -12,7 +12,6 @@ import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNaviga
import { useSetAtomComponentState } from '@/ui/utilities/state/jotai/hooks/useSetAtomComponentState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useNavigate } from 'react-router-dom';
import {
type IconComponent,
@@ -21,7 +20,6 @@ import {
IconSearch,
} from 'twenty-ui/display';
import { NavigationBar } from 'twenty-ui/navigation';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
type NavigationBarItemName = 'main' | 'search' | 'newAiChat';
@@ -37,7 +35,6 @@ export const MobileNavigationBar = () => {
const [currentMobileNavigationDrawer, setCurrentMobileNavigationDrawer] =
useAtomState(currentMobileNavigationDrawerState);
const { switchToNewChat } = useSwitchToNewAiChat();
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
const { alphaSortedActiveNonSystemObjectMetadataItems } =
useFilteredObjectMetadataItems();
@@ -92,19 +89,15 @@ export const MobileNavigationBar = () => {
openRecordsSearchPage();
},
},
...(isAiEnabled
? [
{
name: 'newAiChat' as const,
Icon: IconMessageCirclePlus,
onClick: () => {
setIsNavigationDrawerExpanded(false);
closeSidePanelMenu();
switchToNewChat();
},
},
]
: []),
{
name: 'newAiChat' as const,
Icon: IconMessageCirclePlus,
onClick: () => {
setIsNavigationDrawerExpanded(false);
closeSidePanelMenu();
switchToNewChat();
},
},
];
return <NavigationBar activeItemName={activeItemName} items={items} />;
@@ -12,7 +12,6 @@ import {
type NavigationDrawerItemModifier,
} from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { t } from '@lingui/core/macro';
import { isNonEmptyString } from '@sniptt/guards';
import {
@@ -39,10 +38,7 @@ import {
IconUsers,
IconWorld,
} from 'twenty-ui/display';
import {
FeatureFlagKey,
PermissionFlagType,
} from '~/generated-metadata/graphql';
import { PermissionFlagType } from '~/generated-metadata/graphql';
export type SettingsNavigationSection = {
label: string;
@@ -74,7 +70,6 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
const isAdminEnabled =
(currentUser?.canImpersonate || currentUser?.canAccessFullAdminPanel) ??
false;
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
const isSupportChatConfigured =
supportChat?.supportDriver === 'FRONT' &&
isNonEmptyString(supportChat.supportFrontChatId);
@@ -182,8 +177,7 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
label: t`AI`,
path: SettingsPath.AI,
Icon: IconSparkles,
isHidden:
!isAiEnabled || !permissionMap[PermissionFlagType.WORKSPACE],
isHidden: !permissionMap[PermissionFlagType.WORKSPACE],
modifier: 'new',
},
{
@@ -13,14 +13,12 @@ import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useState } from 'react';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { SettingsPath } from 'twenty-shared/types';
import { useQuery } from '@apollo/client/react';
import {
type Agent,
FeatureFlagKey,
type ApiKeyForRole,
FindManyAgentsDocument,
GetApiKeysDocument,
@@ -40,8 +38,6 @@ export const SettingsRoleAssignment = ({
roleId,
isCreateMode,
}: SettingsRoleAssignmentProps) => {
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
const settingsDraftRole = useAtomFamilyStateValue(
settingsDraftRoleFamilyState,
roleId,
@@ -59,9 +55,7 @@ export const SettingsRoleAssignment = ({
const { addApiKeyToRoleAndUpdateState, updateApiKeyRoleDraftState } =
useUpdateApiKeyRole(roleId);
const { data: agentsData } = useQuery(FindManyAgentsDocument, {
skip: !isAiEnabled,
});
const { data: agentsData } = useQuery(FindManyAgentsDocument);
const { data: apiKeysData } = useQuery(GetApiKeysDocument);
const { openModal, closeModal } = useModal();
@@ -204,28 +198,25 @@ export const SettingsRoleAssignment = ({
return (
<>
{Object.keys(ROLE_TARGET_CONFIG).map(
(roleTargetType) =>
(isAiEnabled || roleTargetType !== 'agent') && (
<RoleAssignmentSection
key={roleTargetType}
roleTargetType={roleTargetType as keyof typeof ROLE_TARGET_CONFIG}
roleId={roleId}
settingsDraftRole={settingsDraftRole}
currentWorkspaceMember={
roleTargetType === 'member'
? currentWorkspaceMember || undefined
: undefined
}
onSelect={handleSelectEntity}
allWorkspaceMembersHaveThisRole={
roleTargetType === 'member'
? allWorkspaceMembersHaveThisRole
: false
}
/>
),
)}
{Object.keys(ROLE_TARGET_CONFIG).map((roleTargetType) => (
<RoleAssignmentSection
key={roleTargetType}
roleTargetType={roleTargetType as keyof typeof ROLE_TARGET_CONFIG}
roleId={roleId}
settingsDraftRole={settingsDraftRole}
currentWorkspaceMember={
roleTargetType === 'member'
? currentWorkspaceMember || undefined
: undefined
}
onSelect={handleSelectEntity}
allWorkspaceMembersHaveThisRole={
roleTargetType === 'member'
? allWorkspaceMembersHaveThisRole
: false
}
/>
))}
{selectedRoleTarget && (
<SettingsRoleAssignmentConfirmationModal
@@ -1,5 +1,4 @@
import { type SettingsRolePermissionsSettingPermission } from '@/settings/roles/role-permissions/permission-flags/types/SettingsRolePermissionsSettingPermission';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { t } from '@lingui/core/macro';
import { useMemo } from 'react';
import {
@@ -14,10 +13,7 @@ import {
IconTable,
IconUser,
} from 'twenty-ui/display';
import {
FeatureFlagKey,
PermissionFlagType,
} from '~/generated-metadata/graphql';
import { PermissionFlagType } from '~/generated-metadata/graphql';
type UseActionRolePermissionFlagConfigParams = {
assignmentCapabilities?: {
@@ -30,8 +26,6 @@ type UseActionRolePermissionFlagConfigParams = {
export const useActionRolePermissionFlagConfig = ({
assignmentCapabilities,
}: UseActionRolePermissionFlagConfigParams = {}): SettingsRolePermissionsSettingPermission[] => {
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
const {
canBeAssignedToAgents = false,
canBeAssignedToUsers = false,
@@ -154,10 +148,6 @@ export const useActionRolePermissionFlagConfig = ({
canBeAssignedToUsers && !canBeAssignedToAgents && !canBeAssignedToApiKeys;
return allPermissions.filter((permission) => {
if (permission.key === PermissionFlagType.AI && !isAiEnabled) {
return false;
}
if (hasAssignmentCapabilities) {
if (canBeAssignedOnlyToAgents && !permission.isRelevantForAgents) {
return false;
@@ -179,6 +169,5 @@ export const useActionRolePermissionFlagConfig = ({
canBeAssignedToAgents,
canBeAssignedToUsers,
canBeAssignedToApiKeys,
isAiEnabled,
]);
};
@@ -1,5 +1,4 @@
import { type SettingsRolePermissionsSettingPermission } from '@/settings/roles/role-permissions/permission-flags/types/SettingsRolePermissionsSettingPermission';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { t } from '@lingui/core/macro';
import { useMemo } from 'react';
import {
@@ -17,10 +16,7 @@ import {
IconSpy,
IconUsers,
} from 'twenty-ui/display';
import {
FeatureFlagKey,
PermissionFlagType,
} from '~/generated-metadata/graphql';
import { PermissionFlagType } from '~/generated-metadata/graphql';
type UseSettingsRolePermissionFlagConfigParams = {
assignmentCapabilities?: {
@@ -33,8 +29,6 @@ type UseSettingsRolePermissionFlagConfigParams = {
export const useSettingsRolePermissionFlagConfig = ({
assignmentCapabilities,
}: UseSettingsRolePermissionFlagConfigParams = {}): SettingsRolePermissionsSettingPermission[] => {
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
const {
canBeAssignedToAgents = false,
canBeAssignedToUsers = false,
@@ -174,9 +168,6 @@ export const useSettingsRolePermissionFlagConfig = ({
canBeAssignedToUsers && !canBeAssignedToAgents && !canBeAssignedToApiKeys;
return allPermissions.filter((permission) => {
if (permission.key === PermissionFlagType.AI_SETTINGS && !isAiEnabled) {
return false;
}
if (hasAssignmentCapabilities) {
if (canBeAssignedOnlyToAgents && !permission.isRelevantForAgents) {
return false;
@@ -198,6 +189,5 @@ export const useSettingsRolePermissionFlagConfig = ({
canBeAssignedToAgents,
canBeAssignedToUsers,
canBeAssignedToApiKeys,
isAiEnabled,
]);
};
@@ -1,4 +1,3 @@
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
@@ -7,7 +6,6 @@ import { Checkbox } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { useContext } from 'react';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
const StyledCheckboxContainer = styled.div<{ disabled: boolean }>`
align-items: center;
@@ -51,7 +49,6 @@ export const SettingsRoleApplicability = ({
isEditable,
}: SettingsRoleApplicabilityProps) => {
const { theme } = useContext(ThemeContext);
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
const options = [
{
@@ -64,15 +61,11 @@ export const SettingsRoleApplicability = ({
label: t`Assignable to Agents`,
Icon: IconRobot,
},
...(isAiEnabled
? [
{
key: 'canBeAssignedToApiKeys' as const,
label: t`Assignable to API Keys`,
Icon: IconKey,
},
]
: []),
{
key: 'canBeAssignedToApiKeys' as const,
label: t`Assignable to API Keys`,
Icon: IconKey,
},
];
return (
<Section>
@@ -4,14 +4,12 @@ import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
import { sidePanelSearchObjectFilterState } from '@/side-panel/states/sidePanelSearchObjectFilterState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { SidePanelPages } from 'twenty-shared/types';
import { IconEdit } from 'twenty-ui/display';
import { IconButton } from 'twenty-ui/input';
import { useIsMobile } from 'twenty-ui/utilities';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledIconButtonContainer = styled.div`
@@ -20,7 +18,6 @@ const StyledIconButtonContainer = styled.div`
export const SidePanelTopBarRightCornerIcon = () => {
const isMobile = useIsMobile();
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
const sidePanelPage = useAtomStateValue(sidePanelPageState);
const { switchToNewChat } = useSwitchToNewAiChat();
const [sidePanelSearchObjectFilter, setSidePanelSearchObjectFilter] =
@@ -42,7 +39,7 @@ export const SidePanelTopBarRightCornerIcon = () => {
SidePanelPages.ViewPreviousAiChats,
].includes(sidePanelPage);
if (isMobile || !isAiEnabled || !isOnAskAiPage) {
if (isMobile || !isOnAskAiPage) {
return null;
}
@@ -10,11 +10,9 @@ import { FLOW_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constan
import { HUMAN_INPUT_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/HumanInputActions';
import { RECORD_ACTIONS } from '@/workflow/workflow-steps/workflow-actions/constants/RecordActions';
import { getActionIconColorOrThrow } from '@/workflow/workflow-steps/workflow-actions/utils/getActionIconColorOrThrow';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useLingui } from '@lingui/react/macro';
import { IconFunction } from 'twenty-ui/display';
import { MenuItem } from 'twenty-ui/navigation';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
export type WorkflowActionSelection = {
type: WorkflowActionType;
@@ -26,8 +24,6 @@ export const SidePanelWorkflowSelectAction = ({
}: {
onActionSelected: (selection: WorkflowActionSelection) => void;
}) => {
const isAiEnabled = useIsFeatureEnabled(FeatureFlagKey.IS_AI_ENABLED);
const { t } = useLingui();
const logicFunctions = useAtomStateValue(logicFunctionsSelector);
@@ -57,17 +53,13 @@ export const SidePanelWorkflowSelectAction = ({
onClick={handleActionClick}
/>
{isAiEnabled && (
<>
<SidePanelWorkflowSelectStepTitle>
{t`AI`}
</SidePanelWorkflowSelectStepTitle>
<WorkflowActionMenuItems
actions={AI_ACTIONS}
onClick={handleActionClick}
/>
</>
)}
<SidePanelWorkflowSelectStepTitle>
{t`AI`}
</SidePanelWorkflowSelectStepTitle>
<WorkflowActionMenuItems
actions={AI_ACTIONS}
onClick={handleActionClick}
/>
<SidePanelWorkflowSelectStepTitle>
{t`Flow`}