From 1c1378f8ebb23fa5cebd2ebb41606f17170f9f0d Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Tue, 13 Oct 2026 02:54:53 +0100 Subject: [PATCH 01/34] add add agent button --- src/pages/workspace/WorkspaceAgentsPage.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/WorkspaceAgentsPage.tsx b/src/pages/workspace/WorkspaceAgentsPage.tsx index 1e8e483a68f2..6b797814e006 100644 --- a/src/pages/workspace/WorkspaceAgentsPage.tsx +++ b/src/pages/workspace/WorkspaceAgentsPage.tsx @@ -1,3 +1,4 @@ +import Button from '@components/Button'; import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; import RenderHTML from '@components/RenderHTML'; @@ -16,12 +17,14 @@ import useWorkspaceAgents from '@hooks/useWorkspaceAgents'; import useWorkspaceDocumentTitle from '@hooks/useWorkspaceDocumentTitle'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; +import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {WorkspaceSplitNavigatorParamList} from '@navigation/types'; import CONST from '@src/CONST'; +import {DYNAMIC_ROUTES} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; @@ -51,8 +54,15 @@ function WorkspaceAgentsPage({route}: WorkspaceAgentsPageProps) { useWorkspaceDocumentTitle(policy?.name, 'agentsPage.title'); - // The new agent functionality will be added after CreateCompanyAgent is exposed - const newAgentButton = null; + const newAgentButton = ( + + ); const bulkActionsButtonOptions: Array>> = [ { From 04bc2f923dcf31acfaf2db2fc58823aeb62f6fad Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Tue, 13 Oct 2026 03:45:34 +0100 Subject: [PATCH 02/34] add role row for agent --- .../settings/Agents/DynamicAddAgentPage.tsx | 33 ++++++++++++++++--- src/types/form/AddAgentForm.ts | 2 ++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/src/pages/settings/Agents/DynamicAddAgentPage.tsx b/src/pages/settings/Agents/DynamicAddAgentPage.tsx index fd5495ba6906..e2ec397e8a2f 100644 --- a/src/pages/settings/Agents/DynamicAddAgentPage.tsx +++ b/src/pages/settings/Agents/DynamicAddAgentPage.tsx @@ -9,6 +9,8 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; +import ValuePicker from '@components/ValuePicker'; +import {getAssignableWorkspaceMemberRoleItems} from '@components/WorkspaceMemberRoleList'; import useBeforeRemove from '@hooks/useBeforeRemove'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; @@ -17,6 +19,7 @@ import useKeyboardState from '@hooks/useKeyboardState'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import usePolicy from '@hooks/usePolicy'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -43,8 +46,9 @@ import type {Errors} from '@src/types/onyx/OnyxCommon'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import type {TextInputKeyPressEvent} from 'react-native'; +import type {ValueOf} from 'type-fest'; -import React, {useCallback, useEffect, useRef} from 'react'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; import {Platform, View} from 'react-native'; import {PROMPT_MAX_AUTO_GROW_HEIGHT, PROMPT_MAX_HEIGHT_ON_KEYBOARD_OPEN_LANDSCAPE_MODE} from './const'; @@ -63,6 +67,7 @@ type DynamicAddAgentPageContentProps = { function DynamicAddAgentPageContent({route, template}: DynamicAddAgentPageContentProps) { const StyleUtils = useStyleUtils(); const policyID = route.params?.policyID; + const policy = usePolicy(policyID); const {translate} = useLocalize(); const styles = useThemeStyles(); const {windowWidth, windowHeight} = useWindowDimensions(); @@ -73,7 +78,7 @@ function DynamicAddAgentPageContent({route, template}: DynamicAddAgentPageConten const shouldAutoGrowPromptInput = Platform.OS !== 'web' && !isInLandscapeMode; const shouldUseScrollableLayout = shouldAutoGrowPromptInput || isInLandscapeMode; const shouldShrinkPromptInput = isInLandscapeMode && isKeyboardActive; - const {accountID: ownerAccountID, login: ownerLogin, displayName} = useCurrentUserPersonalDetails(); + const {accountID: currentUserAccountID, login: currentUserLogin, displayName} = useCurrentUserPersonalDetails(); const defaultAgentName = template?.name ?? (displayName ? translate('addAgentPage.defaultAgentName', displayName) : undefined); const defaultPrompt = template?.prompt ?? translate('addAgentPage.defaultPrompt'); const expensifyIcons = useMemoizedLazyExpensifyIcons(['Pencil']); @@ -82,6 +87,13 @@ function DynamicAddAgentPageContent({route, template}: DynamicAddAgentPageConten const hasSubmittedRef = useRef(false); const hasLeftPageRef = useRef(false); const formRef = useRef(null); + const availableRoleItems = getAssignableWorkspaceMemberRoleItems(translate, undefined, policy, currentUserLogin ?? ''); + const [role, setRole] = useState>(availableRoleItems.at(0)?.value ?? CONST.POLICY.ROLE.ADMIN); + const roleOptions = availableRoleItems.map((roleItem) => ({ + label: roleItem.text, + value: roleItem.value, + description: roleItem.alternateText, + })); const backPath = useDynamicBackPath(DYNAMIC_ROUTES.AGENT_ADD.path); const submitFormOnModEnter = (event: TextInputKeyPressEvent | KeyboardEvent) => { @@ -148,8 +160,8 @@ function DynamicAddAgentPageContent({route, template}: DynamicAddAgentPageConten // reportID it generates client-side, and CreateAgent creates the DM under that exact ID (see // CreateAgent.cpp), so we can navigate to the DM immediately, online or offline, without waiting. const {optimisticReportID, optimisticPersonalDetailPromise} = uploadedAvatar?.uri - ? createAgent(firstName, prompt, ownerAccountID, ownerLogin, undefined, buildFileFromAvatarCropResult(uploadedAvatar), uploadedAvatar.uri, policyID) - : createAgent(firstName, prompt, ownerAccountID, ownerLogin, selectedPresetID ?? AGENT_AVATARS.getRandomID(), undefined, undefined, policyID); + ? createAgent(firstName, prompt, currentUserAccountID, currentUserLogin, undefined, buildFileFromAvatarCropResult(uploadedAvatar), uploadedAvatar.uri, policyID) + : createAgent(firstName, prompt, currentUserAccountID, currentUserLogin, selectedPresetID ?? AGENT_AVATARS.getRandomID(), undefined, undefined, policyID); clearNewAgentTemplate(); @@ -247,6 +259,19 @@ function DynamicAddAgentPageContent({route, template}: DynamicAddAgentPageConten spellCheck={false} defaultValue={defaultAgentName} /> + {!!policyID && ( + + setRole(value as typeof role)} + value={role} + shouldShowTooltips={false} + /> + + )} ; @@ -14,6 +15,7 @@ type AddAgentForm = Form< { [INPUT_IDS.FIRST_NAME]: string; [INPUT_IDS.PROMPT]: string; + [INPUT_IDS.ROLE]: string; } >; From aef8001b79df8d36fdce7d426606dab21909164d Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Tue, 13 Oct 2026 04:09:50 +0100 Subject: [PATCH 03/34] prepare for company-agent creation --- .../settings/Agents/DynamicAddAgentPage.tsx | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/pages/settings/Agents/DynamicAddAgentPage.tsx b/src/pages/settings/Agents/DynamicAddAgentPage.tsx index e2ec397e8a2f..19924aa92404 100644 --- a/src/pages/settings/Agents/DynamicAddAgentPage.tsx +++ b/src/pages/settings/Agents/DynamicAddAgentPage.tsx @@ -39,6 +39,7 @@ import {clearNewAgentAvatarDraft, clearNewAgentTemplate, createAgent, setNewAgen import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; +import type {Route} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/AddAgentForm'; import type NewAgentTemplate from '@src/types/onyx/NewAgentTemplate'; @@ -156,12 +157,22 @@ function DynamicAddAgentPageContent({route, template}: DynamicAddAgentPageConten const firstName = values[INPUT_IDS.FIRST_NAME].trim() || defaultAgentName; const prompt = values[INPUT_IDS.PROMPT].trim(); - // Pure optimistic flow: `createAgent` writes the agent and the owner<->agent DM to Onyx under a - // reportID it generates client-side, and CreateAgent creates the DM under that exact ID (see - // CreateAgent.cpp), so we can navigate to the DM immediately, online or offline, without waiting. - const {optimisticReportID, optimisticPersonalDetailPromise} = uploadedAvatar?.uri - ? createAgent(firstName, prompt, currentUserAccountID, currentUserLogin, undefined, buildFileFromAvatarCropResult(uploadedAvatar), uploadedAvatar.uri, policyID) - : createAgent(firstName, prompt, currentUserAccountID, currentUserLogin, selectedPresetID ?? AGENT_AVATARS.getRandomID(), undefined, undefined, policyID); + // Pure optimistic flow: `createAgent/createCompanyAgent` writes the agent and the owner<->agent DM to Onyx under a + // reportID it generates client-side, and CreateAgent/CreateCompanyAgent creates the DM under that exact ID, + // so we can navigate to the DM immediately, online or offline, without waiting. + let optimisticReportID: string | undefined; + let optimisticPersonalDetailPromise: Promise | undefined; + let backToAgentsListRoute: Route | undefined; + + if (policyID) { + // TODO + backToAgentsListRoute = ROUTES.WORKSPACE_AGENTS.getRoute(policyID); + } else { + ({optimisticReportID, optimisticPersonalDetailPromise} = uploadedAvatar?.uri + ? createAgent(firstName, prompt, currentUserAccountID, currentUserLogin, undefined, buildFileFromAvatarCropResult(uploadedAvatar), uploadedAvatar.uri, policyID) + : createAgent(firstName, prompt, currentUserAccountID, currentUserLogin, selectedPresetID ?? AGENT_AVATARS.getRandomID(), undefined, undefined, policyID)); + backToAgentsListRoute = ROUTES.SETTINGS_AGENTS; + } clearNewAgentTemplate(); @@ -180,7 +191,7 @@ function DynamicAddAgentPageContent({route, template}: DynamicAddAgentPageConten if (isNarrowLayout) { // Reveal the DM under the modal before dismissing so we navigate directly to it in one animation, // instead of dismissing to the agents list first and navigating to the DM afterward. - Navigation.revealRouteBeforeDismissingModal(ROUTES.REPORT_WITH_ID.getRoute(optimisticReportID, undefined, undefined, ROUTES.SETTINGS_AGENTS), { + Navigation.revealRouteBeforeDismissingModal(ROUTES.REPORT_WITH_ID.getRoute(optimisticReportID, undefined, undefined, backToAgentsListRoute), { afterTransition: () => { clearNewAgentAvatarDraft(); }, From 57b0b8943f4e9feee8217f37cda2fe8583fd8dbb Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Tue, 13 Oct 2026 04:14:47 +0100 Subject: [PATCH 04/34] add CreateCompanyAgent api params --- .../API/parameters/CreateCompanyAgentParams.ts | 15 +++++++++++++++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 ++ 3 files changed, 18 insertions(+) create mode 100644 src/libs/API/parameters/CreateCompanyAgentParams.ts diff --git a/src/libs/API/parameters/CreateCompanyAgentParams.ts b/src/libs/API/parameters/CreateCompanyAgentParams.ts new file mode 100644 index 000000000000..d67fe6a7bf0a --- /dev/null +++ b/src/libs/API/parameters/CreateCompanyAgentParams.ts @@ -0,0 +1,15 @@ +import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; + +type CreateCompanyAgentParams = { + prompt: string; + policyID: string; + role: string; + firstName?: string; + customExpensifyAvatarID?: string; + file?: File | CustomRNImageManipulatorResult; + optimisticAccountID: number; + optimisticReportID: string; + createdReportActionID: string; +}; + +export default CreateCompanyAgentParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index a04551c6a393..f451a3d615ea 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -695,6 +695,7 @@ export type {default as UpdateDomainSecurityGroupParams} from './UpdateDomainSec export type {default as SetDefaultDomainSecurityGroupParams} from './SetDefaultDomainSecurityGroupParams'; export type {default as DeleteDomainSecurityGroupParams} from './DeleteDomainSecurityGroupParams'; export type {default as CreateAgentParams} from './CreateAgentParams'; +export type {default as CreateCompanyAgentParams} from './CreateCompanyAgentParams'; export type {default as CreateDomainSecurityGroupParams} from './CreateDomainSecurityGroupParams'; export type {default as UpdateAgentNameParams} from './UpdateAgentNameParams'; export type {default as UpdateAgentPromptParams} from './UpdateAgentPromptParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index d394852f9e2e..f5efddec85b1 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -770,6 +770,7 @@ const WRITE_COMMANDS = { SET_DEFAULT_DOMAIN_SECURITY_GROUP: 'SetDefaultDomainSecurityGroup', DELETE_DOMAIN_SECURITY_GROUP: 'DeleteDomainSecurityGroup', CREATE_AGENT: 'CreateAgent', + CREATE_COMPANY_AGENT: 'CreateCompanyAgent', CREATE_DOMAIN_SECURITY_GROUP: 'CreateDomainSecurityGroup', UPDATE_AGENT_NAME: 'UpdateAgentName', UPDATE_AGENT_PROMPT: 'UpdateAgentPrompt', @@ -1576,6 +1577,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_DEFAULT_DOMAIN_SECURITY_GROUP]: Parameters.SetDefaultDomainSecurityGroupParams; [WRITE_COMMANDS.DELETE_DOMAIN_SECURITY_GROUP]: Parameters.DeleteDomainSecurityGroupParams; [WRITE_COMMANDS.CREATE_AGENT]: Parameters.CreateAgentParams; + [WRITE_COMMANDS.CREATE_COMPANY_AGENT]: Parameters.CreateCompanyAgentParams; [WRITE_COMMANDS.UPDATE_AGENT_NAME]: Parameters.UpdateAgentNameParams; [WRITE_COMMANDS.UPDATE_AGENT_PROMPT]: Parameters.UpdateAgentPromptParams; [WRITE_COMMANDS.UPDATE_AGENT_AVATAR]: Parameters.UpdateAgentAvatarParams; From ecad0f38a5dd5a843e35eb6d80d5526161a8b305 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Tue, 13 Oct 2026 04:20:40 +0100 Subject: [PATCH 05/34] make ownerAccountID and ownerType optional as they cannot be predicted client-side --- src/hooks/useAgents.ts | 6 +++--- src/types/onyx/Agent.ts | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/hooks/useAgents.ts b/src/hooks/useAgents.ts index 14353dc55141..c6fd3229ae07 100644 --- a/src/hooks/useAgents.ts +++ b/src/hooks/useAgents.ts @@ -77,14 +77,14 @@ function useAgents() { let ownerDisplayName: string | undefined; let ownerPolicyID: string | undefined; if (agent.ownerType === CONST.AGENTS.OWNER_TYPE.PERSON) { - const ownerDetails = personalDetailsList?.[agent.ownerAccountID]; + const ownerDetails = agent.ownerAccountID ? personalDetailsList?.[agent.ownerAccountID] : undefined; ownerDisplayName = ownerDetails?.displayName ?? ownerDetails?.login; } else if (agent.ownerType === CONST.AGENTS.OWNER_TYPE.WORKSPACE) { - const policy = usingPolicyAccountIDPoliciesSelector(allPolicies, agent.ownerAccountID); + const policy = agent.ownerAccountID ? usingPolicyAccountIDPoliciesSelector(allPolicies, agent.ownerAccountID) : undefined; ownerDisplayName = policy?.name; ownerPolicyID = policy?.id; } else if (agent.ownerType === CONST.AGENTS.OWNER_TYPE.DOMAIN) { - const ownerDetails = personalDetailsList?.[agent.ownerAccountID]; + const ownerDetails = agent.ownerAccountID ? personalDetailsList?.[agent.ownerAccountID] : undefined; ownerDisplayName = ownerDetails?.login ? Str.extractEmailDomain(ownerDetails.login) : undefined; } diff --git a/src/types/onyx/Agent.ts b/src/types/onyx/Agent.ts index c13225ad111f..8e7b97608031 100644 --- a/src/types/onyx/Agent.ts +++ b/src/types/onyx/Agent.ts @@ -13,10 +13,10 @@ type Agent = OnyxValueWithOfflineFeedback<{ prompt: string; /** The agent owner account ID */ - ownerAccountID: number; + ownerAccountID?: number; /** The agent owner account type (person, workspace or domain) */ - ownerType: AgentOwnerType; + ownerType?: AgentOwnerType; /** Errors from the last failed action */ errors?: Errors | null; From f8e5ab798eeb8fa488f2eae4949b5cc78e596fe6 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Tue, 13 Oct 2026 04:41:14 +0100 Subject: [PATCH 06/34] define prepareNewAgentOnyxData to extract common agent creation logic --- src/libs/actions/Agent.ts | 49 +++++++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/src/libs/actions/Agent.ts b/src/libs/actions/Agent.ts index 012fbde98e98..a887a5158bea 100644 --- a/src/libs/actions/Agent.ts +++ b/src/libs/actions/Agent.ts @@ -40,15 +40,15 @@ function openProfilePage() { read(READ_COMMANDS.OPEN_PROFILE_PAGE, null); } -function createAgent( - firstName: string | undefined, +function prepareNewAgentOnyxData( + currentUserAccountID: number, + currentUserLogin: string | undefined, prompt: string, - ownerAccountID: number, - ownerLogin: string | undefined, + policyID?: string, + role?: string, + firstName?: string, customExpensifyAvatarID?: string, - file?: File | CustomRNImageManipulatorResult, optimisticAvatarURI?: string, - policyID?: string, ) { const optimisticAccountID = Number(generateReportID()); @@ -59,7 +59,7 @@ function createAgent( avatarURI = optimisticAvatarURI; } - // Optimistic row for Settings > Agents until CREATE_AGENT responds. Omit `login` (server-assigned). + // Optimistic row for Settings > Agents until BE responds. Omit `login` (server-assigned). const optimisticPersonalDetail = { accountID: optimisticAccountID, displayName: firstName, @@ -71,16 +71,17 @@ function createAgent( // Generate the DM report ID client-side so it can be written to Onyx and opened immediately. const optimisticReportID = generateReportID(); const optimisticChatReport = buildOptimisticChatReport({ - participantList: [ownerAccountID, optimisticAccountID], - ownerAccountID, + participantList: [currentUserAccountID, optimisticAccountID], + ownerAccountID: currentUserAccountID, optimisticReportID, - currentUserAccountID: ownerAccountID, + currentUserAccountID, }); + // Reuse the client-generated CREATED action ID when CreateAgent creates the DM. const createdReportActionID = rand64(); const optimisticCreatedAction = buildOptimisticCreatedReportAction({ - emailCreatingAction: ownerLogin ?? CONST.REPORT.OWNER_EMAIL_FAKE, - currentUserAccountID: ownerAccountID, + emailCreatingAction: currentUserLogin ?? CONST.REPORT.OWNER_EMAIL_FAKE, + currentUserAccountID, optimisticReportActionID: createdReportActionID, }); @@ -150,6 +151,30 @@ function createAgent( }, ]; + return {optimisticData, successData, failureData, optimisticAccountID, avatarURI, optimisticReportID, createdReportActionID, optimisticPersonalDetailPromise}; +} + +function createAgent( + firstName: string | undefined, + prompt: string, + currentUserAccountID: number, + currentUserLogin: string | undefined, + customExpensifyAvatarID?: string, + file?: File | CustomRNImageManipulatorResult, + optimisticAvatarURI?: string, + policyID?: string, +) { + const {optimisticData, successData, failureData, optimisticAccountID, avatarURI, optimisticReportID, createdReportActionID, optimisticPersonalDetailPromise} = prepareNewAgentOnyxData( + currentUserAccountID, + currentUserLogin, + prompt, + policyID, + undefined, + firstName, + customExpensifyAvatarID, + optimisticAvatarURI, + ); + write( // Flag this as the user's personal agent; the backend makes personal agents a full co-pilot of the creator. // optimisticReportID is the owner<->agent DM's optimistic reportID; CreateAgent creates the DM under this exact ID. From 757af6511348feff0e6f9f470273a6c523b3dd31 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Tue, 13 Oct 2026 04:58:32 +0100 Subject: [PATCH 07/34] add createCompanyAgent --- src/libs/actions/Agent.ts | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/Agent.ts b/src/libs/actions/Agent.ts index a887a5158bea..3f6e7d89d2b2 100644 --- a/src/libs/actions/Agent.ts +++ b/src/libs/actions/Agent.ts @@ -44,8 +44,6 @@ function prepareNewAgentOnyxData( currentUserAccountID: number, currentUserLogin: string | undefined, prompt: string, - policyID?: string, - role?: string, firstName?: string, customExpensifyAvatarID?: string, optimisticAvatarURI?: string, @@ -168,8 +166,6 @@ function createAgent( currentUserAccountID, currentUserLogin, prompt, - policyID, - undefined, firstName, customExpensifyAvatarID, optimisticAvatarURI, @@ -186,6 +182,35 @@ function createAgent( return {optimisticAccountID, avatarURI, optimisticReportID, optimisticPersonalDetailPromise}; } +function createCompanyAgent( + currentUserAccountID: number, + currentUserLogin: string | undefined, + prompt: string, + policyID: string, + role: string, + firstName?: string, + customExpensifyAvatarID?: string, + file?: File | CustomRNImageManipulatorResult, + optimisticAvatarURI?: string, +) { + const {optimisticData, successData, failureData, optimisticAccountID, avatarURI, optimisticReportID, createdReportActionID, optimisticPersonalDetailPromise} = prepareNewAgentOnyxData( + currentUserAccountID, + currentUserLogin, + prompt, + firstName, + customExpensifyAvatarID, + optimisticAvatarURI, + ); + + write( + WRITE_COMMANDS.CREATE_COMPANY_AGENT, + {prompt, policyID, role, firstName, customExpensifyAvatarID, file, optimisticAccountID, optimisticReportID, createdReportActionID}, + {optimisticData, successData, failureData}, + ); + + return {optimisticAccountID, avatarURI, optimisticReportID, optimisticPersonalDetailPromise}; +} + /** * Stash the template chosen in the "New agent" picker so the custom-agent builder can open pre-filled. */ @@ -458,6 +483,7 @@ export { openAgentsPage, openProfilePage, createAgent, + createCompanyAgent, setNewAgentTemplate, clearNewAgentTemplate, clearAgentError, From 2c460a5eead6991d50d16cd9962ba3e08bad360d Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Tue, 13 Oct 2026 05:05:36 +0100 Subject: [PATCH 08/34] call createCompanyAgent --- src/pages/settings/Agents/DynamicAddAgentPage.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/settings/Agents/DynamicAddAgentPage.tsx b/src/pages/settings/Agents/DynamicAddAgentPage.tsx index 19924aa92404..b1152b5b343b 100644 --- a/src/pages/settings/Agents/DynamicAddAgentPage.tsx +++ b/src/pages/settings/Agents/DynamicAddAgentPage.tsx @@ -34,7 +34,7 @@ import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavig import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import type {AvatarSource} from '@libs/UserAvatarUtils'; -import {clearNewAgentAvatarDraft, clearNewAgentTemplate, createAgent, setNewAgentAvatarPreset} from '@userActions/Agent'; +import {clearNewAgentAvatarDraft, clearNewAgentTemplate, createAgent, createCompanyAgent, setNewAgentAvatarPreset} from '@userActions/Agent'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -165,7 +165,9 @@ function DynamicAddAgentPageContent({route, template}: DynamicAddAgentPageConten let backToAgentsListRoute: Route | undefined; if (policyID) { - // TODO + ({optimisticReportID, optimisticPersonalDetailPromise} = uploadedAvatar?.uri + ? createCompanyAgent(currentUserAccountID, currentUserLogin, prompt, policyID, role, firstName, undefined, buildFileFromAvatarCropResult(uploadedAvatar), uploadedAvatar.uri) + : createCompanyAgent(currentUserAccountID, currentUserLogin, prompt, policyID, role, firstName, selectedPresetID ?? AGENT_AVATARS.getRandomID())); backToAgentsListRoute = ROUTES.WORKSPACE_AGENTS.getRoute(policyID); } else { ({optimisticReportID, optimisticPersonalDetailPromise} = uploadedAvatar?.uri From 1dc4f7bce75ac3af5609ebe7323b893f5b5e2ae5 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:11:19 +0100 Subject: [PATCH 09/34] fix AddAgentPageTest --- tests/unit/pages/settings/AddAgentPageTest.tsx | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/tests/unit/pages/settings/AddAgentPageTest.tsx b/tests/unit/pages/settings/AddAgentPageTest.tsx index 5bf4e0067b73..7952499c706e 100644 --- a/tests/unit/pages/settings/AddAgentPageTest.tsx +++ b/tests/unit/pages/settings/AddAgentPageTest.tsx @@ -11,6 +11,7 @@ import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import AddAgentPage from '@pages/settings/Agents/DynamicAddAgentPage'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; @@ -35,6 +36,16 @@ const mockCreateAgent = jest.fn<{optimisticAccountID: number; avatarURI: string resolveOptimisticPersonalDetail = resolve; }), })); +const mockCreateCompanyAgent = jest.fn<{optimisticAccountID: number; avatarURI: string | undefined; optimisticReportID: string; optimisticPersonalDetailPromise: Promise}, unknown[]>( + () => ({ + optimisticAccountID: -123456, + avatarURI: undefined, + optimisticReportID: OPTIMISTIC_REPORT_ID, + optimisticPersonalDetailPromise: new Promise((resolve) => { + resolveOptimisticPersonalDetail = resolve; + }), + }), +); const mockSetNewAgentAvatarPreset = jest.fn(); const mockClearNewAgentAvatarDraft = jest.fn(); const mockClearNewAgentTemplate = jest.fn(); @@ -45,6 +56,7 @@ let mockTemplate: {name: string; prompt: string; avatarID: string} | undefined; jest.mock('@userActions/Agent', () => ({ createAgent: (...args: unknown[]) => mockCreateAgent(...args), + createCompanyAgent: (...args: unknown[]) => mockCreateCompanyAgent(...args), setNewAgentAvatarPreset: (...args: unknown[]) => mockSetNewAgentAvatarPreset(...args), clearNewAgentAvatarDraft: (...args: unknown[]) => mockClearNewAgentAvatarDraft(...args), clearNewAgentTemplate: (...args: unknown[]) => mockClearNewAgentTemplate(...args), @@ -354,16 +366,16 @@ describe('AddAgentPage', () => { expect(mockClearNewAgentAvatarDraft).toHaveBeenCalledTimes(1); }); - it('forwards policyID from route params to createAgent', async () => { + it('forwards policyID from route params to createCompanyAgent', async () => { renderAddAgentPage({policyID: 'POL_42'}); mockFormOnSubmit?.({firstName: 'Bot', prompt: 'Reject gambling.'}); - expect(mockCreateAgent).toHaveBeenCalledWith('Bot', 'Reject gambling.', OWNER_ACCOUNT_ID, OWNER_LOGIN, 'bot-avatar--blue', undefined, undefined, 'POL_42'); + expect(mockCreateCompanyAgent).toHaveBeenCalledWith(OWNER_ACCOUNT_ID, OWNER_LOGIN, 'Reject gambling.', 'POL_42', CONST.POLICY.ROLE.ADMIN, 'Bot', 'bot-avatar--blue'); resolveOptimisticPersonalDetail?.(); await Promise.resolve(); expect(mockRevealRouteBeforeDismissingModal).toHaveBeenCalledWith( - ROUTES.REPORT_WITH_ID.getRoute(OPTIMISTIC_REPORT_ID, undefined, undefined, ROUTES.SETTINGS_AGENTS), + ROUTES.REPORT_WITH_ID.getRoute(OPTIMISTIC_REPORT_ID, undefined, undefined, ROUTES.WORKSPACE_AGENTS.getRoute('POL_42')), expect.anything(), ); expect(getRevealAfterTransition()).toEqual(expect.any(Function)); From adc41422a4bc3100b41448363268a7954c66730a Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:16:35 +0100 Subject: [PATCH 10/34] fix react compiler warnings --- src/pages/settings/Agents/DynamicAddAgentPage.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/pages/settings/Agents/DynamicAddAgentPage.tsx b/src/pages/settings/Agents/DynamicAddAgentPage.tsx index b1152b5b343b..d0957c409b7c 100644 --- a/src/pages/settings/Agents/DynamicAddAgentPage.tsx +++ b/src/pages/settings/Agents/DynamicAddAgentPage.tsx @@ -165,14 +165,18 @@ function DynamicAddAgentPageContent({route, template}: DynamicAddAgentPageConten let backToAgentsListRoute: Route | undefined; if (policyID) { - ({optimisticReportID, optimisticPersonalDetailPromise} = uploadedAvatar?.uri + const result = uploadedAvatar?.uri ? createCompanyAgent(currentUserAccountID, currentUserLogin, prompt, policyID, role, firstName, undefined, buildFileFromAvatarCropResult(uploadedAvatar), uploadedAvatar.uri) - : createCompanyAgent(currentUserAccountID, currentUserLogin, prompt, policyID, role, firstName, selectedPresetID ?? AGENT_AVATARS.getRandomID())); + : createCompanyAgent(currentUserAccountID, currentUserLogin, prompt, policyID, role, firstName, selectedPresetID ?? AGENT_AVATARS.getRandomID()); + optimisticReportID = result.optimisticReportID; + optimisticPersonalDetailPromise = result.optimisticPersonalDetailPromise; backToAgentsListRoute = ROUTES.WORKSPACE_AGENTS.getRoute(policyID); } else { - ({optimisticReportID, optimisticPersonalDetailPromise} = uploadedAvatar?.uri + const result = uploadedAvatar?.uri ? createAgent(firstName, prompt, currentUserAccountID, currentUserLogin, undefined, buildFileFromAvatarCropResult(uploadedAvatar), uploadedAvatar.uri, policyID) - : createAgent(firstName, prompt, currentUserAccountID, currentUserLogin, selectedPresetID ?? AGENT_AVATARS.getRandomID(), undefined, undefined, policyID)); + : createAgent(firstName, prompt, currentUserAccountID, currentUserLogin, selectedPresetID ?? AGENT_AVATARS.getRandomID(), undefined, undefined, policyID); + optimisticReportID = result.optimisticReportID; + optimisticPersonalDetailPromise = result.optimisticPersonalDetailPromise; backToAgentsListRoute = ROUTES.SETTINGS_AGENTS; } From 41228ff1882826c624fca345818a0ccfe12a3967 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:20:05 +0100 Subject: [PATCH 11/34] ts --- src/hooks/useAgents.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useAgents.ts b/src/hooks/useAgents.ts index 4ed146df9f41..ea5fdb91b77a 100644 --- a/src/hooks/useAgents.ts +++ b/src/hooks/useAgents.ts @@ -81,7 +81,7 @@ function useAgents() { const ownerDetails = agent.ownerAccountID ? personalDetailsList?.[agent.ownerAccountID] : undefined; ownerDisplayName = ownerDetails?.displayName ?? ownerDetails?.login; } else if (agent.ownerType === CONST.AGENTS.OWNER_TYPE.WORKSPACE) { - const policy = policiesByAccountID.get(agent.ownerAccountID); + const policy = agent.ownerAccountID ? policiesByAccountID.get(agent.ownerAccountID) : undefined; ownerDisplayName = policy?.name; ownerPolicyID = policy?.id; } else if (agent.ownerType === CONST.AGENTS.OWNER_TYPE.DOMAIN) { From a06455ffe495c7c860b9158a1b4b057128c9ad6d Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:22:50 +0100 Subject: [PATCH 12/34] lint --- config/eslint/eslint.seatbelt.tsv | 1 + 1 file changed, 1 insertion(+) diff --git a/config/eslint/eslint.seatbelt.tsv b/config/eslint/eslint.seatbelt.tsv index 5938652fdb39..306026527794 100644 --- a/config/eslint/eslint.seatbelt.tsv +++ b/config/eslint/eslint.seatbelt.tsv @@ -1234,6 +1234,7 @@ "../../src/pages/settings/AboutPage/AboutPage.tsx" "no-restricted-imports" 1 "../../src/pages/settings/AboutPage/AboutPage.tsx" "react-hooks/refs" 1 "../../src/pages/settings/Agents/AgentsPage.tsx" "no-restricted-imports" 1 +"../../src/pages/settings/Agents/DynamicAddAgentPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 "../../src/pages/settings/Agents/DynamicAddAgentPage.tsx" "no-restricted-imports" 1 "../../src/pages/settings/Agents/DynamicEditAgentPage.tsx" "no-restricted-imports" 1 "../../src/pages/settings/Agents/DynamicNewAgentPage.tsx" "no-restricted-imports" 1 From a4df129e85a5a68d22bba1285c9e44a6fea8cbe0 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:24:20 +0100 Subject: [PATCH 13/34] add comment --- src/hooks/useWorkspaceMembers.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/hooks/useWorkspaceMembers.ts b/src/hooks/useWorkspaceMembers.ts index 3e7e1d5a7147..f492151be1dd 100644 --- a/src/hooks/useWorkspaceMembers.ts +++ b/src/hooks/useWorkspaceMembers.ts @@ -16,6 +16,9 @@ import useNetwork from './useNetwork'; import {usePersonalDetailsByLogins} from './usePersonalDetailByLogin'; import {useAllPersonalDetails} from './usePersonalDetails'; +/** + * Hook that returns a workspace's members joined with personal details, filling in fallback identities when details haven't loaded and filtering out Expensify team members on Expensify-owned policies. + */ function useWorkspaceMembers(policy: OnyxEntry) { const policyID = policy?.id; const policyOwner = policy?.owner; From 81c9619576b3c4b3e653773eb22f4b7be86fc7ef Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:51:31 +0100 Subject: [PATCH 14/34] pass policy object to createCompanyAgent and remove policyID from createAgent --- src/libs/actions/Agent.ts | 7 +++---- .../settings/Agents/DynamicAddAgentPage.tsx | 17 ++++++++--------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/libs/actions/Agent.ts b/src/libs/actions/Agent.ts index 3f6e7d89d2b2..a8b9d736ce90 100644 --- a/src/libs/actions/Agent.ts +++ b/src/libs/actions/Agent.ts @@ -160,7 +160,6 @@ function createAgent( customExpensifyAvatarID?: string, file?: File | CustomRNImageManipulatorResult, optimisticAvatarURI?: string, - policyID?: string, ) { const {optimisticData, successData, failureData, optimisticAccountID, avatarURI, optimisticReportID, createdReportActionID, optimisticPersonalDetailPromise} = prepareNewAgentOnyxData( currentUserAccountID, @@ -175,7 +174,7 @@ function createAgent( // Flag this as the user's personal agent; the backend makes personal agents a full co-pilot of the creator. // optimisticReportID is the owner<->agent DM's optimistic reportID; CreateAgent creates the DM under this exact ID. WRITE_COMMANDS.CREATE_AGENT, - {firstName, prompt, customExpensifyAvatarID, file, policyID, optimisticAccountID: String(optimisticAccountID), isPersonalAgent: true, optimisticReportID, createdReportActionID}, + {firstName, prompt, customExpensifyAvatarID, file, optimisticAccountID: String(optimisticAccountID), isPersonalAgent: true, optimisticReportID, createdReportActionID}, {optimisticData, successData, failureData}, ); @@ -186,7 +185,7 @@ function createCompanyAgent( currentUserAccountID: number, currentUserLogin: string | undefined, prompt: string, - policyID: string, + policy: Policy, role: string, firstName?: string, customExpensifyAvatarID?: string, @@ -204,7 +203,7 @@ function createCompanyAgent( write( WRITE_COMMANDS.CREATE_COMPANY_AGENT, - {prompt, policyID, role, firstName, customExpensifyAvatarID, file, optimisticAccountID, optimisticReportID, createdReportActionID}, + {prompt, policyID: policy.id, role, firstName, customExpensifyAvatarID, file, optimisticAccountID, optimisticReportID, createdReportActionID}, {optimisticData, successData, failureData}, ); diff --git a/src/pages/settings/Agents/DynamicAddAgentPage.tsx b/src/pages/settings/Agents/DynamicAddAgentPage.tsx index d0957c409b7c..ec0448beacdf 100644 --- a/src/pages/settings/Agents/DynamicAddAgentPage.tsx +++ b/src/pages/settings/Agents/DynamicAddAgentPage.tsx @@ -67,8 +67,7 @@ type DynamicAddAgentPageContentProps = { function DynamicAddAgentPageContent({route, template}: DynamicAddAgentPageContentProps) { const StyleUtils = useStyleUtils(); - const policyID = route.params?.policyID; - const policy = usePolicy(policyID); + const policy = usePolicy(route.params.policyID); const {translate} = useLocalize(); const styles = useThemeStyles(); const {windowWidth, windowHeight} = useWindowDimensions(); @@ -164,17 +163,17 @@ function DynamicAddAgentPageContent({route, template}: DynamicAddAgentPageConten let optimisticPersonalDetailPromise: Promise | undefined; let backToAgentsListRoute: Route | undefined; - if (policyID) { + if (policy) { const result = uploadedAvatar?.uri - ? createCompanyAgent(currentUserAccountID, currentUserLogin, prompt, policyID, role, firstName, undefined, buildFileFromAvatarCropResult(uploadedAvatar), uploadedAvatar.uri) - : createCompanyAgent(currentUserAccountID, currentUserLogin, prompt, policyID, role, firstName, selectedPresetID ?? AGENT_AVATARS.getRandomID()); + ? createCompanyAgent(currentUserAccountID, currentUserLogin, prompt, policy, role, firstName, undefined, buildFileFromAvatarCropResult(uploadedAvatar), uploadedAvatar.uri) + : createCompanyAgent(currentUserAccountID, currentUserLogin, prompt, policy, role, firstName, selectedPresetID ?? AGENT_AVATARS.getRandomID()); optimisticReportID = result.optimisticReportID; optimisticPersonalDetailPromise = result.optimisticPersonalDetailPromise; - backToAgentsListRoute = ROUTES.WORKSPACE_AGENTS.getRoute(policyID); + backToAgentsListRoute = ROUTES.WORKSPACE_AGENTS.getRoute(policy.id); } else { const result = uploadedAvatar?.uri - ? createAgent(firstName, prompt, currentUserAccountID, currentUserLogin, undefined, buildFileFromAvatarCropResult(uploadedAvatar), uploadedAvatar.uri, policyID) - : createAgent(firstName, prompt, currentUserAccountID, currentUserLogin, selectedPresetID ?? AGENT_AVATARS.getRandomID(), undefined, undefined, policyID); + ? createAgent(firstName, prompt, currentUserAccountID, currentUserLogin, undefined, buildFileFromAvatarCropResult(uploadedAvatar), uploadedAvatar.uri) + : createAgent(firstName, prompt, currentUserAccountID, currentUserLogin, selectedPresetID ?? AGENT_AVATARS.getRandomID(), undefined, undefined); optimisticReportID = result.optimisticReportID; optimisticPersonalDetailPromise = result.optimisticPersonalDetailPromise; backToAgentsListRoute = ROUTES.SETTINGS_AGENTS; @@ -276,7 +275,7 @@ function DynamicAddAgentPageContent({route, template}: DynamicAddAgentPageConten spellCheck={false} defaultValue={defaultAgentName} /> - {!!policyID && ( + {!!policy && ( Date: Mon, 19 Oct 2026 13:57:48 +0100 Subject: [PATCH 15/34] fix optimistic data for agent owner --- src/libs/actions/Agent.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/Agent.ts b/src/libs/actions/Agent.ts index a8b9d736ce90..9adc42b3a421 100644 --- a/src/libs/actions/Agent.ts +++ b/src/libs/actions/Agent.ts @@ -44,6 +44,7 @@ function prepareNewAgentOnyxData( currentUserAccountID: number, currentUserLogin: string | undefined, prompt: string, + policy?: Policy, firstName?: string, customExpensifyAvatarID?: string, optimisticAvatarURI?: string, @@ -87,7 +88,11 @@ function prepareNewAgentOnyxData( { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.AGENT}${optimisticAccountID}`, - value: {prompt, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}, + value: { + prompt, + ownerAccountID: policy?.policyAccountID, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + }, }, { onyxMethod: Onyx.METHOD.SET, @@ -127,8 +132,6 @@ function prepareNewAgentOnyxData( onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.AGENT}${optimisticAccountID}`, value: { - prompt, - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, errors: getMicroSecondOnyxErrorWithTranslationKey('agentsPage.error.genericAdd'), }, }, @@ -165,6 +168,7 @@ function createAgent( currentUserAccountID, currentUserLogin, prompt, + undefined, firstName, customExpensifyAvatarID, optimisticAvatarURI, @@ -196,6 +200,7 @@ function createCompanyAgent( currentUserAccountID, currentUserLogin, prompt, + policy, firstName, customExpensifyAvatarID, optimisticAvatarURI, From 5e37f696bf1b60c5aa98ccdb7858018c68c7f4bf Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Mon, 19 Oct 2026 14:09:03 +0100 Subject: [PATCH 16/34] fix AddAgentPageTest --- tests/unit/pages/settings/AddAgentPageTest.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/tests/unit/pages/settings/AddAgentPageTest.tsx b/tests/unit/pages/settings/AddAgentPageTest.tsx index 7952499c706e..65408dd50a17 100644 --- a/tests/unit/pages/settings/AddAgentPageTest.tsx +++ b/tests/unit/pages/settings/AddAgentPageTest.tsx @@ -53,6 +53,7 @@ let mockAvatarDraft: {customExpensifyAvatarID?: string; uploadedAvatar?: {uri: s let mockAvatarDraftStatus: 'loading' | 'loaded' = 'loaded'; let mockIsNarrowLayout = true; let mockTemplate: {name: string; prompt: string; avatarID: string} | undefined; +let mockPolicy: {id: string; policyAccountID: number} | undefined; jest.mock('@userActions/Agent', () => ({ createAgent: (...args: unknown[]) => mockCreateAgent(...args), @@ -103,6 +104,9 @@ jest.mock('@hooks/useOnyx', () => { if (key === onyxKeys.NEW_AGENT_TEMPLATE) { return [mockTemplate, {status: 'loaded'}] as const; } + if (key.startsWith(onyxKeys.COLLECTION.POLICY)) { + return [mockPolicy, {status: 'loaded'}] as const; + } return [undefined, {status: 'loaded'}] as const; }), }; @@ -222,6 +226,7 @@ describe('AddAgentPage', () => { mockAvatarDraftStatus = 'loaded'; mockIsNarrowLayout = true; mockTemplate = undefined; + mockPolicy = undefined; mockUseCurrentUserPersonalDetails.mockReturnValue({accountID: OWNER_ACCOUNT_ID, login: OWNER_LOGIN}); mockAvatarOnPress = undefined; resolveOptimisticPersonalDetail = undefined; @@ -351,7 +356,7 @@ describe('AddAgentPage', () => { mockFormOnSubmit?.({firstName: 'Bot', prompt: 'Reject gambling.'}); - expect(mockCreateAgent).toHaveBeenCalledWith('Bot', 'Reject gambling.', OWNER_ACCOUNT_ID, OWNER_LOGIN, 'bot-avatar--blue', undefined, undefined, undefined); + expect(mockCreateAgent).toHaveBeenCalledWith('Bot', 'Reject gambling.', OWNER_ACCOUNT_ID, OWNER_LOGIN, 'bot-avatar--blue', undefined, undefined); expect(mockClearNewAgentTemplate).toHaveBeenCalledTimes(1); expect(mockClearNewAgentAvatarDraft).not.toHaveBeenCalled(); expect(mockRevealRouteBeforeDismissingModal).not.toHaveBeenCalled(); @@ -367,11 +372,13 @@ describe('AddAgentPage', () => { }); it('forwards policyID from route params to createCompanyAgent', async () => { + mockPolicy = {id: 'POL_42', policyAccountID: 5444}; + renderAddAgentPage({policyID: 'POL_42'}); mockFormOnSubmit?.({firstName: 'Bot', prompt: 'Reject gambling.'}); - expect(mockCreateCompanyAgent).toHaveBeenCalledWith(OWNER_ACCOUNT_ID, OWNER_LOGIN, 'Reject gambling.', 'POL_42', CONST.POLICY.ROLE.ADMIN, 'Bot', 'bot-avatar--blue'); + expect(mockCreateCompanyAgent).toHaveBeenCalledWith(OWNER_ACCOUNT_ID, OWNER_LOGIN, 'Reject gambling.', mockPolicy, CONST.POLICY.ROLE.ADMIN, 'Bot', 'bot-avatar--blue'); resolveOptimisticPersonalDetail?.(); await Promise.resolve(); expect(mockRevealRouteBeforeDismissingModal).toHaveBeenCalledWith( @@ -427,7 +434,7 @@ describe('AddAgentPage', () => { mockFormOnSubmit?.({firstName: 'Bot', prompt: 'Reject gambling.'}); - expect(mockCreateAgent).toHaveBeenCalledWith('Bot', 'Reject gambling.', OWNER_ACCOUNT_ID, OWNER_LOGIN, 'bot-avatar--blue', undefined, undefined, undefined); + expect(mockCreateAgent).toHaveBeenCalledWith('Bot', 'Reject gambling.', OWNER_ACCOUNT_ID, OWNER_LOGIN, 'bot-avatar--blue', undefined, undefined); }); it('creates the agent with the reconstructed uploaded file when a photo was uploaded', () => { From cf1216e49f3585f772e7507c78cf2823930e24ce Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Mon, 19 Oct 2026 14:22:52 +0100 Subject: [PATCH 17/34] fix AgentActionTest --- tests/unit/AgentActionTest.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/tests/unit/AgentActionTest.ts b/tests/unit/AgentActionTest.ts index a3236debd56f..4ea9c96021e8 100644 --- a/tests/unit/AgentActionTest.ts +++ b/tests/unit/AgentActionTest.ts @@ -3,7 +3,7 @@ import {WRITE_COMMANDS} from '@libs/API/types'; import Navigation from '@libs/Navigation/Navigation'; import {isRecord} from '@libs/ObjectUtils'; -import {clearAgentAvatarUpdateError, clearAgentUpdateError, createAgent, deleteAgent, updateAgentAvatar, updateAgentName, updateAgentPrompt} from '@userActions/Agent'; +import {clearAgentAvatarUpdateError, clearAgentUpdateError, createAgent, createCompanyAgent, deleteAgent, updateAgentAvatar, updateAgentName, updateAgentPrompt} from '@userActions/Agent'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -227,9 +227,16 @@ describe('createAgent', () => { }); it('forwards policyID in the write params when provided', () => { - createAgent('Bot', 'My prompt', OWNER_ACCOUNT_ID, OWNER_LOGIN, undefined, undefined, undefined, 'POLICY_42'); - - expect(mockWrite).toHaveBeenCalledWith(WRITE_COMMANDS.CREATE_AGENT, expect.objectContaining({firstName: 'Bot', prompt: 'My prompt', policyID: 'POLICY_42'}), expect.any(Object)); + const policy = { + ...createRandomPolicy(0), + id: 'POLICY_42', + }; + createCompanyAgent(OWNER_ACCOUNT_ID, OWNER_LOGIN, 'My prompt', policy, CONST.POLICY.ROLE.ADMIN, 'Bot'); + expect(mockWrite).toHaveBeenCalledWith( + WRITE_COMMANDS.CREATE_COMPANY_AGENT, + expect.objectContaining({firstName: 'Bot', prompt: 'My prompt', policyID: 'POLICY_42'}), + expect.any(Object), + ); }); it('returns the optimistic accountID and avatarURI so callers can chain follow-up navigation', () => { @@ -326,7 +333,7 @@ describe('createAgent', () => { }); it('omits login on the optimistic personal detail entry — the real email is server-assigned', () => { - createAgent('Bot', 'My prompt', OWNER_ACCOUNT_ID, OWNER_LOGIN, undefined, undefined, undefined, 'POLICY_42'); + createAgent('Bot', 'My prompt', OWNER_ACCOUNT_ID, OWNER_LOGIN, undefined, undefined, undefined); const accountID = getOptimisticAccountID(); const entry = getOptimisticPersonalDetailEntry(accountID); @@ -360,7 +367,7 @@ describe('createAgent', () => { expect(mockWrite).toHaveBeenCalledWith(WRITE_COMMANDS.CREATE_AGENT, expect.objectContaining({optimisticAccountID: String(result.optimisticAccountID)}), expect.any(Object)); }); - it('failure data preserves optimistic personal detail and merges errors onto the prompt entry', () => { + it('failure data merges errors onto the prompt entry', () => { createAgent('Bot', 'My prompt', OWNER_ACCOUNT_ID, OWNER_LOGIN); const {failureData} = getWriteOptions(); @@ -372,11 +379,6 @@ describe('createAgent', () => { isOptimisticPersonalDetail: true, }); const promptValue = getUpdateRecord(failureData, `${ONYXKEYS.COLLECTION.AGENT}${accountID}`); - - expect(promptValue).toMatchObject({ - prompt: 'My prompt', - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, - }); expect(promptValue.errors).toBeTruthy(); }); }); From d21c357b64ff2b6a8343b8140e17921dd43d49e8 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Tue, 20 Oct 2026 07:54:21 +0100 Subject: [PATCH 18/34] fix crash --- src/pages/settings/Agents/DynamicAddAgentPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/settings/Agents/DynamicAddAgentPage.tsx b/src/pages/settings/Agents/DynamicAddAgentPage.tsx index ec0448beacdf..cd9cccee5078 100644 --- a/src/pages/settings/Agents/DynamicAddAgentPage.tsx +++ b/src/pages/settings/Agents/DynamicAddAgentPage.tsx @@ -67,7 +67,7 @@ type DynamicAddAgentPageContentProps = { function DynamicAddAgentPageContent({route, template}: DynamicAddAgentPageContentProps) { const StyleUtils = useStyleUtils(); - const policy = usePolicy(route.params.policyID); + const policy = usePolicy(route.params?.policyID); const {translate} = useLocalize(); const styles = useThemeStyles(); const {windowWidth, windowHeight} = useWindowDimensions(); From eff60be00a311a249e8a4f060be62980c94e1ec6 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Tue, 20 Oct 2026 08:06:37 +0100 Subject: [PATCH 19/34] better type safety --- config/eslint/eslint.seatbelt.tsv | 1 - src/pages/settings/Agents/DynamicAddAgentPage.tsx | 9 ++++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/config/eslint/eslint.seatbelt.tsv b/config/eslint/eslint.seatbelt.tsv index 34de8c5b8e9d..b6246dce5253 100644 --- a/config/eslint/eslint.seatbelt.tsv +++ b/config/eslint/eslint.seatbelt.tsv @@ -1213,7 +1213,6 @@ "../../src/pages/settings/AboutPage/AboutPage.tsx" "no-restricted-imports" 1 "../../src/pages/settings/AboutPage/AboutPage.tsx" "react-hooks/refs" 1 "../../src/pages/settings/Agents/AgentsPage.tsx" "no-restricted-imports" 1 -"../../src/pages/settings/Agents/DynamicAddAgentPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 "../../src/pages/settings/Agents/DynamicAddAgentPage.tsx" "no-restricted-imports" 1 "../../src/pages/settings/Agents/DynamicEditAgentPage.tsx" "no-restricted-imports" 1 "../../src/pages/settings/Agents/DynamicNewAgentPage.tsx" "no-restricted-imports" 1 diff --git a/src/pages/settings/Agents/DynamicAddAgentPage.tsx b/src/pages/settings/Agents/DynamicAddAgentPage.tsx index cd9cccee5078..f77ef2fb6c2e 100644 --- a/src/pages/settings/Agents/DynamicAddAgentPage.tsx +++ b/src/pages/settings/Agents/DynamicAddAgentPage.tsx @@ -47,9 +47,8 @@ import type {Errors} from '@src/types/onyx/OnyxCommon'; import isLoadingOnyxValue from '@src/types/utils/isLoadingOnyxValue'; import type {TextInputKeyPressEvent} from 'react-native'; -import type {ValueOf} from 'type-fest'; -import React, {useCallback, useEffect, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useRef} from 'react'; import {Platform, View} from 'react-native'; import {PROMPT_MAX_AUTO_GROW_HEIGHT, PROMPT_MAX_HEIGHT_ON_KEYBOARD_OPEN_LANDSCAPE_MODE} from './const'; @@ -88,7 +87,7 @@ function DynamicAddAgentPageContent({route, template}: DynamicAddAgentPageConten const hasLeftPageRef = useRef(false); const formRef = useRef(null); const availableRoleItems = getAssignableWorkspaceMemberRoleItems(translate, undefined, policy, currentUserLogin ?? ''); - const [role, setRole] = useState>(availableRoleItems.at(0)?.value ?? CONST.POLICY.ROLE.ADMIN); + const defaultRole = availableRoleItems.at(0)?.value ?? CONST.POLICY.ROLE.ADMIN; const roleOptions = availableRoleItems.map((roleItem) => ({ label: roleItem.text, value: roleItem.value, @@ -155,6 +154,7 @@ function DynamicAddAgentPageContent({route, template}: DynamicAddAgentPageConten hasSubmittedRef.current = true; const firstName = values[INPUT_IDS.FIRST_NAME].trim() || defaultAgentName; const prompt = values[INPUT_IDS.PROMPT].trim(); + const role = values[INPUT_IDS.ROLE]; // Pure optimistic flow: `createAgent/createCompanyAgent` writes the agent and the owner<->agent DM to Onyx under a // reportID it generates client-side, and CreateAgent/CreateCompanyAgent creates the DM under that exact ID, @@ -282,8 +282,7 @@ function DynamicAddAgentPageContent({route, template}: DynamicAddAgentPageConten inputID={INPUT_IDS.ROLE} label={translate('common.role')} items={roleOptions} - onValueChange={(value) => setRole(value as typeof role)} - value={role} + defaultValue={defaultRole} shouldShowTooltips={false} /> From 0823caff18e2a7fe447c154808ee4e371119c4d9 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Tue, 20 Oct 2026 08:14:14 +0100 Subject: [PATCH 20/34] handle default role properely --- src/pages/settings/Agents/DynamicAddAgentPage.tsx | 5 ++++- src/types/form/AddAgentForm.ts | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/pages/settings/Agents/DynamicAddAgentPage.tsx b/src/pages/settings/Agents/DynamicAddAgentPage.tsx index f77ef2fb6c2e..44a6ca87dfaa 100644 --- a/src/pages/settings/Agents/DynamicAddAgentPage.tsx +++ b/src/pages/settings/Agents/DynamicAddAgentPage.tsx @@ -87,7 +87,7 @@ function DynamicAddAgentPageContent({route, template}: DynamicAddAgentPageConten const hasLeftPageRef = useRef(false); const formRef = useRef(null); const availableRoleItems = getAssignableWorkspaceMemberRoleItems(translate, undefined, policy, currentUserLogin ?? ''); - const defaultRole = availableRoleItems.at(0)?.value ?? CONST.POLICY.ROLE.ADMIN; + const defaultRole = availableRoleItems.at(0)?.value; const roleOptions = availableRoleItems.map((roleItem) => ({ label: roleItem.text, value: roleItem.value, @@ -147,6 +147,9 @@ function DynamicAddAgentPageContent({route, template}: DynamicAddAgentPageConten if (!values[INPUT_IDS.PROMPT].trim()) { errors[INPUT_IDS.PROMPT] = translate('common.error.fieldRequired'); } + if (!values[INPUT_IDS.ROLE]) { + errors[INPUT_IDS.ROLE] = translate('common.error.fieldRequired'); + } return errors; }; diff --git a/src/types/form/AddAgentForm.ts b/src/types/form/AddAgentForm.ts index 8e2315c28738..2be221400a23 100644 --- a/src/types/form/AddAgentForm.ts +++ b/src/types/form/AddAgentForm.ts @@ -1,3 +1,5 @@ +import type CONST from '@src/CONST'; + import type {ValueOf} from 'type-fest'; import type Form from './Form'; @@ -15,7 +17,7 @@ type AddAgentForm = Form< { [INPUT_IDS.FIRST_NAME]: string; [INPUT_IDS.PROMPT]: string; - [INPUT_IDS.ROLE]: string; + [INPUT_IDS.ROLE]: ValueOf; } >; From 5f364fc0f7af39845681be0b2a9b0b1144d04869 Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Tue, 20 Oct 2026 08:18:39 +0100 Subject: [PATCH 21/34] clean up --- .../settings/Agents/DynamicAddAgentPage.tsx | 33 +++++++------------ 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/src/pages/settings/Agents/DynamicAddAgentPage.tsx b/src/pages/settings/Agents/DynamicAddAgentPage.tsx index 44a6ca87dfaa..023587129613 100644 --- a/src/pages/settings/Agents/DynamicAddAgentPage.tsx +++ b/src/pages/settings/Agents/DynamicAddAgentPage.tsx @@ -159,28 +159,14 @@ function DynamicAddAgentPageContent({route, template}: DynamicAddAgentPageConten const prompt = values[INPUT_IDS.PROMPT].trim(); const role = values[INPUT_IDS.ROLE]; - // Pure optimistic flow: `createAgent/createCompanyAgent` writes the agent and the owner<->agent DM to Onyx under a - // reportID it generates client-side, and CreateAgent/CreateCompanyAgent creates the DM under that exact ID, - // so we can navigate to the DM immediately, online or offline, without waiting. - let optimisticReportID: string | undefined; - let optimisticPersonalDetailPromise: Promise | undefined; - let backToAgentsListRoute: Route | undefined; - - if (policy) { - const result = uploadedAvatar?.uri - ? createCompanyAgent(currentUserAccountID, currentUserLogin, prompt, policy, role, firstName, undefined, buildFileFromAvatarCropResult(uploadedAvatar), uploadedAvatar.uri) - : createCompanyAgent(currentUserAccountID, currentUserLogin, prompt, policy, role, firstName, selectedPresetID ?? AGENT_AVATARS.getRandomID()); - optimisticReportID = result.optimisticReportID; - optimisticPersonalDetailPromise = result.optimisticPersonalDetailPromise; - backToAgentsListRoute = ROUTES.WORKSPACE_AGENTS.getRoute(policy.id); - } else { - const result = uploadedAvatar?.uri - ? createAgent(firstName, prompt, currentUserAccountID, currentUserLogin, undefined, buildFileFromAvatarCropResult(uploadedAvatar), uploadedAvatar.uri) - : createAgent(firstName, prompt, currentUserAccountID, currentUserLogin, selectedPresetID ?? AGENT_AVATARS.getRandomID(), undefined, undefined); - optimisticReportID = result.optimisticReportID; - optimisticPersonalDetailPromise = result.optimisticPersonalDetailPromise; - backToAgentsListRoute = ROUTES.SETTINGS_AGENTS; - } + const avatarArgs = uploadedAvatar?.uri + ? {file: buildFileFromAvatarCropResult(uploadedAvatar), optimisticAvatarURI: uploadedAvatar.uri} + : {customExpensifyAvatarID: selectedPresetID ?? AGENT_AVATARS.getRandomID()}; + + const {optimisticReportID, optimisticPersonalDetailPromise} = policy + ? createCompanyAgent(currentUserAccountID, currentUserLogin, prompt, policy, role, firstName, avatarArgs.customExpensifyAvatarID, avatarArgs.file, avatarArgs.optimisticAvatarURI) + : createAgent(firstName, prompt, currentUserAccountID, currentUserLogin, avatarArgs.customExpensifyAvatarID, avatarArgs.file, avatarArgs.optimisticAvatarURI); + const backToAgentsListRoute = policy ? ROUTES.WORKSPACE_AGENTS.getRoute(policy.id) : ROUTES.SETTINGS_AGENTS; clearNewAgentTemplate(); @@ -189,6 +175,9 @@ function DynamicAddAgentPageContent({route, template}: DynamicAddAgentPageConten // regardless of window size. getIsNarrowLayout() reflects the actual window width. const isNarrowLayout = getIsNarrowLayout(); + // Pure optimistic flow: `createAgent/createCompanyAgent` writes the agent and the owner<->agent DM to Onyx under a + // reportID it generates client-side, and CreateAgent/CreateCompanyAgent creates the DM under that exact ID, + // so we can navigate to the DM immediately, online or offline, without waiting. optimisticPersonalDetailPromise.then(() => { // The user left the builder before the write resolved, so don't pull them into the DM from whatever screen is now active. if (hasLeftPageRef.current) { From 2147496a73e63df2d283e37a7f4d90e95d27fede Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Tue, 20 Oct 2026 08:21:52 +0100 Subject: [PATCH 22/34] better types --- src/libs/API/parameters/CreateCompanyAgentParams.ts | 6 +++++- src/libs/actions/Agent.ts | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/libs/API/parameters/CreateCompanyAgentParams.ts b/src/libs/API/parameters/CreateCompanyAgentParams.ts index d67fe6a7bf0a..0236edb08004 100644 --- a/src/libs/API/parameters/CreateCompanyAgentParams.ts +++ b/src/libs/API/parameters/CreateCompanyAgentParams.ts @@ -1,9 +1,13 @@ import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; +import type CONST from '@src/CONST'; + +import type {ValueOf} from 'type-fest'; + type CreateCompanyAgentParams = { prompt: string; policyID: string; - role: string; + role: ValueOf; firstName?: string; customExpensifyAvatarID?: string; file?: File | CustomRNImageManipulatorResult; diff --git a/src/libs/actions/Agent.ts b/src/libs/actions/Agent.ts index 9adc42b3a421..8b53c3784ae0 100644 --- a/src/libs/actions/Agent.ts +++ b/src/libs/actions/Agent.ts @@ -21,6 +21,7 @@ import type PolicyEmployee from '@src/types/onyx/PolicyEmployee'; import type {AnyOnyxUpdate} from '@src/types/onyx/Request'; import type {OnyxCollection, OnyxCollectionInputValue, OnyxUpdate} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import Onyx from 'react-native-onyx'; @@ -190,7 +191,7 @@ function createCompanyAgent( currentUserLogin: string | undefined, prompt: string, policy: Policy, - role: string, + role: ValueOf, firstName?: string, customExpensifyAvatarID?: string, file?: File | CustomRNImageManipulatorResult, From 51b967398f66d015efcf68df4dea17074ad1031d Mon Sep 17 00:00:00 2001 From: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> Date: Tue, 20 Oct 2026 08:22:19 +0100 Subject: [PATCH 23/34] remov stale comment --- src/pages/workspace/WorkspaceAgentsPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/workspace/WorkspaceAgentsPage.tsx b/src/pages/workspace/WorkspaceAgentsPage.tsx index 1d9899195da9..b203a3966b25 100644 --- a/src/pages/workspace/WorkspaceAgentsPage.tsx +++ b/src/pages/workspace/WorkspaceAgentsPage.tsx @@ -58,7 +58,6 @@ function WorkspaceAgentsPage({route}: WorkspaceAgentsPageProps) { useWorkspaceDocumentTitle(policy?.name, 'agentsPage.title'); - // The new agent functionality will be added after CreateCompanyAgent is exposed const newAgentButton = canWriteAgents && hasAgents ? (