fix(renderer): defer model validation while providers load

- Treat checking, deferred and loading provider model catalog states as pending instead of unavailable.

- Show selected provider activity inside create and launch dialogs while keeping ready providers visible during checks.

- Remove the global provider status header so provider activity is scoped to launch flows.
This commit is contained in:
777genius 2026-05-24 15:58:22 +03:00
parent 7aa87f2278
commit 57931c0abd
8 changed files with 228 additions and 297 deletions

View file

@ -1,85 +0,0 @@
import { useMemo } from 'react';
import {
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 { useStore } from '@renderer/store';
import { createLoadingMultimodelCliStatus } from '@renderer/store/slices/cliInstallerSlice';
import { useShallow } from 'zustand/react/shallow';
import { ProviderActivityStatusStrip } from './ProviderActivityStatusStrip';
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 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_MIN_DELAY_MS,
initialRefreshMaxDelayMs: CODEX_ACCOUNT_STARTUP_IDLE_MAX_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;
if (isDashboardFocused) {
return null;
}
return (
<ProviderActivityStatusStrip
cliStatus={effectiveCliStatus}
sourceCliStatus={loadingCliStatus}
cliStatusLoading={cliStatusLoading}
cliProviderStatusLoading={cliProviderStatusLoading}
multimodelEnabled={multimodelEnabled}
codexSnapshotPending={codexSnapshotPending}
className="shrink-0 border-b border-[var(--color-border)] bg-[var(--color-surface-sidebar)] px-4 py-2"
/>
);
};

View file

