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:
parent
7aa87f2278
commit
57931c0abd
8 changed files with 228 additions and 297 deletions
|
|
@ -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"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { isElectronMode } from '@renderer/api';
|
||||||
import { formatProviderStatusText } from '@renderer/components/runtime/providerConnectionUi';
|
import { formatProviderStatusText } from '@renderer/components/runtime/providerConnectionUi';
|
||||||
import { createLoadingMultimodelCliStatus } from '@renderer/store/slices/cliInstallerSlice';
|
import { createLoadingMultimodelCliStatus } from '@renderer/store/slices/cliInstallerSlice';
|
||||||
import { filterMainScreenCliProviders } from '@renderer/utils/geminiUiFreeze';
|
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 { AlertTriangle, CheckCircle2, Loader2 } from 'lucide-react';
|
||||||
|
|
||||||
import { ProviderBrandLogo } from './ProviderBrandLogo';
|
import { ProviderBrandLogo } from './ProviderBrandLogo';
|
||||||
|
|
@ -27,17 +27,13 @@ interface ProviderActivityStatusStripProps {
|
||||||
readonly providerIds?: readonly CliProviderId[];
|
readonly providerIds?: readonly CliProviderId[];
|
||||||
readonly className?: string;
|
readonly className?: string;
|
||||||
readonly label?: string | null;
|
readonly label?: string | null;
|
||||||
|
readonly layout?: 'inline' | 'stacked';
|
||||||
|
readonly showReadyProviders?: boolean;
|
||||||
|
readonly readyStatusText?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isProviderCardLoading(provider: CliProviderStatus, providerLoading: boolean): boolean {
|
function isProviderCardLoading(provider: CliProviderStatus, providerLoading: boolean): boolean {
|
||||||
return (
|
return providerLoading || isTeamProviderModelVerificationPending(provider.providerId, provider);
|
||||||
providerLoading ||
|
|
||||||
(!provider.authenticated &&
|
|
||||||
(provider.statusMessage === 'Checking...' ||
|
|
||||||
provider.statusMessage === CLI_PROVIDER_STATUS_DEFERRED_MESSAGE) &&
|
|
||||||
provider.models.length === 0 &&
|
|
||||||
provider.backend == null)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function shouldMaskCodexNegativeBootstrapState(
|
function shouldMaskCodexNegativeBootstrapState(
|
||||||
|
|
@ -97,6 +93,7 @@ function useProviderActivityDisplay({
|
||||||
multimodelEnabled,
|
multimodelEnabled,
|
||||||
codexSnapshotPending = false,
|
codexSnapshotPending = false,
|
||||||
providerIds,
|
providerIds,
|
||||||
|
showReadyProviders,
|
||||||
}: Pick<
|
}: Pick<
|
||||||
ProviderActivityStatusStripProps,
|
ProviderActivityStatusStripProps,
|
||||||
| 'cliStatus'
|
| 'cliStatus'
|
||||||
|
|
@ -106,6 +103,7 @@ function useProviderActivityDisplay({
|
||||||
| 'multimodelEnabled'
|
| 'multimodelEnabled'
|
||||||
| 'codexSnapshotPending'
|
| 'codexSnapshotPending'
|
||||||
| 'providerIds'
|
| 'providerIds'
|
||||||
|
| 'showReadyProviders'
|
||||||
>): {
|
>): {
|
||||||
displayProviderIds: CliProviderId[];
|
displayProviderIds: CliProviderId[];
|
||||||
providerStateMap: Map<CliProviderId, ProviderActivityState>;
|
providerStateMap: Map<CliProviderId, ProviderActivityState>;
|
||||||
|
|
@ -201,6 +199,10 @@ function useProviderActivityDisplay({
|
||||||
}, [errorProviderIds, loadingProviderIds, visibleProviderIds]);
|
}, [errorProviderIds, loadingProviderIds, visibleProviderIds]);
|
||||||
|
|
||||||
const displayProviderIds = useMemo(() => {
|
const displayProviderIds = useMemo(() => {
|
||||||
|
if (showReadyProviders) {
|
||||||
|
return visibleProviderIds;
|
||||||
|
}
|
||||||
|
|
||||||
if (loadingProviderIds.length > 0) {
|
if (loadingProviderIds.length > 0) {
|
||||||
const activeCycleIds = (
|
const activeCycleIds = (
|
||||||
cycleProviderIds.length > 0 ? cycleProviderIds : loadingProviderIds
|
cycleProviderIds.length > 0 ? cycleProviderIds : loadingProviderIds
|
||||||
|
|
@ -213,7 +215,14 @@ function useProviderActivityDisplay({
|
||||||
}
|
}
|
||||||
|
|
||||||
return [];
|
return [];
|
||||||
}, [cycleProviderIds, errorProviderIds, loadingProviderIds, providerStateMap]);
|
}, [
|
||||||
|
cycleProviderIds,
|
||||||
|
errorProviderIds,
|
||||||
|
loadingProviderIds,
|
||||||
|
providerStateMap,
|
||||||
|
showReadyProviders,
|
||||||
|
visibleProviderIds,
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
displayProviderIds,
|
displayProviderIds,
|
||||||
|
|
@ -237,6 +246,9 @@ export const ProviderActivityStatusStrip = ({
|
||||||
providerIds,
|
providerIds,
|
||||||
className = '',
|
className = '',
|
||||||
label = 'Provider Activity',
|
label = 'Provider Activity',
|
||||||
|
layout = 'inline',
|
||||||
|
showReadyProviders = false,
|
||||||
|
readyStatusText = 'Checked',
|
||||||
}: ProviderActivityStatusStripProps): React.JSX.Element | null => {
|
}: ProviderActivityStatusStripProps): React.JSX.Element | null => {
|
||||||
const { displayProviderIds, providerStateMap, shouldRender } = useProviderActivityDisplay({
|
const { displayProviderIds, providerStateMap, shouldRender } = useProviderActivityDisplay({
|
||||||
cliStatus,
|
cliStatus,
|
||||||
|
|
@ -246,14 +258,24 @@ export const ProviderActivityStatusStrip = ({
|
||||||
multimodelEnabled,
|
multimodelEnabled,
|
||||||
codexSnapshotPending,
|
codexSnapshotPending,
|
||||||
providerIds,
|
providerIds,
|
||||||
|
showReadyProviders,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!shouldRender) {
|
if (!shouldRender) {
|
||||||
return null;
|
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 (
|
return (
|
||||||
<div className={`flex min-w-0 flex-wrap items-center gap-2 ${className}`.trim()}>
|
<div className={rootClassName}>
|
||||||
{label ? (
|
{label ? (
|
||||||
<span
|
<span
|
||||||
className="shrink-0 text-[11px] font-medium uppercase tracking-[0.08em]"
|
className="shrink-0 text-[11px] font-medium uppercase tracking-[0.08em]"
|
||||||
|
|
@ -262,7 +284,7 @@ export const ProviderActivityStatusStrip = ({
|
||||||
{label}
|
{label}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2">
|
<div className={itemsClassName}>
|
||||||
{displayProviderIds.map((providerId) => {
|
{displayProviderIds.map((providerId) => {
|
||||||
const providerState = providerStateMap.get(providerId);
|
const providerState = providerStateMap.get(providerId);
|
||||||
if (!providerState) {
|
if (!providerState) {
|
||||||
|
|
@ -280,13 +302,13 @@ export const ProviderActivityStatusStrip = ({
|
||||||
? 'Checking...'
|
? 'Checking...'
|
||||||
: tone === 'error'
|
: tone === 'error'
|
||||||
? formatProviderStatusText(providerState.provider)
|
? formatProviderStatusText(providerState.provider)
|
||||||
: 'Checked';
|
: readyStatusText;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={providerId}
|
key={providerId}
|
||||||
data-testid={`provider-activity-status-${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={{
|
style={{
|
||||||
borderColor: styles.borderColor,
|
borderColor: styles.borderColor,
|
||||||
backgroundColor: styles.backgroundColor,
|
backgroundColor: styles.backgroundColor,
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,6 @@ import { useStore } from '@renderer/store';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
|
||||||
import { CliInstallWarningBanner } from '../common/CliInstallWarningBanner';
|
import { CliInstallWarningBanner } from '../common/CliInstallWarningBanner';
|
||||||
import { GlobalProviderStatusHeader } from '../common/GlobalProviderStatusHeader';
|
|
||||||
import { UpdateBanner } from '../common/UpdateBanner';
|
import { UpdateBanner } from '../common/UpdateBanner';
|
||||||
import { UpdateDialog } from '../common/UpdateDialog';
|
import { UpdateDialog } from '../common/UpdateDialog';
|
||||||
import { WorkspaceIndicator } from '../common/WorkspaceIndicator';
|
import { WorkspaceIndicator } from '../common/WorkspaceIndicator';
|
||||||
|
|
@ -164,7 +163,6 @@ export const TabbedLayout = (): React.JSX.Element => {
|
||||||
>
|
>
|
||||||
<TabBarRow />
|
<TabBarRow />
|
||||||
<CliInstallWarningBanner />
|
<CliInstallWarningBanner />
|
||||||
<GlobalProviderStatusHeader />
|
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
{/* Command Palette (Cmd+K) */}
|
{/* Command Palette (Cmd+K) */}
|
||||||
<CommandPalette />
|
<CommandPalette />
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@ import { refreshCliStatusForCurrentMode } from '@renderer/utils/refreshCliStatus
|
||||||
import { getAvailableTeamEffortValue } from '@renderer/utils/teamEffortOptions';
|
import { getAvailableTeamEffortValue } from '@renderer/utils/teamEffortOptions';
|
||||||
import {
|
import {
|
||||||
getTeamModelSelectionError,
|
getTeamModelSelectionError,
|
||||||
|
isTeamProviderRuntimeStatusLoading,
|
||||||
normalizeExplicitTeamModelForUi,
|
normalizeExplicitTeamModelForUi,
|
||||||
} from '@renderer/utils/teamModelAvailability';
|
} from '@renderer/utils/teamModelAvailability';
|
||||||
import { getTeamProviderLabel as getCatalogTeamProviderLabel } from '@renderer/utils/teamModelCatalog';
|
import { getTeamProviderLabel as getCatalogTeamProviderLabel } from '@renderer/utils/teamModelCatalog';
|
||||||
|
|
@ -699,6 +700,29 @@ export const CreateTeamDialog = ({
|
||||||
),
|
),
|
||||||
[effectiveCliStatus?.providers]
|
[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(
|
const selectedProviderBackendId = useMemo(
|
||||||
() =>
|
() =>
|
||||||
resolveUiOwnedProviderBackendId(
|
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({
|
const providerPlans = buildProviderPreparePlans({
|
||||||
cwd: effectiveCwd,
|
cwd: effectiveCwd,
|
||||||
providerIds: selectedMemberProviders,
|
providerIds: readyProviderIds,
|
||||||
selectedModelChecksByProvider,
|
selectedModelChecksByProvider,
|
||||||
backendSummaryByProvider: runtimeBackendSummaryByProviderRef.current,
|
backendSummaryByProvider: runtimeBackendSummaryByProviderRef.current,
|
||||||
limitContext: effectiveAnthropicRuntimeLimitContext,
|
limitContext: effectiveAnthropicRuntimeLimitContext,
|
||||||
|
|
@ -1048,7 +1078,7 @@ export const CreateTeamDialog = ({
|
||||||
return lastSignature !== plan.requestSignature && pendingSignature !== plan.requestSignature;
|
return lastSignature !== plan.requestSignature && pendingSignature !== plan.requestSignature;
|
||||||
});
|
});
|
||||||
const loadingMessage = getProvisioningProviderProgressMessage(
|
const loadingMessage = getProvisioningProviderProgressMessage(
|
||||||
changedPlans.map((plan) => plan.providerId),
|
[...loadingProviderIds, ...changedPlans.map((plan) => plan.providerId)],
|
||||||
selectedMemberProviders.length
|
selectedMemberProviders.length
|
||||||
);
|
);
|
||||||
const getSelectedWarnings = (): string[] =>
|
const getSelectedWarnings = (): string[] =>
|
||||||
|
|
@ -1089,6 +1119,20 @@ export const CreateTeamDialog = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
let checks = alignProvisioningChecks(prepareChecksRef.current, selectedMemberProviders);
|
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) {
|
for (const plan of changedPlans) {
|
||||||
checks = updateProviderCheck(checks, plan.providerId, {
|
checks = updateProviderCheck(checks, plan.providerId, {
|
||||||
status: plan.selectedModelIds.length > 0 ? plan.cachedSnapshot.status : 'checking',
|
status: plan.selectedModelIds.length > 0 ? plan.cachedSnapshot.status : 'checking',
|
||||||
|
|
@ -1229,6 +1273,7 @@ export const CreateTeamDialog = ({
|
||||||
effectiveAnthropicRuntimeLimitContext,
|
effectiveAnthropicRuntimeLimitContext,
|
||||||
prepareProviderInvalidationEpochById,
|
prepareProviderInvalidationEpochById,
|
||||||
runtimeProviderStatusById,
|
runtimeProviderStatusById,
|
||||||
|
runtimeProviderLoadingById,
|
||||||
selectedModel,
|
selectedModel,
|
||||||
selectedModelChecksByProvider,
|
selectedModelChecksByProvider,
|
||||||
selectedModelChecksByProviderSignature,
|
selectedModelChecksByProviderSignature,
|
||||||
|
|
@ -1724,13 +1769,15 @@ export const CreateTeamDialog = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const leadError = getTeamModelSelectionError(
|
if (!runtimeProviderLoadingById.get(selectedProviderId)) {
|
||||||
selectedProviderId,
|
const leadError = getTeamModelSelectionError(
|
||||||
selectedModel,
|
selectedProviderId,
|
||||||
runtimeProviderStatusById.get(selectedProviderId)
|
selectedModel,
|
||||||
);
|
runtimeProviderStatusById.get(selectedProviderId)
|
||||||
if (leadError) {
|
);
|
||||||
return leadError;
|
if (leadError) {
|
||||||
|
return leadError;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const member of effectiveMemberDrafts) {
|
for (const member of effectiveMemberDrafts) {
|
||||||
|
|
@ -1739,6 +1786,9 @@ export const CreateTeamDialog = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const providerId = normalizeOptionalTeamProviderId(member.providerId) ?? selectedProviderId;
|
const providerId = normalizeOptionalTeamProviderId(member.providerId) ?? selectedProviderId;
|
||||||
|
if (runtimeProviderLoadingById.get(providerId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const memberError = getTeamModelSelectionError(
|
const memberError = getTeamModelSelectionError(
|
||||||
providerId,
|
providerId,
|
||||||
member.model,
|
member.model,
|
||||||
|
|
@ -1756,6 +1806,7 @@ export const CreateTeamDialog = ({
|
||||||
}, [
|
}, [
|
||||||
effectiveMemberDrafts,
|
effectiveMemberDrafts,
|
||||||
runtimeProviderStatusById,
|
runtimeProviderStatusById,
|
||||||
|
runtimeProviderLoadingById,
|
||||||
selectedModel,
|
selectedModel,
|
||||||
selectedProviderId,
|
selectedProviderId,
|
||||||
soloTeam,
|
soloTeam,
|
||||||
|
|
@ -2488,6 +2539,12 @@ export const CreateTeamDialog = ({
|
||||||
codexSnapshotPending={codexSnapshotPending}
|
codexSnapshotPending={codexSnapshotPending}
|
||||||
providerIds={selectedMemberProviders}
|
providerIds={selectedMemberProviders}
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
|
label="Selected providers"
|
||||||
|
layout="stacked"
|
||||||
|
showReadyProviders={
|
||||||
|
effectivePrepare.state === 'idle' || effectivePrepare.state === 'loading'
|
||||||
|
}
|
||||||
|
readyStatusText="Ready"
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{canCreate &&
|
{canCreate &&
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ import { refreshCliStatusForCurrentMode } from '@renderer/utils/refreshCliStatus
|
||||||
import { getAvailableTeamEffortValue } from '@renderer/utils/teamEffortOptions';
|
import { getAvailableTeamEffortValue } from '@renderer/utils/teamEffortOptions';
|
||||||
import {
|
import {
|
||||||
getTeamModelSelectionError,
|
getTeamModelSelectionError,
|
||||||
|
isTeamProviderRuntimeStatusLoading,
|
||||||
normalizeExplicitTeamModelForUi,
|
normalizeExplicitTeamModelForUi,
|
||||||
} from '@renderer/utils/teamModelAvailability';
|
} from '@renderer/utils/teamModelAvailability';
|
||||||
import { getTeamProviderLabel as getCatalogTeamProviderLabel } from '@renderer/utils/teamModelCatalog';
|
import { getTeamProviderLabel as getCatalogTeamProviderLabel } from '@renderer/utils/teamModelCatalog';
|
||||||
|
|
@ -609,6 +610,29 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
),
|
),
|
||||||
[effectiveCliStatus?.providers]
|
[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(() => {
|
useEffect(() => {
|
||||||
if (!open) {
|
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({
|
const providerPlans = buildProviderPreparePlans({
|
||||||
cwd: effectiveCwd,
|
cwd: effectiveCwd,
|
||||||
providerIds: selectedMemberProviders,
|
providerIds: readyProviderIds,
|
||||||
selectedModelChecksByProvider,
|
selectedModelChecksByProvider,
|
||||||
backendSummaryByProvider: runtimeBackendSummaryByProviderRef.current,
|
backendSummaryByProvider: runtimeBackendSummaryByProviderRef.current,
|
||||||
limitContext: effectiveAnthropicRuntimeLimitContext,
|
limitContext: effectiveAnthropicRuntimeLimitContext,
|
||||||
|
|
@ -1634,7 +1664,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
lastPrepareProviderSignatureByIdRef.current.get(plan.providerId) !== plan.requestSignature
|
lastPrepareProviderSignatureByIdRef.current.get(plan.providerId) !== plan.requestSignature
|
||||||
);
|
);
|
||||||
const loadingMessage = getProvisioningProviderProgressMessage(
|
const loadingMessage = getProvisioningProviderProgressMessage(
|
||||||
changedPlans.map((plan) => plan.providerId),
|
[...loadingProviderIds, ...changedPlans.map((plan) => plan.providerId)],
|
||||||
selectedMemberProviders.length
|
selectedMemberProviders.length
|
||||||
);
|
);
|
||||||
const getSelectedWarnings = (): string[] =>
|
const getSelectedWarnings = (): string[] =>
|
||||||
|
|
@ -1675,6 +1705,19 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
};
|
};
|
||||||
|
|
||||||
let checks = alignProvisioningChecks(prepareChecksRef.current, selectedMemberProviders);
|
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) {
|
for (const plan of changedPlans) {
|
||||||
checks = updateProviderCheck(checks, plan.providerId, {
|
checks = updateProviderCheck(checks, plan.providerId, {
|
||||||
status: plan.selectedModelIds.length > 0 ? plan.cachedSnapshot.status : 'checking',
|
status: plan.selectedModelIds.length > 0 ? plan.cachedSnapshot.status : 'checking',
|
||||||
|
|
@ -1789,6 +1832,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
effectiveCwd,
|
effectiveCwd,
|
||||||
effectiveAnthropicRuntimeLimitContext,
|
effectiveAnthropicRuntimeLimitContext,
|
||||||
prepareProviderInvalidationEpochById,
|
prepareProviderInvalidationEpochById,
|
||||||
|
runtimeProviderLoadingById,
|
||||||
runtimeProviderStatusById,
|
runtimeProviderStatusById,
|
||||||
selectedMemberProviders,
|
selectedMemberProviders,
|
||||||
selectedModelChecksByProvider,
|
selectedModelChecksByProvider,
|
||||||
|
|
@ -2056,13 +2100,15 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const leadError = getTeamModelSelectionError(
|
if (!runtimeProviderLoadingById.get(selectedProviderId)) {
|
||||||
selectedProviderId,
|
const leadError = getTeamModelSelectionError(
|
||||||
selectedModel,
|
selectedProviderId,
|
||||||
runtimeProviderStatusById.get(selectedProviderId)
|
selectedModel,
|
||||||
);
|
runtimeProviderStatusById.get(selectedProviderId)
|
||||||
if (leadError) {
|
);
|
||||||
return leadError;
|
if (leadError) {
|
||||||
|
return leadError;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isLaunchMode) {
|
if (!isLaunchMode) {
|
||||||
|
|
@ -2075,6 +2121,9 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
}
|
}
|
||||||
|
|
||||||
const providerId = normalizeOptionalTeamProviderId(member.providerId) ?? selectedProviderId;
|
const providerId = normalizeOptionalTeamProviderId(member.providerId) ?? selectedProviderId;
|
||||||
|
if (runtimeProviderLoadingById.get(providerId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const memberError = getTeamModelSelectionError(
|
const memberError = getTeamModelSelectionError(
|
||||||
providerId,
|
providerId,
|
||||||
member.model,
|
member.model,
|
||||||
|
|
@ -2092,6 +2141,7 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
}, [
|
}, [
|
||||||
effectiveMemberDrafts,
|
effectiveMemberDrafts,
|
||||||
isLaunchMode,
|
isLaunchMode,
|
||||||
|
runtimeProviderLoadingById,
|
||||||
runtimeProviderStatusById,
|
runtimeProviderStatusById,
|
||||||
selectedModel,
|
selectedModel,
|
||||||
selectedProviderId,
|
selectedProviderId,
|
||||||
|
|
@ -3054,6 +3104,12 @@ export const LaunchTeamDialog = (props: LaunchTeamDialogProps): React.JSX.Elemen
|
||||||
multimodelEnabled={multimodelEnabled}
|
multimodelEnabled={multimodelEnabled}
|
||||||
codexSnapshotPending={codexSnapshotPending}
|
codexSnapshotPending={codexSnapshotPending}
|
||||||
providerIds={selectedMemberProviders}
|
providerIds={selectedMemberProviders}
|
||||||
|
label="Selected providers"
|
||||||
|
layout="stacked"
|
||||||
|
showReadyProviders={
|
||||||
|
effectivePrepare.state === 'idle' || effectivePrepare.state === 'loading'
|
||||||
|
}
|
||||||
|
readyStatusText="Ready"
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
/>
|
/>
|
||||||
{effectivePrepare.state === 'idle' || effectivePrepare.state === 'loading' ? (
|
{effectivePrepare.state === 'idle' || effectivePrepare.state === 'loading' ? (
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
getAvailableTeamProviderModels,
|
getAvailableTeamProviderModels,
|
||||||
getTeamModelSelectionError,
|
getTeamModelSelectionError,
|
||||||
isTeamModelAvailableForUi,
|
isTeamModelAvailableForUi,
|
||||||
|
isTeamProviderModelVerificationPending,
|
||||||
normalizeTeamModelForUi,
|
normalizeTeamModelForUi,
|
||||||
} from '../teamModelAvailability';
|
} from '../teamModelAvailability';
|
||||||
|
|
||||||
|
|
@ -204,6 +205,32 @@ describe('team model availability Codex catalog integration', () => {
|
||||||
expect(getTeamModelSelectionError('codex', 'gpt-5.5', providerStatus)).toBeNull();
|
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', () => {
|
it('orders GPT-5.5 first after the virtual default option', () => {
|
||||||
const providerStatus = createCodexProviderStatus([
|
const providerStatus = createCodexProviderStatus([
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { CLI_PROVIDER_STATUS_DEFERRED_MESSAGE } from '@shared/types/cliInstaller';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getProviderScopedTeamModelLabel,
|
getProviderScopedTeamModelLabel,
|
||||||
getRuntimeAwareProviderScopedTeamModelLabel,
|
getRuntimeAwareProviderScopedTeamModelLabel,
|
||||||
|
|
@ -43,6 +45,7 @@ export type TeamModelRuntimeProviderStatus = Pick<
|
||||||
| 'providerId'
|
| 'providerId'
|
||||||
| 'models'
|
| 'models'
|
||||||
| 'modelCatalog'
|
| 'modelCatalog'
|
||||||
|
| 'modelCatalogRefreshState'
|
||||||
| 'modelAvailability'
|
| 'modelAvailability'
|
||||||
| 'modelVerificationState'
|
| 'modelVerificationState'
|
||||||
| 'runtimeCapabilities'
|
| 'runtimeCapabilities'
|
||||||
|
|
@ -163,6 +166,21 @@ export function isTeamProviderModelVerificationPending(
|
||||||
return true;
|
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 =
|
const hasRuntimeModelTruth =
|
||||||
providerStatus.models.length > 0 ||
|
providerStatus.models.length > 0 ||
|
||||||
(providerStatus.modelCatalog?.models.length ?? 0) > 0 ||
|
(providerStatus.modelCatalog?.models.length ?? 0) > 0 ||
|
||||||
|
|
@ -193,10 +211,25 @@ export function isTeamProviderModelVerificationPending(
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusMessage = providerStatus.statusMessage?.trim().toLowerCase() ?? '';
|
|
||||||
return statusMessage.length === 0 || statusMessage === 'checking...';
|
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[] {
|
function getFallbackTeamProviderModels(providerId: SupportedProviderId): string[] {
|
||||||
return getVisibleTeamProviderModels(
|
return getVisibleTeamProviderModels(
|
||||||
providerId,
|
providerId,
|
||||||
|
|
|
||||||
|
|
@ -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();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Loading…
Reference in a new issue