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' && ( - +