From e9cebe64ff68e58a738dd6c57ba191fbde66fa56 Mon Sep 17 00:00:00 2001 From: 777genius Date: Sun, 24 May 2026 00:23:04 +0300 Subject: [PATCH] feat: improve provider status startup hydration Keep connected provider details visible while refreshes are in flight, restore reusable provider status UI, and separate fast startup summaries from heavier provider hydration. Replace the fixed 30s startup wait with an idle-aware scheduler with a 30s safety cap and cover the Electron timer binding crash. --- .../renderer/hooks/useCodexAccountSnapshot.ts | 27 +- src/features/codex-account/renderer/index.ts | 2 + src/main/ipc/cliInstaller.ts | 25 +- .../common/GlobalProviderStatusHeader.tsx | 261 +------------ .../common/ProviderActivityStatusStrip.tsx | 323 ++++++++++++++++ .../components/dashboard/CliStatusBanner.tsx | 20 +- .../extensions/ExtensionStoreView.tsx | 13 +- .../runtime/providerConnectionUi.ts | 44 +++ .../settings/sections/CliStatusSection.tsx | 15 +- .../team/dialogs/CreateTeamDialog.tsx | 25 +- .../team/dialogs/LaunchTeamDialog.tsx | 16 + src/renderer/store/index.ts | 32 +- src/renderer/utils/startupIdleTask.ts | 96 +++++ .../renderer/useCodexAccountSnapshot.test.ts | 81 ++++ test/main/ipc/cliInstaller.test.ts | 49 +++ .../cli/CliStatusVisibility.test.ts | 86 +++++ .../common/GlobalProviderStatusHeader.test.ts | 217 +---------- .../ProviderActivityStatusStrip.test.ts | 362 ++++++++++++++++++ .../extensions/ExtensionStoreView.test.ts | 6 +- .../team/dialogs/LaunchTeamDialog.test.ts | 2 + test/renderer/utils/startupIdleTask.test.ts | 182 +++++++++ 21 files changed, 1379 insertions(+), 505 deletions(-) create mode 100644 src/renderer/components/common/ProviderActivityStatusStrip.tsx create mode 100644 src/renderer/utils/startupIdleTask.ts create mode 100644 test/renderer/components/common/ProviderActivityStatusStrip.test.ts create mode 100644 test/renderer/utils/startupIdleTask.test.ts diff --git a/src/features/codex-account/renderer/hooks/useCodexAccountSnapshot.ts b/src/features/codex-account/renderer/hooks/useCodexAccountSnapshot.ts index 8bdad308..1269ffd6 100644 --- a/src/features/codex-account/renderer/hooks/useCodexAccountSnapshot.ts +++ b/src/features/codex-account/renderer/hooks/useCodexAccountSnapshot.ts @@ -1,6 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { api, isElectronMode } from '@renderer/api'; +import { scheduleStartupIdleTask } from '@renderer/utils/startupIdleTask'; import type { CodexAccountSnapshotDto, @@ -11,7 +12,9 @@ const CODEX_PENDING_LOGIN_REFRESH_MS = 3_000; const CODEX_VISIBLE_RATE_LIMITS_REFRESH_MS = 10_000; const CODEX_VISIBLE_STANDARD_REFRESH_MS = 20_000; const CODEX_HIDDEN_REFRESH_MS = 60_000; -export const CODEX_ACCOUNT_STARTUP_IDLE_DELAY_MS = 30_000; +export const CODEX_ACCOUNT_STARTUP_IDLE_MIN_DELAY_MS = 2_000; +export const CODEX_ACCOUNT_STARTUP_IDLE_MAX_DELAY_MS = 30_000; +export const CODEX_ACCOUNT_STARTUP_IDLE_DELAY_MS = CODEX_ACCOUNT_STARTUP_IDLE_MAX_DELAY_MS; function isDocumentVisible(): boolean { if (typeof document === 'undefined') { @@ -43,6 +46,7 @@ export function useCodexAccountSnapshot(options: { enabled: boolean; includeRateLimits?: boolean; initialRefreshDelayMs?: number; + initialRefreshMaxDelayMs?: number; }): { snapshot: CodexAccountSnapshotDto | null; loading: boolean; @@ -65,6 +69,7 @@ export function useCodexAccountSnapshot(options: { const [visible, setVisible] = useState(() => isDocumentVisible()); const lastUpdatedAtRef = useRef(null); const initialRefreshDelayMs = options.initialRefreshDelayMs ?? 0; + const initialRefreshMaxDelayMs = options.initialRefreshMaxDelayMs; const [initialRefreshAttempted, setInitialRefreshAttempted] = useState( () => initialRefreshDelayMs <= 0 ); @@ -124,7 +129,7 @@ export function useCodexAccountSnapshot(options: { } let active = true; - let initialRefreshTimer: number | null = null; + let cancelInitialRefresh: (() => void) | null = null; const startInitialSnapshotRequest = (): void => { if (!active || lastUpdatedAtRef.current !== null) { @@ -169,7 +174,18 @@ export function useCodexAccountSnapshot(options: { }; if (initialRefreshDelayMs > 0) { - initialRefreshTimer = window.setTimeout(startInitialSnapshotRequest, initialRefreshDelayMs); + if (typeof initialRefreshMaxDelayMs === 'number') { + cancelInitialRefresh = scheduleStartupIdleTask(startInitialSnapshotRequest, { + minDelayMs: initialRefreshDelayMs, + maxDelayMs: initialRefreshMaxDelayMs, + }); + } else { + const initialRefreshTimer = window.setTimeout( + startInitialSnapshotRequest, + initialRefreshDelayMs + ); + cancelInitialRefresh = () => window.clearTimeout(initialRefreshTimer); + } } else { startInitialSnapshotRequest(); } @@ -180,15 +196,14 @@ export function useCodexAccountSnapshot(options: { return () => { active = false; - if (initialRefreshTimer) { - window.clearTimeout(initialRefreshTimer); - } + cancelInitialRefresh?.(); unsubscribe(); }; }, [ applySnapshot, electronMode, initialRefreshDelayMs, + initialRefreshMaxDelayMs, options.enabled, options.includeRateLimits, ]); diff --git a/src/features/codex-account/renderer/index.ts b/src/features/codex-account/renderer/index.ts index 3cfd5851..baa82d23 100644 --- a/src/features/codex-account/renderer/index.ts +++ b/src/features/codex-account/renderer/index.ts @@ -1,5 +1,7 @@ export { CODEX_ACCOUNT_STARTUP_IDLE_DELAY_MS, + CODEX_ACCOUNT_STARTUP_IDLE_MAX_DELAY_MS, + CODEX_ACCOUNT_STARTUP_IDLE_MIN_DELAY_MS, useCodexAccountSnapshot, } from './hooks/useCodexAccountSnapshot'; export { mergeCodexCliStatusWithSnapshot } from './mergeCodexCliStatusWithSnapshot'; diff --git a/src/main/ipc/cliInstaller.ts b/src/main/ipc/cliInstaller.ts index fd967b39..16d618f3 100644 --- a/src/main/ipc/cliInstaller.ts +++ b/src/main/ipc/cliInstaller.ts @@ -86,11 +86,28 @@ function isDeferredProviderStatusSnapshot(status: CliInstallationStatus): boolea ); } -function canUseLatestSnapshotForCacheKey( +function hasDeferredProviderStatus(status: CliInstallationStatus): boolean { + return ( + status.flavor === 'agent_teams_orchestrator' && + status.providers.some( + (provider) => provider.statusMessage === CLI_PROVIDER_STATUS_DEFERRED_MESSAGE + ) + ); +} + +function canUseStatusForCacheKey( cacheKey: CliInstallerProviderStatusMode, status: CliInstallationStatus ): boolean { - return cacheKey === 'defer' || !isDeferredProviderStatusSnapshot(status); + if (cacheKey === 'defer') { + return true; + } + + return ( + !status.authStatusChecking && + !hasDeferredProviderStatus(status) && + !isDeferredProviderStatusSnapshot(status) + ); } /** @@ -140,7 +157,7 @@ async function handleGetStatus( const latestSnapshot = service.getLatestStatusSnapshot(); const cached = cachedStatus.get(cacheKey); if (cached && Date.now() - cached.at < STATUS_CACHE_TTL_MS) { - if (latestSnapshot && canUseLatestSnapshotForCacheKey(cacheKey, latestSnapshot)) { + if (latestSnapshot && canUseStatusForCacheKey(cacheKey, latestSnapshot)) { cachedStatus.set(cacheKey, { value: latestSnapshot, at: Date.now() }); return { success: true, data: latestSnapshot }; } @@ -153,7 +170,7 @@ async function handleGetStatus( const request = service .getStatus(normalizedOptions) .then((status) => { - if (generation === statusCacheGeneration) { + if (generation === statusCacheGeneration && canUseStatusForCacheKey(cacheKey, status)) { cachedStatus.set(cacheKey, { value: status, at: Date.now() }); } return status; diff --git a/src/renderer/components/common/GlobalProviderStatusHeader.tsx b/src/renderer/components/common/GlobalProviderStatusHeader.tsx index 5f37cc24..8daa8480 100644 --- a/src/renderer/components/common/GlobalProviderStatusHeader.tsx +++ b/src/renderer/components/common/GlobalProviderStatusHeader.tsx @@ -1,88 +1,17 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useMemo } from 'react'; import { - CODEX_ACCOUNT_STARTUP_IDLE_DELAY_MS, + CODEX_ACCOUNT_STARTUP_IDLE_MAX_DELAY_MS, + CODEX_ACCOUNT_STARTUP_IDLE_MIN_DELAY_MS, mergeCodexCliStatusWithSnapshot, useCodexAccountSnapshot, } from '@features/codex-account/renderer'; import { isElectronMode } from '@renderer/api'; -import { formatProviderStatusText } from '@renderer/components/runtime/providerConnectionUi'; import { useStore } from '@renderer/store'; import { createLoadingMultimodelCliStatus } from '@renderer/store/slices/cliInstallerSlice'; -import { filterMainScreenCliProviders } from '@renderer/utils/geminiUiFreeze'; -import { CLI_PROVIDER_STATUS_DEFERRED_MESSAGE } from '@shared/types/cliInstaller'; -import { AlertTriangle, CheckCircle2, Loader2 } from 'lucide-react'; import { useShallow } from 'zustand/react/shallow'; -import { ProviderBrandLogo } from './ProviderBrandLogo'; - -import type { CliProviderId, CliProviderStatus } from '@shared/types'; - -interface ProviderActivityState { - provider: CliProviderStatus; - loading: boolean; - error: boolean; -} - -function isProviderCardLoading(provider: CliProviderStatus, providerLoading: boolean): boolean { - return ( - providerLoading || - (!provider.authenticated && - (provider.statusMessage === 'Checking...' || - provider.statusMessage === CLI_PROVIDER_STATUS_DEFERRED_MESSAGE) && - provider.models.length === 0 && - provider.backend == null) - ); -} - -function shouldMaskCodexNegativeBootstrapState( - sourceProvider: CliProviderStatus | null, - mergedProvider: CliProviderStatus -): boolean { - return ( - sourceProvider?.providerId === 'codex' && - sourceProvider.statusMessage === 'Checking...' && - mergedProvider.providerId === 'codex' && - mergedProvider.connection?.codex?.launchReadinessState === 'missing_auth' && - mergedProvider.connection.codex.login.status === 'idle' - ); -} - -function getActivityToneStyles(tone: 'loading' | 'checked' | 'error'): { - borderColor: string; - backgroundColor: string; - textColor: string; - statusColor: string; -} { - switch (tone) { - case 'checked': - return { - borderColor: 'rgba(34, 197, 94, 0.22)', - backgroundColor: 'rgba(34, 197, 94, 0.08)', - textColor: '#dcfce7', - statusColor: '#86efac', - }; - case 'error': - return { - borderColor: 'rgba(239, 68, 68, 0.28)', - backgroundColor: 'rgba(239, 68, 68, 0.08)', - textColor: '#fee2e2', - statusColor: '#fca5a5', - }; - case 'loading': - default: - return { - borderColor: 'var(--color-border-emphasis)', - backgroundColor: 'rgba(255, 255, 255, 0.03)', - textColor: 'var(--color-text-secondary)', - statusColor: 'var(--color-text-muted)', - }; - } -} - -function areProviderIdListsEqual(nextIds: CliProviderId[], prevIds: CliProviderId[]): boolean { - return nextIds.length === prevIds.length && nextIds.every((id, index) => prevIds[index] === id); -} +import { ProviderActivityStatusStrip } from './ProviderActivityStatusStrip'; export const GlobalProviderStatusHeader = (): React.JSX.Element | null => { const isElectron = useMemo(() => isElectronMode(), []); @@ -109,7 +38,6 @@ export const GlobalProviderStatusHeader = (): React.JSX.Element | null => { }; }) ); - const [cycleProviderIds, setCycleProviderIds] = useState([]); const loadingCliStatus = useMemo( () => @@ -126,7 +54,8 @@ export const GlobalProviderStatusHeader = (): React.JSX.Element | null => { loadingCliStatus?.flavor === 'agent_teams_orchestrator' && Boolean(loadingCliStatus?.providers.some((provider) => provider.providerId === 'codex')), includeRateLimits: false, - initialRefreshDelayMs: CODEX_ACCOUNT_STARTUP_IDLE_DELAY_MS, + initialRefreshDelayMs: CODEX_ACCOUNT_STARTUP_IDLE_MIN_DELAY_MS, + initialRefreshMaxDelayMs: CODEX_ACCOUNT_STARTUP_IDLE_MAX_DELAY_MS, }); const effectiveCliStatus = useMemo( @@ -138,177 +67,19 @@ export const GlobalProviderStatusHeader = (): React.JSX.Element | null => { Boolean(loadingCliStatus?.providers.some((provider) => provider.providerId === 'codex')) && !codexAccount.snapshot; - const sourceProviderMap = useMemo( - () => - new Map( - (loadingCliStatus?.providers ?? []).map((provider) => [provider.providerId, provider]) - ), - [loadingCliStatus?.providers] - ); - - const providerStates = useMemo(() => { - const visibleProviders = filterMainScreenCliProviders(effectiveCliStatus?.providers ?? []); - - return visibleProviders.map((provider) => { - const sourceProvider = sourceProviderMap.get(provider.providerId) ?? null; - const loading = - isProviderCardLoading(provider, cliProviderStatusLoading[provider.providerId] === true) || - (provider.providerId === 'codex' && codexSnapshotPending) || - shouldMaskCodexNegativeBootstrapState(sourceProvider, provider); - - return { - provider, - loading, - error: !loading && provider.verificationState === 'error', - }; - }); - }, [ - cliProviderStatusLoading, - codexSnapshotPending, - effectiveCliStatus?.providers, - sourceProviderMap, - ]); - - const visibleProviderIds = useMemo( - () => providerStates.map((state) => state.provider.providerId), - [providerStates] - ); - const loadingProviderIds = useMemo( - () => providerStates.filter((state) => state.loading).map((state) => state.provider.providerId), - [providerStates] - ); - const errorProviderIds = useMemo( - () => providerStates.filter((state) => state.error).map((state) => state.provider.providerId), - [providerStates] - ); - const providerStateMap = useMemo( - () => new Map(providerStates.map((state) => [state.provider.providerId, state])), - [providerStates] - ); - - useEffect(() => { - setCycleProviderIds((previousIds) => { - const visiblePreviousIds = previousIds.filter((providerId) => - visibleProviderIds.includes(providerId) - ); - - if (loadingProviderIds.length > 0) { - const nextIds = [...visiblePreviousIds]; - for (const providerId of loadingProviderIds) { - if (!nextIds.includes(providerId)) { - nextIds.push(providerId); - } - } - - return areProviderIdListsEqual(nextIds, previousIds) ? previousIds : nextIds; - } - - if (errorProviderIds.length > 0) { - return areProviderIdListsEqual(errorProviderIds, previousIds) - ? previousIds - : errorProviderIds; - } - - return previousIds.length === 0 ? previousIds : []; - }); - }, [errorProviderIds, loadingProviderIds, visibleProviderIds]); - - const displayProviderIds = useMemo(() => { - if (loadingProviderIds.length > 0) { - const activeCycleIds = ( - cycleProviderIds.length > 0 ? cycleProviderIds : loadingProviderIds - ).filter((providerId) => providerStateMap.has(providerId)); - return Array.from(new Set([...activeCycleIds, ...errorProviderIds])); - } - - if (errorProviderIds.length > 0) { - return errorProviderIds; - } - - return []; - }, [cycleProviderIds, errorProviderIds, loadingProviderIds, providerStateMap]); - - if ( - !isElectron || - isDashboardFocused || - !multimodelEnabled || - effectiveCliStatus?.flavor !== 'agent_teams_orchestrator' || - !effectiveCliStatus.installed || - displayProviderIds.length === 0 - ) { + if (isDashboardFocused) { return null; } return ( -
- - Provider Activity - -
- {displayProviderIds.map((providerId) => { - const providerState = providerStateMap.get(providerId); - if (!providerState) { - return null; - } - - const tone = providerState.loading - ? 'loading' - : providerState.error - ? 'error' - : 'checked'; - const styles = getActivityToneStyles(tone); - const statusText = - tone === 'loading' - ? 'Checking...' - : tone === 'error' - ? formatProviderStatusText(providerState.provider) - : 'Checked'; - - return ( -
- {tone === 'loading' ? ( - - ) : tone === 'error' ? ( - - ) : ( - - )} - - - {providerState.provider.displayName} - - - {statusText} - -
- ); - })} -
-
+ ); }; diff --git a/src/renderer/components/common/ProviderActivityStatusStrip.tsx b/src/renderer/components/common/ProviderActivityStatusStrip.tsx new file mode 100644 index 00000000..1c576dd7 --- /dev/null +++ b/src/renderer/components/common/ProviderActivityStatusStrip.tsx @@ -0,0 +1,323 @@ +import { useEffect, useMemo, useState } from 'react'; + +import { isElectronMode } from '@renderer/api'; +import { formatProviderStatusText } from '@renderer/components/runtime/providerConnectionUi'; +import { createLoadingMultimodelCliStatus } from '@renderer/store/slices/cliInstallerSlice'; +import { filterMainScreenCliProviders } from '@renderer/utils/geminiUiFreeze'; +import { CLI_PROVIDER_STATUS_DEFERRED_MESSAGE } from '@shared/types/cliInstaller'; +import { AlertTriangle, CheckCircle2, Loader2 } from 'lucide-react'; + +import { ProviderBrandLogo } from './ProviderBrandLogo'; + +import type { CliInstallationStatus, CliProviderId, CliProviderStatus } from '@shared/types'; + +interface ProviderActivityState { + provider: CliProviderStatus; + loading: boolean; + error: boolean; +} + +interface ProviderActivityStatusStripProps { + readonly cliStatus: CliInstallationStatus | null | undefined; + readonly sourceCliStatus?: CliInstallationStatus | null | undefined; + readonly cliStatusLoading: boolean; + readonly cliProviderStatusLoading: Partial>; + readonly multimodelEnabled: boolean; + readonly codexSnapshotPending?: boolean; + readonly providerIds?: readonly CliProviderId[]; + readonly className?: string; + readonly label?: string | null; +} + +function isProviderCardLoading(provider: CliProviderStatus, providerLoading: boolean): boolean { + return ( + providerLoading || + (!provider.authenticated && + (provider.statusMessage === 'Checking...' || + provider.statusMessage === CLI_PROVIDER_STATUS_DEFERRED_MESSAGE) && + provider.models.length === 0 && + provider.backend == null) + ); +} + +function shouldMaskCodexNegativeBootstrapState( + sourceProvider: CliProviderStatus | null, + mergedProvider: CliProviderStatus +): boolean { + return ( + sourceProvider?.providerId === 'codex' && + sourceProvider.statusMessage === 'Checking...' && + mergedProvider.providerId === 'codex' && + mergedProvider.connection?.codex?.launchReadinessState === 'missing_auth' && + mergedProvider.connection.codex.login.status === 'idle' + ); +} + +function getActivityToneStyles(tone: 'loading' | 'checked' | 'error'): { + borderColor: string; + backgroundColor: string; + textColor: string; + statusColor: string; +} { + switch (tone) { + case 'checked': + return { + borderColor: 'rgba(34, 197, 94, 0.22)', + backgroundColor: 'rgba(34, 197, 94, 0.08)', + textColor: '#dcfce7', + statusColor: '#86efac', + }; + case 'error': + return { + borderColor: 'rgba(239, 68, 68, 0.28)', + backgroundColor: 'rgba(239, 68, 68, 0.08)', + textColor: '#fee2e2', + statusColor: '#fca5a5', + }; + case 'loading': + default: + return { + borderColor: 'var(--color-border-emphasis)', + backgroundColor: 'rgba(255, 255, 255, 0.03)', + textColor: 'var(--color-text-secondary)', + statusColor: 'var(--color-text-muted)', + }; + } +} + +function areProviderIdListsEqual(nextIds: CliProviderId[], prevIds: CliProviderId[]): boolean { + return nextIds.length === prevIds.length && nextIds.every((id, index) => prevIds[index] === id); +} + +function useProviderActivityDisplay({ + cliStatus, + sourceCliStatus, + cliStatusLoading, + cliProviderStatusLoading, + multimodelEnabled, + codexSnapshotPending = false, + providerIds, +}: Pick< + ProviderActivityStatusStripProps, + | 'cliStatus' + | 'sourceCliStatus' + | 'cliStatusLoading' + | 'cliProviderStatusLoading' + | 'multimodelEnabled' + | 'codexSnapshotPending' + | 'providerIds' +>): { + displayProviderIds: CliProviderId[]; + providerStateMap: Map; + shouldRender: boolean; +} { + const [cycleProviderIds, setCycleProviderIds] = useState([]); + const renderCliStatus = useMemo( + () => + !cliStatus && cliStatusLoading && multimodelEnabled + ? createLoadingMultimodelCliStatus() + : (cliStatus ?? null), + [cliStatus, cliStatusLoading, multimodelEnabled] + ); + const sourceStatus = sourceCliStatus ?? renderCliStatus; + const providerIdSet = useMemo( + () => (providerIds ? new Set(providerIds) : null), + [providerIds] + ); + const sourceProviderMap = useMemo( + () => + new Map((sourceStatus?.providers ?? []).map((provider) => [provider.providerId, provider])), + [sourceStatus?.providers] + ); + + const providerStates = useMemo(() => { + const visibleProviders = filterMainScreenCliProviders(renderCliStatus?.providers ?? []).filter( + (provider) => !providerIdSet || providerIdSet.has(provider.providerId) + ); + + return visibleProviders.map((provider) => { + const sourceProvider = sourceProviderMap.get(provider.providerId) ?? null; + const loading = + isProviderCardLoading(provider, cliProviderStatusLoading[provider.providerId] === true) || + (provider.providerId === 'codex' && codexSnapshotPending) || + shouldMaskCodexNegativeBootstrapState(sourceProvider, provider); + + return { + provider, + loading, + error: !loading && provider.verificationState === 'error', + }; + }); + }, [ + cliProviderStatusLoading, + codexSnapshotPending, + providerIdSet, + renderCliStatus?.providers, + sourceProviderMap, + ]); + + const visibleProviderIds = useMemo( + () => providerStates.map((state) => state.provider.providerId), + [providerStates] + ); + const loadingProviderIds = useMemo( + () => providerStates.filter((state) => state.loading).map((state) => state.provider.providerId), + [providerStates] + ); + const errorProviderIds = useMemo( + () => providerStates.filter((state) => state.error).map((state) => state.provider.providerId), + [providerStates] + ); + const providerStateMap = useMemo( + () => new Map(providerStates.map((state) => [state.provider.providerId, state])), + [providerStates] + ); + + useEffect(() => { + setCycleProviderIds((previousIds) => { + const visiblePreviousIds = previousIds.filter((providerId) => + visibleProviderIds.includes(providerId) + ); + + if (loadingProviderIds.length > 0) { + const nextIds = [...visiblePreviousIds]; + for (const providerId of loadingProviderIds) { + if (!nextIds.includes(providerId)) { + nextIds.push(providerId); + } + } + + return areProviderIdListsEqual(nextIds, previousIds) ? previousIds : nextIds; + } + + if (errorProviderIds.length > 0) { + return areProviderIdListsEqual(errorProviderIds, previousIds) + ? previousIds + : errorProviderIds; + } + + return previousIds.length === 0 ? previousIds : []; + }); + }, [errorProviderIds, loadingProviderIds, visibleProviderIds]); + + const displayProviderIds = useMemo(() => { + if (loadingProviderIds.length > 0) { + const activeCycleIds = ( + cycleProviderIds.length > 0 ? cycleProviderIds : loadingProviderIds + ).filter((providerId) => providerStateMap.has(providerId)); + return Array.from(new Set([...activeCycleIds, ...errorProviderIds])); + } + + if (errorProviderIds.length > 0) { + return errorProviderIds; + } + + return []; + }, [cycleProviderIds, errorProviderIds, loadingProviderIds, providerStateMap]); + + return { + displayProviderIds, + providerStateMap, + shouldRender: + isElectronMode() && + multimodelEnabled && + renderCliStatus?.flavor === 'agent_teams_orchestrator' && + renderCliStatus.installed && + displayProviderIds.length > 0, + }; +} + +export const ProviderActivityStatusStrip = ({ + cliStatus, + sourceCliStatus, + cliStatusLoading, + cliProviderStatusLoading, + multimodelEnabled, + codexSnapshotPending = false, + providerIds, + className = '', + label = 'Provider Activity', +}: ProviderActivityStatusStripProps): React.JSX.Element | null => { + const { displayProviderIds, providerStateMap, shouldRender } = useProviderActivityDisplay({ + cliStatus, + sourceCliStatus, + cliStatusLoading, + cliProviderStatusLoading, + multimodelEnabled, + codexSnapshotPending, + providerIds, + }); + + if (!shouldRender) { + return null; + } + + return ( +
+ {label ? ( + + {label} + + ) : null} +
+ {displayProviderIds.map((providerId) => { + const providerState = providerStateMap.get(providerId); + if (!providerState) { + return null; + } + + const tone = providerState.loading + ? 'loading' + : providerState.error + ? 'error' + : 'checked'; + const styles = getActivityToneStyles(tone); + const statusText = + tone === 'loading' + ? 'Checking...' + : tone === 'error' + ? formatProviderStatusText(providerState.provider) + : 'Checked'; + + return ( +
+ {tone === 'loading' ? ( + + ) : tone === 'error' ? ( + + ) : ( + + )} + + + {providerState.provider.displayName} + + + {statusText} + +
+ ); + })} +
+
+ ); +}; diff --git a/src/renderer/components/dashboard/CliStatusBanner.tsx b/src/renderer/components/dashboard/CliStatusBanner.tsx index 0c4c2f55..4f3a4134 100644 --- a/src/renderer/components/dashboard/CliStatusBanner.tsx +++ b/src/renderer/components/dashboard/CliStatusBanner.tsx @@ -10,7 +10,8 @@ import { lazy, Suspense, useCallback, useEffect, useMemo, useState } from 'react'; import { - CODEX_ACCOUNT_STARTUP_IDLE_DELAY_MS, + CODEX_ACCOUNT_STARTUP_IDLE_MAX_DELAY_MS, + CODEX_ACCOUNT_STARTUP_IDLE_MIN_DELAY_MS, mergeCodexProviderStatusWithSnapshot, useCodexAccountSnapshot, } from '@features/codex-account/renderer'; @@ -32,6 +33,7 @@ import { isConnectionManagedRuntimeProvider, isOpenCodeCatalogHydrating, shouldShowProviderConnectAction, + shouldShowProviderStatusSkeleton, } from '@renderer/components/runtime/providerConnectionUi'; import { ProviderModelBadges } from '@renderer/components/runtime/ProviderModelBadges'; import { getProviderRuntimeBackendSummary } from '@renderer/components/runtime/ProviderRuntimeBackendSelector'; @@ -445,17 +447,6 @@ const ProviderDetailSkeleton = (): React.JSX.Element => { ); }; -function isProviderCardLoading(provider: CliProviderStatus, providerLoading: boolean): boolean { - return ( - providerLoading || - (!provider.authenticated && - (provider.statusMessage === 'Checking...' || - provider.statusMessage === CLI_PROVIDER_STATUS_DEFERRED_MESSAGE) && - provider.models.length === 0 && - provider.backend == null) - ); -} - function isCodexSnapshotPending( provider: CliProviderStatus, codexSnapshotPending: boolean @@ -973,7 +964,7 @@ const InstalledBanner = ({ provider ); const showSkeleton = - isProviderCardLoading(provider, providerLoading) || + shouldShowProviderStatusSkeleton(provider, providerLoading) || isCodexSnapshotPending(provider, codexSnapshotPending) || maskNegativeBootstrapState; const anthropicRateLimitsLoading = @@ -1376,7 +1367,8 @@ export const CliStatusBanner = (): React.JSX.Element | null => { loadingCliStatus?.flavor === 'agent_teams_orchestrator' && Boolean(loadingCliStatus?.providers.some((provider) => provider.providerId === 'codex')), includeRateLimits: true, - initialRefreshDelayMs: CODEX_ACCOUNT_STARTUP_IDLE_DELAY_MS, + initialRefreshDelayMs: CODEX_ACCOUNT_STARTUP_IDLE_MIN_DELAY_MS, + initialRefreshMaxDelayMs: CODEX_ACCOUNT_STARTUP_IDLE_MAX_DELAY_MS, }); const visibleCliProviders = useMemo( () => diff --git a/src/renderer/components/extensions/ExtensionStoreView.tsx b/src/renderer/components/extensions/ExtensionStoreView.tsx index 0e368433..d635f6bf 100644 --- a/src/renderer/components/extensions/ExtensionStoreView.tsx +++ b/src/renderer/components/extensions/ExtensionStoreView.tsx @@ -258,13 +258,21 @@ export const ExtensionStoreView = (): React.JSX.Element => { }, [fetchPluginCatalog, projectPath]); useEffect(() => { + const cliStatusMatchesCurrentMode = + cliStatus && + (multimodelEnabled + ? cliStatus.flavor === 'agent_teams_orchestrator' + : cliStatus.flavor !== 'agent_teams_orchestrator'); + if (cliStatusLoading || cliStatusMatchesCurrentMode) { + return; + } void refreshCliStatusForCurrentMode({ multimodelEnabled, providerStatusMode: 'defer', bootstrapCliStatus, fetchCliStatus, }); - }, [bootstrapCliStatus, fetchCliStatus, multimodelEnabled]); + }, [bootstrapCliStatus, cliStatus, cliStatusLoading, fetchCliStatus, multimodelEnabled]); // Fetch MCP installed state on mount useEffect(() => { @@ -513,6 +521,7 @@ export const ExtensionStoreView = (): React.JSX.Element => { effectiveCliStatus, effectiveCliStatusLoading, openDashboard, + runtimeDisplayName, ]); // Browser mode guard @@ -587,7 +596,7 @@ export const ExtensionStoreView = (): React.JSX.Element => { {tabState.activeSubTab === 'mcp-servers' && ( - +