From bbc11559eaa3d7fa80a02ab75bfc757a1d19fc79 Mon Sep 17 00:00:00 2001 From: Adam Horodyski Date: Tue, 17 Mar 2026 14:20:35 +0100 Subject: [PATCH 01/15] Consolidate AgentZero status into a shared context provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace three scattered modules (useAgentZeroStatusIndicator hook, ConciergeReasoningStore, and Pusher reasoning subscriptions in Report/index.ts) with a single AgentZeroStatusContext that uses a two-level gate pattern: - Outer gate (AgentZeroStatusProvider): cheap scalar check — if not Concierge chat, renders children with no hooks/Pusher/state overhead - Inner gate (AgentZeroStatusGate): owns all logic for Concierge chats: Onyx subscription, Pusher subscription with per-callback cleanup, reasoning state with deduplication, debounced label updates Consumers (ConciergeThinkingMessage, ReportActionCompose) now read from context instead of receiving props drilled through 4 levels. The prop chain ReportScreen → ReportActionsView → ReportActionsList → ConciergeThinkingMessage is eliminated. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/hooks/useAgentZeroStatusIndicator.ts | 162 ------ src/libs/ConciergeReasoningStore.ts | 122 ----- src/libs/actions/Report/index.ts | 50 -- .../home/report/ConciergeThinkingMessage.tsx | 35 +- src/pages/inbox/AgentZeroStatusContext.tsx | 224 ++++++++ src/pages/inbox/ReportScreen.tsx | 107 ++-- .../ReportActionCompose.tsx | 11 +- src/pages/inbox/report/ReportActionsList.tsx | 22 +- src/pages/inbox/report/ReportActionsView.tsx | 16 - src/pages/inbox/report/ReportFooter.tsx | 18 +- src/selectors/ReportNameValuePairs.ts | 8 +- ...rTest.ts => AgentZeroStatusContextTest.ts} | 305 +++++------ tests/unit/ConciergeReasoningStoreTest.ts | 501 ------------------ .../unit/ReportNameValuePairsSelectorTest.ts | 30 ++ tests/unit/ReportReasoningSubscriptionTest.ts | 237 --------- 15 files changed, 462 insertions(+), 1386 deletions(-) delete mode 100644 src/hooks/useAgentZeroStatusIndicator.ts delete mode 100644 src/libs/ConciergeReasoningStore.ts create mode 100644 src/pages/inbox/AgentZeroStatusContext.tsx rename tests/unit/{useAgentZeroStatusIndicatorTest.ts => AgentZeroStatusContextTest.ts} (51%) delete mode 100644 tests/unit/ConciergeReasoningStoreTest.ts create mode 100644 tests/unit/ReportNameValuePairsSelectorTest.ts delete mode 100644 tests/unit/ReportReasoningSubscriptionTest.ts diff --git a/src/hooks/useAgentZeroStatusIndicator.ts b/src/hooks/useAgentZeroStatusIndicator.ts deleted file mode 100644 index fd2e6503593dc..0000000000000 --- a/src/hooks/useAgentZeroStatusIndicator.ts +++ /dev/null @@ -1,162 +0,0 @@ -import {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {subscribeToReportReasoningEvents, unsubscribeFromReportReasoningChannel} from '@libs/actions/Report'; -import ConciergeReasoningStore from '@libs/ConciergeReasoningStore'; -import type {ReasoningEntry} from '@libs/ConciergeReasoningStore'; -import ONYXKEYS from '@src/ONYXKEYS'; -import useLocalize from './useLocalize'; -import useNetwork from './useNetwork'; -import useOnyx from './useOnyx'; - -type AgentZeroStatusState = { - isProcessing: boolean; - reasoningHistory: ReasoningEntry[]; - statusLabel: string; - kickoffWaitingIndicator: () => void; -}; - -/** - * Hook to manage AgentZero status indicator for Concierge chats. - * Subscribes to real-time reasoning updates via Pusher and manages processing state. - */ -function useAgentZeroStatusIndicator(reportID: string, isConciergeChat: boolean): AgentZeroStatusState { - const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`); - const serverLabel = reportNameValuePairs?.agentZeroProcessingRequestIndicator?.trim() ?? ''; - - const [optimisticStartTime, setOptimisticStartTime] = useState(null); - const [displayedLabel, setDisplayedLabel] = useState(''); - const [reasoningHistory, setReasoningHistory] = useState([]); - const {translate} = useLocalize(); - const prevServerLabelRef = useRef(serverLabel); - const updateTimerRef = useRef(null); - const lastUpdateTimeRef = useRef(0); - const {isOffline} = useNetwork(); - - // Minimum time to display a label before allowing change (prevents rapid flicker) - const MIN_DISPLAY_TIME = 300; // ms - // Debounce delay for server label updates - const DEBOUNCE_DELAY = 150; // ms - - useEffect(() => { - setReasoningHistory(ConciergeReasoningStore.getReasoningHistory(reportID)); - }, [reportID]); - - useEffect(() => { - const unsubscribe = ConciergeReasoningStore.subscribe((updatedReportID, entries) => { - if (updatedReportID !== reportID) { - return; - } - setReasoningHistory(entries); - }); - - return unsubscribe; - }, [reportID]); - - useEffect(() => { - if (!isConciergeChat) { - return; - } - - subscribeToReportReasoningEvents(reportID); - - // Cleanup: unsubscribeFromReportReasoningChannel handles Pusher unsubscribing, - // clearing reasoning history from ConciergeReasoningStore, and subscription tracking - return () => { - unsubscribeFromReportReasoningChannel(reportID); - }; - }, [isConciergeChat, reportID]); - - useEffect(() => { - const hadServerLabel = !!prevServerLabelRef.current; - const hasServerLabel = !!serverLabel; - - // Helper function to update label with timing control - const updateLabel = (newLabel: string) => { - const now = Date.now(); - const timeSinceLastUpdate = now - lastUpdateTimeRef.current; - const remainingMinTime = Math.max(0, MIN_DISPLAY_TIME - timeSinceLastUpdate); - - // Clear any pending update - if (updateTimerRef.current) { - clearTimeout(updateTimerRef.current); - updateTimerRef.current = null; - } - - // If enough time has passed or it's a critical update (clearing), update immediately - if (remainingMinTime === 0 || newLabel === '') { - if (displayedLabel !== newLabel) { - setDisplayedLabel(newLabel); - lastUpdateTimeRef.current = now; - } - } else { - // Schedule update after debounce + remaining min display time - const delay = DEBOUNCE_DELAY + remainingMinTime; - updateTimerRef.current = setTimeout(() => { - if (displayedLabel !== newLabel) { - setDisplayedLabel(newLabel); - lastUpdateTimeRef.current = Date.now(); - } - updateTimerRef.current = null; - }, delay); - } - }; - - // When server label arrives, transition smoothly without flicker - if (hasServerLabel) { - updateLabel(serverLabel); - if (optimisticStartTime) { - setOptimisticStartTime(null); - } - } - // When optimistic state is active but no server label, show "Concierge is thinking..." - else if (optimisticStartTime) { - const thinkingLabel = translate('common.thinking'); - updateLabel(thinkingLabel); - } - // Clear everything when processing ends - else if (hadServerLabel && !hasServerLabel) { - updateLabel(''); - if (reasoningHistory.length > 0) { - ConciergeReasoningStore.clearReasoning(reportID); - } - } - - prevServerLabelRef.current = serverLabel; - - // Cleanup timer on unmount - return () => { - if (!updateTimerRef.current) { - return; - } - clearTimeout(updateTimerRef.current); - }; - }, [serverLabel, reasoningHistory.length, reportID, optimisticStartTime, translate, displayedLabel]); - - useEffect(() => { - if (isOffline) { - return; - } - setOptimisticStartTime(null); - }, [isOffline]); - - const kickoffWaitingIndicator = useCallback(() => { - if (!isConciergeChat) { - return; - } - setOptimisticStartTime(Date.now()); - }, [isConciergeChat]); - - const isProcessing = isConciergeChat && !isOffline && (!!serverLabel || !!optimisticStartTime); - - return useMemo( - () => ({ - isProcessing, - reasoningHistory, - statusLabel: displayedLabel, - kickoffWaitingIndicator, - }), - [isProcessing, reasoningHistory, displayedLabel, kickoffWaitingIndicator], - ); -} - -export default useAgentZeroStatusIndicator; -export type {AgentZeroStatusState}; diff --git a/src/libs/ConciergeReasoningStore.ts b/src/libs/ConciergeReasoningStore.ts deleted file mode 100644 index 1e945033dd022..0000000000000 --- a/src/libs/ConciergeReasoningStore.ts +++ /dev/null @@ -1,122 +0,0 @@ -/** - * Ephemeral in-memory store for managing Concierge reasoning summaries per report. - * This data is transient UI feedback and is NOT persisted to Onyx. - */ - -type ReasoningEntry = { - reasoning: string; - loopCount: number; - timestamp: number; -}; - -type ReasoningData = { - reasoning: string; - agentZeroRequestID: string; - loopCount: number; -}; - -type ReportState = { - agentZeroRequestID: string; - entries: ReasoningEntry[]; -}; - -type Listener = (reportID: string, state: ReasoningEntry[]) => void; - -// In-memory store -const store = new Map(); -const listeners = new Set(); - -/** - * Notify all subscribers of state changes - */ -function notifyListeners(reportID: string, entries: ReasoningEntry[]) { - for (const listener of listeners) { - listener(reportID, entries); - } -} - -/** - * Add a reasoning entry to a report's history. - * If the agentZeroRequestID differs from the current state, resets all entries (new request). - * Skips duplicates (same loopCount + same reasoning text). - */ -function addReasoning(reportID: string, data: ReasoningData) { - // Ignore empty reasoning strings - if (!data.reasoning.trim()) { - return; - } - - const currentState = store.get(reportID); - - // If agentZeroRequestID differs, reset all entries (new request) - if (currentState && currentState.agentZeroRequestID !== data.agentZeroRequestID) { - store.set(reportID, { - agentZeroRequestID: data.agentZeroRequestID, - entries: [], - }); - } - - // Get or create state - const state = store.get(reportID) ?? { - agentZeroRequestID: data.agentZeroRequestID, - entries: [], - }; - - // Skip duplicates (same loopCount + same reasoning text) - const isDuplicate = state.entries.some((entry) => entry.loopCount === data.loopCount && entry.reasoning === data.reasoning); - - if (!isDuplicate) { - const timestamp = Date.now(); - const newEntries = [ - ...state.entries, - { - reasoning: data.reasoning, - loopCount: data.loopCount, - timestamp, - }, - ]; - const newState = { - agentZeroRequestID: state.agentZeroRequestID, - entries: newEntries, - }; - store.set(reportID, newState); - notifyListeners(reportID, newEntries); - } -} - -/** - * Remove all reasoning entries for a report. - * Called when the final Concierge message arrives or when unsubscribing. - */ -function clearReasoning(reportID: string) { - store.delete(reportID); - notifyListeners(reportID, []); -} - -/** - * Get the reasoning history for a report - */ -function getReasoningHistory(reportID: string): ReasoningEntry[] { - return store.get(reportID)?.entries ?? []; -} - -/** - * Subscribe to state changes. - * Listener receives (reportID, state) on every change. - * Returns an unsubscribe function. - */ -function subscribe(listener: Listener): () => void { - listeners.add(listener); - return () => { - listeners.delete(listener); - }; -} - -export default { - addReasoning, - clearReasoning, - getReasoningHistory, - subscribe, -}; - -export type {ReasoningEntry, ReasoningData}; diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 31d47f7dd603e..2ad0b56474944 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -63,7 +63,6 @@ import {READ_COMMANDS, SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs import * as ApiUtils from '@libs/ApiUtils'; import * as Browser from '@libs/Browser'; import * as CollectionUtils from '@libs/CollectionUtils'; -import ConciergeReasoningStore from '@libs/ConciergeReasoningStore'; import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; import DateUtils from '@libs/DateUtils'; import * as EmojiUtils from '@libs/EmojiUtils'; @@ -403,9 +402,6 @@ Onyx.connect({ const typingWatchTimers: Record = {}; -// Track subscriptions to conciergeReasoning Pusher events to avoid duplicates -const reasoningSubscriptions = new Set(); - let reportIDDeeplinkedFromOldDot: string | undefined; Linking.getInitialURL().then((url) => { reportIDDeeplinkedFromOldDot = processReportIDDeeplink(url ?? ''); @@ -583,50 +579,6 @@ function unsubscribeFromLeavingRoomReportChannel(reportID: string | undefined) { Pusher.unsubscribe(pusherChannelName, Pusher.TYPE.USER_IS_LEAVING_ROOM); } -/** - * Subscribe to conciergeReasoning Pusher events for a report. - * Tracks subscriptions to avoid duplicates and updates ConciergeReasoningStore with reasoning data. - */ -function subscribeToReportReasoningEvents(reportID: string) { - if (!reportID || reasoningSubscriptions.has(reportID)) { - return; - } - - const pusherChannelName = getReportChannelName(reportID); - - // Add to subscriptions immediately to prevent duplicate subscriptions - reasoningSubscriptions.add(reportID); - - Pusher.subscribe(pusherChannelName, Pusher.TYPE.CONCIERGE_REASONING, (data: Record) => { - const eventData = data as {reasoning: string; agentZeroRequestID: string; loopCount: number}; - - ConciergeReasoningStore.addReasoning(reportID, { - reasoning: eventData.reasoning, - agentZeroRequestID: eventData.agentZeroRequestID, - loopCount: eventData.loopCount, - }); - }).catch((error: ReportError) => { - Log.hmmm('[Report] Failed to subscribe to Pusher concierge reasoning events', {errorType: error.type, pusherChannelName, reportID}); - // Remove from subscriptions if subscription failed - reasoningSubscriptions.delete(reportID); - }); -} - -/** - * Unsubscribe from conciergeReasoning Pusher events for a report. - * Clears reasoning state and removes from subscription tracking. - */ -function unsubscribeFromReportReasoningChannel(reportID: string) { - if (!reportID || !reasoningSubscriptions.has(reportID)) { - return; - } - - const pusherChannelName = getReportChannelName(reportID); - Pusher.unsubscribe(pusherChannelName, Pusher.TYPE.CONCIERGE_REASONING); - ConciergeReasoningStore.clearReasoning(reportID); - reasoningSubscriptions.delete(reportID); -} - // New action subscriber array for report pages let newActionSubscribers: ActionSubscriber[] = []; @@ -7252,14 +7204,12 @@ export { startNewChat, subscribeToNewActionEvent, subscribeToReportLeavingEvents, - subscribeToReportReasoningEvents, subscribeToReportTypingEvents, toggleEmojiReaction, togglePinnedState, toggleSubscribeToChildReport, unsubscribeFromLeavingRoomReportChannel, unsubscribeFromReportChannel, - unsubscribeFromReportReasoningChannel, updateDescription, updateGroupChatAvatar, updatePolicyRoomAvatar, diff --git a/src/pages/home/report/ConciergeThinkingMessage.tsx b/src/pages/home/report/ConciergeThinkingMessage.tsx index 80d2ab9ef9882..8c03607fefe0b 100644 --- a/src/pages/home/report/ConciergeThinkingMessage.tsx +++ b/src/pages/home/report/ConciergeThinkingMessage.tsx @@ -14,10 +14,10 @@ import useOnyx from '@hooks/useOnyx'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import type {ReasoningEntry} from '@libs/ConciergeReasoningStore'; import DateUtils from '@libs/DateUtils'; import Parser from '@libs/Parser'; import {getDisplayNameOrDefault} from '@libs/PersonalDetailsUtils'; +import {useAgentZeroStatus} from '@pages/inbox/AgentZeroStatusContext'; import ReportActionItemMessageHeaderSender from '@pages/inbox/report/ReportActionItemMessageHeaderSender'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -30,15 +30,36 @@ type ConciergeThinkingMessageProps = { /** The report action if available */ action?: OnyxEntry; +}; - /** Reasoning history to display */ - reasoningHistory?: ReasoningEntry[]; +function ConciergeThinkingMessage({report, action}: ConciergeThinkingMessageProps) { + const {isProcessing, reasoningHistory, statusLabel} = useAgentZeroStatus(); - /** Status label text */ - statusLabel?: string; -}; + if (!isProcessing) { + return null; + } + + return ( + + ); +} -function ConciergeThinkingMessage({report, action, reasoningHistory, statusLabel}: ConciergeThinkingMessageProps) { +function ConciergeThinkingMessageContent({ + report, + action, + reasoningHistory, + statusLabel, +}: { + report: OnyxEntry; + action?: OnyxEntry; + reasoningHistory: Array<{reasoning: string; loopCount: number; timestamp: number}>; + statusLabel: string; +}) { const styles = useThemeStyles(); const theme = useTheme(); const StyleUtils = useStyleUtils(); diff --git a/src/pages/inbox/AgentZeroStatusContext.tsx b/src/pages/inbox/AgentZeroStatusContext.tsx new file mode 100644 index 0000000000000..08fb7dab40d81 --- /dev/null +++ b/src/pages/inbox/AgentZeroStatusContext.tsx @@ -0,0 +1,224 @@ +import {agentZeroProcessingIndicatorSelector} from '@selectors/ReportNameValuePairs'; +import React, {createContext, useContext, useEffect, useRef, useState} from 'react'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useOnyx from '@hooks/useOnyx'; +import Log from '@libs/Log'; +import Pusher from '@libs/Pusher'; +import CONFIG from '@src/CONFIG'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; + +type ReasoningEntry = { + reasoning: string; + loopCount: number; + timestamp: number; +}; + +type AgentZeroStatusState = { + isProcessing: boolean; + reasoningHistory: ReasoningEntry[]; + statusLabel: string; + kickoffWaitingIndicator: () => void; +}; + +const defaultValue: AgentZeroStatusState = { + isProcessing: false, + reasoningHistory: [], + statusLabel: '', + kickoffWaitingIndicator: () => {}, +}; + +const AgentZeroStatusContext = createContext(defaultValue); + +/** + * Cheap outer guard — only subscribes to the scalar CONCIERGE_REPORT_ID. + * For non-Concierge reports (the common case), returns children directly + * without mounting any Pusher subscriptions or heavy state logic. + */ +function AgentZeroStatusProvider({reportID, children}: React.PropsWithChildren<{reportID: string | undefined}>) { + const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); + const isConciergeChat = reportID === conciergeReportID; + + if (!reportID || !isConciergeChat) { + return children; + } + + return {children}; +} + +/** + * Inner gate — all Pusher, reasoning, label, and processing state. + * Only mounted when reportID matches the Concierge report. + */ +function AgentZeroStatusGate({reportID, children}: React.PropsWithChildren<{reportID: string}>) { + const [serverLabel] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`, {selector: agentZeroProcessingIndicatorSelector}); + + const [optimisticStartTime, setOptimisticStartTime] = useState(null); + const [displayedLabel, setDisplayedLabel] = useState(''); + const [reasoningHistory, setReasoningHistory] = useState([]); + const {translate} = useLocalize(); + const prevServerLabelRef = useRef(serverLabel); + const updateTimerRef = useRef(null); + const lastUpdateTimeRef = useRef(0); + const {isOffline} = useNetwork(); + + // Tracks the current agentZeroRequestID so the Pusher callback can detect new requests + const agentZeroRequestIDRef = useRef(''); + + // Minimum time to display a label before allowing change (prevents rapid flicker) + const MIN_DISPLAY_TIME = 300; // ms + // Debounce delay for server label updates + const DEBOUNCE_DELAY = 150; // ms + + const addReasoning = (data: {reasoning: string; agentZeroRequestID: string; loopCount: number}) => { + if (!data.reasoning.trim()) { + return; + } + + const isNewRequest = agentZeroRequestIDRef.current !== data.agentZeroRequestID; + if (isNewRequest) { + agentZeroRequestIDRef.current = data.agentZeroRequestID; + } + + const entry: ReasoningEntry = { + reasoning: data.reasoning, + loopCount: data.loopCount, + timestamp: Date.now(), + }; + + if (isNewRequest) { + setReasoningHistory([entry]); + return; + } + + setReasoningHistory((prev) => { + const isDuplicate = prev.some((e) => e.loopCount === data.loopCount && e.reasoning === data.reasoning); + if (isDuplicate) { + return prev; + } + return [...prev, entry]; + }); + }; + + // Reset state when reportID changes + useEffect(() => { + setOptimisticStartTime(null); + setReasoningHistory([]); + agentZeroRequestIDRef.current = ''; + }, [reportID]); + + // Pusher subscription lifecycle + useEffect(() => { + const channelName = `${CONST.PUSHER.PRIVATE_REPORT_CHANNEL_PREFIX}${reportID}${CONFIG.PUSHER.SUFFIX}`; + + const listener = Pusher.subscribe(channelName, Pusher.TYPE.CONCIERGE_REASONING, (data: Record) => { + const eventData = data as {reasoning: string; agentZeroRequestID: string; loopCount: number}; + addReasoning(eventData); + }); + listener.catch((error: unknown) => { + Log.hmmm('[AgentZeroStatusGate] Failed to subscribe to Pusher concierge reasoning events', {reportID, error}); + }); + + return () => { + listener.unsubscribe(); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps -- addReasoning is stable (uses only refs + functional updater) + }, [reportID]); + + useEffect(() => { + const hadServerLabel = !!prevServerLabelRef.current; + const hasServerLabel = !!serverLabel; + + // Helper function to update label with timing control + const updateLabel = (newLabel: string) => { + const now = Date.now(); + const timeSinceLastUpdate = now - lastUpdateTimeRef.current; + const remainingMinTime = Math.max(0, MIN_DISPLAY_TIME - timeSinceLastUpdate); + + // Clear any pending update + if (updateTimerRef.current) { + clearTimeout(updateTimerRef.current); + updateTimerRef.current = null; + } + + // If enough time has passed or it's a critical update (clearing), update immediately + if (remainingMinTime === 0 || newLabel === '') { + if (displayedLabel !== newLabel) { + setDisplayedLabel(newLabel); + lastUpdateTimeRef.current = now; + } + } else { + // Schedule update after debounce + remaining min display time + const delay = DEBOUNCE_DELAY + remainingMinTime; + updateTimerRef.current = setTimeout(() => { + if (displayedLabel !== newLabel) { + setDisplayedLabel(newLabel); + lastUpdateTimeRef.current = Date.now(); + } + updateTimerRef.current = null; + }, delay); + } + }; + + // When server label arrives, transition smoothly without flicker + if (hasServerLabel) { + updateLabel(serverLabel); + if (optimisticStartTime) { + setOptimisticStartTime(null); + } + } + // When optimistic state is active but no server label, show "Concierge is thinking..." + else if (optimisticStartTime) { + const thinkingLabel = translate('common.thinking'); + updateLabel(thinkingLabel); + } + // Clear everything when processing ends + else if (hadServerLabel && !hasServerLabel) { + updateLabel(''); + if (reasoningHistory.length > 0) { + setReasoningHistory([]); + } + } + + prevServerLabelRef.current = serverLabel; + + // Cleanup timer on unmount + return () => { + if (!updateTimerRef.current) { + return; + } + clearTimeout(updateTimerRef.current); + }; + }, [serverLabel, reasoningHistory.length, reportID, optimisticStartTime, translate, displayedLabel]); + + useEffect(() => { + if (isOffline) { + return; + } + setOptimisticStartTime(null); + }, [isOffline, reportID]); + + const kickoffWaitingIndicator = () => { + setOptimisticStartTime(Date.now()); + }; + + const isProcessing = !isOffline && (!!serverLabel || !!optimisticStartTime); + + // eslint-disable-next-line react/jsx-no-constructed-context-values -- React Compiler handles memoization + const value: AgentZeroStatusState = { + isProcessing, + reasoningHistory, + statusLabel: displayedLabel, + kickoffWaitingIndicator, + }; + + return {children}; +} + +function useAgentZeroStatus(): AgentZeroStatusState { + return useContext(AgentZeroStatusContext); +} + +export {AgentZeroStatusProvider, useAgentZeroStatus}; +export type {AgentZeroStatusState, ReasoningEntry}; diff --git a/src/pages/inbox/ReportScreen.tsx b/src/pages/inbox/ReportScreen.tsx index cce3a62827b17..028c3abc4b4e7 100644 --- a/src/pages/inbox/ReportScreen.tsx +++ b/src/pages/inbox/ReportScreen.tsx @@ -20,7 +20,6 @@ import ScrollView from '@components/ScrollView'; import useShowWideRHPVersion from '@components/WideRHPContextProvider/useShowWideRHPVersion'; import WideRHPOverlayWrapper from '@components/WideRHPOverlayWrapper'; import useActionListContextValue from '@hooks/useActionListContextValue'; -import useAgentZeroStatusIndicator from '@hooks/useAgentZeroStatusIndicator'; import useAppFocusEvent from '@hooks/useAppFocusEvent'; import useArchivedReportsIdSet from '@hooks/useArchivedReportsIdSet'; import {useCurrentReportIDState} from '@hooks/useCurrentReportID'; @@ -110,6 +109,7 @@ import {reportByIDsSelector} from '@src/selectors/Attributes'; import type * as OnyxTypes from '@src/types/onyx'; import {getEmptyObject, isEmptyObject} from '@src/types/utils/EmptyObject'; import AccountManagerBanner from './AccountManagerBanner'; +import {AgentZeroStatusProvider} from './AgentZeroStatusContext'; import DeleteTransactionNavigateBackHandler from './DeleteTransactionNavigateBackHandler'; import HeaderView from './HeaderView'; import useReportWasDeleted from './hooks/useReportWasDeleted'; @@ -359,14 +359,6 @@ function ReportScreen({route, navigation}: ReportScreenProps) { const newTransactions = useNewTransactions(reportMetadata?.hasOnceLoadedReportActions, reportTransactions); - const isConciergeChat = isConciergeChatReport(report); - const { - isProcessing: isConciergeProcessing, - reasoningHistory: conciergeReasoningHistory, - statusLabel: conciergeStatusLabel, - kickoffWaitingIndicator, - } = useAgentZeroStatusIndicator(String(report?.reportID ?? CONST.DEFAULT_NUMBER_ID), isConciergeChat); - const {closeSidePanel} = useSidePanelActions(); useEffect(() => { @@ -1074,56 +1066,53 @@ function ReportScreen({route, navigation}: ReportScreenProps) { )} - - {(!report || shouldWaitForTransactions) && } - {!!report && !shouldDisplayMoneyRequestActionsList && !shouldWaitForTransactions ? ( - - ) : null} - {!!report && shouldDisplayMoneyRequestActionsList && !shouldWaitForTransactions ? ( - - ) : null} - {isCurrentReportLoadedFromOnyx ? ( - - ) : null} - + + + {(!report || shouldWaitForTransactions) && } + {!!report && !shouldDisplayMoneyRequestActionsList && !shouldWaitForTransactions ? ( + + ) : null} + {!!report && shouldDisplayMoneyRequestActionsList && !shouldWaitForTransactions ? ( + + ) : null} + {isCurrentReportLoadedFromOnyx ? ( + + ) : null} + + diff --git a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx index 6ecf5dfef0a11..1993fd94dfc4d 100644 --- a/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/inbox/report/ReportActionCompose/ReportActionCompose.tsx @@ -60,6 +60,7 @@ import { import {startSpan} from '@libs/telemetry/activeSpans'; import {getTransactionID, hasReceipt as hasReceiptTransactionUtils} from '@libs/TransactionUtils'; import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside'; +import {useAgentZeroStatus} from '@pages/inbox/AgentZeroStatusContext'; import ParticipantLocalTime from '@pages/inbox/report/ParticipantLocalTime'; import ReportTypingIndicator from '@pages/inbox/report/ReportTypingIndicator'; import {ActionListContext} from '@pages/inbox/ReportScreenContext'; @@ -112,11 +113,6 @@ type ReportActionComposeProps = Pick