fix(extensions): harden hidden provider runtime handling

This commit is contained in:
777genius 2026-04-17 20:20:27 +03:00
parent 12f6f90701
commit 3446ef0100
5 changed files with 218 additions and 67 deletions

View file

@ -33,6 +33,7 @@ import { useStore } from '@renderer/store';
import { createLoadingMultimodelCliStatus } from '@renderer/store/slices/cliInstallerSlice';
import { formatBytes } from '@renderer/utils/formatters';
import { filterMainScreenCliProviders } from '@renderer/utils/geminiUiFreeze';
import { isMultimodelRuntimeStatus } from '@renderer/utils/multimodelProviderVisibility';
import {
AlertTriangle,
CheckCircle,
@ -321,7 +322,11 @@ function formatRuntimeAuthSummary(
cliStatus: NonNullable<ReturnType<typeof useCliInstaller>['cliStatus']>,
visibleProviders: readonly CliProviderStatus[]
): string | null {
if (cliStatus.flavor === 'agent_teams_orchestrator' && visibleProviders.length > 0) {
if (isMultimodelRuntimeStatus(cliStatus)) {
if (visibleProviders.length === 0) {
return null;
}
if (
visibleProviders.every(
(provider) => provider.statusMessage === 'Checking...' && !provider.authenticated
@ -351,7 +356,7 @@ function isCheckingMultimodelStatus(
visibleProviders: readonly CliProviderStatus[]
): boolean {
return (
cliStatus.flavor === 'agent_teams_orchestrator' &&
isMultimodelRuntimeStatus(cliStatus) &&
visibleProviders.length > 0 &&
visibleProviders.every(
(provider) => provider.statusMessage === 'Checking...' && !provider.authenticated

View file

@ -20,6 +20,11 @@ import {
import { useTabIdOptional } from '@renderer/contexts/useTabUIContext';
import { useExtensionsTabState } from '@renderer/hooks/useExtensionsTabState';
import { useStore } from '@renderer/store';
import {
formatCliExtensionCapabilityStatus,
getVisibleMultimodelProviders,
isMultimodelRuntimeStatus,
} from '@renderer/utils/multimodelProviderVisibility';
import { resolveProjectPathById } from '@renderer/utils/projectLookup';
import { getExtensionActionDisableReason } from '@shared/utils/extensionNormalizers';
import { AlertTriangle, BookOpen, Info, Key, Plus, Puzzle, RefreshCw, Server } from 'lucide-react';
@ -178,9 +183,8 @@ export const ExtensionStoreView = (): React.JSX.Element => {
);
const cliStatusBanner = useMemo(() => {
const providers = cliStatus?.providers ?? [];
const visibleProviders = providers.filter((provider) => provider.providerId !== 'gemini');
const isMultimodel =
cliStatus?.flavor === 'agent_teams_orchestrator' && visibleProviders.length > 0;
const visibleProviders = getVisibleMultimodelProviders(providers);
const isMultimodel = isMultimodelRuntimeStatus(cliStatus);
if (cliStatusLoading || cliStatus === null) {
return (
@ -260,57 +264,64 @@ export const ExtensionStoreView = (): React.JSX.Element => {
</p>
</div>
</div>
<div className="mt-3 grid gap-2 md:grid-cols-2">
{visibleProviders.map((provider) => {
const statusTone = provider.authenticated
? 'border-emerald-500/30 bg-emerald-500/5 text-emerald-300'
: provider.supported
? 'border-amber-500/30 bg-amber-500/5 text-amber-300'
: 'border-border bg-surface-raised text-text-muted';
const statusLabel = provider.authenticated
? 'Connected'
: provider.supported
? 'Needs setup'
: 'Unsupported';
const pluginStatus = provider.capabilities.extensions.plugins.status;
{visibleProviders.length > 0 && (
<div className="mt-3 grid gap-2 md:grid-cols-2">
{visibleProviders.map((provider) => {
const statusTone = provider.authenticated
? 'border-emerald-500/30 bg-emerald-500/5 text-emerald-300'
: provider.supported
? 'border-amber-500/30 bg-amber-500/5 text-amber-300'
: 'border-border bg-surface-raised text-text-muted';
const statusLabel = provider.authenticated
? 'Connected'
: provider.supported
? 'Needs setup'
: 'Unsupported';
const pluginStatus = provider.capabilities.extensions.plugins.status;
return (
<div
key={provider.providerId}
className={`rounded-md border px-3 py-2 ${statusTone}`}
>
<div className="flex items-center justify-between gap-2">
<div className="min-w-0">
<p className="inline-flex items-center gap-2 text-sm font-medium">
<ProviderBrandLogo
providerId={provider.providerId}
className="size-4 shrink-0"
/>
<span>{provider.displayName}</span>
</p>
<p className="truncate text-[11px] text-text-muted">
{provider.statusMessage ?? provider.backend?.label ?? 'Ready to configure'}
</p>
return (
<div
key={provider.providerId}
className={`rounded-md border px-3 py-2 ${statusTone}`}
>
<div className="flex items-center justify-between gap-2">
<div className="min-w-0">
<p className="inline-flex items-center gap-2 text-sm font-medium">
<ProviderBrandLogo
providerId={provider.providerId}
className="size-4 shrink-0"
/>
<span>{provider.displayName}</span>
</p>
<p className="truncate text-[11px] text-text-muted">
{provider.statusMessage ??
provider.backend?.label ??
'Ready to configure'}
</p>
</div>
<Badge variant="outline" className="shrink-0">
{statusLabel}
</Badge>
</div>
<div className="mt-2 flex flex-wrap gap-1.5 text-[11px]">
<Badge variant="secondary">
Plugins: {formatCliExtensionCapabilityStatus(pluginStatus)}
</Badge>
<Badge variant="secondary">
MCP:{' '}
{formatCliExtensionCapabilityStatus(
provider.capabilities.extensions.mcp.status
)}
</Badge>
<Badge variant="secondary">
Skills: {provider.capabilities.extensions.skills.ownership}
</Badge>
</div>
<Badge variant="outline" className="shrink-0">
{statusLabel}
</Badge>
</div>
<div className="mt-2 flex flex-wrap gap-1.5 text-[11px]">
<Badge variant="secondary">
Plugins: {pluginStatus === 'supported' ? 'supported' : 'limited'}
</Badge>
<Badge variant="secondary">
MCP: {provider.capabilities.extensions.mcp.status}
</Badge>
<Badge variant="secondary">
Skills: {provider.capabilities.extensions.skills.ownership}
</Badge>
</div>
</div>
);
})}
</div>
);
})}
</div>
)}
</div>
);
}

View file

@ -0,0 +1,32 @@
import { filterMainScreenCliProviders } from './geminiUiFreeze';
import type {
CliExtensionCapability,
CliInstallationStatus,
CliProviderStatus,
} from '@shared/types';
export function getVisibleMultimodelProviders(
providers: readonly CliProviderStatus[]
): CliProviderStatus[] {
return filterMainScreenCliProviders(providers);
}
export function isMultimodelRuntimeStatus(
cliStatus: Pick<CliInstallationStatus, 'flavor' | 'providers'> | null | undefined
): boolean {
return cliStatus?.flavor === 'agent_teams_orchestrator' && (cliStatus.providers?.length ?? 0) > 0;
}
export function formatCliExtensionCapabilityStatus(
status: CliExtensionCapability['status']
): string {
switch (status) {
case 'supported':
return 'supported';
case 'read-only':
return 'read-only';
default:
return 'unsupported';
}
}

View file

@ -170,7 +170,8 @@ function createApiKeyMisconfiguredProvider(
connection: {
supportsOAuth: true,
supportsApiKey: true,
configurableAuthModes: providerId === 'anthropic' ? ['auto', 'oauth', 'api_key'] : ['oauth', 'api_key'],
configurableAuthModes:
providerId === 'anthropic' ? ['auto', 'oauth', 'api_key'] : ['oauth', 'api_key'],
configuredAuthMode: 'api_key',
apiKeyBetaAvailable: providerId === 'codex' ? true : undefined,
apiKeyBetaEnabled: providerId === 'codex' ? true : undefined,
@ -181,9 +182,7 @@ function createApiKeyMisconfiguredProvider(
};
}
function createApiKeyModeProviderIssue(
providerId: 'anthropic' | 'codex'
): Record<string, unknown> {
function createApiKeyModeProviderIssue(providerId: 'anthropic' | 'codex'): Record<string, unknown> {
return {
...createApiKeyMisconfiguredProvider(providerId),
statusMessage:
@ -191,8 +190,8 @@ function createApiKeyModeProviderIssue(
? 'Anthropic API key was rejected by the runtime.'
: 'OpenAI API key was rejected by the runtime.',
connection: {
...((createApiKeyMisconfiguredProvider(providerId) as { connection: Record<string, unknown> })
.connection),
...(createApiKeyMisconfiguredProvider(providerId) as { connection: Record<string, unknown> })
.connection,
apiKeyConfigured: true,
apiKeySource: 'stored',
apiKeySourceLabel:
@ -287,9 +286,7 @@ describe('CLI status visibility during completed install state', () => {
it('preserves dashboard runtime backend refresh errors for the manage dialog', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
storeState.cliInstallerState = 'idle';
storeState.fetchCliProviderStatus = vi.fn(() =>
Promise.reject(new Error('refresh failed'))
);
storeState.fetchCliProviderStatus = vi.fn(() => Promise.reject(new Error('refresh failed')));
const host = document.createElement('div');
document.body.appendChild(host);
@ -345,6 +342,49 @@ describe('CLI status visibility during completed install state', () => {
});
});
it('does not fall back to direct-Claude auth copy when only hidden multimodel providers are available', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
storeState.cliInstallerState = 'idle';
storeState.cliStatus = createInstalledCliStatus({
flavor: 'agent_teams_orchestrator',
authLoggedIn: true,
providers: [
{
providerId: 'gemini',
displayName: 'Gemini',
supported: true,
authenticated: true,
authMethod: 'cli_oauth_personal',
verificationState: 'verified',
statusMessage: 'Resolved to CLI SDK',
models: [],
canLoginFromUi: true,
capabilities: {
teamLaunch: true,
oneShot: true,
},
},
],
});
const host = document.createElement('div');
document.body.appendChild(host);
const root = createRoot(host);
await act(async () => {
root.render(React.createElement(CliStatusBanner));
await Promise.resolve();
});
expect(host.textContent).not.toContain('Authenticated');
expect(host.textContent).not.toContain('Providers:');
await act(async () => {
root.unmount();
await Promise.resolve();
});
});
it('shows a degraded runtime warning when a binary is found but the health check fails', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
storeState.cliInstallerState = 'idle';
@ -413,9 +453,7 @@ describe('CLI status visibility during completed install state', () => {
it('preserves settings runtime backend refresh errors for the manage dialog', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
storeState.cliInstallerState = 'idle';
storeState.fetchCliProviderStatus = vi.fn(() =>
Promise.reject(new Error('refresh failed'))
);
storeState.fetchCliProviderStatus = vi.fn(() => Promise.reject(new Error('refresh failed')));
const host = document.createElement('div');
document.body.appendChild(host);
@ -490,8 +528,8 @@ describe('CLI status visibility during completed install state', () => {
expect(host.textContent).not.toContain('Already logged in?');
expect(host.textContent).not.toContain('Login');
const manageButton = Array.from(host.querySelectorAll('button')).find(
(button) => button.textContent?.includes('Manage Providers')
const manageButton = Array.from(host.querySelectorAll('button')).find((button) =>
button.textContent?.includes('Manage Providers')
);
expect(manageButton).not.toBeUndefined();

View file

@ -0,0 +1,65 @@
import { describe, expect, it } from 'vitest';
import {
formatCliExtensionCapabilityStatus,
getVisibleMultimodelProviders,
isMultimodelRuntimeStatus,
} from '@renderer/utils/multimodelProviderVisibility';
import type { CliInstallationStatus, CliProviderStatus } from '@shared/types';
function createProvider(providerId: CliProviderStatus['providerId']): CliProviderStatus {
return {
providerId,
displayName:
providerId === 'anthropic' ? 'Anthropic' : providerId === 'codex' ? 'Codex' : 'Gemini',
supported: true,
authenticated: true,
authMethod: 'oauth_token',
verificationState: 'verified',
canLoginFromUi: true,
capabilities: {
teamLaunch: true,
oneShot: true,
},
statusMessage: null,
detailMessage: null,
selectedBackendId: null,
resolvedBackendId: null,
availableBackends: [],
externalRuntimeDiagnostics: [],
models: [],
backend: null,
connection: null,
};
}
describe('multimodelProviderVisibility', () => {
it('keeps multimodel runtime detection true even when all visible provider cards are hidden', () => {
const cliStatus = {
flavor: 'agent_teams_orchestrator',
providers: [createProvider('gemini')],
} satisfies Pick<CliInstallationStatus, 'flavor' | 'providers'>;
expect(isMultimodelRuntimeStatus(cliStatus)).toBe(true);
expect(getVisibleMultimodelProviders(cliStatus.providers)).toHaveLength(0);
});
it('filters Gemini from the visible provider cards while keeping supported providers', () => {
const providers = [
createProvider('anthropic'),
createProvider('codex'),
createProvider('gemini'),
];
expect(getVisibleMultimodelProviders(providers).map((provider) => provider.providerId)).toEqual(
['anthropic', 'codex']
);
});
it('formats capability statuses without collapsing read-only into a vague limited label', () => {
expect(formatCliExtensionCapabilityStatus('supported')).toBe('supported');
expect(formatCliExtensionCapabilityStatus('read-only')).toBe('read-only');
expect(formatCliExtensionCapabilityStatus('unsupported')).toBe('unsupported');
});
});