{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 (
{
>
-
{/* Command Palette (Cmd+K) */}
diff --git a/src/renderer/components/team/dialogs/CreateTeamDialog.tsx b/src/renderer/components/team/dialogs/CreateTeamDialog.tsx
index 05dcfe38..d57f838a 100644
--- a/src/renderer/components/team/dialogs/CreateTeamDialog.tsx
+++ b/src/renderer/components/team/dialogs/CreateTeamDialog.tsx
@@ -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 &&
diff --git a/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx b/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx
index 7d4fff47..a25dfb7c 100644
--- a/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx
+++ b/src/renderer/components/team/dialogs/LaunchTeamDialog.tsx
@@ -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' ? (
diff --git a/src/renderer/utils/__tests__/teamModelAvailability.codexCatalog.test.ts b/src/renderer/utils/__tests__/teamModelAvailability.codexCatalog.test.ts
index 2d4073f1..a46a06b5 100644
--- a/src/renderer/utils/__tests__/teamModelAvailability.codexCatalog.test.ts
+++ b/src/renderer/utils/__tests__/teamModelAvailability.codexCatalog.test.ts
@@ -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([
{
diff --git a/src/renderer/utils/teamModelAvailability.ts b/src/renderer/utils/teamModelAvailability.ts
index 68d5af04..9b11f080 100644
--- a/src/renderer/utils/teamModelAvailability.ts
+++ b/src/renderer/utils/teamModelAvailability.ts
@@ -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,
diff --git a/test/renderer/components/common/GlobalProviderStatusHeader.test.ts b/test/renderer/components/common/GlobalProviderStatusHeader.test.ts
deleted file mode 100644
index c28c352a..00000000
--- a/test/renderer/components/common/GlobalProviderStatusHeader.test.ts
+++ /dev/null
@@ -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 | null;
- cliStatusLoading: boolean;
- cliProviderStatusLoading: Record;
- 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();
- 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): Record {
- 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[]): Record {
- 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();
- });
- });
-});