feat(runtime): surface codex-native internal rollout states
This commit is contained in:
parent
ba37c1caf5
commit
30fce3c64d
7 changed files with 346 additions and 17 deletions
|
|
@ -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,
|
||||
})) ?? [],
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue