Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/libs/API/parameters/CreateAgentParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ type CreateAgentParams = {
policyID?: string;
optimisticAccountID: string;
isPersonalAgent: boolean;
optimisticReportID: string;
createdReportActionID: string;
};

export default CreateAgentParams;
6 changes: 4 additions & 2 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3531,8 +3531,10 @@ function getDisplayNameForParticipant({

// This is to check if account is an invite/optimistically created one
// and prevent from falling back to 'Hidden', so a correct value is shown
// when searching for a new user
if (personalDetails.isOptimisticPersonalDetail === true) {
// when searching for a new user. Optimistic personal details that already have a known displayName
// (e.g. an agent pending creation, whose name the user just chose) fall through to the normal name
// resolution below instead, so that name is shown rather than an empty/placeholder login.
if (personalDetails.isOptimisticPersonalDetail === true && !personalDetails.displayName) {
return formattedLogin;
}

Expand Down
70 changes: 67 additions & 3 deletions src/libs/actions/Agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import {AGENT_AVATARS} from '@libs/Avatars/AgentAvatarCatalog';
import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types';
import {getMicroSecondOnyxErrorWithTranslationKey} from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import {generateReportID} from '@libs/ReportUtils';
import {rand64} from '@libs/NumberUtils';
import {buildOptimisticChatReport, buildOptimisticCreatedReportAction, generateReportID} from '@libs/ReportUtils';
import type {AvatarSource} from '@libs/UserAvatarUtils';

import CONST from '@src/CONST';
Expand All @@ -29,6 +30,8 @@ function openProfilePage() {
function createAgent(
firstName: string | undefined,
prompt: string,
ownerAccountID: number,
ownerLogin: string | undefined,
customExpensifyAvatarID?: string,
file?: File | CustomRNImageManipulatorResult,
optimisticAvatarURI?: string,
Expand All @@ -51,6 +54,26 @@ function createAgent(
...(avatarURI ? {avatar: avatarURI, avatarThumbnail: avatarURI} : {}),
};

// The owner<->agent DM's reportID, generated the same way as any other new chat's reportID. CreateAgent
// creates the DM under this exact ID (see CreateAgent.cpp), so we can write the report to Onyx and
// navigate to it immediately, instead of waiting for the response and guessing the agent's identity.
const optimisticReportID = generateReportID();
const optimisticChatReport = buildOptimisticChatReport({
participantList: [ownerAccountID, optimisticAccountID],
ownerAccountID,
optimisticReportID,
currentUserAccountID: ownerAccountID,
});
// Generated up front (rather than left to buildOptimisticCreatedReportAction's default) so it can also
// be sent to CreateAgent, the same way OpenReport's callers forward createdReportActionID: the DM's
// CREATED action then reconciles onto this exact ID instead of the server generating a duplicate.
const createdReportActionID = rand64();
const optimisticCreatedAction = buildOptimisticCreatedReportAction({
emailCreatingAction: ownerLogin ?? CONST.REPORT.OWNER_EMAIL_FAKE,
currentUserAccountID: ownerAccountID,
optimisticReportActionID: createdReportActionID,
});

const optimisticData: AnyOnyxUpdate[] = [
{
onyxMethod: Onyx.METHOD.MERGE,
Expand All @@ -62,6 +85,21 @@ function createAgent(
key: `${ONYXKEYS.COLLECTION.SHARED_NVP_AGENT_PROMPT}${optimisticAccountID}`,
value: {prompt, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD},
},
{
onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticReportID}`,
value: {...optimisticChatReport, pendingFields: {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}},
},
{
onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticReportID}`,
value: {[optimisticCreatedAction.reportActionID]: optimisticCreatedAction},
},
{
onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${optimisticReportID}`,
value: {isOptimisticReport: true},
},
];

const successData: AnyOnyxUpdate[] = [
Expand All @@ -75,6 +113,16 @@ function createAgent(
key: `${ONYXKEYS.COLLECTION.SHARED_NVP_AGENT_PROMPT}${optimisticAccountID}`,
value: null,
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticReportID}`,
value: {pendingFields: {createChat: null}},
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${optimisticReportID}`,
value: {isOptimisticReport: false},
},
];

const failureData: AnyOnyxUpdate[] = [
Expand All @@ -92,16 +140,32 @@ function createAgent(
errors: getMicroSecondOnyxErrorWithTranslationKey('agentsPage.error.genericAdd'),
},
},
{
onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.REPORT}${optimisticReportID}`,
value: null,
},
{
onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${optimisticReportID}`,
value: null,
},
{
onyxMethod: Onyx.METHOD.SET,
key: `${ONYXKEYS.COLLECTION.REPORT_METADATA}${optimisticReportID}`,
value: null,
},
];

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.
WRITE_COMMANDS.CREATE_AGENT,
{firstName, prompt, customExpensifyAvatarID, file, policyID, optimisticAccountID: String(optimisticAccountID), isPersonalAgent: true},
{firstName, prompt, customExpensifyAvatarID, file, policyID, optimisticAccountID: String(optimisticAccountID), isPersonalAgent: true, optimisticReportID, createdReportActionID},
{optimisticData, successData, failureData},
);

return {optimisticAccountID, avatarURI};
return {optimisticAccountID, avatarURI, optimisticReportID};
}

function clearAgentError(optimisticAccountID: number) {
Expand Down
18 changes: 8 additions & 10 deletions src/pages/settings/Agents/AddAgentPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ function AddAgentPage({route}: AddAgentPageProps) {
const styles = useThemeStyles();
const {windowWidth, windowHeight} = useWindowDimensions();
const shouldUseScrollableLayout = useIsInLandscapeMode() || (isMobile() && windowWidth > windowHeight);
const {displayName} = useCurrentUserPersonalDetails();
const {accountID: ownerAccountID, login: ownerLogin, displayName} = useCurrentUserPersonalDetails();
const defaultAgentName = displayName ? translate('addAgentPage.defaultAgentName', displayName) : undefined;
const defaultPrompt = translate('addAgentPage.defaultPrompt');
const expensifyIcons = useMemoizedLazyExpensifyIcons(['Pencil']);
Expand Down Expand Up @@ -97,16 +97,14 @@ function AddAgentPage({route}: AddAgentPageProps) {
const prompt = values[INPUT_IDS.PROMPT].trim();
const pendingFile = pendingFileRef.current;

// Pure optimistic flow — no waiting on the server, online or offline. `createAgent`
// returns the optimistic accountID it wrote into Onyx so we can hand it to the next
// screen and let it render the agent with opacity until CREATE_AGENT resolves.
if (pendingFile) {
createAgent(firstName, prompt, undefined, pendingFile.file, pendingFile.uri, policyID);
} else {
createAgent(firstName, prompt, selectedPresetID ?? undefined, undefined, undefined, policyID);
}
// 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} = pendingFile
? createAgent(firstName, prompt, ownerAccountID, ownerLogin, undefined, pendingFile.file, pendingFile.uri, policyID)
: createAgent(firstName, prompt, ownerAccountID, ownerLogin, selectedPresetID ?? undefined, undefined, undefined, policyID);

Navigation.goBack();
Navigation.dismissModalWithReport({reportID: optimisticReportID});
};

return (
Expand Down
Loading
Loading