agent-ecosystem/src/renderer/components/common/GlobalProviderStatusHeader.tsx

314 lines
10 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react';
import {
CODEX_ACCOUNT_STARTUP_IDLE_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);
}
export const GlobalProviderStatusHeader = (): React.JSX.Element | null => {
const isElectron = useMemo(() => isElectronMode(), []);
const {
cliStatus,
cliStatusLoading,
cliProviderStatusLoading,
multimodelEnabled,
isDashboardFocused,
} = useStore(
useShallow((state) => {
const focusedPane = state.paneLayout.panes.find(
(pane) => pane.id === state.paneLayout.focusedPaneId
);
const activeTab = focusedPane?.tabs.find((tab) => tab.id === focusedPane.activeTabId) ?? null;
return {
cliStatus: state.cliStatus,
cliStatusLoading: state.cliStatusLoading,
cliProviderStatusLoading: state.cliProviderStatusLoading,
multimodelEnabled: state.appConfig?.general?.multimodelEnabled ?? true,
isDashboardFocused:
!focusedPane || focusedPane.tabs.length === 0 || activeTab?.type === 'dashboard',
};
})
);
const [cycleProviderIds, setCycleProviderIds] = useState<CliProviderId[]>([]);
const loadingCliStatus = useMemo(
() =>
!cliStatus && cliStatusLoading && multimodelEnabled
? createLoadingMultimodelCliStatus()
: cliStatus,
[cliStatus, cliStatusLoading, multimodelEnabled]
);
const codexAccount = useCodexAccountSnapshot({
enabled:
isElectron &&
multimodelEnabled &&
loadingCliStatus?.flavor === 'agent_teams_orchestrator' &&
Boolean(loadingCliStatus?.providers.some((provider) => provider.providerId === 'codex')),
includeRateLimits: false,
initialRefreshDelayMs: CODEX_ACCOUNT_STARTUP_IDLE_DELAY_MS,
});
const effectiveCliStatus = useMemo(
() => mergeCodexCliStatusWithSnapshot(loadingCliStatus, codexAccount.snapshot),
[codexAccount.snapshot, loadingCliStatus]
);
const codexSnapshotPending =
codexAccount.loading &&
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<ProviderActivityState[]>(() => {
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
) {
return null;
}
return (
<div
className="flex shrink-0 flex-wrap items-center gap-2 border-b px-4 py-2"
style={{
backgroundColor: 'var(--color-surface-sidebar)',
borderColor: 'var(--color-border)',
}}
>
<span
className="shrink-0 text-[11px] font-medium uppercase tracking-[0.08em]"
style={{ color: 'var(--color-text-muted)' }}
>
Provider Activity
</span>
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">
{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 (
<div
key={providerId}
data-testid={`global-provider-status-${providerId}`}
className="flex min-w-0 items-center gap-1.5 rounded-md border px-2 py-1 text-[11px]"
style={{
borderColor: styles.borderColor,
backgroundColor: styles.backgroundColor,
color: styles.textColor,
}}
>
{tone === 'loading' ? (
<Loader2
className="size-3 shrink-0 animate-spin"
style={{ color: styles.statusColor }}
/>
) : tone === 'error' ? (
<AlertTriangle className="size-3 shrink-0" style={{ color: styles.statusColor }} />
) : (
<CheckCircle2 className="size-3 shrink-0" style={{ color: styles.statusColor }} />
)}
<ProviderBrandLogo providerId={providerId} className="size-3.5 shrink-0" />
<span className="shrink-0 font-medium" style={{ color: styles.textColor }}>
{providerState.provider.displayName}
</span>
<span
className="max-w-[280px] truncate"
style={{ color: styles.statusColor }}
title={statusText}
>
{statusText}
</span>
</div>
);
})}
</div>
</div>
);
};