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 React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { useAppTranslation } from '@features/localization/renderer';
|
import { useAppTranslation } from '@features/localization/renderer';
|
||||||
|
import { ProviderActivityStatusStrip } from '@renderer/components/common/ProviderActivityStatusStrip';
|
||||||
import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo';
|
import { ProviderBrandLogo } from '@renderer/components/common/ProviderBrandLogo';
|
||||||
import { isOpenCodeCatalogHydrating } from '@renderer/components/runtime/providerConnectionUi';
|
import { isOpenCodeCatalogHydrating } from '@renderer/components/runtime/providerConnectionUi';
|
||||||
import { Checkbox } from '@renderer/components/ui/checkbox';
|
import { Checkbox } from '@renderer/components/ui/checkbox';
|
||||||
|
|
@ -814,8 +815,14 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
const previousSelectedProviderIdRef = useRef<TeamProviderId>(selectedProviderId);
|
const previousSelectedProviderIdRef = useRef<TeamProviderId>(selectedProviderId);
|
||||||
const effectiveProviderId = inspectedProviderId ?? selectedProviderId;
|
const effectiveProviderId = inspectedProviderId ?? selectedProviderId;
|
||||||
const isInspectingInactiveProvider = inspectedProviderId !== null;
|
const isInspectingInactiveProvider = inspectedProviderId !== null;
|
||||||
const { cliStatus: effectiveCliStatus, providerStatus: runtimeProviderStatus } =
|
const {
|
||||||
useEffectiveCliProviderStatus(effectiveProviderId);
|
cliStatus: effectiveCliStatus,
|
||||||
|
sourceCliStatus,
|
||||||
|
providerStatus: runtimeProviderStatus,
|
||||||
|
codexSnapshotPending,
|
||||||
|
} = useEffectiveCliProviderStatus(effectiveProviderId);
|
||||||
|
const cliStatusLoading = useStore((s) => s.cliStatusLoading);
|
||||||
|
const cliProviderStatusLoading = useStore((s) => s.cliProviderStatusLoading ?? {});
|
||||||
const multimodelAvailable =
|
const multimodelAvailable =
|
||||||
multimodelEnabled || effectiveCliStatus?.flavor === 'agent_teams_orchestrator';
|
multimodelEnabled || effectiveCliStatus?.flavor === 'agent_teams_orchestrator';
|
||||||
const runtimeProviderStatusById = useMemo(
|
const runtimeProviderStatusById = useMemo(
|
||||||
|
|
@ -1777,9 +1784,21 @@ export const TeamModelSelector: React.FC<TeamModelSelectorProps> = ({
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{shouldAwaitRuntimeModelList ? (
|
{shouldAwaitRuntimeModelList ? (
|
||||||
<p className="mb-2 text-[11px] text-[var(--color-text-muted)]">
|
<div className="mb-2 space-y-1.5">
|
||||||
{t('modelSelector.runtimeModelsSyncing')}
|
<p className="text-[11px] text-[var(--color-text-muted)]">
|
||||||
</p>
|
{t('modelSelector.runtimeModelsSyncing')}
|
||||||
|
</p>
|
||||||
|
<ProviderActivityStatusStrip
|
||||||
|
cliStatus={effectiveCliStatus}
|
||||||
|
sourceCliStatus={sourceCliStatus}
|
||||||
|
cliStatusLoading={cliStatusLoading}
|
||||||
|
cliProviderStatusLoading={cliProviderStatusLoading}
|
||||||
|
multimodelEnabled={multimodelEnabled}
|
||||||
|
codexSnapshotPending={codexSnapshotPending}
|
||||||
|
providerIds={[effectiveProviderId]}
|
||||||
|
label={null}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{showAnthropicCompatibleCustomModelInput ? (
|
{showAnthropicCompatibleCustomModelInput ? (
|
||||||
<div className="mb-2 rounded-md border border-[var(--color-border-subtle)] bg-[var(--color-surface-raised)] p-2">
|
<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 {
|
export interface EffectiveCliProviderStatusSnapshot {
|
||||||
cliStatus: CliInstallationStatus | null;
|
cliStatus: CliInstallationStatus | null;
|
||||||
|
sourceCliStatus: CliInstallationStatus | null;
|
||||||
providerStatus: CliProviderStatus | null;
|
providerStatus: CliProviderStatus | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
codexSnapshotPending: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useEffectiveCliProviderStatus(
|
export function useEffectiveCliProviderStatus(
|
||||||
|
|
@ -42,6 +44,10 @@ export function useEffectiveCliProviderStatus(
|
||||||
() => mergeCodexCliStatusWithSnapshot(loadingCliStatus, codexAccount.snapshot),
|
() => mergeCodexCliStatusWithSnapshot(loadingCliStatus, codexAccount.snapshot),
|
||||||
[codexAccount.snapshot, loadingCliStatus]
|
[codexAccount.snapshot, loadingCliStatus]
|
||||||
);
|
);
|
||||||
|
const codexSnapshotPending =
|
||||||
|
codexAccount.loading &&
|
||||||
|
Boolean(loadingCliStatus?.providers.some((provider) => provider.providerId === 'codex')) &&
|
||||||
|
!codexAccount.snapshot;
|
||||||
const providerStatus = useMemo(
|
const providerStatus = useMemo(
|
||||||
() =>
|
() =>
|
||||||
providerId
|
providerId
|
||||||
|
|
@ -53,7 +59,9 @@ export function useEffectiveCliProviderStatus(
|
||||||
|
|
||||||
return {
|
return {
|
||||||
cliStatus: effectiveCliStatus,
|
cliStatus: effectiveCliStatus,
|
||||||
|
sourceCliStatus: loadingCliStatus,
|
||||||
providerStatus,
|
providerStatus,
|
||||||
loading: cliStatusLoading && effectiveCliStatus === null,
|
loading: cliStatusLoading && effectiveCliStatus === null,
|
||||||
|
codexSnapshotPending,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -166,6 +166,10 @@ export function isTeamProviderModelVerificationPending(
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (providerStatus.verificationState === 'error') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const statusMessage = providerStatus.statusMessage?.trim().toLowerCase() ?? '';
|
const statusMessage = providerStatus.statusMessage?.trim().toLowerCase() ?? '';
|
||||||
const statusMessagePending =
|
const statusMessagePending =
|
||||||
statusMessage === 'checking...' ||
|
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 () => {
|
it('masks a negative Codex bootstrap state while source placeholder loading is still active', async () => {
|
||||||
const sourceCliStatus = createMultimodelStatus([
|
const sourceCliStatus = createMultimodelStatus([
|
||||||
createProvider({
|
createProvider({
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,7 @@ vi.mock('@renderer/components/ui/tabs', () => {
|
||||||
const storeState = {
|
const storeState = {
|
||||||
cliStatus: null as unknown,
|
cliStatus: null as unknown,
|
||||||
cliStatusLoading: false,
|
cliStatusLoading: false,
|
||||||
|
cliProviderStatusLoading: {} as Record<string, boolean>,
|
||||||
appConfig: { general: { multimodelEnabled: true } },
|
appConfig: { general: { multimodelEnabled: true } },
|
||||||
fetchCliProviderStatus: vi.fn().mockResolvedValue(undefined),
|
fetchCliProviderStatus: vi.fn().mockResolvedValue(undefined),
|
||||||
};
|
};
|
||||||
|
|
@ -109,8 +110,10 @@ import { TeamModelSelector } from '@renderer/components/team/dialogs/TeamModelSe
|
||||||
describe('TeamModelSelector disabled Codex models', () => {
|
describe('TeamModelSelector disabled Codex models', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
document.body.innerHTML = '';
|
document.body.innerHTML = '';
|
||||||
|
Reflect.deleteProperty(window, 'electronAPI');
|
||||||
storeState.cliStatus = null;
|
storeState.cliStatus = null;
|
||||||
storeState.cliStatusLoading = false;
|
storeState.cliStatusLoading = false;
|
||||||
|
storeState.cliProviderStatusLoading = {};
|
||||||
storeState.fetchCliProviderStatus.mockClear();
|
storeState.fetchCliProviderStatus.mockClear();
|
||||||
codexAccountHookState.snapshot = null;
|
codexAccountHookState.snapshot = null;
|
||||||
codexAccountHookState.loading = false;
|
codexAccountHookState.loading = false;
|
||||||
|
|
@ -124,6 +127,7 @@ describe('TeamModelSelector disabled Codex models', () => {
|
||||||
|
|
||||||
it('shows only Default while Codex runtime models are still loading', async () => {
|
it('shows only Default while Codex runtime models are still loading', async () => {
|
||||||
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
|
||||||
|
Object.defineProperty(window, 'electronAPI', { value: {}, configurable: true });
|
||||||
storeState.cliStatusLoading = true;
|
storeState.cliStatusLoading = true;
|
||||||
const host = document.createElement('div');
|
const host = document.createElement('div');
|
||||||
document.body.appendChild(host);
|
document.body.appendChild(host);
|
||||||
|
|
@ -142,6 +146,7 @@ describe('TeamModelSelector disabled Codex models', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(host.textContent).toContain('Default');
|
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.1 Codex Mini');
|
||||||
expect(host.textContent).not.toContain('5.3 Codex Spark');
|
expect(host.textContent).not.toContain('5.3 Codex Spark');
|
||||||
const defaultButton = Array.from(host.querySelectorAll('button')).find((button) =>
|
const defaultButton = Array.from(host.querySelectorAll('button')).find((button) =>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue