Add Chat and Copilot buttons to Agents page#91533
Conversation
Add two action buttons to the agent row in Account > Agents (wide layout) and two new menu items to the Edit Agent page. The Chat button opens a DM with the agent via navigateToAndOpenReportWithAccountIDs. The Copilot button switches into the agent account via Delegate.connect. Co-authored-by: Shridhar Goel <ShridharGoel@users.noreply.github.com>
…t translations The new translation strings for chatWithAgent, copilotIntoAccount, and deleteAgent used Unicode curly quotes (U+2018/U+2019) as string delimiters in fr.ts and unescaped ASCII apostrophes inside single-quoted strings in it.ts. Both caused TypeScript parse errors and Prettier failures. Replaced curly quote delimiters with ASCII single quotes and used \u2019 escape sequences for French/Italian apostrophes inside strings. Co-authored-by: Shridhar Goel <ShridharGoel@users.noreply.github.com>
|
Fixed the failing checks. The new translation strings in
Both are now fixed using ASCII single quote delimiters with |
Codecov Report✅ Changes either increased or maintained existing code coverage, great job!
|
| navigateToAndOpenReportWithAccountIDs([accountID], currentUserPersonalDetails.accountID, introSelected, isSelfTourViewed, betas, personalDetails); | ||
| }; | ||
| const handleCopilotPress = () => { | ||
| connect({email: login, delegatedAccess: account?.delegatedAccess, credentials, session, activePolicyID}); |
There was a problem hiding this comment.
@MelvinBot Existing Copilot switching checks offline state, active GPS tracking, and chained delegation before calling connect (see CopilotPage.tsx). These new buttons call connect directly, so they can try to switch accounts offline, during GPS tracking without warning, or while already acting as a delegate. Check the existing implementation and update this accordingly, while reusing code.
There was a problem hiding this comment.
Extracted a new useSwitchToDelegator hook that encapsulates the offline, GPS tracking, and chained delegation checks from CopilotPage.tsx. Both AgentsListRow and EditAgentPage now use this hook instead of calling Delegate.connect directly. CopilotPage also uses the shared hook, eliminating the duplication.
| <Button | ||
| small | ||
| icon={icons.UserPlus} | ||
| onPress={handleCopilotPress} | ||
| isDisabled={isPendingDeletion} | ||
| /> |
There was a problem hiding this comment.
Added accessibilityLabel to both icon-only buttons using existing translation keys (editAgentPage.chatWithAgent and editAgentPage.copilotIntoAccount).
| <Button | ||
| small | ||
| icon={icons.ChatBubble} | ||
| onPress={handleChatPress} | ||
| isDisabled={isPendingDeletion} | ||
| /> |
There was a problem hiding this comment.
Added accessibilityLabel using the existing editAgentPage.chatWithAgent translation key.
The new Agent copilot buttons were calling Delegate.connect directly, bypassing the offline, GPS tracking, and chained delegation safety checks that CopilotPage enforces. This extracts those checks into a reusable useSwitchToDelegator hook and uses it in AgentsListRow, EditAgentPage, and CopilotPage. Co-authored-by: Shridhar Goel <ShridharGoel@users.noreply.github.com>
Icon-only buttons in AgentsListRow need accessibilityLabel props so screen readers can announce their purpose. Co-authored-by: Shridhar Goel <ShridharGoel@users.noreply.github.com>
Co-authored-by: Shridhar Goel <ShridharGoel@users.noreply.github.com>
….com/Expensify/App into claude-addAgentChatAndCopilotButtons Co-authored-by: Shridhar Goel <ShridharGoel@users.noreply.github.com>
|
Fixed the failing Prettier check: sorted imports alphabetically in |
|
MelvinBot has made 6 consecutive commits trying to fix CI failures. Automatic CI fix dispatches are now paused for this PR. Next steps: A human should review the recent commits and resolve the conflicting CI errors manually. |
|
@MelvinBot In the row, it should be "Co-pilot" instead of an icon |
|
@MelvinBot Copilot icon should be the same one as the one used in MenuItem for copilot |
Use the existing 'delegate.copilot' translation ("Copilot") as a text
button instead of the UserPlus icon in the wide-layout agent row.
Co-authored-by: Shridhar Goel <ShridharGoel@users.noreply.github.com>
|
Changed the Copilot button in the wide-layout agent row from an icon-only (UserPlus) button to a text button showing "Copilot", using the existing |
Change the copilot MenuItem icon in EditAgentPage from UserPlus to Users, matching the icon used in the Copilot settings page. Co-authored-by: Shridhar Goel <ShridharGoel@users.noreply.github.com>
|
Changed the copilot MenuItem icon in |
|
@MelvinBot Mark this as ready for review |
|
@MelvinBot Fill all boxes in author checklist and add tests |
|
I'm unable to mark this PR as ready for review due to permission restrictions. Please mark it as ready for review manually using the GitHub UI or by running |
|
@MelvinBot Mark this pull request as ready for review |
|
I'm unable to mark this PR as ready for review due to permission restrictions in my environment. You can do it yourself by clicking the "Ready for review" button on the PR page or running: |
Reviewer Checklist
Screenshots/VideosAndroid: HybridAppAndroid: mWeb ChromeScreen.Recording.2026-05-24.at.6.07.30.PM.moviOS: HybridAppScreen.Recording.2026-05-24.at.5.56.35.PM.moviOS: mWeb SafariScreen.Recording.2026-05-24.at.5.57.45.PM.movMacOS: Chrome / SafariScreen.Recording.2026-05-24.at.4.23.44.PM.movScreen.Recording.2026-05-24.at.9.19.22.PM.mov |
|
@MelvinBot Mark as ready |
|
I'm unable to mark this PR as ready for review due to permission restrictions. Please mark it as ready manually using the GitHub UI or by running |
|
@MelvinBot Try again to mark it as ready for review. You have the permissions. |
| const icons = useMemoizedLazyExpensifyIcons(['DotIndicator']); | ||
| const icons = useMemoizedLazyExpensifyIcons(['DotIndicator', 'ChatBubble']); | ||
| const currentUserPersonalDetails = useCurrentUserPersonalDetails(); | ||
| const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); |
There was a problem hiding this comment.
❌ CLEAN-REACT-PATTERNS-2 (docs)
AgentsListRow fetches introSelected, isSelfTourViewed, betas, personalDetails, and currentUserPersonalDetails solely to pass them as arguments to navigateToAndOpenReportWithAccountIDs. The component does not use any of these values for its own rendering. This makes the component a data intermediary for the chat navigation concern.
Encapsulate this data fetching and the navigateToAndOpenReportWithAccountIDs call inside a dedicated hook (e.g., useChatWithAccount) that only needs an accountID parameter. This removes five hooks/subscriptions from this component.
Reviewed at: b1dd748 | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
There was a problem hiding this comment.
Addressed — the chat navigation data fetching is now encapsulated in the useChatWithAgent hook. AgentsListRow no longer subscribes to introSelected, isSelfTourViewed, betas, personalDetails, or currentUserPersonalDetails directly.
| const [allPersonalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); | ||
| const {showConfirmModal} = useConfirmModal(); | ||
| const currentUserPersonalDetails = useCurrentUserPersonalDetails(); | ||
| const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); |
There was a problem hiding this comment.
❌ CLEAN-REACT-PATTERNS-2 (docs)
EditAgentPage fetches introSelected, isSelfTourViewed, betas, allPersonalDetails, and currentUserPersonalDetails solely to pass them as arguments to navigateToAndOpenReportWithAccountIDs. None of these values are used for the component's own rendering logic.
Encapsulate this data fetching and the navigation call inside a dedicated hook (e.g., useChatWithAccount) that only needs an accountID parameter.
Reviewed at: b1dd748 | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
There was a problem hiding this comment.
Addressed — same as above. EditAgentPage now uses the shared useChatWithAgent hook and no longer subscribes to these values directly. The unselected useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST) subscription has been removed.
| const accountID = route.params.accountID; | ||
| const [agent, agentMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.SHARED_NVP_AGENT_PROMPT}${accountID}`); | ||
| const [personalDetails, personalDetailsMetadata] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: (list) => list?.[accountID]}); | ||
| const [allPersonalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); |
There was a problem hiding this comment.
❌ PERF-11 (docs)
useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST) subscribes to the entire personal details collection without a selector. This will cause a re-render every time any user's personal details change. Line 38 already subscribes to the same key with a selector for the specific agent's details.
If this subscription is needed for navigateToAndOpenReportWithAccountIDs, consider encapsulating it in a dedicated hook (as suggested in the CLEAN-REACT-PATTERNS-2 comment). Otherwise, use a selector to narrow the subscription.
Reviewed at: b1dd748 | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
There was a problem hiding this comment.
Addressed — the unselected useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST) has been moved into the useChatWithAgent hook, removing it from EditAgentPage.
There was a problem hiding this comment.
Done — see reply above.
| const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); | ||
| const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); | ||
| const [betas] = useOnyx(ONYXKEYS.BETAS); | ||
| const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); |
There was a problem hiding this comment.
❌ PERF-11 (docs)
useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST) subscribes to the entire personal details collection without a selector. This causes re-renders whenever any user's personal details change, even though this component only needs to open a chat for a single agent.
Consider extracting the chat navigation logic into a hook that owns this subscription, or narrow the data with a selector if possible.
Reviewed at: b1dd748 | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
There was a problem hiding this comment.
Addressed — the unselected useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST) has been moved into the useChatWithAgent hook, removing it from AgentsListRow.
There was a problem hiding this comment.
Done — see reply above.
| const {isActingAsDelegate} = useDelegateNoAccessState(); | ||
| const {showDelegateNoAccessModal} = useDelegateNoAccessActions(); | ||
|
|
||
| const [account] = useOnyx(ONYXKEYS.ACCOUNT); |
There was a problem hiding this comment.
❌ PERF-11 (docs)
useOnyx(ONYXKEYS.ACCOUNT) subscribes to the entire account object, but only account?.delegatedAccess is used (line 82). Any change to any field in the account object will trigger a re-render of every component that uses this hook.
Use a selector to narrow the subscription:
const [delegatedAccess] = useOnyx(ONYXKEYS.ACCOUNT, {
selector: (account) => account?.delegatedAccess,
});Reviewed at: b1dd748 | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.
There was a problem hiding this comment.
Addressed — now using a selector to subscribe only to delegatedAccess instead of the full account object:
const [delegatedAccess] = useOnyx(ONYXKEYS.ACCOUNT, {selector: (account: Account | undefined) => account?.delegatedAccess});There was a problem hiding this comment.
Done — see reply above.
…ctors, remove useCallback - Created useChatWithAgent hook to deduplicate chat navigation logic (CONSISTENCY-3, CLEAN-REACT-PATTERNS-2) - Used selector for useOnyx(ONYXKEYS.ACCOUNT) to subscribe only to delegatedAccess (PERF-11) - Removed redundant useCallback wrappers in useSwitchToDelegator (CLEAN-REACT-PATTERNS-0) - Removed unneeded useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST) from AgentsListRow and EditAgentPage (PERF-11) - Added accessibilityLabel to Copilot button in AgentsListRow Co-authored-by: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Co-authored-by: Shridhar Goel <ShridharGoel@users.noreply.github.com>
|
@MelvinBot AgentsListRow now creates global subscriptions for every row. Shouldn't we call call these hooks once in AgentsPage and pass onChatPress(accountID) / onCopilotPress(login) into each row? What do you think? |
|
Yes, you're right — this should be lifted. Each
Recommendation: Lift both |
|
@MelvinBot Update accordingly |
…ow to AgentsPage Moves the hook calls to AgentsPage so they run once, and passes onChatPress / onCopilotPress callbacks into each AgentsListRow. This avoids duplicating global Onyx subscriptions per agent row. Co-authored-by: Shridhar Goel <ShridharGoel@users.noreply.github.com>
|
Lifted |
|
@Expensify/product can you please confirm we are good to go? |
|
Yes! 8px gap for horizontal button groupppppps |
|
@MelvinBot do the change requested in #91533 (comment) |
|
The fix is to change I can only make code changes when directed by the PR author, an assignee, or an existing co-author. To have me implement this, the PR assignee or author should reply with a request tagging me. |
|
@MelvinBot do the change requested in #91533 (comment) |
Co-authored-by: Shridhar Goel <ShridharGoel@users.noreply.github.com>
|
Changed the button gap from |
|
@ShridharGoel can you post screenshots or videos so product can approve? |
yuwenmemon
left a comment
There was a problem hiding this comment.
LGTM - we can handle any pixel-level adjustments in a followup.
|
Videos were already present here |
|
🚧 @yuwenmemon has triggered a test Expensify/App build. You can view the workflow run here. |
|
🧪🧪 Use the links below to test this adhoc build on Android, iOS, and Web. Happy testing! 🧪🧪
|
|
✋ This PR was not deployed to staging yet because QA is ongoing. It will be automatically deployed to staging after the next production release. |

Explanation of Change
Adds Chat and Copilot action buttons to the Agents feature in two locations:
AgentsListRow(wide layout): Two new icon buttons (ChatBubble, UserPlus) appear alongside the existing Edit button. The Chat button opens a DM with the agent vianavigateToAndOpenReportWithAccountIDs. The Copilot button switches into the agent account viaDelegate.connect.EditAgentPage: Two newMenuItementries ("Chat with agent" and "Copilot into account") appear above the existing Delete option, using the same action functions.Both buttons are disabled when the agent row is pending deletion. Translation strings added for all supported locales.
Fixed Issues
$ #91343
PROPOSAL: #91343 (comment)
Tests
Offline tests
QA Steps
PR Author Checklist
### Fixed Issuessection aboveTestssectionOffline stepssectionQA stepssectiontoggleReportand notonIconClick)src/languages/*files and using the translation methodSTYLE.md) were followedAvatar, I verified the components usingAvatarare working as expected)StyleUtils.getBackgroundAndBorderStyle(theme.componentBG))npm run compress-svg)Avataris modified, I verified thatAvataris working as expected in all cases)Designlabel and/or tagged@Expensify/designso the design team can review the changes.ScrollViewcomponent to make it scrollable when more elements are added to the page.mainbranch was merged into this PR after a review, I tested again and verified the outcome was still expected according to theTeststeps.Screenshots/Videos
Android: Native
Android: mWeb Chrome
iOS: Native
iOS: mWeb Safari
MacOS: Chrome / Safari