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 ? (