-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Fix stuck Concierge thinking indicator when client misses Onyx clear update #85620
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
marcochavezf
wants to merge
42
commits into
main
Choose a base branch
from
marcochavezf/612534-fix-stuck-thinking-indicator
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
42 commits
Select commit
Hold shift + click to select a range
1dffb24
Fix stuck Concierge thinking indicator when Onyx batches SET+CLEAR up…
marcochavezf 96487ea
Fix CI: move Onyx.connect to lib module and fix spellcheck
marcochavezf c7604b1
Fix CI: use Connection type for Onyx.connect return value and prefer …
marcochavezf 22b0816
Scope Onyx write counter to track pending requests for multi-message …
marcochavezf 1a681db
Replace NVP version tracker with client-side TTL (lease pattern) for …
marcochavezf 0515c33
Add XMPP to cspell allowed words
marcochavezf 32c1893
Trigger CI re-run for checklist validation
marcochavezf fb98ac7
Add getNewerActions fallback when safety timer fires or network recon…
marcochavezf b1ce260
Merge branch 'main' into marcochavezf/612534-fix-stuck-thinking-indic…
marcochavezf 3de2a0b
Replace hard TTL clear with progressive retry for thinking indicator
marcochavezf f6f8678
Update safety timeout tests for progressive retry behavior
marcochavezf ac267d7
Fix ESLint: use Set for PROGRESSIVE_RETRY_INTERVALS_MS in tests
marcochavezf 8938ec1
Replace progressive retry with 30s polling for thinking indicator
marcochavezf 2e00661
Keep thinking indicator on reconnect until response arrives
marcochavezf 8d2d8c4
Restore original offline behavior: hide indicator when offline, reapp…
marcochavezf bb281de
Fix reconnect test: indicator persists through offline/online cycle
marcochavezf 9815ba2
Fix: clear indicator NVP alongside getNewerActions poll
marcochavezf cd11423
Fix: detect new actions to clear stuck indicator instead of blind NVP…
marcochavezf 5e86d94
Clear indicator NVP on network reconnect to fix stuck state after Pus…
marcochavezf 6a18430
Delegate AgentZeroStatusContext to useAgentZeroStatusIndicator hook (…
marcochavezf 5bd1c44
Clear indicator immediately when Concierge response detected via Onyx
marcochavezf 74ed559
Fix: move Concierge detection useEffect after dependency declarations
marcochavezf 0790eec
Fix ESLint: prefer-early-return + narrow hook dependencies
marcochavezf ac8ba88
Address all bot review comments on PR #85620
marcochavezf cc74304
Fix infinite re-render loop in useAgentZeroStatusIndicator tests
marcochavezf efa98d2
Remove redundant useCallback/useMemo wrappers (React Compiler handles…
marcochavezf bc884e3
Merge remote-tracking branch 'origin/main' into marcochavezf/612534-f…
marcochavezf 19aebb9
Merge main, resolve conflicts, address review comments
marcochavezf fecc854
Merge main + fix broken isModalVisible reference from #87077
marcochavezf d781630
Revert AddNewCardPage.tsx changes — already fixed in #87431
marcochavezf 82f3a1c
Merge remote-tracking branch 'origin/main' into marcochavezf/612534-f…
marcochavezf beb21b7
Fix stuck indicator on reconnect: clear stale optimistic state
marcochavezf 5e94954
Align hook with main's proven patterns to fix mobile indicator
marcochavezf 85f2019
Merge branch 'main' into marcochavezf/612534-fix-stuck-thinking-indic…
marcochavezf 7422499
Fix Concierge DM indicator + bot review follow-ups
marcochavezf ef90643
Fix CI: React Compiler, typecheck, spellcheck, ESLint
marcochavezf 78a9113
Merge remote-tracking branch 'origin/main' into marcochavezf/612534-f…
marcochavezf dc63db9
Tests: use setForceOffline after NetworkState refactor
marcochavezf dc5cf51
Merge remote-tracking branch 'origin/main' into marcochavezf/612534-f…
marcochavezf afe0eab
Fix AgentZeroStatusContext test for hook-based safety timeout
marcochavezf 7de7bca
Drop trailing waitForBatchedUpdates in rolling-safety-window test
marcochavezf 17208ab
Address situchan review: drop manual memoization, sort by created, dr…
marcochavezf File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,386 @@ | ||
| import agentZeroProcessingIndicatorSelector from '@selectors/ReportNameValuePairs'; | ||
| import {useEffect, useRef, useState, useSyncExternalStore} from 'react'; | ||
| import type {OnyxEntry} from 'react-native-onyx'; | ||
| import {clearAgentZeroProcessingIndicator, getNewerActions, subscribeToReportReasoningEvents, unsubscribeFromReportReasoningChannel} from '@libs/actions/Report'; | ||
| import ConciergeReasoningStore from '@libs/ConciergeReasoningStore'; | ||
| import type {ReasoningEntry} from '@libs/ConciergeReasoningStore'; | ||
| import CONST from '@src/CONST'; | ||
| import ONYXKEYS from '@src/ONYXKEYS'; | ||
| import type {ReportActions} from '@src/types/onyx/ReportAction'; | ||
| import useLocalize from './useLocalize'; | ||
| import useNetwork from './useNetwork'; | ||
| import useOnyx from './useOnyx'; | ||
|
|
||
| type AgentZeroStatusState = { | ||
| isProcessing: boolean; | ||
| reasoningHistory: ReasoningEntry[]; | ||
| statusLabel: string; | ||
| kickoffWaitingIndicator: () => void; | ||
| }; | ||
|
|
||
| type NewestReportAction = { | ||
| reportActionID: string; | ||
| actorAccountID?: number; | ||
| }; | ||
|
|
||
| /** | ||
| * Polling interval for fetching missed Concierge responses while the thinking indicator is visible. | ||
| * | ||
| * While the indicator is active, we poll getNewerActions every 30s to recover from | ||
| * WebSocket drops or missed Pusher events. If a Concierge reply arrives (via Pusher | ||
| * or the poll response), the normal Onyx update clears the indicator automatically. | ||
| * | ||
| * A hard safety clear at MAX_POLL_DURATION_MS ensures the indicator doesn't stay | ||
| * forever if something goes wrong. | ||
| */ | ||
| const POLL_INTERVAL_MS = 30000; | ||
|
|
||
| /** | ||
| * Maximum duration to poll before hard-clearing the indicator (safety net). | ||
| * After this time, if we're online and no response has arrived, we clear the indicator. | ||
| */ | ||
| const MAX_POLL_DURATION_MS = 120000; | ||
|
|
||
| // 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 | ||
|
|
||
| /** | ||
| * Selector that extracts the newest report action ID and actor from the report actions collection. | ||
| * | ||
| * Sorts by `created` timestamp (ISO strings compare chronologically), with reportActionID as a | ||
| * tiebreaker. reportActionID alone is unreliable because optimistic actions use random IDs, so | ||
| * a purely numeric comparison can rank them ahead of real server actions. | ||
| */ | ||
| function selectNewestReportAction(reportActions: OnyxEntry<ReportActions>): NewestReportAction | undefined { | ||
| if (!reportActions) { | ||
| return undefined; | ||
| } | ||
| const actions = Object.values(reportActions).filter(Boolean); | ||
| if (actions.length === 0) { | ||
| return undefined; | ||
| } | ||
| const newest = actions.reduce((a, b) => { | ||
| const createdA = a.created ?? ''; | ||
| const createdB = b.created ?? ''; | ||
| if (createdA !== createdB) { | ||
| return createdA > createdB ? a : b; | ||
| } | ||
| return a.reportActionID > b.reportActionID ? a : b; | ||
| }); | ||
| return { | ||
| reportActionID: newest.reportActionID, | ||
| actorAccountID: newest.actorAccountID, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Hook to manage AgentZero status indicator for chats where AgentZero responds. | ||
| * | ||
| * Callers must gate this hook at the mount level (only mount for AgentZero-enabled chats: | ||
| * Concierge DMs or policy #admins rooms). The outer `AgentZeroStatusProvider` already | ||
| * enforces this, so the hook assumes it's always running for an AgentZero chat. | ||
| * | ||
| * @param reportID - The report ID to monitor | ||
| */ | ||
| function useAgentZeroStatusIndicator(reportID: string): AgentZeroStatusState { | ||
| // Server-driven processing label from report name-value pairs (e.g. "Looking up categories...") | ||
| // Uses selector to only re-render when the specific field changes, not on any NVP change. | ||
| const [serverLabel] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`, {selector: agentZeroProcessingIndicatorSelector}); | ||
|
|
||
| // Track the newest report action so we can fetch missed actions and detect actual Concierge replies. | ||
| const [newestReportAction] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, {selector: selectNewestReportAction}); | ||
| const newestReportActionRef = useRef<NewestReportAction | undefined>(newestReportAction); | ||
| useEffect(() => { | ||
| newestReportActionRef.current = newestReportAction; | ||
| }, [newestReportAction]); | ||
|
|
||
| // Track pending optimistic requests with a counter. | ||
| // Each kickoffWaitingIndicator() call increments the counter; when a Concierge reply | ||
| // is detected (via polling, Pusher, reconnect, or safety timeout), the counter resets | ||
| // to 0 rather than decrementing — any signal that a response arrived is treated as | ||
| // resolving all pending requests (optimistic state is a display signal, not a queue). | ||
| const [pendingOptimisticRequests, setPendingOptimisticRequests] = useState(0); | ||
| // Debounced label shown to the user — smooths rapid server label changes. | ||
| // displayedLabelRef mirrors state so the label-sync effect can read the current value | ||
| // without including displayedLabel in its dependency array (avoids extra effect cycles). | ||
| const displayedLabelRef = useRef<string>(''); | ||
| const [displayedLabel, setDisplayedLabel] = useState<string>(''); | ||
| const {translate} = useLocalize(); | ||
| const prevServerLabelRef = useRef<string>(serverLabel ?? ''); | ||
| const updateTimerRef = useRef<NodeJS.Timeout | null>(null); | ||
| const lastUpdateTimeRef = useRef<number>(0); | ||
| const pollIntervalRef = useRef<NodeJS.Timeout | null>(null); | ||
| const pollSafetyTimerRef = useRef<NodeJS.Timeout | null>(null); | ||
| const isOfflineRef = useRef<boolean>(false); | ||
| // Newest reportActionID at the moment the indicator became active (raw state, ignoring | ||
| // offline). Lets us distinguish "a pre-existing Concierge action was already the newest" | ||
| // (common in Concierge DMs, where the previous reply is still the latest action) from | ||
| // "a new Concierge reply arrived after the indicator started." Without this, sending a | ||
| // message in a Concierge DM would immediately clear the just-activated indicator. | ||
| const indicatorBaselineActionIDRef = useRef<string | null>(null); | ||
| const wasIndicatorActiveRef = useRef<boolean>(false); | ||
|
|
||
| /** | ||
| * Clear the polling interval and safety timer. Called when the indicator clears normally, | ||
| * when a new processing cycle starts, or when the component unmounts. | ||
| */ | ||
| const clearPolling = () => { | ||
| if (pollIntervalRef.current) { | ||
| clearInterval(pollIntervalRef.current); | ||
| pollIntervalRef.current = null; | ||
| } | ||
| if (pollSafetyTimerRef.current) { | ||
| clearTimeout(pollSafetyTimerRef.current); | ||
| pollSafetyTimerRef.current = null; | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * Hard-clear the indicator by resetting local state and clearing the Onyx NVP. | ||
| * Called as a safety net after MAX_POLL_DURATION_MS if no response has arrived. | ||
| */ | ||
| const hardClearIndicator = () => { | ||
| // If offline, don't clear — the response may arrive when reconnected | ||
| if (isOfflineRef.current) { | ||
| return; | ||
| } | ||
| clearPolling(); | ||
| setPendingOptimisticRequests(0); | ||
| displayedLabelRef.current = ''; | ||
| setDisplayedLabel(''); | ||
| clearAgentZeroProcessingIndicator(reportID); | ||
| getNewerActions(reportID, newestReportActionRef.current?.reportActionID); | ||
| }; | ||
|
|
||
| /** | ||
| * Start polling for missed actions every POLL_INTERVAL_MS. Every time processing | ||
| * becomes active or the server label changes (renewal), the existing polling is | ||
| * cleared and restarted. | ||
| * | ||
| * - Every 30s: call getNewerActions to fetch any missed Concierge responses | ||
| * - After MAX_POLL_DURATION_MS: hard-clear the indicator if still showing (safety net) | ||
| * | ||
| * Polling stops when: indicator clears, component unmounts, or user goes offline. | ||
| */ | ||
| const startPolling = () => { | ||
| clearPolling(); | ||
|
|
||
| // Poll every 30s for missed actions. Track the newest action ID before polling | ||
| // so we can detect if new actions arrived (meaning Concierge responded). | ||
| // If new actions arrive but the NVP CLEAR was missed via Pusher, we clear | ||
| // the indicator client-side. | ||
| const prePollingActionID = newestReportActionRef.current?.reportActionID; | ||
| pollIntervalRef.current = setInterval(() => { | ||
| if (isOfflineRef.current) { | ||
| return; | ||
| } | ||
| const currentNewestReportAction = newestReportActionRef.current; | ||
| const didConciergeReplyAfterPollingStarted = | ||
| currentNewestReportAction?.actorAccountID === CONST.ACCOUNT_ID.CONCIERGE && currentNewestReportAction.reportActionID !== prePollingActionID; | ||
|
|
||
| if (didConciergeReplyAfterPollingStarted) { | ||
| clearAgentZeroProcessingIndicator(reportID); | ||
| clearPolling(); | ||
| setPendingOptimisticRequests(0); | ||
| return; | ||
| } | ||
| getNewerActions(reportID, currentNewestReportAction?.reportActionID); | ||
| }, POLL_INTERVAL_MS); | ||
|
|
||
| // Safety net: hard-clear after MAX_POLL_DURATION_MS | ||
| pollSafetyTimerRef.current = setTimeout(() => { | ||
| hardClearIndicator(); | ||
| }, MAX_POLL_DURATION_MS); | ||
| }; | ||
|
|
||
| // On reconnect, proactively clear stale optimistic state + NVP and refetch missed actions. | ||
| // | ||
| // If the server processed the request and cleared the NVP while Pusher was disconnected, | ||
| // Onyx sync can deliver the stale (uncleared) NVP on reconnect. Clearing the NVP locally | ||
| // ensures we don't show a stuck indicator while we wait for polling to detect the reply. | ||
| // If the server is still genuinely processing, its next Pusher/Onyx update will repopulate | ||
| // the NVP and re-trigger the indicator + polling via the label-sync effect. | ||
| const {isOffline} = useNetwork({ | ||
| onReconnect: () => { | ||
| const wasOptimistic = pendingOptimisticRequests > 0; | ||
|
|
||
| if (wasOptimistic) { | ||
| setPendingOptimisticRequests(0); | ||
| clearAgentZeroProcessingIndicator(reportID); | ||
| } | ||
|
|
||
| // Fetch missed actions so the Onyx-driven Concierge-reply detection can fire. | ||
| getNewerActions(reportID, newestReportActionRef.current?.reportActionID); | ||
|
|
||
| // Only restart polling if we still have a server-driven label after the clear — | ||
| // otherwise there's nothing to poll for and the next serverLabel arrival will | ||
| // restart polling via the label-sync effect below. | ||
| if (serverLabel) { | ||
| startPolling(); | ||
| } | ||
| }, | ||
| }); | ||
|
|
||
| // Subscribe to ConciergeReasoningStore using useSyncExternalStore for correct | ||
| // synchronization with React's render cycle. React Compiler memoizes these closures | ||
| // based on reportID, so useSyncExternalStore doesn't unsubscribe/resubscribe on every render. | ||
| const subscribeToReasoningStore = (onStoreChange: () => void) => { | ||
| const unsubscribe = ConciergeReasoningStore.subscribe((updatedReportID) => { | ||
| if (updatedReportID !== reportID) { | ||
| return; | ||
| } | ||
| onStoreChange(); | ||
| }); | ||
| return unsubscribe; | ||
| }; | ||
| const getReasoningSnapshot = () => ConciergeReasoningStore.getReasoningHistory(reportID); | ||
| const reasoningHistory = useSyncExternalStore(subscribeToReasoningStore, getReasoningSnapshot, getReasoningSnapshot); | ||
|
|
||
| useEffect(() => { | ||
| subscribeToReportReasoningEvents(reportID); | ||
|
|
||
| // Cleanup: unsubscribeFromReportReasoningChannel handles Pusher unsubscribing, | ||
| // clearing reasoning history from ConciergeReasoningStore, and subscription tracking | ||
| return () => { | ||
| unsubscribeFromReportReasoningChannel(reportID); | ||
| }; | ||
| }, [reportID]); | ||
|
|
||
| // Synchronize the displayed label with debounce and minimum display time. | ||
| // displayedLabelRef mirrors state so the effect can check the current value without depending on displayedLabel. | ||
| useEffect(() => { | ||
| const hadServerLabel = !!prevServerLabelRef.current; | ||
| const hasServerLabel = !!serverLabel; | ||
|
|
||
| let targetLabel = ''; | ||
| if (hasServerLabel) { | ||
| targetLabel = serverLabel ?? ''; | ||
| } else if (pendingOptimisticRequests > 0) { | ||
| targetLabel = translate('common.thinking'); | ||
| } | ||
|
|
||
| // Start/reset polling when server label arrives (acts as a lease renewal) | ||
| if (hasServerLabel) { | ||
| startPolling(); | ||
| if (pendingOptimisticRequests > 0) { | ||
| // eslint-disable-next-line react-hooks/set-state-in-effect -- server label takeover; fires once per optimistic→server transition | ||
| setPendingOptimisticRequests(0); | ||
| } | ||
| } | ||
| // Clear polling when processing ends | ||
| else if (pendingOptimisticRequests === 0) { | ||
| clearPolling(); | ||
| if (hadServerLabel && reasoningHistory.length > 0) { | ||
| ConciergeReasoningStore.clearReasoning(reportID); | ||
| } | ||
| } | ||
|
|
||
| // Use ref to check current value without depending on displayedLabel in deps | ||
| if (displayedLabelRef.current === targetLabel) { | ||
| prevServerLabelRef.current = serverLabel ?? ''; | ||
| return; | ||
| } | ||
|
|
||
| const now = Date.now(); | ||
| const timeSinceLastUpdate = now - lastUpdateTimeRef.current; | ||
| const remainingMinTime = Math.max(0, MIN_DISPLAY_TIME - timeSinceLastUpdate); | ||
|
|
||
| if (updateTimerRef.current) { | ||
| clearTimeout(updateTimerRef.current); | ||
| updateTimerRef.current = null; | ||
| } | ||
|
|
||
| // Immediate update when enough time has passed or when clearing the label | ||
| if (remainingMinTime === 0 || targetLabel === '') { | ||
| displayedLabelRef.current = targetLabel; | ||
| // eslint-disable-next-line react-hooks/set-state-in-effect -- guarded by displayedLabelRef check above; fires once per serverLabel/optimistic transition | ||
| setDisplayedLabel(targetLabel); | ||
| lastUpdateTimeRef.current = now; | ||
| } else { | ||
| // Schedule update after debounce + remaining min display time | ||
| const delay = DEBOUNCE_DELAY + remainingMinTime; | ||
| updateTimerRef.current = setTimeout(() => { | ||
| displayedLabelRef.current = targetLabel; | ||
| setDisplayedLabel(targetLabel); | ||
| lastUpdateTimeRef.current = Date.now(); | ||
| updateTimerRef.current = null; | ||
| }, delay); | ||
| } | ||
|
|
||
| prevServerLabelRef.current = serverLabel ?? ''; | ||
|
|
||
| return () => { | ||
| if (!updateTimerRef.current) { | ||
| return; | ||
| } | ||
| clearTimeout(updateTimerRef.current); | ||
| }; | ||
| }, [serverLabel, reasoningHistory.length, reportID, pendingOptimisticRequests, translate, startPolling, clearPolling]); | ||
|
|
||
| useEffect(() => { | ||
| isOfflineRef.current = isOffline; | ||
| }, [isOffline]); | ||
|
|
||
| // Clean up polling on unmount (and if clearPolling identity changes — no-op when no timers) | ||
| useEffect( | ||
| () => () => { | ||
| clearPolling(); | ||
| }, | ||
| [clearPolling], | ||
| ); | ||
|
|
||
| const kickoffWaitingIndicator = () => { | ||
| setPendingOptimisticRequests((prev) => prev + 1); | ||
| startPolling(); | ||
| }; | ||
|
|
||
| // Capture the newest reportActionID as a baseline whenever the indicator transitions | ||
| // from inactive to active (serverLabel or optimistic). The baseline survives offline | ||
| // cycles (it tracks raw active state, not UI-visible isProcessing) so a new Concierge | ||
| // reply that arrives during offline → online is still detected as "new" on reconnect. | ||
| const isIndicatorActive = !!serverLabel || pendingOptimisticRequests > 0; | ||
| useEffect(() => { | ||
| if (isIndicatorActive && !wasIndicatorActiveRef.current) { | ||
| indicatorBaselineActionIDRef.current = newestReportActionRef.current?.reportActionID ?? null; | ||
| } else if (!isIndicatorActive) { | ||
| indicatorBaselineActionIDRef.current = null; | ||
| } | ||
| wasIndicatorActiveRef.current = isIndicatorActive; | ||
| }, [isIndicatorActive]); | ||
|
|
||
| // Immediately clear the indicator when a *new* Concierge response arrives while processing. | ||
| // In a Concierge DM, the newest action is usually already from Concierge (the previous reply), | ||
| // so we only clear when the newest action ID is different from the baseline captured when | ||
| // the indicator activated. This eliminates the 30s delay waiting for the next poll cycle. | ||
| const newestActorAccountID = newestReportAction?.actorAccountID; | ||
| const newestActionID = newestReportAction?.reportActionID; | ||
| useEffect(() => { | ||
| if (newestActorAccountID !== CONST.ACCOUNT_ID.CONCIERGE) { | ||
| return; | ||
| } | ||
| if (!serverLabel && pendingOptimisticRequests === 0) { | ||
| return; | ||
| } | ||
| if (!newestActionID || newestActionID === indicatorBaselineActionIDRef.current) { | ||
| return; | ||
| } | ||
| clearAgentZeroProcessingIndicator(reportID); | ||
| clearPolling(); | ||
|
marcochavezf marked this conversation as resolved.
|
||
| // eslint-disable-next-line react-hooks/set-state-in-effect -- reply-detection transition; guarded by the early returns above, fires once per Concierge reply | ||
| setPendingOptimisticRequests(0); | ||
| }, [newestActorAccountID, newestActionID, serverLabel, pendingOptimisticRequests, reportID, clearPolling]); | ||
|
|
||
| const isProcessing = !isOffline && isIndicatorActive; | ||
|
|
||
| return { | ||
| isProcessing, | ||
| reasoningHistory, | ||
| statusLabel: displayedLabel, | ||
| kickoffWaitingIndicator, | ||
| }; | ||
| } | ||
|
|
||
| export default useAgentZeroStatusIndicator; | ||
| export type {AgentZeroStatusState}; | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.