fix(team): show provider loading while models sync

This commit is contained in:
777genius 2026-06-01 22:53:49 +03:00
parent 544f4576d4
commit cb17bcfdb5
5 changed files with 78 additions and 5 deletions

View file

@ -1,6 +1,7 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useAppTranslation } from '@features/localization/renderer';
import { ProviderActivityStatusStrip } from '@renderer/components/common/ProviderActivityStatusStrip';
import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo';
import { isOpenCodeCatalogHydrating } from '@renderer/components/runtime/providerConnectionUi';
import { Checkbox } from '@renderer/components/ui/checkbox';
@ -814,8 +815,14 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
const previousSelectedProviderIdRef = useRef<TeamProviderId>(selectedProviderId);
const effectiveProviderId = inspectedProviderId ?? selectedProviderId;
const isInspectingInactiveProvider = inspectedProviderId !== null;
const { cliStatus: effectiveCliStatus, providerStatus: runtimeProviderStatus } =
useEffectiveCliProviderStatus(effectiveProviderId);
const {
cliStatus: effectiveCliStatus,
sourceCliStatus,
providerStatus: runtimeProviderStatus,
codexSnapshotPending,
} = useEffectiveCliProviderStatus(effectiveProviderId);
const cliStatusLoading = useStore((s) => s.cliStatusLoading);
const cliProviderStatusLoading = useStore((s) => s.cliProviderStatusLoading ?? {});
const multimodelAvailable =
multimodelEnabled || effectiveCliStatus?.flavor === 'agent_teams_orchestrator';
const runtimeProviderStatusById = useMemo(
@ -1777,9 +1784,21 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
</div>
) : null}
{shouldAwaitRuntimeModelList ? (
<p className="mb-2 text-[11px] text-[var(--color-text-muted)]">
{t('modelSelector.runtimeModelsSyncing')}
</p>
<div className="mb-2 space-y-1.5">
<p className="text-[11px] text-[var(--color-text-muted)]">
{t('modelSelector.runtimeModelsSyncing')}
</p>
<ProviderActivityStatusStrip
cliStatus={effectiveCliStatus}
sourceCliStatus={sourceCliStatus}
cliStatusLoading={cliStatusLoading}
cliProviderStatusLoading={cliProviderStatusLoading}
multimodelEnabled={multimodelEnabled}
codexSnapshotPending={codexSnapshotPending}
providerIds={[effectiveProviderId]}
label={null}
/>
</div>
) : null}
{showAnthropicCompatibleCustomModelInput ? (
<div className="mb-2 rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface-raised)] p-2">

View file

@ -11,8 +11,10 @@ import type { CliInstallationStatus, CliProviderId, CliProviderStatus } from '@s
export interface EffectiveCliProviderStatusSnapshot {
cliStatus: CliInstallationStatus | null;
sourceCliStatus: CliInstallationStatus | null;
providerStatus: CliProviderStatus | null;
loading: boolean;
codexSnapshotPending: boolean;
}
export function useEffectiveCliProviderStatus(
@ -42,6 +44,10 @@ export function useEffectiveCliProviderStatus(
() => mergeCodexCliStatusWithSnapshot(loadingCliStatus, codexAccount.snapshot),
[codexAccount.snapshot, loadingCliStatus]
);
const codexSnapshotPending =
codexAccount.loading &&
Boolean(loadingCliStatus?.providers.some((provider) => provider.providerId === 'codex')) &&
!codexAccount.snapshot;
const providerStatus = useMemo(
() =>
providerId
@ -53,7 +59,9 @@ export function useEffectiveCliProviderStatus(
return {
cliStatus: effectiveCliStatus,
sourceCliStatus: loadingCliStatus,
providerStatus,
loading: cliStatusLoading && effectiveCliStatus === null,
codexSnapshotPending,
};
}

View file

@ -166,6 +166,10 @@ export function isTeamProviderModelVerificationPending(
return true;
}
if (providerStatus.verificationState === 'error') {
return false;
}
const statusMessage = providerStatus.statusMessage?.trim().toLowerCase() ?? '';
const statusMessagePending =
statusMessage === 'checking...' ||

View file

@ -292,6 +292,43 @@ describe('ProviderActivityStatusStrip', () => {
});
});
it('does not mask finished Codex native provider errors as model loading', async () => {
const host = document.createElement('div');
document.body.appendChild(host);
let root!: ReturnType<typeof createRoot>;
await act(async () => {
root = renderStrip(host, {
cliStatus: createMultimodelStatus([
createProvider({
providerId: 'codex',
displayName: 'Codex',
supported: true,
verificationState: 'error',
statusMessage: 'Failed to refresh Codex status',
backend: {
kind: 'codex-native',
label: 'Codex native',
endpointLabel: 'codex exec --json',
},
models: [],
modelAvailability: [],
}),
]),
});
await Promise.resolve();
});
expect(host.textContent).toContain('Codex');
expect(host.textContent).toContain('Failed to refresh Codex status');
expect(host.textContent).not.toContain('Checking...');
await act(async () => {
root.unmount();
await Promise.resolve();
});
});
it('masks a negative Codex bootstrap state while source placeholder loading is still active', async () => {
const sourceCliStatus = createMultimodelStatus([
createProvider({

View file

@ -60,6 +60,7 @@ vi.mock('@renderer/components/ui/tabs', () => {
const storeState = {
cliStatus: null as unknown,
cliStatusLoading: false,
cliProviderStatusLoading: {} as Record<string, boolean>,
appConfig: { general: { multimodelEnabled: true } },
fetchCliProviderStatus: vi.fn().mockResolvedValue(undefined),
};
@ -109,8 +110,10 @@ import { TeamModelSelector } from '@renderer/components/team/dialogs/TeamModelSe
describe('TeamModelSelector disabled Codex models', () => {
afterEach(() => {
document.body.innerHTML = '';
Reflect.deleteProperty(window, 'electronAPI');
storeState.cliStatus = null;
storeState.cliStatusLoading = false;
storeState.cliProviderStatusLoading = {};
storeState.fetchCliProviderStatus.mockClear();
codexAccountHookState.snapshot = null;
codexAccountHookState.loading = false;
@ -124,6 +127,7 @@ describe('TeamModelSelector disabled Codex models', () => {
it('shows only Default while Codex runtime models are still loading', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
Object.defineProperty(window, 'electronAPI', { value: {}, configurable: true });
storeState.cliStatusLoading = true;
const host = document.createElement('div');
document.body.appendChild(host);
@ -142,6 +146,7 @@ describe('TeamModelSelector disabled Codex models', () => {
});
expect(host.textContent).toContain('Default');
expect(host.querySelector('[data-testid="provider-activity-status-codex"]')).not.toBeNull();
expect(host.textContent).not.toContain('5.1 Codex Mini');
expect(host.textContent).not.toContain('5.3 Codex Spark');
const defaultButton = Array.from(host.querySelectorAll('button')).find((button) =>