feat(runtime): surface codex-native internal rollout states

This commit is contained in:
777genius 2026-04-19 19:41:36 +03:00
parent ba37c1caf5
commit 30fce3c64d
7 changed files with 346 additions and 17 deletions

View file

@ -88,6 +88,14 @@ interface UnifiedRuntimeStatusResponse {
selectable?: boolean;
recommended?: boolean;
available?: boolean;
state?:
| 'ready'
| 'locked'
| 'disabled'
| 'authentication-required'
| 'runtime-missing'
| 'degraded';
audience?: 'general' | 'internal';
statusMessage?: string | null;
detailMessage?: string | null;
}[];
@ -270,6 +278,8 @@ export class ClaudeMultimodelBridgeService {
selectable: backend.selectable !== false,
recommended: backend.recommended === true,
available: backend.available === true,
state: backend.state ?? undefined,
audience: backend.audience ?? undefined,
statusMessage: backend.statusMessage ?? null,
detailMessage: backend.detailMessage ?? null,
})) ?? [],

View file

@ -20,6 +20,39 @@ interface Props {
onSelect: (providerId: CliProviderStatus['providerId'], backendId: string) => void;
}
export function getProviderRuntimeBackendStateLabel(
option: NonNullable<CliProviderStatus['availableBackends']>[number]
): string | null {
switch (option.state) {
case 'ready':
return null;
case 'locked':
return 'Locked';
case 'disabled':
return 'Disabled';
case 'authentication-required':
return 'Auth required';
case 'runtime-missing':
return 'Runtime missing';
case 'degraded':
return 'Degraded';
default:
if (!option.available) {
return 'Unavailable';
}
if (option.selectable === false) {
return 'Locked';
}
return null;
}
}
export function getProviderRuntimeBackendAudienceLabel(
option: NonNullable<CliProviderStatus['availableBackends']>[number]
): string | null {
return option.audience === 'internal' ? 'Internal' : null;
}
export function getOptionDisplayLabel(
option: NonNullable<CliProviderStatus['availableBackends']>[number],
resolvedOption: NonNullable<CliProviderStatus['availableBackends']>[number] | null
@ -44,8 +77,18 @@ export function getProviderRuntimeBackendSummary(provider: CliProviderStatus): s
const selectedBackendId = provider.selectedBackendId ?? options[0]?.id ?? '';
const selectedOption = options.find((option) => option.id === selectedBackendId) ?? options[0];
const resolvedOption = options.find((option) => option.id === provider.resolvedBackendId) ?? null;
const parts = [getOptionDisplayLabel(selectedOption, resolvedOption)];
const audienceLabel = getProviderRuntimeBackendAudienceLabel(selectedOption);
const stateLabel = getProviderRuntimeBackendStateLabel(selectedOption);
return getOptionDisplayLabel(selectedOption, resolvedOption);
if (audienceLabel) {
parts.push(audienceLabel.toLowerCase());
}
if (stateLabel) {
parts.push(stateLabel.toLowerCase());
}
return parts.join(' - ');
}
export const ProviderRuntimeBackendSelector = ({
@ -62,6 +105,8 @@ export const ProviderRuntimeBackendSelector = ({
const selectedOption = options.find((option) => option.id === selectedBackendId) ?? options[0];
const resolvedOption = options.find((option) => option.id === provider.resolvedBackendId) ?? null;
const selectedLabel = getOptionDisplayLabel(selectedOption, resolvedOption);
const selectedStateLabel = getProviderRuntimeBackendStateLabel(selectedOption);
const selectedAudienceLabel = getProviderRuntimeBackendAudienceLabel(selectedOption);
return (
<div className="mt-2 space-y-2.5">
@ -120,25 +165,40 @@ export const ProviderRuntimeBackendSelector = ({
Recommended
</span>
) : null}
{!option.available ? (
{getProviderRuntimeBackendAudienceLabel(option) ? (
<span
className="shrink-0 rounded-full px-1.5 py-0.5 text-[10px]"
style={{
color: '#fca5a5',
backgroundColor: 'rgba(248, 113, 113, 0.14)',
color: '#93c5fd',
backgroundColor: 'rgba(59, 130, 246, 0.14)',
}}
>
Unavailable
{getProviderRuntimeBackendAudienceLabel(option)}
</span>
) : option.selectable === false ? (
) : null}
{getProviderRuntimeBackendStateLabel(option) ? (
<span
className="shrink-0 rounded-full px-1.5 py-0.5 text-[10px]"
style={{
color: 'var(--color-text-secondary)',
backgroundColor: 'rgba(255, 255, 255, 0.08)',
color:
option.state === 'disabled' ||
option.state === 'authentication-required' ||
option.state === 'runtime-missing' ||
option.state === 'degraded' ||
(!option.available && option.state !== 'locked')
? '#fca5a5'
: 'var(--color-text-secondary)',
backgroundColor:
option.state === 'disabled' ||
option.state === 'authentication-required' ||
option.state === 'runtime-missing' ||
option.state === 'degraded' ||
(!option.available && option.state !== 'locked')
? 'rgba(248, 113, 113, 0.14)'
: 'rgba(255, 255, 255, 0.08)',
}}
>
Locked
{getProviderRuntimeBackendStateLabel(option)}
</span>
) : null}
</div>
@ -173,7 +233,18 @@ export const ProviderRuntimeBackendSelector = ({
Recommended
</span>
) : null}
{!selectedOption.available ? (
{selectedAudienceLabel ? (
<span
className="rounded-full px-1.5 py-0.5 text-[10px]"
style={{
color: '#93c5fd',
backgroundColor: 'rgba(59, 130, 246, 0.14)',
}}
>
{selectedAudienceLabel}
</span>
) : null}
{!selectedStateLabel && !selectedOption.available ? (
<TooltipProvider delayDuration={150}>
<Tooltip>
<TooltipTrigger asChild>
@ -192,18 +263,24 @@ export const ProviderRuntimeBackendSelector = ({
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : selectedOption.selectable === false ? (
) : selectedStateLabel ? (
<TooltipProvider delayDuration={150}>
<Tooltip>
<TooltipTrigger asChild>
<span
className="cursor-help rounded-full px-1.5 py-0.5 text-[10px]"
style={{
color: 'var(--color-text-secondary)',
backgroundColor: 'rgba(255, 255, 255, 0.08)',
color:
selectedOption.state === 'locked'
? 'var(--color-text-secondary)'
: '#fca5a5',
backgroundColor:
selectedOption.state === 'locked'
? 'rgba(255, 255, 255, 0.08)'
: 'rgba(248, 113, 113, 0.14)',
}}
>
Locked
{selectedStateLabel}
</span>
</TooltipTrigger>
<TooltipContent>

View file

@ -46,12 +46,43 @@ export function getProvisioningProviderBackendSummary(
const options = provider.availableBackends ?? [];
const optionById = new Map(options.map((option) => [option.id, option.label]));
const effectiveBackendId = provider.resolvedBackendId ?? provider.selectedBackendId;
const effectiveOption = options.find((option) => option.id === effectiveBackendId) ?? null;
if (effectiveBackendId) {
return optionById.get(effectiveBackendId) ?? provider.backend?.label ?? effectiveBackendId;
const baseSummary = effectiveBackendId
? (optionById.get(effectiveBackendId) ?? provider.backend?.label ?? effectiveBackendId)
: (provider.backend?.label ?? null);
if (!baseSummary) {
return null;
}
return provider.backend?.label ?? null;
const suffixes: string[] = [];
if (effectiveOption?.audience === 'internal') {
suffixes.push('internal');
}
if (effectiveOption?.state && effectiveOption.state !== 'ready') {
switch (effectiveOption.state) {
case 'locked':
suffixes.push('locked');
break;
case 'disabled':
suffixes.push('disabled');
break;
case 'authentication-required':
suffixes.push('auth required');
break;
case 'runtime-missing':
suffixes.push('runtime missing');
break;
case 'degraded':
suffixes.push('degraded');
break;
default:
break;
}
}
return suffixes.length > 0 ? `${baseSummary} - ${suffixes.join(', ')}` : baseSummary;
}
export function updateProviderCheck(

View file

@ -45,6 +45,14 @@ export interface CliProviderBackendOption {
selectable: boolean;
recommended: boolean;
available: boolean;
state?:
| 'ready'
| 'locked'
| 'disabled'
| 'authentication-required'
| 'runtime-missing'
| 'degraded';
audience?: 'general' | 'internal';
statusMessage?: string | null;
detailMessage?: string | null;
}

View file

@ -346,6 +346,8 @@ describe('ClaudeMultimodelBridgeService', () => {
selectable: false,
recommended: false,
available: true,
state: 'locked',
audience: 'internal',
statusMessage: 'Experimental native lane',
detailMessage: 'Phase 0 keeps the lane locked behind rollout policy.',
},
@ -422,6 +424,8 @@ describe('ClaudeMultimodelBridgeService', () => {
id: 'codex-native',
selectable: false,
available: true,
state: 'locked',
audience: 'internal',
statusMessage: 'Experimental native lane',
}),
],
@ -439,4 +443,67 @@ describe('ClaudeMultimodelBridgeService', () => {
expect(getProviderConnectionModeSummary(codex!)).toBeNull();
expect(getProviderCurrentRuntimeSummary(codex!)).toBeNull();
});
it('preserves codex-native internal unlock readiness from runtime status payloads', async () => {
execCliMock.mockResolvedValue({
stdout: JSON.stringify({
providers: {
codex: {
supported: true,
authenticated: true,
authMethod: 'api_key',
verificationState: 'verified',
canLoginFromUi: false,
selectedBackendId: 'codex-native',
resolvedBackendId: 'codex-native',
availableBackends: [
{
id: 'codex-native',
label: 'Codex native',
selectable: true,
recommended: false,
available: true,
state: 'ready',
audience: 'internal',
statusMessage: 'Ready for internal use',
detailMessage: 'Internal rollout only.',
},
],
capabilities: {
teamLaunch: true,
oneShot: true,
extensions: {
plugins: { status: 'unsupported', ownership: 'shared', reason: 'Phase 1' },
mcp: { status: 'unsupported', ownership: 'shared', reason: 'Phase 1' },
skills: { status: 'unsupported', ownership: 'shared', reason: 'Phase 1' },
apiKeys: { status: 'supported', ownership: 'shared', reason: null },
},
},
backend: {
kind: 'codex-native',
label: 'Codex native',
authMethodDetail: 'api_key',
},
},
},
}),
stderr: '',
exitCode: 0,
});
const { ClaudeMultimodelBridgeService } =
await import('@main/services/runtime/ClaudeMultimodelBridgeService');
const service = new ClaudeMultimodelBridgeService();
const codex = await service.getProviderStatus('/mock/agent_teams_orchestrator', 'codex');
expect(codex.availableBackends?.find((backend) => backend.id === 'codex-native')).toMatchObject({
id: 'codex-native',
selectable: true,
available: true,
state: 'ready',
audience: 'internal',
statusMessage: 'Ready for internal use',
});
});
});

View file

@ -0,0 +1,109 @@
import { describe, expect, it } from 'vitest';
import {
getProviderRuntimeBackendAudienceLabel,
getProviderRuntimeBackendStateLabel,
getProviderRuntimeBackendSummary,
} from '@renderer/components/runtime/ProviderRuntimeBackendSelector';
import { createDefaultCliExtensionCapabilities } from '@shared/utils/providerExtensionCapabilities';
import type { CliProviderStatus } from '@shared/types';
function createCodexProvider(
overrides?: Partial<CliProviderStatus>
): CliProviderStatus {
return {
providerId: 'codex',
displayName: 'Codex',
supported: true,
authenticated: true,
authMethod: 'oauth_token',
verificationState: 'verified',
statusMessage: 'Connected',
models: ['gpt-5-codex'],
canLoginFromUi: true,
capabilities: {
teamLaunch: true,
oneShot: true,
extensions: createDefaultCliExtensionCapabilities(),
},
selectedBackendId: 'codex-native',
resolvedBackendId: 'codex-native',
availableBackends: [
{
id: 'auto',
label: 'Auto',
description: 'Automatically choose the best backend.',
selectable: true,
recommended: true,
available: true,
state: 'ready',
audience: 'general',
},
{
id: 'codex-native',
label: 'Codex native',
description: 'Use the local codex exec JSON seam.',
selectable: false,
recommended: false,
available: true,
state: 'locked',
audience: 'internal',
statusMessage: 'Ready but locked',
detailMessage: 'Internal rollout only.',
},
],
externalRuntimeDiagnostics: [],
backend: {
kind: 'codex-native',
label: 'Codex native',
},
connection: null,
...overrides,
};
}
describe('ProviderRuntimeBackendSelector helpers', () => {
it('exposes explicit internal-audience and locked-state labels', () => {
const provider = createCodexProvider();
const option = provider.availableBackends?.find((backend) => backend.id === 'codex-native');
expect(option).toBeDefined();
expect(getProviderRuntimeBackendAudienceLabel(option!)).toBe('Internal');
expect(getProviderRuntimeBackendStateLabel(option!)).toBe('Locked');
});
it('builds a runtime summary that keeps internal locked truth visible', () => {
const provider = createCodexProvider();
expect(getProviderRuntimeBackendSummary(provider)).toBe(
'Codex native - internal - locked'
);
});
it('shows auth-required state for degraded internal native rollout', () => {
const provider = createCodexProvider({
availableBackends: [
{
id: 'codex-native',
label: 'Codex native',
description: 'Use the local codex exec JSON seam.',
selectable: false,
recommended: false,
available: false,
state: 'authentication-required',
audience: 'internal',
statusMessage: 'Authentication required',
detailMessage: 'Set CODEX_API_KEY.',
},
],
});
const option = provider.availableBackends?.[0];
expect(getProviderRuntimeBackendAudienceLabel(option!)).toBe('Internal');
expect(getProviderRuntimeBackendStateLabel(option!)).toBe('Auth required');
expect(getProviderRuntimeBackendSummary(provider)).toBe(
'Codex native - internal - auth required'
);
});
});

View file

@ -4,6 +4,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest';
import {
getPrimaryProvisioningFailureDetail,
getProvisioningProviderBackendSummary,
ProvisioningProviderStatusList,
createInitialProviderChecks,
} from '@renderer/components/team/dialogs/ProvisioningProviderStatusList';
@ -160,4 +161,30 @@ describe('ProvisioningProviderStatusList', () => {
await Promise.resolve();
});
});
it('keeps internal native rollout state visible in provisioning backend summaries', () => {
expect(
getProvisioningProviderBackendSummary({
selectedBackendId: 'codex-native',
resolvedBackendId: 'codex-native',
backend: {
kind: 'codex-native',
label: 'Codex native',
},
availableBackends: [
{
id: 'codex-native',
label: 'Codex native',
description: 'Use codex exec JSON mode.',
selectable: false,
recommended: false,
available: true,
state: 'locked',
audience: 'internal',
statusMessage: 'Ready but locked',
},
],
})
).toBe('Codex native - internal, locked');
});
});