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

View file

@ -20,6 +20,11 @@ import {
import { useTabIdOptional } from '@renderer/contexts/useTabUIContext'; import { useTabIdOptional } from '@renderer/contexts/useTabUIContext';
import { useExtensionsTabState } from '@renderer/hooks/useExtensionsTabState'; import { useExtensionsTabState } from '@renderer/hooks/useExtensionsTabState';
import { useStore } from '@renderer/store'; import { useStore } from '@renderer/store';
import {
formatCliExtensionCapabilityStatus,
getVisibleMultimodelProviders,
isMultimodelRuntimeStatus,
} from '@renderer/utils/multimodelProviderVisibility';
import { resolveProjectPathById } from '@renderer/utils/projectLookup'; import { resolveProjectPathById } from '@renderer/utils/projectLookup';
import { getExtensionActionDisableReason } from '@shared/utils/extensionNormalizers'; import { getExtensionActionDisableReason } from '@shared/utils/extensionNormalizers';
import { AlertTriangle, BookOpen, Info, Key, Plus, Puzzle, RefreshCw, Server } from 'lucide-react'; 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 cliStatusBanner = useMemo(() => {
const providers = cliStatus?.providers ?? []; const providers = cliStatus?.providers ?? [];
const visibleProviders = providers.filter((provider) => provider.providerId !== 'gemini'); const visibleProviders = getVisibleMultimodelProviders(providers);
const isMultimodel = const isMultimodel = isMultimodelRuntimeStatus(cliStatus);
cliStatus?.flavor === 'agent_teams_orchestrator' && visibleProviders.length > 0;
if (cliStatusLoading || cliStatus === null) { if (cliStatusLoading || cliStatus === null) {
return ( return (
@ -260,57 +264,64 @@ export const ExtensionStoreView = (): React.JSX.Element => {
</p> </p>
</div> </div>
</div> </div>
<div className="mt-3 grid gap-2 md:grid-cols-2"> {visibleProviders.length > 0 && (
{visibleProviders.map((provider) => { <div className="mt-3 grid gap-2 md:grid-cols-2">
const statusTone = provider.authenticated {visibleProviders.map((provider) => {
? 'border-emerald-500/30 bg-emerald-500/5 text-emerald-300' const statusTone = provider.authenticated
: provider.supported ? 'border-emerald-500/30 bg-emerald-500/5 text-emerald-300'
? 'border-amber-500/30 bg-amber-500/5 text-amber-300' : provider.supported
: 'border-border bg-surface-raised text-text-muted'; ? 'border-amber-500/30 bg-amber-500/5 text-amber-300'
const statusLabel = provider.authenticated : 'border-border bg-surface-raised text-text-muted';
? 'Connected' const statusLabel = provider.authenticated
: provider.supported ? 'Connected'
? 'Needs setup' : provider.supported
: 'Unsupported'; ? 'Needs setup'
const pluginStatus = provider.capabilities.extensions.plugins.status; : 'Unsupported';
const pluginStatus = provider.capabilities.extensions.plugins.status;
return ( return (
<div <div
key={provider.providerId} key={provider.providerId}
className={`rounded-md border px-3 py-2 ${statusTone}`} className={`rounded-md border px-3 py-2 ${statusTone}`}
> >
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div className="min-w-0"> <div className="min-w-0">
<p className="inline-flex items-center gap-2 text-sm font-medium"> <p className="inline-flex items-center gap-2 text-sm font-medium">
<ProviderBrandLogo <ProviderBrandLogo
providerId={provider.providerId} providerId={provider.providerId}
className="size-4 shrink-0" className="size-4 shrink-0"
/> />
<span>{provider.displayName}</span> <span>{provider.displayName}</span>
</p> </p>
<p className="truncate text-[11px] text-text-muted"> <p className="truncate text-[11px] text-text-muted">
{provider.statusMessage ?? provider.backend?.label ?? 'Ready to configure'} {provider.statusMessage ??
</p> 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> </div>
<Badge variant="outline" className="shrink-0">
{statusLabel}
</Badge>
</div> </div>
<div className="mt-2 flex flex-wrap gap-1.5 text-[11px]"> );
<Badge variant="secondary"> })}
Plugins: {pluginStatus === 'supported' ? 'supported' : 'limited'} </div>
</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: { connection: {
supportsOAuth: true, supportsOAuth: true,
supportsApiKey: 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', configuredAuthMode: 'api_key',
apiKeyBetaAvailable: providerId === 'codex' ? true : undefined, apiKeyBetaAvailable: providerId === 'codex' ? true : undefined,
apiKeyBetaEnabled: providerId === 'codex' ? true : undefined, apiKeyBetaEnabled: providerId === 'codex' ? true : undefined,
@ -181,9 +182,7 @@ function createApiKeyMisconfiguredProvider(
}; };
} }
function createApiKeyModeProviderIssue( function createApiKeyModeProviderIssue(providerId: 'anthropic' | 'codex'): Record<string, unknown> {
providerId: 'anthropic' | 'codex'
): Record<string, unknown> {
return { return {
...createApiKeyMisconfiguredProvider(providerId), ...createApiKeyMisconfiguredProvider(providerId),
statusMessage: statusMessage:
@ -191,8 +190,8 @@ function createApiKeyModeProviderIssue(
? 'Anthropic API key was rejected by the runtime.' ? 'Anthropic API key was rejected by the runtime.'
: 'OpenAI API key was rejected by the runtime.', : 'OpenAI API key was rejected by the runtime.',
connection: { connection: {
...((createApiKeyMisconfiguredProvider(providerId) as { connection: Record<string, unknown> }) ...(createApiKeyMisconfiguredProvider(providerId) as { connection: Record<string, unknown> })
.connection), .connection,
apiKeyConfigured: true, apiKeyConfigured: true,
apiKeySource: 'stored', apiKeySource: 'stored',
apiKeySourceLabel: 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 () => { it('preserves dashboard runtime backend refresh errors for the manage dialog', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
storeState.cliInstallerState = 'idle'; storeState.cliInstallerState = 'idle';
storeState.fetchCliProviderStatus = vi.fn(() => storeState.fetchCliProviderStatus = vi.fn(() => Promise.reject(new Error('refresh failed')));
Promise.reject(new Error('refresh failed'))
);
const host = document.createElement('div'); const host = document.createElement('div');
document.body.appendChild(host); 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 () => { it('shows a degraded runtime warning when a binary is found but the health check fails', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
storeState.cliInstallerState = 'idle'; 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 () => { it('preserves settings runtime backend refresh errors for the manage dialog', async () => {
vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true); vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true);
storeState.cliInstallerState = 'idle'; storeState.cliInstallerState = 'idle';
storeState.fetchCliProviderStatus = vi.fn(() => storeState.fetchCliProviderStatus = vi.fn(() => Promise.reject(new Error('refresh failed')));
Promise.reject(new Error('refresh failed'))
);
const host = document.createElement('div'); const host = document.createElement('div');
document.body.appendChild(host); 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('Already logged in?');
expect(host.textContent).not.toContain('Login'); expect(host.textContent).not.toContain('Login');
const manageButton = Array.from(host.querySelectorAll('button')).find( const manageButton = Array.from(host.querySelectorAll('button')).find((button) =>
(button) => button.textContent?.includes('Manage Providers') button.textContent?.includes('Manage Providers')
); );
expect(manageButton).not.toBeUndefined(); 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');
});
});