fix(team): show provider loading while models sync
This commit is contained in:
parent
544f4576d4
commit
cb17bcfdb5
5 changed files with 78 additions and 5 deletions
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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...' ||
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
Loading…
Reference in a new issue