@ -4,7 +4,7 @@ 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 { isTeamProviderModelVerificationPending } from '@renderer/utils/teamModelAvailability';
import { AlertTriangle, CheckCircle2, Loader2 } from 'lucide-react';
import { ProviderBrandLogo } from './ProviderBrandLogo';
@ -27,17 +27,13 @@ interface ProviderActivityStatusStripProps {
readonly providerIds?: readonly CliProviderId[];
readonly className?: string;
readonly label?: string | null;
readonly layout?: 'inline' | 'stacked';
readonly showReadyProviders?: boolean;
readonly readyStatusText?: string;
}
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)
);
return providerLoading || isTeamProviderModelVerificationPending(provider.providerId, provider);
}
function shouldMaskCodexNegativeBootstrapState(
@ -97,6 +93,7 @@ function useProviderActivityDisplay({
multimodelEnabled,
codexSnapshotPending = false,
providerIds,
showReadyProviders,
}: Pick<
ProviderActivityStatusStripProps,
| 'cliStatus'
@ -106,6 +103,7 @@ function useProviderActivityDisplay({
| 'multimodelEnabled'
| 'codexSnapshotPending'
| 'providerIds'
| 'showReadyProviders'
>): {
displayProviderIds: CliProviderId[];
providerStateMap: Map<CliProviderId, ProviderActivityState>;
@ -201,6 +199,10 @@ function useProviderActivityDisplay({
}, [errorProviderIds, loadingProviderIds, visibleProviderIds]);
const displayProviderIds = useMemo(() => {
if (showReadyProviders) {
return visibleProviderIds;
}
if (loadingProviderIds.length > 0) {
const activeCycleIds = (
cycleProviderIds.length > 0 ? cycleProviderIds : loadingProviderIds
@ -213,7 +215,14 @@ function useProviderActivityDisplay({
}
return [];
}, [cycleProviderIds, errorProviderIds, loadingProviderIds, providerStateMap]);
}, [
cycleProviderIds,
errorProviderIds,
loadingProviderIds,
providerStateMap,
showReadyProviders,
visibleProviderIds,
]);
return {
displayProviderIds,
@ -237,6 +246,9 @@ export const ProviderActivityStatusStrip = ({
providerIds,
className = '',
label = 'Provider Activity',
layout = 'inline',
showReadyProviders = false,
readyStatusText = 'Checked',
}: ProviderActivityStatusStripProps): React.JSX.Element | null => {
const { displayProviderIds, providerStateMap, shouldRender } = useProviderActivityDisplay({
cliStatus,
@ -246,14 +258,24 @@ export const ProviderActivityStatusStrip = ({
multimodelEnabled,
codexSnapshotPending,
providerIds,
showReadyProviders,
});
if (!shouldRender) {
return null;
}
const rootClassName =
layout === 'stacked'
? `flex min-w-0 flex-col items-start gap-1.5 ${className}`.trim()
: `flex min-w-0 flex-wrap items-center gap-2 ${className}`.trim();
const itemsClassName =
layout === 'stacked'
? 'flex min-w-0 w-full flex-wrap items-center gap-1.5'
: 'flex min-w-0 flex-1 flex-wrap items-center gap-2';
return (
<div className={`flex min-w-0 flex-wrap items-center gap-2 ${className}`.trim()}>
<div className={rootClassName}>
{label ? (
<span
className="shrink-0 text-[11px] font-medium uppercase tracking-[0.08em]"
@ -262,7 +284,7 @@ export const ProviderActivityStatusStrip = ({
{label}
</span>
) : null}
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">
<div className={itemsClassName}>
{displayProviderIds.map((providerId) => {
const providerState = providerStateMap.get(providerId);
if (!providerState) {
@ -280,13 +302,13 @@ export const ProviderActivityStatusStrip = ({
? 'Checking...'
: tone === 'error'
? formatProviderStatusText(providerState.provider)
: 'Checked';
: readyStatusText;
return (
<div
key={providerId}
data-testid={`provider-activity-status-${providerId}`}
className="flex min-w-0 items-center gap-1.5 rounded-md border px-2 py-1 text-[11px]"
className="flex min-w-0 max-w-full items-center gap-1.5 rounded-md border px-2 py-1 text-[11px]"
style={{
borderColor: styles.borderColor,
backgroundColor: styles.backgroundColor,

View file

@ -29,7 +29,6 @@ import { useStore } from '@renderer/store';
import { useShallow } from 'zustand/react/shallow';
import { CliInstallWarningBanner } from '../common/CliInstallWarningBanner';
import { GlobalProviderStatusHeader } from '../common/GlobalProviderStatusHeader';
import { UpdateBanner } from '../common/UpdateBanner';
import { UpdateDialog } from '../common/UpdateDialog';
import { WorkspaceIndicator } from '../common/WorkspaceIndicator';
@ -164,7 +163,6 @@ export const TabbedLayout = (): React.JSX.Element => {
>
<TabBarRow />
<CliInstallWarningBanner />
<GlobalProviderStatusHeader />
<div className="flex flex-1 overflow-hidden">
{/* Command Palette (Cmd+K) */}
<CommandPalette />

View file

@ -77,6 +77,7 @@ import { refreshCliStatusForCurrentMode } from '@renderer/utils/refreshCliStatus
import { getAvailableTeamEffortValue } from '@renderer/utils/teamEffortOptions';
import {
getTeamModelSelectionError,
isTeamProviderRuntimeStatusLoading,
normalizeExplicitTeamModelForUi,
} from '@renderer/utils/teamModelAvailability';
import { getTeamProviderLabel as getCatalogTeamProviderLabel } from '@renderer/utils/teamModelCatalog';
@ -699,6 +700,29 @@ export const CreateTeamDialog = ({
),
[effectiveCliStatus?.providers]
);
const runtimeProviderLoadingById = useMemo(
() =>
new Map(
selectedMemberProviders.map(
(providerId) =>
[
providerId,
isTeamProviderRuntimeStatusLoading(
providerId,
runtimeProviderStatusById.get(providerId),
cliProviderStatusLoading[providerId] === true ||
(providerId === 'codex' && codexSnapshotPending)
),
] as const
)
),
[
cliProviderStatusLoading,
codexSnapshotPending,
runtimeProviderStatusById,
selectedMemberProviders,
]
);
const selectedProviderBackendId = useMemo(
() =>
resolveUiOwnedProviderBackendId(
@ -1033,9 +1057,15 @@ export const CreateTeamDialog = ({
}
}
const loadingProviderIds = selectedMemberProviders.filter((providerId) =>
runtimeProviderLoadingById.get(providerId)
);
const readyProviderIds = selectedMemberProviders.filter(
(providerId) => !runtimeProviderLoadingById.get(providerId)
);
const providerPlans = buildProviderPreparePlans({
cwd: effectiveCwd,
providerIds: selectedMemberProviders,
providerIds: readyProviderIds,
selectedModelChecksByProvider,
backendSummaryByProvider: runtimeBackendSummaryByProviderRef.current,
limitContext: effectiveAnthropicRuntimeLimitContext,
@ -1048,7 +1078,7 @@ export const CreateTeamDialog = ({
return lastSignature !== plan.requestSignature && pendingSignature !== plan.requestSignature;
});
const loadingMessage = getProvisioningProviderProgressMessage(
changedPlans.map((plan) => plan.providerId),
[...loadingProviderIds, ...changedPlans.map((plan) => plan.providerId)],
selectedMemberProviders.length
);
const getSelectedWarnings = (): string[] =>
@ -1089,6 +1119,20 @@ export const CreateTeamDialog = ({
};
let checks = alignProvisioningChecks(prepareChecksRef.current, selectedMemberProviders);
for (const providerId of loadingProviderIds) {
lastPrepareProviderSignatureByIdRef.current.delete(providerId);
pendingPrepareProviderSignatureByIdRef.current.delete(providerId);
prepareProviderRequestSeqByIdRef.current.delete(providerId);
prepareWarningsByProviderIdRef.current.delete(providerId);
checks = updateProviderCheck(checks, providerId, {
status: 'checking',
backendSummary: runtimeBackendSummaryByProviderRef.current.get(providerId) ?? null,
details: [
`${getProviderLabel(providerId)} provider status is still loading. Model checks will start automatically.`,
],
supportDiagnostics: undefined,
});
}
for (const plan of changedPlans) {
checks = updateProviderCheck(checks, plan.providerId, {
status: plan.selectedModelIds.length > 0 ? plan.cachedSnapshot.status : 'checking',
@ -1229,6 +1273,7 @@ export const CreateTeamDialog = ({
effectiveAnthropicRuntimeLimitContext,
prepareProviderInvalidationEpochById,
runtimeProviderStatusById,
runtimeProviderLoadingById,
selectedModel,
selectedModelChecksByProvider,
selectedModelChecksByProviderSignature,
@ -1724,13 +1769,15 @@ export const CreateTeamDialog = ({
}
}
const leadError = getTeamModelSelectionError(
selectedProviderId,
selectedModel,
runtimeProviderStatusById.get(selectedProviderId)
);
if (leadError) {
return leadError;
if (!runtimeProviderLoadingById.get(selectedProviderId)) {
const leadError = getTeamModelSelectionError(
selectedProviderId,
selectedModel,
runtimeProviderStatusById.get(selectedProviderId)
);
if (leadError) {
return leadError;
}
}
for (const member of effectiveMemberDrafts) {
@ -1739,6 +1786,9 @@ export const CreateTeamDialog = ({
}
const providerId = normalizeOptionalTeamProviderId(member.providerId) ?? selectedProviderId;
if (runtimeProviderLoadingById.get(providerId)) {
continue;
}
const memberError = getTeamModelSelectionError(
providerId,
member.model,
@ -1756,6 +1806,7 @@ export const CreateTeamDialog = ({
}, [
effectiveMemberDrafts,
runtimeProviderStatusById,
runtimeProviderLoadingById,
selectedModel,
selectedProviderId,
soloTeam,
@ -2488,6 +2539,12 @@ export const CreateTeamDialog = ({
codexSnapshotPending={codexSnapshotPending}
providerIds={selectedMemberProviders}
className="mb-2"
label="Selected providers"
layout="stacked"
showReadyProviders={
effectivePrepare.state === 'idle' || effectivePrepare.state === 'loading'
}
readyStatusText="Ready"
/>
) : null}
{canCreate &&

View file

@ -68,6 +68,7 @@ import { refreshCliStatusForCurrentMode } from '@renderer/utils/refreshCliStatus
import { getAvailableTeamEffortValue } from '@renderer/utils/teamEffortOptions';
import {
getTeamModelSelectionError,
isTeamProviderRuntimeStatusLoading,
normalizeExplicitTeamModelForUi,
} from '@renderer/utils/teamModelAvailability';
import { getTeamProviderLabel as getCatalogTeamProviderLabel } from '@renderer/utils/teamModelCatalog';
@ -609,6 +610,29 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
),
[effectiveCliStatus?.providers]
);
const runtimeProviderLoadingById = useMemo(
() =>
new Map(
selectedMemberProviders.map(
(providerId) =>
[
providerId,
isTeamProviderRuntimeStatusLoading(
providerId,
runtimeProviderStatusById.get(providerId),
cliProviderStatusLoading[providerId] === true ||
(providerId === 'codex' && codexSnapshotPending)
),
] as const
)
),
[
cliProviderStatusLoading,
codexSnapshotPending,
runtimeProviderStatusById,
selectedMemberProviders,
]
);
useEffect(() => {
if (!open) {
@ -1620,9 +1644,15 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
}
}
const loadingProviderIds = selectedMemberProviders.filter((providerId) =>
runtimeProviderLoadingById.get(providerId)
);
const readyProviderIds = selectedMemberProviders.filter(
(providerId) => !runtimeProviderLoadingById.get(providerId)
);
const providerPlans = buildProviderPreparePlans({
cwd: effectiveCwd,
providerIds: selectedMemberProviders,
providerIds: readyProviderIds,
selectedModelChecksByProvider,
backendSummaryByProvider: runtimeBackendSummaryByProviderRef.current,
limitContext: effectiveAnthropicRuntimeLimitContext,
@ -1634,7 +1664,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
lastPrepareProviderSignatureByIdRef.current.get(plan.providerId) !== plan.requestSignature
);
const loadingMessage = getProvisioningProviderProgressMessage(
changedPlans.map((plan) => plan.providerId),
[...loadingProviderIds, ...changedPlans.map((plan) => plan.providerId)],
selectedMemberProviders.length
);
const getSelectedWarnings = (): string[] =>
@ -1675,6 +1705,19 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
};
let checks = alignProvisioningChecks(prepareChecksRef.current, selectedMemberProviders);
for (const providerId of loadingProviderIds) {
lastPrepareProviderSignatureByIdRef.current.delete(providerId);
prepareProviderRequestSeqByIdRef.current.delete(providerId);
prepareWarningsByProviderIdRef.current.delete(providerId);
checks = updateProviderCheck(checks, providerId, {
status: 'checking',
backendSummary: runtimeBackendSummaryByProviderRef.current.get(providerId) ?? null,
details: [
`${getProviderLabel(providerId)} provider status is still loading. Model checks will start automatically.`,
],
supportDiagnostics: undefined,
});
}
for (const plan of changedPlans) {
checks = updateProviderCheck(checks, plan.providerId, {
status: plan.selectedModelIds.length > 0 ? plan.cachedSnapshot.status : 'checking',
@ -1789,6 +1832,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
effectiveCwd,
effectiveAnthropicRuntimeLimitContext,
prepareProviderInvalidationEpochById,
runtimeProviderLoadingById,
runtimeProviderStatusById,
selectedMemberProviders,
selectedModelChecksByProvider,
@ -2056,13 +2100,15 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
}
}
const leadError = getTeamModelSelectionError(
selectedProviderId,
selectedModel,
runtimeProviderStatusById.get(selectedProviderId)
);
if (leadError) {
return leadError;
if (!runtimeProviderLoadingById.get(selectedProviderId)) {
const leadError = getTeamModelSelectionError(
selectedProviderId,
selectedModel,
runtimeProviderStatusById.get(selectedProviderId)
);
if (leadError) {
return leadError;
}
}
if (!isLaunchMode) {
@ -2075,6 +2121,9 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
}
const providerId = normalizeOptionalTeamProviderId(member.providerId) ?? selectedProviderId;
if (runtimeProviderLoadingById.get(providerId)) {
continue;
}
const memberError = getTeamModelSelectionError(
providerId,
member.model,
@ -2092,6 +2141,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
}, [
effectiveMemberDrafts,
isLaunchMode,
runtimeProviderLoadingById,
runtimeProviderStatusById,
selectedModel,
selectedProviderId,
@ -3054,6 +3104,12 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
multimodelEnabled={multimodelEnabled}
codexSnapshotPending={codexSnapshotPending}
providerIds={selectedMemberProviders}
label="Selected providers"
layout="stacked"
showReadyProviders={
effectivePrepare.state === 'idle' || effectivePrepare.state === 'loading'
}
readyStatusText="Ready"
className="mb-2"
/>
{effectivePrepare.state === 'idle' || effectivePrepare.state === 'loading' ? (

View file

@ -5,6 +5,7 @@ import {
getAvailableTeamProviderModels,
getTeamModelSelectionError,
isTeamModelAvailableForUi,
isTeamProviderModelVerificationPending,
normalizeTeamModelForUi,
} from '../teamModelAvailability';
@ -204,6 +205,32 @@ describe('team model availability Codex catalog integration', () => {
expect(getTeamModelSelectionError('codex', 'gpt-5.5', providerStatus)).toBeNull();
});
it('does not reject a selected Codex model while provider status is still loading', () => {
const providerStatus = {
...createCodexProviderStatus([
{
id: 'gpt-5.1-codex',
launchModel: 'gpt-5.1-codex',
displayName: 'GPT-5.1 Codex',
hidden: false,
supportedReasoningEfforts: ['low', 'medium', 'high'],
defaultReasoningEffort: 'medium',
inputModalities: ['text', 'image'],
supportsPersonality: false,
isDefault: true,
upgrade: false,
source: 'static-fallback',
},
]),
verificationState: 'unknown' as const,
statusMessage: 'Checking...',
};
expect(isTeamProviderModelVerificationPending('codex', providerStatus)).toBe(true);
expect(getTeamModelSelectionError('codex', 'gpt-5.5', providerStatus)).toBeNull();
expect(normalizeTeamModelForUi('codex', 'gpt-5.5', providerStatus)).toBe('gpt-5.5');
});
it('orders GPT-5.5 first after the virtual default option', () => {
const providerStatus = createCodexProviderStatus([
{

View file

@ -1,3 +1,5 @@
import { CLI_PROVIDER_STATUS_DEFERRED_MESSAGE } from '@shared/types/cliInstaller';
import {
getProviderScopedTeamModelLabel,
getRuntimeAwareProviderScopedTeamModelLabel,
@ -43,6 +45,7 @@ export type TeamModelRuntimeProviderStatus = Pick<
| 'providerId'
| 'models'
| 'modelCatalog'
| 'modelCatalogRefreshState'
| 'modelAvailability'
| 'modelVerificationState'
| 'runtimeCapabilities'
@ -163,6 +166,21 @@ export function isTeamProviderModelVerificationPending(
return true;
}
const statusMessage = providerStatus.statusMessage?.trim().toLowerCase() ?? '';
const statusMessagePending =
statusMessage === 'checking...' ||
statusMessage === CLI_PROVIDER_STATUS_DEFERRED_MESSAGE.toLowerCase();
if (providerStatus.verificationState !== 'error' && statusMessagePending) {
return true;
}
if (
providerStatus.verificationState !== 'error' &&
providerStatus.modelCatalogRefreshState === 'loading'
) {
return true;
}
const hasRuntimeModelTruth =
providerStatus.models.length > 0 ||
(providerStatus.modelCatalog?.models.length ?? 0) > 0 ||
@ -193,10 +211,25 @@ export function isTeamProviderModelVerificationPending(
return false;
}
const statusMessage = providerStatus.statusMessage?.trim().toLowerCase() ?? '';
return statusMessage.length === 0 || statusMessage === 'checking...';
}
export function isTeamProviderRuntimeStatusLoading(
providerId: SupportedProviderId | undefined,
providerStatus?: TeamModelRuntimeProviderStatus | null,
providerLoading = false
): boolean {
if (!providerId) {
return false;
}
if (providerLoading) {
return true;
}
return isTeamProviderModelVerificationPending(providerId, providerStatus);
}
function getFallbackTeamProviderModels(providerId: SupportedProviderId): string[] {
return getVisibleTeamProviderModels(
providerId,

View file

@ -1,177 +0,0 @@
import React, { act } from 'react';
import { createRoot } from 'react-dom/client';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
interface StoreState {
cliStatus: Record<string, unknown> | null;
cliStatusLoading: boolean;
cliProviderStatusLoading: Record<string, boolean>;
appConfig: {
general: {
multimodelEnabled: boolean;
};
};
paneLayout: {
focusedPaneId: string;
panes: Array<{
id: string;
activeTabId: string | null;
tabs: Array<{
id: string;
type: string;
}>;
}>;
};
}
const storeState = {} as StoreState;
const codexAccountHookState = {
snapshot: null,
loading: false,
error: null,
refresh: vi.fn(() => Promise.resolve(undefined)),
startChatgptLogin: vi.fn(() => Promise.resolve(true)),
cancelChatgptLogin: vi.fn(() => Promise.resolve(true)),
logout: vi.fn(() => Promise.resolve(true)),
};
vi.mock('@renderer/api', () => ({
isElectronMode: () => true,
}));
vi.mock('@renderer/components/common/ProviderBrandLogo', () => ({
ProviderBrandLogo: ({ providerId }: { providerId: string }) =>
React.createElement('span', { 'data-testid': `provider-logo-${providerId}` }, providerId),
}));
vi.mock('@features/codex-account/renderer', async (importOriginal) => {
const actual = await importOriginal<typeof import('@features/codex-account/renderer')>();
return {
...actual,
useCodexAccountSnapshot: () => codexAccountHookState,
};
});
vi.mock('@renderer/store', () => ({
useStore: (selector: (state: StoreState) => unknown) => selector(storeState),
}));
import { GlobalProviderStatusHeader } from '@renderer/components/common/GlobalProviderStatusHeader';
function createProvider(overrides: Record<string, unknown>): Record<string, unknown> {
return {
providerId: 'anthropic',
displayName: 'Anthropic',
supported: true,
authenticated: false,
authMethod: null,
verificationState: 'unknown',
statusMessage: 'Checking...',
detailMessage: null,
models: [],
modelVerificationState: 'idle',
modelAvailability: [],
canLoginFromUi: true,
capabilities: {
teamLaunch: true,
oneShot: true,
extensions: {},
},
backend: null,
availableBackends: [],
connection: null,
...overrides,
};
}
function createMultimodelStatus(providers: Record<string, unknown>[]): Record<string, unknown> {
return {
flavor: 'agent_teams_orchestrator',
displayName: 'Multimodel runtime',
supportsSelfUpdate: false,
showVersionDetails: false,
showBinaryPath: false,
installed: true,
installedVersion: '0.0.3',
binaryPath: '/tmp/claude-multimodel',
latestVersion: null,
updateAvailable: false,
authLoggedIn: providers.some((provider) => provider.authenticated === true),
authStatusChecking: false,
authMethod: null,
providers,
};
}
function setFocusedTab(type: string): void {
storeState.paneLayout = {
focusedPaneId: 'pane-1',
panes: [
{
id: 'pane-1',
activeTabId: type === 'empty' ? null : 'tab-1',
tabs: type === 'empty' ? [] : [{ id: 'tab-1', type }],
},
],
};
}
describe('GlobalProviderStatusHeader', () => {
beforeEach(() => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
storeState.cliStatus = createMultimodelStatus([createProvider({})]);
storeState.cliStatusLoading = false;
storeState.cliProviderStatusLoading = { anthropic: true };
storeState.appConfig = {
general: {
multimodelEnabled: true,
},
};
setFocusedTab('team');
});
afterEach(() => {
document.body.innerHTML = '';
vi.unstubAllGlobals();
});
it('shows provider activity on non-dashboard screens', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
await act(async () => {
root.render(React.createElement(GlobalProviderStatusHeader));
await Promise.resolve();
});
expect(host.textContent).toContain('Provider Activity');
expect(host.textContent).toContain('Anthropic');
expect(host.textContent).toContain('Checking...');
await act(async () => {
root.unmount();
await Promise.resolve();
});
});
it('hides on dashboard screens', async () => {
setFocusedTab('dashboard');
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
await act(async () => {
root.render(React.createElement(GlobalProviderStatusHeader));
await Promise.resolve();
});
expect(host.textContent).toBe('');
await act(async () => {
root.unmount();
await Promise.resolve();
});
});
});