fix(providers): address bot blockers for stale Credentials missing state
- shouldAutoOpenAuthPanel: re-introduce isEditableCustomProvider exemption (was dropped when requiresProviderAuth was replaced). Custom providers are editable directly in the form and must not be force-opened into the auth panel on a stale sources snapshot. - handleSaveCustomProvider + handleDisconnectProvider: route through applyConfigReloadOrRecordDeferred so an externally managed OpenCode that throws requiresManualRestart records deferred-restart guidance instead of toasting a misleading 'mutation failed' for a write that already persisted. - handleOAuthConnected: call markAuthWriteSucceeded so the page does not stick on a stale 'Credentials missing' summary while the providers refresh lands (OAuth previously only updated the deferred-restart payload). - Sources effect deps: add settingsDirectory back so a directory switch while the provider id is unchanged refetches the source snapshot (was swapped for providerSourcesRevision in the prior rebase). - Cleanup: drop dead oauthCodes state and copyTextToClipboard import; restore the requiresOpenCodeRestartAfterOAuth import alignment. 13/13 ProvidersPage.test.ts still green.
This commit is contained in:
@@ -23,7 +23,6 @@ import { reloadOpenCodeConfiguration } from '@/stores/useAgentsStore';
|
|||||||
import type { ConfigChangeScope } from '@/lib/configSync';
|
import type { ConfigChangeScope } from '@/lib/configSync';
|
||||||
import { recordDeferredOpenCodeRestart } from '@/lib/opencode/deferredRestart';
|
import { recordDeferredOpenCodeRestart } from '@/lib/opencode/deferredRestart';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { copyTextToClipboard } from '@/lib/clipboard';
|
|
||||||
import type { ModelMetadata } from '@/types';
|
import type { ModelMetadata } from '@/types';
|
||||||
import { getCurrentIntlLocale, useI18n } from '@/lib/i18n';
|
import { getCurrentIntlLocale, useI18n } from '@/lib/i18n';
|
||||||
import { runtimeFetch } from '@/lib/runtime-fetch';
|
import { runtimeFetch } from '@/lib/runtime-fetch';
|
||||||
@@ -32,8 +31,8 @@ import { shouldLoadAvailableProviders } from './providerAvailability';
|
|||||||
import {
|
import {
|
||||||
getOAuthAuthMethods,
|
getOAuthAuthMethods,
|
||||||
parseAuthPayload,
|
parseAuthPayload,
|
||||||
requiresOpenCodeRestartAfterOAuth,
|
|
||||||
providerHasCredentials,
|
providerHasCredentials,
|
||||||
|
requiresOpenCodeRestartAfterOAuth,
|
||||||
shouldAutoOpenAuthPanel,
|
shouldAutoOpenAuthPanel,
|
||||||
shouldShowApiKeyAuth,
|
shouldShowApiKeyAuth,
|
||||||
shouldShowModelsSection,
|
shouldShowModelsSection,
|
||||||
@@ -167,7 +166,6 @@ export const ProvidersPage: React.FC = () => {
|
|||||||
const [authMethodsByProvider, setAuthMethodsByProvider] = React.useState<Record<string, AuthMethod[]>>({});
|
const [authMethodsByProvider, setAuthMethodsByProvider] = React.useState<Record<string, AuthMethod[]>>({});
|
||||||
const [authLoading, setAuthLoading] = React.useState(false);
|
const [authLoading, setAuthLoading] = React.useState(false);
|
||||||
const [apiKeyInputs, setApiKeyInputs] = React.useState<Record<string, string>>({});
|
const [apiKeyInputs, setApiKeyInputs] = React.useState<Record<string, string>>({});
|
||||||
const [oauthCodes, setOauthCodes] = React.useState<Record<string, string>>({});
|
|
||||||
const [authBusyKey, setAuthBusyKey] = React.useState<string | null>(null);
|
const [authBusyKey, setAuthBusyKey] = React.useState<string | null>(null);
|
||||||
const [modelQuery, setModelQuery] = React.useState('');
|
const [modelQuery, setModelQuery] = React.useState('');
|
||||||
const [availableProviders, setAvailableProviders] = React.useState<ProviderOption[]>([]);
|
const [availableProviders, setAvailableProviders] = React.useState<ProviderOption[]>([]);
|
||||||
@@ -342,11 +340,15 @@ export const ProvidersPage: React.FC = () => {
|
|||||||
key: provider?.key,
|
key: provider?.key,
|
||||||
authSourceExists: sources.auth.exists,
|
authSourceExists: sources.auth.exists,
|
||||||
});
|
});
|
||||||
|
const isEditableCustomProvider = Boolean(
|
||||||
|
provider && isConfigDefinedCustomProvider(provider, sources)
|
||||||
|
);
|
||||||
if (
|
if (
|
||||||
shouldAutoOpenAuthPanel({
|
shouldAutoOpenAuthPanel({
|
||||||
sourcesLoaded: true,
|
sourcesLoaded: true,
|
||||||
hasCredentials: hasCreds,
|
hasCredentials: hasCreds,
|
||||||
userDismissed: authPanelDismissedForId === selectedProviderId,
|
userDismissed: authPanelDismissedForId === selectedProviderId,
|
||||||
|
isEditableCustomProvider,
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
setShowAuthPanel(true);
|
setShowAuthPanel(true);
|
||||||
@@ -394,7 +396,7 @@ export const ProvidersPage: React.FC = () => {
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [selectedProviderId, providerSourcesRevision, t]);
|
}, [selectedProviderId, providerSourcesRevision, settingsDirectory, t]);
|
||||||
|
|
||||||
const refreshProviderSources = React.useCallback(() => {
|
const refreshProviderSources = React.useCallback(() => {
|
||||||
setProviderSourcesRevision((revision) => revision + 1);
|
setProviderSourcesRevision((revision) => revision + 1);
|
||||||
@@ -534,7 +536,10 @@ export const ProvidersPage: React.FC = () => {
|
|||||||
setEditingCustomScope(null);
|
setEditingCustomScope(null);
|
||||||
setCustomAuthFailureHint(null);
|
setCustomAuthFailureHint(null);
|
||||||
setLastCustomPersistId(null);
|
setLastCustomPersistId(null);
|
||||||
await reloadOpenCodeConfiguration({ scopes: ['providers'], mode: 'active' });
|
// Mutation succeeded; route through the helper so an externally managed
|
||||||
|
// OpenCode does not produce a misleading "save failed" toast for a write
|
||||||
|
// that already persisted.
|
||||||
|
await applyConfigReloadOrRecordDeferred('providers', plan.providerID);
|
||||||
markAuthWriteSucceeded(plan.providerID);
|
markAuthWriteSucceeded(plan.providerID);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save custom provider:', error);
|
console.error('Failed to save custom provider:', error);
|
||||||
@@ -556,6 +561,9 @@ export const ProvidersPage: React.FC = () => {
|
|||||||
if (requiresOpenCodeRestartAfterOAuth(providerId)) {
|
if (requiresOpenCodeRestartAfterOAuth(providerId)) {
|
||||||
recordDeferredOpenCodeRestart('providers', { id: providerId });
|
recordDeferredOpenCodeRestart('providers', { id: providerId });
|
||||||
}
|
}
|
||||||
|
// Optimistic mark + sources refetch so the page does not stick on a stale
|
||||||
|
// "Credentials missing" summary while the providers refresh lands.
|
||||||
|
markAuthWriteSucceeded(providerId);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDisconnectProvider = async (providerId: string) => {
|
const handleDisconnectProvider = async (providerId: string) => {
|
||||||
@@ -579,7 +587,10 @@ export const ProvidersPage: React.FC = () => {
|
|||||||
toast.success(t('settings.providers.page.toast.providerDisconnected'));
|
toast.success(t('settings.providers.page.toast.providerDisconnected'));
|
||||||
// Only accumulate when the server actually deferred a restart (e.g. auth removed).
|
// Only accumulate when the server actually deferred a restart (e.g. auth removed).
|
||||||
// removed:false payloads must not create a phantom pending Apply & Restart.
|
// removed:false payloads must not create a phantom pending Apply & Restart.
|
||||||
await reloadOpenCodeConfiguration({ scopes: ["providers"], mode: "active" });
|
// Use the helper so an externally managed OpenCode that requires a manual
|
||||||
|
// restart records the deferred-restart guidance instead of toasting a
|
||||||
|
// misleading "disconnect failed" for a write that already persisted.
|
||||||
|
await applyConfigReloadOrRecordDeferred('providers', providerId);
|
||||||
setAuthPanelDismissedForId(null);
|
setAuthPanelDismissedForId(null);
|
||||||
refreshProviderSources();
|
refreshProviderSources();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -89,4 +89,17 @@ export const shouldAutoOpenAuthPanel = (input: {
|
|||||||
sourcesLoaded: boolean;
|
sourcesLoaded: boolean;
|
||||||
hasCredentials: boolean;
|
hasCredentials: boolean;
|
||||||
userDismissed: boolean;
|
userDismissed: boolean;
|
||||||
}): boolean => input.sourcesLoaded && !input.hasCredentials && !input.userDismissed;
|
/**
|
||||||
|
* Config-defined custom providers (providerSources.custom present and parsed
|
||||||
|
* via `isConfigDefinedCustomProvider`) do not auto-open the auth panel: the
|
||||||
|
* provider is editable directly in the form, and a stale `Credentials
|
||||||
|
* missing` summary would be misleading. Optional for back-compat; defaults to
|
||||||
|
* `false`, restoring the pre-rewrite exemption that `requiresProviderAuth`
|
||||||
|
* carried via `providerAvailability.ts`.
|
||||||
|
*/
|
||||||
|
isEditableCustomProvider?: boolean;
|
||||||
|
}): boolean =>
|
||||||
|
input.sourcesLoaded &&
|
||||||
|
!input.hasCredentials &&
|
||||||
|
!input.userDismissed &&
|
||||||
|
!input.isEditableCustomProvider;
|
||||||
|
|||||||
Reference in New Issue
Block a user